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

@stealthscale/provider-router

v0.1.0

Published

Publishes the routes an application navigates by, and the options every router starts from.

Readme

@stealthscale/provider-router

@stealthscale/provider-router gives you the pieces an application builds a router from. You call TanStack Router's own createRouter, so all forty-eight of its options remain yours to set.

Three things come from here. routerOptions states what every router in this design system starts from. compileRoutes turns route declarations a host read out of a manifest into routes you place in your own tree. namedRoute, routeMap, routeHref and RouteLink let anything link to anything by id.

Use this package when an address is decided at run time. An application whose routes are all in the build should use TanStack Router directly, where the compiler checks every path. This package exists for the pages that are not in the build: ones another deployment declares, ones a condition decides, ones a host mounts under a path it chooses when it starts.

Link by id, not by path. A path belongs to whoever composed the application, so a page that might be composed differently tomorrow has no path it can write down. An id does not move. That holds for a host's own routes as much as for a plugin's, which is why namedRoute gives a route written in code the same kind of id the compiler gives a declared one.

Install

pnpm add @stealthscale/provider-router

The package peers on @tanstack/react-router and react. It re-exports the library, so you import the router from here rather than from two places.

A host that draws routes a manifest declared

import {
  compileRoutes,
  createAppRootRoute,
  createRoute,
  createRouter,
  namedRoute,
  routeMap,
  routerOptions,
} from "@stealthscale/provider-router";

function buildTree(declarations) {
  const root = createAppRootRoute()();
  const shell = createRoute({ getParentRoute: () => root, path: "/app" });
  const home = createRoute({
    ...namedRoute("app.home"),
    getParentRoute: () => shell,
    path: "/home",
  });

  const plugins = compileRoutes(declarations, { evaluate, layouts, parent: shell });

  return root.addChildren([shell.addChildren([home, ...plugins])]);
}

const tree = buildTree(await loadManifests());
const router = createRouter({ ...routerOptions({ routes: routeMap(tree) }), routeTree: tree });

compileRoutes creates routes and never touches the parent you pass it. Build the tree as a function of the declarations and call it once per declaration set. Compile every contributor's declarations in one call, because two calls under one parent cannot read each other's paths.

Use createAppRootRoute rather than createRootRoute. It types the router context so the route map fits, and the alternative reports the mismatch at createRouter rather than at the root.

Do not graft into a tree a router has already been built from. The library caches a processed tree on a server, keyed by the tree object and written once per process, so a second router built from a mutated tree reads the first one's routes. That failure appears in production and not in development.

Naming routes

routeMap walks the assembled tree and reads the id off every route that carries one. The compiler writes that id for a declared route. namedRoute writes it for one you wrote yourself. Both kinds enter the map the same way, so you pass one value rather than a pair.

const settings = createRoute({
  ...namedRoute("app.settings"),
  getParentRoute: () => shell,
  path: "/settings",
});

The walk refuses two routes carrying one id. Two routes under one id would send a link somewhere different without the link itself changing.

A path only one route may serve

The same walk refuses two routes serving one path under a shared parent. It counts the routes you placed yourself as well as the ones it compiled. A pathless layout consumes no path segment, so /app/_yours/settings and /app/settings are one URL however many layouts stand between.

A development build of the library reports that pair as a duplicate route. A production build keeps the first and drops the rest without reporting it. The refusal is here as well so that both builds fail the same way.

Linking by id

Pass the reference the plugin SDK returned, not a string. A reference carries the id and, in its type alone, the parameters the route's path names.

import { RouteLink, useRouteHref } from "@stealthscale/provider-router";

<RouteLink activeProps={{ className: "current" }} params={{ invoice }} to={invoices.one}>
  Open
</RouteLink>;

const href = useRouteHref(invoices.one, { invoice });

Filling the wrong parameter is a compile error, because the reference's type says which ones the path names. A bare id string works too, and gives up that check.

<RouteLink params={{ id: "42" }} to={invoices.one} />
// 'id' does not exist in type '{ invoice: string }'

RouteRef is declared structurally, so a plugin SDK's own reference type satisfies it without this package depending on that SDK. Anything carrying an id fits.

The same resolution serves navigate and a redirect thrown in a loader, because both take the path routeHref returns. A hook cannot run inside an event handler, so take the map and resolve there.

const map = useRouteMap();
const navigate = useNavigate();

const open = (row: Row) => navigate({ to: routeHref(map, invoices.one, { invoice: row.id }) });

A resolved path is relative to the route tree, and the library adds your basepath when it builds the link. A router at basepath: "/admin" draws /admin/app/invoices for a route compiled at /app/invoices.

