W WRNexusJS
Core · Package reference

@wrnexus/router

Filesystem discovery, route matching, and typed route generation.

v0.8.7Private registryCore

Install the package

After WorkRoot approves private registry access, install the release-aligned package:

bun add @wrnexus/router@0.8.7

Request 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

ExportSignatureDescription
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) and node:path — runs under Bun.
  • Depends on [@wrnexus/compiler](../compiler) to parse .wrn pages and extract embedded api / realtime blocks.
  • Depends on [@wrnexus/core](../core) for isSafeIslandName (name validation) and the Middleware type.
  • 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 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" } }