first commit
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
# @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.
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@wrnexus/router",
|
||||
"version": "0.2.12",
|
||||
"type": "module",
|
||||
"description": "@wrnexus/router — part of the WrNexus framework.",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"engines": {
|
||||
"bun": ">=1.1.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://registry.npmjs.org/",
|
||||
"access": "restricted"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/compiler": "^0.2.12",
|
||||
"@wrnexus/core": "^0.2.12"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user