@alxia/react-router 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -36,6 +36,7 @@ a loader, a message React Router or the browser prints, or an error from
36
36
  - [`Module "…" has been externalized for browser compatibility, imported by "…"`](#module--has-been-externalized-for-browser-compatibility-imported-by-)
37
37
  - [`error: lockfile had changes, but lockfile is frozen`](#error-lockfile-had-changes-but-lockfile-is-frozen)
38
38
  - [`EACCES: permission denied, open '/app/…'`](#eacces-permission-denied-open-app)
39
+ - [`error: Cannot find package '…' from '/app/build/server/index.js'`](#error-cannot-find-package--from-appbuildserverindexjs)
39
40
 
40
41
  **Types**
41
42
 
@@ -54,6 +55,8 @@ a loader, a message React Router or the browser prints, or an error from
54
55
  - [A page answers alxia's JSON 404 or 405 instead of rendering](#a-page-answers-alxias-json-404-or-405-instead-of-rendering)
55
56
  - [A streamed page arrives in one piece](#a-streamed-page-arrives-in-one-piece)
56
57
  - [The logger times a streamed page at a few milliseconds](#the-logger-times-a-streamed-page-at-a-few-milliseconds)
58
+ - [A page, or a missing path, answers 401 behind a guard](#a-page-or-a-missing-path-answers-401-behind-a-guard)
59
+ - [The logger, CORS or secure headers miss the client's files](#the-logger-cors-or-secure-headers-miss-the-clients-files)
57
60
  - [`ctx.server` is `undefined` under `react-router dev`](#ctxserver-is-undefined-under-react-router-dev)
58
61
  - [A `publish` under `react-router dev` misses the sockets opened before an edit](#a-publish-under-react-router-dev-misses-the-sockets-opened-before-an-edit)
59
62
  - [The build has a package's Node variant, not its `bun` one](#the-build-has-a-packages-node-variant-not-its-bun-one)
@@ -675,6 +678,63 @@ VOLUME /data
675
678
  const db = new Database('/data/app.sqlite');
676
679
  ```
677
680
 
681
+ ### `error: Cannot find package '…' from '/app/build/server/index.js'`
682
+
683
+ `bun build/server/index.js`, in the container or from a copy of `build/`
684
+ alone, stops at startup. For a package loaded with `require`, the message
685
+ can read `Cannot find module '…'`. Or it starts, and a request fails with
686
+ `ENOENT: no such file or directory, open '/app/build/…'`.
687
+
688
+ **When:** the image holds `build/` alone, as the template's `Dockerfile`
689
+ copies it, and the server imports a package that is not inside
690
+ `build/server/index.js`:
691
+
692
+ - the app keeps it external with `ssr.external`, and the image has no
693
+ `node_modules`;
694
+ - or the build bundled it, but it reads a file of its own folder at
695
+ runtime (`new URL('./data.json', import.meta.url)`), or loads a native
696
+ addon (a `.node` file), neither of which bundling carries along.
697
+
698
+ The image runs `bun --no-install`: without the flag, Bun finds no
699
+ `node_modules` and fetches the missing package from npm at startup, at
700
+ whatever version npm has, instead of failing.
701
+
702
+ **Why:** under `react-router build` the plugin bundles every package into
703
+ `build/server/index.js` ([Self-contained](guide.md#self-contained)), so
704
+ the image needs no `node_modules`. What stays outside the file must be
705
+ installed beside it.
706
+
707
+ **Fix:** keep the package external, and give the image the production
708
+ dependencies:
709
+
710
+ ```ts
711
+ // vite.config.ts
712
+ export default defineConfig({
713
+ ssr: { external: ['sharp'] },
714
+ plugins: [reactRouter(), alxia()],
715
+ });
716
+ ```
717
+
718
+ ```dockerfile
719
+ # Dockerfile, before the build stage
720
+ FROM oven/bun:1 AS production-dependencies
721
+ WORKDIR /app
722
+ COPY package.json bun.lock* bunfig.toml* ./
723
+ RUN bun install --frozen-lockfile --production
724
+
725
+ # Dockerfile, in the final stage, beside build/
726
+ COPY --from=production-dependencies /app/node_modules ./node_modules
727
+ ```
728
+
729
+ `grep '^import' build/server/index.js` lists what the build still
730
+ imports: `node:*`, `bun`, `bun:*` and the packages `ssr.external` names.
731
+
732
+ The image runs on `oven/bun:1-alpine`, whose C library is musl. A native
733
+ addon built for glibc alone then stops it with
734
+ `… is linked against glibc (DT_NEEDED …), but this Bun build uses musl.`:
735
+ put the final stage back on `oven/bun:1`
736
+ ([`@alxia/create`'s troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/create/docs/troubleshooting.md#error--is-linked-against-glibc-dt_needed-libmso6-but-this-bun-build-uses-musl)).
737
+
678
738
  ## Types
679
739
 
680
740
  ### `Property '…' does not exist on type 'BaseContext & …'`
@@ -684,10 +744,12 @@ error TS2339: Property 'tenant' does not exist on type 'BaseContext & Empty & {
684
744
  ```
685
745
 
686
746
  **When:** a loader reads, through `alxiaOf`, or `getLoadContext`
687
- destructures, something no hook of the server derives. With the type
747
+ destructures, something no middleware of the server derives. With the type
688
748
  `'BaseContext & Empty'` alone, `alxiaOf(context)` has no server to read:
689
749
  `app/server.ts` has no `Register` declaration, or there is no
690
- `app/server.ts`.
750
+ `app/server.ts`, and `@alxia/core`'s `Register` names no base either.
751
+ With core's `Register` alone, `alxiaOf` reads that base: a key
752
+ `configure` adds after it is missing.
691
753
 
692
754
  **Why:** the type is the app's context at the point of the catch-all, as
693
755
  for any route: "order is meaning".
@@ -810,7 +872,7 @@ argument, `alxiaOf<Server>(context)`.
810
872
  error TS2375: Type '{ context: EntryContext; url: string; nonce: string | undefined; }' is not assignable to type 'ServerRouterProps' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
811
873
  ```
812
874
 
813
- **Why:** `nonceOf` returns `undefined` when no hook set a nonce, and
875
+ **Why:** `nonceOf` returns `undefined` when no middleware set a nonce, and
814
876
  `ServerRouterProps.nonce` is optional without `undefined`. React's
815
877
  `nonce` option takes `undefined`, so only the prop complains.
816
878
 
@@ -935,9 +997,45 @@ deferred value later. If the whole page comes at once:
935
997
  page that streams for 800 ms is logged at its first byte. The duration is
936
998
  the server's time to first byte, not the page's.
937
999
 
1000
+ ### A page, or a missing path, answers 401 behind a guard
1001
+
1002
+ A guard given to `use` in `configure` or `beforeAll` runs on every request
1003
+ it is in front of, a request no route matches included, and answers before
1004
+ the 404. An anonymous request to `/nope` gets the 401, not the 404, and so
1005
+ does the login page the guard sits in front of. Scope the guard to what it
1006
+ guards, with a path. A path-scoped `use` takes a middleware that adds nothing
1007
+ to the context, so `bearer`, which adds `user`, does not compile there: write
1008
+ the guard as a middleware that only refuses. The pages are a catch-all outside
1009
+ any `group`, so a `group` would not reach them.
1010
+
1011
+ ```ts
1012
+ import { defineMiddleware } from '@alxia/core';
1013
+
1014
+ const account = defineMiddleware(async ({ request, reply }, next) => {
1015
+ const token = request.headers.get('authorization')?.slice('Bearer '.length) ?? '';
1016
+ return (await jwt.verify(token)).ok ? next() : reply(401, { error: 'unauthorized' as const });
1017
+ });
1018
+
1019
+ createServer({
1020
+ configure: (app) => app.use('/account', account), // the pages under /account only
1021
+ });
1022
+ ```
1023
+
1024
+ ### The logger, CORS or secure headers miss the client's files
1025
+
1026
+ A middleware in `configure` runs for the routes declared after it. The
1027
+ client's files are declared before `configure`, after `beforeAll`: what must
1028
+ see them too goes in `beforeAll`.
1029
+
1030
+ ```ts
1031
+ createServer({
1032
+ beforeAll: (app) => app.use(logger()).use(secureHeaders()),
1033
+ });
1034
+ ```
1035
+
938
1036
  ### `ctx.server` is `undefined` under `react-router dev`
939
1037
 
940
- A hook or a route reads `ctx.server`, or the app declares a `page()`,
1038
+ A middleware or a route reads `ctx.server`, or the app declares a `page()`,
941
1039
  under `react-router dev` or `vite preview`; the same code works from
942
1040
  `bun build/server/index.js`.
943
1041
 
@@ -946,7 +1044,7 @@ the preview server under `vite preview`. An HTTP request reaches the app
946
1044
  through `app.fetch`, as in a test, not through `listen`: there is no
947
1045
  `Bun.serve` behind it, so `ctx.server` is `undefined`, the default
948
1046
  `ctx.ip` too, and a `page()` (Bun's HTML bundle) is not served. A socket's upgrade is the exception: the plugin relays it
949
- to a `Bun.serve` of the app, so its hooks read a server
1047
+ to a `Bun.serve` of the app, so its middlewares read a server
950
1048
  ([WebSockets](guide.md#under-react-router-dev)).
951
1049
 
952
1050
  **Fix:** read `ctx.server` as optional, and publish to sockets from the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alxia/react-router",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "alxia as the server of a React Router framework app: server rendering behind the app's hooks, loaders reading its typed context, the client's assets served",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -51,9 +51,9 @@
51
51
  ]
52
52
  },
53
53
  "devDependencies": {
54
- "@alxia/compress": "^0.1.2",
55
- "@alxia/core": "^0.3.1",
56
- "@alxia/openapi": "^0.3.0",
54
+ "@alxia/compress": "^0.2.0",
55
+ "@alxia/core": "^0.4.0",
56
+ "@alxia/openapi": "^0.4.0",
57
57
  "@react-router/dev": "^8.4.0",
58
58
  "@types/bun": "^1.4.2",
59
59
  "@types/react": "^19.3.0",
@@ -65,7 +65,7 @@
65
65
  "vite": "^7.0.0"
66
66
  },
67
67
  "peerDependencies": {
68
- "@alxia/core": "^0.3.1",
68
+ "@alxia/core": "^0.4.0",
69
69
  "react-router": "^8.0.0",
70
70
  "typescript": "^6.0.3 || ^7.0.0",
71
71
  "vite": "^7.0.0 || ^8.0.0"