@alxia/react-router 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +222 -10
  2. package/dist/chunks/index-59vhfj6n.js +10 -0
  3. package/dist/chunks/index-59vhfj6n.js.map +10 -0
  4. package/dist/chunks/index-5gqcpyyg.js +34 -0
  5. package/dist/chunks/index-5gqcpyyg.js.map +10 -0
  6. package/dist/chunks/index-9xv2a8et.js +108 -0
  7. package/dist/chunks/index-9xv2a8et.js.map +11 -0
  8. package/dist/chunks/index-mppz0806.js +119 -0
  9. package/dist/chunks/index-mppz0806.js.map +11 -0
  10. package/dist/chunks/index-rfr02vg4.js +40 -0
  11. package/dist/chunks/index-rfr02vg4.js.map +10 -0
  12. package/dist/chunks/index-zdbqzcjp.js +97 -0
  13. package/dist/chunks/index-zdbqzcjp.js.map +12 -0
  14. package/dist/pwa/build/build-worker.d.ts +41 -0
  15. package/dist/pwa/build/build-worker.d.ts.map +1 -0
  16. package/dist/pwa/build/index.d.ts +9 -0
  17. package/dist/pwa/build/index.d.ts.map +1 -0
  18. package/dist/pwa/build/index.js +9 -0
  19. package/dist/pwa/build/index.js.map +9 -0
  20. package/dist/pwa/build/precache.d.ts +17 -0
  21. package/dist/pwa/build/precache.d.ts.map +1 -0
  22. package/dist/pwa/index.d.ts +14 -0
  23. package/dist/pwa/index.d.ts.map +1 -0
  24. package/dist/pwa/index.js +65 -0
  25. package/dist/pwa/index.js.map +10 -0
  26. package/dist/pwa/model/caching.d.ts +46 -0
  27. package/dist/pwa/model/caching.d.ts.map +1 -0
  28. package/dist/pwa/model/clear-app-caches.d.ts +9 -0
  29. package/dist/pwa/model/clear-app-caches.d.ts.map +1 -0
  30. package/dist/pwa/model/manifest.d.ts +80 -0
  31. package/dist/pwa/model/manifest.d.ts.map +1 -0
  32. package/dist/pwa/model/precache-list.d.ts +40 -0
  33. package/dist/pwa/model/precache-list.d.ts.map +1 -0
  34. package/dist/pwa/model/registration.d.ts +39 -0
  35. package/dist/pwa/model/registration.d.ts.map +1 -0
  36. package/dist/pwa/model/start-service-worker.d.ts +24 -0
  37. package/dist/pwa/model/start-service-worker.d.ts.map +1 -0
  38. package/dist/pwa/model/storable.d.ts +8 -0
  39. package/dist/pwa/model/storable.d.ts.map +1 -0
  40. package/dist/pwa/model/update-notice.d.ts +38 -0
  41. package/dist/pwa/model/update-notice.d.ts.map +1 -0
  42. package/dist/pwa/model/worker-contract.d.ts +61 -0
  43. package/dist/pwa/model/worker-contract.d.ts.map +1 -0
  44. package/dist/pwa/react/index.d.ts +9 -0
  45. package/dist/pwa/react/index.d.ts.map +1 -0
  46. package/dist/pwa/react/index.js +75 -0
  47. package/dist/pwa/react/index.js.map +13 -0
  48. package/dist/pwa/react/use-online.d.ts +6 -0
  49. package/dist/pwa/react/use-online.d.ts.map +1 -0
  50. package/dist/pwa/react/use-service-worker.d.ts +33 -0
  51. package/dist/pwa/react/use-service-worker.d.ts.map +1 -0
  52. package/dist/pwa/react/use-update-notice.d.ts +14 -0
  53. package/dist/pwa/react/use-update-notice.d.ts.map +1 -0
  54. package/dist/pwa/server/index.d.ts +7 -0
  55. package/dist/pwa/server/index.d.ts.map +1 -0
  56. package/dist/pwa/server/index.js +18 -0
  57. package/dist/pwa/server/index.js.map +10 -0
  58. package/dist/pwa/server/no-cache.d.ts +22 -0
  59. package/dist/pwa/server/no-cache.d.ts.map +1 -0
  60. package/dist/pwa/worker/index.d.ts +9 -0
  61. package/dist/pwa/worker/index.d.ts.map +1 -0
  62. package/dist/pwa/worker/index.js +144 -0
  63. package/dist/pwa/worker/index.js.map +12 -0
  64. package/dist/pwa/worker/injected.d.ts +15 -0
  65. package/dist/pwa/worker/injected.d.ts.map +1 -0
  66. package/dist/pwa/worker/plan.d.ts +65 -0
  67. package/dist/pwa/worker/plan.d.ts.map +1 -0
  68. package/dist/pwa/worker/scope.d.ts +18 -0
  69. package/dist/pwa/worker/scope.d.ts.map +1 -0
  70. package/dist/pwa/worker/start-worker.d.ts +17 -0
  71. package/dist/pwa/worker/start-worker.d.ts.map +1 -0
  72. package/dist/vite/index.d.ts +10 -0
  73. package/dist/vite/index.d.ts.map +1 -1
  74. package/dist/vite/index.js +49 -3
  75. package/dist/vite/index.js.map +6 -5
  76. package/dist/vite/runtime.d.ts +1 -1
  77. package/dist/vite/runtime.d.ts.map +1 -1
  78. package/dist/vite/worker.d.ts +16 -0
  79. package/dist/vite/worker.d.ts.map +1 -0
  80. package/docs/README.md +12 -3
  81. package/docs/guide/beside-the-pages.md +178 -0
  82. package/docs/guide/csp-nonce.md +100 -0
  83. package/docs/guide/customising-the-server.md +292 -0
  84. package/docs/guide/deploying.md +99 -0
  85. package/docs/guide/getting-started.md +112 -0
  86. package/docs/guide/how-it-works.md +268 -0
  87. package/docs/guide/openapi.md +51 -0
  88. package/docs/guide/pwa.md +636 -0
  89. package/docs/guide/testing.md +42 -0
  90. package/docs/guide/typing-the-loaders.md +311 -0
  91. package/docs/guide.md +27 -1391
  92. package/docs/roadmap.md +39 -1
  93. package/docs/troubleshooting.md +402 -11
  94. package/package.json +70 -6
