@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/README.md CHANGED
@@ -92,7 +92,10 @@ Vite's preview server, as `bun run start` would: the pages, `/api`, the
92
92
  client's files and the app's middlewares. So does React Router's prerendering.
93
93
 
94
94
  `@alxia/core` and `react-router` 8 are peers, and `vite` 7 or 8 is an
95
- optional peer, for `/vite`. The package declares no dependency.
95
+ optional peer, for `/vite`; `react` 19 is an optional peer, for `/pwa/react`. The six `workbox-*`
96
+ packages (`workbox-core`, `workbox-expiration`, `workbox-navigation-preload`,
97
+ `workbox-precaching`, `workbox-routing`, `workbox-strategies`) are optional
98
+ peers too, for `/pwa/worker` (the [PWA section](#installable-and-offline-pwa) has the install line). The package declares no dependency.
96
99
 
97
100
  Its `typescript` peer is 6 or 7. The template's TypeScript 5.9 typechecks,
98
101
  but `bun add` warns about it; `bun add -d typescript@^6` silences the
@@ -246,7 +249,7 @@ the `Bun.Server` the app's sockets are relayed to, on a loopback port:
246
249
  It is not `Register`'s `server`, which names what `createServer()` returned
247
250
  for the types.
248
251
 
249
- [More](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide.md#typing-the-loaders)
252
+ [More](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/typing-the-loaders.md#typing-the-loaders)
250
253
 
251
254
  ## A CSP nonce
252
255
 
@@ -293,7 +296,7 @@ bunx react-router reveal entry.server
293
296
  `nonceOf` returns `undefined` when no middleware set a nonce, so the entry works
294
297
  with or without secure-headers: neither package depends on the other. Any
295
298
  `derive` that returns a string `nonce` works the same.
296
- [More](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide.md#a-csp-nonce)
299
+ [More](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/csp-nonce.md#a-csp-nonce)
297
300
 
298
301
  ## WebSockets
299
302
 
@@ -321,7 +324,7 @@ middlewares before the route, a refusal's status, `socket.data`, `publish`. An
321
324
  edit to the server is used from the next connection. An action's
322
325
  `alxiaOf(context).server.publish` reaches those sockets too
323
326
  ([The server](#the-server-server)).
324
- [More](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide.md#websockets)
327
+ [More](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/beside-the-pages.md#websockets)
325
328
 
326
329
  ## Options
327
330
 
@@ -351,7 +354,7 @@ ignored. A server of your own gives `alxia({ proxy })` the same option. [More](h
351
354
  `build`, `mode` and `client` override what the plugin wires;
352
355
  `client: false` serves no client files, so you can serve them yourself.
353
356
  A server of your own, with no plugin, calls `reactRouter(app, { build,
354
- client })` itself. [More](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide.md#escape-hatches)
357
+ client })` itself. [More](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/customising-the-server.md#escape-hatches)
355
358
 
356
359
  ## The client's files
357
360
 
@@ -439,7 +442,7 @@ export default defineConfig({
439
442
  ```
440
443
 
441
444
  The
442
- [guide](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide.md#built-for-bun)
445
+ [guide](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/how-it-works.md#built-for-bun)
443
446
  has each option and why.
444
447
 
445
448
  ## Docker
@@ -474,9 +477,150 @@ docker run -p 3000:3000 my-app
474
477
 
475
478
  Keep the template's `.dockerignore`, and commit `bun.lock`. The server
476
479
  runs as the non-root `bun` user, on `PORT` (3000). The
477
- [guide](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide.md#docker)
480
+ [guide](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/deploying.md#docker)
478
481
  has the commented file, and what to copy for a package left external.
479
482
 
483
+ ## Installable and offline (PWA)
484
+
485
+ `@alxia/react-router/pwa` holds what an installable app shares and that has
486
+ no UI: the service worker's contract, which requests it caches and which
487
+ responses it may store, the precache list read from a build, the
488
+ registration and update notice, the web app manifest, and the cleaning at
489
+ sign-out. It imports nothing, so the worker, a build script, the page and
490
+ the server can all import it. The worker itself is `startWorker` in
491
+ `/pwa/worker`, its build `buildWorker` in `/pwa/build`, the hooks are in
492
+ `/pwa/react` (`react`, an optional peer) and `noCache` in `/pwa/server`; this
493
+ package never reads `import.meta.env`, so the app passes `enabled`.
494
+
495
+ With `app/service-worker.ts` in the app, `react-router build` writes
496
+ `build/client/service-worker.js` itself: `alxia()` bundles the file after the
497
+ client build, with the precache list written in, and there is no script to
498
+ add. It needs Bun (`bun --bun react-router build`, or a `bunfig.toml` with
499
+ `[run] bun = true`). `alxia({ worker: 'app/pwa/worker.ts' })` names another file
500
+ and `alxia({ worker: false })` builds none.
501
+
502
+ The worker bundles Workbox, whose six packages are optional peers:
503
+
504
+ ```sh
505
+ bun add -d workbox-core workbox-expiration workbox-navigation-preload workbox-precaching workbox-routing workbox-strategies
506
+ ```
507
+
508
+ ```ts
509
+ // file: app/pwa-contract.ts
510
+ // Shared by the worker and the page: every cache is named `my-app-<kind>-<version>`.
511
+ import { defineWorkerContract } from '@alxia/react-router/pwa';
512
+
513
+ export const workerContract = defineWorkerContract({ cachePrefix: 'my-app' });
514
+ ```
515
+
516
+ ```ts
517
+ // file: app/service-worker.ts
518
+ import { startWorker } from '@alxia/react-router/pwa/worker';
519
+ import { workerContract } from './pwa-contract';
520
+
521
+ startWorker({ contract: workerContract });
522
+ ```
523
+
524
+ ```tsx
525
+ // file: app/root.tsx
526
+ import { manifestHref } from '@alxia/react-router/pwa';
527
+ import { useServiceWorker } from '@alxia/react-router/pwa/react';
528
+ import { Outlet } from 'react-router';
529
+
530
+ // The browser reads the manifest to offer the install.
531
+ export function links() {
532
+ return [{ rel: 'manifest', href: manifestHref('en') }];
533
+ }
534
+
535
+ // A production build only, never under `react-router dev`.
536
+ export default function App() {
537
+ useServiceWorker({ enabled: import.meta.env.PROD });
538
+ return <Outlet />;
539
+ }
540
+ ```
541
+
542
+ ```ts
543
+ // file: app/server.ts
544
+ import { createServer } from '@alxia/react-router';
545
+ import {
546
+ MANIFEST_HEADERS,
547
+ MANIFEST_PATH,
548
+ SERVICE_WORKER_URL,
549
+ webAppManifest,
550
+ } from '@alxia/react-router/pwa';
551
+ import { noCache } from '@alxia/react-router/pwa/server';
552
+
553
+ // beforeAll runs before the client's files: the worker's script is
554
+ // revalidated. The manifest is a route, served with its own headers.
555
+ export default createServer({
556
+ beforeAll: (app) => app.use(SERVICE_WORKER_URL, noCache),
557
+ configure: (app) =>
558
+ app.get(MANIFEST_PATH, ({ reply }) => {
559
+ const manifest = webAppManifest({
560
+ name: 'My app',
561
+ description: 'My installable app',
562
+ lang: 'en',
563
+ themeColor: '#1e40af',
564
+ backgroundColor: '#ffffff',
565
+ });
566
+ return reply(200, JSON.stringify(manifest), { headers: MANIFEST_HEADERS });
567
+ }),
568
+ });
569
+ ```
570
+
571
+ The manifest names four icons (`MINIMAL_2023_ICONS`): put
572
+ `pwa-64x64.png`, `pwa-192x192.png`, `pwa-512x512.png` and
573
+ `maskable-icon-512x512.png` in `public/`, which `@vite-pwa/assets-generator`'s
574
+ `minimal2023Preset` writes from one image. A browser offers the install with
575
+ a 192 and a 512.
576
+
577
+ The worker serves a page load from `public/offline.html` when the network
578
+ fails, and the build needs the file:
579
+
580
+ ```html
581
+ <!-- file: public/offline.html -->
582
+ <!doctype html>
583
+ <html lang="en">
584
+ <head>
585
+ <meta charset="utf-8" />
586
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
587
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'" />
588
+ <title>Offline</title>
589
+ </head>
590
+ <body>
591
+ <h1>You are offline</h1>
592
+ <p>Check your connection and try again.</p>
593
+ </body>
594
+ </html>
595
+ ```
596
+
597
+ Set `routeDiscovery: { mode: 'initial' }` in `react-router.config.ts`, so a
598
+ client navigation offline to a route never visited finds its modules
599
+ ([more](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md#offline-a-client-navigation-to-a-route-never-visited-shows-the-offline-page)):
600
+
601
+ ```ts
602
+ import type { Config } from '@react-router/dev/config';
603
+
604
+ export default { ssr: true, routeDiscovery: { mode: 'initial' } } satisfies Config;
605
+ ```
606
+
607
+ An app that built its worker with a script of its own has steps to drop:
608
+ [moving from a build script of your own](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/pwa.md#moving-from-a-build-script-of-your-own).
609
+
610
+ For a build pipeline of your own, `buildWorker` from `/pwa/build` does what
611
+ `alxia()` does. It needs Bun and runs after `react-router build`:
612
+
613
+ ```ts
614
+ // file: scripts/build-worker.ts
615
+ import { buildWorker } from '@alxia/react-router/pwa/build';
616
+
617
+ await buildWorker({ file: 'app/service-worker.ts' });
618
+ ```
619
+
620
+ The [PWA guide](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/pwa.md)
621
+ has the worker's options, the offline page, the manifest route, the update
622
+ banner and the rest.
623
+
480
624
  ## Traps
481
625
 
482
626
  - **A context key made in `app/` reaches the loaders only through the
@@ -527,8 +671,70 @@ From `@alxia/react-router/vite`:
527
671
 
528
672
  | export | |
529
673
  | --- | --- |
530
- | `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 |
531
- | `AlxiaOptions` | its options |
674
+ | `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. `worker` is the service worker built after `react-router build`: `app/service-worker.ts` by default when it exists, a file, `AlxiaWorkerOptions`, or `false` |
675
+ | `AlxiaOptions`, `AlxiaWorkerOptions` | its options, and the worker's (`buildWorker()`'s but `root`, `client` and `file`, which is optional) |
676
+
677
+ From `@alxia/react-router/pwa`:
678
+
679
+ | export | |
680
+ | --- | --- |
681
+ | `defineWorkerContract(options)` | the contract of one app's worker from its `cachePrefix` (required), with `cacheVersion` (`v1`), `serviceWorkerUrl` and `offlineUrl`: its `cacheNames`, `obsoleteCaches(names)` and `clearableCaches(names)`. Throws on an empty prefix or one ending with `-`, and on an empty `cacheVersion` or one with a `-` |
682
+ | `WorkerContract`, `WorkerContractOptions`, `CacheNames` | its types |
683
+ | `SERVICE_WORKER_URL`, `OFFLINE_URL`, `SKIP_WAITING` | `/service-worker.js`, `/offline.html` and the message that applies an update |
684
+ | `cachingFor(request, origin, options?)` | `navigation`, `immutable`, `static` or `none` for a request. `options`: `never(path)`, `immutable(path)` (everything under `/assets/` by default), `serviceWorkerUrl`, `contract` (whose `serviceWorkerUrl` is never cached), `manifestPath` |
685
+ | `Caching`, `RequestFacts`, `CachingOptions` | its types |
686
+ | `isStorable(response)` | whether a response may be stored: a full, same-origin, `public` 200 of a script, style, font or image, not varying on who asks |
687
+ | `parseBrowserManifest(source)` | React Router's browser manifest, read from `build/client/assets/manifest-*.js` |
688
+ | `precacheList(manifest)` | the hashed scripts and stylesheets every route needs, as Workbox precache entries |
689
+ | `assetPrecache(files, include, base?)` | the entries of the files in `build/client/assets` that a regex of yours matches, such as fonts |
690
+ | `BrowserManifest`, `PrecacheEntry` | their types |
691
+ | `clearAppCaches(contract)` | deletes the caches the worker filled but the offline page; for sign-out. Does nothing without Cache Storage |
692
+ | `registerServiceWorker(container, onWaiting, options?)` | registers the worker (`url`, `scope` options) and calls `onWaiting(worker, apply)` for each new version waiting behind a controller; checks for an update when the tab shows again. `apply` activates the worker and reloads once |
693
+ | `watchForUpdates(registration, container, onWaiting)`, `createUpdater(container, reload?)`, `afterLoad(run)` | its parts: offering waiting workers, the one update path of a page, and running once the page has loaded |
694
+ | `ApplyUpdate`, `RegisterOptions`, `ServiceWorkerContainerLike` | their types |
695
+ | `createUpdateNotice(storage, key?)` | the update notice's store (`offer`, `current`, `subscribe`, as `useSyncExternalStore` takes it): hidden for the session by "Later", kept in `storage()`, which may be missing or throw |
696
+ | `UpdateNotice`, `UpdateNoticeStore`, `NoticeStorage` | its types. `NoticeStorage` is any object with `getItem(key)` and `setItem(key, value)` |
697
+ | `sessionStore`, `UPDATE_LATER_KEY` | `sessionStorage` or nothing, and the default key, `alxia.update-later` |
698
+ | `MANIFEST_PATH`, `MANIFEST_HEADERS` | `/manifest.webmanifest`, and the headers to serve it with: cacheable for an hour, varying on `Accept-Language`, a policy that loads nothing |
699
+ | `webAppManifest(options)` | the web app manifest's JSON from the app's `name`, `description`, `lang`, `themeColor`, `backgroundColor` and the optional rest; `id`, `start_url` and `scope` are `/` in every language |
700
+ | `WebAppManifest`, `WebAppManifestOptions`, `ManifestIcon` | its types |
701
+ | `MINIMAL_2023_ICONS` | the four icons `@vite-pwa/assets-generator`'s `minimal2023Preset` writes |
702
+ | `manifestHref(language, path?)` | the manifest's URL for a page: `/manifest.webmanifest?lang=fr` |
703
+ | `manifestLanguage(url, supported, fallback)` | `?lang=` when it is one of `supported`, else `fallback` |
704
+
705
+ From `@alxia/react-router/pwa/react` (`react`, an optional peer, is its only package import):
706
+
707
+ | export | |
708
+ | --- | --- |
709
+ | `useOnline()` | whether the browser says it is online; online on the server and while hydrating |
710
+ | `useServiceWorker(options)` | registers the worker after `load` when `options.enabled` (required; the app passes `import.meta.env.PROD`), in a browser that has one. `url`, `scope`, `notice` (where a waiting version is offered), `onError` (registering failed; dropped by default) |
711
+ | `ServiceWorkerOptions` | its options |
712
+ | `useUpdateNotice(notice?)` | the `{ reload, later }` to show while a new version waits and "Later" was not asked, else `undefined`; always `undefined` on the server |
713
+ | `updateNotice` | the page's one `UpdateNoticeStore`, which `useServiceWorker` offers and `useUpdateNotice` reads by default |
714
+
715
+ From `@alxia/react-router/pwa/server` (`@alxia/core` is its only package import):
716
+
717
+ | export | |
718
+ | --- | --- |
719
+ | `noCache` | a middleware: the response it wraps gets `Cache-Control: no-cache` and the worker's policy. For `use(SERVICE_WORKER_URL, noCache)` in `beforeAll` |
720
+ | `revalidated(response, policy?)` | a response with those two headers set, on a copy when its headers are immutable; `policy` is `WORKER_POLICY` by default |
721
+ | `WORKER_POLICY` | `default-src 'none'; connect-src 'self'`: the worker runs no other script and fetches this site only |
722
+
723
+ From `@alxia/react-router/pwa/worker` (the six `workbox-*` packages, optional peers, are its only package imports):
724
+
725
+ | export | |
726
+ | --- | --- |
727
+ | `startWorker(options)` | the worker's strategies: the precache route, `NetworkOnly` for a page load with the precached offline page as its fallback, `CacheFirst` for hashed bundles, `StaleWhileRevalidate` for other static files (both storing only what `isStorable` accepts), the obsolete caches swept on `activate`, and `SKIP_WAITING` applied |
728
+ | `WorkerOptions` | `contract` (required), `caching`, `assets`, `static`, `navigationPreload`, `precache` |
729
+ | `ExpirationOptions` | `{ maxEntries?, maxAgeSeconds? }` |
730
+
731
+ From `@alxia/react-router/pwa/build` (Bun; Node's built-ins its only imports):
732
+
733
+ | export | |
734
+ | --- | --- |
735
+ | `buildWorker(options)` | bundles the worker into `build/client/service-worker.js` with its precache list (the offline page by its content, every route's code, `precacheAssets`), after `react-router build` |
736
+ | `BuildWorkerOptions` | `file` (required), `root`, `client`, `contract`, `offline`, `precacheAssets`, `minify` |
737
+ | `WorkerBuild` | `{ file, precache, precacheBytes, bytes }` |
532
738
 
533
739
  The `alxia-react-router` bin, run with `bunx`:
534
740
 
@@ -538,7 +744,13 @@ The `alxia-react-router` bin, run with `bunx`:
538
744
 
539
745
  ## Documentation
540
746
 
541
- - [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.
747
+ - [Documentation index](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/README.md): every page, and when to read it.
748
+ - [Getting started](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/getting-started.md): a new app, the official template or an existing app.
749
+ - [How it works](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/how-it-works.md): dev, the build and `vite preview`, and what the build sets for Bun.
750
+ - [Customising the server](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/customising-the-server.md): `createServer()`, and the escape hatches.
751
+ - [Typing the loaders](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/typing-the-loaders.md): `Register`, and the app's own context keys.
752
+ - [A CSP nonce](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/csp-nonce.md), [Beside the pages](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/beside-the-pages.md) (middlewares, routes, WebSockets, the client's files), [OpenAPI](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/openapi.md), [Testing](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/testing.md) and [Deploying](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/deploying.md), with Docker.
753
+ - [Installable and offline](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/pwa.md): what the subpaths hold, the worker's contract, the worker, what it caches, the offline page, building the worker with `alxia()` or `buildWorker`, the hooks in React, registering the worker and offering an update, serving the worker's script, the web app manifest, and clearing at sign-out.
542
754
  - [Troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md): each message, and the traps that print none.
543
755
  - [Roadmap](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/roadmap.md): what is coming, and what is not planned.
544
756
  - [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.
@@ -0,0 +1,10 @@
1
+ // src/pwa/worker/injected.ts
2
+ var PRECACHE_DEFINE = "globalThis.__ALXIA_PRECACHE__";
3
+ function injectedPrecache() {
4
+ return globalThis.__ALXIA_PRECACHE__;
5
+ }
6
+
7
+ export { PRECACHE_DEFINE, injectedPrecache };
8
+
9
+ //# debugId=CA917CBC5F688F4264756E2164756E21
10
+ //# sourceMappingURL=index-59vhfj6n.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/pwa/worker/injected.ts"],
4
+ "sourcesContent": [
5
+ "/** The define key the build sets: the worker's precache list, as JSON. */\nexport const PRECACHE_DEFINE = 'globalThis.__ALXIA_PRECACHE__';\n\n/** One file the worker precaches: its URL and a revision (`null` when hashed in the name). */\ntype Entry = { readonly url: string; readonly revision: string | null };\n\n/**\n * The precache list the build wrote into this worker, or `undefined` when the\n * worker did not go through the build. Read through `globalThis`, so a missing\n * define is `undefined`, never a `ReferenceError`.\n */\nexport function injectedPrecache(): readonly Entry[] | undefined {\n\treturn (globalThis as { __ALXIA_PRECACHE__?: readonly Entry[] })\n\t\t.__ALXIA_PRECACHE__;\n}\n"
6
+ ],
7
+ "mappings": ";AACO,IAAM,kBAAkB;AAUxB,SAAS,gBAAgB,GAAiC;AAAA,EAChE,OAAQ,WACN;AAAA;",
8
+ "debugId": "CA917CBC5F688F4264756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,34 @@
1
+ // src/pwa/model/precache-list.ts
2
+ var HASHED_ASSET = /^\/assets\/[^/]+-[\w-]{8}\.(?:js|css)$/;
3
+ function parseBrowserManifest2(source) {
4
+ const start = source.indexOf("{");
5
+ const end = source.lastIndexOf("}");
6
+ if (start < 0 || end < start) {
7
+ throw new Error("parseBrowserManifest(): not React Router's browser manifest (build/client/assets/manifest-*.js).");
8
+ }
9
+ return JSON.parse(source.slice(start, end + 1));
10
+ }
11
+ function precacheList2(manifest) {
12
+ const urls = new Set([manifest.url]);
13
+ for (const part of [manifest.entry, ...Object.values(manifest.routes)]) {
14
+ for (const url of [
15
+ ...part.module ? [part.module] : [],
16
+ ...part.imports ?? [],
17
+ ...part.css ?? []
18
+ ]) {
19
+ urls.add(url);
20
+ }
21
+ }
22
+ return [...urls].filter((url) => HASHED_ASSET.test(url)).map((url) => ({ url, revision: null }));
23
+ }
24
+ function assetPrecache2(assetFiles, include, base = "/assets/") {
25
+ return assetFiles.filter((file) => {
26
+ include.lastIndex = 0;
27
+ return include.test(file);
28
+ }).sort().map((file) => ({ url: `${base}${file}`, revision: null }));
29
+ }
30
+
31
+ export { parseBrowserManifest2, precacheList2, assetPrecache2 };
32
+
33
+ //# debugId=812DDA9793A6A64B64756E2164756E21
34
+ //# sourceMappingURL=index-5gqcpyyg.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/pwa/model/precache-list.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * What the service worker precaches, read from a build: every script and\n * stylesheet React Router's manifest names (the client entry and each\n * route's module, imports and CSS), and the manifest itself, so a client\n * navigation offline to a route never visited still finds its code. Only\n * hashed `/assets/` files: a new build names new files, so each precaches\n * under its own URL, revision `null`. Chunks only a component loads on\n * demand stay out: a cache-first route stores them the first time they load.\n *\n * Dependency-free: a build script runs it at build time.\n */\n\n/** A route or the entry, as `window.__reactRouterManifest` lists it. */\ntype ManifestModule = { module?: string; imports?: string[]; css?: string[] };\n\n/** The parts of React Router's browser manifest the list reads. */\nexport type BrowserManifest = {\n\turl: string;\n\tentry: ManifestModule;\n\troutes: Record<string, ManifestModule>;\n};\n\n/** A Workbox precache entry: a hashed URL needs no revision. */\nexport type PrecacheEntry = { url: string; revision: string | null };\n\n/** A Vite-hashed file under `/assets/`: `name-<8 hash chars>.js|css`. */\nconst HASHED_ASSET = /^\\/assets\\/[^/]+-[\\w-]{8}\\.(?:js|css)$/;\n\n/** Reads `window.__reactRouterManifest={…};` (build/client/assets/manifest-*.js). */\nexport function parseBrowserManifest(source: string): BrowserManifest {\n\tconst start = source.indexOf('{');\n\tconst end = source.lastIndexOf('}');\n\tif (start < 0 || end < start) {\n\t\tthrow new Error(\n\t\t\t\"parseBrowserManifest(): not React Router's browser manifest (build/client/assets/manifest-*.js).\",\n\t\t);\n\t}\n\treturn JSON.parse(source.slice(start, end + 1)) as BrowserManifest;\n}\n\n/** The hashed scripts and stylesheets every route needs, the manifest's own file first. */\nexport function precacheList(manifest: BrowserManifest): PrecacheEntry[] {\n\tconst urls = new Set<string>([manifest.url]);\n\tfor (const part of [manifest.entry, ...Object.values(manifest.routes)]) {\n\t\tfor (const url of [\n\t\t\t...(part.module ? [part.module] : []),\n\t\t\t...(part.imports ?? []),\n\t\t\t...(part.css ?? []),\n\t\t]) {\n\t\t\turls.add(url);\n\t\t}\n\t}\n\treturn [...urls]\n\t\t.filter((url) => HASHED_ASSET.test(url))\n\t\t.map((url) => ({ url, revision: null }));\n}\n\n/**\n * The precache entries of the files among `assetFiles` (the names in\n * build/client/assets) that `include` matches, sorted, under `base`: fonts\n * an app keeps offline, say. The regex is the app's.\n */\nexport function assetPrecache(\n\tassetFiles: readonly string[],\n\tinclude: RegExp,\n\tbase = '/assets/',\n): PrecacheEntry[] {\n\treturn assetFiles\n\t\t.filter((file) => {\n\t\t\tinclude.lastIndex = 0; // a /g or /y regex remembers its last match\n\t\t\treturn include.test(file);\n\t\t})\n\t\t.sort()\n\t\t.map((file) => ({ url: `${base}${file}`, revision: null }));\n}\n"
6
+ ],
7
+ "mappings": ";AA0BA,IAAM,eAAe;AAGd,SAAS,qBAAoB,CAAC,QAAiC;AAAA,EACrE,MAAM,QAAQ,OAAO,QAAQ,GAAG;AAAA,EAChC,MAAM,MAAM,OAAO,YAAY,GAAG;AAAA,EAClC,IAAI,QAAQ,KAAK,MAAM,OAAO;AAAA,IAC7B,MAAM,IAAI,MACT,kGACD;AAAA,EACD;AAAA,EACA,OAAO,KAAK,MAAM,OAAO,MAAM,OAAO,MAAM,CAAC,CAAC;AAAA;AAIxC,SAAS,aAAY,CAAC,UAA4C;AAAA,EACxE,MAAM,OAAO,IAAI,IAAY,CAAC,SAAS,GAAG,CAAC;AAAA,EAC3C,WAAW,QAAQ,CAAC,SAAS,OAAO,GAAG,OAAO,OAAO,SAAS,MAAM,CAAC,GAAG;AAAA,IACvE,WAAW,OAAO;AAAA,MACjB,GAAI,KAAK,SAAS,CAAC,KAAK,MAAM,IAAI,CAAC;AAAA,MACnC,GAAI,KAAK,WAAW,CAAC;AAAA,MACrB,GAAI,KAAK,OAAO,CAAC;AAAA,IAClB,GAAG;AAAA,MACF,KAAK,IAAI,GAAG;AAAA,IACb;AAAA,EACD;AAAA,EACA,OAAO,CAAC,GAAG,IAAI,EACb,OAAO,CAAC,QAAQ,aAAa,KAAK,GAAG,CAAC,EACtC,IAAI,CAAC,SAAS,EAAE,KAAK,UAAU,KAAK,EAAE;AAAA;AAQlC,SAAS,cAAa,CAC5B,YACA,SACA,OAAO,YACW;AAAA,EAClB,OAAO,WACL,OAAO,CAAC,SAAS;AAAA,IACjB,QAAQ,YAAY;AAAA,IACpB,OAAO,QAAQ,KAAK,IAAI;AAAA,GACxB,EACA,KAAK,EACL,IAAI,CAAC,UAAU,EAAE,KAAK,GAAG,OAAO,QAAQ,UAAU,KAAK,EAAE;AAAA;",
8
+ "debugId": "812DDA9793A6A64B64756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,108 @@
1
+ import {
2
+ SERVICE_WORKER_URL2,
3
+ SKIP_WAITING2
4
+ } from "./index-rfr02vg4.js";
5
+
6
+ // src/pwa/model/registration.ts
7
+ function watchForUpdates2(registration, container, onWaiting) {
8
+ const offer = (worker) => {
9
+ if (worker && container.controller)
10
+ onWaiting(worker);
11
+ };
12
+ offer(registration.waiting);
13
+ registration.addEventListener("updatefound", () => {
14
+ const installing = registration.installing;
15
+ installing?.addEventListener("statechange", () => {
16
+ if (installing.state === "installed")
17
+ offer(installing);
18
+ });
19
+ });
20
+ }
21
+ function createUpdater2(container, reload = () => window.location.reload()) {
22
+ let requested = false;
23
+ let reloaded = false;
24
+ const reloadOnce = () => {
25
+ if (reloaded)
26
+ return;
27
+ reloaded = true;
28
+ reload();
29
+ };
30
+ container.addEventListener("controllerchange", () => {
31
+ if (requested)
32
+ reloadOnce();
33
+ });
34
+ return (worker) => {
35
+ const active = worker.state === "activating" || worker.state === "activated" || container.controller === worker;
36
+ if (active)
37
+ return reloadOnce();
38
+ requested = true;
39
+ worker.postMessage({ type: SKIP_WAITING2 });
40
+ };
41
+ }
42
+ function afterLoad2(run) {
43
+ if (document.readyState === "complete")
44
+ run();
45
+ else
46
+ window.addEventListener("load", run, { once: true });
47
+ }
48
+ var updater;
49
+ async function registerServiceWorker2(container, onWaiting, options = {}) {
50
+ updater ??= createUpdater2(container);
51
+ const apply = updater;
52
+ const registration = await container.register(options.url ?? SERVICE_WORKER_URL2, { scope: options.scope ?? "/" });
53
+ watchForUpdates2(registration, container, (worker) => onWaiting(worker, apply));
54
+ document.addEventListener("visibilitychange", () => {
55
+ if (document.visibilityState === "visible")
56
+ registration.update().catch(() => {});
57
+ });
58
+ return registration;
59
+ }
60
+
61
+ // src/pwa/model/update-notice.ts
62
+ var UPDATE_LATER_KEY2 = "alxia.update-later";
63
+ function createUpdateNotice2(storage, key = UPDATE_LATER_KEY2) {
64
+ let reload;
65
+ let laterThisPage = false;
66
+ let notice;
67
+ const listeners = new Set;
68
+ const later = () => {
69
+ laterThisPage = true;
70
+ try {
71
+ storage()?.setItem(key, "1");
72
+ } catch {}
73
+ refresh();
74
+ };
75
+ const laterAsked = () => {
76
+ if (laterThisPage)
77
+ return true;
78
+ try {
79
+ return storage()?.getItem(key) === "1";
80
+ } catch {
81
+ return false;
82
+ }
83
+ };
84
+ function refresh() {
85
+ notice = reload && !laterAsked() ? { reload, later } : undefined;
86
+ for (const listener of listeners)
87
+ listener();
88
+ }
89
+ return {
90
+ offer(apply) {
91
+ reload = apply;
92
+ refresh();
93
+ },
94
+ current: () => notice,
95
+ subscribe(listener) {
96
+ listeners.add(listener);
97
+ return () => {
98
+ listeners.delete(listener);
99
+ };
100
+ }
101
+ };
102
+ }
103
+ var sessionStore2 = () => typeof sessionStorage === "undefined" ? undefined : sessionStorage;
104
+
105
+ export { watchForUpdates2, createUpdater2, afterLoad2, registerServiceWorker2, UPDATE_LATER_KEY2, createUpdateNotice2, sessionStore2 };
106
+
107
+ //# debugId=824F7298E795FD3C64756E2164756E21
108
+ //# sourceMappingURL=index-9xv2a8et.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/pwa/model/registration.ts", "../src/pwa/model/update-notice.ts"],
4
+ "sourcesContent": [
5
+ "import { SERVICE_WORKER_URL, SKIP_WAITING } from './worker-contract';\n\n/** What the registration reads of `navigator.serviceWorker`. */\nexport type ServiceWorkerContainerLike = Pick<\n\tServiceWorkerContainer,\n\t'controller' | 'addEventListener' | 'register'\n>;\n\n/**\n * Calls `onWaiting` with each new version that finished installing while an\n * older one controls the page: the one waiting now, and each found later.\n * A first install controls nothing yet, so it offers nothing.\n */\nexport function watchForUpdates(\n\tregistration: ServiceWorkerRegistration,\n\tcontainer: Pick<ServiceWorkerContainerLike, 'controller'>,\n\tonWaiting: (worker: ServiceWorker) => void,\n): void {\n\tconst offer = (worker: ServiceWorker | null) => {\n\t\tif (worker && container.controller) onWaiting(worker);\n\t};\n\toffer(registration.waiting);\n\tregistration.addEventListener('updatefound', () => {\n\t\tconst installing = registration.installing;\n\t\tinstalling?.addEventListener('statechange', () => {\n\t\t\tif (installing.state === 'installed') offer(installing);\n\t\t});\n\t});\n}\n\n/** Moves this page onto a waiting worker: `apply(worker)`. */\nexport type ApplyUpdate = (\n\tworker: Pick<ServiceWorker, 'postMessage' | 'state'>,\n) => void;\n\n/**\n * The one update path of a page, created once: `apply(worker)` activates\n * the waiting worker and reloads once, when it takes control, so the page\n * runs the version its assets came from.\n *\n * - Several tabs: another tab may have activated the worker already. Its\n * `controllerchange` reached this page before anyone asked, so this page\n * did not reload (a form may be half filled); its \"Reload\" then reloads\n * at once, the worker being active.\n * - Clicked twice, or a `controllerchange` after a direct reload: one\n * reload, never two.\n */\nexport function createUpdater(\n\tcontainer: Pick<\n\t\tServiceWorkerContainerLike,\n\t\t'controller' | 'addEventListener'\n\t>,\n\treload: () => void = () => window.location.reload(),\n): ApplyUpdate {\n\tlet requested = false;\n\tlet reloaded = false;\n\tconst reloadOnce = () => {\n\t\tif (reloaded) return;\n\t\treloaded = true;\n\t\treload();\n\t};\n\tcontainer.addEventListener('controllerchange', () => {\n\t\tif (requested) reloadOnce();\n\t});\n\treturn (worker) => {\n\t\tconst active =\n\t\t\tworker.state === 'activating' ||\n\t\t\tworker.state === 'activated' ||\n\t\t\tcontainer.controller === worker;\n\t\tif (active) return reloadOnce();\n\t\trequested = true;\n\t\tworker.postMessage({ type: SKIP_WAITING });\n\t};\n}\n\n/** Runs `run` once the page has loaded: registering never competes with it. */\nexport function afterLoad(run: () => void): void {\n\tif (document.readyState === 'complete') run();\n\telse window.addEventListener('load', run, { once: true });\n}\n\nexport interface RegisterOptions {\n\t/** The worker's path. `SERVICE_WORKER_URL` by default. */\n\turl?: string;\n\t/** The scope it controls. `'/'` by default. */\n\tscope?: string;\n}\n\nlet updater: ApplyUpdate | undefined;\n\n/**\n * Registers the worker and watches for updates, handing each waiting worker\n * to `onWaiting` with the page's one `apply` (`createUpdater`, made on the\n * first call: one per page). A tab coming back to the foreground checks for\n * an update, as a long-lived page otherwise would only on its next full load.\n */\nexport async function registerServiceWorker(\n\tcontainer: ServiceWorkerContainerLike,\n\tonWaiting: (worker: ServiceWorker, apply: ApplyUpdate) => void,\n\toptions: RegisterOptions = {},\n): Promise<ServiceWorkerRegistration> {\n\tupdater ??= createUpdater(container);\n\tconst apply = updater;\n\tconst registration = await container.register(\n\t\toptions.url ?? SERVICE_WORKER_URL,\n\t\t{ scope: options.scope ?? '/' },\n\t);\n\twatchForUpdates(registration, container, (worker) =>\n\t\tonWaiting(worker, apply),\n\t);\n\tdocument.addEventListener('visibilitychange', () => {\n\t\tif (document.visibilityState === 'visible')\n\t\t\tregistration.update().catch(() => {\n\t\t\t\t// Offline, or the server is down: the next check will tell.\n\t\t\t});\n\t});\n\treturn registration;\n}\n",
6
+ "/**\n * The session's \"Later\": the notice stays hidden until the next visit.\n * `sessionStorage` is per origin, so one key serves every app; a second\n * argument of `createUpdateNotice` names another.\n */\nexport const UPDATE_LATER_KEY = 'alxia.update-later';\n\n/** The part of `Storage` the notice reads and writes. */\nexport type NoticeStorage = {\n\tgetItem(key: string): string | null;\n\tsetItem(key: string, value: string): void;\n};\n\n/** What the notice offers while a new version waits. */\nexport type UpdateNotice = {\n\t/** Moves the page onto the new version: the worker's `apply`. */\n\treload: () => void;\n\t/** Hides the notice until the next visit (the tab's session). */\n\tlater: () => void;\n};\n\n/** What `createUpdateNotice` returns: the shape `useSyncExternalStore` takes. */\nexport interface UpdateNoticeStore {\n\t/** A new version waits: `apply` moves the page onto it. */\n\toffer(apply: () => void): void;\n\t/** The notice to show, if any (the same object until it changes). */\n\tcurrent(): UpdateNotice | undefined;\n\t/** Calls `listener` on each change; returns the function that stops it. */\n\tsubscribe(listener: () => void): () => void;\n}\n\n/**\n * The update notice's state: offered by the worker's registration, read by\n * the banners, hidden for the session by \"Later\". `storage` is the session's\n * (`sessionStorage`), which may be missing or throw (private mode, blocked\n * storage): the notice then hides for this page only. It is a function so\n * that nothing reads the global at load.\n */\nexport function createUpdateNotice(\n\tstorage: () => NoticeStorage | undefined,\n\tkey: string = UPDATE_LATER_KEY,\n): UpdateNoticeStore {\n\tlet reload: (() => void) | undefined;\n\tlet laterThisPage = false;\n\tlet notice: UpdateNotice | undefined;\n\tconst listeners = new Set<() => void>();\n\n\tconst later = () => {\n\t\tlaterThisPage = true;\n\t\ttry {\n\t\t\tstorage()?.setItem(key, '1');\n\t\t} catch {\n\t\t\t// No storage: hidden for this page only.\n\t\t}\n\t\trefresh();\n\t};\n\tconst laterAsked = () => {\n\t\tif (laterThisPage) return true;\n\t\ttry {\n\t\t\treturn storage()?.getItem(key) === '1';\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t};\n\tfunction refresh() {\n\t\tnotice = reload && !laterAsked() ? { reload, later } : undefined;\n\t\tfor (const listener of listeners) listener();\n\t}\n\n\treturn {\n\t\toffer(apply) {\n\t\t\treload = apply;\n\t\t\trefresh();\n\t\t},\n\t\tcurrent: () => notice,\n\t\tsubscribe(listener) {\n\t\t\tlisteners.add(listener);\n\t\t\treturn () => {\n\t\t\t\tlisteners.delete(listener);\n\t\t\t};\n\t\t},\n\t};\n}\n\n/** `sessionStorage`, or nothing where there is none (the server). */\nexport const sessionStore = (): NoticeStorage | undefined =>\n\ttypeof sessionStorage === 'undefined' ? undefined : sessionStorage;\n"
7
+ ],
8
+ "mappings": "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",
9
+ "debugId": "824F7298E795FD3C64756E2164756E21",
10
+ "names": []
11
+ }
@@ -0,0 +1,119 @@
1
+ import {
2
+ parseBrowserManifest2,
3
+ precacheList2,
4
+ assetPrecache2
5
+ } from "./index-5gqcpyyg.js";
6
+ import {
7
+ PRECACHE_DEFINE
8
+ } from "./index-59vhfj6n.js";
9
+ import {
10
+ SERVICE_WORKER_URL2,
11
+ OFFLINE_URL2
12
+ } from "./index-rfr02vg4.js";
13
+
14
+ // src/pwa/build/build-worker.ts
15
+ import { existsSync } from "node:fs";
16
+ import { readdir, readFile, stat } from "node:fs/promises";
17
+ import { basename, dirname, join, resolve, sep } from "node:path";
18
+
19
+ // src/pwa/build/precache.ts
20
+ import { createHash } from "node:crypto";
21
+ function browserManifestFile(files) {
22
+ const found = files.filter((file) => /^manifest-[\w-]+\.js$/.test(file));
23
+ const [only] = found;
24
+ if (found.length !== 1 || only === undefined) {
25
+ throw new Error(`buildWorker(): expected one React Router browser manifest, build/client/assets/manifest-*.js, found ${found.length}. Run react-router build first.`);
26
+ }
27
+ return only;
28
+ }
29
+ function revisionOf(bytes) {
30
+ return createHash("sha256").update(bytes).digest("hex").slice(0, 16);
31
+ }
32
+ function workerPrecache(input) {
33
+ const all = [
34
+ ...input.offline ? [input.offline] : [],
35
+ ...precacheList2(parseBrowserManifest2(input.manifestSource)),
36
+ ...input.include ? assetPrecache2(input.assetFiles, input.include) : []
37
+ ];
38
+ const seen = new Set;
39
+ return all.filter(({ url }) => !seen.has(url) && seen.add(url));
40
+ }
41
+
42
+ // src/pwa/build/build-worker.ts
43
+ function assertBun(onBun = globalThis.Bun !== undefined) {
44
+ if (onBun)
45
+ return;
46
+ throw new Error("buildWorker() runs on Bun: it is running on Node. Run it with bun, or run react-router build with bun --bun.");
47
+ }
48
+ function urlInClient(client, url) {
49
+ const path = join(client, url);
50
+ if (!url.startsWith("/") || /^[a-z][a-z0-9+.-]*:|^\/\//i.test(url) || !path.startsWith(client + sep) || path.endsWith(sep)) {
51
+ throw new Error(`buildWorker(): ${url} is outside ${client}.`);
52
+ }
53
+ return path;
54
+ }
55
+ async function buildWorker2(options) {
56
+ assertBun();
57
+ const root = resolve(options.root ?? process.cwd());
58
+ const client = resolve(root, options.client ?? "build/client");
59
+ const source = resolve(root, options.file);
60
+ const workerUrl = options.contract?.serviceWorkerUrl ?? SERVICE_WORKER_URL2;
61
+ const offlineUrl = options.contract?.offlineUrl ?? OFFLINE_URL2;
62
+ if (!existsSync(source)) {
63
+ throw new Error(`buildWorker(): ${options.file} does not exist.`);
64
+ }
65
+ const assets = join(client, "assets");
66
+ const files = await readdir(assets).catch(() => []);
67
+ const manifest = browserManifestFile(files);
68
+ let offline;
69
+ if (options.offline !== false) {
70
+ const page = urlInClient(client, offlineUrl);
71
+ if (!existsSync(page)) {
72
+ throw new Error(`buildWorker(): ${options.client ?? "build/client"}${offlineUrl} does not exist. Add public${offlineUrl}, or pass offline: false (\`worker: { offline: false }\` in alxia()), or pass the app's contract (\`worker: { contract }\` in alxia()).`);
73
+ }
74
+ offline = { url: offlineUrl, revision: revisionOf(await readFile(page)) };
75
+ }
76
+ const precache = workerPrecache({
77
+ manifestSource: await readFile(join(assets, manifest), "utf8"),
78
+ assetFiles: files,
79
+ offline,
80
+ include: options.precacheAssets
81
+ });
82
+ const out = urlInClient(client, workerUrl);
83
+ const result = await Bun.build({
84
+ entrypoints: [source],
85
+ outdir: dirname(out),
86
+ naming: basename(out),
87
+ target: "browser",
88
+ format: "iife",
89
+ minify: options.minify ?? true,
90
+ define: {
91
+ "process.env.NODE_ENV": '"production"',
92
+ [PRECACHE_DEFINE]: JSON.stringify(precache)
93
+ }
94
+ }).catch((error) => {
95
+ const detail = error instanceof AggregateError ? error.errors.map(String).join(`
96
+ `) : String(error);
97
+ throw new Error(`buildWorker(): Bun.build failed:
98
+ ${detail}`);
99
+ });
100
+ if (!result.success) {
101
+ throw new Error(`buildWorker(): Bun.build failed:
102
+ ${result.logs.map(String).join(`
103
+ `)}`);
104
+ }
105
+ let precacheBytes = 0;
106
+ for (const { url } of precache) {
107
+ precacheBytes += (await stat(join(client, url))).size;
108
+ }
109
+ return {
110
+ file: out,
111
+ precache,
112
+ precacheBytes,
113
+ bytes: (await stat(out)).size
114
+ };
115
+ }
116
+ export { buildWorker2 };
117
+
118
+ //# debugId=763F0D0F0869B4B964756E2164756E21
119
+ //# sourceMappingURL=index-mppz0806.js.map