@wrnexus/router
Filesystem discovery, route matching, and typed route generation.
Install the package
After WorkRoot approves private registry access, install the release-aligned package:
bun add @wrnexus/router@0.8.7Request preview access. Never put registry tokens in source control.
File-based router that maps an app/ directory onto route tables and matches request paths against them.
Part of the WRNexusJS framework — an SSR-first, Bun-native full-stack web framework.
Overview
@wrnexus/router scans an application's app/ directory once at startup and builds route tables for pages, API endpoints, realtime channels, middleware, server-rendered .wrn components, layouts, and validation schemas. It also compiles URL patterns (/users/[id]) into RegExps and matches request paths against them. Request input is never turned into a file path, which makes the router immune to path traversal. This is a server-side package used by the WRNexusJS runtime to resolve incoming requests, plus a codegen helper for compile-time typed links.
bun add @wrnexus/router
Private package — the machine must be authenticated to the wrnexus npm org
(a read token in ~/.npmrc). Requires Bun (Node is not supported).
Directory conventions
The router maps files under appDir onto routes:
app/pages/index.tsx -> GET /
app/pages/about.tsx -> GET /about
app/pages/users/[id].tsx -> GET /users/:id
app/api/hello.ts -> /api/hello
app/realtime/chat.ts -> /realtime/chat
app/pages/*.wrn (api) -> embedded /api/* routes
app/pages/*.wrn (rt) -> embedded /realtime/* routes
app/middleware/*.ts -> global middleware (alphabetical)
app/components/*.wrn -> server-rendered components (by basename)
app/layouts/*.wrn -> named page layouts
app/schemas/*.ts -> validation schemas
Allowed route extensions are .ts, .tsx, and .wrn. Dotfiles and underscore-prefixed files are ignored. A trailing index segment is dropped from the route. .wrn pages may embed api and realtime blocks, which the router extracts and mounts under /api/* and /realtime/*.
API
buildRouter(appDir, opts?): Router
Scan an app directory and build all route tables.
function buildRouter(appDir: string, opts?: RouterOptions): Router;
interface RouterOptions {
/** Extra dirs scanned for `.wrn` components (e.g. `@wrnexus/ui`), before
* `app/components`, so an app component of the same name wins. */
componentDirs?: string[];
}
The returned Router exposes the built tables plus per-kind matchers:
interface Router {
pages: Route[];
api: Route[];
realtime: Route[];
/** Absolute paths of middleware modules, in execution order (alphabetical). */
middlewareFiles: string[];
/** Server-rendered `.wrn` components, mounted via `data-component`. */
components: ComponentRef[];
/** Named page layouts (`app/layouts/<name>.wrn`); a page picks one via `layout`. */
layouts: ComponentRef[];
/** Validation schemas (`app/schemas/<name>.ts`) shared by API + forms. */
schemas: ComponentRef[];
matchPage(pathname: string): RouteMatch | null;
matchApi(pathname: string): RouteMatch | null;
matchRealtime(pathname: string): RouteMatch | null;
}
interface ComponentRef {
/** Validated component name (matches a `data-component` attribute). */
name: string;
/** Absolute path to the component's `.wrn` module. */
file: string;
}
Component, layout, and schema names are validated with isSafeIslandName from @wrnexus/core; unsafe names are skipped with a warning. Realtime channel names are validated the same way.
Route matching
| Export | Signature | Description | |
|---|---|---|---|
compileRoutePattern | `(raw: string) => Pick<Route, "regex" \ | "paramNames">` | Compile a /users/[id] pattern into a RegExp (with optional trailing slash) plus ordered param names. |
matchRoute | `(routes: Route[], pathname: string) => RouteMatch \ | null` | Return the first route whose regex matches; captured params are decodeURIComponent-decoded. |
sortRoutes | (routes: Route[]) => Route[] | Order routes so static routes win over dynamic ones (fewer params first), then longer/more specific patterns first. |
interface Route {
raw: string; // e.g. "/users/[id]"
file: string; // absolute path to the handling module
regex: RegExp; // compiled matcher
paramNames: string[]; // ordered dynamic param names
}
interface RouteMatch {
route: Route;
params: Record<string, string>;
}
Typed-routes codegen
function generateRoutesFile(pages: Route[]): string;
Emits the source for app/routes.gen.ts: a Routes map (each page path → its [param] types), a RoutePath union, and an href() builder that fills params and rejects unknown paths at compile time. Entries are de-duplicated and sorted by path.
Re-exports
Middleware (the type from @wrnexus/core) is re-exported for callers that load middleware modules themselves.
Usage
import { buildRouter } from "@wrnexus/router";
const router = buildRouter("./app", {
componentDirs: ["./node_modules/@wrnexus/ui/components"],
});
// Resolve an incoming request.
const match = router.matchPage("/users/42");
if (match) {
console.log(match.route.file); // absolute path to the page module
console.log(match.params); // { id: "42" }
}
const api = router.matchApi("/api/hello");
const rt = router.matchRealtime("/realtime/chat");
Generating the typed-routes file (as wrnexus dev does):
import { generateRoutesFile } from "@wrnexus/router";
import { writeFileSync } from "node:fs";
const router = buildRouter("./app");
writeFileSync("./app/routes.gen.ts", generateRoutesFile(router.pages));
// Then, in app code, links are checked at compile time:
import { href } from "./routes.gen.ts";
href("/users/[id]", { id: "42" }); // "/users/42"
href("/about"); // "/about"
href("/nope"); // type error: unknown path
Lower-level pattern matching, if you need it directly:
import { compileRoutePattern, matchRoute, sortRoutes, type Route } from "@wrnexus/router";
const { regex, paramNames } = compileRoutePattern("/posts/[slug]");
const routes = sortRoutes([{ raw: "/posts/[slug]", file: "…", regex, paramNames }]);
const m = matchRoute(routes, "/posts/hello"); // { route, params: { slug: "hello" } }
Requirements / Notes
- Scanning uses
node:fs(existsSync,readdirSync,statSync) andnode:path— runs under Bun. - Depends on [
@wrnexus/compiler](../compiler) toparse.wrnpages and extract embeddedapi/realtimeblocks. - Depends on [
@wrnexus/core](../core) forisSafeIslandName(name validation) and theMiddlewaretype. - Missing route directories are tolerated — a route kind you don't use simply yields an empty table.
Complete TypeScript API
Generated from the exact installed package declarations.
export { Middleware } from '@wrnexus/core';
/**
* Route compilation + matching.
*
* Supported segments:
* [id] required parameter
* [id?] optional parameter
* [[id]] optional parameter (directory-friendly form)
* [...slug] required catch-all
* [[...slug]] optional catch-all
*/
interface RouteParam {
name: string;
optional: boolean;
catchAll: boolean;
}
interface Route {
/** The human-readable route pattern, e.g. `/users/[id]`. */
raw: string;
/** Absolute path to the module that handles this route. */
file: string;
/** Compiled matcher. */
regex: RegExp;
/** Ordered names of dynamic params captured by `regex`. */
paramNames: string[];
/** Rich parameter metadata. Optional for compatibility with old manifests. */
paramMeta?: RouteParam[];
}
interface RouteMatch {
route: Route;
params: Record<string, string>;
}
/** Return parameter metadata without requiring callers to inspect the regex. */
declare function getRouteParams(raw: string): RouteParam[];
/** Compile a WRNexus route pattern into a RegExp + parameter metadata. */
declare function compileRoutePattern(raw: string): Pick<Route, "regex" | "paramNames" | "paramMeta">;
/**
* Order routes so static and constrained routes win over optional/catch-all
* routes. The ordering remains deterministic for identical specificity.
*/
declare function sortRoutes(routes: Route[]): Route[];
/** Find duplicate URL patterns before request handling starts. */
declare function findRouteConflicts(routes: Route[]): Array<{
raw: string;
files: string[];
}>;
/** Find the first route whose pattern matches `pathname`. */
declare function matchRoute(routes: Route[], pathname: string): RouteMatch | null;
/**
* Typed-routes codegen. From the scanned page routes, emit `app/routes.gen.ts`
* with a `Routes` map (path -> param types) and an `href()` builder.
*/
declare function generateRoutesFile(pages: Route[]): string;
interface NamedRoute extends Route {
name: string;
metadata?: Record<string, unknown>;
}
interface RouteManifestEntry {
name: string;
path: string;
file: string;
params: ReturnType<typeof getRouteParams>;
metadata?: Record<string, unknown>;
}
declare function routeName(raw: string): string;
declare function nameRoutes(routes: readonly Route[], metadata?: Record<string, Record<string, unknown>>): NamedRoute[];
declare function createRouteManifest(routes: readonly NamedRoute[]): RouteManifestEntry[];
declare function routeUrl(route: Pick<NamedRoute, "raw" | "paramMeta">, params?: Record<string, string | number | Array<string | number> | null | undefined>, query?: Record<string, string | number | boolean | null | undefined>): string;
declare function findNamedRoute(routes: readonly NamedRoute[], name: string): NamedRoute;
/**
* @wrnexus/router — file-based router.
*
* Maps the `app/` directory onto route tables:
* app/pages/index.tsx -> GET /
* app/pages/about.tsx -> GET /about
* app/pages/users/[id].tsx-> GET /users/:id
* app/api/hello.ts -> /api/hello
* app/realtime/chat.ts -> /realtime/chat
* app/pages/*.wrn api -> embedded /api/* routes
* app/pages/*.wrn realtime -> embedded /realtime/* routes
* app/middleware/*.ts -> global middleware (alphabetical)
* app/components/*.wrn -> server-rendered components (by declaration),
* mounted in a page via data-component="<name>"
*/
interface ComponentRef {
/** Validated component name (matches a `data-component` attribute). */
name: string;
/** Absolute path to the component's `.wrn` module. */
file: string;
}
interface Router {
pages: Route[];
api: Route[];
realtime: Route[];
/** Absolute paths of middleware modules, in execution order. */
middlewareFiles: string[];
/** Server-rendered `.wrn` components, mounted via `data-component`. */
components: ComponentRef[];
/** Named page layouts (`app/layouts/<name>.wrn`); a page picks one via `layout`. */
layouts: ComponentRef[];
/** Typed global/page stores discovered under `app/stores/`. */
stores: ComponentRef[];
/** Validation schemas (`app/schemas/<name>.ts`) shared by API + forms. */
schemas: ComponentRef[];
/** Authorization declarations (`app/authz/<name>.ts`) merged into the catalog. */
authz: ComponentRef[];
/** Service implementations (`app/services/<name>.ts`) mounted for inter-app calls. */
services: ComponentRef[];
matchPage(pathname: string): RouteMatch | null;
matchApi(pathname: string): RouteMatch | null;
matchRealtime(pathname: string): RouteMatch | null;
}
interface ExternalRouteDefinition {
kind: "page" | "api" | "realtime";
path: string;
entry: string;
name?: string;
}
interface RouterOptions {
/**
* Extra directories to scan for `.wrn` components (e.g. `@wrnexus/ui`).
* Scanned before `app/components`, so an app component of the same name wins.
*/
componentDirs?: string[];
/** Package-owned routes registered by the plugin contribution system. */
externalRoutes?: ExternalRouteDefinition[];
/** Package-owned middleware executed before app/middleware. */
middlewareFiles?: string[];
}
/**
* Convert a scanned file's relative path into a URL route pattern.
* - strips the extension
* - drops a trailing `index` segment
* - prefixes with `prefix` (e.g. "/api")
*/
declare function fileToRoute(rel: string, prefix?: string): string;
/** Scan an app directory and build all route tables. */
declare function buildRouter(appDir: string, opts?: RouterOptions): Router;
export { type ComponentRef, type ExternalRouteDefinition, type NamedRoute, type Route, type RouteManifestEntry, type RouteMatch, type Router, type RouterOptions, buildRouter, compileRoutePattern, createRouteManifest, fileToRoute, findNamedRoute, findRouteConflicts, generateRoutesFile, getRouteParams, matchRoute, nameRoutes, routeName, routeUrl, sortRoutes };
Examples
Copy-ready examples from the installed package documentation.
Typical usage
import { buildRouter } from "@wrnexus/router";
const router = buildRouter("./app", {
componentDirs: ["./node_modules/@wrnexus/ui/components"],
});
// Resolve an incoming request.
const match = router.matchPage("/users/42");
if (match) {
console.log(match.route.file); // absolute path to the page module
console.log(match.params); // { id: "42" }
}
const api = router.matchApi("/api/hello");
const rt = router.matchRealtime("/realtime/chat");Generating the typed-routes file (as wrnexus dev does)
import { generateRoutesFile } from "@wrnexus/router";
import { writeFileSync } from "node:fs";
const router = buildRouter("./app");
writeFileSync("./app/routes.gen.ts", generateRoutesFile(router.pages));Typical usage
// Then, in app code, links are checked at compile time:
import { href } from "./routes.gen.ts";
href("/users/[id]", { id: "42" }); // "/users/42"
href("/about"); // "/about"
href("/nope"); // type error: unknown pathLower-level pattern matching, if you need it directly
import { compileRoutePattern, matchRoute, sortRoutes, type Route } from "@wrnexus/router";
const { regex, paramNames } = compileRoutePattern("/posts/[slug]");
const routes = sortRoutes([{ raw: "/posts/[slug]", file: "…", regex, paramNames }]);
const m = matchRoute(routes, "/posts/hello"); // { route, params: { slug: "hello" } }