package/docs/roadmap.md CHANGED
@@ -11,10 +11,15 @@ Nothing in progress.
11
11
 
12
12
  ## Next
13
13
 
14
- Nothing scheduled yet.
14
+ Nothing planned.
15
15
 
16
16
  ## Later
17
17
 
18
+ - **`service-worker.js` served revalidated with no setup.** Today the app
19
+ adds `noCache` for `SERVICE_WORKER_URL` itself.
20
+ - **A check of an offline page's policy.** The offline page declares its
21
+ Content-Security-Policy in a `<meta>`; a check would read the page and
22
+ confirm that each inline script and style is allowed by its hash.
18
23
  - **`context.alxia` typed under `Route.LoaderArgs`.** Once react-router's
19
24
  `./internal` types and its main entry share one `RouterContextProvider`
20
25
  declaration, the generated route types see the shorthand too; today
@@ -48,6 +53,39 @@ Nothing scheduled yet.
48
53
 
49
54
  ## Shipped
50
55
 
56
+ ### 0.10.0
57
+
58
+ - **`@alxia/react-router/pwa/worker`.** `startWorker({ contract })` is the
59
+ whole service worker on Workbox: the precache, a page load always from the
60
+ network with the offline page when it fails, hashed bundles cache-first,
61
+ other static files stale-while-revalidate, and the update applied on
62
+ request. An app's `app/service-worker.ts` is a few lines; the `workbox-*`
63
+ packages are optional peers ([guide](guide/pwa.md)).
64
+ - **`@alxia/react-router/pwa/build`.** `buildWorker({ file })` bundles the
65
+ worker after `react-router build`, with the precache list written in: the
66
+ offline page, every route's code and the fonts you name.
67
+ - **`alxia({ worker })`.** With `app/service-worker.ts` in the app,
68
+ `react-router build` writes `build/client/service-worker.js` itself, with
69
+ no script to add; `worker` names another file, tunes the build or turns
70
+ it off.
71
+
72
+ ### 0.9.0
73
+
74
+ - **`@alxia/react-router/pwa`.** The code with no UI an installable app
75
+ shares, importing nothing: the service worker's contract and cache names
76
+ (`defineWorkerContract`), which requests the worker caches and which
77
+ responses it may store (`cachingFor`, `isStorable`), the precache list read
78
+ from the build, registering the worker and the update notice's store, the
79
+ web app manifest with its language in `?lang=`, and `clearAppCaches` for
80
+ sign-out. Every app-specific name is an option, so two apps never share a
81
+ default ([guide](guide/pwa.md)).
82
+ - **`@alxia/react-router/pwa/react`.** `useOnline`, `useServiceWorker({
83
+ enabled })` and `useUpdateNotice`, with the page's one `updateNotice`;
84
+ `react` is an optional peer.
85
+ - **`@alxia/react-router/pwa/server`.** `noCache` serves the worker's
86
+ script revalidated on every update check, with the worker's own policy
87
+ (`revalidated` and `WORKER_POLICY` beneath it).
88
+
51
89
  ### 0.8.0
52
90
 
53
91
  - **`withAlxia(fn)`.** A loader, an action or a middleware given `alxia`,
@@ -8,12 +8,14 @@ a loader, a message React Router or the browser prints, or an error from
8
8
  **Thrown or printed**
9
9
 
