Files
WRNexusJS/packages/router
ClintchizandClaude Opus 5 e136fbc56a fix(router): quietly skip permissions.gen.{ts,js} in authz scan
Task 10 fix round 1: the coordinator's plan doc (41fb82b9) recorded that
generated authz type files should be skipped before the isSafeIslandName
check, but the code change never landed. isSafeIslandName rejects the dot
in the stripped basename "permissions.gen", so every app running Task 12's
codegen would warn on every boot.

Add a quiet skip for *.gen.ts / *.gen.js immediately after the extension
guard, before the name check. Add tests: a .gen.ts file is skipped without
a console.warn (spied), and a .gen.js file is skipped the same way while a
legitimately named .js declaration is still discovered.

Also corrects the scanDir extraExtensions doc comment, which incorrectly
implied app/schemas passes it too (only app/authz does).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 20:09:16 +05:30
..
2026-08-04 12:19:09 +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.