npm.io
0.62.0 • Published 1h ago

@real-router/core

Licence
MIT
Version
0.62.0
Deps
3
Size
1.3 MB
Vulns
0
Weekly
3.5K
Stars
6

@real-router/core

Mutation Score npm npm downloads bundle size License: MIT

Simple, powerful, view-agnostic, modular and extensible router for JavaScript applications.

This is the core package of the Real-Router monorepo. It provides the router implementation, lifecycle management, navigation pipeline, and tree-shakeable standalone API modules.

Installation

npm install @real-router/core

Quick Start

import { createRouter } from "@real-router/core";
import { browserPluginFactory } from "@real-router/browser-plugin";

const routes = [
  { name: "home", path: "/" },
  {
    name: "users",
    path: "/users",
    children: [{ name: "profile", path: "/:id" }],
  },
];

const router = createRouter(routes);
router.usePlugin(browserPluginFactory());

await router.start("/");
await router.navigate("users.profile", { id: "123" });

Router API

Lifecycle
Method Returns Description
start(path) Promise<State> Start the router with an initial path
stop() this Stop the router, cancel in-progress transition
dispose() void Permanently terminate (cannot restart)
isActive() boolean Whether the router is started
Navigation
Method Returns Description
navigate(name, params?, options?) Promise<State> Navigate to a route. Fire-and-forget safe
navigateToDefault(options?) Promise<State> Navigate to the default route. Fire-and-forget safe
navigateToNotFound(path?) State Synchronously set UNKNOWN_ROUTE state
canNavigateTo(name, params?) boolean Check if guards allow navigation
await router.navigate("users.profile", { id: "123" });
await router.navigate("dashboard", {}, { replace: true });

// Cancellable navigation
const controller = new AbortController();
router.navigate("users", {}, { signal: controller.signal });
controller.abort();
State
Method Returns Description
getState() State | undefined Current router state (deeply frozen)
getPreviousState() State | undefined Previous router state
areStatesEqual(s1, s2, ignoreQP?) boolean Compare two states
isActiveRoute(name, params?, strict?, ignoreQP?) boolean Check if route is active
buildPath(name, params?) string Build URL path from route name
isLeaveApproved() boolean True when deactivation guards pass
Events & Plugins
Method Returns Description
subscribe(listener) Unsubscribe Listen to successful transitions
subscribeLeave(listener) Unsubscribe Subscribe to approved route departures — tentative, not committed: an activation guard can still reject. Listener may return Promise<void> to block the pipeline (exit animations). Receives an AbortSignal that aborts (with the failure reason) if the navigation does not commit
usePlugin(...plugins) Unsubscribe Register plugin factories
const unsub = router.subscribe(({ route, previousRoute }) => {
  console.log(previousRoute?.name, "->", route.name);
});

// Save scroll position when leaving a route (fires when departure is approved —
// tentative: an activation guard can still keep you on the current route)
const unsubLeave = router.subscribeLeave(({ route }) => {
  if (route.name === "products") {
    sessionStorage.setItem("products:scroll", String(window.scrollY));
  }
});

// Async leave: exit animation blocks navigation until complete
router.subscribeLeave(async ({ signal }) => {
  await animateOut(document.querySelector(".page"), { signal });
});

Standalone API

Tree-shakeable functions imported from @real-router/core/api. Only imported functions are bundled.

import {
  getRoutesApi,
  getDependenciesApi,
  getLifecycleApi,
  getPluginApi,
  cloneRouter,
} from "@real-router/core/api";
Function Purpose Key methods
getRoutesApi(router) Dynamic route CRUD add, remove, update, replace, has, get
getDependenciesApi(router) Dependency injection get, set, setAll, remove, has
getLifecycleApi(router) Guard registration addActivateGuard, addDeactivateGuard, remove*
getPluginApi(router) Plugin infrastructure makeState, matchPath, addInterceptor, extendRouter, emitTransitionError, getRouteConfig
cloneRouter(router, deps?) SSR cloning Shares route definitions, independent state

Utilities

SSR/SSG/hydration helpers imported from @real-router/core/utils.

import {
  serializeRouterState,
  hydrateRouter,
  getStaticPaths,
  serializeState,
} from "@real-router/core/utils";

// SSR: serialize the full resolved State (incl. plugin context namespaces)
const state = await router.start(req.url);
const html = `<script>window.__SSR_STATE__=${serializeRouterState(state)}</script>`;

// Client: hydrate from server payload — runs router.start(state.path) under
// a one-shot scratchpad so SSR plugins (#596) can skip the loader re-run
await hydrateRouter(router, window.__SSR_STATE__);

