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

cross-router-core

v1.0.4

Published

Agnostic router engine for cross-router.

Readme

cross-router-core

The framework-agnostic engine behind cross-router. Contains the router, history, matcher, middleware chain, loaders, actions, and runtime route registry. Has no dependency on any UI framework.

Framework adapters (cross-router-svelte, etc.) import exclusively from this package. Plugin authors who only need to define routes and patch the route tree can also import directly from here.


Installation

npm i cross-router-core
# or with pnpm
pnpm add cross-router-core

Creating a router

In practice you will use a factory from a framework adapter (e.g. createBrowserRouter from cross-router-svelte) rather than calling createRouter directly. The adapter factory handles history creation and wires the router into the framework's reactivity model.

If you are building a new adapter, use createRouter directly:

import { createRouter, createBrowserHistory } from 'cross-router-core'

const router = createRouter(createBrowserHistory(), {
  routes: [...],
  middleware: [...],
})

await router.initialize()

Route definition

Routes are plain objects. The RouteDefinition type is fully generic — params, search, loader data, and middleware context are all inferred from the definition.

import type { RouteDefinition } from 'cross-router-core'

const routes: RouteDefinition[] = [
  {
    id: 'root',
    path: '/',
    children: [
      {
        id: 'home',
        index: true, // matches the parent path exactly
        loader: async () => {
          return { message: 'Hello' }
        },
      },
      {
        id: 'user',
        path: 'users/:id', // :id is extracted and typed in loader/action
        loader: async ({ params }) => {
          return fetchUser(params.id)
        },
      },
      {
        id: 'not-found',
        path: '*', // catch-all — always matched last
      },
    ],
  },
]

Route fields

| Field | Type | Description | |---|---|---| | id | string | Required. Must be unique across the entire tree. | | path | string | URL segment(s) this route matches. Omit for pathless layout routes. | | index | true | Matches the parent path exactly. Cannot be combined with path. | | middleware | Middleware[] | Route-level middleware. Runs after global middleware, before the loader. | | search | valibot schema | Validates and types the URL search params for this route. | | loader | function | Runs before the route renders. Return value becomes loaderData. | | action | function | Handles form submissions (POST). | | shouldRevalidate | function | Return false to skip re-running the loader after a navigation or action. | | component | unknown | The component to render. Type is narrowed by the framework adapter. | | errorComponent | unknown | Rendered when this route or any descendant throws. | | lazy | function | Returns a promise resolving loader, action, component, or errorComponent. Used for code splitting. | | children | RouteDefinition[] | Nested routes. |


Middleware

Middleware runs before every navigation, in order: global middleware first, then each matched route's own middleware from outermost to innermost.

Each middleware receives the accumulated context from the previous middleware and passes an extended context to the next one. This is the primary way to propagate things like authenticated user objects, feature flags, or API clients down to loaders.

import { createContext, defineMiddleware, redirect } from 'cross-router-core'

const USER_CONTEXT = createContext<User>()

const authMiddleware = defineMiddleware(async ({ context, request }, next) => {
  const user = await getSession()
  if (!user)
    throw redirect('/login')
  // Pass the user into the context — loaders will receive it typed
  context.set(USER_CONTEXT, user)
  await next()
})

Pass global middleware to createRouter:

createRouter(history, {
  middleware: [authMiddleware],
  routes: [...],
})

Or attach middleware to a specific route:

{
  id: 'admin',
  path: 'admin',
  middleware: [requireAdminMiddleware],
  loader: async ({ context }) => {
    const user = context.get(USER_CONTEXT) // `user` is properly typed here
  },
}

defineMiddleware

Helper for authoring middleware with full type inference. Without it you would need to annotate TIn and TOut manually.

import { createContext, defineMiddleware } from 'cross-router-core'

const ORG_CONTEXT = createContext<Organization>()

const myMiddleware = defineMiddleware<
  { user: User }, // TIn  — what this middleware expects
  { user: User, org: Org }
>(async ({ context }, next) => {
  const org = await fetchOrg(context.user.orgId)
  context.set(ORG_CONTEXT, org)
  await next()
})

Redirect

Throw redirect() inside any middleware, loader, or action to short-circuit navigation and send the user elsewhere.

import { redirect } from 'cross-router-core'

// In a middleware:
if (!user)
  throw redirect('/login')

// With a specific HTTP status:
throw redirect('/new-path', 301)

Supported status codes: 301, 302 (default), 307, 308.


Search params

Pass a valibot schema as the search field to validate and type URL search params for a route. The inferred type flows into the loader automatically.

import type { RouteDefinition } from 'cross-router-core'
import { coerce, number, object, optional, string } from 'valibot'

const searchSchema = object({
  q: optional(string(), ''),
  page: optional(coerce(number(), Number), 1),
})

const route: RouteDefinition<'/search', [], typeof searchSchema> = {
  id: 'search',
  path: 'search',
  search: searchSchema,
  loader: async ({ search }) => {
    // search.q and search.page are fully typed
    return fetchResults(search.q, search.page)
  },
}

Lazy routes

Use lazy to defer loading a route's component and/or loader until first navigation. This enables automatic code splitting.

{
  id: 'settings',
  path: 'settings',
  lazy: () => import('./pages/SettingsPage').then(m => ({
    component: m.default,
    loader: m.loader,
  })),
}

The returned object can include any combination of loader, action, component, and errorComponent. Fields defined statically on the route take precedence over lazy-resolved ones.


Runtime route patching

Routes can be added and removed from a live router instance at any time. This is useful for plugin architectures where features are loaded on demand.

// Add routes under an existing parent
router.patch([
  {
    id: 'plugin-settings',
    path: 'plugin/settings',
    component: PluginSettingsPage,
  }
], 'root') // 'root' is the id of the parent route

// Remove a route subtree by id
router.unpatch('plugin-settings')

Route specificity is resolved automatically — a newly patched route with a specific path will always be tried before a catch-all * route, regardless of registration order.


RouterInstance API

The object returned by createRouter.

| Method | Description | |---|---| | state | Current NavigationState. | | subscribe(fn) | Subscribe to state changes. Returns an unsubscribe function. | | initialize() | Must be called once before first render. Performs the initial navigation. | | navigate(to, options?) | Navigate to a path, adding a history entry. | | replace(to, options?) | Navigate without adding a history entry. | | back() | Go back one entry in the history stack. | | forward() | Go forward one entry in the history stack. | | submit(formData, options) | Submit a form to a route action. | | patch(routes, parentId?) | Add routes at runtime. | | unpatch(routeId) | Remove a route subtree at runtime. | | destroy() | Clean up history listeners. Call when tearing down the app. |

NavigationState

interface NavigationState {
  location: Location // current URL broken into parts
  status: NavigationStatus // 'idle' | 'loading' | 'submitting' | 'redirecting' | 'error'
  matches: RouteMatch[] // matched routes from root to leaf
  loaderData: Record<string, unknown> // keyed by route id
  actionData: unknown // result of the last action
  error: unknown // set when status === 'error'
  isTransitioning: boolean
  viewTransition: boolean
}

Writing a framework adapter

An adapter needs to:

  1. Create a router via createRouter with the appropriate history
  2. Call router.initialize() before first render
  3. Subscribe to router.subscribe() and pipe state into the framework's reactivity model
  4. Render the matched route tree by reading state.matches
  5. Expose hooks (useLoaderData, useParams, etc.) that read from the current state
  6. Export a routes() helper that tags route definitions with a RouteRenderer for cross-framework rendering

See cross-router-svelte as the reference implementation.


License

This project is under MIT license.