@alxia/react-router 0.1.2 → 0.3.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 +128 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +4 -3
- package/dist/nonce.d.ts +16 -0
- package/dist/nonce.d.ts.map +1 -0
- package/dist/vite/bun.d.ts +21 -0
- package/dist/vite/bun.d.ts.map +1 -0
- package/dist/vite/index.d.ts +3 -0
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +40 -1
- package/dist/vite/index.js.map +5 -4
- package/docs/README.md +2 -2
- package/docs/guide.md +247 -8
- package/docs/roadmap.md +19 -3
- package/docs/troubleshooting.md +168 -3
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -151,6 +151,53 @@ How `alxiaOf(context)` is typed:
|
|
|
151
151
|
|
|
152
152
|
[More](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide.md#typing-the-loaders)
|
|
153
153
|
|
|
154
|
+
## A CSP nonce
|
|
155
|
+
|
|
156
|
+
With `secureHeaders({ nonce: true })` from `@alxia/secure-headers` in
|
|
157
|
+
`configure`, each request's policy names a fresh nonce, and the context
|
|
158
|
+
carries it. `nonceOf(loadContext)` reads it in `entry.server.tsx`, and React
|
|
159
|
+
Router puts it on every script it renders, so `script-src` needs no
|
|
160
|
+
`'unsafe-inline'`:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
// app/server.ts
|
|
164
|
+
configure: (app) =>
|
|
165
|
+
app.use(
|
|
166
|
+
secureHeaders({
|
|
167
|
+
nonce: true,
|
|
168
|
+
contentSecurityPolicy:
|
|
169
|
+
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; form-action 'self'; base-uri 'self'; frame-ancestors 'none'",
|
|
170
|
+
}),
|
|
171
|
+
),
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The template has no `entry.server.tsx`; reveal React Router's, then add
|
|
175
|
+
three lines:
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
bunx react-router reveal entry.server
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
```diff
|
|
182
|
+
// app/entry.server.tsx, as reveal writes it
|
|
183
|
+
import { PassThrough } from "node:stream";
|
|
184
|
+
|
|
185
|
+
+import { nonceOf } from "@alxia/react-router";
|
|
186
|
+
import type { EntryContext, RouterContextProvider } from "react-router";
|
|
187
|
+
…
|
|
188
|
+
const { pipe, abort } = renderToPipeableStream(
|
|
189
|
+
- <ServerRouter context={routerContext} url={request.url} />,
|
|
190
|
+
+ <ServerRouter context={routerContext} url={request.url} nonce={nonceOf(loadContext)} />,
|
|
191
|
+
{
|
|
192
|
+
+ nonce: nonceOf(loadContext),
|
|
193
|
+
[readyOption]() {
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`nonceOf` returns `undefined` when no hook set a nonce, so the entry works
|
|
197
|
+
with or without secure-headers: neither package depends on the other. Any
|
|
198
|
+
`derive` that returns a string `nonce` works the same.
|
|
199
|
+
[More](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide.md#a-csp-nonce)
|
|
200
|
+
|
|
154
201
|
## WebSockets
|
|
155
202
|
|
|
156
203
|
A `ws` route in `configure` connects under `react-router dev`, under
|
|
@@ -230,6 +277,81 @@ The catch-all adds nothing to the app's route table, so the typed client
|
|
|
230
277
|
never shows it. `isReactRouterRoute` names the catch-all and the client
|
|
231
278
|
files, so `@alxia/openapi` can leave them out.
|
|
232
279
|
|
|
280
|
+
## Built for Bun
|
|
281
|
+
|
|
282
|
+
The server build runs on Bun, so the plugin builds it for Bun, with
|
|
283
|
+
nothing to configure. In Vite's `ssr` environment, in dev and in the
|
|
284
|
+
build, it adds:
|
|
285
|
+
|
|
286
|
+
- the `bun` export condition, to `resolve.conditions` and
|
|
287
|
+
`resolve.externalConditions`: a package that exports a `bun` variant is
|
|
288
|
+
bundled, and loaded in dev, as that variant;
|
|
289
|
+
- `bun` and `bun:*` to `resolve.builtins`: Bun's own modules stay imports
|
|
290
|
+
of `build/server/index.js`;
|
|
291
|
+
- `build.target: 'esnext'`, where Vite's default targets browsers.
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
// app/server.ts: Bun's modules, bundled as they are
|
|
295
|
+
import { createServer } from '@alxia/react-router';
|
|
296
|
+
import { Database } from 'bun:sqlite';
|
|
297
|
+
|
|
298
|
+
const db = new Database(':memory:');
|
|
299
|
+
|
|
300
|
+
export default createServer({
|
|
301
|
+
configure: (app) =>
|
|
302
|
+
app.get('/api/sqlite', ({ reply }) =>
|
|
303
|
+
reply.ok(db.query('select sqlite_version() as version').get()),
|
|
304
|
+
),
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
What the app sets wins: its own conditions and builtins are kept, `bun` is
|
|
309
|
+
added beside them, and a `build.target` it set is left as it is.
|
|
310
|
+
`ssr.target` stays `node`, and no polyfill is added; an app that sets
|
|
311
|
+
`ssr.target: 'webworker'` itself gets none of this, only Vite's defaults. The
|
|
312
|
+
[guide](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide.md#built-for-bun)
|
|
313
|
+
has each option and why.
|
|
314
|
+
|
|
315
|
+
## Docker
|
|
316
|
+
|
|
317
|
+
A multi-stage `Dockerfile` on Bun's official image, as `@alxia/create`'s
|
|
318
|
+
`react-router` template ships it. For an app from `create-react-router`,
|
|
319
|
+
it replaces the template's, which runs on Node:
|
|
320
|
+
|
|
321
|
+
```dockerfile
|
|
322
|
+
FROM oven/bun:1 AS production-dependencies
|
|
323
|
+
WORKDIR /app
|
|
324
|
+
COPY package.json bun.lock* bunfig.toml* ./
|
|
325
|
+
RUN bun install --frozen-lockfile --production
|
|
326
|
+
|
|
327
|
+
FROM oven/bun:1 AS build
|
|
328
|
+
WORKDIR /app
|
|
329
|
+
COPY package.json bun.lock* bunfig.toml* ./
|
|
330
|
+
RUN bun install --frozen-lockfile
|
|
331
|
+
COPY . .
|
|
332
|
+
RUN bun run build
|
|
333
|
+
|
|
334
|
+
FROM oven/bun:1
|
|
335
|
+
WORKDIR /app
|
|
336
|
+
ENV NODE_ENV=production
|
|
337
|
+
COPY package.json ./
|
|
338
|
+
COPY --from=production-dependencies /app/node_modules ./node_modules
|
|
339
|
+
COPY --from=build /app/build ./build
|
|
340
|
+
USER bun
|
|
341
|
+
EXPOSE 3000
|
|
342
|
+
CMD ["bun", "build/server/index.js"]
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
```sh
|
|
346
|
+
docker build -t my-app .
|
|
347
|
+
docker run -p 3000:3000 my-app
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Keep the template's `.dockerignore`, and commit `bun.lock`. The server
|
|
351
|
+
runs as the non-root `bun` user, on `PORT` (3000). The
|
|
352
|
+
[guide](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide.md#docker)
|
|
353
|
+
has the commented file.
|
|
354
|
+
|
|
233
355
|
## Traps
|
|
234
356
|
|
|
235
357
|
- **A context key made in `app/` reaches the loaders only through the
|
|
@@ -241,7 +363,8 @@ files, so `@alxia/openapi` can leave them out.
|
|
|
241
363
|
- **An index route's action is `POST /?index`**; `POST /` gets React
|
|
242
364
|
Router's 405. [More](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md#you-made-a-post-request-to--but-did-not-provide-an-action-for-route-root-so-there-is-no-way-to-handle-the-request)
|
|
243
365
|
- **`@alxia/secure-headers`' default policy blocks the page's scripts**
|
|
244
|
-
and forms: give the pages a policy of their own
|
|
366
|
+
and forms: give the pages a policy of their own, with `nonce: true` and
|
|
367
|
+
`nonceOf` in `entry.server.tsx` rather than `'unsafe-inline'`. [More](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md#refused-to-execute-inline-script-because-it-violates-the-following-content-security-policy-directive-default-src-none)
|
|
245
368
|
- **Under `react-router dev`, `page()` and an HTTP request's
|
|
246
369
|
`ctx.server` are absent**: requests arrive through `app.fetch`. A
|
|
247
370
|
socket's upgrade has its server. [More](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md#ctxserver-is-undefined-under-react-router-dev)
|
|
@@ -265,6 +388,7 @@ files, so `@alxia/openapi` can leave them out.
|
|
|
265
388
|
| `InvalidRegister` | what a `Register` naming neither a server nor an app reads as: every key of the app's own a compile error |
|
|
266
389
|
| `AppOf<Server>` | the app a server makes |
|
|
267
390
|
| `alxiaContext` | the React Router context key `alxiaOf` reads, set on every request |
|
|
391
|
+
| `nonceOf(context)` | the request's CSP nonce, for `entry.server.tsx`: the context's `nonce` when a hook set one, such as `secureHeaders({ nonce: true })`, else `undefined` |
|
|
268
392
|
| `reactRouter(app, options)` | the catch-all and the client's files, for a server of your own. `build`, `mode`, `getLoadContext`, `client` |
|
|
269
393
|
| `ReactRouterOptions<Ctx>` | its options |
|
|
270
394
|
| `isReactRouterRoute(route)` | whether this package declared a route, for OpenAPI's `exclude` |
|
|
@@ -273,7 +397,7 @@ From `@alxia/react-router/vite`:
|
|
|
273
397
|
|
|
274
398
|
| export | |
|
|
275
399
|
| --- | --- |
|
|
276
|
-
| `alxia(options?)` | the Vite plugin, for `react-router dev`, `react-router build` and `vite preview
|
|
400
|
+
| `alxia(options?)` | the Vite plugin, for `react-router dev`, `react-router build` and `vite preview`, building the server for Bun. `entry` is the server file: `app/server.ts` by default, or the default server when there is none |
|
|
277
401
|
| `AlxiaOptions` | its options |
|
|
278
402
|
|
|
279
403
|
The `alxia-react-router` bin, run with `bunx`:
|
|
@@ -284,7 +408,7 @@ The `alxia-react-router` bin, run with `bunx`:
|
|
|
284
408
|
|
|
285
409
|
## Documentation
|
|
286
410
|
|
|
287
|
-
- [Guide](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide.md): the setup, how dev, the build and `vite preview` work, customising the server, typing the loaders, the app's own keys, escape hatches, WebSockets, the client's files, OpenAPI, testing and deploying.
|
|
411
|
+
- [Guide](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide.md): the setup, how dev, the build and `vite preview` work, what the build sets for Bun, customising the server, typing the loaders, the app's own keys, a CSP nonce, escape hatches, WebSockets, the client's files, OpenAPI, testing and deploying, with Docker.
|
|
288
412
|
- [Troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md): each message, and the traps that print none.
|
|
289
413
|
- [Roadmap](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/roadmap.md): what is coming, and what is not planned.
|
|
290
|
-
- [Example](https://github.com/softistx/alxia/tree/develop/examples/react-router): the official template, these
|
|
414
|
+
- [Example](https://github.com/softistx/alxia/tree/develop/examples/react-router): the official template, these four changes, then an `app/server.ts` with a session, an `/api`, secure headers with a nonce and a streamed page.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { type AppOf, alxiaContext, alxiaOf, type InvalidRegister, type Register, type RegisteredApp, type RegisteredOf, } from './context';
|
|
2
|
+
export { nonceOf } from './nonce';
|
|
2
3
|
export { isReactRouterRoute, type ReactRouterOptions, reactRouter, } from './react-router';
|
|
3
4
|
export { createServer, type FreshApp, type ReactRouterServer, type ServerOptions, type ServerWiring, } from './server';
|
|
4
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,KAAK,EACV,YAAY,EACZ,OAAO,EACP,KAAK,eAAe,EACpB,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,YAAY,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EACN,kBAAkB,EAClB,KAAK,kBAAkB,EACvB,WAAW,GACX,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACN,YAAY,EACZ,KAAK,QAAQ,EACb,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,YAAY,GACjB,MAAM,UAAU,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,KAAK,EACV,YAAY,EACZ,OAAO,EACP,KAAK,eAAe,EACpB,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,YAAY,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EACN,kBAAkB,EAClB,KAAK,kBAAkB,EACvB,WAAW,GACX,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACN,YAAY,EACZ,KAAK,QAAQ,EACb,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,YAAY,GACjB,MAAM,UAAU,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,14 @@ function alxiaOf(context) {
|
|
|
9
9
|
}
|
|
10
10
|
return value;
|
|
11
11
|
}
|
|
12
|
+
// src/nonce.ts
|
|
13
|
+
function nonceOf(context) {
|
|
14
|
+
const value = context.get(alxiaContext);
|
|
15
|
+
if (typeof value !== "object" || value === null || !("nonce" in value)) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
return typeof value.nonce === "string" ? value.nonce : undefined;
|
|
19
|
+
}
|
|
12
20
|
// src/react-router.ts
|
|
13
21
|
import { fileURLToPath } from "node:url";
|
|
14
22
|
import {
|
|
@@ -159,8 +167,9 @@ export {
|
|
|
159
167
|
alxiaOf,
|
|
160
168
|
createServer,
|
|
161
169
|
isReactRouterRoute,
|
|
170
|
+
nonceOf,
|
|
162
171
|
reactRouter
|
|
163
172
|
};
|
|
164
173
|
|
|
165
|
-
//# debugId=
|
|
174
|
+
//# debugId=63BB3B2311BA25F964756E2164756E21
|
|
166
175
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/context.ts", "../src/react-router.ts", "../src/assets.ts", "../src/server.ts"],
|
|
3
|
+
"sources": ["../src/context.ts", "../src/nonce.ts", "../src/react-router.ts", "../src/assets.ts", "../src/server.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"/**\n * The key under which `reactRouter()` hands every loader, action and\n * middleware the request's alxia context, and the typed way to read it.\n */\nimport type { Alxia, AnyAlxia, ContextOf, Empty } from '@alxia/core';\nimport { createContext, type RouterContextProvider } from 'react-router';\nimport type { FreshApp, ReactRouterServer } from './server';\n\n/** The default of the key: no catch-all set it. */\nconst MISSING: unique symbol = Symbol('alxia context missing');\n\n/**\n * The key `reactRouter()` sets on React Router's context provider, on every\n * request, to what alxia's hooks built for it. It lives in this package, so\n * it is one object whichever way the server was built or loaded: a key made\n * in the app's own `app/` folder is copied into React Router's build, and\n * the server that imports it separately sets a different one.\n */\nexport const alxiaContext = createContext<unknown>(MISSING);\n\n/**\n * Names the server whose context `alxiaOf(context)` reads when given no\n * type argument. One React Router build has one server, so it may be\n * declared once, beside it:\n *\n * ```ts\n * // app/server.ts\n * const server = createServer({ configure: (app) => app.use(session) });\n * export default server;\n *\n * declare module '@alxia/react-router' {\n * interface Register {\n * server: typeof server;\n * }\n * }\n * ```\n *\n * Unregistered, `alxiaOf(context)` reads `BaseContext`.\n */\n// biome-ignore lint/suspicious/noEmptyInterface: an app augments it\nexport interface Register {}\n\n/** The app a server makes, or the app itself. */\nexport type AppOf<Server> =\n\tServer extends ReactRouterServer<infer App> ? App : Server;\n\n/**\n * What `Register` names when it is neither a server nor an app — the\n * module, say, rather than its default export: an app whose context has\n * nothing but this key, so that reading anything of it is a compile error.\n */\nexport type InvalidRegister = Alxia<\n\t{\n\t\treadonly 'Register.server must be typeof server, the default export of createServer()': never;\n\t},\n\tEmpty,\n\t'',\n\tnever\n>;\n\n/** The app `alxiaOf` reads for a `Register` interface: its server's, a fresh one, or `InvalidRegister`. */\nexport type RegisteredOf<R> = R extends { readonly server: infer Server }\n\t? Server extends AnyAlxia | ReactRouterServer<AnyAlxia>\n\t\t? AppOf<Server>\n\t\t: InvalidRegister\n\t: FreshApp;\n\n/** What `alxiaOf` reads with no type argument: the registered server's app, or a fresh one. */\nexport type RegisteredApp = RegisteredOf<Register>;\n\n/**\n * What alxia's hooks built for this request, read in a loader, an action or\n * a middleware, typed by the app: the server `Register` names, or the one\n * given as the type argument — `typeof server`, or an app *before* the\n * catch-all. With neither, `BaseContext`.\n *\n * ```ts\n * export async function loader({ context }: Route.LoaderArgs) {\n * const { user, log } = alxiaOf(context);\n * }\n * ```\n *\n * Throws when the request did not come through `reactRouter()`: under\n * `react-router dev` without the plugin, or a test that calls a loader\n * directly.\n */\nexport function alxiaOf<\n\tApp extends AnyAlxia | ReactRouterServer<AnyAlxia> = RegisteredApp,\n>(context: Readonly<RouterContextProvider>): ContextOf<AppOf<App>> {\n\tconst value = context.get(alxiaContext);\n\tif (value === MISSING) {\n\t\tthrow new Error(\n\t\t\t\"alxiaOf(): this request has no alxia context. Serve the React Router app through alxia: add alxia() from @alxia/react-router/vite to vite.config.ts's plugins, or, with a server of your own, serve the build through reactRouter() from @alxia/react-router.\",\n\t\t);\n\t}\n\treturn value as ContextOf<AppOf<App>>;\n}\n",
|
|
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",
|
|
6
7
|
"import { fileURLToPath } from 'node:url';\nimport type {\n\tAlxia,\n\tAnyAlxia,\n\tAnyReply,\n\tBaseContext,\n\tMaybePromise,\n\tRouteDefinition,\n\tStatusCode,\n} from '@alxia/core';\nimport {\n\tcreateRequestHandler,\n\ttype RequestHandler,\n\tRouterContextProvider,\n\ttype ServerBuild,\n} from 'react-router';\nimport { serveClient } from './assets';\nimport { alxiaContext } from './context';\n\nexport interface ReactRouterOptions<Ctx> {\n\t/**\n\t * React Router's server build: the module itself, or a function that\n\t * returns it — `() => import('virtual:react-router/server-build')` in a\n\t * server entry Vite builds, `() => import('./build/server/index.js')`\n\t * beside a build. A function is called on every request in\n\t * `development`, so an edit is picked up, and once in `production`.\n\t */\n\treadonly build: ServerBuild | (() => MaybePromise<ServerBuild>);\n\t/** React Router's server mode: `production` by default. */\n\treadonly mode?: 'development' | 'production';\n\t/**\n\t * Sets the app's own keys on React Router's context provider, from the\n\t * context alxia's hooks built. `ctx` is typed by the app at the point of\n\t * `use`: reading what no hook before it derives is a compile error.\n\t * `alxiaContext` is always set, whether or not this is given.\n\t */\n\treadonly getLoadContext?: (\n\t\tctx: BaseContext & Ctx,\n\t\tcontext: RouterContextProvider,\n\t) => MaybePromise<void>;\n\t/**\n\t * The client build's folder, `build/client`: a path or a `file:` URL.\n\t * Its `assets/` is served immutable, and every other top-level file or\n\t * folder — the copies of `public/` — with an hour's cache, before the\n\t * catch-all. Ignored in `development`, where Vite serves them.\n\t */\n\treadonly client?: string | URL;\n}\n\n/** The methods the catch-all takes. A `HEAD` reaches its `GET`. */\nconst METHODS = ['get', 'post', 'put', 'patch', 'delete'] as const;\n\n/** The handlers `reactRouter()` declared: the catch-all's and the client build's. */\nconst declared = new WeakSet<RouteDefinition['handler']>();\n\n/**\n * Whether `reactRouter()` declared this route: the catch-all, or one of the\n * client build's files. For `@alxia/openapi`'s `exclude`, so the document\n * lists the app's API and not its pages.\n *\n * ```ts\n * app.use(docs(app, { info, exclude: isReactRouterRoute }));\n * ```\n */\nexport function isReactRouterRoute(route: RouteDefinition): boolean {\n\treturn declared.has(route.handler);\n}\n\n/**\n * A React Router framework app, server rendered on `app`: `GET`, `POST`,\n * `PUT`, `PATCH` and `DELETE` at `/*`, behind every hook declared on `app`\n * before it. Each loader, action and middleware reads what those hooks\n * built through `alxiaOf<App>(context)`. The app's own routes — an `/api`\n * — answer their paths, declared before it or after; the client build's\n * files are served when `client` is given. The catch-all adds nothing to\n * the app's route table: pages are not something the typed client calls.\n *\n * ```ts\n * const app = base.use((app) =>\n * reactRouter(app, { build: () => import('./build/server/index.js'), client: 'build/client' }),\n * );\n * ```\n */\nexport function reactRouter<\n\tCtx extends object,\n\tRoutes extends object,\n\tPrefix extends string,\n\tShortcuts extends AnyReply,\n>(\n\tapp: Alxia<Ctx, Routes, Prefix, Shortcuts>,\n\toptions: ReactRouterOptions<Ctx>,\n): Alxia<Ctx, Routes, Prefix, Shortcuts> {\n\tconst mode = options.mode ?? 'production';\n\tconst handle = requestHandler(options.build, mode);\n\tconst getLoadContext = options.getLoadContext;\n\tconst handler = async (ctx: BaseContext & Ctx) => {\n\t\tconst context = new RouterContextProvider();\n\t\tcontext.set(alxiaContext, ctx);\n\t\tawait getLoadContext?.(ctx, context);\n\t\t// React Router answers a HEAD with no headers at all: hand it the GET,\n\t\t// and the core drops the body.\n\t\tconst request =\n\t\t\tctx.request.method === 'HEAD'\n\t\t\t\t? new Request(ctx.request, { method: 'GET' })\n\t\t\t\t: ctx.request;\n\t\tconst response = await handle(request, context);\n\t\treturn ctx.reply(\n\t\t\tresponse.status as StatusCode,\n\t\t\tresponse.body ?? undefined,\n\t\t\t{ headers: response.headers },\n\t\t);\n\t};\n\n\tconst routes = asRoutes(app);\n\tdeclaring(app, () => {\n\t\tif (options.client !== undefined && mode === 'production') {\n\t\t\tserveClient(routes, pathOf(options.client));\n\t\t}\n\t\tfor (const method of METHODS) routes[method]('/*', handler);\n\t});\n\treturn app;\n}\n\n/**\n * Declares the client build's files on `app`, marked for\n * `isReactRouterRoute`: what `reactRouter()` does with `client`, apart, so\n * that `createServer()` serves them before the hooks of `configure`.\n */\nexport function declareClient(app: AnyAlxia, client: string | URL): void {\n\tdeclaring(app, () => serveClient(asRoutes(app), pathOf(client)));\n}\n\n/** Runs `declare`, and marks every route it added to `app` as ours. */\nfunction declaring(app: AnyAlxia, declare: () => void): void {\n\tconst before = new Set(app.routes);\n\tdeclare();\n\tfor (const route of app.routes) {\n\t\tif (!before.has(route)) declared.add(route.handler);\n\t}\n}\n\n/** The app's route methods, untyped: the catch-all's handler is not one a route type admits. */\nfunction asRoutes(app: AnyAlxia) {\n\treturn app as unknown as {\n\t\tstatic(path: string, source: string, options: object): unknown;\n\t\tfile(path: string, file: string, options: object): unknown;\n\t} & Record<\n\t\t(typeof METHODS)[number],\n\t\t(path: string, handler: unknown) => unknown\n\t>;\n}\n\n/** A handler that resolves a function build once in production, on every request in development. */\nfunction requestHandler(\n\tbuild: ReactRouterOptions<never>['build'],\n\tmode: 'development' | 'production',\n): RequestHandler {\n\tif (typeof build !== 'function' || mode === 'development') {\n\t\treturn createRequestHandler(build, mode);\n\t}\n\tlet resolved: Promise<RequestHandler> | undefined;\n\treturn async (request, context) => {\n\t\tresolved ??= Promise.resolve(build()).then(\n\t\t\t(server) => createRequestHandler(server, mode),\n\t\t\t(error: unknown) => {\n\t\t\t\tresolved = undefined;\n\t\t\t\tthrow error;\n\t\t\t},\n\t\t);\n\t\treturn (await resolved)(request, context);\n\t};\n}\n\nfunction pathOf(client: string | URL): string {\n\treturn client instanceof URL ? fileURLToPath(client) : client;\n}\n",
|
|
7
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",
|
|
8
9
|
"/**\n * `createServer()`: the alxia server of a React Router app, as\n * `@alxia/react-router/vite` builds it. The plugin hands it React Router's\n * build, the mode and the client folder; the app's own `app/server.ts`\n * customises the rest, or is not written at all.\n */\nimport {\n\ttype Alxia,\n\ttype AnyAlxia,\n\talxia,\n\ttype ContextOf,\n\ttype Empty,\n\ttype ListenOptions,\n\ttype MaybePromise,\n} from '@alxia/core';\nimport type { RouterContextProvider, ServerBuild } from 'react-router';\nimport { declareClient, reactRouter } from './react-router';\n\n/** An app as `alxia()` makes it: what `beforeAll`, or `configure`, receives. */\nexport type FreshApp = Alxia<Empty, Empty, '', never>;\n\n/**\n * What the Vite plugin hands the server, and a test passes to `create`:\n * React Router's server build, its mode, and the client build's folder.\n */\nexport interface ServerWiring {\n\t/**\n\t * React Router's server build, or a function that returns it: called on\n\t * every request in `development`, once in `production`.\n\t */\n\treadonly build: ServerBuild | (() => MaybePromise<ServerBuild>);\n\t/** `development` under `react-router dev`, `production` in a build, and by default. */\n\treadonly mode?: 'development' | 'production';\n\t/** The client build's folder, a path or a `file:` URL: served in `production`. */\n\treadonly client?: string | URL | undefined;\n}\n\nexport interface ServerOptions<Before extends AnyAlxia, App extends AnyAlxia> {\n\t/**\n\t * Runs first, on a new app: what is declared here runs before the\n\t * client's files too — a guard, a rate limit, a logger that should see\n\t * every request. Returns the app, so its types flow to `configure`.\n\t */\n\treadonly beforeAll?: (app: FreshApp) => Before;\n\t/**\n\t * The app the pages run behind: its plugins, its hooks, its `/api`.\n\t * Declared after the client's files and before the catch-all. Returns\n\t * the app: what it builds is what the loaders read through `alxiaOf`.\n\t */\n\treadonly configure?: (app: Before) => App;\n\t/**\n\t * Sets the app's own keys on React Router's context provider, `ctx`\n\t * typed by `configure`'s app. `alxiaContext` is always set.\n\t */\n\treadonly getLoadContext?: (\n\t\tctx: ContextOf<App>,\n\t\tcontext: RouterContextProvider,\n\t) => MaybePromise<void>;\n\t/** Overrides the plugin's server build: rarely wanted. */\n\treadonly build?: ServerWiring['build'];\n\t/** Overrides the plugin's mode, `development` in dev and `production` in a build. */\n\treadonly mode?: 'development' | 'production';\n\t/**\n\t * Overrides the plugin's client folder, `build/client` beside the built\n\t * server. `false` serves none of it: declare the files yourself, in\n\t * `beforeAll` or `configure`.\n\t */\n\treadonly client?: string | URL | false;\n\t/**\n\t * `listen`'s options for `bun build/server/index.js`; a `port` or `hostname` here wins over `PORT` (3000)\n\t * and `HOST` (`0.0.0.0`) from the environment.\n\t */\n\treadonly listen?: ListenOptions;\n\t/**\n\t * Called once the built server listens and its `SIGINT` and `SIGTERM` handlers are in place.\n\t * Prints `alxia listening on <url>` by default.\n\t */\n\treadonly onListen?: (server: Bun.Server<unknown>) => void;\n}\n\n/**\n * The server `createServer()` describes, made into an app by the Vite\n * plugin, by a test, or by a server file of your own.\n */\nexport interface ReactRouterServer<App extends AnyAlxia> {\n\t/**\n\t * The app, serving React Router's build: `build/server/index.js`'s\n\t * default export. A test drives it with `app.request`.\n\t *\n\t * ```ts\n\t * const app = server.create({ build: await import('./build/server/index.js') });\n\t * ```\n\t */\n\tcreate(wiring: ServerWiring): App;\n\t/**\n\t * Listens with `app`, on `listen`, `PORT` and `HOST`, and stops it on\n\t * `SIGINT` or `SIGTERM`: its `onStop` hooks run, and the process exits.\n\t * What `bun build/server/index.js` runs.\n\t */\n\tstart(app: App): Bun.Server<unknown>;\n}\n\n/**\n * The alxia server of a React Router app, for `app/server.ts`. Every option\n * is optional: the Vite plugin wires React Router's build, the mode and the\n * client folder, and without the file uses `createServer()` as it is.\n *\n * ```ts\n * const server = createServer({\n * configure: (app) => app.use(logger()).get('/api/health', ({ reply }) => reply.ok({ ok: true })),\n * });\n * export default server;\n * ```\n *\n * The request runs through `beforeAll`, the client's files, `configure`,\n * then the pages.\n */\nexport function createServer<\n\tBefore extends AnyAlxia = FreshApp,\n\tApp extends AnyAlxia = Before,\n>(options: ServerOptions<Before, App> = {}): ReactRouterServer<App> {\n\treturn {\n\t\tcreate(wiring) {\n\t\t\tconst mode = options.mode ?? wiring.mode ?? 'production';\n\t\t\tconst client =\n\t\t\t\toptions.client === false\n\t\t\t\t\t? undefined\n\t\t\t\t\t: (options.client ?? wiring.client);\n\t\t\tconst fresh = alxia();\n\t\t\t// Without beforeAll or configure, Before and App are their defaults,\n\t\t\t// the app passed through: a type argument given by hand is believed.\n\t\t\tconst before = (options.beforeAll?.(fresh) ?? fresh) as Before;\n\t\t\tif (client !== undefined && mode === 'production') {\n\t\t\t\tdeclareClient(before, client);\n\t\t\t}\n\t\t\tconst app = (options.configure?.(before) ?? before) as App;\n\t\t\tconst getLoadContext = options.getLoadContext;\n\t\t\treactRouter(app, {\n\t\t\t\tbuild: options.build ?? wiring.build,\n\t\t\t\tmode,\n\t\t\t\t...(getLoadContext === undefined\n\t\t\t\t\t? {}\n\t\t\t\t\t: {\n\t\t\t\t\t\t\tgetLoadContext: (ctx, context) =>\n\t\t\t\t\t\t\t\tgetLoadContext(ctx as ContextOf<App>, context),\n\t\t\t\t\t\t}),\n\t\t\t});\n\t\t\treturn app;\n\t\t},\n\t\tstart(app) {\n\t\t\tconst server = app.listen({\n\t\t\t\tport: Number(process.env['PORT'] || 3000),\n\t\t\t\thostname: process.env['HOST'] || '0.0.0.0',\n\t\t\t\t...options.listen,\n\t\t\t});\n\t\t\t// Stop as the platform asks: the app's onStop hooks run, and the process ends.\n\t\t\t// Installed before onListen: a supervisor may signal as soon as it reads\n\t\t\t// that the server listens, and a signal with no handler yet kills the process.\n\t\t\tfor (const signal of ['SIGINT', 'SIGTERM'] as const) {\n\t\t\t\tprocess.once(signal, () => {\n\t\t\t\t\tvoid app.stop().then(\n\t\t\t\t\t\t() => process.exit(0),\n\t\t\t\t\t\t(error: unknown) => {\n\t\t\t\t\t\t\tconsole.error(error);\n\t\t\t\t\t\t\tprocess.exit(1);\n\t\t\t\t\t\t},\n\t\t\t\t\t);\n\t\t\t\t});\n\t\t\t}\n\t\t\t(options.onListen ?? announce)(server);\n\t\t\treturn server;\n\t\t},\n\t};\n}\n\nfunction announce(server: Bun.Server<unknown>): void {\n\tconsole.log(`alxia listening on ${server.url}`);\n}\n"
|
|
9
10
|
],
|
|
10
|
-
"mappings": ";AAKA;AAIA,IAAM,UAAyB,OAAO,uBAAuB;AAStD,IAAM,eAAe,cAAuB,OAAO;AAoEnD,SAAS,OAEf,CAAC,SAAiE;AAAA,EAClE,MAAM,QAAQ,QAAQ,IAAI,YAAY;AAAA,EACtC,IAAI,UAAU,SAAS;AAAA,IACtB,MAAM,IAAI,MACT,+PACD;AAAA,EACD;AAAA,EACA,OAAO;AAAA;;AC/
|
|
11
|
-
"debugId": "
|
|
11
|
+
"mappings": "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",
|
|
12
|
+
"debugId": "63BB3B2311BA25F964756E2164756E21",
|
|
12
13
|
"names": []
|
|
13
14
|
}
|
package/dist/nonce.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** `nonceOf`: the request's CSP nonce, read from alxia's context if a hook set one. */
|
|
2
|
+
import type { RouterContextProvider } from 'react-router';
|
|
3
|
+
/**
|
|
4
|
+
* This request's CSP nonce, for `entry.server.tsx`: the `nonce` alxia's
|
|
5
|
+
* hooks put on the context — `secureHeaders({ nonce: true })` from
|
|
6
|
+
* `@alxia/secure-headers`, or a `derive` of your own — or `undefined` when
|
|
7
|
+
* none did, or when the request did not come through alxia. It reads the
|
|
8
|
+
* key if present: neither package depends on the other.
|
|
9
|
+
*
|
|
10
|
+
* ```tsx
|
|
11
|
+
* const nonce = nonceOf(loadContext);
|
|
12
|
+
* <ServerRouter context={routerContext} url={request.url} nonce={nonce} />
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare function nonceOf(context: Readonly<RouterContextProvider>): string | undefined;
|
|
16
|
+
//# sourceMappingURL=nonce.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nonce.d.ts","sourceRoot":"","sources":["../src/nonce.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAG1D;;;;;;;;;;;GAWG;AACH,wBAAgB,OAAO,CACtB,OAAO,EAAE,QAAQ,CAAC,qBAAqB,CAAC,GACtC,MAAM,GAAG,SAAS,CAMpB"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { EnvironmentOptions } from 'vite';
|
|
2
|
+
/**
|
|
3
|
+
* What the plugin adds to the `ssr` environment, given its options as the
|
|
4
|
+
* config and every plugin's `config` hook left them. Vite merges the
|
|
5
|
+
* result into them, concatenating arrays, so what the app set is kept:
|
|
6
|
+
*
|
|
7
|
+
* - `resolve.conditions` and `resolve.externalConditions` gain `bun`, so a
|
|
8
|
+
* package that exports a `bun` variant is bundled, and checked, as Bun
|
|
9
|
+
* would load it. Which variant wins stays the package's own order.
|
|
10
|
+
* - `resolve.builtins` gains `bun` and `bun:*`: left external, whichever
|
|
11
|
+
* runtime runs Vite. Vite's own builtins are kept when the app names none.
|
|
12
|
+
* - `build.target` is `esnext`, Bun's, unless the app set one, at the top
|
|
13
|
+
* level or on the environment.
|
|
14
|
+
*
|
|
15
|
+
* `ssr.target` stays `node`: Bun runs Node's modules, and `webworker`
|
|
16
|
+
* would bundle every dependency with the browser's conditions. An app that
|
|
17
|
+
* sets `webworker` itself gets none of this: the plugin leaves that
|
|
18
|
+
* environment to Vite's own defaults.
|
|
19
|
+
*/
|
|
20
|
+
export declare function bunEnvironment(options: EnvironmentOptions): EnvironmentOptions;
|
|
21
|
+
//# sourceMappingURL=bun.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bun.d.ts","sourceRoot":"","sources":["../../src/vite/bun.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,MAAM,CAAC;AAkD/C;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC7B,OAAO,EAAE,kBAAkB,GACzB,kBAAkB,CAiBpB"}
|
package/dist/vite/index.d.ts
CHANGED
|
@@ -25,6 +25,9 @@ export interface AlxiaOptions {
|
|
|
25
25
|
* `PORT` (3000) and `HOST` (`0.0.0.0`); importing it starts nothing.
|
|
26
26
|
* - **`vite preview`**: every request goes to that built server, as it
|
|
27
27
|
* would answer it; so does React Router's prerendering.
|
|
28
|
+
* - **Built for Bun**: the `ssr` environment resolves packages with the
|
|
29
|
+
* `bun` export condition, leaves `bun` and `bun:*` external, and targets
|
|
30
|
+
* `esnext` unless the app set a target. What the app set is kept.
|
|
28
31
|
*
|
|
29
32
|
* The server is `app/server.ts`'s default export, `createServer()` from
|
|
30
33
|
* `@alxia/react-router`, or without that file `createServer()` as it is.
|
package/dist/vite/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,MAAM,EAAkB,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,MAAM,EAAkB,MAAM,MAAM,CAAC;AAgBnD,MAAM,WAAW,YAAY;IAC5B;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACxB;AAKD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,MAAM,CA+ExD"}
|
package/dist/vite/index.js
CHANGED
|
@@ -7,6 +7,40 @@ import {
|
|
|
7
7
|
// src/vite/index.ts
|
|
8
8
|
import { relative as relative3 } from "node:path";
|
|
9
9
|
|
|
10
|
+
// src/vite/bun.ts
|
|
11
|
+
import { builtinModules } from "node:module";
|
|
12
|
+
var BUN_CONDITION = "bun";
|
|
13
|
+
var VITE_BUILTINS = [
|
|
14
|
+
...builtinModules.filter((id) => !id.includes(":")),
|
|
15
|
+
/^node:/,
|
|
16
|
+
/^bun:/
|
|
17
|
+
];
|
|
18
|
+
var VITE_CONDITIONS = ["module", "node", "development|production"];
|
|
19
|
+
var VITE_EXTERNAL_CONDITIONS = ["node", "module-sync"];
|
|
20
|
+
var BUN_BUILTINS = [BUN_CONDITION, /^bun:/];
|
|
21
|
+
function adding(present, fallback, wanted) {
|
|
22
|
+
const list = present ?? fallback;
|
|
23
|
+
const has = (item) => list.some((other) => typeof item === "string" ? other === item : other instanceof RegExp && other.source === item.source);
|
|
24
|
+
return [
|
|
25
|
+
...present === undefined ? fallback : [],
|
|
26
|
+
...wanted.filter((item) => !has(item))
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
function bunEnvironment(options) {
|
|
30
|
+
const resolve = options.resolve ?? {};
|
|
31
|
+
const added = {
|
|
32
|
+
resolve: {
|
|
33
|
+
conditions: adding(resolve.conditions, VITE_CONDITIONS, [BUN_CONDITION]),
|
|
34
|
+
externalConditions: adding(resolve.externalConditions, VITE_EXTERNAL_CONDITIONS, [BUN_CONDITION]),
|
|
35
|
+
builtins: adding(resolve.builtins, VITE_BUILTINS, BUN_BUILTINS)
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
if (options.build?.target === undefined) {
|
|
39
|
+
added.build = { target: "esnext" };
|
|
40
|
+
}
|
|
41
|
+
return added;
|
|
42
|
+
}
|
|
43
|
+
|
|
10
44
|
// src/vite/config.ts
|
|
11
45
|
import { existsSync } from "node:fs";
|
|
12
46
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
@@ -334,6 +368,11 @@ function alxia(options = {}) {
|
|
|
334
368
|
const bundler = serverBuildOptions(meta.rolldownVersion !== undefined);
|
|
335
369
|
return { environments: { ssr: { build: bundler } } };
|
|
336
370
|
},
|
|
371
|
+
configEnvironment(name, options, env) {
|
|
372
|
+
if (name !== "ssr" || env.isSsrTargetWebworker === true)
|
|
373
|
+
return;
|
|
374
|
+
return bunEnvironment(options);
|
|
375
|
+
},
|
|
337
376
|
configResolved(resolved) {
|
|
338
377
|
config = resolved;
|
|
339
378
|
if (isChildCompiler(resolved)) {
|
|
@@ -387,5 +426,5 @@ export {
|
|
|
387
426
|
alxia
|
|
388
427
|
};
|
|
389
428
|
|
|
390
|
-
//# debugId=
|
|
429
|
+
//# debugId=2452BAC712BB5B4264756E2164756E21
|
|
391
430
|
//# sourceMappingURL=index.js.map
|
package/dist/vite/index.js.map
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/vite/index.ts", "../src/vite/config.ts", "../src/vite/node.ts", "../src/vite/socket.ts", "../src/vite/dev.ts", "../src/vite/preview.ts", "../src/vite/runtime.ts"],
|
|
3
|
+
"sources": ["../src/vite/index.ts", "../src/vite/bun.ts", "../src/vite/config.ts", "../src/vite/node.ts", "../src/vite/socket.ts", "../src/vite/dev.ts", "../src/vite/preview.ts", "../src/vite/runtime.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * `@alxia/react-router/vite`: `alxia()`, the Vite plugin that makes alxia\n * the server of a React Router app, under `react-router dev` and in\n * `react-router build`, with or without an `app/server.ts`.\n */\nimport { relative } from 'node:path';\n// Types only: the plugin runs on whichever Vite the app has, never on a\n// copy of this package's own.\nimport type { Plugin, ResolvedConfig } from 'vite';\nimport {\n\tclientPath,\n\tcontextOf,\n\tDEFAULT,\n\tisChildCompiler,\n\tSERVER,\n\tserverBuildOptions,\n\tserverFile,\n} from './config';\nimport { serveDev } from './dev';\nimport { NAME, PASS_THROUGH, serverEntry } from './entry';\nimport { servePreview } from './preview';\nimport { requireBun } from './runtime';\n\nexport interface AlxiaOptions {\n\t/**\n\t * The server file, relative to Vite's root: a module whose default export\n\t * is `createServer()` from `@alxia/react-router`. By default `server.ts`\n\t * in React Router's app directory, `app/server.ts`, when it exists, and\n\t * otherwise `createServer()` with no options.\n\t */\n\treadonly entry?: string;\n}\n\nconst RESOLVED_SERVER = `\\0${SERVER}`;\nconst RESOLVED_DEFAULT = `\\0${DEFAULT}`;\n\n/**\n * The Vite plugin that makes alxia the server of a React Router app,\n * anywhere in `plugins`:\n *\n * ```ts\n * export default defineConfig({ plugins: [alxia(), reactRouter()] });\n * ```\n *\n * - **`react-router dev`**: every request Vite does not answer itself —\n * pages, data, `/api` — goes to the server, loaded through Vite's SSR\n * runner. HMR, an edit to the server, the app's own context keys and\n * its WebSocket routes work, with no restart.\n * - **`react-router build`**: `build/server/index.js` is the server, with\n * React Router's build inside it. `bun build/server/index.js` listens on\n * `PORT` (3000) and `HOST` (`0.0.0.0`); importing it starts nothing.\n * - **`vite preview`**: every request goes to that built server, as it\n * would answer it; so does React Router's prerendering.\n *\n * The server is `app/server.ts`'s default export, `createServer()` from\n * `@alxia/react-router`, or without that file `createServer()` as it is.\n */\nexport function alxia(options: AlxiaOptions = {}): Plugin {\n\tlet config: ResolvedConfig | undefined;\n\tlet enabled = true;\n\n\treturn {\n\t\tname: NAME,\n\t\t// Before React Router's plugin, wherever it is listed: its config reads\n\t\t// the server build's input from this one's, and its dev middleware,\n\t\t// which would render the pages without alxia, comes after this one's.\n\t\tenforce: 'pre',\n\t\tconfig(_config, env) {\n\t\t\tif (env.command !== 'build') return;\n\t\t\t// Vite 7's types know neither `rolldownVersion` nor `rolldownOptions`.\n\t\t\tconst meta = this.meta as { readonly rolldownVersion?: string };\n\t\t\tconst bundler = serverBuildOptions(meta.rolldownVersion !== undefined);\n\t\t\treturn { environments: { ssr: { build: bundler as never } } };\n\t\t},\n\t\tconfigResolved(resolved) {\n\t\t\tconfig = resolved;\n\t\t\t// React Router compiles route modules in a Vite server of its own,\n\t\t\t// with every plugin but its own: there is nothing to serve there.\n\t\t\tif (isChildCompiler(resolved)) {\n\t\t\t\tenabled = false;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst rr = contextOf(resolved);\n\t\t\t// A single-page app has no server to be.\n\t\t\tenabled = rr.ssr;\n\t\t\tif (enabled && rr.serverBundles !== undefined) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`${NAME}: serverBundles splits React Router's server build in several, and alxia serves one. Remove serverBundles from react-router.config.ts.`,\n\t\t\t\t);\n\t\t\t}\n\t\t},\n\t\tresolveId(id) {\n\t\t\tif (id === SERVER) return RESOLVED_SERVER;\n\t\t\tif (id === DEFAULT) return RESOLVED_DEFAULT;\n\t\t\treturn undefined;\n\t\t},\n\t\tload(id) {\n\t\t\tif (id !== RESOLVED_SERVER && id !== RESOLVED_DEFAULT) return;\n\t\t\tif (config === undefined) return;\n\t\t\t// Disabled, a single-page app: React Router still builds its server\n\t\t\t// build, to render index.html, from this input.\n\t\t\tif (!enabled) return PASS_THROUGH;\n\t\t\tconst dev = config.command === 'serve';\n\t\t\tconst file =\n\t\t\t\tid === RESOLVED_DEFAULT ? undefined : serverFile(options.entry, config);\n\t\t\treturn serverEntry({\n\t\t\t\tfile,\n\t\t\t\tlabel: file === undefined ? '' : relative(config.root, file),\n\t\t\t\tdev,\n\t\t\t\tclient: dev ? undefined : clientPath(config),\n\t\t\t});\n\t\t},\n\t\tconfigureServer(server) {\n\t\t\tif (!enabled) return;\n\t\t\trequireBun('react-router dev');\n\t\t\treturn serveDev(server, options.entry);\n\t\t},\n\t\tconfigurePreviewServer(server) {\n\t\t\tif (!enabled) return;\n\t\t\t// React Router prerenders through a preview server of its own,\n\t\t\t// resolved as `serve` too: the message cannot tell the two apart.\n\t\t\trequireBun(\n\t\t\t\t'vite preview, or a react-router build that prerenders,',\n\t\t\t\t'bun --bun vite preview or bun --bun react-router build',\n\t\t\t);\n\t\t\t// Before Vite's own files: the built server answers every request.\n\t\t\tservePreview(server);\n\t\t},\n\t};\n}\n",
|
|
5
|
+
"/**\n * `@alxia/react-router/vite`: `alxia()`, the Vite plugin that makes alxia\n * the server of a React Router app, under `react-router dev` and in\n * `react-router build`, with or without an `app/server.ts`.\n */\nimport { relative } from 'node:path';\n// Types only: the plugin runs on whichever Vite the app has, never on a\n// copy of this package's own.\nimport type { Plugin, ResolvedConfig } from 'vite';\nimport { bunEnvironment } from './bun';\nimport {\n\tclientPath,\n\tcontextOf,\n\tDEFAULT,\n\tisChildCompiler,\n\tSERVER,\n\tserverBuildOptions,\n\tserverFile,\n} from './config';\nimport { serveDev } from './dev';\nimport { NAME, PASS_THROUGH, serverEntry } from './entry';\nimport { servePreview } from './preview';\nimport { requireBun } from './runtime';\n\nexport interface AlxiaOptions {\n\t/**\n\t * The server file, relative to Vite's root: a module whose default export\n\t * is `createServer()` from `@alxia/react-router`. By default `server.ts`\n\t * in React Router's app directory, `app/server.ts`, when it exists, and\n\t * otherwise `createServer()` with no options.\n\t */\n\treadonly entry?: string;\n}\n\nconst RESOLVED_SERVER = `\\0${SERVER}`;\nconst RESOLVED_DEFAULT = `\\0${DEFAULT}`;\n\n/**\n * The Vite plugin that makes alxia the server of a React Router app,\n * anywhere in `plugins`:\n *\n * ```ts\n * export default defineConfig({ plugins: [alxia(), reactRouter()] });\n * ```\n *\n * - **`react-router dev`**: every request Vite does not answer itself —\n * pages, data, `/api` — goes to the server, loaded through Vite's SSR\n * runner. HMR, an edit to the server, the app's own context keys and\n * its WebSocket routes work, with no restart.\n * - **`react-router build`**: `build/server/index.js` is the server, with\n * React Router's build inside it. `bun build/server/index.js` listens on\n * `PORT` (3000) and `HOST` (`0.0.0.0`); importing it starts nothing.\n * - **`vite preview`**: every request goes to that built server, as it\n * would answer it; so does React Router's prerendering.\n * - **Built for Bun**: the `ssr` environment resolves packages with the\n * `bun` export condition, leaves `bun` and `bun:*` external, and targets\n * `esnext` unless the app set a target. What the app set is kept.\n *\n * The server is `app/server.ts`'s default export, `createServer()` from\n * `@alxia/react-router`, or without that file `createServer()` as it is.\n */\nexport function alxia(options: AlxiaOptions = {}): Plugin {\n\tlet config: ResolvedConfig | undefined;\n\tlet enabled = true;\n\n\treturn {\n\t\tname: NAME,\n\t\t// Before React Router's plugin, wherever it is listed: its config reads\n\t\t// the server build's input from this one's, and its dev middleware,\n\t\t// which would render the pages without alxia, comes after this one's.\n\t\tenforce: 'pre',\n\t\tconfig(_config, env) {\n\t\t\tif (env.command !== 'build') return;\n\t\t\t// Vite 7's types know neither `rolldownVersion` nor `rolldownOptions`.\n\t\t\tconst meta = this.meta as { readonly rolldownVersion?: string };\n\t\t\tconst bundler = serverBuildOptions(meta.rolldownVersion !== undefined);\n\t\t\treturn { environments: { ssr: { build: bundler as never } } };\n\t\t},\n\t\tconfigEnvironment(name, options, env) {\n\t\t\t// After every config hook, React Router's included: the server\n\t\t\t// environment as the app and the plugins left it, built for Bun.\n\t\t\t// An app that chose `ssr.target: 'webworker'` keeps Vite's own.\n\t\t\tif (name !== 'ssr' || env.isSsrTargetWebworker === true) return;\n\t\t\treturn bunEnvironment(options);\n\t\t},\n\t\tconfigResolved(resolved) {\n\t\t\tconfig = resolved;\n\t\t\t// React Router compiles route modules in a Vite server of its own,\n\t\t\t// with every plugin but its own: there is nothing to serve there.\n\t\t\tif (isChildCompiler(resolved)) {\n\t\t\t\tenabled = false;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst rr = contextOf(resolved);\n\t\t\t// A single-page app has no server to be.\n\t\t\tenabled = rr.ssr;\n\t\t\tif (enabled && rr.serverBundles !== undefined) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`${NAME}: serverBundles splits React Router's server build in several, and alxia serves one. Remove serverBundles from react-router.config.ts.`,\n\t\t\t\t);\n\t\t\t}\n\t\t},\n\t\tresolveId(id) {\n\t\t\tif (id === SERVER) return RESOLVED_SERVER;\n\t\t\tif (id === DEFAULT) return RESOLVED_DEFAULT;\n\t\t\treturn undefined;\n\t\t},\n\t\tload(id) {\n\t\t\tif (id !== RESOLVED_SERVER && id !== RESOLVED_DEFAULT) return;\n\t\t\tif (config === undefined) return;\n\t\t\t// Disabled, a single-page app: React Router still builds its server\n\t\t\t// build, to render index.html, from this input.\n\t\t\tif (!enabled) return PASS_THROUGH;\n\t\t\tconst dev = config.command === 'serve';\n\t\t\tconst file =\n\t\t\t\tid === RESOLVED_DEFAULT ? undefined : serverFile(options.entry, config);\n\t\t\treturn serverEntry({\n\t\t\t\tfile,\n\t\t\t\tlabel: file === undefined ? '' : relative(config.root, file),\n\t\t\t\tdev,\n\t\t\t\tclient: dev ? undefined : clientPath(config),\n\t\t\t});\n\t\t},\n\t\tconfigureServer(server) {\n\t\t\tif (!enabled) return;\n\t\t\trequireBun('react-router dev');\n\t\t\treturn serveDev(server, options.entry);\n\t\t},\n\t\tconfigurePreviewServer(server) {\n\t\t\tif (!enabled) return;\n\t\t\t// React Router prerenders through a preview server of its own,\n\t\t\t// resolved as `serve` too: the message cannot tell the two apart.\n\t\t\trequireBun(\n\t\t\t\t'vite preview, or a react-router build that prerenders,',\n\t\t\t\t'bun --bun vite preview or bun --bun react-router build',\n\t\t\t);\n\t\t\t// Before Vite's own files: the built server answers every request.\n\t\t\tservePreview(server);\n\t\t},\n\t};\n}\n",
|
|
6
|
+
"/**\n * The server build, for Bun: what the plugin adds to Vite's `ssr`\n * environment so that `build/server/index.js` is built for the runtime\n * that runs it, and so is the module the dev server loads.\n */\nimport { builtinModules } from 'node:module';\nimport type { EnvironmentOptions } from 'vite';\n\n/** The export condition Bun resolves a package by, and the plugin adds. */\nconst BUN_CONDITION = 'bun';\n\n/**\n * Vite's own builtins for a server environment (its `nodeLikeBuiltins`),\n * which it uses when the config names none: Node's modules, `node:*` and\n * `bun:*`. Bare `bun` is among Node's modules only when Vite runs on Bun.\n */\nconst VITE_BUILTINS: readonly (string | RegExp)[] = [\n\t...builtinModules.filter((id) => !id.includes(':')),\n\t/^node:/,\n\t/^bun:/,\n];\n\n/**\n * Vite's default conditions for a server environment, and for its\n * externals, which it uses when the config names none. React Router's\n * plugin always names them; these are for a config where nothing did.\n */\nconst VITE_CONDITIONS = ['module', 'node', 'development|production'];\nconst VITE_EXTERNAL_CONDITIONS = ['node', 'module-sync'];\n\n/** Bun's own modules, which no bundler should try to resolve: `bun` and `bun:*`. */\nconst BUN_BUILTINS: readonly (string | RegExp)[] = [BUN_CONDITION, /^bun:/];\n\n/**\n * What to merge into `present` so that it holds `wanted`: Vite replaces its\n * default with any value set, so an unset option gets the default too.\n * Strings compare by value, patterns by source.\n */\nfunction adding<T extends string | RegExp>(\n\tpresent: readonly T[] | undefined,\n\tfallback: readonly T[],\n\twanted: readonly T[],\n): T[] {\n\tconst list = present ?? fallback;\n\tconst has = (item: T) =>\n\t\tlist.some((other) =>\n\t\t\ttypeof item === 'string'\n\t\t\t\t? other === item\n\t\t\t\t: other instanceof RegExp && other.source === item.source,\n\t\t);\n\treturn [\n\t\t...(present === undefined ? fallback : []),\n\t\t...wanted.filter((item) => !has(item)),\n\t];\n}\n\n/**\n * What the plugin adds to the `ssr` environment, given its options as the\n * config and every plugin's `config` hook left them. Vite merges the\n * result into them, concatenating arrays, so what the app set is kept:\n *\n * - `resolve.conditions` and `resolve.externalConditions` gain `bun`, so a\n * package that exports a `bun` variant is bundled, and checked, as Bun\n * would load it. Which variant wins stays the package's own order.\n * - `resolve.builtins` gains `bun` and `bun:*`: left external, whichever\n * runtime runs Vite. Vite's own builtins are kept when the app names none.\n * - `build.target` is `esnext`, Bun's, unless the app set one, at the top\n * level or on the environment.\n *\n * `ssr.target` stays `node`: Bun runs Node's modules, and `webworker`\n * would bundle every dependency with the browser's conditions. An app that\n * sets `webworker` itself gets none of this: the plugin leaves that\n * environment to Vite's own defaults.\n */\nexport function bunEnvironment(\n\toptions: EnvironmentOptions,\n): EnvironmentOptions {\n\tconst resolve = options.resolve ?? {};\n\tconst added: EnvironmentOptions = {\n\t\tresolve: {\n\t\t\tconditions: adding(resolve.conditions, VITE_CONDITIONS, [BUN_CONDITION]),\n\t\t\texternalConditions: adding(\n\t\t\t\tresolve.externalConditions,\n\t\t\t\tVITE_EXTERNAL_CONDITIONS,\n\t\t\t\t[BUN_CONDITION],\n\t\t\t),\n\t\t\tbuiltins: adding(resolve.builtins, VITE_BUILTINS, BUN_BUILTINS),\n\t\t},\n\t};\n\tif (options.build?.target === undefined) {\n\t\tadded.build = { target: 'esnext' };\n\t}\n\treturn added;\n}\n",
|
|
6
7
|
"/**\n * What the plugin reads of Vite's and React Router's config: the server\n * file, the client folder, and the server build's bundler options.\n */\nimport { existsSync } from 'node:fs';\nimport { dirname, isAbsolute, join, relative, resolve } from 'node:path';\nimport type { ResolvedConfig } from 'vite';\nimport { NAME } from './entry';\n\n/** The server build's input, and in dev the module the requests go to. */\nexport const SERVER = 'virtual:alxia-react-router/server';\n/** The same, when there is no server file: `createServer()` as it is. */\nexport const DEFAULT = `${SERVER}-default`;\n\n/** What the plugin reads of React Router's own config. */\nexport interface ReactRouterConfig {\n\treadonly appDirectory: string;\n\treadonly buildDirectory: string;\n\treadonly serverBuildFile: string;\n\treadonly ssr: boolean;\n\treadonly serverBundles?: unknown;\n}\n\n/** React Router's config, which its plugin puts on Vite's. */\nexport function contextOf(config: object): ReactRouterConfig {\n\tconst context = (\n\t\tconfig as {\n\t\t\treadonly __reactRouterPluginContext?: {\n\t\t\t\treadonly reactRouterConfig: ReactRouterConfig;\n\t\t\t};\n\t\t}\n\t).__reactRouterPluginContext;\n\tif (context === undefined) {\n\t\tthrow new Error(\n\t\t\t`${NAME}: React Router's Vite plugin is not in this config. Add reactRouter() from @react-router/dev/vite beside alxia() in vite.config.ts.`,\n\t\t);\n\t}\n\treturn context.reactRouterConfig;\n}\n\n/** React Router's child compiler: a Vite server without its plugin, cached apart. */\nexport function isChildCompiler(config: ResolvedConfig): boolean {\n\treturn (\n\t\t(config as { readonly __reactRouterPluginContext?: unknown })\n\t\t\t.__reactRouterPluginContext === undefined &&\n\t\tconfig.cacheDir.endsWith('.vite-child-compiler')\n\t);\n}\n\n/**\n * The server file: `entry`, which must exist, or `app/server.ts` when it\n * does, or `undefined` for the default server.\n */\nexport function serverFile(\n\tentry: string | undefined,\n\tconfig: ResolvedConfig,\n): string | undefined {\n\tif (entry !== undefined) {\n\t\tconst file = isAbsolute(entry) ? entry : resolve(config.root, entry);\n\t\tif (!existsSync(file)) {\n\t\t\tthrow new Error(\n\t\t\t\t`${NAME}: the entry ${entry} does not exist. Create it, or leave entry out to use app/server.ts, or the default server without it.`,\n\t\t\t);\n\t\t}\n\t\treturn file;\n\t}\n\tconst file = join(contextOf(config).appDirectory, 'server.ts');\n\treturn existsSync(file) ? file : undefined;\n}\n\n/** The built server, absolute: `build/server/index.js`. */\nexport function serverBuildPath(config: ResolvedConfig): string {\n\tconst rr = contextOf(config);\n\tconst outDir = resolve(\n\t\tconfig.root,\n\t\tconfig.environments['ssr']?.build.outDir ??\n\t\t\tjoin(rr.buildDirectory, 'server'),\n\t);\n\treturn join(outDir, rr.serverBuildFile);\n}\n\n/** `build/client`, relative to the built `build/server/index.js`. */\nexport function clientPath(config: ResolvedConfig): string {\n\tconst built = dirname(serverBuildPath(config));\n\tconst path = relative(\n\t\tbuilt,\n\t\tjoin(contextOf(config).buildDirectory, 'client'),\n\t);\n\treturn path.startsWith('.') ? path : `./${path}`;\n}\n\n/**\n * The ssr build's options: one file, `build/server/index.js`, the server\n * and React Router's build together. Rolldown, under Vite 8, names the\n * option otherwise.\n */\nexport function serverBuildOptions(rolldown: boolean): object {\n\treturn rolldown\n\t\t? {\n\t\t\t\trolldownOptions: {\n\t\t\t\t\tinput: SERVER,\n\t\t\t\t\toutput: { codeSplitting: false },\n\t\t\t\t},\n\t\t\t}\n\t\t: {\n\t\t\t\trollupOptions: {\n\t\t\t\t\tinput: SERVER,\n\t\t\t\t\toutput: { inlineDynamicImports: true },\n\t\t\t\t},\n\t\t\t};\n}\n",
|
|
7
8
|
"/**\n * The few lines between Vite's dev and preview servers, which speak\n * `node:http`, and an alxia app, which speaks `Request` and `Response`: no\n * dependency, Bun's own `node:http` and `node:stream` underneath.\n */\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport { Readable } from 'node:stream';\n\n/** What Connect, Vite's middleware stack, adds to a request: the URL before any rewrite. */\ntype ConnectRequest = IncomingMessage & {\n\treadonly originalUrl?: string | undefined;\n};\n\n/** The `Request` a Node request stands for. Aborted when the client goes away. */\nexport function toRequest(req: ConnectRequest, res: ServerResponse): Request {\n\tconst secure = (req.socket as { encrypted?: boolean }).encrypted === true;\n\tconst url = new URL(\n\t\treq.originalUrl ?? req.url ?? '/',\n\t\t`${secure ? 'https' : 'http'}://${req.headers.host ?? 'localhost'}`,\n\t);\n\tconst headers = new Headers();\n\tconst raw = req.rawHeaders;\n\tfor (let i = 0; i + 1 < raw.length; i += 2) {\n\t\tconst name = raw[i] as string;\n\t\t// HTTP/2's pseudo-headers (`:path`, `:method`) are not headers.\n\t\tif (!name.startsWith(':')) headers.append(name, raw[i + 1] as string);\n\t}\n\tconst controller = new AbortController();\n\tres.once('close', () => {\n\t\tif (!res.writableFinished) controller.abort();\n\t});\n\tconst method = req.method ?? 'GET';\n\tconst init: RequestInit & { duplex?: 'half' } = {\n\t\tmethod,\n\t\theaders,\n\t\tsignal: controller.signal,\n\t};\n\tif (method !== 'GET' && method !== 'HEAD') {\n\t\tinit.body = Readable.toWeb(req) as unknown as ReadableStream<Uint8Array>;\n\t\tinit.duplex = 'half';\n\t}\n\treturn new Request(url, init);\n}\n\n/** Writes `response` to `res`, streaming its body chunk by chunk as it comes. */\nexport async function send(\n\tres: ServerResponse,\n\tresponse: Response,\n): Promise<void> {\n\tres.statusCode = response.status;\n\tif (response.statusText !== '') res.statusMessage = response.statusText;\n\tfor (const [name, value] of response.headers) {\n\t\tif (name !== 'set-cookie') res.setHeader(name, value);\n\t}\n\tconst cookies = response.headers.getSetCookie();\n\tif (cookies.length > 0) res.setHeader('set-cookie', cookies);\n\tif (response.body === null) {\n\t\tres.end();\n\t\treturn;\n\t}\n\tres.flushHeaders();\n\tconst reader = response.body.getReader();\n\tconst cancel = () => void reader.cancel().catch(() => {});\n\tres.once('close', cancel);\n\ttry {\n\t\tfor (;;) {\n\t\t\tconst { done, value } = await reader.read();\n\t\t\tif (done) break;\n\t\t\tif (res.destroyed) return;\n\t\t\tif (!res.write(value)) {\n\t\t\t\tawait new Promise<void>((resolve) => {\n\t\t\t\t\tconst go = () => {\n\t\t\t\t\t\tres.off('drain', go);\n\t\t\t\t\t\tres.off('close', go);\n\t\t\t\t\t\tresolve();\n\t\t\t\t\t};\n\t\t\t\t\tres.on('drain', go);\n\t\t\t\t\tres.on('close', go);\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tres.end();\n\t} finally {\n\t\tres.off('close', cancel);\n\t}\n}\n",
|
|
8
9
|
"/**\n * Under `react-router dev` and `vite preview`: the app's WebSocket routes. Vite's server is a\n * `node:http` one, and an alxia socket is `Bun.serve`'s upgrade, so each\n * upgrade Vite leaves is relayed, bytes as they come, to a `Bun.serve` of\n * the app's own on a loopback port: its hooks, its validation, its socket\n * handlers, exactly as `listen` runs them.\n */\nimport type { IncomingMessage } from 'node:http';\nimport { connect, type Socket } from 'node:net';\nimport type { DevApp } from './dev';\nimport { NAME } from './entry';\n\n/** The subprotocols of Vite's own socket, the HMR one and its ping: Vite answers them. */\nconst VITE = new Set(['vite-hmr', 'vite-ping']);\n\n/** Whether the upgrade asks for Vite's own socket. */\nexport function isVites(req: Pick<IncomingMessage, 'headers'>): boolean {\n\tconst protocols = req.headers['sec-websocket-protocol'];\n\tif (protocols === undefined) return false;\n\treturn protocols.split(',').some((protocol) => VITE.has(protocol.trim()));\n}\n\n/** One entry of Vite's `server.proxy`, as far as its sockets go. */\ntype ProxyEntry = string | { readonly ws?: boolean; readonly target?: unknown };\n\n/**\n * Whether Vite's `server.proxy` relays this upgrade itself: an entry whose\n * context matches the URL, as Vite matches it (`^…` a RegExp, otherwise a\n * prefix), and that proxies sockets (`ws: true`, or a `ws:`/`wss:` target).\n */\nexport function isProxied(\n\turl: string,\n\tproxy: Readonly<Record<string, ProxyEntry>> | undefined,\n): boolean {\n\tfor (const [context, entry] of Object.entries(proxy ?? {})) {\n\t\tconst matches =\n\t\t\t(context.startsWith('^') && new RegExp(context).test(url)) ||\n\t\t\turl.startsWith(context);\n\t\tif (!matches) continue;\n\t\tconst options = typeof entry === 'string' ? { target: entry } : entry;\n\t\tconst target = String(options.target ?? '');\n\t\tif (\n\t\t\toptions.ws === true ||\n\t\t\ttarget.startsWith('ws:') ||\n\t\t\ttarget.startsWith('wss:')\n\t\t) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n/** The request line and headers of `req`, as they came: what the side server reads. */\nexport function head(\n\treq: Pick<IncomingMessage, 'method' | 'url' | 'rawHeaders'>,\n): string {\n\tconst lines = [`${req.method ?? 'GET'} ${req.url ?? '/'} HTTP/1.1`];\n\tconst raw = req.rawHeaders;\n\tfor (let i = 0; i + 1 < raw.length; i += 2) {\n\t\tlines.push(`${raw[i]}: ${raw[i + 1]}`);\n\t}\n\treturn `${lines.join('\\r\\n')}\\r\\n\\r\\n`;\n}\n\n/** The side servers: the current app's, and those an edit retired, until their sockets close. */\nexport interface Sides {\n\tcurrent:\n\t\t| { readonly app: DevApp; readonly side: Bun.Server<unknown> }\n\t\t| undefined;\n\treadonly retired: Set<Bun.Server<unknown>>;\n}\n\n/**\n * The side server of `app`, started on its first upgrade. A new app, an\n * edit's, retires the previous one: no new connection, and the sockets open\n * on it stay, with the handlers they opened with, until they close.\n */\nexport function sideFor(sides: Sides, app: DevApp): Bun.Server<unknown> {\n\tif (sides.current?.app === app) return sides.current.side;\n\tconst side = Bun.serve({\n\t\tport: 0,\n\t\thostname: '127.0.0.1',\n\t\tfetch: (request, bun) => app.fetch(request, bun),\n\t\twebsocket: app.websocket,\n\t} as Bun.Serve.Options<never>) as Bun.Server<unknown>;\n\tconst old = sides.current?.side;\n\tif (old !== undefined) {\n\t\tsides.retired.add(old);\n\t\tvoid old.stop().then(() => sides.retired.delete(old));\n\t}\n\tsides.current = { app, side };\n\treturn side;\n}\n\n/** Stops every side server, open sockets and all: Vite's server closed. */\nexport function closeAll(sides: Sides): void {\n\tfor (const side of [...sides.retired, sides.current?.side]) {\n\t\tvoid side?.stop(true);\n\t}\n\tsides.current = undefined;\n\tsides.retired.clear();\n}\n\n/** Relays the upgrade on `socket` to the side server on `port`, both ways. */\nfunction relay(\n\treq: IncomingMessage,\n\tsocket: Socket,\n\tbytes: Buffer,\n\tport: number,\n): void {\n\tconst upstream = connect(port, '127.0.0.1', () => {\n\t\tupstream.write(head(req));\n\t\tif (bytes.length > 0) upstream.write(bytes);\n\t\tupstream.pipe(socket);\n\t\tsocket.pipe(upstream);\n\t});\n\tupstream.on('error', () => socket.destroy());\n\tupstream.on('close', () => socket.destroy());\n\tsocket.on('close', () => upstream.destroy());\n}\n\n/** What the relay listens to on Vite's HTTP server, HTTP/1 or HTTP/2 alike. */\ninterface Upgrades {\n\ton(\n\t\tevent: 'upgrade',\n\t\tlistener: (req: IncomingMessage, socket: Socket, bytes: Buffer) => void,\n\t): unknown;\n\tonce(event: 'close', listener: () => void): unknown;\n}\n\n/** What the relay needs of the server it listens on: Vite's dev server or its preview. */\nexport interface Bridge {\n\t/** Vite's `server.proxy` (or `preview.proxy`): the upgrades it relays are its own. */\n\treadonly proxy: Readonly<Record<string, ProxyEntry>> | undefined;\n\t/** The app, as it is now. */\n\treadonly load: () => Promise<DevApp>;\n\t/** Where an upgrade that failed is printed: Vite's logger. */\n\treadonly logger: { error(message: string): void };\n\t/** Maps an error's stack to the source, as Vite's `ssrFixStacktrace` does. */\n\treadonly fix?: (error: Error) => void;\n}\n\n/** An upgrade the app could not take: reported, and a 500 to the client. */\nfunction refuse(bridge: Bridge, socket: Socket, error: unknown): void {\n\tif (error instanceof Error) bridge.fix?.(error);\n\tbridge.logger.error(\n\t\t`${NAME}: a WebSocket upgrade failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`,\n\t);\n\tif (socket.writable) {\n\t\tsocket.end(\n\t\t\t'HTTP/1.1 500 Internal Server Error\\r\\nConnection: close\\r\\nContent-Length: 0\\r\\n\\r\\n',\n\t\t);\n\t}\n}\n\n/**\n * Relays the app's upgrades on Vite's HTTP server to a side server of the\n * app. Vite's own socket and the sockets its proxy relays are left to\n * Vite. Without an HTTP server (Vite's middleware mode), there is nothing\n * to listen on.\n */\nexport function bridgeSockets(http: Upgrades | null, bridge: Bridge): void {\n\tif (http === null) return;\n\tconst sides: Sides = { current: undefined, retired: new Set() };\n\thttp.on('upgrade', (req: IncomingMessage, socket: Socket, bytes: Buffer) => {\n\t\tif (isVites(req) || isProxied(req.url ?? '/', bridge.proxy)) return;\n\t\tsocket.on('error', () => socket.destroy());\n\t\tbridge\n\t\t\t.load()\n\t\t\t.then((app) => {\n\t\t\t\tif (socket.destroyed) return;\n\t\t\t\trelay(req, socket, bytes, sideFor(sides, app).port as number);\n\t\t\t})\n\t\t\t.catch((error: unknown) => refuse(bridge, socket, error));\n\t});\n\thttp.once('close', () => closeAll(sides));\n}\n",
|
|
@@ -10,7 +11,7 @@
|
|
|
10
11
|
"/**\n * Under `vite preview`, and React Router's prerendering, which runs on it:\n * every request goes to the built server, `build/server/index.js`'s\n * default export, as `bun build/server/index.js` would answer it — its\n * hooks, its `/api`, the client's files and the pages.\n */\nimport { existsSync } from 'node:fs';\nimport { relative } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type { PreviewServer } from 'vite';\nimport { serverBuildPath } from './config';\nimport type { DevApp } from './dev';\nimport { NAME } from './entry';\nimport { send, toRequest } from './node';\nimport { bridgeSockets } from './socket';\n\n/** What the built server's default export is: the alxia app. */\ninterface Fetcher {\n\tfetch(request: Request): Response | Promise<Response>;\n}\n\n/** The built server's app, or an error saying why there is none. */\nasync function load(file: string, label: string): Promise<Fetcher> {\n\tif (!existsSync(file)) {\n\t\tthrow new Error(\n\t\t\t`${NAME}: ${label} does not exist. Run react-router build before vite preview.`,\n\t\t);\n\t}\n\tconst module = (await import(pathToFileURL(file).href)) as {\n\t\treadonly default?: Partial<Fetcher>;\n\t};\n\tif (typeof module.default?.fetch !== 'function') {\n\t\tthrow new Error(\n\t\t\t`${NAME}: ${label} is not alxia's server: its default export has no fetch. Build it with alxia() in vite.config.ts's plugins, then run vite preview again.`,\n\t\t);\n\t}\n\treturn module.default as Fetcher;\n}\n\n/**\n * Hands every request to the built server, before Vite's own files: the\n * app serves `build/client` itself, with its hooks and cache headers. Its\n * WebSocket routes connect too, relayed as under `react-router dev`.\n */\nexport function servePreview(server: PreviewServer): void {\n\tconst file = serverBuildPath(server.config);\n\tconst label = relative(server.config.root, file);\n\t// Loaded on the first request, as `bun build/server/index.js` would be\n\t// at startup; a failed load is tried again on the next, after a build.\n\tlet app: Promise<Fetcher> | undefined;\n\tconst current = (): Promise<Fetcher> => {\n\t\tapp ??= load(file, label);\n\t\treturn app;\n\t};\n\tbridgeSockets(server.httpServer, {\n\t\tproxy: server.config.preview.proxy,\n\t\t// The built server's default export is the alxia app, `websocket` and all.\n\t\tload: () =>\n\t\t\tcurrent().catch((error: unknown) => {\n\t\t\t\tapp = undefined;\n\t\t\t\tthrow error;\n\t\t\t}) as Promise<DevApp>,\n\t\tlogger: server.config.logger,\n\t});\n\tserver.middlewares.use((req, res, next) => {\n\t\tcurrent().then(\n\t\t\tasync (loaded) => {\n\t\t\t\ttry {\n\t\t\t\t\tawait send(res, await loaded.fetch(toRequest(req, res)));\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Mid-body, the response has started: end it rather than answer twice.\n\t\t\t\t\tif (res.headersSent) res.destroy(error as Error);\n\t\t\t\t\telse next(error);\n\t\t\t\t}\n\t\t\t},\n\t\t\t(error: unknown) => {\n\t\t\t\tapp = undefined;\n\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\tserver.config.logger.error(message);\n\t\t\t\tres.statusCode = 500;\n\t\t\t\tres.setHeader('content-type', 'text/plain; charset=utf-8');\n\t\t\t\tres.end(message);\n\t\t\t},\n\t\t);\n\t});\n}\n",
|
|
11
12
|
"import { NAME } from './entry';\n\n/**\n * Refuses, with what to do, a dev or preview server running on Node. The\n * React Router CLI's `#!/usr/bin/env node` runs it on Node whenever a node\n * is installed, even from `bun run dev`, and alxia's server then fails on\n * its first request with `ReferenceError: Bun is not defined`.\n */\nexport function requireBun(\n\tcommand: string,\n\thint = `bun --bun ${command}`,\n\tonBun = (globalThis as { Bun?: unknown }).Bun !== undefined,\n): void {\n\tif (onBun) return;\n\tthrow new Error(\n\t\t`${NAME}: ${command} is running on Node, and alxia's server runs on Bun. Add a bunfig.toml beside package.json with \"[run]\" and \"bun = true\", so bun run starts it on Bun, or run it as ${hint}.`,\n\t);\n}\n"
|
|
12
13
|
],
|
|
13
|
-
"mappings": "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",
|
|
14
|
-
"debugId": "
|
|
14
|
+
"mappings": "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",
|
|
15
|
+
"debugId": "2452BAC712BB5B4264756E2164756E21",
|
|
15
16
|
"names": []
|
|
16
17
|
}
|
package/docs/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# @alxia/react-router documentation
|
|
2
2
|
|
|
3
3
|
The [package README](../README.md) is the short version. This folder is
|
|
4
|
-
the long one: the setup, the Vite plugin and its default server,
|
|
4
|
+
the long one: the setup, the Vite plugin and its default server, the build for Bun,
|
|
5
5
|
customising the server, the typed context, the client's files, and what to
|
|
6
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, 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 |
|
|
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 |
|
|
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
|
@@ -3,16 +3,18 @@
|
|
|
3
3
|
`@alxia/react-router` serves a React Router **framework-mode** app with
|
|
4
4
|
server rendering from an alxia app, under Bun. This page walks an app
|
|
5
5
|
author through it: the setup, what happens in dev, in a build and under
|
|
6
|
-
`vite preview`,
|
|
6
|
+
`vite preview`, what the build sets for Bun,
|
|
7
7
|
customising the server, typing the loaders, the app's own context keys,
|
|
8
|
-
the escape hatches, WebSockets, the client's files, OpenAPI, testing and
|
|
8
|
+
a CSP nonce, the escape hatches, WebSockets, the client's files, OpenAPI, testing and
|
|
9
9
|
deploying.
|
|
10
10
|
|
|
11
11
|
- [Setup](#setup)
|
|
12
12
|
- [How it works](#how-it-works)
|
|
13
|
+
- [Built for Bun](#built-for-bun)
|
|
13
14
|
- [Customising the server](#customising-the-server)
|
|
14
15
|
- [Typing the loaders](#typing-the-loaders)
|
|
15
16
|
- [The app's own context keys](#the-apps-own-context-keys)
|
|
17
|
+
- [A CSP nonce](#a-csp-nonce)
|
|
16
18
|
- [Escape hatches](#escape-hatches)
|
|
17
19
|
- [Hooks around the pages](#hooks-around-the-pages)
|
|
18
20
|
- [Routes beside the pages](#routes-beside-the-pages)
|
|
@@ -208,6 +210,89 @@ prerendered page is therefore rendered by the built server too: its loader
|
|
|
208
210
|
reads `alxiaOf(context)` and `getLoadContext`'s keys, and `beforeAll`'s
|
|
209
211
|
and `configure`'s hooks run around it.
|
|
210
212
|
|
|
213
|
+
## Built for Bun
|
|
214
|
+
|
|
215
|
+
`build/server/index.js` runs on Bun, and the plugin builds it for Bun,
|
|
216
|
+
with nothing to configure. It adds to Vite's `ssr` environment, under
|
|
217
|
+
`react-router dev` and `react-router build` alike:
|
|
218
|
+
|
|
219
|
+
| option | what the plugin adds | why |
|
|
220
|
+
| --- | --- | --- |
|
|
221
|
+
| `resolve.conditions` | `bun` | a package bundled into the server whose `exports` has a `bun` condition is bundled as its Bun variant |
|
|
222
|
+
| `resolve.externalConditions` | `bun` | a dependency left external is loaded as its Bun variant in dev too, as Bun loads it from the build |
|
|
223
|
+
| `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 |
|
|
224
|
+
| `build.target` | `esnext` | the newest syntax is left as written: Bun runs it, and Vite's default targets browsers |
|
|
225
|
+
|
|
226
|
+
A package written for Bun, then, gives the build the variant Bun would
|
|
227
|
+
load, and Bun's own modules are used as they are:
|
|
228
|
+
|
|
229
|
+
```jsonc
|
|
230
|
+
// node_modules/some-package/package.json
|
|
231
|
+
{ "exports": { "bun": "./bun.js", "default": "./node.js" } }
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
// app/server.ts
|
|
236
|
+
import { createServer } from '@alxia/react-router';
|
|
237
|
+
import { Database } from 'bun:sqlite';
|
|
238
|
+
|
|
239
|
+
const db = new Database(':memory:');
|
|
240
|
+
|
|
241
|
+
export default createServer({
|
|
242
|
+
configure: (app) =>
|
|
243
|
+
app.get('/api/count', ({ reply }) =>
|
|
244
|
+
reply.ok(db.query('select sqlite_version() as version').get()),
|
|
245
|
+
),
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
// build/server/index.js, in part
|
|
251
|
+
import { Database } from "bun:sqlite";
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Which variant wins is still the package's own order: `exports` is read
|
|
255
|
+
top to bottom, and the first condition the build has wins. A package that
|
|
256
|
+
lists `node` before `bun` gives its Node variant, as Bun itself would load
|
|
257
|
+
it.
|
|
258
|
+
|
|
259
|
+
What the plugin leaves alone:
|
|
260
|
+
|
|
261
|
+
- **`ssr.target` stays `node`.** Bun runs Node's modules, and `node`
|
|
262
|
+
keeps `node:*` external and resolves packages as a server does;
|
|
263
|
+
`webworker` would bundle every dependency with the browser's conditions.
|
|
264
|
+
An app that sets `ssr.target: 'webworker'` itself gets none of the
|
|
265
|
+
above: that environment keeps Vite's own defaults.
|
|
266
|
+
- **No polyfill is added.** Vite adds none to a server build, and the
|
|
267
|
+
plugin adds none.
|
|
268
|
+
- **The client build** is a browser's, as before: none of this reaches
|
|
269
|
+
it. A Bun module imported by code that reaches the client is replaced
|
|
270
|
+
by an empty module there: Vite 7 fails the build, Vite 8 fails in the
|
|
271
|
+
browser ([troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md#module--has-been-externalized-for-browser-compatibility-imported-by-)).
|
|
272
|
+
|
|
273
|
+
What the app sets wins. The plugin adds to the `ssr` environment once
|
|
274
|
+
every plugin's `config` has run, and Vite merges what it adds by
|
|
275
|
+
concatenating arrays, so conditions and builtins of the app's own are
|
|
276
|
+
kept, and `bun` is added only when they lack it. A `build.target`, at the
|
|
277
|
+
top level or on the environment, is kept as it is:
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
// vite.config.ts
|
|
281
|
+
export default defineConfig({
|
|
282
|
+
// Kept: the build resolves `worker`, then `bun`, and targets es2022.
|
|
283
|
+
environments: {
|
|
284
|
+
ssr: {
|
|
285
|
+
resolve: { conditions: ['worker'] },
|
|
286
|
+
build: { target: 'es2022' },
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
plugins: [reactRouter(), alxia()],
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
`ssr.resolve.conditions` at the top level is the same option, and is kept
|
|
294
|
+
the same way.
|
|
295
|
+
|
|
211
296
|
## Customising the server
|
|
212
297
|
|
|
213
298
|
Write `app/server.ts`, with `createServer()` as its default export, or let
|
|
@@ -503,6 +588,103 @@ A server of your own, without the plugin, holds another copy of every
|
|
|
503
588
|
key under `app/`: see
|
|
504
589
|
[the troubleshooting entry](troubleshooting.md#error-no-value-found-for-context).
|
|
505
590
|
|
|
591
|
+
## A CSP nonce
|
|
592
|
+
|
|
593
|
+
React Router renders inline scripts: the hydration data, the module
|
|
594
|
+
loader, the scroll restoration, and with streaming one more per resolved
|
|
595
|
+
`<Await>`. A policy without `'unsafe-inline'` allows them only when each
|
|
596
|
+
carries the nonce the policy names. React Router writes it on every one of
|
|
597
|
+
them, and on its `modulepreload` links, when `entry.server.tsx` passes it
|
|
598
|
+
to `<ServerRouter nonce>` and to React's renderer
|
|
599
|
+
([React Router's security guide](https://reactrouter.com/how-to/security)).
|
|
600
|
+
|
|
601
|
+
`@alxia/secure-headers` makes the nonce: `secureHeaders({ nonce: true })`
|
|
602
|
+
draws a fresh one per request, adds it to the policy's `script-src`, and
|
|
603
|
+
puts it on the context of the routes after it, the catch-all included.
|
|
604
|
+
`nonceOf(loadContext)` reads it in the entry.
|
|
605
|
+
|
|
606
|
+
### 1. The policy, in `app/server.ts`
|
|
607
|
+
|
|
608
|
+
```ts
|
|
609
|
+
import { createServer } from '@alxia/react-router';
|
|
610
|
+
import { secureHeaders } from '@alxia/secure-headers';
|
|
611
|
+
|
|
612
|
+
export default createServer({
|
|
613
|
+
configure: (app) =>
|
|
614
|
+
app.use(
|
|
615
|
+
secureHeaders({
|
|
616
|
+
nonce: true,
|
|
617
|
+
contentSecurityPolicy: [
|
|
618
|
+
"default-src 'self'",
|
|
619
|
+
"script-src 'self'",
|
|
620
|
+
"style-src 'self' 'unsafe-inline'",
|
|
621
|
+
"img-src 'self' data:",
|
|
622
|
+
"connect-src 'self'",
|
|
623
|
+
"form-action 'self'",
|
|
624
|
+
"base-uri 'self'",
|
|
625
|
+
"frame-ancestors 'none'",
|
|
626
|
+
].join('; '),
|
|
627
|
+
}),
|
|
628
|
+
),
|
|
629
|
+
});
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
`script-src 'self'` goes out as `script-src 'self' 'nonce-…'`, a new
|
|
633
|
+
value on every response. `'self'` still allows the bundles under
|
|
634
|
+
`/assets`, and `connect-src 'self'` the single-fetch data requests.
|
|
635
|
+
|
|
636
|
+
### 2. The nonce, in `app/entry.server.tsx`
|
|
637
|
+
|
|
638
|
+
React Router's template has no entry; reveal the default one, then add an
|
|
639
|
+
import and the nonce in two places:
|
|
640
|
+
|
|
641
|
+
```sh
|
|
642
|
+
bunx react-router reveal entry.server
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
```diff
|
|
646
|
+
// app/entry.server.tsx, as reveal writes it
|
|
647
|
+
import { PassThrough } from "node:stream";
|
|
648
|
+
|
|
649
|
+
+import { nonceOf } from "@alxia/react-router";
|
|
650
|
+
import type { EntryContext, RouterContextProvider } from "react-router";
|
|
651
|
+
…
|
|
652
|
+
const { pipe, abort } = renderToPipeableStream(
|
|
653
|
+
- <ServerRouter context={routerContext} url={request.url} />,
|
|
654
|
+
+ <ServerRouter context={routerContext} url={request.url} nonce={nonceOf(loadContext)} />,
|
|
655
|
+
{
|
|
656
|
+
+ nonce: nonceOf(loadContext),
|
|
657
|
+
[readyOption]() {
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
`loadContext` is the entry's fifth parameter, the `RouterContextProvider`
|
|
661
|
+
alxia hands React Router; the revealed file already declares it. With
|
|
662
|
+
`renderToReadableStream`, the web entry, the option is the same: `{ nonce:
|
|
663
|
+
nonceOf(loadContext), … }`.
|
|
664
|
+
|
|
665
|
+
That is all: under `react-router dev` and from the build, every `<script>`
|
|
666
|
+
of a page carries the nonce of its own response's policy, Vite's dev
|
|
667
|
+
scripts included.
|
|
668
|
+
|
|
669
|
+
### How it stays loose
|
|
670
|
+
|
|
671
|
+
`nonceOf` reads `nonce` from alxia's context if it is a string, and
|
|
672
|
+
returns `undefined` otherwise: no hook set one, or the request did not come
|
|
673
|
+
through alxia at all. `ServerRouter` and React then render no `nonce`
|
|
674
|
+
attribute. So:
|
|
675
|
+
|
|
676
|
+
- this package does not depend on `@alxia/secure-headers`, nor the reverse;
|
|
677
|
+
- the same `entry.server.tsx` serves an app with or without the policy;
|
|
678
|
+
- a nonce of your own works too, from any `derive` in `configure` that
|
|
679
|
+
returns `{ nonce: string }`; the policy is then yours to write with it.
|
|
680
|
+
|
|
681
|
+
```ts
|
|
682
|
+
configure: (app) => app.derive(() => ({ nonce: myNonce() })),
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
Under `exactOptionalPropertyTypes`, spread it in only when there is one:
|
|
686
|
+
see [the troubleshooting entry](troubleshooting.md#type---nonce-string--undefined--is-not-assignable-to-type-serverrouterprops-with-exactoptionalpropertytypes-true).
|
|
687
|
+
|
|
506
688
|
## Escape hatches
|
|
507
689
|
|
|
508
690
|
### Another server file
|
|
@@ -619,6 +801,8 @@ any route:
|
|
|
619
801
|
every script of the page, React Router's inline ones included, and its
|
|
620
802
|
`form-action 'none'` blocks a `<Form>`'s post. Give the pages a policy of
|
|
621
803
|
their own: see [the troubleshooting entry](troubleshooting.md#refused-to-execute-inline-script-because-it-violates-the-following-content-security-policy-directive-default-src-none).
|
|
804
|
+
With `nonce: true`, the scripts need no `'unsafe-inline'`:
|
|
805
|
+
[A CSP nonce](#a-csp-nonce).
|
|
622
806
|
- **A guard** — `@alxia/jwt`'s `bearer`, `@alxia/janus`' session — in
|
|
623
807
|
`configure` guards every page and its data alike; in `beforeAll`, the
|
|
624
808
|
client's files too.
|
|
@@ -831,12 +1015,67 @@ is `POST /?index`, not `POST /`.
|
|
|
831
1015
|
|
|
832
1016
|
Run `bun run build`, then `bun build/server/index.js` beside the project's
|
|
833
1017
|
production `node_modules`: the build imports `react`, `react-router` and
|
|
834
|
-
alxia from them, as any React Router server build does.
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
`
|
|
839
|
-
`
|
|
1018
|
+
alxia from them, as any React Router server build does.
|
|
1019
|
+
|
|
1020
|
+
### Docker
|
|
1021
|
+
|
|
1022
|
+
The `react-router` template of
|
|
1023
|
+
[`@alxia/create`](https://github.com/softistx/alxia/tree/develop/packages/create)
|
|
1024
|
+
and the [example](https://github.com/softistx/alxia/tree/develop/examples/react-router)
|
|
1025
|
+
ship this `Dockerfile`, on Bun's official image. For an app started from
|
|
1026
|
+
`create-react-router`, it replaces the template's, which builds and runs
|
|
1027
|
+
on Node:
|
|
1028
|
+
|
|
1029
|
+
```dockerfile
|
|
1030
|
+
# Bun builds the app and runs it: build/server/index.js is alxia's server,
|
|
1031
|
+
# which runs on Bun. Pinned to Bun 1, the major alxia supports.
|
|
1032
|
+
|
|
1033
|
+
# The production dependencies alone, for the final image.
|
|
1034
|
+
FROM oven/bun:1 AS production-dependencies
|
|
1035
|
+
WORKDIR /app
|
|
1036
|
+
COPY package.json bun.lock* bunfig.toml* ./
|
|
1037
|
+
RUN bun install --frozen-lockfile --production
|
|
1038
|
+
|
|
1039
|
+
# Every dependency, then react-router build: build/client and
|
|
1040
|
+
# build/server/index.js.
|
|
1041
|
+
FROM oven/bun:1 AS build
|
|
1042
|
+
WORKDIR /app
|
|
1043
|
+
COPY package.json bun.lock* bunfig.toml* ./
|
|
1044
|
+
RUN bun install --frozen-lockfile
|
|
1045
|
+
COPY . .
|
|
1046
|
+
RUN bun run build
|
|
1047
|
+
|
|
1048
|
+
# The build and the production dependencies, run as Bun's own non-root
|
|
1049
|
+
# user. The server listens on PORT (3000) and HOST (0.0.0.0).
|
|
1050
|
+
FROM oven/bun:1
|
|
1051
|
+
WORKDIR /app
|
|
1052
|
+
ENV NODE_ENV=production
|
|
1053
|
+
COPY package.json ./
|
|
1054
|
+
COPY --from=production-dependencies /app/node_modules ./node_modules
|
|
1055
|
+
COPY --from=build /app/build ./build
|
|
1056
|
+
USER bun
|
|
1057
|
+
EXPOSE 3000
|
|
1058
|
+
CMD ["bun", "build/server/index.js"]
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
```sh
|
|
1062
|
+
docker build -t my-app .
|
|
1063
|
+
docker run -p 3000:3000 my-app
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
Keep the template's `.dockerignore` beside it: `node_modules`, `build` and
|
|
1067
|
+
`.react-router` stay out of the context, so the image installs and builds
|
|
1068
|
+
its own. The `bunfig.toml` goes in, and `bun run build` runs React
|
|
1069
|
+
Router's CLI on Bun, as it does outside Docker. The image's command is
|
|
1070
|
+
`start`'s, run directly, so the platform's `SIGTERM` reaches the server.
|
|
1071
|
+
|
|
1072
|
+
- **Commit `bun.lock`.** The installs are `--frozen-lockfile`: the image
|
|
1073
|
+
gets the versions you tested, and a `package.json` changed without a
|
|
1074
|
+
`bun install` fails the build
|
|
1075
|
+
([troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md#error-lockfile-had-changes-but-lockfile-is-frozen)).
|
|
1076
|
+
- **The server runs as `bun`**, a user with no write access to `/app`.
|
|
1077
|
+
Write files to a volume the user owns
|
|
1078
|
+
([troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md#eacces-permission-denied-open-app)).
|
|
840
1079
|
|
|
841
1080
|
`PORT` and `HOST` set where the server listens. The platform's `SIGTERM`
|
|
842
1081
|
stops it once the requests in flight are answered, and runs the app's
|
package/docs/roadmap.md
CHANGED
|
@@ -11,9 +11,7 @@ Nothing scheduled yet.
|
|
|
11
11
|
|
|
12
12
|
## Next
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
`<Scripts nonce>` sharing one nonce, so a page's policy needs no
|
|
16
|
-
`'unsafe-inline'`.
|
|
14
|
+
Nothing scheduled yet.
|
|
17
15
|
|
|
18
16
|
## Later
|
|
19
17
|
|
|
@@ -47,6 +45,24 @@ Nothing scheduled yet.
|
|
|
47
45
|
|
|
48
46
|
## Shipped
|
|
49
47
|
|
|
48
|
+
### Next release
|
|
49
|
+
|
|
50
|
+
- **The server is built for Bun.** `alxia()` adds the `bun` export
|
|
51
|
+
condition to the `ssr` environment's conditions and external
|
|
52
|
+
conditions, keeps `bun` and `bun:*` external whichever runtime runs
|
|
53
|
+
Vite, and targets `esnext`, in dev and in the build, with nothing to
|
|
54
|
+
configure. A package's `bun` variant is the one bundled, and what the
|
|
55
|
+
app sets is kept.
|
|
56
|
+
- **A Docker image on Bun.** The guide's Deploying has the multi-stage
|
|
57
|
+
`Dockerfile` on `oven/bun:1` that `@alxia/create`'s template and the
|
|
58
|
+
example ship, running `bun build/server/index.js` as a non-root user.
|
|
59
|
+
- **A per-request CSP nonce.** `nonceOf(loadContext)` reads the nonce that
|
|
60
|
+
`@alxia/secure-headers`' `nonce: true`, or a `derive` of the app's own,
|
|
61
|
+
put on the context, so `entry.server.tsx` hands it to `<ServerRouter
|
|
62
|
+
nonce>` and React in three lines. Every script of the page carries the
|
|
63
|
+
nonce of its own response's policy, which needs no `'unsafe-inline'`.
|
|
64
|
+
Neither package depends on the other.
|
|
65
|
+
|
|
50
66
|
### 0.1.0
|
|
51
67
|
|
|
52
68
|
- **React Router as a catch-all.** `reactRouter(app, { build })` serves
|
package/docs/troubleshooting.md
CHANGED
|
@@ -33,6 +33,9 @@ a loader, a message React Router or the browser prints, or an error from
|
|
|
33
33
|
- [`alxia-react-router: build/server/index.js does not exist. Run react-router build before vite preview.`](#alxia-react-router-buildserverindexjs-does-not-exist-run-react-router-build-before-vite-preview)
|
|
34
34
|
- [`alxia-react-router: build/server/index.js is not alxia's server: its default export has no fetch. …`](#alxia-react-router-buildserverindexjs-is-not-alxias-server-its-default-export-has-no-fetch-)
|
|
35
35
|
- [`warn: incorrect peer dependency "typescript@5.9.3"`](#warn-incorrect-peer-dependency-typescript593)
|
|
36
|
+
- [`Module "…" has been externalized for browser compatibility, imported by "…"`](#module--has-been-externalized-for-browser-compatibility-imported-by-)
|
|
37
|
+
- [`error: lockfile had changes, but lockfile is frozen`](#error-lockfile-had-changes-but-lockfile-is-frozen)
|
|
38
|
+
- [`EACCES: permission denied, open '/app/…'`](#eacces-permission-denied-open-app)
|
|
36
39
|
|
|
37
40
|
**Types**
|
|
38
41
|
|
|
@@ -41,16 +44,19 @@ a loader, a message React Router or the browser prints, or an error from
|
|
|
41
44
|
- [`Type '(app: …) => void' is not assignable to type '(app: …) => AnyAlxia'`](#type-app---void-is-not-assignable-to-type-app---anyalxia)
|
|
42
45
|
- [`Property '…' does not exist on type 'BaseContext & { readonly 'Register.server must be typeof server, the default export of createServer()': never; }'`](#property--does-not-exist-on-type-basecontext---readonly-registerserver-must-be-typeof-server-the-default-export-of-createserver-never-)
|
|
43
46
|
- [`Subsequent property declarations must have the same type. Property 'server' must be of type …`](#subsequent-property-declarations-must-have-the-same-type-property-server-must-be-of-type-)
|
|
47
|
+
- [`Type '{ …; nonce: string | undefined; }' is not assignable to type 'ServerRouterProps' with 'exactOptionalPropertyTypes: true'`](#type---nonce-string--undefined--is-not-assignable-to-type-serverrouterprops-with-exactoptionalpropertytypes-true)
|
|
44
48
|
|
|
45
49
|
**Traps**
|
|
46
50
|
|
|
47
51
|
- [`bun build/server/index.js` exits at once, printing nothing](#bun-buildserverindexjs-exits-at-once-printing-nothing)
|
|
52
|
+
- [A script of the page has no nonce](#a-script-of-the-page-has-no-nonce)
|
|
48
53
|
- [A loader reads `null` from the app's own key](#a-loader-reads-null-from-the-apps-own-key)
|
|
49
54
|
- [A page answers alxia's JSON 404 or 405 instead of rendering](#a-page-answers-alxias-json-404-or-405-instead-of-rendering)
|
|
50
55
|
- [A streamed page arrives in one piece](#a-streamed-page-arrives-in-one-piece)
|
|
51
56
|
- [The logger times a streamed page at a few milliseconds](#the-logger-times-a-streamed-page-at-a-few-milliseconds)
|
|
52
57
|
- [`ctx.server` is `undefined` under `react-router dev`](#ctxserver-is-undefined-under-react-router-dev)
|
|
53
58
|
- [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)
|
|
59
|
+
- [The build has a package's Node variant, not its `bun` one](#the-build-has-a-packages-node-variant-not-its-bun-one)
|
|
54
60
|
|
|
55
61
|
## Thrown or printed
|
|
56
62
|
|
|
@@ -419,9 +425,9 @@ export default createServer({
|
|
|
419
425
|
});
|
|
420
426
|
```
|
|
421
427
|
|
|
422
|
-
or `contentSecurityPolicy: false`.
|
|
423
|
-
|
|
424
|
-
[
|
|
428
|
+
or `contentSecurityPolicy: false`. Better than `'unsafe-inline'`: a nonce
|
|
429
|
+
per request, with `nonce: true` and `nonceOf(loadContext)` in
|
|
430
|
+
`entry.server.tsx` ([guide](guide.md#a-csp-nonce)).
|
|
425
431
|
|
|
426
432
|
### `alxia-react-router: … already exists, and reveal leaves it as it is. Run alxia-react-router reveal --force to overwrite it.`
|
|
427
433
|
|
|
@@ -588,6 +594,87 @@ alxia is tested on:
|
|
|
588
594
|
bun add -d typescript@^6
|
|
589
595
|
```
|
|
590
596
|
|
|
597
|
+
### `Module "…" has been externalized for browser compatibility, imported by "…"`
|
|
598
|
+
|
|
599
|
+
`react-router build` warns it while building the client, for
|
|
600
|
+
`Module "bun:sqlite"` or `Module "bun"`. What follows depends on Vite:
|
|
601
|
+
|
|
602
|
+
- **Vite 7** then fails the build:
|
|
603
|
+
|
|
604
|
+
```
|
|
605
|
+
RollupError: app/routes/todos.tsx (1:9): "Database" is not exported by "__vite-browser-external", imported by "app/routes/todos.tsx".
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
- **Vite 8** builds, and the page fails in the browser instead, with
|
|
609
|
+
`TypeError: … is not a constructor` or `… is not a function` once it
|
|
610
|
+
hydrates.
|
|
611
|
+
|
|
612
|
+
**Why:** a route module imports one of Bun's modules, `bun` or `bun:*`,
|
|
613
|
+
and uses it outside its `loader` and `action`, in the component or at the
|
|
614
|
+
module's top level. React Router keeps that code in the client build, for
|
|
615
|
+
the browser, which has no such module. The server build leaves Bun's
|
|
616
|
+
modules external ([Built for Bun](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide.md#built-for-bun));
|
|
617
|
+
the client build cannot.
|
|
618
|
+
|
|
619
|
+
**Fix:** use them only in the server's code: in `loader` and `action`, or
|
|
620
|
+
in a `.server.ts` module, which React Router keeps out of the client.
|
|
621
|
+
|
|
622
|
+
```ts
|
|
623
|
+
// app/db.server.ts
|
|
624
|
+
import { Database } from 'bun:sqlite';
|
|
625
|
+
|
|
626
|
+
export const db = new Database(':memory:');
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
```tsx
|
|
630
|
+
// app/routes/todos.tsx
|
|
631
|
+
import { db } from '../db.server';
|
|
632
|
+
|
|
633
|
+
export function loader() {
|
|
634
|
+
return { version: db.query('select sqlite_version() as v').get() };
|
|
635
|
+
}
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
### `error: lockfile had changes, but lockfile is frozen`
|
|
639
|
+
|
|
640
|
+
`docker build` stops at `RUN bun install --frozen-lockfile`, followed by
|
|
641
|
+
`note: try re-running without --frozen-lockfile and commit the updated lockfile`.
|
|
642
|
+
|
|
643
|
+
**Why:** the `Dockerfile` installs exactly what `bun.lock` records, and
|
|
644
|
+
`package.json` asks for something it does not: a dependency added or
|
|
645
|
+
changed by hand, without `bun install`.
|
|
646
|
+
|
|
647
|
+
**Fix:** update the lockfile, commit it, and build again:
|
|
648
|
+
|
|
649
|
+
```sh
|
|
650
|
+
bun install
|
|
651
|
+
git add bun.lock
|
|
652
|
+
docker build -t my-app .
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
### `EACCES: permission denied, open '/app/…'`
|
|
656
|
+
|
|
657
|
+
The container starts, then a write fails: `Bun.write` gives this, and
|
|
658
|
+
`new Database('/app/data.sqlite')` gives
|
|
659
|
+
`SQLiteError: unable to open database file`.
|
|
660
|
+
|
|
661
|
+
**Why:** the `Dockerfile` runs the server as the image's `bun` user, not
|
|
662
|
+
root, and `/app` is root's: the user reads the build and cannot write
|
|
663
|
+
beside it.
|
|
664
|
+
|
|
665
|
+
**Fix:** write to a directory the user owns, a volume for what must
|
|
666
|
+
outlive the container:
|
|
667
|
+
|
|
668
|
+
```dockerfile
|
|
669
|
+
# Dockerfile, before USER bun
|
|
670
|
+
RUN mkdir /data && chown bun:bun /data
|
|
671
|
+
VOLUME /data
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
```ts
|
|
675
|
+
const db = new Database('/data/app.sqlite');
|
|
676
|
+
```
|
|
677
|
+
|
|
591
678
|
## Types
|
|
592
679
|
|
|
593
680
|
### `Property '…' does not exist on type 'BaseContext & …'`
|
|
@@ -714,6 +801,30 @@ whose catch-all every loader of the build runs behind.
|
|
|
714
801
|
a monorepo its own tsconfig, or drop `Register` and pass the type
|
|
715
802
|
argument, `alxiaOf<Server>(context)`.
|
|
716
803
|
|
|
804
|
+
### `Type '{ …; nonce: string | undefined; }' is not assignable to type 'ServerRouterProps' with 'exactOptionalPropertyTypes: true'`
|
|
805
|
+
|
|
806
|
+
**When:** `entry.server.tsx` passes `nonceOf(loadContext)` to
|
|
807
|
+
`<ServerRouter nonce>` under `exactOptionalPropertyTypes`.
|
|
808
|
+
|
|
809
|
+
```text
|
|
810
|
+
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.
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
**Why:** `nonceOf` returns `undefined` when no hook set a nonce, and
|
|
814
|
+
`ServerRouterProps.nonce` is optional without `undefined`. React's
|
|
815
|
+
`nonce` option takes `undefined`, so only the prop complains.
|
|
816
|
+
|
|
817
|
+
**Fix:** spread the prop in only when there is a nonce:
|
|
818
|
+
|
|
819
|
+
```tsx
|
|
820
|
+
const nonce = nonceOf(loadContext);
|
|
821
|
+
<ServerRouter
|
|
822
|
+
context={routerContext}
|
|
823
|
+
url={request.url}
|
|
824
|
+
{...(nonce === undefined ? {} : { nonce })}
|
|
825
|
+
/>
|
|
826
|
+
```
|
|
827
|
+
|
|
717
828
|
## Traps
|
|
718
829
|
|
|
719
830
|
### `bun build/server/index.js` exits at once, printing nothing
|
|
@@ -727,6 +838,44 @@ is the process's entry point.
|
|
|
727
838
|
`alxia listening on <url>` then comes up. Run the file itself,
|
|
728
839
|
`bun build/server/index.js`, not through another module's `import`.
|
|
729
840
|
|
|
841
|
+
### A script of the page has no nonce
|
|
842
|
+
|
|
843
|
+
The policy names `'nonce-…'`, and the browser refuses some or all of the
|
|
844
|
+
page's scripts: `Refused to execute inline script because it violates the
|
|
845
|
+
following Content Security Policy directive: "script-src 'self'
|
|
846
|
+
'nonce-…'"`. The page renders, then never hydrates.
|
|
847
|
+
|
|
848
|
+
**Why:** one of these:
|
|
849
|
+
|
|
850
|
+
- the app has no `entry.server.tsx`, so React Router's default renders
|
|
851
|
+
with no nonce;
|
|
852
|
+
- the entry does not pass it to both `<ServerRouter nonce>` and the
|
|
853
|
+
renderer's `nonce` option: the first covers React Router's scripts, the
|
|
854
|
+
second React's own streaming ones;
|
|
855
|
+
- a `<script>` of the app's own, in `root.tsx` say, has no `nonce`
|
|
856
|
+
attribute.
|
|
857
|
+
|
|
858
|
+
**Fix:** reveal the entry and pass `nonceOf(loadContext)` to both
|
|
859
|
+
([guide](guide.md#a-csp-nonce)). A script of your own reads the nonce
|
|
860
|
+
through a loader, where `nonceOf(context)` works as well:
|
|
861
|
+
|
|
862
|
+
```tsx
|
|
863
|
+
// app/root.tsx
|
|
864
|
+
import { nonceOf } from '@alxia/react-router';
|
|
865
|
+
import type { Route } from './+types/root';
|
|
866
|
+
|
|
867
|
+
export function loader({ context }: Route.LoaderArgs) {
|
|
868
|
+
return { nonce: nonceOf(context) };
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// in Layout: <script nonce={useRouteLoaderData('root')?.nonce}>…</script>
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
Check what was sent with
|
|
875
|
+
`curl -s -A 'Mozilla/5.0 Chrome/130' http://localhost:5173/ | grep -o '<script[^>]*>'`:
|
|
876
|
+
every tag should hold `nonce="…"`, the value of the response's
|
|
877
|
+
`Content-Security-Policy`.
|
|
878
|
+
|
|
730
879
|
### A loader reads `null` from the app's own key
|
|
731
880
|
|
|
732
881
|
The same cause as [`Error: No value found for context`](#error-no-value-found-for-context),
|
|
@@ -830,3 +979,19 @@ server, with the handlers it opened with, and a topic is one server's.
|
|
|
830
979
|
|
|
831
980
|
**Fix:** reconnect the clients after an edit: reload their pages. From
|
|
832
981
|
the build there is one server, and every socket shares its topics.
|
|
982
|
+
|
|
983
|
+
### The build has a package's Node variant, not its `bun` one
|
|
984
|
+
|
|
985
|
+
A dependency ships a `bun` variant, and the server runs its other one.
|
|
986
|
+
|
|
987
|
+
**Why:** the plugin adds the `bun` condition, and the package's `exports`
|
|
988
|
+
decides which of its conditions comes first: read top to bottom, the
|
|
989
|
+
first one the build has wins. A package listing `node` or `import` before
|
|
990
|
+
`bun` gives that variant, under the plugin as under Bun itself.
|
|
991
|
+
|
|
992
|
+
**Fix:** none in the config can reorder a package's `exports`; that is
|
|
993
|
+
the package's to change. To check which file a condition picks, ask Bun:
|
|
994
|
+
|
|
995
|
+
```sh
|
|
996
|
+
bun -e "console.log(import.meta.resolve('some-package'))"
|
|
997
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alxia/react-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "alxia as the server of a React Router framework app: server rendering behind the app's hooks, loaders reading its typed context, the client's assets served",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
]
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@alxia/compress": "^0.1.
|
|
55
|
-
"@alxia/core": "^0.
|
|
56
|
-
"@alxia/openapi": "^0.
|
|
54
|
+
"@alxia/compress": "^0.1.2",
|
|
55
|
+
"@alxia/core": "^0.3.1",
|
|
56
|
+
"@alxia/openapi": "^0.3.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.3.1",
|
|
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"
|