// SSG: enumerate all URLs for pre-rendering
const paths = await getStaticPaths(router, {
  "users.profile": async () => [{ id: "1" }, { id: "2" }],
});
// → ["/", "/users", "/users/1", "/users/2"]
Function Purpose
serializeRouterState(state, { excludeContext? }) XSS-safe JSON serialization of a router State for SSR → client transport. Strips transition; keeps name, params, path, context. Pass excludeContext to drop non-JSON-safe namespaces (e.g. rsc)
hydrateRouter(router, source) Hydrate a fresh router from the server-serialized payload. Deposits the parsed state onto a one-shot scratchpad consumed by SSR loader plugins (#596) so the first start() reuses server-resolved namespace values instead of re-running loaders
serializeState(data) XSS-safe JSON serialization for embedding arbitrary data in HTML <script> tags (lower-level than serializeRouterState)
getStaticPaths(router, entries?) Enumerate leaf routes and build URLs for SSG pre-rendering
SerializedRouterState (type) Parsed shape produced by serializeRouterState after JSON.parseOmit<State, "transition">
StaticPathEntries (type) Type for the entries parameter: Record<string, () => Promise<Record<string, string>[]>>
getNavigator(router) (main entry)

Frozen read-only subset of router methods for view layers. Pre-bound, safe to destructure. Imported from @real-router/core, not /api.

import { getNavigator } from "@real-router/core";
// Dynamic route management
const routes = getRoutesApi(router);
routes.add({ name: "settings", path: "/settings" });
routes.replace(newRoutes); // atomic HMR-safe replacement

// Dependency injection for guards and plugins
const deps = getDependenciesApi(router);
deps.set("authService", authService);

// Global lifecycle guards
const lifecycle = getLifecycleApi(router);
lifecycle.addActivateGuard("admin", (router, getDep) => (toState) => {
  return getDep("authService").isAuthenticated();
});

// SSR — clone with request-scoped deps
const requestRouter = cloneRouter(router, { store: requestStore });
await requestRouter.start(req.url);

Route Configuration

import type { Route } from "@real-router/core";

const routes: Route[] = [
  {
    name: "admin",
    path: "/admin",
    canActivate: (router, getDep) => (toState, fromState, signal) => {
      return getDep("authService").isAdmin();
    },
    children: [
      {
        name: "dashboard",
        path: "/dashboard",
        defaultParams: { tab: "overview" },
      },
    ],
  },
  {
    name: "legacy",
    path: "/old-path",
    forwardTo: "home", // URL alias — guards on source are NOT executed
  },
  {
    name: "product",
    path: "/product/:id",
    encodeParams: ({ id }) => ({ id: String(id) }),
    decodeParams: ({ id }) => ({ id: Number(id) }),
  },
];

Params Contract

router.navigate(name, params) and router.buildPath(name, params) follow a stable contract for how each value type is serialized into the URL and preserved in state.params:

Input — params object values
Value URL path param (:id) URL query param (?q) state.params after navigation
undefined Error (required) / skip (optional :id?) stripped — parameter absent from URL Key absent ("q" in params is false)
null Same as undefined ?q (key-only, via nullFormat: "default") null
"" (empty string) Empty segment (caller's responsibility) ?q= (explicit empty value, distinct from null) ""
string Encoded per urlParamsEncoding ?q=value (URI-encoded) Unchanged
number /users/42 ?q=42 42 (number, via numberFormat: "auto")
boolean /users/true ?q=true / ?q=false (via booleanFormat: "auto") true / false
0, false (falsy-defined) Coerced to string Preserved (not stripped) Preserved

undefined is stripped at the core boundary. This is an explicit public contract, not an implementation detail. Plugins that add undefined values via addInterceptor("forwardState") also have them scrubbed before URL and state.

Output — parsing query strings back (match())
URL fragment booleanFormat: "auto" (default) booleanFormat: "empty-true" booleanFormat: "none"
?flag null true null
?flag= "" "" ""
?flag=x "x" "x" "x"
?flag=true true (coerced) "true" "true"
?flag=false false (coerced) "false" "false"

?flag and ?flag= are distinct: three-state expressiveness (absent / explicit empty / has value). Matches search-params engine semantics.

Example
router.navigate("search", {
  q: "hello",
  page: undefined, // stripped
  sort: null, // becomes ?sort (key-only)
  filter: "", // becomes ?filter= (explicit empty)
  active: true, // becomes ?active=true
});
// URL: /search?q=hello&sort&filter=&active=true
//
// state.params:
//   { q: "hello", sort: null, filter: "", active: true }
//   ("page" key is absent)
Configuration

Query string behavior is configurable via queryParams option on createRouter:

const router = createRouter(routes, {
  queryParams: {
    booleanFormat: "empty-true", // `true` → ?flag, `false` → ?flag=false
    nullFormat: "hidden", // `null` → stripped (vs `default`: ?key)
    numberFormat: "none", // `"42"` stays string after parse
    arrayFormat: "brackets", // `[1,2]` → ?x[]=1&x[]=2
  },
});

See @real-router/search-schema-plugin for schema-driven parsing with Zod/Valibot/ArkType — handles booleanFormat interaction and explicit type coercion.

Error Handling

Navigation errors are instances of RouterError with typed error codes:

import { RouterError, errorCodes } from "@real-router/core";

try {
  await router.navigate("admin");
} catch (err) {
  if (err instanceof RouterError) {
    // err.code: ROUTE_NOT_FOUND | CANNOT_ACTIVATE | CANNOT_DEACTIVATE
    //           | TRANSITION_CANCELLED | SAME_STATES | DISPOSED | ...
  }
}

See RouterError and Error Codes for the full reference.

Validation

Runtime argument validation is available via @real-router/validation-plugin:

import { validationPlugin } from "@real-router/validation-plugin";

router.usePlugin(validationPlugin()); // register before start()
await router.start("/");

The plugin adds descriptive error messages for every public API call. Register it in development, skip in production.

Documentation

Full documentation: Wiki

Package Description
@real-router/react React integration (RouterProvider, hooks, Link, RouteView)
@real-router/browser-plugin Browser History API and URL synchronization
@real-router/hash-plugin Hash-based routing
@real-router/rx Observable API (state$, events$, TC39 Observable)
@real-router/logger-plugin Development logging
@real-router/persistent-params-plugin Parameter persistence
@real-router/route-utils Route tree queries and segment testing

Contributing

See contributing guidelines for development setup and PR process.

License

MIT Oleg Ivanov

Keywords