@alxia/react-router 0.4.0 → 0.6.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/docs/guide.md CHANGED
@@ -16,7 +16,7 @@ deploying.
16
16
  - [The app's own context keys](#the-apps-own-context-keys)
17
17
  - [A CSP nonce](#a-csp-nonce)
18
18
  - [Escape hatches](#escape-hatches)
19
- - [Hooks around the pages](#hooks-around-the-pages)
19
+ - [Middlewares around the pages](#middlewares-around-the-pages)
20
20
  - [Routes beside the pages](#routes-beside-the-pages)
21
21
  - [WebSockets](#websockets)
22
22
  - [The client's files](#the-clients-files)
@@ -26,7 +26,35 @@ deploying.
26
26
 
27
27
  ## Setup
28
28
 
29
- Start from React Router's official template:
29
+ ### A new app: `bun create @alxia`
30
+
31
+ ```sh
32
+ bun create @alxia my-app --template react-router
33
+ cd my-app
34
+ bun dev
35
+ ```
36
+
37
+ [`@alxia/create`](https://www.npmjs.com/package/@alxia/create) copies React
38
+ Router's official template with alxia's layer already applied: the steps
39
+ of the next section, done.
40
+
41
+ - `@alxia/core` and `@alxia/react-router` installed, and every dependency
42
+ moved to the newest release alxia accepts (see
43
+ [`@alxia/create`'s Versions](https://github.com/softistx/alxia/blob/develop/packages/create/docs/guide.md#versions));
44
+ - `alxia()` in `vite.config.ts`, the `bunfig.toml`, and `start` running
45
+ `NODE_ENV=production 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
@@ -68,7 +96,7 @@ middleware must come after alxia's.
68
96
  "scripts": {
69
97
  "build": "react-router build",
70
98
  "dev": "react-router dev",
71
- "start": "bun build/server/index.js",
99
+ "start": "NODE_ENV=production bun build/server/index.js",
72
100
  "typecheck": "react-router typegen && tsc"
73
101
  }
74
102
  ```
@@ -96,9 +124,10 @@ What the template keeps:
96
124
  - `@react-router/node`, which React Router's default `entry.server`
97
125
  renders with: it streams under Bun.
98
126
  - Vite 7 or 8 (the template ships 8).
99
- - Its `typescript` 5.9. This package's peer asks for 6 or 7, so `bun add`
100
- warns, but the template's code typechecks with 5.9.
101
- `bun add -d typescript@^6` silences the warning.
127
+ - With `bunx create-react-router`, its `typescript` 5.9. This package's
128
+ peer asks for 6 or 7, so `bun add` warns, but the template's code
129
+ typechecks with 5.9. `bun add -d typescript@^6` silences the warning;
130
+ `bun create @alxia` writes 6 or 7 already.
102
131
 
103
132
  React Router 8 is required: its loaders receive a `RouterContextProvider`,
104
133
  and alxia's context is set on it.
@@ -144,13 +173,21 @@ server build inside it, into one file:
144
173
  stay out of it.
145
174
  - **Run, it listens**: `bun build/server/index.js` listens on `PORT`
146
175
  (3000 by default) and `HOST` (`0.0.0.0`). It prints
147
- `alxia listening on <url>`. On `SIGINT` or `SIGTERM` it stops the app,
148
- runs its `onStop` hooks, and exits.
176
+ `alxia listening on <url>`, and in dev — under `react-router dev`; a
177
+ production build never is, whatever `NODE_ENV` says — the route table
178
+ ([`@alxia/core`'s Development](https://github.com/softistx/alxia/blob/develop/packages/core/docs/guide/development.md#the-route-table)). On `SIGINT` or `SIGTERM` it shuts the app
179
+ down as `@alxia/core`'s `listen` does: readiness turns 503, new
180
+ connections are refused, the requests in flight finish within
181
+ `shutdownTimeout` (10 s; `listen: { shutdownTimeout }` sets it), the
182
+ `onStop` hooks run, and the process exits
183
+ ([core's guide](https://github.com/softistx/alxia/blob/develop/packages/core/docs/guide/health-and-shutdown.md)).
149
184
  - **Imported, it starts nothing**: prerendering, a test or another server
150
185
  can import it safely, since it listens only when it is the process's
151
186
  entry point (`import.meta.main`).
152
187
  - **The mode follows the command**: `development` under `react-router dev`,
153
- `production` in a build, whatever `NODE_ENV` says.
188
+ `production` in a build, whatever `NODE_ENV` says. The app's dev switch
189
+ follows it: `alxia({ dev: mode === 'development' })`, so a 500 of the
190
+ build never shows its stack.
154
191
  - **The client folder is resolved against the built file**: `../client`,
155
192
  from React Router's `buildDirectory`, so it is found wherever the
156
193
  process starts.
@@ -188,7 +225,7 @@ The plugin loads `build/server/index.js` on the first request and hands
188
225
  every request to its default export, the alxia app, before Vite's own
189
226
  files. So the preview answers as `bun run start` does: the pages and their
190
227
  data, `/api`, `build/client` with the cache headers of
191
- [the client's files](#the-clients-files), and every hook of `beforeAll`
228
+ [the client's files](#the-clients-files), and every middleware of `beforeAll`
192
229
  and `configure`. Requests, their bodies and every `Set-Cookie` pass
193
230
  through whole, and pages stream.
194
231
 
@@ -212,7 +249,7 @@ React Router prerenders the `prerender` paths of `react-router.config.ts`
212
249
  through the same preview server, during `react-router build`. A
213
250
  prerendered page is therefore rendered by the built server too: its loader
214
251
  reads `alxiaOf(context)` and `getLoadContext`'s keys, and `beforeAll`'s
215
- and `configure`'s hooks run around it.
252
+ and `configure`'s middlewares run around it.
216
253
 
217
254
  ## Built for Bun
218
255
 
@@ -368,7 +405,7 @@ bunx alxia-react-router reveal
368
405
 
369
406
  ```text
370
407
  alxia-react-router: wrote app/server.ts, the server alxia() runs by default.
371
- Next: uncomment configure in app/server.ts to add the app's hooks and /api; bun run dev picks it up.
408
+ Next: uncomment configure in app/server.ts to add the app's middlewares and /api; bun run dev picks it up.
372
409
  ```
373
410
 
374
411
  The file is the server the plugin runs without one, `createServer()`, so
@@ -427,7 +464,7 @@ React Router's own rendering entries, `app/entry.server.tsx` and
427
464
  ### By hand
428
465
 
429
466
  The example uses `@alxia/logger` and `@alxia/compress`
430
- (`bun add @alxia/logger @alxia/compress`); any plugin works the same way.
467
+ (`bun add @alxia/logger @alxia/compress`); any middleware works the same way.
431
468
 
432
469
  ```ts
433
470
  // app/server.ts
@@ -460,12 +497,12 @@ Every option is optional:
460
497
 
461
498
  | option | what it does |
462
499
  | --- | --- |
463
- | `configure(app)` | the app the pages run behind: its plugins, hooks and `/api`. It returns the app, and what it builds is what the loaders read |
500
+ | `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
501
  | `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
502
  | `getLoadContext(ctx, context)` | sets the app's own keys on React Router's provider, `ctx` typed by `configure`'s app |
466
503
  | `build`, `mode`, `client` | override what the plugin wires; see [Escape hatches](#escape-hatches) |
467
- | `listen` | `listen`'s options for `bun build/server/index.js`: `port`, `hostname`, `idleTimeout`, `maxRequestBodySize`, `tls`. A `port` or `hostname` given here wins over `PORT` and `HOST` |
468
- | `onListen(server)` | called once the built server listens and its `SIGINT` and `SIGTERM` handlers are in place, in place of the `alxia listening on …` line; a signal sent from then on runs the `onStop` hooks |
504
+ | `listen` | `listen`'s options for `bun build/server/index.js`: `port`, `hostname`, `idleTimeout`, `maxRequestBodySize`, `tls`, `onListen`. A `port` or `hostname` given here wins over `PORT` and `HOST`; an `onListen` here, told `@alxia/core`'s `ListenInfo` — `url`, `routes`, `table`, `dev` — wins over the option below |
505
+ | `onListen(server)` | called once the built server listens and its `SIGINT` and `SIGTERM` handlers are in place, in place of the `alxia listening on …` line (and, in dev, of the route table); a signal sent from then on runs the `onStop` hooks |
469
506
 
470
507
  A request goes through four layers, in order:
471
508
 
@@ -474,11 +511,16 @@ A request goes through four layers, in order:
474
511
  3. what `configure` declared;
475
512
  4. the pages: `GET`, `POST`, `PUT`, `PATCH` and `DELETE` at `/*`.
476
513
 
477
- Hooks apply to the routes declared after them, so a session or a guard in
478
- `configure` runs around every page and its data, but not around the
479
- JavaScript of the login page. Global hooks (`onRequest`, `@alxia/cors`,
480
- `@alxia/compress`, `@alxia/secure-headers`) apply everywhere, wherever
481
- they are declared.
514
+ A middleware given to `use` runs on every route declared after it, and on a
515
+ request no route matches. So a session or a guard in `configure` runs around
516
+ every page and its data, and not around what is declared before it: the
517
+ client's files, which sit between `beforeAll` and `configure`. What must see
518
+ every request, the client's files included, goes in `beforeAll`: the logger,
519
+ `@alxia/cors`, `@alxia/compress`, `@alxia/secure-headers`. A guard on the app
520
+ answers a request no route matches too: an anonymous request to a missing path
521
+ gets the 401, not the 404. Scope it with a path, `use('/app', guard)`
522
+ (a guard that adds nothing to the context: `bearer` and `session` add `user`),
523
+ to guard only some pages.
482
524
 
483
525
  ```ts
484
526
  // app/server.ts: a guard on everything, the assets included
@@ -495,13 +537,13 @@ export default createServer({
495
537
  ```
496
538
 
497
539
  `configure` and `beforeAll` must return the app; one that returns nothing
498
- is a compile error. A plugin that needs something an earlier one
540
+ is a compile error. A middleware that needs something an earlier one
499
541
  derived is typed by the app it is given, as anywhere in alxia.
500
542
 
501
543
  ## Typing the loaders
502
544
 
503
545
  Every request through the catch-all sets `alxiaContext`, this package's
504
- key, on React Router's context provider, to what alxia's hooks built for
546
+ key, on React Router's context provider, to what alxia's middlewares built for
505
547
  that request. `alxiaOf(context)` reads it.
506
548
 
507
549
  ### With `Register`: no type argument
@@ -546,7 +588,7 @@ shows. The checks it gives:
546
588
 
547
589
  ```ts
548
590
  const ctx = alxiaOf(context);
549
- ctx.tenant; // error: no hook derives `tenant`
591
+ ctx.tenant; // error: no middleware derives `tenant`
550
592
  ctx.user.name; // error: `user` may be null
551
593
  ```
552
594
 
@@ -577,19 +619,57 @@ export function loader({ context }: Route.LoaderArgs) {
577
619
 
578
620
  The type argument may also be an alxia app, the app *before* the
579
621
  catch-all, for a server of your own. Anything else is a compile error.
580
- With neither `Register` nor a type argument, `alxiaOf(context)` is
581
- `BaseContext`: the request, its URL, `reply` and the rest of what every
582
- hook reads.
622
+
623
+ ### With `@alxia/core`'s `Register`
624
+
625
+ `@alxia/core` has a `Register` of its own, which names the chain that
626
+ builds an app's context, for its route files
627
+ ([The app's type](https://github.com/softistx/alxia/blob/develop/packages/core/docs/guide/types.md#register-and-appcontext)).
628
+ `alxiaOf(context)` reads it when this package's `Register` names no
629
+ server:
630
+
631
+ ```ts
632
+ // app/context.ts
633
+ import { alxia } from '@alxia/core';
634
+
635
+ export const base = alxia().derive(({ request }) => ({ user: request.headers.get('x-user') }));
636
+
637
+ declare module '@alxia/core' {
638
+ interface Register {
639
+ context: typeof base;
640
+ }
641
+ }
642
+
643
+ // app/server.ts
644
+ const server = createServer({ configure: (app) => app.plugin(base) });
645
+ export default server;
646
+ ```
647
+
648
+ `alxiaOf(context).user` is then typed with no declaration in
649
+ `@alxia/react-router`. Which one is read, in order:
650
+
651
+ 1. a type argument, `alxiaOf<typeof server>(context)`;
652
+ 2. this package's `Register`, `server: typeof server`: the whole app the
653
+ pages run behind, `base` and everything `configure` adds after it;
654
+ 3. `@alxia/core`'s `Register`, `context: typeof base`: only what `base`
655
+ builds;
656
+ 4. neither: `BaseContext`, the request, its URL, `reply` and the rest of
657
+ what every middleware reads.
658
+
659
+ Declare this package's `Register` when `configure` adds to the context
660
+ after the base, and the loaders read it.
583
661
 
584
662
  ### Why a global augmentation is right here
585
663
 
586
- alxia's core refuses global augmentation of its context: a plugin that
587
- added `user` to every route, declared before it or after, would type
588
- `user` on routes that run before the plugin. That is the lie "order is
589
- meaning" forbids. `Register` here does something else: it names the
590
- **one** server of the React Router build, at the point of its catch-all,
591
- which is exactly what every loader runs behind. One build has one server
592
- entry, so there is no second app for a module to be confused with.
664
+ alxia's core refuses a global augmentation of a context key: a plugin
665
+ that added `user` to every route, declared before it or after, would
666
+ type `user` on routes that run before the plugin. That is the lie "order
667
+ is meaning" forbids. Core's own `Register` names the base that builds
668
+ the context instead, and what reads it requires it. `Register` here does
669
+ something else: it names the **one** server of the React Router build,
670
+ at the point of its catch-all, which is exactly what every loader runs
671
+ behind. One build has one server entry, so there is no second app for a
672
+ module to be confused with.
593
673
 
594
674
  If two React Router apps share one TypeScript program (one tsconfig over
595
675
  both folders of a monorepo), their two declarations conflict, and `tsc`
@@ -639,7 +719,7 @@ export function loader({ context }: Route.LoaderArgs) {
639
719
 
640
720
  `getLoadContext` runs before React Router, on every request, with
641
721
  `alxiaContext` already set. Its `ctx` is typed by `configure`'s app:
642
- reading something no hook derives is a compile error. Keep the keys in a
722
+ reading something no middleware derives is a compile error. Keep the keys in a
643
723
  module of their own, such as `app/context.ts`, rather than in
644
724
  `app/server.ts`: a route imports them, and the server file then stays out
645
725
  of the client's module graph.
@@ -729,7 +809,7 @@ scripts included.
729
809
  ### How it stays loose
730
810
 
731
811
  `nonceOf` reads `nonce` from alxia's context if it is a string, and
732
- returns `undefined` otherwise: no hook set one, or the request did not come
812
+ returns `undefined` otherwise: no middleware set one, or the request did not come
733
813
  through alxia at all. `ServerRouter` and React then render no `nonce`
734
814
  attribute. So:
735
815
 
@@ -816,7 +896,7 @@ export const base = alxia()
816
896
  /** What the loaders read: alxiaOf<Base>(context). */
817
897
  export type Base = typeof base;
818
898
 
819
- const app = base.use((app) =>
899
+ const app = base.plugin((app) =>
820
900
  reactRouter(app, {
821
901
  build: () =>
822
902
  import(new URL('./build/server/index.js', import.meta.url).href) as Promise<ServerBuild>,
@@ -838,7 +918,7 @@ bun run build && bun server.ts
838
918
  | `getLoadContext(ctx, context)` | as `createServer`'s |
839
919
  | `client` | the client build's folder, a path or a `file:` URL, served before the catch-all in `production` |
840
920
 
841
- `reactRouter()` goes through `use`, as `@alxia/graphql`'s `graphql(app, …)`
921
+ `reactRouter()` goes through `app.plugin`, as `@alxia/graphql`'s `graphql(app, …)`
842
922
  does: that is how it knows the app's context type. A `HEAD` is handed to
843
923
  React Router as a `GET`, since React Router answers a `HEAD` of its own
844
924
  with no headers at all; the core then drops the body. In a monorepo where
@@ -846,14 +926,15 @@ this package is linked rather than installed, add
846
926
  `ssr: { external: ['@alxia/react-router'] }` to `vite.config.ts`: see
847
927
  [the troubleshooting entry](troubleshooting.md#alxiaof-this-request-has-no-alxia-context-).
848
928
 
849
- ## Hooks around the pages
929
+ ## Middlewares around the pages
850
930
 
851
- Every hook declared before the catch-all runs around each page, as around
852
- any route:
931
+ Every middleware declared before the catch-all runs around each page, as around
932
+ any route, and `use` observers go first so they wrap everything:
853
933
 
854
934
  - **`@alxia/logger`** writes one entry per request and sets
855
935
  `x-request-id`. It times a streamed page by its first byte, since
856
- `onResponse` runs when the headers leave.
936
+ the response is settled when its headers leave. Every request is logged,
937
+ 404s and errors included.
857
938
  - **`@alxia/compress`** compresses documents and data, and flushes a
858
939
  streamed page as React writes it: the shell and its `<Suspense>`
859
940
  fallback still arrive first, in zstd, Brotli or gzip. See [the troubleshooting entry](troubleshooting.md#a-streamed-page-arrives-in-one-piece).
@@ -865,7 +946,8 @@ any route:
865
946
  [A CSP nonce](#a-csp-nonce).
866
947
  - **A guard** — `@alxia/jwt`'s `bearer`, `@alxia/janus`' session — in
867
948
  `configure` guards every page and its data alike; in `beforeAll`, the
868
- client's files too.
949
+ client's files too. On the app, a guard also answers a request no route
950
+ matches with its 401.
869
951
 
870
952
  ## Routes beside the pages
871
953
 
@@ -883,7 +965,7 @@ export default createServer({
883
965
  });
884
966
  ```
885
967
 
886
- Each answers with its own schemas, replies and typed client. The core ranks
968
+ Each answers with its own middlewares and replies. The core ranks
887
969
  paths as `Bun.serve` does, through `listen`, `app.fetch` and
888
970
  `app.request` alike: segment by segment, a literal beats a parameter,
889
971
  which beats the catch-all's wildcard. Two consequences:
@@ -894,10 +976,10 @@ which beats the catch-all's wildcard. Two consequences:
894
976
  every one-segment path, `/about` included, before React Router sees it.
895
977
  Put alxia's routes under a prefix of their own, `/api`.
896
978
 
897
- The catch-all adds nothing to the app's route table: `RoutesOf` and the
898
- typed client never show `/*`. Pages and single-fetch data are not
899
- something a typed client calls; what it does call, your `/api`, is typed
900
- as always.
979
+ Pages and single-fetch data are not something a generated client calls:
980
+ leave the catch-all out of `matchesSpec`'s check with `isReactRouterRoute`
981
+ ([OpenAPI](#openapi)); what a client does call, your `/api`, is what the
982
+ OpenAPI document declares.
901
983
 
902
984
  What React Router answers comes back as it sent it: documents,
903
985
  single-fetch data (`/_.data`, `/login.data`), lazy route discovery
@@ -943,10 +1025,10 @@ export default createServer({
943
1025
  });
944
1026
  ```
945
1027
 
946
- The upgrade runs the hooks declared before the route, and the route's
947
- validation, as any request does. A hook's reply refuses it: the client
1028
+ The upgrade runs the middlewares declared before the route, and the route's
1029
+ validation, as any request does. A middleware's reply refuses it: the client
948
1030
  gets the 401 and its body, and no socket opens. `socket.data` holds the
949
- validated request and what each hook derived, typed. See
1031
+ validated request and what each middleware derived, typed. See
950
1032
  [`@alxia/core`'s WebSockets](https://github.com/softistx/alxia/blob/develop/packages/core/docs/guide/websockets.md)
951
1033
  for the schemas and the rest of the socket.
952
1034
 
@@ -966,7 +1048,7 @@ byte for byte, to a `Bun.serve` of the app on a loopback port of its
966
1048
  own, started on the first upgrade and given
967
1049
  `app.fetch` and `app.websocket`, as `listen` gives them. So in dev:
968
1050
 
969
- - **The same hooks, refusals and handlers run** as from the build:
1051
+ - **The same middlewares, refusals and handlers run** as from the build:
970
1052
  `open`, `message`, `close`, `drain`, the `message` and `send` schemas,
971
1053
  `subscribe` and `publish`.
972
1054
  - **An edit to `app/server.ts`, or to a module it imports, is used from
@@ -1016,25 +1098,49 @@ refused when the server starts: see
1016
1098
 
1017
1099
  ## OpenAPI
1018
1100
 
1019
- `@alxia/openapi` documents every route of `app.routes`, and the catch-all
1020
- and the client's files are routes. Leave them out with
1101
+ alxia is OpenAPI spec first: the document is written by hand, the API's
1102
+ routes are bound to the operations generated from it, and
1103
+ [`@alxia/openapi`](https://www.npmjs.com/package/@alxia/openapi)'s
1104
+ `matchesSpec` checks, in a test, that `app.routes` and the operations
1105
+ match both ways. The catch-all and the client's files are routes of
1106
+ `app.routes`, and no operation of the document. Leave them out with
1021
1107
  `isReactRouterRoute`, which is true for each route this package declared:
1022
1108
 
1023
1109
  ```ts
1024
1110
  // app/server.ts
1025
- import { docs } from '@alxia/openapi';
1026
- import { createServer, isReactRouterRoute } from '@alxia/react-router';
1111
+ import { createServer } from '@alxia/react-router';
1112
+ import { operations } from './generated/alxia';
1027
1113
 
1028
1114
  export default createServer({
1029
1115
  configure: (app) =>
1030
1116
  app
1031
1117
  .get('/api/health', ({ reply }) => reply.ok({ ok: true }))
1032
- .use(docs(app, { info: { title: 'Shop', version: '1.0.0' }, exclude: isReactRouterRoute })),
1118
+ .route(operations.listOrders, ({ reply }) => reply.ok([])),
1119
+ });
1120
+ ```
1121
+
1122
+ ```ts
1123
+ // app/server.test.ts
1124
+ import { test } from 'bun:test';
1125
+ import { matchesSpec } from '@alxia/openapi';
1126
+ import { isReactRouterRoute } from '@alxia/react-router';
1127
+ import type { ServerBuild } from 'react-router';
1128
+ import { operations } from './generated/alxia';
1129
+ import server from './server';
1130
+
1131
+ test('the API routes every operation of openapi.yaml, and nothing else', async () => {
1132
+ const build: ServerBuild = await import(new URL('../build/server/index.js', import.meta.url).href);
1133
+ const app = server.create({ build });
1134
+ matchesSpec(app, operations, {
1135
+ exclude: (route) => isReactRouterRoute(route) || route.path === '/api/health',
1136
+ });
1033
1137
  });
1034
1138
  ```
1035
1139
 
1036
- Combine it with your own: `exclude: (route) => isReactRouterRoute(route) ||
1037
- route.path === '/api/health'`.
1140
+ When the app puts the operations under a prefix the document's paths
1141
+ leave out, `app.group('/api', (api) => api.route(…))`, give `matchesSpec`
1142
+ the same `prefix: '/api'`. See
1143
+ [`@alxia/openapi`'s checks](https://github.com/softistx/alxia/blob/develop/packages/openapi/docs/guide/checks.md).
1038
1144
 
1039
1145
  ## Testing
1040
1146
 
@@ -1083,7 +1189,8 @@ no `node_modules`, unless the app keeps a package external with
1083
1189
  The `react-router` template of
1084
1190
  [`@alxia/create`](https://github.com/softistx/alxia/tree/develop/packages/create)
1085
1191
  and the [example](https://github.com/softistx/alxia/tree/develop/examples/react-router)
1086
- ship this `Dockerfile`, on Bun's official image. For an app started from
1192
+ ship this `Dockerfile`, on Bun's official images: built on `oven/bun:1`,
1193
+ Debian's, and run on `oven/bun:1-alpine`, about 130 MB. For an app started from
1087
1194
  `create-react-router`, it replaces the template's, which builds and runs
1088
1195
  on Node:
1089
1196
 
@@ -1106,8 +1213,10 @@ RUN bun run build
1106
1213
  # command, and --no-install: with no node_modules, Bun would otherwise
1107
1214
  # fetch a package the build left out from the registry at startup,
1108
1215
  # where this fails.
1109
- # The server listens on PORT (3000) and HOST (0.0.0.0).
1110
- FROM oven/bun:1
1216
+ # The server listens on PORT (3000) and HOST (0.0.0.0). On Alpine, about
1217
+ # 200 MB lighter: build/ is JavaScript, which runs the same on musl. A
1218
+ # native addon built for glibc alone needs oven/bun:1 here.
1219
+ FROM oven/bun:1-alpine
1111
1220
  WORKDIR /app
1112
1221
  ENV NODE_ENV=production
1113
1222
  COPY --from=build /app/build ./build
@@ -1133,6 +1242,11 @@ startup, where Bun would otherwise fetch it from npm.
1133
1242
  gets the versions you tested, and a `package.json` changed without a
1134
1243
  `bun install` fails the build
1135
1244
  ([troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md#error-lockfile-had-changes-but-lockfile-is-frozen)).
1245
+ - **The final stage is Alpine's**, musl where the build stage has glibc.
1246
+ `build/` is JavaScript, which runs the same on both. A native addon
1247
+ kept external and built for glibc alone does not load there: put the
1248
+ final stage back on `oven/bun:1`
1249
+ ([`@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
1250
  - **The server runs as `bun`**, a user with no write access to `/app`.
1137
1251
  Write files to a volume the user owns
1138
1252
  ([troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md#eacces-permission-denied-open-app)).
@@ -1157,5 +1271,6 @@ startup, where Bun would otherwise fetch it from npm.
1157
1271
  ([troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md#error-cannot-find-package--from-appbuildserverindexjs)).
1158
1272
 
1159
1273
  `PORT` and `HOST` set where the server listens. The platform's `SIGTERM`
1160
- stops it once the requests in flight are answered, and runs the app's
1161
- `onStop` hooks.
1274
+ stops it once the requests in flight are answered, within
1275
+ `shutdownTimeout`, and runs the app's `onStop` hooks. Give the platform a
1276
+ readiness probe with `@alxia/core`'s `health()`, mounted in `beforeAll`.
package/docs/roadmap.md CHANGED
@@ -7,7 +7,20 @@ number on it. Every release, with each change it made, is in
7
7
 
8
8
  ## Now
9
9
 
10
- Nothing scheduled yet.
10
+ - **Middlewares around the pages.** `beforeAll` and `configure` take
11
+ `use(logger())`, `use(secureHeaders())` and the other middlewares of
12
+ alxia 0.4, which run on the pages, the client's files declared after
13
+ them, and every request no route matches; the request hooks they replaced,
14
+ deprecated in 0.4, were removed in 0.5.
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.
19
+ - **One graceful shutdown.** `start` relies on `@alxia/core`'s `listen`,
20
+ which handles `SIGINT` and `SIGTERM` itself, before `onListen` as
21
+ before: the requests in flight drain within `shutdownTimeout`
22
+ (`listen: { shutdownTimeout }`), readiness turns 503, the `onStop`
23
+ hooks run, then the process exits.
11
24
 
12
25
  ## Next
13
26
 
@@ -29,9 +42,9 @@ Nothing scheduled yet.
29
42
  only behind `future.v8_middleware`, and the line is in maintenance.
30
43
  - **Node or Cloudflare.** alxia is a Bun framework; React Router's own
31
44
  adapters serve those runtimes.
32
- - **The pages in the route table.** Documents and single-fetch data are not
33
- something the typed client calls, so the catch-all stays out of
34
- `RoutesOf`.
45
+ - **The pages in the OpenAPI document.** Documents and single-fetch data
46
+ are not something a generated client calls, so `isReactRouterRoute`
47
+ leaves the catch-all out of `matchesSpec`'s check.
35
48
  - **A runtime dependency.** The package declares none: `@alxia/core` and
36
49
  `react-router` are peers.
37
50
  - **A `react-router-serve` of its own.** `bun build/server/index.js` is the
@@ -45,23 +58,28 @@ Nothing scheduled yet.
45
58
 
46
59
  ## Shipped
47
60
 
48
- ### Next release
61
+ ### 0.4.0
49
62
 
50
63
  - **A self-contained build.** `react-router build` bundles every package
51
64
  into `build/server/index.js`, React, React Router and alxia included,
52
65
  so `build/` runs with no `node_modules` and a Docker image copies it
53
66
  alone. Never in dev; `ssr.external` keeps a package external, and
54
67
  `ssr.external: true` every package.
68
+
69
+ ### 0.3.0
70
+
55
71
  - **The server is built for Bun.** `alxia()` adds the `bun` export
56
72
  condition to the `ssr` environment's conditions and external
57
73
  conditions, keeps `bun` and `bun:*` external whichever runtime runs
58
74
  Vite, and targets `esnext`, in dev and in the build, with nothing to
59
75
  configure. A package's `bun` variant is the one bundled, and what the
60
76
  app sets is kept.
61
- - **A Docker image on Bun.** The guide's Deploying has the multi-stage
62
- `Dockerfile` on `oven/bun:1` that `@alxia/create`'s template and the
63
- example ship, running `bun build/server/index.js` as a non-root user,
64
- from `build/` alone.
77
+ - **A Docker image on Bun.** The guide's Deploying has a multi-stage
78
+ `Dockerfile` on `oven/bun:1`, running `bun build/server/index.js` as a
79
+ non-root user.
80
+
81
+ ### 0.2.0
82
+
65
83
  - **A per-request CSP nonce.** `nonceOf(loadContext)` reads the nonce that
66
84
  `@alxia/secure-headers`' `nonce: true`, or a `derive` of the app's own,
67
85
  put on the context, so `entry.server.tsx` hands it to `<ServerRouter
@@ -73,9 +91,9 @@ Nothing scheduled yet.
73
91
 
74
92
  - **React Router as a catch-all.** `reactRouter(app, { build })` serves
75
93
  documents, single-fetch data, actions, redirects with every cookie, lazy
76
- route discovery and the error pages, behind every hook declared before
77
- it, streamed as React renders them.
78
- - **The context, typed.** `alxiaOf<App>(context)` reads what alxia's hooks
94
+ route discovery and the error pages, behind every middleware declared
95
+ before it, streamed as React renders them.
96
+ - **The context, typed.** `alxiaOf<App>(context)` reads what alxia's middlewares
79
97
  built in any loader, action or middleware, through a key that is one
80
98
  object however the server was built; `getLoadContext` sets the app's own
81
99
  keys.
@@ -85,7 +103,8 @@ Nothing scheduled yet.
85
103
  - **`HEAD` with headers.** A `HEAD` is answered as its `GET`, less the
86
104
  body.
87
105
  - **OpenAPI.** `isReactRouterRoute` leaves the catch-all and the client's
88
- files out of `@alxia/openapi`'s document.
106
+ files out of a check of `app.routes` against the document, such as
107
+ `@alxia/openapi`'s `matchesSpec`.
89
108
  - **Zero config, with Vite.** `@alxia/react-router/vite`'s `alxia()`,
90
109
  anywhere in `plugins`, is all an app from the official template needs.
91
110
  Without `app/server.ts` a default server serves the pages; with it,
@@ -99,7 +118,7 @@ Nothing scheduled yet.
99
118
  - **`vite preview` serves the built server.** After `react-router build`,
100
119
  `bunx --bun vite preview` hands every request to `build/server/index.js`,
101
120
  as `bun run start` would answer it: the pages, `/api`, the client's files
102
- and the app's hooks. React Router's prerendering runs on the same
121
+ and the app's middlewares. React Router's prerendering runs on the same
103
122
  server, so a prerendered page's loader reads `alxiaOf`.
104
123
  - **`bunx alxia-react-router reveal`.** The package's bin writes the
105
124
  default server into the app, `app/server.ts` or `alxia({ entry })`'s
@@ -110,7 +129,7 @@ Nothing scheduled yet.
110
129
  - **WebSockets in dev and preview.** An alxia `ws` route connects under
111
130
  `react-router dev` and `vite preview` as from the build, with nothing to
112
131
  configure: the plugin relays each upgrade Vite does not claim (HMR, its
113
- proxy) to a `Bun.serve` of the app, so its hooks, refusals,
132
+ proxy) to a `Bun.serve` of the app, so its middlewares, refusals,
114
133
  `socket.data` and handlers run as `listen` runs them, and in dev an edit
115
134
  to the server is used from the next connection.
116
135
  - **Loaders typed with no type argument.** A `Register` declaration beside