@alxia/react-router 0.8.1 → 0.9.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.
Files changed (56) hide show
  1. package/README.md +107 -8
  2. package/dist/chunks/index-zqa30gr7.js +139 -0
  3. package/dist/chunks/index-zqa30gr7.js.map +12 -0
  4. package/dist/pwa/index.d.ts +14 -0
  5. package/dist/pwa/index.d.ts.map +1 -0
  6. package/dist/pwa/index.js +165 -0
  7. package/dist/pwa/index.js.map +14 -0
  8. package/dist/pwa/model/caching.d.ts +46 -0
  9. package/dist/pwa/model/caching.d.ts.map +1 -0
  10. package/dist/pwa/model/clear-app-caches.d.ts +9 -0
  11. package/dist/pwa/model/clear-app-caches.d.ts.map +1 -0
  12. package/dist/pwa/model/manifest.d.ts +80 -0
  13. package/dist/pwa/model/manifest.d.ts.map +1 -0
  14. package/dist/pwa/model/precache-list.d.ts +40 -0
  15. package/dist/pwa/model/precache-list.d.ts.map +1 -0
  16. package/dist/pwa/model/registration.d.ts +39 -0
  17. package/dist/pwa/model/registration.d.ts.map +1 -0
  18. package/dist/pwa/model/start-service-worker.d.ts +24 -0
  19. package/dist/pwa/model/start-service-worker.d.ts.map +1 -0
  20. package/dist/pwa/model/storable.d.ts +8 -0
  21. package/dist/pwa/model/storable.d.ts.map +1 -0
  22. package/dist/pwa/model/update-notice.d.ts +38 -0
  23. package/dist/pwa/model/update-notice.d.ts.map +1 -0
  24. package/dist/pwa/model/worker-contract.d.ts +61 -0
  25. package/dist/pwa/model/worker-contract.d.ts.map +1 -0
  26. package/dist/pwa/react/index.d.ts +9 -0
  27. package/dist/pwa/react/index.d.ts.map +1 -0
  28. package/dist/pwa/react/index.js +75 -0
  29. package/dist/pwa/react/index.js.map +13 -0
  30. package/dist/pwa/react/use-online.d.ts +6 -0
  31. package/dist/pwa/react/use-online.d.ts.map +1 -0
  32. package/dist/pwa/react/use-service-worker.d.ts +33 -0
  33. package/dist/pwa/react/use-service-worker.d.ts.map +1 -0
  34. package/dist/pwa/react/use-update-notice.d.ts +14 -0
  35. package/dist/pwa/react/use-update-notice.d.ts.map +1 -0
  36. package/dist/pwa/server/index.d.ts +7 -0
  37. package/dist/pwa/server/index.d.ts.map +1 -0
  38. package/dist/pwa/server/index.js +18 -0
  39. package/dist/pwa/server/index.js.map +10 -0
  40. package/dist/pwa/server/no-cache.d.ts +22 -0
  41. package/dist/pwa/server/no-cache.d.ts.map +1 -0
  42. package/docs/README.md +12 -3
  43. package/docs/guide/beside-the-pages.md +178 -0
  44. package/docs/guide/csp-nonce.md +100 -0
  45. package/docs/guide/customising-the-server.md +292 -0
  46. package/docs/guide/deploying.md +99 -0
  47. package/docs/guide/getting-started.md +112 -0
  48. package/docs/guide/how-it-works.md +262 -0
  49. package/docs/guide/openapi.md +51 -0
  50. package/docs/guide/pwa.md +475 -0
  51. package/docs/guide/testing.md +42 -0
  52. package/docs/guide/typing-the-loaders.md +311 -0
  53. package/docs/guide.md +27 -1391
  54. package/docs/roadmap.md +25 -1
  55. package/docs/troubleshooting.md +194 -8
  56. package/package.json +26 -4
