@alxia/react-router 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,14 +4,31 @@
4
4
  [React Router](https://reactrouter.com) framework app, server rendered,
5
5
  under Bun. One Vite plugin and no server file: the pages are served by
6
6
  alxia in `react-router dev` and from the build. When you want more,
7
- `app/server.ts` adds the app's hooks — logger, compression, sessions,
7
+ `app/server.ts` adds the app's middlewares — logger, compression, sessions,
8
8
  guards — and an `/api` beside the pages, and each loader reads what those
9
- hooks built, typed.
9
+ middlewares built, typed.
10
10
 
11
11
  ## Quick start
12
12
 
13
- Start from the official template, `bunx create-react-router@latest`, and
14
- make four changes.
13
+ ### A new app
14
+
15
+ ```sh
16
+ bun create @alxia my-app --template react-router
17
+ cd my-app
18
+ bun dev
19
+ ```
20
+
21
+ [`@alxia/create`](https://www.npmjs.com/package/@alxia/create) writes React
22
+ Router's official template with everything below already done: this
23
+ package and `@alxia/core` installed, `alxia()` in `vite.config.ts`, the
24
+ `bunfig.toml`, `start` on Bun, Biome, and a `Dockerfile` that builds on
25
+ `oven/bun` and runs `build/` alone on Alpine. Its dependencies are
26
+ moved to the newest releases alxia accepts.
27
+
28
+ ### An existing React Router app
29
+
30
+ Start from the official template, `bunx create-react-router@latest`, or your
31
+ own app, and make four changes.
15
32
 
16
33
  **1. Install** alxia and this package:
17
34
 
@@ -68,7 +85,7 @@ With the `bunfig.toml` in place, the template's `dev`, `build` and
68
85
 
69
86
  After a build, `bunx --bun vite preview` serves that built server through
70
87
  Vite's preview server, as `bun run start` would: the pages, `/api`, the
71
- client's files and the app's hooks. So does React Router's prerendering.
88
+ client's files and the app's middlewares. So does React Router's prerendering.
72
89
 
73
90
  `@alxia/core` and `react-router` 8 are peers, and `vite` 7 or 8 is an
74
91
  optional peer, for `/vite`. The package declares no dependency.
@@ -91,8 +108,8 @@ commented, and the `Register` declaration. It refuses to overwrite a file
91
108
  already there; `--force` overwrites it. Run it from the app's root, once
92
109
  `@alxia/react-router` is installed.
93
110
 
94
- The examples use `@alxia/logger` (`bun add @alxia/logger`); any plugin
95
- works the same way. The plugin picks the file up in dev and in the build:
111
+ The examples use `@alxia/logger` (`bun add @alxia/logger`); any middleware
112
+ works the same way (`use` the observers first). The Vite plugin picks the file up in dev and in the build:
96
113
 
97
114
  ```ts
98
115
  // app/server.ts
@@ -145,9 +162,14 @@ export function loader({ context }: Route.LoaderArgs) {
145
162
  How `alxiaOf(context)` is typed:
146
163
 
147
164
  - with the `Register` declaration above, by that server: reading something
148
- no hook derives is a compile error;
165
+ no middleware derives is a compile error;
149
166
  - without it, `alxiaOf<typeof server>(context)` names the server;
150
- - with neither, `alxiaOf(context)` is `BaseContext`.
167
+ - with neither, by the base `@alxia/core`'s own `Register` names
168
+ (`context: typeof base`), when the app has one;
169
+ - with none of them, `alxiaOf(context)` is `BaseContext`.
170
+
171
+ This package's `Register` wins over core's: the server's app is the base
172
+ and all `configure` adds after it.
151
173
 
152
174
  [More](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide.md#typing-the-loaders)
153
175
 
@@ -193,7 +215,7 @@ bunx react-router reveal entry.server
193
215
  [readyOption]() {
194
216
  ```
195
217
 
196
- `nonceOf` returns `undefined` when no hook set a nonce, so the entry works
218
+ `nonceOf` returns `undefined` when no middleware set a nonce, so the entry works
197
219
  with or without secure-headers: neither package depends on the other. Any
198
220
  `derive` that returns a string `nonce` works the same.
199
221
  [More](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide.md#a-csp-nonce)
@@ -220,7 +242,7 @@ export default createServer({
220
242
 
221
243
  In dev, the plugin hands each upgrade Vite does not claim (its HMR, its
222
244
  `server.proxy`) to the app, run by `Bun.serve` as `listen` runs it: the
223
- hooks before the route, a refusal's status, `socket.data`, `publish`. An
245
+ middlewares before the route, a refusal's status, `socket.data`, `publish`. An
224
246
  edit to the server is used from the next connection.
225
247
  [More](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide.md#websockets)
226
248
 
@@ -248,7 +270,7 @@ client })` itself. [More](https://github.com/softistx/alxia/blob/develop/package
248
270
 
249
271
  ## The client's files
250
272
 
251
- In a build, `build/client` is served before `configure`'s hooks:
273
+ In a build, `build/client` is served before `configure`'s middlewares:
252
274
 
253
275
  | path | `Cache-Control` |
254
276
  | --- | --- |
@@ -258,24 +280,32 @@ In a build, `build/client` is served before `configure`'s hooks:
258
280
  A missing asset gets alxia's JSON 404, not a page. In dev, Vite serves
259
281
  these files.
260
282
 
261
- ## Leaving the pages out of OpenAPI
283
+ ## Leaving the pages out of `matchesSpec`
262
284
 
263
- The examples use `@alxia/openapi` (`bun add @alxia/openapi`):
285
+ An API under `/api`, spec first, is checked against its OpenAPI document
286
+ by [`@alxia/openapi`](https://www.npmjs.com/package/@alxia/openapi)'s
287
+ `matchesSpec` (`bun add -d @alxia/openapi`), which reads `app.routes`. The
288
+ catch-all and the client's files are routes too, and no operation of the
289
+ document:
264
290
 
265
291
  ```ts
266
- // app/server.ts
267
- import { docs } from '@alxia/openapi';
268
- import { createServer, isReactRouterRoute } from '@alxia/react-router';
269
-
270
- export default createServer({
271
- configure: (app) =>
272
- app.use(docs(app, { info: { title: 'Shop', version: '1.0.0' }, exclude: isReactRouterRoute })),
292
+ // app/server.test.ts
293
+ import { test } from 'bun:test';
294
+ import { matchesSpec } from '@alxia/openapi';
295
+ import { isReactRouterRoute } from '@alxia/react-router';
296
+ import type { ServerBuild } from 'react-router';
297
+ import { operations } from './generated/alxia';
298
+ import server from './server';
299
+
300
+ test('the API routes every operation of openapi.yaml, and nothing else', async () => {
301
+ const build: ServerBuild = await import(new URL('../build/server/index.js', import.meta.url).href);
302
+ const app = server.create({ build });
303
+ matchesSpec(app, operations, { exclude: isReactRouterRoute });
273
304
  });
274
305
  ```
275
306
 
276
- The catch-all adds nothing to the app's route table, so the typed client
277
- never shows it. `isReactRouterRoute` names the catch-all and the client
278
- files, so `@alxia/openapi` can leave them out.
307
+ `isReactRouterRoute` is true for each route this package declared, so
308
+ `matchesSpec` checks your own routes alone.
279
309
 
280
310
  ## Built for Bun
281
311
 
@@ -328,7 +358,8 @@ has each option and why.
328
358
 
329
359
  ## Docker
330
360
 
331
- A multi-stage `Dockerfile` on Bun's official image, as `@alxia/create`'s
361
+ A multi-stage `Dockerfile` on Bun's official images, built on Debian's
362
+ and run on Alpine's, as `@alxia/create`'s
332
363
  `react-router` template ships it. For an app from `create-react-router`,
333
364
  it replaces the template's, which runs on Node. The build is
334
365
  self-contained, so the image holds `build/` alone, no `node_modules`:
@@ -341,7 +372,7 @@ RUN bun install --frozen-lockfile
341
372
  COPY . .
342
373
  RUN bun run build
343
374
 
344
- FROM oven/bun:1
375
+ FROM oven/bun:1-alpine
345
376
  WORKDIR /app
346
377
  ENV NODE_ENV=production
347
378
  COPY --from=build /app/build ./build
@@ -389,17 +420,17 @@ has the commented file, and what to copy for a package left external.
389
420
  | `ReactRouterServer<App>` | what it returns: `create(wiring)` makes the app, `start(app)` listens |
390
421
  | `ServerWiring` | what `create` takes: `build`, `mode`, `client` |
391
422
  | `FreshApp` | the app `beforeAll`, or `configure` without it, receives |
392
- | `alxiaOf<App>(context)` | what alxia's hooks built, in a loader, an action or a middleware. Typed by the registered server, by the type argument (a server or an app), or as `BaseContext` |
423
+ | `alxiaOf<App>(context)` | what alxia's middlewares built, in a loader, an action or a middleware. Typed by the registered server, by the type argument (a server or an app), or as `BaseContext` |
393
424
  | `Register` | the interface to augment with `server: typeof server` |
394
- | `RegisteredApp` | the app `alxiaOf` reads with no type argument |
395
- | `RegisteredOf<R>` | the app a `Register`-shaped interface names: its server's, a fresh app, or `InvalidRegister` |
425
+ | `RegisteredApp` | the app `alxiaOf` reads with no type argument: the registered server's, else the one `@alxia/core`'s `Register` names, else a fresh app |
426
+ | `RegisteredOf<R, Core?>` | the app a `Register`-shaped interface names: its server's, `InvalidRegister`, or with no server `Core`, by default core's `RegisteredBase` |
396
427
  | `InvalidRegister` | what a `Register` naming neither a server nor an app reads as: every key of the app's own a compile error |
397
428
  | `AppOf<Server>` | the app a server makes |
398
429
  | `alxiaContext` | the React Router context key `alxiaOf` reads, set on every request |
399
- | `nonceOf(context)` | the request's CSP nonce, for `entry.server.tsx`: the context's `nonce` when a hook set one, such as `secureHeaders({ nonce: true })`, else `undefined` |
430
+ | `nonceOf(context)` | the request's CSP nonce, for `entry.server.tsx`: the context's `nonce` when a middleware set one, such as `secureHeaders({ nonce: true })`, else `undefined` |
400
431
  | `reactRouter(app, options)` | the catch-all and the client's files, for a server of your own. `build`, `mode`, `getLoadContext`, `client` |
401
432
  | `ReactRouterOptions<Ctx>` | its options |
402
- | `isReactRouterRoute(route)` | whether this package declared a route, for OpenAPI's `exclude` |
433
+ | `isReactRouterRoute(route)` | whether this package declared a route, for `matchesSpec`'s `exclude` |
403
434
 
404
435
  From `@alxia/react-router/vite`:
405
436
 
package/dist/cli/index.js CHANGED
@@ -29,7 +29,7 @@ const server = createServer({
29
29
  // limit, a logger that should see every request. Returns the app.
30
30
  // beforeAll: (app) => app,
31
31
 
32
- // The app the pages run behind: its plugins, its hooks, its /api. What it
32
+ // The app the pages run behind: its middlewares, its plugins, its /api. What it
33
33
  // builds is what alxiaOf(context) reads in the loaders. Returns the app.
34
34
  // configure: (app) => app.get('/api/health', ({ reply }) => reply.ok({ ok: true })),
35
35
 
@@ -142,7 +142,7 @@ ${USAGE}`);
142
142
  return 1;
143
143
  }
144
144
  output.out(`${NAME}: wrote ${result.written}, the server alxia() runs by default.
145
- Next: uncomment configure in ${result.written} to add the app's hooks and /api; bun run dev picks it up.`);
145
+ Next: uncomment configure in ${result.written} to add the app's middlewares and /api; bun run dev picks it up.`);
146
146
  return 0;
147
147
  } catch (error) {
148
148
  output.err(error instanceof Error ? error.message : String(error));
@@ -156,5 +156,5 @@ process.exitCode = await main(process.argv.slice(2), process.cwd(), {
156
156
  err: (line) => console.error(line)
157
157
  });
158
158
 
159
- //# debugId=7E4161A01E7D668A64756E2164756E21
159
+ //# debugId=8582AE498438642864756E2164756E21
160
160
  //# sourceMappingURL=index.js.map
@@ -3,11 +3,11 @@
3
3
  "sources": ["../src/cli/reveal.ts", "../src/cli/template.ts", "../src/cli/main.ts", "../src/cli/index.ts"],
4
4
  "sourcesContent": [
5
5
  "/**\n * `alxia-react-router reveal`: where the server file goes, read cheaply from\n * the app's own config files, and writing it there.\n */\nimport { existsSync } from 'node:fs';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport {\n\tbasename,\n\tdirname,\n\tisAbsolute,\n\tjoin,\n\trelative,\n\tresolve,\n} from 'node:path';\nimport { NAME } from '../vite/entry';\nimport { SERVER_FILE } from './template';\n\nconst EXTENSIONS = ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs'];\n\n/** `vite.config.ts`, or the first of its other extensions found in `root`. */\nfunction configFile(root: string, base: string): string | undefined {\n\treturn EXTENSIONS.map((extension) => join(root, `${base}${extension}`)).find(\n\t\t(file) => existsSync(file),\n\t);\n}\n\n/** A config file's source, without its comments: Bun's transpiler drops them. */\nasync function sourceOf(file: string): Promise<string> {\n\tconst loader = /\\.[cm]?ts$/.test(file) ? 'ts' : 'js';\n\treturn new Bun.Transpiler({ loader }).transformSync(\n\t\tawait readFile(file, 'utf8'),\n\t);\n}\n\n/**\n * The string literal given to `key` in `source`; `undefined` when `key` is\n * not there. A `key` given anything else is refused with `computed`.\n */\nfunction literal(\n\tsource: string,\n\t{ key, value, computed }: { key: RegExp; value: RegExp; computed: string },\n): string | undefined {\n\tconst found = source.match(value)?.[2];\n\tif (found !== undefined) return found;\n\tif (key.test(source)) throw new Error(computed);\n\treturn undefined;\n}\n\n/**\n * The file the plugin would load as the server: `alxia({ entry })`'s, or\n * `server.ts` in React Router's app directory, `app/` unless its config\n * names another. Each is read as a string literal; one computed is refused,\n * since reveal would write a file the plugin does not load.\n */\nexport async function revealTarget(root: string): Promise<string> {\n\tconst vite = configFile(root, 'vite.config');\n\tif (vite === undefined) {\n\t\tthrow new Error(\n\t\t\t`${NAME}: no vite.config.ts in ${root}. Run reveal from the app's root, beside vite.config.ts.`,\n\t\t);\n\t}\n\tconst entry = literal(await sourceOf(vite), {\n\t\tkey: /\\balxia\\(\\s*\\{[^}]*?\\bentry\\s*:/s,\n\t\tvalue: /\\balxia\\(\\s*\\{[^}]*?\\bentry\\s*:\\s*(['\"`])([^'\"`$]+)\\1/s,\n\t\tcomputed: `${NAME}: ${basename(vite)} gives alxia() an entry reveal cannot read. Write it as a string literal, alxia({ entry: 'app/server.ts' }), and run reveal again.`,\n\t});\n\tif (entry !== undefined) {\n\t\treturn isAbsolute(entry) ? entry : resolve(root, entry);\n\t}\n\tconst rr = configFile(root, 'react-router.config');\n\tconst app =\n\t\t(rr === undefined\n\t\t\t? undefined\n\t\t\t: literal(await sourceOf(rr), {\n\t\t\t\t\tkey: /\\bappDirectory\\s*:/,\n\t\t\t\t\tvalue: /\\bappDirectory\\s*:\\s*(['\"`])([^'\"`$]+)\\1/,\n\t\t\t\t\tcomputed: `${NAME}: ${basename(rr)} computes appDirectory, which reveal cannot read. Write it as a string literal, appDirectory: 'app', and run reveal again.`,\n\t\t\t\t})) ?? 'app';\n\treturn resolve(root, app, 'server.ts');\n}\n\nexport type Revealed =\n\t| { readonly written: string }\n\t| { readonly exists: string };\n\n/**\n * Writes the server file into the app at `root`, unless one is already\n * there and `force` is not set. The path comes back relative to `root`.\n */\nexport async function reveal(root: string, force: boolean): Promise<Revealed> {\n\tconst file = await revealTarget(root);\n\tconst label = relative(root, file);\n\tif (existsSync(file) && !force) return { exists: label };\n\tawait mkdir(dirname(file), { recursive: true });\n\tawait writeFile(file, SERVER_FILE);\n\treturn { written: label };\n}\n",
6
- "/**\n * The server file `alxia-react-router reveal` writes: `createServer()` as\n * the default server runs it, its options commented, and the `Register`\n * declaration that types the loaders.\n */\n\n/** The file, as written. Every commented option compiles once uncommented. */\nexport const SERVER_FILE = `/**\n * The alxia server of this React Router app: what alxia() from\n * @alxia/react-router/vite runs when there is no server file, written out by\n * \\`alxia-react-router reveal\\`. Uncomment an option to customise it.\n */\nimport { createServer } from '@alxia/react-router';\n// import { userAgentContext } from './context'; // for getLoadContext, below\n\nconst server = createServer({\n\t// Runs first, on a new app, before the client's files too: a guard, a rate\n\t// limit, a logger that should see every request. Returns the app.\n\t// beforeAll: (app) => app,\n\n\t// The app the pages run behind: its plugins, its hooks, its /api. What it\n\t// builds is what alxiaOf(context) reads in the loaders. Returns the app.\n\t// configure: (app) => app.get('/api/health', ({ reply }) => reply.ok({ ok: true })),\n\n\t// Sets the app's own React Router context keys, ctx typed by configure's\n\t// app. A key made with createContext() from react-router, in app/context.ts.\n\t// getLoadContext: (ctx, context) => {\n\t// \tcontext.set(userAgentContext, ctx.request.headers.get('user-agent'));\n\t// },\n});\n\nexport default server;\n\n// What alxiaOf(context) reads in the loaders, with no type argument.\ndeclare module '@alxia/react-router' {\n\tinterface Register {\n\t\tserver: typeof server;\n\t}\n}\n`;\n",
7
- "/**\n * The `alxia-react-router` command: its arguments, what it prints, and its\n * exit code. `index.ts` runs it on the process's own.\n */\nimport { NAME } from '../vite/entry';\nimport { reveal } from './reveal';\n\nexport const USAGE = `Usage: ${NAME} <command> [options]\n\nCommands:\n reveal [--force] Write the server alxia() runs by default to app/server.ts,\n or to alxia({ entry })'s file, to customise it. --force\n overwrites a file already there.\n\nOptions:\n --help Show this help.`;\n\n/** Where the command prints: the process's streams, or a spec's. */\nexport interface Output {\n\treadonly out: (line: string) => void;\n\treadonly err: (line: string) => void;\n}\n\n/** Runs the command in `root`; resolves to the process's exit code. */\nexport async function main(\n\targs: readonly string[],\n\troot: string,\n\toutput: Output,\n): Promise<number> {\n\tconst [command, ...options] = args;\n\tif (command === undefined) {\n\t\toutput.err(USAGE);\n\t\treturn 1;\n\t}\n\tif (command === '--help' || command === '-h') {\n\t\toutput.out(USAGE);\n\t\treturn 0;\n\t}\n\tif (command !== 'reveal') {\n\t\toutput.err(`${NAME}: unknown command ${command}.\\n\\n${USAGE}`);\n\t\treturn 1;\n\t}\n\tif (options.includes('--help') || options.includes('-h')) {\n\t\toutput.out(USAGE);\n\t\treturn 0;\n\t}\n\tconst unknown = options.find((option) => option !== '--force');\n\tif (unknown !== undefined) {\n\t\toutput.err(`${NAME}: unknown option ${unknown} for reveal.\\n\\n${USAGE}`);\n\t\treturn 1;\n\t}\n\ttry {\n\t\tconst result = await reveal(root, options.includes('--force'));\n\t\tif ('exists' in result) {\n\t\t\toutput.err(\n\t\t\t\t`${NAME}: ${result.exists} already exists, and reveal leaves it as it is. Run ${NAME} reveal --force to overwrite it.`,\n\t\t\t);\n\t\t\treturn 1;\n\t\t}\n\t\toutput.out(\n\t\t\t`${NAME}: wrote ${result.written}, the server alxia() runs by default.\\nNext: uncomment configure in ${result.written} to add the app's hooks and /api; bun run dev picks it up.`,\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\toutput.err(error instanceof Error ? error.message : String(error));\n\t\treturn 1;\n\t}\n}\n",
6
+ "/**\n * The server file `alxia-react-router reveal` writes: `createServer()` as\n * the default server runs it, its options commented, and the `Register`\n * declaration that types the loaders.\n */\n\n/** The file, as written. Every commented option compiles once uncommented. */\nexport const SERVER_FILE = `/**\n * The alxia server of this React Router app: what alxia() from\n * @alxia/react-router/vite runs when there is no server file, written out by\n * \\`alxia-react-router reveal\\`. Uncomment an option to customise it.\n */\nimport { createServer } from '@alxia/react-router';\n// import { userAgentContext } from './context'; // for getLoadContext, below\n\nconst server = createServer({\n\t// Runs first, on a new app, before the client's files too: a guard, a rate\n\t// limit, a logger that should see every request. Returns the app.\n\t// beforeAll: (app) => app,\n\n\t// The app the pages run behind: its middlewares, its plugins, its /api. What it\n\t// builds is what alxiaOf(context) reads in the loaders. Returns the app.\n\t// configure: (app) => app.get('/api/health', ({ reply }) => reply.ok({ ok: true })),\n\n\t// Sets the app's own React Router context keys, ctx typed by configure's\n\t// app. A key made with createContext() from react-router, in app/context.ts.\n\t// getLoadContext: (ctx, context) => {\n\t// \tcontext.set(userAgentContext, ctx.request.headers.get('user-agent'));\n\t// },\n});\n\nexport default server;\n\n// What alxiaOf(context) reads in the loaders, with no type argument.\ndeclare module '@alxia/react-router' {\n\tinterface Register {\n\t\tserver: typeof server;\n\t}\n}\n`;\n",
7
+ "/**\n * The `alxia-react-router` command: its arguments, what it prints, and its\n * exit code. `index.ts` runs it on the process's own.\n */\nimport { NAME } from '../vite/entry';\nimport { reveal } from './reveal';\n\nexport const USAGE = `Usage: ${NAME} <command> [options]\n\nCommands:\n reveal [--force] Write the server alxia() runs by default to app/server.ts,\n or to alxia({ entry })'s file, to customise it. --force\n overwrites a file already there.\n\nOptions:\n --help Show this help.`;\n\n/** Where the command prints: the process's streams, or a spec's. */\nexport interface Output {\n\treadonly out: (line: string) => void;\n\treadonly err: (line: string) => void;\n}\n\n/** Runs the command in `root`; resolves to the process's exit code. */\nexport async function main(\n\targs: readonly string[],\n\troot: string,\n\toutput: Output,\n): Promise<number> {\n\tconst [command, ...options] = args;\n\tif (command === undefined) {\n\t\toutput.err(USAGE);\n\t\treturn 1;\n\t}\n\tif (command === '--help' || command === '-h') {\n\t\toutput.out(USAGE);\n\t\treturn 0;\n\t}\n\tif (command !== 'reveal') {\n\t\toutput.err(`${NAME}: unknown command ${command}.\\n\\n${USAGE}`);\n\t\treturn 1;\n\t}\n\tif (options.includes('--help') || options.includes('-h')) {\n\t\toutput.out(USAGE);\n\t\treturn 0;\n\t}\n\tconst unknown = options.find((option) => option !== '--force');\n\tif (unknown !== undefined) {\n\t\toutput.err(`${NAME}: unknown option ${unknown} for reveal.\\n\\n${USAGE}`);\n\t\treturn 1;\n\t}\n\ttry {\n\t\tconst result = await reveal(root, options.includes('--force'));\n\t\tif ('exists' in result) {\n\t\t\toutput.err(\n\t\t\t\t`${NAME}: ${result.exists} already exists, and reveal leaves it as it is. Run ${NAME} reveal --force to overwrite it.`,\n\t\t\t);\n\t\t\treturn 1;\n\t\t}\n\t\toutput.out(\n\t\t\t`${NAME}: wrote ${result.written}, the server alxia() runs by default.\\nNext: uncomment configure in ${result.written} to add the app's middlewares and /api; bun run dev picks it up.`,\n\t\t);\n\t\treturn 0;\n\t} catch (error) {\n\t\toutput.err(error instanceof Error ? error.message : String(error));\n\t\treturn 1;\n\t}\n}\n",
8
8
  "#!/usr/bin/env bun\n/**\n * `alxia-react-router`, the package's bin: `bunx alxia-react-router reveal`\n * writes the default server into the app, to customise it.\n */\nimport { main } from './main';\n\nprocess.exitCode = await main(process.argv.slice(2), process.cwd(), {\n\tout: (line) => console.log(line),\n\terr: (line) => console.error(line),\n});\n"
9
9
  ],
10
- "mappings": "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",
11
- "debugId": "7E4161A01E7D668A64756E2164756E21",
10
+ "mappings": "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",
11
+ "debugId": "8582AE498438642864756E2164756E21",
12
12
  "names": []
13
13
  }
@@ -4,5 +4,5 @@
4
4
  * declaration that types the loaders.
5
5
  */
6
6
  /** The file, as written. Every commented option compiles once uncommented. */
7
- export declare const SERVER_FILE = "/**\n * The alxia server of this React Router app: what alxia() from\n * @alxia/react-router/vite runs when there is no server file, written out by\n * `alxia-react-router reveal`. Uncomment an option to customise it.\n */\nimport { createServer } from '@alxia/react-router';\n// import { userAgentContext } from './context'; // for getLoadContext, below\n\nconst server = createServer({\n\t// Runs first, on a new app, before the client's files too: a guard, a rate\n\t// limit, a logger that should see every request. Returns the app.\n\t// beforeAll: (app) => app,\n\n\t// The app the pages run behind: its plugins, its hooks, its /api. What it\n\t// builds is what alxiaOf(context) reads in the loaders. Returns the app.\n\t// configure: (app) => app.get('/api/health', ({ reply }) => reply.ok({ ok: true })),\n\n\t// Sets the app's own React Router context keys, ctx typed by configure's\n\t// app. A key made with createContext() from react-router, in app/context.ts.\n\t// getLoadContext: (ctx, context) => {\n\t// \tcontext.set(userAgentContext, ctx.request.headers.get('user-agent'));\n\t// },\n});\n\nexport default server;\n\n// What alxiaOf(context) reads in the loaders, with no type argument.\ndeclare module '@alxia/react-router' {\n\tinterface Register {\n\t\tserver: typeof server;\n\t}\n}\n";
7
+ export declare const SERVER_FILE = "/**\n * The alxia server of this React Router app: what alxia() from\n * @alxia/react-router/vite runs when there is no server file, written out by\n * `alxia-react-router reveal`. Uncomment an option to customise it.\n */\nimport { createServer } from '@alxia/react-router';\n// import { userAgentContext } from './context'; // for getLoadContext, below\n\nconst server = createServer({\n\t// Runs first, on a new app, before the client's files too: a guard, a rate\n\t// limit, a logger that should see every request. Returns the app.\n\t// beforeAll: (app) => app,\n\n\t// The app the pages run behind: its middlewares, its plugins, its /api. What it\n\t// builds is what alxiaOf(context) reads in the loaders. Returns the app.\n\t// configure: (app) => app.get('/api/health', ({ reply }) => reply.ok({ ok: true })),\n\n\t// Sets the app's own React Router context keys, ctx typed by configure's\n\t// app. A key made with createContext() from react-router, in app/context.ts.\n\t// getLoadContext: (ctx, context) => {\n\t// \tcontext.set(userAgentContext, ctx.request.headers.get('user-agent'));\n\t// },\n});\n\nexport default server;\n\n// What alxiaOf(context) reads in the loaders, with no type argument.\ndeclare module '@alxia/react-router' {\n\tinterface Register {\n\t\tserver: typeof server;\n\t}\n}\n";
8
8
  //# sourceMappingURL=template.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../src/cli/template.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,8EAA8E;AAC9E,eAAO,MAAM,WAAW,iyCAgCvB,CAAC"}
1
+ {"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../src/cli/template.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,8EAA8E;AAC9E,eAAO,MAAM,WAAW,uyCAgCvB,CAAC"}
package/dist/context.d.ts CHANGED
@@ -2,12 +2,12 @@
2
2
  * The key under which `reactRouter()` hands every loader, action and
3
3
  * middleware the request's alxia context, and the typed way to read it.
4
4
  */
5
- import type { Alxia, AnyAlxia, ContextOf, Empty } from '@alxia/core';
5
+ import type { Alxia, AnyAlxia, ContextOf, RegisteredBase } from '@alxia/core';
6
6
  import { type RouterContextProvider } from 'react-router';
7
- import type { FreshApp, ReactRouterServer } from './server';
7
+ import type { ReactRouterServer } from './server';
8
8
  /**
9
9
  * The key `reactRouter()` sets on React Router's context provider, on every
10
- * request, to what alxia's hooks built for it. It lives in this package, so
10
+ * request, to what alxia's middlewares built for it. It lives in this package, so
11
11
  * it is one object whichever way the server was built or loaded: a key made
12
12
  * in the app's own `app/` folder is copied into React Router's build, and
13
13
  * the server that imports it separately sets a different one.
@@ -30,7 +30,10 @@ export declare const alxiaContext: import("react-router").RouterContext<unknown>
30
30
  * }
31
31
  * ```
32
32
  *
33
- * Unregistered, `alxiaOf(context)` reads `BaseContext`.
33
+ * Unregistered here, `alxiaOf(context)` reads the app `@alxia/core`'s own
34
+ * `Register` names as its `context`, and `BaseContext` when neither is
35
+ * declared. When both are, this one wins: the server's app is the whole
36
+ * app the pages run behind, the base's context and all `configure` adds.
34
37
  */
35
38
  export interface Register {
36
39
  }
@@ -43,18 +46,23 @@ export type AppOf<Server> = Server extends ReactRouterServer<infer App> ? App :
43
46
  */
44
47
  export type InvalidRegister = Alxia<{
45
48
  readonly 'Register.server must be typeof server, the default export of createServer()': never;
46
- }, Empty, '', never>;
47
- /** The app `alxiaOf` reads for a `Register` interface: its server's, a fresh one, or `InvalidRegister`. */
48
- export type RegisteredOf<R> = R extends {
49
+ }, '', never>;
50
+ /**
51
+ * The app `alxiaOf` reads for a `Register` interface: its server's, or
52
+ * `InvalidRegister`; with no server, `Core`, the app `@alxia/core`'s
53
+ * `Register` names (a fresh one when it names none).
54
+ */
55
+ export type RegisteredOf<R, Core extends AnyAlxia = RegisteredBase> = R extends {
49
56
  readonly server: infer Server;
50
- } ? Server extends AnyAlxia | ReactRouterServer<AnyAlxia> ? AppOf<Server> : InvalidRegister : FreshApp;
51
- /** What `alxiaOf` reads with no type argument: the registered server's app, or a fresh one. */
57
+ } ? Server extends AnyAlxia | ReactRouterServer<AnyAlxia> ? AppOf<Server> : InvalidRegister : Core;
58
+ /** What `alxiaOf` reads with no type argument: the registered server's app, else core's registered app, else a fresh one. */
52
59
  export type RegisteredApp = RegisteredOf<Register>;
53
60
  /**
54
- * What alxia's hooks built for this request, read in a loader, an action or
61
+ * What alxia's middlewares built for this request, read in a loader, an action or
55
62
  * a middleware, typed by the app: the server `Register` names, or the one
56
63
  * given as the type argument — `typeof server`, or an app *before* the
57
- * catch-all. With neither, `BaseContext`.
64
+ * catch-all. With neither, the app `@alxia/core`'s `Register` names, and
65
+ * `BaseContext` when nothing is registered.
58
66
  *
59
67
  * ```ts
60
68
  * export async function loader({ context }: Route.LoaderArgs) {
@@ -66,5 +74,8 @@ export type RegisteredApp = RegisteredOf<Register>;
66
74
  * `react-router dev` without the plugin, or a test that calls a loader
67
75
  * directly.
68
76
  */
69
- export declare function alxiaOf<App extends AnyAlxia | ReactRouterServer<AnyAlxia> = RegisteredApp>(context: Readonly<RouterContextProvider>): ContextOf<AppOf<App>>;
77
+ export declare function alxiaOf<App extends AnyAlxia | ReactRouterServer<AnyAlxia> = RegisteredApp>(context: Readonly<RouterContextProvider>): ContextOf<AppOf<App>> & {
78
+ /** The catch-all's route, which every request reaching React Router matched. */
79
+ readonly route: string;
80
+ };
70
81
  //# sourceMappingURL=context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACrE,OAAO,EAAiB,KAAK,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,KAAK,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAK5D;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,+CAAkC,CAAC;AAE5D;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,QAAQ;CAAG;AAE5B,iDAAiD;AACjD,MAAM,MAAM,KAAK,CAAC,MAAM,IACvB,MAAM,SAAS,iBAAiB,CAAC,MAAM,GAAG,CAAC,GAAG,GAAG,GAAG,MAAM,CAAC;AAE5D;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,KAAK,CAClC;IACC,QAAQ,CAAC,6EAA6E,EAAE,KAAK,CAAC;CAC9F,EACD,KAAK,EACL,EAAE,EACF,KAAK,CACL,CAAC;AAEF,2GAA2G;AAC3G,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,CAAC,SAAS;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,MAAM,CAAA;CAAE,GACtE,MAAM,SAAS,QAAQ,GAAG,iBAAiB,CAAC,QAAQ,CAAC,GACpD,KAAK,CAAC,MAAM,CAAC,GACb,eAAe,GAChB,QAAQ,CAAC;AAEZ,+FAA+F;AAC/F,MAAM,MAAM,aAAa,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;AAEnD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,OAAO,CACtB,GAAG,SAAS,QAAQ,GAAG,iBAAiB,CAAC,QAAQ,CAAC,GAAG,aAAa,EACjE,OAAO,EAAE,QAAQ,CAAC,qBAAqB,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAQjE"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAiB,KAAK,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAKlD;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,+CAAkC,CAAC;AAE5D;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,WAAW,QAAQ;CAAG;AAE5B,iDAAiD;AACjD,MAAM,MAAM,KAAK,CAAC,MAAM,IACvB,MAAM,SAAS,iBAAiB,CAAC,MAAM,GAAG,CAAC,GAAG,GAAG,GAAG,MAAM,CAAC;AAE5D;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,KAAK,CAClC;IACC,QAAQ,CAAC,6EAA6E,EAAE,KAAK,CAAC;CAC9F,EACD,EAAE,EACF,KAAK,CACL,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,YAAY,CACvB,CAAC,EACD,IAAI,SAAS,QAAQ,GAAG,cAAc,IACnC,CAAC,SAAS;IACb,QAAQ,CAAC,MAAM,EAAE,MAAM,MAAM,CAAC;CAC9B,GACE,MAAM,SAAS,QAAQ,GAAG,iBAAiB,CAAC,QAAQ,CAAC,GACpD,KAAK,CAAC,MAAM,CAAC,GACb,eAAe,GAChB,IAAI,CAAC;AAER,6HAA6H;AAC7H,MAAM,MAAM,aAAa,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;AAEnD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,OAAO,CACtB,GAAG,SAAS,QAAQ,GAAG,iBAAiB,CAAC,QAAQ,CAAC,GAAG,aAAa,EAElE,OAAO,EAAE,QAAQ,CAAC,qBAAqB,CAAC,GACtC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG;IAC1B,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACvB,CAQA"}
package/dist/index.js.map CHANGED
@@ -2,13 +2,13 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/context.ts", "../src/nonce.ts", "../src/react-router.ts", "../src/assets.ts", "../src/server.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * The key under which `reactRouter()` hands every loader, action and\n * middleware the request's alxia context, and the typed way to read it.\n */\nimport type { Alxia, AnyAlxia, ContextOf, Empty } from '@alxia/core';\nimport { createContext, type RouterContextProvider } from 'react-router';\nimport type { FreshApp, ReactRouterServer } from './server';\n\n/** The default of the key: no catch-all set it. */\nconst MISSING: unique symbol = Symbol('alxia context missing');\n\n/**\n * The key `reactRouter()` sets on React Router's context provider, on every\n * request, to what alxia's hooks built for it. It lives in this package, so\n * it is one object whichever way the server was built or loaded: a key made\n * in the app's own `app/` folder is copied into React Router's build, and\n * the server that imports it separately sets a different one.\n */\nexport const alxiaContext = createContext<unknown>(MISSING);\n\n/**\n * Names the server whose context `alxiaOf(context)` reads when given no\n * type argument. One React Router build has one server, so it may be\n * declared once, beside it:\n *\n * ```ts\n * // app/server.ts\n * const server = createServer({ configure: (app) => app.use(session) });\n * export default server;\n *\n * declare module '@alxia/react-router' {\n * interface Register {\n * server: typeof server;\n * }\n * }\n * ```\n *\n * Unregistered, `alxiaOf(context)` reads `BaseContext`.\n */\n// biome-ignore lint/suspicious/noEmptyInterface: an app augments it\nexport interface Register {}\n\n/** The app a server makes, or the app itself. */\nexport type AppOf<Server> =\n\tServer extends ReactRouterServer<infer App> ? App : Server;\n\n/**\n * What `Register` names when it is neither a server nor an app — the\n * module, say, rather than its default export: an app whose context has\n * nothing but this key, so that reading anything of it is a compile error.\n */\nexport type InvalidRegister = Alxia<\n\t{\n\t\treadonly 'Register.server must be typeof server, the default export of createServer()': never;\n\t},\n\tEmpty,\n\t'',\n\tnever\n>;\n\n/** The app `alxiaOf` reads for a `Register` interface: its server's, a fresh one, or `InvalidRegister`. */\nexport type RegisteredOf<R> = R extends { readonly server: infer Server }\n\t? Server extends AnyAlxia | ReactRouterServer<AnyAlxia>\n\t\t? AppOf<Server>\n\t\t: InvalidRegister\n\t: FreshApp;\n\n/** What `alxiaOf` reads with no type argument: the registered server's app, or a fresh one. */\nexport type RegisteredApp = RegisteredOf<Register>;\n\n/**\n * What alxia's hooks built for this request, read in a loader, an action or\n * a middleware, typed by the app: the server `Register` names, or the one\n * given as the type argument — `typeof server`, or an app *before* the\n * catch-all. With neither, `BaseContext`.\n *\n * ```ts\n * export async function loader({ context }: Route.LoaderArgs) {\n * const { user, log } = alxiaOf(context);\n * }\n * ```\n *\n * Throws when the request did not come through `reactRouter()`: under\n * `react-router dev` without the plugin, or a test that calls a loader\n * directly.\n */\nexport function alxiaOf<\n\tApp extends AnyAlxia | ReactRouterServer<AnyAlxia> = RegisteredApp,\n>(context: Readonly<RouterContextProvider>): ContextOf<AppOf<App>> {\n\tconst value = context.get(alxiaContext);\n\tif (value === MISSING) {\n\t\tthrow new Error(\n\t\t\t\"alxiaOf(): this request has no alxia context. Serve the React Router app through alxia: add alxia() from @alxia/react-router/vite to vite.config.ts's plugins, or, with a server of your own, serve the build through reactRouter() from @alxia/react-router.\",\n\t\t);\n\t}\n\treturn value as ContextOf<AppOf<App>>;\n}\n",
5
+ "/**\n * The key under which `reactRouter()` hands every loader, action and\n * middleware the request's alxia context, and the typed way to read it.\n */\nimport type { Alxia, AnyAlxia, ContextOf, RegisteredBase } from '@alxia/core';\nimport { createContext, type RouterContextProvider } from 'react-router';\nimport type { ReactRouterServer } from './server';\n\n/** The default of the key: no catch-all set it. */\nconst MISSING: unique symbol = Symbol('alxia context missing');\n\n/**\n * The key `reactRouter()` sets on React Router's context provider, on every\n * request, to what alxia's middlewares built for it. It lives in this package, so\n * it is one object whichever way the server was built or loaded: a key made\n * in the app's own `app/` folder is copied into React Router's build, and\n * the server that imports it separately sets a different one.\n */\nexport const alxiaContext = createContext<unknown>(MISSING);\n\n/**\n * Names the server whose context `alxiaOf(context)` reads when given no\n * type argument. One React Router build has one server, so it may be\n * declared once, beside it:\n *\n * ```ts\n * // app/server.ts\n * const server = createServer({ configure: (app) => app.use(session) });\n * export default server;\n *\n * declare module '@alxia/react-router' {\n * interface Register {\n * server: typeof server;\n * }\n * }\n * ```\n *\n * Unregistered here, `alxiaOf(context)` reads the app `@alxia/core`'s own\n * `Register` names as its `context`, and `BaseContext` when neither is\n * declared. When both are, this one wins: the server's app is the whole\n * app the pages run behind, the base's context and all `configure` adds.\n */\n// biome-ignore lint/suspicious/noEmptyInterface: an app augments it\nexport interface Register {}\n\n/** The app a server makes, or the app itself. */\nexport type AppOf<Server> =\n\tServer extends ReactRouterServer<infer App> ? App : Server;\n\n/**\n * What `Register` names when it is neither a server nor an app — the\n * module, say, rather than its default export: an app whose context has\n * nothing but this key, so that reading anything of it is a compile error.\n */\nexport type InvalidRegister = Alxia<\n\t{\n\t\treadonly 'Register.server must be typeof server, the default export of createServer()': never;\n\t},\n\t'',\n\tnever\n>;\n\n/**\n * The app `alxiaOf` reads for a `Register` interface: its server's, or\n * `InvalidRegister`; with no server, `Core`, the app `@alxia/core`'s\n * `Register` names (a fresh one when it names none).\n */\nexport type RegisteredOf<\n\tR,\n\tCore extends AnyAlxia = RegisteredBase,\n> = R extends {\n\treadonly server: infer Server;\n}\n\t? Server extends AnyAlxia | ReactRouterServer<AnyAlxia>\n\t\t? AppOf<Server>\n\t\t: InvalidRegister\n\t: Core;\n\n/** What `alxiaOf` reads with no type argument: the registered server's app, else core's registered app, else a fresh one. */\nexport type RegisteredApp = RegisteredOf<Register>;\n\n/**\n * What alxia's middlewares built for this request, read in a loader, an action or\n * a middleware, typed by the app: the server `Register` names, or the one\n * given as the type argument — `typeof server`, or an app *before* the\n * catch-all. With neither, the app `@alxia/core`'s `Register` names, and\n * `BaseContext` when nothing is registered.\n *\n * ```ts\n * export async function loader({ context }: Route.LoaderArgs) {\n * const { user, log } = alxiaOf(context);\n * }\n * ```\n *\n * Throws when the request did not come through `reactRouter()`: under\n * `react-router dev` without the plugin, or a test that calls a loader\n * directly.\n */\nexport function alxiaOf<\n\tApp extends AnyAlxia | ReactRouterServer<AnyAlxia> = RegisteredApp,\n>(\n\tcontext: Readonly<RouterContextProvider>,\n): ContextOf<AppOf<App>> & {\n\t/** The catch-all's route, which every request reaching React Router matched. */\n\treadonly route: string;\n} {\n\tconst value = context.get(alxiaContext);\n\tif (value === MISSING) {\n\t\tthrow new Error(\n\t\t\t\"alxiaOf(): this request has no alxia context. Serve the React Router app through alxia: add alxia() from @alxia/react-router/vite to vite.config.ts's plugins, or, with a server of your own, serve the build through reactRouter() from @alxia/react-router.\",\n\t\t);\n\t}\n\treturn value as ContextOf<AppOf<App>> & { readonly route: string };\n}\n",
6
6
  "/** `nonceOf`: the request's CSP nonce, read from alxia's context if a hook set one. */\nimport type { RouterContextProvider } from 'react-router';\nimport { alxiaContext } from './context';\n\n/**\n * This request's CSP nonce, for `entry.server.tsx`: the `nonce` alxia's\n * hooks put on the context — `secureHeaders({ nonce: true })` from\n * `@alxia/secure-headers`, or a `derive` of your own — or `undefined` when\n * none did, or when the request did not come through alxia. It reads the\n * key if present: neither package depends on the other.\n *\n * ```tsx\n * const nonce = nonceOf(loadContext);\n * <ServerRouter context={routerContext} url={request.url} nonce={nonce} />\n * ```\n */\nexport function nonceOf(\n\tcontext: Readonly<RouterContextProvider>,\n): string | undefined {\n\tconst value = context.get(alxiaContext);\n\tif (typeof value !== 'object' || value === null || !('nonce' in value)) {\n\t\treturn undefined;\n\t}\n\treturn typeof value.nonce === 'string' ? value.nonce : undefined;\n}\n",
7
- "import { fileURLToPath } from 'node:url';\nimport type {\n\tAlxia,\n\tAnyAlxia,\n\tAnyReply,\n\tBaseContext,\n\tMaybePromise,\n\tRouteDefinition,\n\tStatusCode,\n} from '@alxia/core';\nimport {\n\tcreateRequestHandler,\n\ttype RequestHandler,\n\tRouterContextProvider,\n\ttype ServerBuild,\n} from 'react-router';\nimport { serveClient } from './assets';\nimport { alxiaContext } from './context';\n\nexport interface ReactRouterOptions<Ctx> {\n\t/**\n\t * React Router's server build: the module itself, or a function that\n\t * returns it — `() => import('virtual:react-router/server-build')` in a\n\t * server entry Vite builds, `() => import('./build/server/index.js')`\n\t * beside a build. A function is called on every request in\n\t * `development`, so an edit is picked up, and once in `production`.\n\t */\n\treadonly build: ServerBuild | (() => MaybePromise<ServerBuild>);\n\t/** React Router's server mode: `production` by default. */\n\treadonly mode?: 'development' | 'production';\n\t/**\n\t * Sets the app's own keys on React Router's context provider, from the\n\t * context alxia's hooks built. `ctx` is typed by the app at the point of\n\t * `use`: reading what no hook before it derives is a compile error.\n\t * `alxiaContext` is always set, whether or not this is given.\n\t */\n\treadonly getLoadContext?: (\n\t\tctx: BaseContext & Ctx,\n\t\tcontext: RouterContextProvider,\n\t) => MaybePromise<void>;\n\t/**\n\t * The client build's folder, `build/client`: a path or a `file:` URL.\n\t * Its `assets/` is served immutable, and every other top-level file or\n\t * folder — the copies of `public/` — with an hour's cache, before the\n\t * catch-all. Ignored in `development`, where Vite serves them.\n\t */\n\treadonly client?: string | URL;\n}\n\n/** The methods the catch-all takes. A `HEAD` reaches its `GET`. */\nconst METHODS = ['get', 'post', 'put', 'patch', 'delete'] as const;\n\n/** The handlers `reactRouter()` declared: the catch-all's and the client build's. */\nconst declared = new WeakSet<RouteDefinition['handler']>();\n\n/**\n * Whether `reactRouter()` declared this route: the catch-all, or one of the\n * client build's files. For `@alxia/openapi`'s `exclude`, so the document\n * lists the app's API and not its pages.\n *\n * ```ts\n * app.use(docs(app, { info, exclude: isReactRouterRoute }));\n * ```\n */\nexport function isReactRouterRoute(route: RouteDefinition): boolean {\n\treturn declared.has(route.handler);\n}\n\n/**\n * A React Router framework app, server rendered on `app`: `GET`, `POST`,\n * `PUT`, `PATCH` and `DELETE` at `/*`, behind every hook declared on `app`\n * before it. Each loader, action and middleware reads what those hooks\n * built through `alxiaOf<App>(context)`. The app's own routes — an `/api`\n * — answer their paths, declared before it or after; the client build's\n * files are served when `client` is given. The catch-all adds nothing to\n * the app's route table: pages are not something the typed client calls.\n *\n * ```ts\n * const app = base.use((app) =>\n * reactRouter(app, { build: () => import('./build/server/index.js'), client: 'build/client' }),\n * );\n * ```\n */\nexport function reactRouter<\n\tCtx extends object,\n\tRoutes extends object,\n\tPrefix extends string,\n\tShortcuts extends AnyReply,\n>(\n\tapp: Alxia<Ctx, Routes, Prefix, Shortcuts>,\n\toptions: ReactRouterOptions<Ctx>,\n): Alxia<Ctx, Routes, Prefix, Shortcuts> {\n\tconst mode = options.mode ?? 'production';\n\tconst handle = requestHandler(options.build, mode);\n\tconst getLoadContext = options.getLoadContext;\n\tconst handler = async (ctx: BaseContext & Ctx) => {\n\t\tconst context = new RouterContextProvider();\n\t\tcontext.set(alxiaContext, ctx);\n\t\tawait getLoadContext?.(ctx, context);\n\t\t// React Router answers a HEAD with no headers at all: hand it the GET,\n\t\t// and the core drops the body.\n\t\tconst request =\n\t\t\tctx.request.method === 'HEAD'\n\t\t\t\t? new Request(ctx.request, { method: 'GET' })\n\t\t\t\t: ctx.request;\n\t\tconst response = await handle(request, context);\n\t\treturn ctx.reply(\n\t\t\tresponse.status as StatusCode,\n\t\t\tresponse.body ?? undefined,\n\t\t\t{ headers: response.headers },\n\t\t);\n\t};\n\n\tconst routes = asRoutes(app);\n\tdeclaring(app, () => {\n\t\tif (options.client !== undefined && mode === 'production') {\n\t\t\tserveClient(routes, pathOf(options.client));\n\t\t}\n\t\tfor (const method of METHODS) routes[method]('/*', handler);\n\t});\n\treturn app;\n}\n\n/**\n * Declares the client build's files on `app`, marked for\n * `isReactRouterRoute`: what `reactRouter()` does with `client`, apart, so\n * that `createServer()` serves them before the hooks of `configure`.\n */\nexport function declareClient(app: AnyAlxia, client: string | URL): void {\n\tdeclaring(app, () => serveClient(asRoutes(app), pathOf(client)));\n}\n\n/** Runs `declare`, and marks every route it added to `app` as ours. */\nfunction declaring(app: AnyAlxia, declare: () => void): void {\n\tconst before = new Set(app.routes);\n\tdeclare();\n\tfor (const route of app.routes) {\n\t\tif (!before.has(route)) declared.add(route.handler);\n\t}\n}\n\n/** The app's route methods, untyped: the catch-all's handler is not one a route type admits. */\nfunction asRoutes(app: AnyAlxia) {\n\treturn app as unknown as {\n\t\tstatic(path: string, source: string, options: object): unknown;\n\t\tfile(path: string, file: string, options: object): unknown;\n\t} & Record<\n\t\t(typeof METHODS)[number],\n\t\t(path: string, handler: unknown) => unknown\n\t>;\n}\n\n/** A handler that resolves a function build once in production, on every request in development. */\nfunction requestHandler(\n\tbuild: ReactRouterOptions<never>['build'],\n\tmode: 'development' | 'production',\n): RequestHandler {\n\tif (typeof build !== 'function' || mode === 'development') {\n\t\treturn createRequestHandler(build, mode);\n\t}\n\tlet resolved: Promise<RequestHandler> | undefined;\n\treturn async (request, context) => {\n\t\tresolved ??= Promise.resolve(build()).then(\n\t\t\t(server) => createRequestHandler(server, mode),\n\t\t\t(error: unknown) => {\n\t\t\t\tresolved = undefined;\n\t\t\t\tthrow error;\n\t\t\t},\n\t\t);\n\t\treturn (await resolved)(request, context);\n\t};\n}\n\nfunction pathOf(client: string | URL): string {\n\treturn client instanceof URL ? fileURLToPath(client) : client;\n}\n",
7
+ "import { fileURLToPath } from 'node:url';\nimport type {\n\tAlxia,\n\tAnyAlxia,\n\tAnyReply,\n\tBaseContext,\n\tMaybePromise,\n\tRouteDefinition,\n\tStatusCode,\n} from '@alxia/core';\nimport {\n\tcreateRequestHandler,\n\ttype RequestHandler,\n\tRouterContextProvider,\n\ttype ServerBuild,\n} from 'react-router';\nimport { serveClient } from './assets';\nimport { alxiaContext } from './context';\n\nexport interface ReactRouterOptions<Ctx> {\n\t/**\n\t * React Router's server build: the module itself, or a function that\n\t * returns it — `() => import('virtual:react-router/server-build')` in a\n\t * server entry Vite builds, `() => import('./build/server/index.js')`\n\t * beside a build. A function is called on every request in\n\t * `development`, so an edit is picked up, and once in `production`.\n\t */\n\treadonly build: ServerBuild | (() => MaybePromise<ServerBuild>);\n\t/** React Router's server mode: `production` by default. */\n\treadonly mode?: 'development' | 'production';\n\t/**\n\t * Sets the app's own keys on React Router's context provider, from the\n\t * context alxia's middlewares built. `ctx` is typed by the app at the point of\n\t * `app.plugin`: reading what nothing before it adds is a compile error.\n\t * `alxiaContext` is always set, whether or not this is given.\n\t */\n\treadonly getLoadContext?: (\n\t\tctx: BaseContext & Ctx,\n\t\tcontext: RouterContextProvider,\n\t) => MaybePromise<void>;\n\t/**\n\t * The client build's folder, `build/client`: a path or a `file:` URL.\n\t * Its `assets/` is served immutable, and every other top-level file or\n\t * folder — the copies of `public/` — with an hour's cache, before the\n\t * catch-all. Ignored in `development`, where Vite serves them.\n\t */\n\treadonly client?: string | URL;\n}\n\n/** The methods the catch-all takes. A `HEAD` reaches its `GET`. */\nconst METHODS = ['get', 'post', 'put', 'patch', 'delete'] as const;\n\n/** The handlers `reactRouter()` declared: the catch-all's and the client build's. */\nconst declared = new WeakSet<RouteDefinition['handler']>();\n\n/**\n * Whether `reactRouter()` declared this route: the catch-all, or one of the\n * client build's files. For `@alxia/openapi`'s `matchesSpec`, whose\n * `exclude` leaves them out, so the app's routes are checked against the\n * operations of its OpenAPI document and its pages are not.\n *\n * ```ts\n * matchesSpec(app, operations, { exclude: isReactRouterRoute });\n * ```\n */\nexport function isReactRouterRoute(route: RouteDefinition): boolean {\n\treturn declared.has(route.handler);\n}\n\n/**\n * A React Router framework app, server rendered on `app`: `GET`, `POST`,\n * `PUT`, `PATCH` and `DELETE` at `/*`, behind every middleware declared on `app`\n * before it. Each loader, action and middleware reads what those added\n * through `alxiaOf<App>(context)`. The app's own routes — an `/api`\n * — answer their paths, declared before it or after; the client build's\n * files are served when `client` is given.\n *\n * ```ts\n * const app = base.plugin((app) =>\n * reactRouter(app, { build: () => import('./build/server/index.js'), client: 'build/client' }),\n * );\n * ```\n */\nexport function reactRouter<\n\tCtx extends object,\n\tPrefix extends string,\n\tShortcuts extends AnyReply,\n>(\n\tapp: Alxia<Ctx, Prefix, Shortcuts>,\n\toptions: ReactRouterOptions<Ctx>,\n): Alxia<Ctx, Prefix, Shortcuts> {\n\tconst mode = options.mode ?? 'production';\n\tconst handle = requestHandler(options.build, mode);\n\tconst getLoadContext = options.getLoadContext;\n\tconst handler = async (ctx: BaseContext & Ctx) => {\n\t\tconst context = new RouterContextProvider();\n\t\tcontext.set(alxiaContext, ctx);\n\t\tawait getLoadContext?.(ctx, context);\n\t\t// React Router answers a HEAD with no headers at all: hand it the GET,\n\t\t// and the core drops the body.\n\t\tconst request =\n\t\t\tctx.request.method === 'HEAD'\n\t\t\t\t? new Request(ctx.request, { method: 'GET' })\n\t\t\t\t: ctx.request;\n\t\tconst response = await handle(request, context);\n\t\treturn ctx.reply(\n\t\t\tresponse.status as StatusCode,\n\t\t\tresponse.body ?? undefined,\n\t\t\t{ headers: response.headers },\n\t\t);\n\t};\n\n\tconst routes = asRoutes(app);\n\tdeclaring(app, () => {\n\t\tif (options.client !== undefined && mode === 'production') {\n\t\t\tserveClient(routes, pathOf(options.client));\n\t\t}\n\t\tfor (const method of METHODS) routes[method]('/*', handler);\n\t});\n\treturn app;\n}\n\n/**\n * Declares the client build's files on `app`, marked for\n * `isReactRouterRoute`: what `reactRouter()` does with `client`, apart, so\n * that `createServer()` serves them before the middlewares of `configure`.\n */\nexport function declareClient(app: AnyAlxia, client: string | URL): void {\n\tdeclaring(app, () => serveClient(asRoutes(app), pathOf(client)));\n}\n\n/** Runs `declare`, and marks every route it added to `app` as ours. */\nfunction declaring(app: AnyAlxia, declare: () => void): void {\n\tconst before = new Set(app.routes);\n\tdeclare();\n\tfor (const route of app.routes) {\n\t\tif (!before.has(route)) declared.add(route.handler);\n\t}\n}\n\n/** The app's route methods, untyped: the catch-all's handler is not one a route type admits. */\nfunction asRoutes(app: AnyAlxia) {\n\treturn app as unknown as {\n\t\tstatic(path: string, source: string, options: object): unknown;\n\t\tfile(path: string, file: string, options: object): unknown;\n\t} & Record<\n\t\t(typeof METHODS)[number],\n\t\t(path: string, handler: unknown) => unknown\n\t>;\n}\n\n/** A handler that resolves a function build once in production, on every request in development. */\nfunction requestHandler(\n\tbuild: ReactRouterOptions<never>['build'],\n\tmode: 'development' | 'production',\n): RequestHandler {\n\tif (typeof build !== 'function' || mode === 'development') {\n\t\treturn createRequestHandler(build, mode);\n\t}\n\tlet resolved: Promise<RequestHandler> | undefined;\n\treturn async (request, context) => {\n\t\tresolved ??= Promise.resolve(build()).then(\n\t\t\t(server) => createRequestHandler(server, mode),\n\t\t\t(error: unknown) => {\n\t\t\t\tresolved = undefined;\n\t\t\t\tthrow error;\n\t\t\t},\n\t\t);\n\t\treturn (await resolved)(request, context);\n\t};\n}\n\nfunction pathOf(client: string | URL): string {\n\treturn client instanceof URL ? fileURLToPath(client) : client;\n}\n",
8
8
  "/**\n * The client build's files, served before the catch-all: the hashed\n * `assets/` immutable, every other top-level file or folder — what React\n * Router copied from `public/` — with an hour's cache.\n */\nimport { readdirSync, statSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/** A hashed asset never changes under its name. */\nexport const IMMUTABLE = 'public, max-age=31536000, immutable';\n/** A public file keeps its name across builds: revalidated after an hour. */\nexport const AN_HOUR = 'public, max-age=3600';\n\ninterface Serving {\n\tstatic(path: string, source: string, options: object): unknown;\n\tfile(path: string, file: string, options: object): unknown;\n}\n\n/** Declares the client build's routes on `app`. */\nexport function serveClient(app: Serving, client: string): void {\n\tif (!isDirectory(client)) {\n\t\tthrow new TypeError(\n\t\t\t`reactRouter(): client is ${client}, which is not a directory. Pass the client build, build/client by default.`,\n\t\t);\n\t}\n\tfor (const name of readdirSync(client).sort()) {\n\t\tif (name.startsWith('.')) continue;\n\t\tconst path = join(client, name);\n\t\tconst cacheControl = name === 'assets' ? IMMUTABLE : AN_HOUR;\n\t\t// Declared as a request's URL carries the name: `my file.pdf` is\n\t\t// `/my%20file.pdf`, `café.png` is `/caf%C3%A9.png`.\n\t\tconst route = new URL(`/${name}`, 'http://localhost').pathname;\n\t\ttry {\n\t\t\tif (isDirectory(path)) app.static(route, path, { cacheControl });\n\t\t\telse app.file(route, path, { cacheControl });\n\t\t} catch (error) {\n\t\t\tthrow new TypeError(\n\t\t\t\t`reactRouter(): ${path} cannot be served at a path of its own name; rename it. ${(error as Error).message}`,\n\t\t\t);\n\t\t}\n\t}\n}\n\nfunction isDirectory(path: string): boolean {\n\ttry {\n\t\treturn statSync(path).isDirectory();\n\t} catch {\n\t\treturn false;\n\t}\n}\n",
9
- "/**\n * `createServer()`: the alxia server of a React Router app, as\n * `@alxia/react-router/vite` builds it. The plugin hands it React Router's\n * build, the mode and the client folder; the app's own `app/server.ts`\n * customises the rest, or is not written at all.\n */\nimport {\n\ttype Alxia,\n\ttype AnyAlxia,\n\talxia,\n\ttype ContextOf,\n\ttype Empty,\n\ttype ListenOptions,\n\ttype MaybePromise,\n} from '@alxia/core';\nimport type { RouterContextProvider, ServerBuild } from 'react-router';\nimport { declareClient, reactRouter } from './react-router';\n\n/** An app as `alxia()` makes it: what `beforeAll`, or `configure`, receives. */\nexport type FreshApp = Alxia<Empty, Empty, '', never>;\n\n/**\n * What the Vite plugin hands the server, and a test passes to `create`:\n * React Router's server build, its mode, and the client build's folder.\n */\nexport interface ServerWiring {\n\t/**\n\t * React Router's server build, or a function that returns it: called on\n\t * every request in `development`, once in `production`.\n\t */\n\treadonly build: ServerBuild | (() => MaybePromise<ServerBuild>);\n\t/** `development` under `react-router dev`, `production` in a build, and by default. */\n\treadonly mode?: 'development' | 'production';\n\t/** The client build's folder, a path or a `file:` URL: served in `production`. */\n\treadonly client?: string | URL | undefined;\n}\n\nexport interface ServerOptions<Before extends AnyAlxia, App extends AnyAlxia> {\n\t/**\n\t * Runs first, on a new app: what is declared here runs before the\n\t * client's files too — a guard, a rate limit, a logger that should see\n\t * every request. Returns the app, so its types flow to `configure`.\n\t */\n\treadonly beforeAll?: (app: FreshApp) => Before;\n\t/**\n\t * The app the pages run behind: its plugins, its hooks, its `/api`.\n\t * Declared after the client's files and before the catch-all. Returns\n\t * the app: what it builds is what the loaders read through `alxiaOf`.\n\t */\n\treadonly configure?: (app: Before) => App;\n\t/**\n\t * Sets the app's own keys on React Router's context provider, `ctx`\n\t * typed by `configure`'s app. `alxiaContext` is always set.\n\t */\n\treadonly getLoadContext?: (\n\t\tctx: ContextOf<App>,\n\t\tcontext: RouterContextProvider,\n\t) => MaybePromise<void>;\n\t/** Overrides the plugin's server build: rarely wanted. */\n\treadonly build?: ServerWiring['build'];\n\t/** Overrides the plugin's mode, `development` in dev and `production` in a build. */\n\treadonly mode?: 'development' | 'production';\n\t/**\n\t * Overrides the plugin's client folder, `build/client` beside the built\n\t * server. `false` serves none of it: declare the files yourself, in\n\t * `beforeAll` or `configure`.\n\t */\n\treadonly client?: string | URL | false;\n\t/**\n\t * `listen`'s options for `bun build/server/index.js`; a `port` or `hostname` here wins over `PORT` (3000)\n\t * and `HOST` (`0.0.0.0`) from the environment.\n\t */\n\treadonly listen?: ListenOptions;\n\t/**\n\t * Called once the built server listens and its `SIGINT` and `SIGTERM` handlers are in place.\n\t * Prints `alxia listening on <url>` by default.\n\t */\n\treadonly onListen?: (server: Bun.Server<unknown>) => void;\n}\n\n/**\n * The server `createServer()` describes, made into an app by the Vite\n * plugin, by a test, or by a server file of your own.\n */\nexport interface ReactRouterServer<App extends AnyAlxia> {\n\t/**\n\t * The app, serving React Router's build: `build/server/index.js`'s\n\t * default export. A test drives it with `app.request`.\n\t *\n\t * ```ts\n\t * const app = server.create({ build: await import('./build/server/index.js') });\n\t * ```\n\t */\n\tcreate(wiring: ServerWiring): App;\n\t/**\n\t * Listens with `app`, on `listen`, `PORT` and `HOST`, and stops it on\n\t * `SIGINT` or `SIGTERM`: its `onStop` hooks run, and the process exits.\n\t * What `bun build/server/index.js` runs.\n\t */\n\tstart(app: App): Bun.Server<unknown>;\n}\n\n/**\n * The alxia server of a React Router app, for `app/server.ts`. Every option\n * is optional: the Vite plugin wires React Router's build, the mode and the\n * client folder, and without the file uses `createServer()` as it is.\n *\n * ```ts\n * const server = createServer({\n * configure: (app) => app.use(logger()).get('/api/health', ({ reply }) => reply.ok({ ok: true })),\n * });\n * export default server;\n * ```\n *\n * The request runs through `beforeAll`, the client's files, `configure`,\n * then the pages.\n */\nexport function createServer<\n\tBefore extends AnyAlxia = FreshApp,\n\tApp extends AnyAlxia = Before,\n>(options: ServerOptions<Before, App> = {}): ReactRouterServer<App> {\n\treturn {\n\t\tcreate(wiring) {\n\t\t\tconst mode = options.mode ?? wiring.mode ?? 'production';\n\t\t\tconst client =\n\t\t\t\toptions.client === false\n\t\t\t\t\t? undefined\n\t\t\t\t\t: (options.client ?? wiring.client);\n\t\t\tconst fresh = alxia();\n\t\t\t// Without beforeAll or configure, Before and App are their defaults,\n\t\t\t// the app passed through: a type argument given by hand is believed.\n\t\t\tconst before = (options.beforeAll?.(fresh) ?? fresh) as Before;\n\t\t\tif (client !== undefined && mode === 'production') {\n\t\t\t\tdeclareClient(before, client);\n\t\t\t}\n\t\t\tconst app = (options.configure?.(before) ?? before) as App;\n\t\t\tconst getLoadContext = options.getLoadContext;\n\t\t\treactRouter(app, {\n\t\t\t\tbuild: options.build ?? wiring.build,\n\t\t\t\tmode,\n\t\t\t\t...(getLoadContext === undefined\n\t\t\t\t\t? {}\n\t\t\t\t\t: {\n\t\t\t\t\t\t\tgetLoadContext: (ctx, context) =>\n\t\t\t\t\t\t\t\tgetLoadContext(ctx as ContextOf<App>, context),\n\t\t\t\t\t\t}),\n\t\t\t});\n\t\t\treturn app;\n\t\t},\n\t\tstart(app) {\n\t\t\tconst server = app.listen({\n\t\t\t\tport: Number(process.env['PORT'] || 3000),\n\t\t\t\thostname: process.env['HOST'] || '0.0.0.0',\n\t\t\t\t...options.listen,\n\t\t\t});\n\t\t\t// Stop as the platform asks: the app's onStop hooks run, and the process ends.\n\t\t\t// Installed before onListen: a supervisor may signal as soon as it reads\n\t\t\t// that the server listens, and a signal with no handler yet kills the process.\n\t\t\tfor (const signal of ['SIGINT', 'SIGTERM'] as const) {\n\t\t\t\tprocess.once(signal, () => {\n\t\t\t\t\tvoid app.stop().then(\n\t\t\t\t\t\t() => process.exit(0),\n\t\t\t\t\t\t(error: unknown) => {\n\t\t\t\t\t\t\tconsole.error(error);\n\t\t\t\t\t\t\tprocess.exit(1);\n\t\t\t\t\t\t},\n\t\t\t\t\t);\n\t\t\t\t});\n\t\t\t}\n\t\t\t(options.onListen ?? announce)(server);\n\t\t\treturn server;\n\t\t},\n\t};\n}\n\nfunction announce(server: Bun.Server<unknown>): void {\n\tconsole.log(`alxia listening on ${server.url}`);\n}\n"
9
+ "/**\n * `createServer()`: the alxia server of a React Router app, as\n * `@alxia/react-router/vite` builds it. The plugin hands it React Router's\n * build, the mode and the client folder; the app's own `app/server.ts`\n * customises the rest, or is not written at all.\n */\nimport {\n\ttype Alxia,\n\ttype AnyAlxia,\n\talxia,\n\ttype ContextOf,\n\ttype Empty,\n\ttype ListenOptions,\n\ttype MaybePromise,\n} from '@alxia/core';\nimport type { RouterContextProvider, ServerBuild } from 'react-router';\nimport { declareClient, reactRouter } from './react-router';\n\n/** An app as `alxia()` makes it: what `beforeAll`, or `configure`, receives. */\nexport type FreshApp = Alxia<Empty, '', never>;\n\n/**\n * What the Vite plugin hands the server, and a test passes to `create`:\n * React Router's server build, its mode, and the client build's folder.\n */\nexport interface ServerWiring {\n\t/**\n\t * React Router's server build, or a function that returns it: called on\n\t * every request in `development`, once in `production`.\n\t */\n\treadonly build: ServerBuild | (() => MaybePromise<ServerBuild>);\n\t/** `development` under `react-router dev`, `production` in a build, and by default. */\n\treadonly mode?: 'development' | 'production';\n\t/** The client build's folder, a path or a `file:` URL: served in `production`. */\n\treadonly client?: string | URL | undefined;\n}\n\nexport interface ServerOptions<Before extends AnyAlxia, App extends AnyAlxia> {\n\t/**\n\t * Runs first, on a new app: what is declared here runs before the\n\t * client's files too — a guard, a rate limit, a logger that should see\n\t * every request. Returns the app, so its types flow to `configure`.\n\t */\n\treadonly beforeAll?: (app: FreshApp) => Before;\n\t/**\n\t * The app the pages run behind: its middlewares, its plugins, its `/api`.\n\t * Declared after the client's files and before the catch-all. Returns\n\t * the app: what it builds is what the loaders read through `alxiaOf`.\n\t */\n\treadonly configure?: (app: Before) => App;\n\t/**\n\t * Sets the app's own keys on React Router's context provider, `ctx`\n\t * typed by `configure`'s app. `alxiaContext` is always set.\n\t */\n\treadonly getLoadContext?: (\n\t\tctx: ContextOf<App>,\n\t\tcontext: RouterContextProvider,\n\t) => MaybePromise<void>;\n\t/** Overrides the plugin's server build: rarely wanted. */\n\treadonly build?: ServerWiring['build'];\n\t/** Overrides the plugin's mode, `development` in dev and `production` in a build. */\n\treadonly mode?: 'development' | 'production';\n\t/**\n\t * Overrides the plugin's client folder, `build/client` beside the built\n\t * server. `false` serves none of it: declare the files yourself, in\n\t * `beforeAll` or `configure`.\n\t */\n\treadonly client?: string | URL | false;\n\t/**\n\t * `listen`'s options for `bun build/server/index.js`; a `port` or `hostname` here wins over `PORT` (3000)\n\t * and `HOST` (`0.0.0.0`) from the environment.\n\t */\n\treadonly listen?: ListenOptions;\n\t/**\n\t * Called once the built server listens and its `SIGINT` and `SIGTERM` handlers are in place.\n\t * Prints `alxia listening on <url>` by default.\n\t */\n\treadonly onListen?: (server: Bun.Server<unknown>) => void;\n}\n\n/**\n * The server `createServer()` describes, made into an app by the Vite\n * plugin, by a test, or by a server file of your own.\n */\nexport interface ReactRouterServer<App extends AnyAlxia> {\n\t/**\n\t * The app, serving React Router's build: `build/server/index.js`'s\n\t * default export. A test drives it with `app.request`.\n\t *\n\t * ```ts\n\t * const app = server.create({ build: await import('./build/server/index.js') });\n\t * ```\n\t */\n\tcreate(wiring: ServerWiring): App;\n\t/**\n\t * Listens with `app`, on `listen`, `PORT` and `HOST`, and stops it on\n\t * `SIGINT` or `SIGTERM`: its `onStop` hooks run, and the process exits.\n\t * What `bun build/server/index.js` runs.\n\t */\n\tstart(app: App): Bun.Server<unknown>;\n}\n\n/**\n * The alxia server of a React Router app, for `app/server.ts`. Every option\n * is optional: the Vite plugin wires React Router's build, the mode and the\n * client folder, and without the file uses `createServer()` as it is.\n *\n * ```ts\n * const server = createServer({\n * configure: (app) => app.use(logger()).get('/api/health', ({ reply }) => reply.ok({ ok: true })),\n * });\n * export default server;\n * ```\n *\n * The request runs through `beforeAll`, the client's files, `configure`,\n * then the pages.\n */\nexport function createServer<\n\tBefore extends AnyAlxia = FreshApp,\n\tApp extends AnyAlxia = Before,\n>(options: ServerOptions<Before, App> = {}): ReactRouterServer<App> {\n\treturn {\n\t\tcreate(wiring) {\n\t\t\tconst mode = options.mode ?? wiring.mode ?? 'production';\n\t\t\tconst client =\n\t\t\t\toptions.client === false\n\t\t\t\t\t? undefined\n\t\t\t\t\t: (options.client ?? wiring.client);\n\t\t\tconst fresh = alxia();\n\t\t\t// Without beforeAll or configure, Before and App are their defaults,\n\t\t\t// the app passed through: a type argument given by hand is believed.\n\t\t\tconst before = (options.beforeAll?.(fresh) ?? fresh) as Before;\n\t\t\tif (client !== undefined && mode === 'production') {\n\t\t\t\tdeclareClient(before, client);\n\t\t\t}\n\t\t\tconst app = (options.configure?.(before) ?? before) as App;\n\t\t\tconst getLoadContext = options.getLoadContext;\n\t\t\treactRouter(app, {\n\t\t\t\tbuild: options.build ?? wiring.build,\n\t\t\t\tmode,\n\t\t\t\t...(getLoadContext === undefined\n\t\t\t\t\t? {}\n\t\t\t\t\t: {\n\t\t\t\t\t\t\tgetLoadContext: (ctx, context) =>\n\t\t\t\t\t\t\t\tgetLoadContext(ctx as ContextOf<App>, context),\n\t\t\t\t\t\t}),\n\t\t\t});\n\t\t\treturn app;\n\t\t},\n\t\tstart(app) {\n\t\t\tconst server = app.listen({\n\t\t\t\tport: Number(process.env['PORT'] || 3000),\n\t\t\t\thostname: process.env['HOST'] || '0.0.0.0',\n\t\t\t\t...options.listen,\n\t\t\t});\n\t\t\t// Stop as the platform asks: the app's onStop hooks run, and the process ends.\n\t\t\t// Installed before onListen: a supervisor may signal as soon as it reads\n\t\t\t// that the server listens, and a signal with no handler yet kills the process.\n\t\t\tfor (const signal of ['SIGINT', 'SIGTERM'] as const) {\n\t\t\t\tprocess.once(signal, () => {\n\t\t\t\t\tvoid app.stop().then(\n\t\t\t\t\t\t() => process.exit(0),\n\t\t\t\t\t\t(error: unknown) => {\n\t\t\t\t\t\t\tconsole.error(error);\n\t\t\t\t\t\t\tprocess.exit(1);\n\t\t\t\t\t\t},\n\t\t\t\t\t);\n\t\t\t\t});\n\t\t\t}\n\t\t\t(options.onListen ?? announce)(server);\n\t\t\treturn server;\n\t\t},\n\t};\n}\n\nfunction announce(server: Bun.Server<unknown>): void {\n\tconsole.log(`alxia listening on ${server.url}`);\n}\n"
10
10
  ],
11
- "mappings": "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",
11
+ "mappings": "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",
12
12
  "debugId": "63BB3B2311BA25F964756E2164756E21",
13
13
  "names": []
14
14
  }
@@ -13,8 +13,8 @@ export interface ReactRouterOptions<Ctx> {
13
13
  readonly mode?: 'development' | 'production';
14
14
  /**
15
15
  * Sets the app's own keys on React Router's context provider, from the
16
- * context alxia's hooks built. `ctx` is typed by the app at the point of
17
- * `use`: reading what no hook before it derives is a compile error.
16
+ * context alxia's middlewares built. `ctx` is typed by the app at the point of
17
+ * `app.plugin`: reading what nothing before it adds is a compile error.
18
18
  * `alxiaContext` is always set, whether or not this is given.
19
19
  */
20
20
  readonly getLoadContext?: (ctx: BaseContext & Ctx, context: RouterContextProvider) => MaybePromise<void>;
@@ -28,34 +28,34 @@ export interface ReactRouterOptions<Ctx> {
28
28
  }
29
29
  /**
30
30
  * Whether `reactRouter()` declared this route: the catch-all, or one of the
31
- * client build's files. For `@alxia/openapi`'s `exclude`, so the document
32
- * lists the app's API and not its pages.
31
+ * client build's files. For `@alxia/openapi`'s `matchesSpec`, whose
32
+ * `exclude` leaves them out, so the app's routes are checked against the
33
+ * operations of its OpenAPI document and its pages are not.
33
34
  *
34
35
  * ```ts
35
- * app.use(docs(app, { info, exclude: isReactRouterRoute }));
36
+ * matchesSpec(app, operations, { exclude: isReactRouterRoute });
36
37
  * ```
37
38
  */
38
39
  export declare function isReactRouterRoute(route: RouteDefinition): boolean;
39
40
  /**
40
41
  * A React Router framework app, server rendered on `app`: `GET`, `POST`,
41
- * `PUT`, `PATCH` and `DELETE` at `/*`, behind every hook declared on `app`
42
- * before it. Each loader, action and middleware reads what those hooks
43
- * built through `alxiaOf<App>(context)`. The app's own routes — an `/api`
42
+ * `PUT`, `PATCH` and `DELETE` at `/*`, behind every middleware declared on `app`
43
+ * before it. Each loader, action and middleware reads what those added
44
+ * through `alxiaOf<App>(context)`. The app's own routes — an `/api`
44
45
  * — answer their paths, declared before it or after; the client build's
45
- * files are served when `client` is given. The catch-all adds nothing to
46
- * the app's route table: pages are not something the typed client calls.
46
+ * files are served when `client` is given.
47
47
  *
48
48
  * ```ts
49
- * const app = base.use((app) =>
49
+ * const app = base.plugin((app) =>
50
50
  * reactRouter(app, { build: () => import('./build/server/index.js'), client: 'build/client' }),
51
51
  * );
52
52
  * ```
53
53
  */
54
- export declare function reactRouter<Ctx extends object, Routes extends object, Prefix extends string, Shortcuts extends AnyReply>(app: Alxia<Ctx, Routes, Prefix, Shortcuts>, options: ReactRouterOptions<Ctx>): Alxia<Ctx, Routes, Prefix, Shortcuts>;
54
+ export declare function reactRouter<Ctx extends object, Prefix extends string, Shortcuts extends AnyReply>(app: Alxia<Ctx, Prefix, Shortcuts>, options: ReactRouterOptions<Ctx>): Alxia<Ctx, Prefix, Shortcuts>;
55
55
  /**
56
56
  * Declares the client build's files on `app`, marked for
57
57
  * `isReactRouterRoute`: what `reactRouter()` does with `client`, apart, so
58
- * that `createServer()` serves them before the hooks of `configure`.
58
+ * that `createServer()` serves them before the middlewares of `configure`.
59
59
  */
60
60
  export declare function declareClient(app: AnyAlxia, client: string | URL): void;
61
61
  //# sourceMappingURL=react-router.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"react-router.d.ts","sourceRoot":"","sources":["../src/react-router.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,eAAe,EAEf,MAAM,aAAa,CAAC;AACrB,OAAO,EAGN,qBAAqB,EACrB,KAAK,WAAW,EAChB,MAAM,cAAc,CAAC;AAItB,MAAM,WAAW,kBAAkB,CAAC,GAAG;IACtC;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,CAAC,MAAM,YAAY,CAAC,WAAW,CAAC,CAAC,CAAC;IAChE,2DAA2D;IAC3D,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,GAAG,YAAY,CAAC;IAC7C;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,CACzB,GAAG,EAAE,WAAW,GAAG,GAAG,EACtB,OAAO,EAAE,qBAAqB,KAC1B,YAAY,CAAC,IAAI,CAAC,CAAC;IACxB;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;CAC/B;AAQD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAElE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CAC1B,GAAG,SAAS,MAAM,EAClB,MAAM,SAAS,MAAM,EACrB,MAAM,SAAS,MAAM,EACrB,SAAS,SAAS,QAAQ,EAE1B,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC,EAC1C,OAAO,EAAE,kBAAkB,CAAC,GAAG,CAAC,GAC9B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC,CA8BvC;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI,CAEvE"}
1
+ {"version":3,"file":"react-router.d.ts","sourceRoot":"","sources":["../src/react-router.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,eAAe,EAEf,MAAM,aAAa,CAAC;AACrB,OAAO,EAGN,qBAAqB,EACrB,KAAK,WAAW,EAChB,MAAM,cAAc,CAAC;AAItB,MAAM,WAAW,kBAAkB,CAAC,GAAG;IACtC;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,CAAC,MAAM,YAAY,CAAC,WAAW,CAAC,CAAC,CAAC;IAChE,2DAA2D;IAC3D,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,GAAG,YAAY,CAAC;IAC7C;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,CACzB,GAAG,EAAE,WAAW,GAAG,GAAG,EACtB,OAAO,EAAE,qBAAqB,KAC1B,YAAY,CAAC,IAAI,CAAC,CAAC;IACxB;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;CAC/B;AAQD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAElE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAC1B,GAAG,SAAS,MAAM,EAClB,MAAM,SAAS,MAAM,EACrB,SAAS,SAAS,QAAQ,EAE1B,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAClC,OAAO,EAAE,kBAAkB,CAAC,GAAG,CAAC,GAC9B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,CA8B/B;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI,CAEvE"}
package/dist/server.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  import { type Alxia, type AnyAlxia, type ContextOf, type Empty, type ListenOptions, type MaybePromise } from '@alxia/core';
8
8
  import type { RouterContextProvider, ServerBuild } from 'react-router';
9
9
  /** An app as `alxia()` makes it: what `beforeAll`, or `configure`, receives. */
10
- export type FreshApp = Alxia<Empty, Empty, '', never>;
10
+ export type FreshApp = Alxia<Empty, '', never>;
11
11
  /**
12
12
  * What the Vite plugin hands the server, and a test passes to `create`:
13
13
  * React Router's server build, its mode, and the client build's folder.
@@ -31,7 +31,7 @@ export interface ServerOptions<Before extends AnyAlxia, App extends AnyAlxia> {
31
31
  */
32
32
  readonly beforeAll?: (app: FreshApp) => Before;
33
33
  /**
34
- * The app the pages run behind: its plugins, its hooks, its `/api`.
34
+ * The app the pages run behind: its middlewares, its plugins, its `/api`.
35
35
  * Declared after the client's files and before the catch-all. Returns
36
36
  * the app: what it builds is what the loaders read through `alxiaOf`.
37
37
  */
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACN,KAAK,KAAK,EACV,KAAK,QAAQ,EAEb,KAAK,SAAS,EACd,KAAK,KAAK,EACV,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,qBAAqB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGvE,gFAAgF;AAChF,MAAM,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC;AAEtD;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC5B;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,CAAC,MAAM,YAAY,CAAC,WAAW,CAAC,CAAC,CAAC;IAChE,uFAAuF;IACvF,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,GAAG,YAAY,CAAC;IAC7C,kFAAkF;IAClF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS,CAAC;CAC3C;AAED,MAAM,WAAW,aAAa,CAAC,MAAM,SAAS,QAAQ,EAAE,GAAG,SAAS,QAAQ;IAC3E;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,MAAM,CAAC;IAC/C;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,GAAG,CAAC;IAC1C;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,CACzB,GAAG,EAAE,SAAS,CAAC,GAAG,CAAC,EACnB,OAAO,EAAE,qBAAqB,KAC1B,YAAY,CAAC,IAAI,CAAC,CAAC;IACxB,0DAA0D;IAC1D,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC;IACvC,qFAAqF;IACrF,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,GAAG,YAAY,CAAC;IAC7C;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,GAAG,GAAG,KAAK,CAAC;IACvC;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;CAC1D;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB,CAAC,GAAG,SAAS,QAAQ;IACtD;;;;;;;OAOG;IACH,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,GAAG,CAAC;IAClC;;;;OAIG;IACH,KAAK,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;CACrC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAC3B,MAAM,SAAS,QAAQ,GAAG,QAAQ,EAClC,GAAG,SAAS,QAAQ,GAAG,MAAM,EAC5B,OAAO,GAAE,aAAa,CAAC,MAAM,EAAE,GAAG,CAAM,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAqDlE"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACN,KAAK,KAAK,EACV,KAAK,QAAQ,EAEb,KAAK,SAAS,EACd,KAAK,KAAK,EACV,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,qBAAqB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGvE,gFAAgF;AAChF,MAAM,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC;AAE/C;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC5B;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,CAAC,MAAM,YAAY,CAAC,WAAW,CAAC,CAAC,CAAC;IAChE,uFAAuF;IACvF,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,GAAG,YAAY,CAAC;IAC7C,kFAAkF;IAClF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS,CAAC;CAC3C;AAED,MAAM,WAAW,aAAa,CAAC,MAAM,SAAS,QAAQ,EAAE,GAAG,SAAS,QAAQ;IAC3E;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,MAAM,CAAC;IAC/C;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,GAAG,CAAC;IAC1C;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,CACzB,GAAG,EAAE,SAAS,CAAC,GAAG,CAAC,EACnB,OAAO,EAAE,qBAAqB,KAC1B,YAAY,CAAC,IAAI,CAAC,CAAC;IACxB,0DAA0D;IAC1D,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC;IACvC,qFAAqF;IACrF,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,GAAG,YAAY,CAAC;IAC7C;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,GAAG,GAAG,KAAK,CAAC;IACvC;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;CAC1D;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB,CAAC,GAAG,SAAS,QAAQ;IACtD;;;;;;;OAOG;IACH,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,GAAG,CAAC;IAClC;;;;OAIG;IACH,KAAK,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;CACrC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAC3B,MAAM,SAAS,QAAQ,GAAG,QAAQ,EAClC,GAAG,SAAS,QAAQ,GAAG,MAAM,EAC5B,OAAO,GAAE,aAAa,CAAC,MAAM,EAAE,GAAG,CAAM,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAqDlE"}
package/docs/README.md CHANGED
@@ -7,6 +7,6 @@ do with each message.
7
7
 
8
8
  | Page | Read it when |
9
9
  | --- | --- |
10
- | [Guide](guide.md) | adding alxia to the official template, revealing the default server with `bunx alxia-react-router reveal`, customising the server with `createServer()`, typing the loaders with `Register` or a type argument, the app's own context keys, what `vite preview` and prerendering serve, what the plugin sets so the server is built for Bun, the escape hatches (another entry, overriding the wiring, a server of your own), what runs around the pages, WebSockets in dev, in preview and in the build, the client's files, OpenAPI, testing, or deploying, with the Docker image |
10
+ | [Guide](guide.md) | starting a new app with `bun create @alxia --template react-router`, adding alxia to the official template or an existing app, revealing the default server with `bunx alxia-react-router reveal`, customising the server with `createServer()`, typing the loaders with `Register` or a type argument, the app's own context keys, what `vite preview` and prerendering serve, what the plugin sets so the server is built for Bun, the escape hatches (another entry, overriding the wiring, a server of your own), what runs around the pages, WebSockets in dev, in preview and in the build, the client's files, OpenAPI, testing, or deploying, with the Docker image |
11
11
  | [Troubleshooting](troubleshooting.md) | something went wrong and you have the message, or a page streams, hydrates or reads its context unlike you expected |
12
12
  | [Roadmap](roadmap.md) | wondering what is coming, and what is not planned |
package/docs/guide.md CHANGED
@@ -16,7 +16,7 @@ deploying.
16
16
  - [The app's own context keys](#the-apps-own-context-keys)
17
17
  - [A CSP nonce](#a-csp-nonce)
18
18
  - [Escape hatches](#escape-hatches)
19
- - [Hooks around the pages](#hooks-around-the-pages)
19
+ - [Middlewares around the pages](#middlewares-around-the-pages)
20
20
  - [Routes beside the pages](#routes-beside-the-pages)
21
21
  - [WebSockets](#websockets)
22
22
  - [The client's files](#the-clients-files)
@@ -26,7 +26,35 @@ deploying.
26
26
 
27
27
  ## Setup
28
28
 
29
- Start from React Router's official template:
29
+ ### A new app: `bun create @alxia`
30
+
31
+ ```sh
32
+ bun create @alxia my-app --template react-router
33
+ cd my-app
34
+ bun dev
35
+ ```
36
+
37
+ [`@alxia/create`](https://www.npmjs.com/package/@alxia/create) copies React
38
+ Router's official template with alxia's layer already applied: the steps
39
+ of the next section, done.
40
+
41
+ - `@alxia/core` and `@alxia/react-router` installed, and every dependency
42
+ moved to the newest release alxia accepts (see
43
+ [`@alxia/create`'s Versions](https://github.com/softistx/alxia/blob/develop/packages/create/docs/guide.md#versions));
44
+ - `alxia()` in `vite.config.ts`, the `bunfig.toml`, and `start` running
45
+ `bun build/server/index.js`;
46
+ - Biome, with `lint`, `format`, `check`, `check:ci` and `verify` scripts;
47
+ - a `Dockerfile` that builds on `oven/bun:1` and runs `build/` alone on
48
+ `oven/bun:1-alpine`, as [Deploying](#deploying) describes.
49
+
50
+ `bun run verify` checks, typechecks and builds it.
51
+ [`@alxia/create`'s guide](https://github.com/softistx/alxia/blob/develop/packages/create/docs/guide.md#running-it)
52
+ lists the command's options. The template keeps `@react-router/serve` in its
53
+ dependencies, unused: remove it if you like.
54
+
55
+ ### An existing React Router app
56
+
57
+ Start from React Router's official template, or your own app:
30
58
 
31
59
  ```sh
32
60
  bunx create-react-router@latest my-app
@@ -96,9 +124,10 @@ What the template keeps:
96
124
  - `@react-router/node`, which React Router's default `entry.server`
97
125
  renders with: it streams under Bun.
98
126
  - Vite 7 or 8 (the template ships 8).
99
- - Its `typescript` 5.9. This package's peer asks for 6 or 7, so `bun add`
100
- warns, but the template's code typechecks with 5.9.
101
- `bun add -d typescript@^6` silences the warning.
127
+ - With `bunx create-react-router`, its `typescript` 5.9. This package's
128
+ peer asks for 6 or 7, so `bun add` warns, but the template's code
129
+ typechecks with 5.9. `bun add -d typescript@^6` silences the warning;
130
+ `bun create @alxia` writes 6 or 7 already.
102
131
 
103
132
  React Router 8 is required: its loaders receive a `RouterContextProvider`,
104
133
  and alxia's context is set on it.
@@ -188,7 +217,7 @@ The plugin loads `build/server/index.js` on the first request and hands
188
217
  every request to its default export, the alxia app, before Vite's own
189
218
  files. So the preview answers as `bun run start` does: the pages and their
190
219
  data, `/api`, `build/client` with the cache headers of
191
- [the client's files](#the-clients-files), and every hook of `beforeAll`
220
+ [the client's files](#the-clients-files), and every middleware of `beforeAll`
192
221
  and `configure`. Requests, their bodies and every `Set-Cookie` pass
193
222
  through whole, and pages stream.
194
223
 
@@ -212,7 +241,7 @@ React Router prerenders the `prerender` paths of `react-router.config.ts`
212
241
  through the same preview server, during `react-router build`. A
213
242
  prerendered page is therefore rendered by the built server too: its loader
214
243
  reads `alxiaOf(context)` and `getLoadContext`'s keys, and `beforeAll`'s
215
- and `configure`'s hooks run around it.
244
+ and `configure`'s middlewares run around it.
216
245
 
217
246
  ## Built for Bun
218
247
 
@@ -368,7 +397,7 @@ bunx alxia-react-router reveal
368
397
 
369
398
  ```text
370
399
  alxia-react-router: wrote app/server.ts, the server alxia() runs by default.
371
- Next: uncomment configure in app/server.ts to add the app's hooks and /api; bun run dev picks it up.
400
+ Next: uncomment configure in app/server.ts to add the app's middlewares and /api; bun run dev picks it up.
372
401
  ```
373
402
 
374
403
  The file is the server the plugin runs without one, `createServer()`, so
@@ -427,7 +456,7 @@ React Router's own rendering entries, `app/entry.server.tsx` and
427
456
  ### By hand
428
457
 
429
458
  The example uses `@alxia/logger` and `@alxia/compress`
430
- (`bun add @alxia/logger @alxia/compress`); any plugin works the same way.
459
+ (`bun add @alxia/logger @alxia/compress`); any middleware works the same way.
431
460
 
432
461
  ```ts
433
462
  // app/server.ts
@@ -460,7 +489,7 @@ Every option is optional:
460
489
 
461
490
  | option | what it does |
462
491
  | --- | --- |
463
- | `configure(app)` | the app the pages run behind: its plugins, hooks and `/api`. It returns the app, and what it builds is what the loaders read |
492
+ | `configure(app)` | the app the pages run behind: its middlewares and `/api`. It returns the app, and what it builds is what the loaders read |
464
493
  | `beforeAll(app)` | runs first, on a new app. What it declares applies to the client's files too: a rate limit, a guard on everything, a logger that should see every asset. It returns the app, which `configure` then receives |
465
494
  | `getLoadContext(ctx, context)` | sets the app's own keys on React Router's provider, `ctx` typed by `configure`'s app |
466
495
  | `build`, `mode`, `client` | override what the plugin wires; see [Escape hatches](#escape-hatches) |
@@ -474,11 +503,16 @@ A request goes through four layers, in order:
474
503
  3. what `configure` declared;
475
504
  4. the pages: `GET`, `POST`, `PUT`, `PATCH` and `DELETE` at `/*`.
476
505
 
477
- Hooks apply to the routes declared after them, so a session or a guard in
478
- `configure` runs around every page and its data, but not around the
479
- JavaScript of the login page. Global hooks (`onRequest`, `@alxia/cors`,
480
- `@alxia/compress`, `@alxia/secure-headers`) apply everywhere, wherever
481
- they are declared.
506
+ A middleware given to `use` runs on every route declared after it, and on a
507
+ request no route matches. So a session or a guard in `configure` runs around
508
+ every page and its data, and not around what is declared before it: the
509
+ client's files, which sit between `beforeAll` and `configure`. What must see
510
+ every request, the client's files included, goes in `beforeAll`: the logger,
511
+ `@alxia/cors`, `@alxia/compress`, `@alxia/secure-headers`. A guard on the app
512
+ answers a request no route matches too: an anonymous request to a missing path
513
+ gets the 401, not the 404. Scope it with a path, `use('/app', guard)`
514
+ (a guard that adds nothing to the context: `bearer` and `session` add `user`),
515
+ to guard only some pages.
482
516
 
483
517
  ```ts
484
518
  // app/server.ts: a guard on everything, the assets included
@@ -495,13 +529,13 @@ export default createServer({
495
529
  ```
496
530
 
497
531
  `configure` and `beforeAll` must return the app; one that returns nothing
498
- is a compile error. A plugin that needs something an earlier one
532
+ is a compile error. A middleware that needs something an earlier one
499
533
  derived is typed by the app it is given, as anywhere in alxia.
500
534
 
501
535
  ## Typing the loaders
502
536
 
503
537
  Every request through the catch-all sets `alxiaContext`, this package's
504
- key, on React Router's context provider, to what alxia's hooks built for
538
+ key, on React Router's context provider, to what alxia's middlewares built for
505
539
  that request. `alxiaOf(context)` reads it.
506
540
 
507
541
  ### With `Register`: no type argument
@@ -546,7 +580,7 @@ shows. The checks it gives:
546
580
 
547
581
  ```ts
548
582
  const ctx = alxiaOf(context);
549
- ctx.tenant; // error: no hook derives `tenant`
583
+ ctx.tenant; // error: no middleware derives `tenant`
550
584
  ctx.user.name; // error: `user` may be null
551
585
  ```
552
586
 
@@ -577,19 +611,57 @@ export function loader({ context }: Route.LoaderArgs) {
577
611
 
578
612
  The type argument may also be an alxia app, the app *before* the
579
613
  catch-all, for a server of your own. Anything else is a compile error.
580
- With neither `Register` nor a type argument, `alxiaOf(context)` is
581
- `BaseContext`: the request, its URL, `reply` and the rest of what every
582
- hook reads.
614
+
615
+ ### With `@alxia/core`'s `Register`
616
+
617
+ `@alxia/core` has a `Register` of its own, which names the chain that
618
+ builds an app's context, for its route files
619
+ ([The app's type](https://github.com/softistx/alxia/blob/develop/packages/core/docs/guide/types.md#register-and-appcontext)).
620
+ `alxiaOf(context)` reads it when this package's `Register` names no
621
+ server:
622
+
623
+ ```ts
624
+ // app/context.ts
625
+ import { alxia } from '@alxia/core';
626
+
627
+ export const base = alxia().derive(({ request }) => ({ user: request.headers.get('x-user') }));
628
+
629
+ declare module '@alxia/core' {
630
+ interface Register {
631
+ context: typeof base;
632
+ }
633
+ }
634
+
635
+ // app/server.ts
636
+ const server = createServer({ configure: (app) => app.plugin(base) });
637
+ export default server;
638
+ ```
639
+
640
+ `alxiaOf(context).user` is then typed with no declaration in
641
+ `@alxia/react-router`. Which one is read, in order:
642
+
643
+ 1. a type argument, `alxiaOf<typeof server>(context)`;
644
+ 2. this package's `Register`, `server: typeof server`: the whole app the
645
+ pages run behind, `base` and everything `configure` adds after it;
646
+ 3. `@alxia/core`'s `Register`, `context: typeof base`: only what `base`
647
+ builds;
648
+ 4. neither: `BaseContext`, the request, its URL, `reply` and the rest of
649
+ what every middleware reads.
650
+
651
+ Declare this package's `Register` when `configure` adds to the context
652
+ after the base, and the loaders read it.
583
653
 
584
654
  ### Why a global augmentation is right here
585
655
 
586
- alxia's core refuses global augmentation of its context: a plugin that
587
- added `user` to every route, declared before it or after, would type
588
- `user` on routes that run before the plugin. That is the lie "order is
589
- meaning" forbids. `Register` here does something else: it names the
590
- **one** server of the React Router build, at the point of its catch-all,
591
- which is exactly what every loader runs behind. One build has one server
592
- entry, so there is no second app for a module to be confused with.
656
+ alxia's core refuses a global augmentation of a context key: a plugin
657
+ that added `user` to every route, declared before it or after, would
658
+ type `user` on routes that run before the plugin. That is the lie "order
659
+ is meaning" forbids. Core's own `Register` names the base that builds
660
+ the context instead, and what reads it requires it. `Register` here does
661
+ something else: it names the **one** server of the React Router build,
662
+ at the point of its catch-all, which is exactly what every loader runs
663
+ behind. One build has one server entry, so there is no second app for a
664
+ module to be confused with.
593
665
 
594
666
  If two React Router apps share one TypeScript program (one tsconfig over
595
667
  both folders of a monorepo), their two declarations conflict, and `tsc`
@@ -639,7 +711,7 @@ export function loader({ context }: Route.LoaderArgs) {
639
711
 
640
712
  `getLoadContext` runs before React Router, on every request, with
641
713
  `alxiaContext` already set. Its `ctx` is typed by `configure`'s app:
642
- reading something no hook derives is a compile error. Keep the keys in a
714
+ reading something no middleware derives is a compile error. Keep the keys in a
643
715
  module of their own, such as `app/context.ts`, rather than in
644
716
  `app/server.ts`: a route imports them, and the server file then stays out
645
717
  of the client's module graph.
@@ -729,7 +801,7 @@ scripts included.
729
801
  ### How it stays loose
730
802
 
731
803
  `nonceOf` reads `nonce` from alxia's context if it is a string, and
732
- returns `undefined` otherwise: no hook set one, or the request did not come
804
+ returns `undefined` otherwise: no middleware set one, or the request did not come
733
805
  through alxia at all. `ServerRouter` and React then render no `nonce`
734
806
  attribute. So:
735
807
 
@@ -816,7 +888,7 @@ export const base = alxia()
816
888
  /** What the loaders read: alxiaOf<Base>(context). */
817
889
  export type Base = typeof base;
818
890
 
819
- const app = base.use((app) =>
891
+ const app = base.plugin((app) =>
820
892
  reactRouter(app, {
821
893
  build: () =>
822
894
  import(new URL('./build/server/index.js', import.meta.url).href) as Promise<ServerBuild>,
@@ -838,7 +910,7 @@ bun run build && bun server.ts
838
910
  | `getLoadContext(ctx, context)` | as `createServer`'s |
839
911
  | `client` | the client build's folder, a path or a `file:` URL, served before the catch-all in `production` |
840
912
 
841
- `reactRouter()` goes through `use`, as `@alxia/graphql`'s `graphql(app, …)`
913
+ `reactRouter()` goes through `app.plugin`, as `@alxia/graphql`'s `graphql(app, …)`
842
914
  does: that is how it knows the app's context type. A `HEAD` is handed to
843
915
  React Router as a `GET`, since React Router answers a `HEAD` of its own
844
916
  with no headers at all; the core then drops the body. In a monorepo where
@@ -846,14 +918,15 @@ this package is linked rather than installed, add
846
918
  `ssr: { external: ['@alxia/react-router'] }` to `vite.config.ts`: see
847
919
  [the troubleshooting entry](troubleshooting.md#alxiaof-this-request-has-no-alxia-context-).
848
920
 
849
- ## Hooks around the pages
921
+ ## Middlewares around the pages
850
922
 
851
- Every hook declared before the catch-all runs around each page, as around
852
- any route:
923
+ Every middleware declared before the catch-all runs around each page, as around
924
+ any route, and `use` observers go first so they wrap everything:
853
925
 
854
926
  - **`@alxia/logger`** writes one entry per request and sets
855
927
  `x-request-id`. It times a streamed page by its first byte, since
856
- `onResponse` runs when the headers leave.
928
+ the response is settled when its headers leave. Every request is logged,
929
+ 404s and errors included.
857
930
  - **`@alxia/compress`** compresses documents and data, and flushes a
858
931
  streamed page as React writes it: the shell and its `<Suspense>`
859
932
  fallback still arrive first, in zstd, Brotli or gzip. See [the troubleshooting entry](troubleshooting.md#a-streamed-page-arrives-in-one-piece).
@@ -865,7 +938,8 @@ any route:
865
938
  [A CSP nonce](#a-csp-nonce).
866
939
  - **A guard** — `@alxia/jwt`'s `bearer`, `@alxia/janus`' session — in
867
940
  `configure` guards every page and its data alike; in `beforeAll`, the
868
- client's files too.
941
+ client's files too. On the app, a guard also answers a request no route
942
+ matches with its 401.
869
943
 
870
944
  ## Routes beside the pages
871
945
 
@@ -883,7 +957,7 @@ export default createServer({
883
957
  });
884
958
  ```
885
959
 
886
- Each answers with its own schemas, replies and typed client. The core ranks
960
+ Each answers with its own middlewares and replies. The core ranks
887
961
  paths as `Bun.serve` does, through `listen`, `app.fetch` and
888
962
  `app.request` alike: segment by segment, a literal beats a parameter,
889
963
  which beats the catch-all's wildcard. Two consequences:
@@ -894,10 +968,10 @@ which beats the catch-all's wildcard. Two consequences:
894
968
  every one-segment path, `/about` included, before React Router sees it.
895
969
  Put alxia's routes under a prefix of their own, `/api`.
896
970
 
897
- The catch-all adds nothing to the app's route table: `RoutesOf` and the
898
- typed client never show `/*`. Pages and single-fetch data are not
899
- something a typed client calls; what it does call, your `/api`, is typed
900
- as always.
971
+ Pages and single-fetch data are not something a generated client calls:
972
+ leave the catch-all out of `matchesSpec`'s check with `isReactRouterRoute`
973
+ ([OpenAPI](#openapi)); what a client does call, your `/api`, is what the
974
+ OpenAPI document declares.
901
975
 
902
976
  What React Router answers comes back as it sent it: documents,
903
977
  single-fetch data (`/_.data`, `/login.data`), lazy route discovery
@@ -943,10 +1017,10 @@ export default createServer({
943
1017
  });
944
1018
  ```
945
1019
 
946
- The upgrade runs the hooks declared before the route, and the route's
947
- validation, as any request does. A hook's reply refuses it: the client
1020
+ The upgrade runs the middlewares declared before the route, and the route's
1021
+ validation, as any request does. A middleware's reply refuses it: the client
948
1022
  gets the 401 and its body, and no socket opens. `socket.data` holds the
949
- validated request and what each hook derived, typed. See
1023
+ validated request and what each middleware derived, typed. See
950
1024
  [`@alxia/core`'s WebSockets](https://github.com/softistx/alxia/blob/develop/packages/core/docs/guide/websockets.md)
951
1025
  for the schemas and the rest of the socket.
952
1026
 
@@ -966,7 +1040,7 @@ byte for byte, to a `Bun.serve` of the app on a loopback port of its
966
1040
  own, started on the first upgrade and given
967
1041
  `app.fetch` and `app.websocket`, as `listen` gives them. So in dev:
968
1042
 
969
- - **The same hooks, refusals and handlers run** as from the build:
1043
+ - **The same middlewares, refusals and handlers run** as from the build:
970
1044
  `open`, `message`, `close`, `drain`, the `message` and `send` schemas,
971
1045
  `subscribe` and `publish`.
972
1046
  - **An edit to `app/server.ts`, or to a module it imports, is used from
@@ -1016,25 +1090,49 @@ refused when the server starts: see
1016
1090
 
1017
1091
  ## OpenAPI
1018
1092
 
1019
- `@alxia/openapi` documents every route of `app.routes`, and the catch-all
1020
- and the client's files are routes. Leave them out with
1093
+ alxia is OpenAPI spec first: the document is written by hand, the API's
1094
+ routes are bound to the operations generated from it, and
1095
+ [`@alxia/openapi`](https://www.npmjs.com/package/@alxia/openapi)'s
1096
+ `matchesSpec` checks, in a test, that `app.routes` and the operations
1097
+ match both ways. The catch-all and the client's files are routes of
1098
+ `app.routes`, and no operation of the document. Leave them out with
1021
1099
  `isReactRouterRoute`, which is true for each route this package declared:
1022
1100
 
1023
1101
  ```ts
1024
1102
  // app/server.ts
1025
- import { docs } from '@alxia/openapi';
1026
- import { createServer, isReactRouterRoute } from '@alxia/react-router';
1103
+ import { createServer } from '@alxia/react-router';
1104
+ import { operations } from './generated/alxia';
1027
1105
 
1028
1106
  export default createServer({
1029
1107
  configure: (app) =>
1030
1108
  app
1031
1109
  .get('/api/health', ({ reply }) => reply.ok({ ok: true }))
1032
- .use(docs(app, { info: { title: 'Shop', version: '1.0.0' }, exclude: isReactRouterRoute })),
1110
+ .route(operations.listOrders, ({ reply }) => reply.ok([])),
1111
+ });
1112
+ ```
1113
+
1114
+ ```ts
1115
+ // app/server.test.ts
1116
+ import { test } from 'bun:test';
1117
+ import { matchesSpec } from '@alxia/openapi';
1118
+ import { isReactRouterRoute } from '@alxia/react-router';
1119
+ import type { ServerBuild } from 'react-router';
1120
+ import { operations } from './generated/alxia';
1121
+ import server from './server';
1122
+
1123
+ test('the API routes every operation of openapi.yaml, and nothing else', async () => {
1124
+ const build: ServerBuild = await import(new URL('../build/server/index.js', import.meta.url).href);
1125
+ const app = server.create({ build });
1126
+ matchesSpec(app, operations, {
1127
+ exclude: (route) => isReactRouterRoute(route) || route.path === '/api/health',
1128
+ });
1033
1129
  });
1034
1130
  ```
1035
1131
 
1036
- Combine it with your own: `exclude: (route) => isReactRouterRoute(route) ||
1037
- route.path === '/api/health'`.
1132
+ When the app puts the operations under a prefix the document's paths
1133
+ leave out, `app.group('/api', (api) => api.route(…))`, give `matchesSpec`
1134
+ the same `prefix: '/api'`. See
1135
+ [`@alxia/openapi`'s checks](https://github.com/softistx/alxia/blob/develop/packages/openapi/docs/guide/checks.md).
1038
1136
 
1039
1137
  ## Testing
1040
1138
 
@@ -1083,7 +1181,8 @@ no `node_modules`, unless the app keeps a package external with
1083
1181
  The `react-router` template of
1084
1182
  [`@alxia/create`](https://github.com/softistx/alxia/tree/develop/packages/create)
1085
1183
  and the [example](https://github.com/softistx/alxia/tree/develop/examples/react-router)
1086
- ship this `Dockerfile`, on Bun's official image. For an app started from
1184
+ ship this `Dockerfile`, on Bun's official images: built on `oven/bun:1`,
1185
+ Debian's, and run on `oven/bun:1-alpine`, about 130 MB. For an app started from
1087
1186
  `create-react-router`, it replaces the template's, which builds and runs
1088
1187
  on Node:
1089
1188
 
@@ -1106,8 +1205,10 @@ RUN bun run build
1106
1205
  # command, and --no-install: with no node_modules, Bun would otherwise
1107
1206
  # fetch a package the build left out from the registry at startup,
1108
1207
  # where this fails.
1109
- # The server listens on PORT (3000) and HOST (0.0.0.0).
1110
- FROM oven/bun:1
1208
+ # The server listens on PORT (3000) and HOST (0.0.0.0). On Alpine, about
1209
+ # 200 MB lighter: build/ is JavaScript, which runs the same on musl. A
1210
+ # native addon built for glibc alone needs oven/bun:1 here.
1211
+ FROM oven/bun:1-alpine
1111
1212
  WORKDIR /app
1112
1213
  ENV NODE_ENV=production
1113
1214
  COPY --from=build /app/build ./build
@@ -1133,6 +1234,11 @@ startup, where Bun would otherwise fetch it from npm.
1133
1234
  gets the versions you tested, and a `package.json` changed without a
1134
1235
  `bun install` fails the build
1135
1236
  ([troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md#error-lockfile-had-changes-but-lockfile-is-frozen)).
1237
+ - **The final stage is Alpine's**, musl where the build stage has glibc.
1238
+ `build/` is JavaScript, which runs the same on both. A native addon
1239
+ kept external and built for glibc alone does not load there: put the
1240
+ final stage back on `oven/bun:1`
1241
+ ([`@alxia/create`'s troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/create/docs/troubleshooting.md#error--is-linked-against-glibc-dt_needed-libmso6-but-this-bun-build-uses-musl)).
1136
1242
  - **The server runs as `bun`**, a user with no write access to `/app`.
1137
1243
  Write files to a volume the user owns
1138
1244
  ([troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md#eacces-permission-denied-open-app)).
package/docs/roadmap.md CHANGED
@@ -7,7 +7,15 @@ number on it. Every release, with each change it made, is in
7
7
 
8
8
  ## Now
9
9
 
10
- Nothing scheduled yet.
10
+ - **Middlewares around the pages.** `beforeAll` and `configure` take
11
+ `use(logger())`, `use(secureHeaders())` and the other middlewares of
12
+ alxia 0.4, which run on the pages, the client's files declared after
13
+ them, and every request no route matches; the hooks they replace still
14
+ work, deprecated.
15
+ - **Loaders typed by core's `Register`.** `alxiaOf(context)` reads the base
16
+ `@alxia/core`'s `Register` names when this package's names no server, so
17
+ an app that registers its context once types its loaders too. This
18
+ package's `Register` still wins when both are declared.
11
19
 
12
20
  ## Next
13
21
 
@@ -29,9 +37,9 @@ Nothing scheduled yet.
29
37
  only behind `future.v8_middleware`, and the line is in maintenance.
30
38
  - **Node or Cloudflare.** alxia is a Bun framework; React Router's own
31
39
  adapters serve those runtimes.
32
- - **The pages in the route table.** Documents and single-fetch data are not
33
- something the typed client calls, so the catch-all stays out of
34
- `RoutesOf`.
40
+ - **The pages in the OpenAPI document.** Documents and single-fetch data
41
+ are not something a generated client calls, so `isReactRouterRoute`
42
+ leaves the catch-all out of `matchesSpec`'s check.
35
43
  - **A runtime dependency.** The package declares none: `@alxia/core` and
36
44
  `react-router` are peers.
37
45
  - **A `react-router-serve` of its own.** `bun build/server/index.js` is the
@@ -45,23 +53,28 @@ Nothing scheduled yet.
45
53
 
46
54
  ## Shipped
47
55
 
48
- ### Next release
56
+ ### 0.4.0
49
57
 
50
58
  - **A self-contained build.** `react-router build` bundles every package
51
59
  into `build/server/index.js`, React, React Router and alxia included,
52
60
  so `build/` runs with no `node_modules` and a Docker image copies it
53
61
  alone. Never in dev; `ssr.external` keeps a package external, and
54
62
  `ssr.external: true` every package.
63
+
64
+ ### 0.3.0
65
+
55
66
  - **The server is built for Bun.** `alxia()` adds the `bun` export
56
67
  condition to the `ssr` environment's conditions and external
57
68
  conditions, keeps `bun` and `bun:*` external whichever runtime runs
58
69
  Vite, and targets `esnext`, in dev and in the build, with nothing to
59
70
  configure. A package's `bun` variant is the one bundled, and what the
60
71
  app sets is kept.
61
- - **A Docker image on Bun.** The guide's Deploying has the multi-stage
62
- `Dockerfile` on `oven/bun:1` that `@alxia/create`'s template and the
63
- example ship, running `bun build/server/index.js` as a non-root user,
64
- from `build/` alone.
72
+ - **A Docker image on Bun.** The guide's Deploying has a multi-stage
73
+ `Dockerfile` on `oven/bun:1`, running `bun build/server/index.js` as a
74
+ non-root user.
75
+
76
+ ### 0.2.0
77
+
65
78
  - **A per-request CSP nonce.** `nonceOf(loadContext)` reads the nonce that
66
79
  `@alxia/secure-headers`' `nonce: true`, or a `derive` of the app's own,
67
80
  put on the context, so `entry.server.tsx` hands it to `<ServerRouter
@@ -85,7 +98,8 @@ Nothing scheduled yet.
85
98
  - **`HEAD` with headers.** A `HEAD` is answered as its `GET`, less the
86
99
  body.
87
100
  - **OpenAPI.** `isReactRouterRoute` leaves the catch-all and the client's
88
- files out of `@alxia/openapi`'s document.
101
+ files out of a check of `app.routes` against the document, such as
102
+ `@alxia/openapi`'s `matchesSpec`.
89
103
  - **Zero config, with Vite.** `@alxia/react-router/vite`'s `alxia()`,
90
104
  anywhere in `plugins`, is all an app from the official template needs.
91
105
  Without `app/server.ts` a default server serves the pages; with it,
@@ -55,6 +55,8 @@ a loader, a message React Router or the browser prints, or an error from
55
55
  - [A page answers alxia's JSON 404 or 405 instead of rendering](#a-page-answers-alxias-json-404-or-405-instead-of-rendering)
56
56
  - [A streamed page arrives in one piece](#a-streamed-page-arrives-in-one-piece)
57
57
  - [The logger times a streamed page at a few milliseconds](#the-logger-times-a-streamed-page-at-a-few-milliseconds)
58
+ - [A page, or a missing path, answers 401 behind a guard](#a-page-or-a-missing-path-answers-401-behind-a-guard)
59
+ - [The logger, CORS or secure headers miss the client's files](#the-logger-cors-or-secure-headers-miss-the-clients-files)
58
60
  - [`ctx.server` is `undefined` under `react-router dev`](#ctxserver-is-undefined-under-react-router-dev)
59
61
  - [A `publish` under `react-router dev` misses the sockets opened before an edit](#a-publish-under-react-router-dev-misses-the-sockets-opened-before-an-edit)
60
62
  - [The build has a package's Node variant, not its `bun` one](#the-build-has-a-packages-node-variant-not-its-bun-one)
@@ -727,6 +729,12 @@ COPY --from=production-dependencies /app/node_modules ./node_modules
727
729
  `grep '^import' build/server/index.js` lists what the build still
728
730
  imports: `node:*`, `bun`, `bun:*` and the packages `ssr.external` names.
729
731
 
732
+ The image runs on `oven/bun:1-alpine`, whose C library is musl. A native
733
+ addon built for glibc alone then stops it with
734
+ `… is linked against glibc (DT_NEEDED …), but this Bun build uses musl.`:
735
+ put the final stage back on `oven/bun:1`
736
+ ([`@alxia/create`'s troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/create/docs/troubleshooting.md#error--is-linked-against-glibc-dt_needed-libmso6-but-this-bun-build-uses-musl)).
737
+
730
738
  ## Types
731
739
 
732
740
  ### `Property '…' does not exist on type 'BaseContext & …'`
@@ -736,10 +744,12 @@ error TS2339: Property 'tenant' does not exist on type 'BaseContext & Empty & {
736
744
  ```
737
745
 
738
746
  **When:** a loader reads, through `alxiaOf`, or `getLoadContext`
739
- destructures, something no hook of the server derives. With the type
747
+ destructures, something no middleware of the server derives. With the type
740
748
  `'BaseContext & Empty'` alone, `alxiaOf(context)` has no server to read:
741
749
  `app/server.ts` has no `Register` declaration, or there is no
742
- `app/server.ts`.
750
+ `app/server.ts`, and `@alxia/core`'s `Register` names no base either.
751
+ With core's `Register` alone, `alxiaOf` reads that base: a key
752
+ `configure` adds after it is missing.
743
753
 
744
754
  **Why:** the type is the app's context at the point of the catch-all, as
745
755
  for any route: "order is meaning".
@@ -862,7 +872,7 @@ argument, `alxiaOf<Server>(context)`.
862
872
  error TS2375: Type '{ context: EntryContext; url: string; nonce: string | undefined; }' is not assignable to type 'ServerRouterProps' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
863
873
  ```
864
874
 
865
- **Why:** `nonceOf` returns `undefined` when no hook set a nonce, and
875
+ **Why:** `nonceOf` returns `undefined` when no middleware set a nonce, and
866
876
  `ServerRouterProps.nonce` is optional without `undefined`. React's
867
877
  `nonce` option takes `undefined`, so only the prop complains.
868
878
 
@@ -987,9 +997,45 @@ deferred value later. If the whole page comes at once:
987
997
  page that streams for 800 ms is logged at its first byte. The duration is
988
998
  the server's time to first byte, not the page's.
989
999
 
1000
+ ### A page, or a missing path, answers 401 behind a guard
1001
+
1002
+ A guard given to `use` in `configure` or `beforeAll` runs on every request
1003
+ it is in front of, a request no route matches included, and answers before
1004
+ the 404. An anonymous request to `/nope` gets the 401, not the 404, and so
1005
+ does the login page the guard sits in front of. Scope the guard to what it
1006
+ guards, with a path. A path-scoped `use` takes a middleware that adds nothing
1007
+ to the context, so `bearer`, which adds `user`, does not compile there: write
1008
+ the guard as a middleware that only refuses. The pages are a catch-all outside
1009
+ any `group`, so a `group` would not reach them.
1010
+
1011
+ ```ts
1012
+ import { defineMiddleware } from '@alxia/core';
1013
+
1014
+ const account = defineMiddleware(async ({ request, reply }, next) => {
1015
+ const token = request.headers.get('authorization')?.slice('Bearer '.length) ?? '';
1016
+ return (await jwt.verify(token)).ok ? next() : reply(401, { error: 'unauthorized' as const });
1017
+ });
1018
+
1019
+ createServer({
1020
+ configure: (app) => app.use('/account', account), // the pages under /account only
1021
+ });
1022
+ ```
1023
+
1024
+ ### The logger, CORS or secure headers miss the client's files
1025
+
1026
+ A middleware in `configure` runs for the routes declared after it. The
1027
+ client's files are declared before `configure`, after `beforeAll`: what must
1028
+ see them too goes in `beforeAll`.
1029
+
1030
+ ```ts
1031
+ createServer({
1032
+ beforeAll: (app) => app.use(logger()).use(secureHeaders()),
1033
+ });
1034
+ ```
1035
+
990
1036
  ### `ctx.server` is `undefined` under `react-router dev`
991
1037
 
992
- A hook or a route reads `ctx.server`, or the app declares a `page()`,
1038
+ A middleware or a route reads `ctx.server`, or the app declares a `page()`,
993
1039
  under `react-router dev` or `vite preview`; the same code works from
994
1040
  `bun build/server/index.js`.
995
1041
 
@@ -998,7 +1044,7 @@ the preview server under `vite preview`. An HTTP request reaches the app
998
1044
  through `app.fetch`, as in a test, not through `listen`: there is no
999
1045
  `Bun.serve` behind it, so `ctx.server` is `undefined`, the default
1000
1046
  `ctx.ip` too, and a `page()` (Bun's HTML bundle) is not served. A socket's upgrade is the exception: the plugin relays it
1001
- to a `Bun.serve` of the app, so its hooks read a server
1047
+ to a `Bun.serve` of the app, so its middlewares read a server
1002
1048
  ([WebSockets](guide.md#under-react-router-dev)).
1003
1049
 
1004
1050
  **Fix:** read `ctx.server` as optional, and publish to sockets from the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alxia/react-router",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "alxia as the server of a React Router framework app: server rendering behind the app's hooks, loaders reading its typed context, the client's assets served",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -51,9 +51,9 @@
51
51
  ]
52
52
  },
53
53
  "devDependencies": {
54
- "@alxia/compress": "^0.1.2",
55
- "@alxia/core": "^0.3.1",
56
- "@alxia/openapi": "^0.3.0",
54
+ "@alxia/compress": "^0.2.0",
55
+ "@alxia/core": "^0.4.0",
56
+ "@alxia/openapi": "^0.4.0",
57
57
  "@react-router/dev": "^8.4.0",
58
58
  "@types/bun": "^1.4.2",
59
59
  "@types/react": "^19.3.0",
@@ -65,7 +65,7 @@
65
65
  "vite": "^7.0.0"
66
66
  },
67
67
  "peerDependencies": {
68
- "@alxia/core": "^0.3.1",
68
+ "@alxia/core": "^0.4.0",
69
69
  "react-router": "^8.0.0",
70
70
  "typescript": "^6.0.3 || ^7.0.0",
71
71
  "vite": "^7.0.0 || ^8.0.0"