Files
WRNexusJS/.publish/router/README.md
T
2026-07-12 15:55:18 +05:30

175 lines
6.9 KiB
Markdown

# @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
```bash
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.
```ts
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:
```ts
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. |
```ts
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
```ts
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
```ts
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):
```ts
import { generateRoutesFile } from "@wrnexus/router";
import { writeFileSync } from "node:fs";
const router = buildRouter("./app");
writeFileSync("./app/routes.gen.ts", generateRoutesFile(router.pages));
```
```ts
// 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:
```ts
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.