10
10
  - [`alxia-react-router: … is running on Node, and alxia's server runs on Bun. …`](#alxia-react-router--is-running-on-node-and-alxias-server-runs-on-bun-)
11
+ - [`alxia-react-router: react-router build with a service worker is running on Node, and the worker is bundled by Bun.build`](#alxia-react-router-react-router-build-with-a-service-worker-is-running-on-node-and-the-worker-is-bundled-by-bunbuild)
11
12
  - [`ReferenceError: Bun is not defined`](#referenceerror-bun-is-not-defined)
12
13
  - [`alxiaOf(): this request has no alxia context. …`](#alxiaof-this-request-has-no-alxia-context-)
13
14
  - [`TypeError: undefined is not an object (evaluating 'context.alxia.user')`](#typeerror-undefined-is-not-an-object-evaluating-contextalxiauser)
14
15
  - [`Error: No value found for context`](#error-no-value-found-for-context)
15
16
  - [`alxia-react-router: … must export createServer() from @alxia/react-router as its default export: …`](#alxia-react-router--must-export-createserver-from-alxiareact-router-as-its-default-export-)
16
17
  - [`alxia-react-router: the entry … does not exist. …`](#alxia-react-router-the-entry--does-not-exist-)
18
+ - [`alxia-react-router: the worker … does not exist. …`](#alxia-react-router-the-worker--does-not-exist-)
17
19
  - [`alxia-react-router: React Router's Vite plugin is not in this config. …`](#alxia-react-router-react-routers-vite-plugin-is-not-in-this-config-)
18
20
  - [`alxia-react-router: serverBundles splits React Router's server build in several, and alxia serves one. …`](#alxia-react-router-serverbundles-splits-react-routers-server-build-in-several-and-alxia-serves-one-)
19
21
  - [`alxia-react-router: Vite's ssr environment does not run modules in this process, so the server cannot be loaded.`](#alxia-react-router-vites-ssr-environment-does-not-run-modules-in-this-process-so-the-server-cannot-be-loaded)
@@ -38,6 +40,19 @@ a loader, a message React Router or the browser prints, or an error from
38
40
  - [`error: lockfile had changes, but lockfile is frozen`](#error-lockfile-had-changes-but-lockfile-is-frozen)
39
41
  - [`EACCES: permission denied, open '/app/…'`](#eacces-permission-denied-open-app)
40
42
  - [`error: Cannot find package '…' from '/app/build/server/index.js'`](#error-cannot-find-package--from-appbuildserverindexjs)
43
+ - [`TypeError: defineWorkerContract(): cachePrefix must be a non-empty name, …`](#typeerror-defineworkercontract-cacheprefix-must-be-a-non-empty-name-)
44
+ - [`TypeError: defineWorkerContract(): cacheVersion must be a non-empty name without '-', …`](#typeerror-defineworkercontract-cacheversion-must-be-a-non-empty-name-without---)
45
+ - [`parseBrowserManifest(): not React Router's browser manifest …`](#parsebrowsermanifest-not-react-routers-browser-manifest-)
46
+ - [`error: Cannot find package 'react' from '…/@alxia/react-router/dist/pwa/react/index.js'`](#error-cannot-find-package-react-from-alxiareact-routerdistpwareactindexjs)
47
+ - [`startWorker(): no precache list. …`](#startworker-no-precache-list-)
48
+ - [`TypeError: startWorker(): contract is required: …`](#typeerror-startworker-contract-is-required-)
49
+ - [`error: Could not resolve: "workbox-core". Maybe you need to "bun install"?`](#error-could-not-resolve-workbox-core-maybe-you-need-to-bun-install)
50
+ - [`buildWorker() runs on Bun: it is running on Node`](#buildworker-runs-on-bun-it-is-running-on-node)
51
+ - [`buildWorker(): app/service-worker.ts does not exist.`](#buildworker-appservice-workerts-does-not-exist)
52
+ - [`buildWorker(): expected one React Router browser manifest`](#buildworker-expected-one-react-router-browser-manifest)
53
+ - [`buildWorker(): build/client/offline.html does not exist`](#buildworker-buildclientofflinehtml-does-not-exist)
54
+ - [`buildWorker(): … is outside ….`](#buildworker--is-outside-)
55
+ - [`buildWorker(): Bun.build failed:`](#buildworker-bunbuild-failed)
41
56
 
42
57
  **Types**
43
58
 
@@ -63,14 +78,23 @@ a loader, a message React Router or the browser prints, or an error from
63
78
  - [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)
64
79
  - [The build has a package's Node variant, not its `bun` one](#the-build-has-a-packages-node-variant-not-its-bun-one)
65
80
  - [`request.url` in a loader is `http://` and the internal host behind a proxy](#requesturl-in-a-loader-is-http-and-the-internal-host-behind-a-proxy)
81
+ - [An unhashed file under `public/assets/` is cached for a year](#an-unhashed-file-under-publicassets-is-cached-for-a-year)
82
+ - [A cache the contract did not make is swept](#a-cache-the-contract-did-not-make-is-swept)
83
+ - [The update notice never shows](#the-update-notice-never-shows)
84
+ - [The worker registers under `react-router dev`](#the-worker-registers-under-react-router-dev)
85
+ - [The service worker is not built](#the-service-worker-is-not-built)
86
+ - [Offline, a client navigation to a route never visited shows the offline page](#offline-a-client-navigation-to-a-route-never-visited-shows-the-offline-page)
87
+ - [An update waits an hour](#an-update-waits-an-hour)
88
+ - [The worker cannot fetch another origin](#the-worker-cannot-fetch-another-origin)
89
+ - [The manifest is in the wrong language](#the-manifest-is-in-the-wrong-language)
66
90
 
67
91
  ## Thrown or printed
68
92
 
69
93
  ### `alxia-react-router: … is running on Node, and alxia's server runs on Bun. …`
70
94
 
71
95
  **When:** `bun run dev`, `vite preview`, or a `react-router build` that
72
- prerenders, on a machine where a node is installed. The full message under
73
- `react-router dev`:
96
+ prerenders or builds a service worker (`app/service-worker.ts`), on a machine
97
+ where a node is installed. The full message under `react-router dev`:
74
98
 
75
99
  ```
76
100
  alxia-react-router: react-router dev 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 bun --bun react-router dev.
@@ -92,7 +116,18 @@ bun = true
92
116
  Or run the one command on Bun: `bun --bun react-router dev`. Under
93
117
  `vite preview` or a prerendering build, the message begins
94
118
  `alxia-react-router: vite preview, or a react-router build that prerenders, is running on Node`
95
- and names `bun --bun vite preview` and `bun --bun react-router build`.
119
+ and names `bun --bun vite preview` and `bun --bun react-router build`. A
120
+ build that has a service worker reads
121
+ `alxia-react-router: react-router build with a service worker is running on Node, and the worker is bundled by Bun.build`
122
+ and names the same `bun --bun react-router build`.
123
+
124
+ ### `alxia-react-router: react-router build with a service worker is running on Node, and the worker is bundled by Bun.build`
125
+
126
+ The same cause as
127
+ [the entry above](#alxia-react-router--is-running-on-node-and-alxias-server-runs-on-bun-):
128
+ the build runs on Node, and the worker is bundled with `Bun.build`. Run it as
129
+ `bun --bun react-router build`, or add a `bunfig.toml` beside `package.json`
130
+ with `[run]` and `bun = true`.
96
131
 
97
132
  ### `ReferenceError: Bun is not defined`
98
133
 
@@ -133,7 +168,7 @@ key: one copy is the fix for both.
133
168
  sets.
134
169
 
135
170
  **Fix:** add `alxia()` to `vite.config.ts`, as in the
136
- [setup](guide.md#setup). With a server of your own in a monorepo, tell
171
+ [setup](guide/getting-started.md). With a server of your own in a monorepo, tell
137
172
  Vite to leave the package external:
138
173
 
139
174
  ```ts
@@ -210,7 +245,7 @@ reactRouter(app, { build, getLoadContext: ({ user }, context) => context.set(use
210
245
  **Fix:** serve the app with the plugin, `alxia()` in `vite.config.ts`: the
211
246
  server is then built with the routes, and `app/context.ts` is one module
212
247
  for both, in dev and in the build. See
213
- [the app's own context keys](guide.md#the-apps-own-context-keys).
248
+ [the app's own context keys](guide/typing-the-loaders.md#the-apps-own-context-keys).
214
249
 
215
250
  Without the plugin, read alxia's context with `alxiaOf`: its key,
216
251
  `alxiaContext`, lives in this package under `node_modules`, which Vite
@@ -245,7 +280,7 @@ export default server;
245
280
  ```
246
281
 
247
282
  An alxia app of your own, with `reactRouter()` on it, is served without the
248
- plugin: see [a server of your own](guide.md#a-server-of-your-own-without-the-plugin).
283
+ plugin: see [a server of your own](guide/customising-the-server.md#a-server-of-your-own-without-the-plugin).
249
284
 
250
285
  ### `alxia-react-router: the entry … does not exist. …`
251
286
 
@@ -262,6 +297,20 @@ Vite's root. Under `react-router dev` each request is a 500 saying so;
262
297
 
263
298
  **Fix:** correct the path, or leave `entry` out.
264
299
 
300
+ ### `alxia-react-router: the worker … does not exist. …`
301
+
302
+ ```text
303
+ Error: alxia-react-router: the worker app/pwa/worker.ts does not exist. Create it, or leave worker out to build app/service-worker.ts when it exists.
304
+ ```
305
+
306
+ **When:** `alxia({ worker })` names a file that is not there, relative to
307
+ Vite's root, and `react-router build` fails.
308
+
309
+ **Why:** a missing `app/service-worker.ts` just means no worker, but a
310
+ `worker` you named is taken as meant.
311
+
312
+ **Fix:** correct the path, or leave `worker` out.
313
+
265
314
  ### `alxia-react-router: React Router's Vite plugin is not in this config. …`
266
315
 
267
316
  ```text
@@ -465,7 +514,7 @@ export default createServer({
465
514
 
466
515
  or `contentSecurityPolicy: false`. Better than `'unsafe-inline'`: a nonce
467
516
  per request, with `nonce: true` and `nonceOf(loadContext)` in
468
- `entry.server.tsx` ([guide](guide.md#a-csp-nonce)).
517
+ `entry.server.tsx` ([guide](guide/csp-nonce.md#a-csp-nonce)).
469
518
 
470
519
  ### `alxia-react-router: … already exists, and reveal leaves it as it is. Run alxia-react-router reveal --force to overwrite it.`
471
520
 
@@ -651,7 +700,7 @@ bun add -d typescript@^6
651
700
  and uses it outside its `loader` and `action`, in the component or at the
652
701
  module's top level. React Router keeps that code in the client build, for
653
702
  the browser, which has no such module. The server build leaves Bun's
654
- modules external ([Built for Bun](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide.md#built-for-bun));
703
+ modules external ([Built for Bun](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/how-it-works.md#built-for-bun));
655
704
  the client build cannot.
656
705
 
657
706
  **Fix:** use them only in the server's code: in `loader` and `action`, or
@@ -735,7 +784,7 @@ The image runs `bun --no-install`: without the flag, Bun finds no
735
784
  whatever version npm has, instead of failing.
736
785
 
737
786
  **Why:** under `react-router build` the plugin bundles every package into
738
- `build/server/index.js` ([Self-contained](guide.md#self-contained)), so
787
+ `build/server/index.js` ([Self-contained](guide/how-it-works.md#self-contained)), so
739
788
  the image needs no `node_modules`. What stays outside the file must be
740
789
  installed beside it.
741
790
 
@@ -770,6 +819,219 @@ addon built for glibc alone then stops it with
770
819
  put the final stage back on `oven/bun:1`
771
820
  ([`@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)).
772
821
 
822
+ ### `TypeError: defineWorkerContract(): cachePrefix must be a non-empty name, …`
823
+
824
+ The full message is `defineWorkerContract(): cachePrefix must be a non-empty
825
+ name, such as 'my-app'; got ''.`, or `… must not end with '-', which the
826
+ contract adds; got 'my-app-'. Use 'my-app'.`
827
+
828
+ **Why:** every cache name is `<prefix>-<kind>-<version>`, and the worker
829
+ deletes the caches shaped `<prefix>-<kind>-<version>`. An empty prefix would own
830
+ every cache of the origin, and a trailing dash would be doubled.
831
+
832
+ **Fix:** pass the app's own name, without the dash:
833
+
834
+ ```ts
835
+ import { defineWorkerContract } from '@alxia/react-router/pwa';
836
+
837
+ export const workerContract = defineWorkerContract({ cachePrefix: 'my-app' });
838
+ ```
839
+
840
+ ### `TypeError: defineWorkerContract(): cacheVersion must be a non-empty name without '-', …`
841
+
842
+ The full message is `defineWorkerContract(): cacheVersion must be a
843
+ non-empty name without '-', such as 'v2'; got 'v1-beta'.`
844
+
845
+ **Why:** every cache name is `<prefix>-<kind>-<version>`, and the contract
846
+ tells a version from the rest of the name by its dashes. A version with a `-`
847
+ (or an empty one) would make the name ambiguous.
848
+
849
+ **Fix:** use a name with no dash, such as `'v2'` or `'2026'`:
850
+
851
+ ```ts
852
+ import { defineWorkerContract } from '@alxia/react-router/pwa';
853
+
854
+ export const workerContract = defineWorkerContract({
855
+ cachePrefix: 'my-app',
856
+ cacheVersion: 'v2',
857
+ });
858
+ ```
859
+
860
+ ### `parseBrowserManifest(): not React Router's browser manifest …`
861
+
862
+ The full message ends with `(build/client/assets/manifest-*.js).`
863
+
864
+ **Why:** the source holds no `{ … }` object. The build script read another
865
+ file, an empty one, or the build has not run.
866
+
867
+ **Fix:** run `react-router build` first, then read the file named
868
+ `manifest-<hash>.js` in `build/client/assets`, not the server's manifest:
869
+
870
+ ```ts
871
+ import { readdirSync, readFileSync } from 'node:fs';
872
+ import { parseBrowserManifest } from '@alxia/react-router/pwa';
873
+
874
+ const assets = 'build/client/assets';
875
+ const file = readdirSync(assets).find((name) => /^manifest-.*\.js$/.test(name));
876
+ if (!file) throw new Error('Run react-router build first.');
877
+ export const manifest = parseBrowserManifest(readFileSync(`${assets}/${file}`, 'utf8'));
878
+ ```
879
+
880
+ ### `error: Cannot find package 'react' from '…/@alxia/react-router/dist/pwa/react/index.js'`
881
+
882
+ **Why:** `@alxia/react-router/pwa/react` imports `react`, an optional peer of
883
+ the package, so nothing installs it for you. An app with no `react` does not
884
+ need the subpath; a build script or the worker importing it by mistake meets
885
+ this message too.
886
+
887
+ **Fix:** add `react` where the page is built (`bun add react`), and import
888
+ `/pwa/react` from the page alone. The worker and a build script import
889
+ `@alxia/react-router/pwa`, which imports nothing.
890
+
891
+ ### `startWorker(): no precache list. …`
892
+
893
+ **Why:** the full message ends "Build this worker with `alxia()` from
894
+ `@alxia/react-router/vite` (`app/service-worker.ts`) or `buildWorker()` from
895
+ `@alxia/react-router/pwa/build`, which write it in." `startWorker` precaches
896
+ the list the build defines in the bundle (`globalThis.__ALXIA_PRECACHE__`);
897
+ a worker bundled by anything else has none, and the page would install a
898
+ worker that precaches nothing.
899
+
900
+ **Fix:** either build the worker with the build that writes the list
901
+ (`alxia()` or `buildWorker()`), or pass the list yourself, which works for a
902
+ worker bundled any other way:
903
+
904
+ ```ts
905
+ startWorker({
906
+ contract,
907
+ precache: [{ url: '/offline.html', revision: 'v1' }],
908
+ });
909
+ ```
910
+
911
+ `precache` wins over the list the build wrote. A bundle of your own may
912
+ instead define `globalThis.__ALXIA_PRECACHE__`.
913
+
914
+ ### `TypeError: startWorker(): contract is required: …`
915
+
916
+ **Why:** the full message continues "pass `defineWorkerContract({ cachePrefix })`
917
+ from `@alxia/react-router/pwa`." The worker's cache names, its offline page
918
+ and its URL come from the contract the page shares, so there is no default.
919
+
920
+ **Fix:**
921
+
922
+ ```ts
923
+ // file: app/service-worker.ts
924
+ import { defineWorkerContract } from '@alxia/react-router/pwa';
925
+ import { startWorker } from '@alxia/react-router/pwa/worker';
926
+
927
+ startWorker({ contract: defineWorkerContract({ cachePrefix: 'my-app' }) });
928
+ ```
929
+
930
+ ### `error: Could not resolve: "workbox-core". Maybe you need to "bun install"?`
931
+
932
+ **Why:** `@alxia/react-router/pwa/worker` imports six `workbox-*` packages,
933
+ optional peers of the package, so nothing installs them for you. They are
934
+ bundled into the worker, never loaded at run time.
935
+
936
+ **Fix:** add them as development dependencies:
937
+
938
+ ```sh
939
+ bun add -d workbox-core workbox-expiration workbox-navigation-preload \
940
+ workbox-precaching workbox-routing workbox-strategies
941
+ ```
942
+
943
+ The `buildWorker()` messages below are the ones a script of your own sees.
944
+ Under `alxia()` each of them begins with `alxia-react-router: `.
945
+
946
+ ### `buildWorker() runs on Bun: it is running on Node`
947
+
948
+ ```text
949
+ buildWorker() runs on Bun: it is running on Node. Run it with bun, or run react-router build with bun --bun.
950
+ ```
951
+
952
+ **When:** a script of your own calls `buildWorker()` and runs on Node. Under
953
+ `alxia()` the check comes first and reads
954
+ [`… is running on Node, and the worker is bundled by Bun.build`](#alxia-react-router--is-running-on-node-and-alxias-server-runs-on-bun-).
955
+
956
+ **Why:** the worker is bundled with `Bun.build`, which Node does not have.
957
+
958
+ **Fix:** run the script with `bun scripts/build-worker.ts`, or
959
+ `react-router build` as `bun --bun react-router build`.
960
+
961
+ ### `buildWorker(): app/service-worker.ts does not exist.`
962
+
963
+ ```text
964
+ buildWorker(): app/service-worker.ts does not exist.
965
+ ```
966
+
967
+ **Why:** `file` is read from `root` (`process.cwd()` by default), not from the
968
+ script's folder. Under `alxia()` the message begins
969
+ `alxia-react-router: buildWorker(): …`.
970
+
971
+ **Fix:** pass the path relative to the project root, or pass `root`.
972
+
973
+ ### `buildWorker(): expected one React Router browser manifest`
974
+
975
+ ```text
976
+ buildWorker(): expected one React Router browser manifest, build/client/assets/manifest-*.js, found 0. Run react-router build first.
977
+ ```
978
+
979
+ The message ends `found 2` when two are left.
980
+
981
+ **Why:** the precache list is read from React Router's browser manifest,
982
+ which `react-router build` writes. None means the build did not run, or
983
+ `client` is wrong. Two mean a stale build folder.
984
+
985
+ **Fix:** run `react-router build` first, check `client`, and delete `build/`
986
+ when two manifests are left.
987
+
988
+ ### `buildWorker(): build/client/offline.html does not exist`
989
+
990
+ ```text
991
+ buildWorker(): build/client/offline.html does not exist. Add public/offline.html, or pass offline: false (`worker: { offline: false }` in alxia()), or pass the app's contract (`worker: { contract }` in alxia()).
992
+ ```
993
+
994
+ **Why:** the offline page is precached by default and is read from the client
995
+ build, where `public/offline.html` is copied. The build reads the contract's
996
+ `offlineUrl` from the `contract` option, and the worker from the contract it
997
+ is given: two objects can disagree.
998
+
999
+ **Fix:** add the page to `public/`, or pass `offline: false` for a worker with
1000
+ no offline page. If the contract's `offlineUrl` is another path, pass the same
1001
+ contract object to the build (`alxia({ worker: { contract } })`, or
1002
+ `buildWorker({ contract })`).
1003
+
1004
+ ### `buildWorker(): … is outside ….`
1005
+
1006
+ ```text
1007
+ buildWorker(): /../service-worker.js is outside /app/build/client.
1008
+ ```
1009
+
1010
+ **Why:** the contract's `serviceWorkerUrl` or `offlineUrl` is joined onto the
1011
+ client build, and the result leaves it, or is the client build itself (`/`).
1012
+ The build refuses to write or read a file there. It also refuses an absolute
1013
+ URL (`https://example.com/sw.js`), a protocol-relative one (`//host/sw.js`) and
1014
+ one that does not start with `/`, since none names a file of the build.
1015
+
1016
+ **Fix:** use a root-relative URL inside the site, such as
1017
+ `/service-worker.js`, in the contract.
1018
+
1019
+ ### `buildWorker(): Bun.build failed:`
1020
+
1021
+ ```text
1022
+ buildWorker(): Bun.build failed:
1023
+ ```
1024
+
1025
+ Bun's logs follow the message.
1026
+
1027
+ **Why:** the worker's source or one of its imports did not bundle: a missing
1028
+ `workbox-*` package, a wrong import path.
1029
+
1030
+ **Fix:** read the first log line. The six `workbox-*` packages are optional
1031
+ peers, installed as
1032
+ [`Could not resolve: "workbox-core"`](#error-could-not-resolve-workbox-core-maybe-you-need-to-bun-install)
1033
+ shows.
1034
+
773
1035
  ## Types
774
1036
 
775
1037
  ### `Property 'alxia' does not exist on type 'Readonly<RouterContextProvider>'`
@@ -989,7 +1251,7 @@ following Content Security Policy directive: "script-src 'self'
989
1251
  attribute.
990
1252
 
991
1253
  **Fix:** reveal the entry and pass `nonceOf(loadContext)` to both
992
- ([guide](guide.md#a-csp-nonce)). A script of your own reads the nonce
1254
+ ([guide](guide/csp-nonce.md#a-csp-nonce)). A script of your own reads the nonce
993
1255
  through a loader, where `nonceOf(context)` works as well:
994
1256
 
995
1257
  ```tsx
@@ -1119,7 +1381,7 @@ plugin relays the app's sockets to. So `ctx.server` is that side server: its
1119
1381
  and its `requestIP` and `timeout` know nothing of a request Vite took, so
1120
1382
  the default `ctx.ip` is `undefined`. A `page()` (Bun's HTML bundle) is not
1121
1383
  served: Vite answers the HTML
1122
- ([The server](guide.md#the-server-server)).
1384
+ ([The server](guide/typing-the-loaders.md#the-server-server)).
1123
1385
 
1124
1386
  **Fix:** read `ctx.server` as optional, which also covers `app.request` in
1125
1387
  a test, and take nothing from its `url` or `requestIP` in dev:
@@ -1188,3 +1450,132 @@ export default createServer({ proxy: trustProxy({ trusted: ['10.0.0.0/8'] }) });
1188
1450
 
1189
1451
  React Router then gets its request at `originalUrl(ctx)`
1190
1452
  ([Behind a proxy](https://github.com/softistx/alxia/blob/develop/packages/core/docs/guide/serving.md#behind-a-proxy-proxy)).
1453
+
1454
+ ### An unhashed file under `public/assets/` is cached for a year
1455
+
1456
+ A file you put in `public/assets/` (a sprite, an icon) changes on a deploy,
1457
+ and visitors keep the old one.
1458
+
1459
+ **Why:** Vite copies `public/` into `build/client/`, and the client's files
1460
+ under `build/client/assets/` are served `public, max-age=31536000,
1461
+ immutable`, hashed or not. The worker's `cachingFor` agrees by default:
1462
+ everything under `/assets/` is `immutable`, cache-first.
1463
+
1464
+ **Fix:** keep unhashed files out of `assets/`: `public/icons/` is served for
1465
+ an hour and is `static` for the worker. If they must stay there, give
1466
+ `cachingFor` an `immutable` option that leaves their folder out, so the worker
1467
+ revalidates them ([the option](guide/pwa.md#what-the-worker-caches)),
1468
+ and expect the HTTP cache to hold them for a year until the file's name
1469
+ changes.
1470
+
1471
+ ### A cache the contract did not make is swept
1472
+
1473
+ Signing out, or a new worker activating, deletes a cache the app made itself.
1474
+
1475
+ **Why:** a contract owns every cache named exactly
1476
+ `<prefix>-<kind>-<version>`, kind being lowercase letters and digits and
1477
+ version having no `-`. That includes a kind the app retired (`my-app-pages-v0` is swept once
1478
+ it is not current). It is also why `app` and `app-admin` can share an origin:
1479
+ `app-admin-assets-v1` has one segment too many to be `app`'s. Contracts never
1480
+ take each other's caches; only a cache made by other code, such as
1481
+ `my-app-runtime-v1`, matches the shape and is swept.
1482
+
1483
+ **Fix:** name your own caches outside the shape. A name is outside it when it
1484
+ has more than `<kind>-<version>` after `<prefix>-`, with the prefix `my-app`:
1485
+ `my-app-runtime-v1-x`. A kind with an uppercase letter, `_` or `.`
1486
+ (`my-app-Runtime-v1`, `my-app-run_time-v1`), or a different prefix, is outside
1487
+ too.
1488
+
1489
+ ### The update notice never shows
1490
+
1491
+ A new version is deployed and the page offers no "Reload".
1492
+
1493
+ **Why:** the notice is offered only when a worker finished installing while
1494
+ an older one controls the page. A first install controls nothing, so it
1495
+ offers nothing; the next visit, with the first worker in control, does. And
1496
+ nothing is registered at all where the app does not call
1497
+ `registerServiceWorker` (for example while `useServiceWorker({ enabled })`
1498
+ is given `false`, as `import.meta.env.PROD` is under `react-router dev`). A
1499
+ tab that chose "Later" stays quiet until the next session.
1500
+
1501
+ **Fix:** build, serve and load the page twice, then deploy a change and load
1502
+ it again. In the console, `navigator.serviceWorker.controller` must not be
1503
+ `null` and `(await navigator.serviceWorker.getRegistration()).waiting` must
1504
+ hold the new worker.
1505
+
1506
+ ### The worker registers under `react-router dev`
1507
+
1508
+ The dev server serves stale code after an edit, or a page keeps an old
1509
+ version of itself, and `navigator.serviceWorker.controller` is not `null`.
1510
+
1511
+ **Why:** `useServiceWorker` registers whenever `enabled` is `true`, and the
1512
+ package does not know the mode: it never reads `import.meta.env`. A worker
1513
+ under a dev server caches the code you are editing.
1514
+
1515
+ **Fix:** pass the mode from the app, `useServiceWorker({ enabled:
1516
+ import.meta.env.PROD })` ([the hook](guide/pwa.md#in-react)). Then unregister
1517
+ the worker the dev server already took (DevTools, Application, Service
1518
+ Workers, Unregister) and clear its site data.
1519
+
1520
+ ### The service worker is not built
1521
+
1522
+ `react-router build` ends and there is no `build/client/service-worker.js`.
1523
+
1524
+ **Why:** `alxia()` builds no worker when the file is not at
1525
+ `app/service-worker.ts` (the `appDirectory` of `react-router.config.ts` may
1526
+ hold another) and no `worker` option names it, when `worker: false` is set,
1527
+ or when the version of `@alxia/react-router` is older than 0.10.
1528
+
1529
+ **Fix:** put the file at `app/service-worker.ts`, or name it with
1530
+ `alxia({ worker: 'app/pwa/worker.ts' })`, and update the package.
1531
+
1532
+ ### Offline, a client navigation to a route never visited shows the offline page
1533
+
1534
+ The page opens offline, but a link to a route the user never opened leads to
1535
+ `/offline.html`.
1536
+
1537
+ **Why:** React Router asks `/__manifest` for the routes a link needs, and the
1538
+ worker never caches that request.
1539
+
1540
+ **Fix:** set `routeDiscovery: { mode: 'initial' }` in `react-router.config.ts`,
1541
+ so the client has every route from its first load.
1542
+
1543
+ ### An update waits an hour
1544
+
1545
+ A new worker is deployed and tabs still run the old one for up to an hour.
1546
+
1547
+ **Why:** the worker's script was served with the client's hour of
1548
+ `Cache-Control`, so the browser's update check found the cached script.
1549
+ Either `noCache` is not declared, or it is declared in `configure`, which
1550
+ runs after the client's files: they answer first and the middleware never
1551
+ sees the request.
1552
+
1553
+ **Fix:** declare it in `beforeAll`
1554
+ ([serving the worker's script](guide/pwa.md#serving-the-workers-script)), and
1555
+ check with `curl -I https://your.app/service-worker.js` that `cache-control` is
1556
+ `no-cache`.
1557
+
1558
+ ### The worker cannot fetch another origin
1559
+
1560
+ The console reports `Refused to connect to 'https://api.example.com/…' because it violates the following Content Security Policy directive: "connect-src 'self'"` from the worker.
1561
+
1562
+ **Why:** `noCache` gives the worker's script `WORKER_POLICY`,
1563
+ `default-src 'none'; connect-src 'self'`: the worker fetches this site only.
1564
+
1565
+ **Fix:** if the worker really must reach another origin, serve the script
1566
+ with a wider policy through `revalidated(response, policy)` in a middleware of
1567
+ your own ([serving the worker's script](guide/pwa.md#serving-the-workers-script)),
1568
+ rather than loosening the pages' policy.
1569
+
1570
+ ### The manifest is in the wrong language
1571
+
1572
+ The installed app's name and description are in the default language,
1573
+ whatever the user chose.
1574
+
1575
+ **Why:** the browser fetches the manifest without cookies, so the `language`
1576
+ cookie never reaches the server. Only the link's query does.
1577
+
1578
+ **Fix:** link `manifestHref(language)` (`/manifest.webmanifest?lang=fr`), and
1579
+ read it with `manifestLanguage(url, supported, fallback)`; the fallback is a
1580
+ supported language the app resolves from the request's `Accept-Language`, not
1581
+ from the cookie ([the manifest route](guide/pwa.md#the-web-app-manifest-and-why-the-language-travels-in-lang)).