@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 +62 -31
- package/dist/cli/index.js +3 -3
- package/dist/cli/index.js.map +4 -4
- package/dist/cli/template.d.ts +1 -1
- package/dist/cli/template.d.ts.map +1 -1
- package/dist/context.d.ts +23 -12
- package/dist/context.d.ts.map +1 -1
- package/dist/index.js.map +4 -4
- package/dist/react-router.d.ts +13 -13
- package/dist/react-router.d.ts.map +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.d.ts.map +1 -1
- package/docs/README.md +1 -1
- package/docs/guide.md +162 -56
- package/docs/roadmap.md +24 -10
- package/docs/troubleshooting.md +51 -5
- package/package.json +5 -5
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
|
|
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
|
-
|
|
9
|
+
middlewares built, typed.
|
|
10
10
|
|
|
11
11
|
## Quick start
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
|
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
|
|
165
|
+
no middleware derives is a compile error;
|
|
149
166
|
- without it, `alxiaOf<typeof server>(context)` names the server;
|
|
150
|
-
- with neither, `
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
283
|
+
## Leaving the pages out of `matchesSpec`
|
|
262
284
|
|
|
263
|
-
|
|
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 {
|
|
268
|
-
import {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
277
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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=
|
|
159
|
+
//# debugId=8582AE498438642864756E2164756E21
|
|
160
160
|
//# sourceMappingURL=index.js.map
|
package/dist/cli/index.js.map
CHANGED
|
@@ -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
|
|
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
|
|
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": "
|
|
10
|
+
"mappings": "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",
|
|
11
|
+
"debugId": "8582AE498438642864756E2164756E21",
|
|
12
12
|
"names": []
|
|
13
13
|
}
|
package/dist/cli/template.d.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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,
|
|
5
|
+
import type { Alxia, AnyAlxia, ContextOf, RegisteredBase } from '@alxia/core';
|
|
6
6
|
import { type RouterContextProvider } from 'react-router';
|
|
7
|
-
import type {
|
|
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
|
|
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 `
|
|
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
|
-
},
|
|
47
|
-
/**
|
|
48
|
-
|
|
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 :
|
|
51
|
-
/** What `alxiaOf` reads with no type argument: the registered server's app,
|
|
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
|
|
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, `
|
|
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
|
package/dist/context.d.ts.map
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
|
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,
|
|
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": ";AAKA;AAIA,IAAM,UAAyB,OAAO,uBAAuB;AAStD,IAAM,eAAe,cAAuB,OAAO;
|
|
11
|
+
"mappings": "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",
|
|
12
12
|
"debugId": "63BB3B2311BA25F964756E2164756E21",
|
|
13
13
|
"names": []
|
|
14
14
|
}
|
package/dist/react-router.d.ts
CHANGED
|
@@ -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
|
|
17
|
-
* `
|
|
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 `
|
|
32
|
-
*
|
|
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
|
-
*
|
|
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
|
|
42
|
-
* before it. Each loader, action and middleware reads what those
|
|
43
|
-
*
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
*/
|
package/dist/server.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
-
- [
|
|
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
|
-
|
|
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
|
-
-
|
|
100
|
-
warns, but the template's code
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
|
587
|
-
added `user` to every route, declared before it or after, would
|
|
588
|
-
`user` on routes that run before the plugin. That is the lie "order
|
|
589
|
-
meaning" forbids. `Register`
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
|
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
|
|
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.
|
|
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 `
|
|
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
|
-
##
|
|
921
|
+
## Middlewares around the pages
|
|
850
922
|
|
|
851
|
-
Every
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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
|
|
947
|
-
validation, as any request does. A
|
|
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
|
|
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
|
|
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
|
-
|
|
1020
|
-
|
|
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 {
|
|
1026
|
-
import {
|
|
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
|
-
.
|
|
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
|
-
|
|
1037
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
33
|
-
something
|
|
34
|
-
`
|
|
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
|
-
###
|
|
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
|
|
62
|
-
`Dockerfile` on `oven/bun:1`
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
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,
|
package/docs/troubleshooting.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
55
|
-
"@alxia/core": "^0.
|
|
56
|
-
"@alxia/openapi": "^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.
|
|
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"
|