@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.
- package/README.md +82 -43
- package/dist/cli/index.js +3 -3
- package/dist/cli/index.js.map +4 -4
- package/dist/cli/template.d.ts +1 -1
- package/dist/cli/template.d.ts.map +1 -1
- package/dist/context.d.ts +23 -12
- package/dist/context.d.ts.map +1 -1
- package/dist/index.js.map +4 -4
- package/dist/react-router.d.ts +13 -13
- package/dist/react-router.d.ts.map +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/vite/bundle.d.ts +33 -0
- package/dist/vite/bundle.d.ts.map +1 -0
- package/dist/vite/index.d.ts +4 -0
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +16 -2
- package/dist/vite/index.js.map +5 -4
- package/docs/README.md +1 -1
- package/docs/guide.md +259 -74
- package/docs/roadmap.md +29 -9
- package/docs/troubleshooting.md +103 -5
- package/package.json +5 -5
package/docs/troubleshooting.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "alxia as the server of a React Router framework app: server rendering behind the app's hooks, loaders reading its typed context, the client's assets served",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
]
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@alxia/compress": "^0.
|
|
55
|
-
"@alxia/core": "^0.
|
|
56
|
-
"@alxia/openapi": "^0.
|
|
54
|
+
"@alxia/compress": "^0.2.0",
|
|
55
|
+
"@alxia/core": "^0.4.0",
|
|
56
|
+
"@alxia/openapi": "^0.4.0",
|
|
57
57
|
"@react-router/dev": "^8.4.0",
|
|
58
58
|
"@types/bun": "^1.4.2",
|
|
59
59
|
"@types/react": "^19.3.0",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"vite": "^7.0.0"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
|
-
"@alxia/core": "^0.
|
|
68
|
+
"@alxia/core": "^0.4.0",
|
|
69
69
|
"react-router": "^8.0.0",
|
|
70
70
|
"typescript": "^6.0.3 || ^7.0.0",
|
|
71
71
|
"vite": "^7.0.0 || ^8.0.0"
|