Both read the map through the router's own context, so nothing extra is mounted. Both throw rather than resolve a path that is wrong: an unknown id, a missing parameter, and a tree no router has processed each fail where you wrote them.

The active state needs no configuration

RouteLink takes everything the library's own Link takes, so an active link is marked without you configuring anything. On the page it names, the anchor carries data-status="active", aria-current="page" and a class of active.

a[data-status="active"] {
  font-weight: bold;
}

Matching is by prefix on a segment boundary. A link to /app/invoices stays marked on /app/invoices/42, which is what a menu wants, and a link to / is not marked on /app/invoices. Pass activeOptions={{ exact: true }} to mark only the exact page, and activeProps={{}} to drop the active class the library adds by default.

Reading the route a person is on

A named route carries its id in the library's own staticData, so a menu, a breadcrumb or a telemetry hook reads it off the match rather than holding a second copy of the declaration list.

import { declaredOf, useDeclaredRoute, useMatches } from "@stealthscale/provider-router";

const here = useDeclaredRoute();
const trail = useMatches().map((match) => declaredOf(match));

useDeclaredRoute returns the deepest named route. That is the page a person is looking at, and it returns nothing on a page drawn entirely from routes nobody named. Both functions check the shape rather than trust it. staticData is untyped by design, and a route may carry anything under the same name.

Each returns a reference to the object the route carries rather than a copy. A navigation that left the page alone therefore re-renders nothing that reads them.

Reading a page's own parameters

A compiled route is outside the tree the application registered. The library cannot type its parameters, and useParams returns a loose record. Pass the reference, which carries the types.

import { useRouteParams } from "@stealthscale/provider-router";

export function Invoice() {
  const { invoice } = useRouteParams(invoices.one);
}

It checks at run time that the page is the route the reference names before it makes the claim, so a reference copied from another page throws rather than mistyping what it returns.

Search parameters have no equivalent. A reference carries no search type, so read them with useSearch({ strict: false }) and validate at the edge.

A link to a route nobody may reach

A condition decides whether a route is routed, not whether it is named. A route whose when fails is still in the tree and still in the map, so a link to it resolves and then returns a 404.

A menu drawn from declarations has to filter them with the same evaluator the compiler was given. The foundation does not do it for you, because which entries a person should see is a question about your product rather than about routing.

const shown = declarations.filter((one) => one.when === undefined || evaluate(one.when));

Inside a declaration

RouteDeclaration is the compiled form a plugin SDK produces, not the manifest form. A page is either a component or an importer with the export it is published under, because a React component is a function and nothing at run time separates one from an importer.

{ component: { export: "Invoices", load: () => import("./invoices.js") } }

The export name is optional, and the module's default export is used without one. Both go straight to the library's lazyRouteComponent. The page's chunk loads on the first navigation to it, and a chunk the deployment has replaced is reported rather than ignored.

layout names layouts outermost first. Two declarations naming the same layouts with the same options share one pathless parent. Naming a layout a route above already draws is refused, because the frame would otherwise be drawn twice.

A condition is whatever language your host writes one in, and the evaluate you pass reads it. A condition that fails makes the route a 404, so a route nobody may reach resolves to nothing. An evaluator wanting anything else, such as sending an unauthenticated person to sign in, throws the library's redirect itself.

compileRoutes refuses a declaration stating outlet. A screen maps to a route and the route decides the whole screen, so a page drawn beside another as a pane has no route of its own.

Draw a detail beside a list by declaring it as a child, which is what an outlet is for. The parent lays out its own content beside <Outlet />, and the pane gets a real URL, the back button and preloading with it.

{ component: List, id: "acme.list", path: "/invoices" }
{ component: Detail, id: "acme.detail", parent: "acme.list", path: "$invoice" }

Two <Outlet /> in one component draw the same child twice. The library matches one route per level and Outlet takes no name, so there is no second pane to fill.

Testing

@stealthscale/testing-router mounts a tree and renders the page a path matches, so a specification reads a screen rather than driving a router.

const { result } = await mountRoute(buildTree(await declarations()), "/app/invoices/42");

Registering the type

The library derives paths, params and search from one declared router type. An application whose routes are all in the build declares it and gets a checked Link.

Put it in a declaration file beside the tree. The statement is type-only and erases to nothing.

// src/register.d.ts
import { type Routed } from "#routes.ts";

declare module "@tanstack/react-router" {
  interface Register {
    router: Routed;
  }
}

The import keeps the file a module. declare module with no import and no export is an ambient declaration, which replaces the library's types rather than adding to them.

That augmentation names @tanstack/react-router, so an application importing everything else from this package still names the library in this one file. A route this package compiled is not in the registered tree, which is why RouteLink and useRouteParams exist.