npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@real-router/core

v0.100.1

Published

A simple, powerful, view-agnostic, modular and extensible router

Readme

@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?, search?, 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. Asks the current route's canDeactivate first — throws CANNOT_DEACTIVATE if it refuses | | canNavigateTo(name, params?, search?) | boolean | Check if guards allow navigation |

await router.navigate("users.profile", { id: "123" });
// RFC-4 M2 (#1548): the 3rd arg is the query/search channel; options move to the 4th
await router.navigate("dashboard", {}, undefined, { replace: true });
await router.navigate("products", {}, { page: 2, sort: "name" }); // query via the search channel
// Descriptor form — navigate(target, options?), target = { name, params?, search? }:
await router.navigate(
  { name: "products", search: { page: 2 } },
  { replace: true },
);

// Cancellable navigation
const controller = new AbortController();
router.navigate("users", {}, undefined, { 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?, search?, strict?, ignoreQP?) | boolean | Check if route is active | | buildPath(name, params?, search?) | 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 are published separately as @real-router/ssr-utils (extracted from the SSR-era @real-router/core/utils subpath — that specifier is live again and now holds something else, see Ingestion primitives below).

import {
  serializeRouterState,
  hydrateRouter,
  getStaticPaths,
  serializeState,
} from "@real-router/ssr-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>[]>> |

Ingestion primitives (@real-router/core/utils)

For plugin authors. A plugin that copies a caller's params / search into a record of its own is writing under a key it did not choose, and record[key] = value consults the DESTINATION's prototype chain before storing. So an application that puts anything on Object.prototype under a name it routes by — id, tab, page, lang, as an ordinary library extension does — intercepts that write:

import { putField, copyFields } from "@real-router/core/utils";

// instead of `mine[key] = value`
putField(mine, key, value);

// instead of `Object.assign(mine, bag)` — that is the same `[[Set]]` per key
copyFields(mine, bag);

| Function | Purpose | | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | putField(target, key, value) | Store value under key as ordinary own DATA, whatever the destination's prototype chain says about that name. Falls back to Object.defineProperty only where it answers | | copyFields(target, source) | Object.assign's replacement: every own enumerable key of source, each through putField |

Measured on the shipped plugins before the fix, with a getter under an ordinary param name: persistent-params dropped the key from the URL with no error at all, and search-schema threw out of a navigation. A getter-only accessor throws, a getter+setter pair silently diverts the value into application code, and a non-writable data property drops it — the middle shape is why a throw-shaped test does not cover this.

Not a general utility belt, and it deliberately will not grow into one: core obeys the same rule at every write of its own, and this publishes the rule rather than a toolkit. Reaching for Object.create(null) instead is the expensive horn — V8 keeps such an object in dictionary mode, so the price lands on every later READ.

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",
    // Codecs are two-channel: receive `{ params, search }`, return `{ params,
    // search }` — transform the channel you own, pass the other through.
    encodeParams: ({ params: { id }, search }) => ({
      params: { id: String(id) },
      search,
    }),
    decodeParams: ({ params: { id }, search }) => ({
      params: { id: Number(id) },
      search,
    }),
  },
];

Params Contract

router.navigate(name, params, search?) and router.buildPath(name, params, search?) follow a stable contract for how each value type is serialized into the URL and stored back — path params land in state.params, query params in state.search (two channels since RFC-4 M2 / #1548). The value-coercion rules below are identical for both channels; only the storage location differs:

Input — params object values

| Value | URL path param (:id) | URL query param (?q) | Stored value (state.params path · state.search query) | | ---------------------------- | --------------------------------------- | ---------------------------------------------------- | --------------------------------------------------------- | | undefined | Error — path param is required | 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

// Query params go through the search channel (3rd arg); path params (none here) use the 2nd.
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.search:
//   { q: "hello", sort: null, filter: "", active: true }
//   ("page" key is absent) — state.params is {} (this route has no path params)

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
    //           | 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

Related Packages

| 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 | | @real-router/ssr-utils | Router-level SSR/SSG/hydration helpers (serialize, hydrate, static paths) |

Contributing

See contributing guidelines for development setup and PR process.

License

MIT © Oleg Ivanov