W WRNexusJS
Core

@wrnexus/router

Filesystem discovery, route matching, and typed route generation.

bun add @wrnexus/router@0.2.15
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.

Installation

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

This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.

export { Middleware } from '@wrnexus/core';

/**
 * Route compilation + matching.
 *
 * A "route" is a URL pattern compiled to a RegExp. We support static segments
 * and dynamic `[param]` segments, e.g. `/users/[id]` -> `{ id }`.
 */
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[];
}
interface RouteMatch {
    route: Route;
    params: Record<string, string>;
}
/** Compile a `/users/[id]` style pattern into a RegExp + param names. */
declare function compileRoutePattern(raw: string): Pick<Route, "regex" | "paramNames">;
/**
 * Order routes so that static routes win over dynamic ones, and longer/more
 * specific routes win over shorter ones. Sorting once keeps matching simple.
 */
declare function sortRoutes(routes: Route[]): Route[];
/** 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 — so links
 * are checked at compile time (unknown path or missing param = type error).
 */

declare function generateRoutesFile(pages: Route[]): string;

/**
 * @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 basename),
 *                              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[];
    /** 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 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[];
}
/** Scan an app directory and build all route tables. */
declare function buildRouter(appDir: string, opts?: RouterOptions): Router;

export { type ComponentRef, type Route, type RouteMatch, type Router, type RouterOptions, buildRouter, compileRoutePattern, generateRoutesFile, matchRoute, sortRoutes };

Examples

Copy-ready examples taken from this package's published documentation.

Example 1

bun add @wrnexus/router

Example 2

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

Example 3

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[];
}

Example 4

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;
}