Files
WRNexusJS/packages/router
ClintchizandClaude Opus 5 7a2b58652a
Quality / quality (ubuntu-latest) (push) Failing after 11m2s
Quality / quality (windows-latest) (push) Canceled after 0s
chore(release): prepare 0.8.6
Bumps all 47 packages, the root manifest and the VS Code extension to 0.8.6,
and rebuilds the editor compiler, language server and extension bundles that
embed the version.

The release carries the output delivery fix: camelCase outputs now reach
parent bindings, and 18 components emit through output.* instead of
hand-built CustomEvents. See the 0.8.6 migration entry for what changes for
consumers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 01:54:44 +05:30
..
2026-08-09 01:54:44 +05:30
2026-07-12 15:55:18 +05:30

@wrnexus/router

File-based router that maps an app/ directory onto route tables and matches request paths against them.

Part of the WrNexus 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 WrNexus 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

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) and node:path — runs under Bun.
  • Depends on @wrnexus/compiler to parse .wrn pages and extract embedded api / realtime blocks.
  • Depends on @wrnexus/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.