@@ -0,0 +1,99 @@
1
+ # Deploying
2
+
3
+ This page covers running the build with Bun, and the Docker image.
4
+
5
+ Run `bun run build`, then `bun build/server/index.js`. The build is
6
+ [self-contained](how-it-works.md#self-contained): `build/` is all the server needs, with
7
+ no `node_modules`, unless the app keeps a package external with
8
+ `ssr.external`.
9
+
10
+ ## Docker
11
+
12
+ The `react-router` template of
13
+ [`@alxia/create`](https://github.com/softistx/alxia/tree/develop/packages/create)
14
+ and the [example](https://github.com/softistx/alxia/tree/develop/examples/react-router)
15
+ ship this `Dockerfile`, on Bun's official images: built on `oven/bun:1`,
16
+ Debian's, and run on `oven/bun:1-alpine`, about 130 MB. For an app started from
17
+ `create-react-router`, it replaces the template's, which builds and runs
18
+ on Node:
19
+
20
+ ```dockerfile
21
+ # Bun builds the app, and the image holds build/ alone: alxia's plugin
22
+ # bundles every dependency into build/server/index.js, alxia's server, which
23
+ # runs on Bun with no node_modules. Pinned to Bun 1, the major alxia
24
+ # supports.
25
+
26
+ # Every dependency, then react-router build: build/client and
27
+ # build/server/index.js.
28
+ FROM oven/bun:1 AS build
29
+ WORKDIR /app
30
+ COPY package.json bun.lock* bunfig.toml* ./
31
+ RUN bun install --frozen-lockfile
32
+ COPY . .
33
+ RUN bun run build
34
+
35
+ # The build, run as Bun's own non-root user with the start script's
36
+ # command, and --no-install: with no node_modules, Bun would otherwise
37
+ # fetch a package the build left out from the registry at startup,
38
+ # where this fails.
39
+ # The server listens on PORT (3000) and HOST (0.0.0.0). On Alpine, about
40
+ # 200 MB lighter: build/ is JavaScript, which runs the same on musl. A
41
+ # native addon built for glibc alone needs oven/bun:1 here.
42
+ FROM oven/bun:1-alpine
43
+ WORKDIR /app
44
+ ENV NODE_ENV=production
45
+ COPY --from=build /app/build ./build
46
+ USER bun
47
+ EXPOSE 3000
48
+ CMD ["bun", "--no-install", "build/server/index.js"]
49
+ ```
50
+
51
+ ```sh
52
+ docker build -t my-app .
53
+ docker run -p 3000:3000 my-app
54
+ ```
55
+
56
+ Keep the template's `.dockerignore` beside it: `node_modules`, `build` and
57
+ `.react-router` stay out of the context, so the image installs and builds
58
+ its own. The `bunfig.toml` goes in, and `bun run build` runs React
59
+ Router's CLI on Bun, as it does outside Docker. The image's command is
60
+ `start`'s, run directly, so the platform's `SIGTERM` reaches the server,
61
+ with `--no-install`: a package missing from `build/` then fails at
62
+ startup, where Bun would otherwise fetch it from npm.
63
+
64
+ - **Commit `bun.lock`.** The installs are `--frozen-lockfile`: the image
65
+ gets the versions you tested, and a `package.json` changed without a
66
+ `bun install` fails the build
67
+ ([troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md#error-lockfile-had-changes-but-lockfile-is-frozen)).
68
+ - **The final stage is Alpine's**, musl where the build stage has glibc.
69
+ `build/` is JavaScript, which runs the same on both. A native addon
70
+ kept external and built for glibc alone does not load there: put the
71
+ final stage back on `oven/bun:1`
72
+ ([`@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)).
73
+ - **The server runs as `bun`**, a user with no write access to `/app`.
74
+ Write files to a volume the user owns
75
+ ([troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md#eacces-permission-denied-open-app)).
76
+ - **The image holds `build/` alone**, no `node_modules`: the build is
77
+ [self-contained](how-it-works.md#self-contained). An app that keeps a package external
78
+ with `ssr.external` copies the production dependencies too, from a
79
+ stage of their own:
80
+
81
+ ```dockerfile
82
+ # Before the build stage: the production dependencies alone.
83
+ FROM oven/bun:1 AS production-dependencies
84
+ WORKDIR /app
85
+ COPY package.json bun.lock* bunfig.toml* ./
86
+ RUN bun install --frozen-lockfile --production
87
+
88
+ # In the final stage, beside build/.
89
+ COPY --from=production-dependencies /app/node_modules ./node_modules
90
+ ```
91
+
92
+ Without it, the container stops at startup with
93
+ `Cannot find package '…'`
94
+ ([troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md#error-cannot-find-package--from-appbuildserverindexjs)).
95
+
96
+ `PORT` and `HOST` set where the server listens. The platform's `SIGTERM`
97
+ stops it once the requests in flight are answered, within
98
+ `shutdownTimeout`, and runs the app's `onStop` hooks. Give the platform a
99
+ readiness probe with `@alxia/core`'s `health()`, mounted in `beforeAll`.
@@ -0,0 +1,112 @@
1
+ # Getting started
2
+
3
+ `@alxia/react-router` serves a React Router **framework-mode** app with
4
+ server rendering from an alxia app, under Bun. This page covers starting one:
5
+ a new app with `bun create @alxia`, the official template with alxia added
6
+ to it, or an app you already have.
7
+
8
+ ## A new app: `bun create @alxia`
9
+
10
+ ```sh
11
+ bun create @alxia my-app --template react-router
12
+ cd my-app
13
+ bun dev
14
+ ```
15
+
16
+ [`@alxia/create`](https://www.npmjs.com/package/@alxia/create) copies React
17
+ Router's official template with alxia's layer already applied: the steps
18
+ of the next section, done.
19
+
20
+ - `@alxia/core` and `@alxia/react-router` installed, and every dependency
21
+ moved to the newest release alxia accepts (see
22
+ [`@alxia/create`'s Versions](https://github.com/softistx/alxia/blob/develop/packages/create/docs/guide.md#versions));
23
+ - `alxia()` in `vite.config.ts`, the `bunfig.toml`, and `start` running
24
+ `NODE_ENV=production bun build/server/index.js`;
25
+ - Biome, with `lint`, `format`, `check`, `check:ci` and `verify` scripts;
26
+ - a `Dockerfile` that builds on `oven/bun:1` and runs `build/` alone on
27
+ `oven/bun:1-alpine`, as [Deploying](deploying.md#deploying) describes.
28
+
29
+ `bun run verify` checks, typechecks and builds it.
30
+ [`@alxia/create`'s guide](https://github.com/softistx/alxia/blob/develop/packages/create/docs/guide.md#running-it)
31
+ lists the command's options. The template keeps `@react-router/serve` in its
32
+ dependencies, unused: remove it if you like.
33
+
34
+ ## An existing React Router app
35
+
36
+ Start from React Router's official template, or your own app:
37
+
38
+ ```sh
39
+ bunx create-react-router@latest my-app
40
+ cd my-app
41
+ ```
42
+
43
+ Then make three changes.
44
+
45
+ **1. Install** alxia and this package:
46
+
47
+ ```sh
48
+ bun add @alxia/core @alxia/react-router
49
+ ```
50
+
51
+ **2. Add `alxia()`** to the plugins in `vite.config.ts`:
52
+
53
+ ```ts
54
+ // vite.config.ts
55
+ import { alxia } from '@alxia/react-router/vite';
56
+ import { reactRouter } from '@react-router/dev/vite';
57
+ import tailwindcss from '@tailwindcss/vite';
58
+ import { defineConfig } from 'vite';
59
+
60
+ export default defineConfig({
61
+ plugins: [tailwindcss(), reactRouter(), alxia()],
62
+ resolve: {
63
+ tsconfigPaths: true,
64
+ },
65
+ });
66
+ ```
67
+
68
+ The order does not matter. The plugin always runs before React Router's:
69
+ React Router's build reads its server input from it, and its dev
70
+ middleware must come after alxia's.
71
+
72
+ **3. Start with Bun**, in `package.json`:
73
+
74
+ ```jsonc
75
+ "scripts": {
76
+ "build": "react-router build",
77
+ "dev": "react-router dev",
78
+ "start": "NODE_ENV=production bun build/server/index.js",
79
+ "typecheck": "react-router typegen && tsc"
80
+ }
81
+ ```
82
+
83
+ Only `start` changes. The `react-router` and `vite` CLIs start with
84
+ `#!/usr/bin/env node`, so where a node is installed `bun run` would run
85
+ them on Node, and alxia's server needs Bun. A `bunfig.toml` beside
86
+ `package.json` makes `bun run` start them on Bun, for every script:
87
+
88
+ ```toml
89
+ # bunfig.toml, beside package.json
90
+ [run]
91
+ bun = true
92
+ ```
93
+
94
+ Then `bun run dev`, `bun run build` and `bun run typecheck` work as they
95
+ are, with or without a node installed. Without that file, run them as
96
+ `bun --bun react-router dev`. The plugin refuses a dev or preview server
97
+ running on Node at startup, rather than failing on the first request:
98
+ see [the troubleshooting entry](../troubleshooting.md#alxia-react-router--is-running-on-node-and-alxias-server-runs-on-bun-). The
99
+ template's `@react-router/serve` is no longer used, and you can remove it.
100
+
101
+ What the template keeps:
102
+
103
+ - `@react-router/node`, which React Router's default `entry.server`
104
+ renders with: it streams under Bun.
105
+ - Vite 7 or 8 (the template ships 8).
106
+ - With `bunx create-react-router`, its `typescript` 5.9. This package's
107
+ peer asks for 6 or 7, so `bun add` warns, but the template's code
108
+ typechecks with 5.9. `bun add -d typescript@^6` silences the warning;
109
+ `bun create @alxia` writes 6 or 7 already.
110
+
111
+ React Router 8 is required: its loaders receive a `RouterContextProvider`,
112
+ and alxia's context is set on it.
@@ -0,0 +1,262 @@
1
+ # How it works
2
+
3
+ This page covers what the plugin does in dev, in a build and under
4
+ `vite preview`, and what it sets so the build runs on Bun with no
5
+ `node_modules`.
6
+
7
+ With no `app/server.ts`, the plugin serves the app with
8
+ `createServer()`, from this package, with no options: a fresh alxia app,
9
+ the client's files, and the pages as a catch-all behind it.
10
+
11
+ ## In dev
12
+
13
+ `react-router dev` starts Vite's server. Vite answers its own requests
14
+ first: modules, `/@fs/…`, `public/`, the HMR socket. The plugin hands
15
+ every other request to the server: pages, single-fetch data, and alxia's
16
+ routes. The server is loaded through Vite's SSR runner, so:
17
+
18
+ - **HMR** works as in any React Router app: a component edit reaches the
19
+ browser over Vite's socket.
20
+ - **An edit to `app/server.ts`, or to a module it imports, is live on the
21
+ next request**, with no restart: the runner reloads what changed.
22
+ - **Creating or deleting `app/server.ts`** takes effect on the next
23
+ request.
24
+ - **The app's own context keys work**: the server and the routes are one
25
+ module graph. See [The app's own context keys](typing-the-loaders.md#the-apps-own-context-keys).
26
+ - **Errors** go to Vite's error page, with the stack mapped to the source.
27
+ - **alxia's `ws` routes connect**: an upgrade Vite's HMR does not claim
28
+ goes to the app, as from the build. See [WebSockets](beside-the-pages.md#websockets).
29
+
30
+ HTTP requests reach the app through `app.fetch`, not through `listen`,
31
+ given the loopback server the sockets are relayed to as `ctx.server`
32
+ ([The server](typing-the-loaders.md#the-server-server)). So `page()` is not served in dev; see
33
+ [the troubleshooting entry](../troubleshooting.md#a-page-is-not-served-under-react-router-dev).
34
+
35
+ ## In a build
36
+
37
+ `react-router build` builds the client as usual. It builds the server
38
+ from `app/server.ts`, or from the default server, with React Router's
39
+ server build inside it, into one file:
40
+
41
+ - **`build/server/index.js`'s default export is the alxia app.** Beside it
42
+ are React Router's own exports, so the file is a server build too, and
43
+ React Router's `prerender` reads it. The server file's other exports
44
+ stay out of it.
45
+ - **Run, it listens**: `bun build/server/index.js` listens on `PORT`
46
+ (3000 by default) and `HOST` (`0.0.0.0`). It prints
47
+ `alxia listening on <url>`, and in dev — under `react-router dev`; a
48
+ production build never is, whatever `NODE_ENV` says — the route table
49
+ ([`@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
50
+ down as `@alxia/core`'s `listen` does: readiness turns 503, new
51
+ connections are refused, the requests in flight finish within
52
+ `shutdownTimeout` (10 s; `listen: { shutdownTimeout }` sets it), the
53
+ `onStop` hooks run, and the process exits
54
+ ([core's guide](https://github.com/softistx/alxia/blob/develop/packages/core/docs/guide/health-and-shutdown.md)).
55
+ - **Imported, it starts nothing**: prerendering, a test or another server
56
+ can import it safely, since it listens only when it is the process's
57
+ entry point (`import.meta.main`).
58
+ - **The mode follows the command**: `development` under `react-router dev`,
59
+ `production` in a build, whatever `NODE_ENV` says. The app's dev switch
60
+ follows it: `alxia({ dev: mode === 'development' })`, so a 500 of the
61
+ build never shows its stack.
62
+ - **The client folder is resolved against the built file**: `../client`,
63
+ from React Router's `buildDirectory`, so it is found wherever the
64
+ process starts.
65
+ - **It is self-contained**: every package the server imports, React,
66
+ React Router and alxia included, is bundled into it
67
+ ([Self-contained](#self-contained)), so `build/` runs with no
68
+ `node_modules`.
69
+
70
+ ```sh
71
+ bun run build
72
+ PORT=8080 bun run start
73
+ ```
74
+
75
+ React Router's `serverBundles` split the server build in several, and
76
+ alxia serves one: the plugin refuses them. With `ssr: false`, a
77
+ single-page app, the plugin does nothing.
78
+
79
+ ## Under `vite preview`
80
+
81
+ After a build, Vite's preview server serves the built server:
82
+
83
+ ```sh
84
+ bun run build
85
+ bunx --bun vite preview
86
+ ```
87
+
88
+ `--bun` runs Vite under Bun even where Node is installed, since Vite's bin
89
+ asks for Node: the built server runs inside Vite's process, and it needs
90
+ Bun's APIs. With the [`bunfig.toml`](getting-started.md#an-existing-react-router-app) (`[run]`,
91
+ `bun = true`), or with no Node installed, a `"preview": "vite preview"`
92
+ script run with `bun run preview` works too, as the template's other
93
+ scripts do.
94
+
95
+ The plugin loads `build/server/index.js` on the first request and hands
96
+ every request to its default export, the alxia app, before Vite's own
97
+ files. So the preview answers as `bun run start` does: the pages and their
98
+ data, `/api`, `build/client` with the cache headers of
99
+ [the client's files](beside-the-pages.md#the-clients-files), and every middleware of `beforeAll`
100
+ and `configure`. Requests, their bodies and every `Set-Cookie` pass
101
+ through whole, and pages stream.
102
+
103
+ What differs from `bun run start`:
104
+
105
+ - **Vite listens**, on `preview.port` (4173), `preview.host` and
106
+ `preview.https`. `listen`, `onListen`, `PORT` and `HOST` are not read.
107
+ - **Requests arrive through `app.fetch`**, as under `react-router dev`:
108
+ `page()` and an HTTP request's `ctx.server` are absent. alxia's `ws`
109
+ routes connect, relayed to a `Bun.serve` of the built app as in dev
110
+ ([WebSockets](beside-the-pages.md#websockets)); an upgrade `preview.proxy` relays stays
111
+ Vite's.
112
+ - **Vite's preview options that come after the plugin never run**:
113
+ `preview.proxy` for HTTP requests, `preview.headers` and Vite's file
114
+ serving.
115
+ `preview.cors` and `preview.allowedHosts` still apply.
116
+ - **The build is loaded once**: after `bun run build` again, restart the
117
+ preview.
118
+
119
+ React Router prerenders the `prerender` paths of `react-router.config.ts`
120
+ through the same preview server, during `react-router build`. A
121
+ prerendered page is therefore rendered by the built server too: its loader
122
+ reads `alxiaOf(context)` and `getLoadContext`'s keys, and `beforeAll`'s
123
+ and `configure`'s middlewares run around it.
124
+
125
+ ## Built for Bun
126
+
127
+ `build/server/index.js` runs on Bun, and the plugin builds it for Bun,
128
+ with nothing to configure. It adds to Vite's `ssr` environment, under
129
+ `react-router dev` and `react-router build` alike but for the last row,
130
+ the build's alone:
131
+
132
+ | option | what the plugin adds | why |
133
+ | --- | --- | --- |
134
+ | `resolve.conditions` | `bun` | a package bundled into the server whose `exports` has a `bun` condition is bundled as its Bun variant |
135
+ | `resolve.externalConditions` | `bun` | a dependency left external, as every one is in dev and one the app names in `ssr.external` is in the build, is loaded as its Bun variant, as Bun loads it |
136
+ | `resolve.builtins` | `bun`, and `bun:*` (`bun:sqlite`, `bun:ffi`, …) | Bun's own modules stay imports of the build, whichever runtime runs Vite. Vite knows `bun:*`, and bare `bun` only when it runs on Bun |
137
+ | `build.target` | `esnext` | the newest syntax is left as written: Bun runs it, and Vite's default targets browsers |
138
+ | `resolve.noExternal` | `true`, under `react-router build` only | every package is bundled into `build/server/index.js`, so `build/` runs with no `node_modules` ([Self-contained](#self-contained)) |
139
+
140
+ A package written for Bun, then, gives the build the variant Bun would
141
+ load, and Bun's own modules are used as they are:
142
+
143
+ ```jsonc
144
+ // node_modules/some-package/package.json
145
+ { "exports": { "bun": "./bun.js", "default": "./node.js" } }
146
+ ```
147
+
148
+ ```ts
149
+ // app/server.ts
150
+ import { createServer } from '@alxia/react-router';
151
+ import { Database } from 'bun:sqlite';
152
+
153
+ const db = new Database(':memory:');
154
+
155
+ export default createServer({
156
+ configure: (app) =>
157
+ app.get('/api/count', ({ reply }) =>
158
+ reply.ok(db.query('select sqlite_version() as version').get()),
159
+ ),
160
+ });
161
+ ```
162
+
163
+ ```js
164
+ // build/server/index.js, in part
165
+ import { Database } from "bun:sqlite";
166
+ ```
167
+
168
+ Which variant wins is still the package's own order: `exports` is read
169
+ top to bottom, and the first condition the build has wins. A package that
170
+ lists `node` before `bun` gives its Node variant, as Bun itself would load
171
+ it.
172
+
173
+ What the plugin leaves alone:
174
+
175
+ - **`ssr.target` stays `node`.** Bun runs Node's modules, and `node`
176
+ keeps `node:*` external and resolves packages as a server does;
177
+ `webworker` would bundle every dependency with the browser's conditions.
178
+ An app that sets `ssr.target: 'webworker'` itself gets none of the
179
+ above: that environment keeps Vite's own defaults.
180
+ - **No polyfill is added.** Vite adds none to a server build, and the
181
+ plugin adds none.
182
+ - **The client build** is a browser's, as before: none of this reaches
183
+ it. A Bun module imported by code that reaches the client is replaced
184
+ by an empty module there: Vite 7 fails the build, Vite 8 fails in the
185
+ browser ([troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md#module--has-been-externalized-for-browser-compatibility-imported-by-)).
186
+
187
+ What the app sets wins. The plugin adds to the `ssr` environment once
188
+ every plugin's `config` has run, and Vite merges what it adds by
189
+ concatenating arrays, so conditions and builtins of the app's own are
190
+ kept, and `bun` is added only when they lack it. A `build.target`, at the
191
+ top level or on the environment, is kept as it is:
192
+
193
+ ```ts
194
+ // vite.config.ts
195
+ export default defineConfig({
196
+ // Kept: the build resolves `worker`, then `bun`, and targets es2022.
197
+ environments: {
198
+ ssr: {
199
+ resolve: { conditions: ['worker'] },
200
+ build: { target: 'es2022' },
201
+ },
202
+ },
203
+ plugins: [reactRouter(), alxia()],
204
+ });
205
+ ```
206
+
207
+ `ssr.resolve.conditions` at the top level is the same option, and is kept
208
+ the same way.
209
+
210
+ ### Self-contained
211
+
212
+ Under `react-router build`, the plugin sets `resolve.noExternal: true` on
213
+ the `ssr` environment: Vite bundles every package the server imports into
214
+ `build/server/index.js`, where it would otherwise leave each one under
215
+ `node_modules` as an `import`. What the file still imports is Node's and
216
+ Bun's own modules:
217
+
218
+ ```js
219
+ // build/server/index.js, its imports
220
+ import { createRequire } from "node:module";
221
+ import { PassThrough } from "node:stream";
222
+ // … and no "react", "react-router" or "@alxia/core"
223
+ ```
224
+
225
+ So `build/` is all a server needs, beside Bun: a Docker image's last
226
+ stage copies it alone ([Docker](deploying.md#docker)). React and `react-dom/server`,
227
+ React Router, alxia and its plugins bundle as they are, CommonJS included;
228
+ the `bun` condition above picks each package's Bun variant. One copy of
229
+ each package is in the file, so `alxiaContext` and the app's own keys are
230
+ one object for the server and the routes. React Router's prerendering and
231
+ `vite preview` load the same file.
232
+
233
+ `react-router dev` is left as it was: Vite's SSR runner loads the
234
+ packages from `node_modules`.
235
+
236
+ What the app sets wins. Vite reads `external` before `noExternal`:
237
+
238
+ - **`ssr.external: ['sharp']`**, or the same on
239
+ `environments.ssr.resolve.external`, keeps those packages external,
240
+ imported from `node_modules` at runtime. Use it for a package that
241
+ cannot be bundled: a native addon (a `.node` file), or one that reads
242
+ files of its own folder at runtime. The image must then hold that
243
+ package ([Docker](deploying.md#docker)).
244
+ - **`ssr.external: true`** keeps every package external, Vite's own
245
+ behaviour: the plugin adds no `noExternal` then, and the server needs
246
+ the production `node_modules` beside `build/`.
247
+ - **`ssr.noExternal`**, a list, changes nothing: everything is bundled
248
+ already.
249
+
250
+ ```ts
251
+ // vite.config.ts
252
+ export default defineConfig({
253
+ // Bundled but for sharp, a native addon.
254
+ ssr: { external: ['sharp'] },
255
+ plugins: [reactRouter(), alxia()],
256
+ });
257
+ ```
258
+
259
+ A package the build could not bundle and was not told to leave external
260
+ fails at runtime, from `build/` alone, with Bun's
261
+ `Cannot find package '…'`: see
262
+ [the troubleshooting entry](../troubleshooting.md#error-cannot-find-package--from-appbuildserverindexjs).
@@ -0,0 +1,51 @@
1
+ # OpenAPI
2
+
3
+ This page covers writing the API of an app that serves pages spec first:
4
+ binding its routes to the generated operations, and checking them with
5
+ `matchesSpec`.
6
+
7
+ alxia is OpenAPI spec first: the document is written by hand, the API's
8
+ routes are bound to the operations generated from it, and
9
+ [`@alxia/openapi`](https://www.npmjs.com/package/@alxia/openapi)'s
10
+ `matchesSpec` checks, in a test, that every operation has its route; with
11
+ `strict: true`, that `app.routes` and the operations match both ways. The
12
+ catch-all and the client's files are routes of `app.routes`, and no
13
+ operation of the document. Under `strict`, leave them out with
14
+ `isReactRouterRoute`, which is true for each route this package declared:
15
+
16
+ ```ts
17
+ // app/server.ts
18
+ import { createServer } from '@alxia/react-router';
19
+ import { operations } from './generated/alxia';
20
+
21
+ export default createServer({
22
+ configure: (app) =>
23
+ app
24
+ .get('/api/health', ({ reply }) => reply.ok({ ok: true }))
25
+ .route(operations.listOrders, ({ reply }) => reply.ok([])),
26
+ });
27
+ ```
28
+
29
+ ```ts
30
+ // app/server.test.ts
31
+ import { test } from 'bun:test';
32
+ import { matchesSpec } from '@alxia/openapi';
33
+ import { isReactRouterRoute } from '@alxia/react-router';
34
+ import type { ServerBuild } from 'react-router';
35
+ import { operations } from './generated/alxia';
36
+ import server from './server';
37
+
38
+ test('the API routes every operation of openapi.yaml, and nothing else', async () => {
39
+ const build: ServerBuild = await import(new URL('../build/server/index.js', import.meta.url).href);
40
+ const app = server.create({ build });
41
+ matchesSpec(app, operations, {
42
+ strict: true,
43
+ exclude: (route) => isReactRouterRoute(route) || route.path === '/api/health',
44
+ });
45
+ });
46
+ ```
47
+
48
+ When the app puts the operations under a prefix the document's paths
49
+ leave out, `app.group('/api', (api) => api.route(…))`, give `matchesSpec`
50
+ the same `prefix: '/api'`. See
51
+ [`@alxia/openapi`'s matching](https://github.com/softistx/alxia/blob/develop/packages/openapi/docs/guide/matching.md#under-a-prefix).