@alxia/react-router 0.9.0 → 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 (51) hide show
  1. package/README.md +126 -13
  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-zqa30gr7.js → index-9xv2a8et.js} +7 -38
  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.js +34 -134
  23. package/dist/pwa/index.js.map +4 -8
  24. package/dist/pwa/react/index.js +1 -1
  25. package/dist/pwa/worker/index.d.ts +9 -0
  26. package/dist/pwa/worker/index.d.ts.map +1 -0
  27. package/dist/pwa/worker/index.js +144 -0
  28. package/dist/pwa/worker/index.js.map +12 -0
  29. package/dist/pwa/worker/injected.d.ts +15 -0
  30. package/dist/pwa/worker/injected.d.ts.map +1 -0
  31. package/dist/pwa/worker/plan.d.ts +65 -0
  32. package/dist/pwa/worker/plan.d.ts.map +1 -0
  33. package/dist/pwa/worker/scope.d.ts +18 -0
  34. package/dist/pwa/worker/scope.d.ts.map +1 -0
  35. package/dist/pwa/worker/start-worker.d.ts +17 -0
  36. package/dist/pwa/worker/start-worker.d.ts.map +1 -0
  37. package/dist/vite/index.d.ts +10 -0
  38. package/dist/vite/index.d.ts.map +1 -1
  39. package/dist/vite/index.js +49 -3
  40. package/dist/vite/index.js.map +6 -5
  41. package/dist/vite/runtime.d.ts +1 -1
  42. package/dist/vite/runtime.d.ts.map +1 -1
  43. package/dist/vite/worker.d.ts +16 -0
  44. package/dist/vite/worker.d.ts.map +1 -0
  45. package/docs/README.md +1 -1
  46. package/docs/guide/how-it-works.md +7 -1
  47. package/docs/guide/pwa.md +372 -211
  48. package/docs/roadmap.md +19 -5
  49. package/docs/troubleshooting.md +208 -3
  50. package/package.json +46 -4
  51. package/dist/chunks/index-zqa30gr7.js.map +0 -12
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`; `react` 19 is an optional peer, for `/pwa/react`. 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
@@ -484,23 +487,51 @@ no UI: the service worker's contract, which requests it caches and which
484
487
  responses it may store, the precache list read from a build, the
485
488
  registration and update notice, the web app manifest, and the cleaning at
486
489
  sign-out. It imports nothing, so the worker, a build script, the page and
487
- the server can all import it. The hooks are in `/pwa/react`
488
- (`react`, an optional peer) and `noCache` in `/pwa/server`; this package
489
- never reads `import.meta.env`, so the app passes `enabled`.
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
+ ```
490
507
 
491
508
  ```ts
492
- // file: app/pwa-contract.ts, shared by the worker and the page:
493
- // every cache is named `my-app-<kind>-<version>`.
509
+ // file: app/pwa-contract.ts
510
+ // Shared by the worker and the page: every cache is named `my-app-<kind>-<version>`.
494
511
  import { defineWorkerContract } from '@alxia/react-router/pwa';
495
512
 
496
513
  export const workerContract = defineWorkerContract({ cachePrefix: 'my-app' });
497
514
  ```
498
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
+
499
524
  ```tsx
500
525
  // file: app/root.tsx
526
+ import { manifestHref } from '@alxia/react-router/pwa';
501
527
  import { useServiceWorker } from '@alxia/react-router/pwa/react';
502
528
  import { Outlet } from 'react-router';
503
529
 
530
+ // The browser reads the manifest to offer the install.
531
+ export function links() {
532
+ return [{ rel: 'manifest', href: manifestHref('en') }];
533
+ }
534
+
504
535
  // A production build only, never under `react-router dev`.
505
536
  export default function App() {
506
537
  useServiceWorker({ enabled: import.meta.env.PROD });
@@ -511,18 +542,84 @@ export default function App() {
511
542
  ```ts
512
543
  // file: app/server.ts
513
544
  import { createServer } from '@alxia/react-router';
514
- import { SERVICE_WORKER_URL } from '@alxia/react-router/pwa';
545
+ import {
546
+ MANIFEST_HEADERS,
547
+ MANIFEST_PATH,
548
+ SERVICE_WORKER_URL,
549
+ webAppManifest,
550
+ } from '@alxia/react-router/pwa';
515
551
  import { noCache } from '@alxia/react-router/pwa/server';
516
552
 
517
- // The worker's script is revalidated; beforeAll runs before the client's files.
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.
518
555
  export default createServer({
519
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
+ }),
520
568
  });
521
569
  ```
522
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
+
523
620
  The [PWA guide](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/pwa.md)
524
- has the worker's fetch handler, the manifest route, the update banner and the
525
- rest.
621
+ has the worker's options, the offline page, the manifest route, the update
622
+ banner and the rest.
526
623
 
527
624
  ## Traps
528
625
 
@@ -574,8 +671,8 @@ From `@alxia/react-router/vite`:
574
671
 
575
672
  | export | |
576
673
  | --- | --- |
577
- | `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 |
578
- | `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) |
579
676
 
580
677
  From `@alxia/react-router/pwa`:
581
678
 
@@ -623,6 +720,22 @@ From `@alxia/react-router/pwa/server` (`@alxia/core` is its only package import)
623
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 |
624
721
  | `WORKER_POLICY` | `default-src 'none'; connect-src 'self'`: the worker runs no other script and fetches this site only |
625
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 }` |
738
+
626
739
  The `alxia-react-router` bin, run with `bunx`:
627
740
 
628
741
  | command | |
@@ -637,7 +750,7 @@ The `alxia-react-router` bin, run with `bunx`:
637
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.
638
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.
639
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.
640
- - [Installable and offline](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/pwa.md): what `/pwa` holds, the worker's contract, what the worker caches, the precache list from a build, registering the worker and offering an update, the web app manifest, the hooks in React, and clearing at sign-out.
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.
641
754
  - [Troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md): each message, and the traps that print none.
642
755
  - [Roadmap](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/roadmap.md): what is coming, and what is not planned.
643
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
+ }
@@ -1,38 +1,7 @@
1
- // src/pwa/model/worker-contract.ts
2
- var SERVICE_WORKER_URL2 = "/service-worker.js";
3
- var OFFLINE_URL2 = "/offline.html";
4
- var SKIP_WAITING2 = "SKIP_WAITING";
5
- function defineWorkerContract2(options) {
6
- const { cachePrefix } = options;
7
- if (typeof cachePrefix !== "string" || cachePrefix === "") {
8
- throw new TypeError(`defineWorkerContract(): cachePrefix must be a non-empty name, such as 'my-app'; got '${String(cachePrefix)}'.`);
9
- }
10
- if (cachePrefix.endsWith("-")) {
11
- throw new TypeError(`defineWorkerContract(): cachePrefix must not end with '-', which the contract adds; got '${cachePrefix}'. Use '${cachePrefix.replace(/-+$/, "")}'.`);
12
- }
13
- const cacheVersion = options.cacheVersion ?? "v1";
14
- if (typeof cacheVersion !== "string" || cacheVersion === "" || cacheVersion.includes("-")) {
15
- throw new TypeError(`defineWorkerContract(): cacheVersion must be a non-empty name without '-', such as 'v2'; got '${String(cacheVersion)}'.`);
16
- }
17
- const cacheNames = Object.freeze({
18
- precache: `${cachePrefix}-precache-${cacheVersion}`,
19
- assets: `${cachePrefix}-assets-${cacheVersion}`,
20
- static: `${cachePrefix}-static-${cacheVersion}`
21
- });
22
- const current = new Set(Object.values(cacheNames));
23
- const shape = new RegExp(`^${cachePrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}-[a-z0-9]+-[^-]+$`);
24
- const owned = (name) => shape.test(name);
25
- const contract = {
26
- cachePrefix,
27
- cacheVersion,
28
- serviceWorkerUrl: options.serviceWorkerUrl ?? SERVICE_WORKER_URL2,
29
- offlineUrl: options.offlineUrl ?? OFFLINE_URL2,
30
- cacheNames,
31
- obsoleteCaches: (names) => names.filter((name) => owned(name) && !current.has(name)),
32
- clearableCaches: (names) => names.filter((name) => owned(name) && name !== cacheNames.precache)
33
- };
34
- return Object.freeze(contract);
35
- }
1
+ import {
2
+ SERVICE_WORKER_URL2,
3
+ SKIP_WAITING2
4
+ } from "./index-rfr02vg4.js";
36
5
 
37
6
  // src/pwa/model/registration.ts
38
7
  function watchForUpdates2(registration, container, onWaiting) {
@@ -133,7 +102,7 @@ function createUpdateNotice2(storage, key = UPDATE_LATER_KEY2) {
133
102
  }
134
103
  var sessionStore2 = () => typeof sessionStorage === "undefined" ? undefined : sessionStorage;
135
104
 
136
- export { SERVICE_WORKER_URL2, OFFLINE_URL2, SKIP_WAITING2, defineWorkerContract2, watchForUpdates2, createUpdater2, afterLoad2, registerServiceWorker2, UPDATE_LATER_KEY2, createUpdateNotice2, sessionStore2 };
105
+ export { watchForUpdates2, createUpdater2, afterLoad2, registerServiceWorker2, UPDATE_LATER_KEY2, createUpdateNotice2, sessionStore2 };
137
106
 
138
- //# debugId=E59639D8397E531464756E2164756E21
139
- //# sourceMappingURL=index-zqa30gr7.js.map
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": ";;;;;;AAaO,SAAS,gBAAe,CAC9B,cACA,WACA,WACO;AAAA,EACP,MAAM,QAAQ,CAAC,WAAiC;AAAA,IAC/C,IAAI,UAAU,UAAU;AAAA,MAAY,UAAU,MAAM;AAAA;AAAA,EAErD,MAAM,aAAa,OAAO;AAAA,EAC1B,aAAa,iBAAiB,eAAe,MAAM;AAAA,IAClD,MAAM,aAAa,aAAa;AAAA,IAChC,YAAY,iBAAiB,eAAe,MAAM;AAAA,MACjD,IAAI,WAAW,UAAU;AAAA,QAAa,MAAM,UAAU;AAAA,KACtD;AAAA,GACD;AAAA;AAoBK,SAAS,cAAa,CAC5B,WAIA,SAAqB,MAAM,OAAO,SAAS,OAAO,GACpC;AAAA,EACd,IAAI,YAAY;AAAA,EAChB,IAAI,WAAW;AAAA,EACf,MAAM,aAAa,MAAM;AAAA,IACxB,IAAI;AAAA,MAAU;AAAA,IACd,WAAW;AAAA,IACX,OAAO;AAAA;AAAA,EAER,UAAU,iBAAiB,oBAAoB,MAAM;AAAA,IACpD,IAAI;AAAA,MAAW,WAAW;AAAA,GAC1B;AAAA,EACD,OAAO,CAAC,WAAW;AAAA,IAClB,MAAM,SACL,OAAO,UAAU,gBACjB,OAAO,UAAU,eACjB,UAAU,eAAe;AAAA,IAC1B,IAAI;AAAA,MAAQ,OAAO,WAAW;AAAA,IAC9B,YAAY;AAAA,IACZ,OAAO,YAAY,EAAE,MAAM,cAAa,CAAC;AAAA;AAAA;AAKpC,SAAS,UAAS,CAAC,KAAuB;AAAA,EAChD,IAAI,SAAS,eAAe;AAAA,IAAY,IAAI;AAAA,EACvC;AAAA,WAAO,iBAAiB,QAAQ,KAAK,EAAE,MAAM,KAAK,CAAC;AAAA;AAUzD,IAAI;AAQJ,eAAsB,sBAAqB,CAC1C,WACA,WACA,UAA2B,CAAC,GACS;AAAA,EACrC,YAAY,eAAc,SAAS;AAAA,EACnC,MAAM,QAAQ;AAAA,EACd,MAAM,eAAe,MAAM,UAAU,SACpC,QAAQ,OAAO,qBACf,EAAE,OAAO,QAAQ,SAAS,IAAI,CAC/B;AAAA,EACA,iBAAgB,cAAc,WAAW,CAAC,WACzC,UAAU,QAAQ,KAAK,CACxB;AAAA,EACA,SAAS,iBAAiB,oBAAoB,MAAM;AAAA,IACnD,IAAI,SAAS,oBAAoB;AAAA,MAChC,aAAa,OAAO,EAAE,MAAM,MAAM,EAEjC;AAAA,GACF;AAAA,EACD,OAAO;AAAA;;;AC/GD,IAAM,oBAAmB;AAiCzB,SAAS,mBAAkB,CACjC,SACA,MAAc,mBACM;AAAA,EACpB,IAAI;AAAA,EACJ,IAAI,gBAAgB;AAAA,EACpB,IAAI;AAAA,EACJ,MAAM,YAAY,IAAI;AAAA,EAEtB,MAAM,QAAQ,MAAM;AAAA,IACnB,gBAAgB;AAAA,IAChB,IAAI;AAAA,MACH,QAAQ,GAAG,QAAQ,KAAK,GAAG;AAAA,MAC1B,MAAM;AAAA,IAGR,QAAQ;AAAA;AAAA,EAET,MAAM,aAAa,MAAM;AAAA,IACxB,IAAI;AAAA,MAAe,OAAO;AAAA,IAC1B,IAAI;AAAA,MACH,OAAO,QAAQ,GAAG,QAAQ,GAAG,MAAM;AAAA,MAClC,MAAM;AAAA,MACP,OAAO;AAAA;AAAA;AAAA,EAGT,SAAS,OAAO,GAAG;AAAA,IAClB,SAAS,UAAU,CAAC,WAAW,IAAI,EAAE,QAAQ,MAAM,IAAI;AAAA,IACvD,WAAW,YAAY;AAAA,MAAW,SAAS;AAAA;AAAA,EAG5C,OAAO;AAAA,IACN,KAAK,CAAC,OAAO;AAAA,MACZ,SAAS;AAAA,MACT,QAAQ;AAAA;AAAA,IAET,SAAS,MAAM;AAAA,IACf,SAAS,CAAC,UAAU;AAAA,MACnB,UAAU,IAAI,QAAQ;AAAA,MACtB,OAAO,MAAM;AAAA,QACZ,UAAU,OAAO,QAAQ;AAAA;AAAA;AAAA,EAG5B;AAAA;AAIM,IAAM,gBAAe,MAC3B,OAAO,mBAAmB,cAAc,YAAY;",
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
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/pwa/build/build-worker.ts", "../src/pwa/build/precache.ts"],
4
+ "sourcesContent": [
5
+ "import { existsSync } from 'node:fs';\nimport { readdir, readFile, stat } from 'node:fs/promises';\nimport { basename, dirname, join, resolve, sep } from 'node:path';\nimport type { PrecacheEntry } from '../model/precache-list';\nimport {\n\tOFFLINE_URL,\n\tSERVICE_WORKER_URL,\n\ttype WorkerContract,\n} from '../model/worker-contract';\nimport { PRECACHE_DEFINE } from '../worker/injected';\nimport { browserManifestFile, revisionOf, workerPrecache } from './precache';\n\nexport interface BuildWorkerOptions {\n\t/**\n\t * The worker's source, relative to `root`: `app/service-worker.ts`. Named\n\t * `file`, not `entry`: `reveal` reads `alxia({ entry })`.\n\t */\n\tfile: string;\n\t/** `process.cwd()` by default. */\n\troot?: string;\n\t/** The client build, relative to `root`: `build/client` by default. */\n\tclient?: string;\n\t/** Where the worker is written and which offline page is precached. */\n\tcontract?: Pick<WorkerContract, 'serviceWorkerUrl' | 'offlineUrl'>;\n\t/** Precache the offline page, which must then exist. `true` by default. */\n\toffline?: boolean;\n\t/** More files of `assets/` to precache, such as fonts: the app's regex. */\n\tprecacheAssets?: RegExp;\n\t/** `true` by default. */\n\tminify?: boolean;\n}\n\nexport interface WorkerBuild {\n\t/** The absolute path of the worker written. */\n\tfile: string;\n\tprecache: PrecacheEntry[];\n\t/** The precached files' size, before compression. */\n\tprecacheBytes: number;\n\t/** The worker's own size. */\n\tbytes: number;\n}\n\n/** Not `requireBun` of `vite/`: `/pwa/build` imports Node's built-ins only. */\nexport function assertBun(\n\tonBun = (globalThis as { Bun?: unknown }).Bun !== undefined,\n): void {\n\tif (onBun) return;\n\tthrow new Error(\n\t\t'buildWorker() runs on Bun: it is running on Node. Run it with bun, or run react-router build with bun --bun.',\n\t);\n}\n\n/**\n * The file a root-relative URL names inside `client`. It fails closed: a URL\n * that is absolute, protocol-relative, not rooted at `/`, or that resolves to\n * `client` itself or outside it, is refused.\n */\nfunction urlInClient(client: string, url: string): string {\n\tconst path = join(client, url);\n\tif (\n\t\t!url.startsWith('/') ||\n\t\t/^[a-z][a-z0-9+.-]*:|^\\/\\//i.test(url) ||\n\t\t!path.startsWith(client + sep) ||\n\t\tpath.endsWith(sep)\n\t) {\n\t\tthrow new Error(`buildWorker(): ${url} is outside ${client}.`);\n\t}\n\treturn path;\n}\n\n/**\n * Bundles the worker (Workbox included) into the client build, after\n * `react-router build`, with the precache list written in: the offline page\n * by its content, every route's code and `precacheAssets`. A new build\n * changes the list, so the worker's bytes, so the browser installs it.\n * Runs on Bun; the only imports of this module are Node's built-ins.\n */\nexport async function buildWorker(\n\toptions: BuildWorkerOptions,\n): Promise<WorkerBuild> {\n\tassertBun();\n\tconst root = resolve(options.root ?? process.cwd());\n\tconst client = resolve(root, options.client ?? 'build/client');\n\tconst source = resolve(root, options.file);\n\tconst workerUrl = options.contract?.serviceWorkerUrl ?? SERVICE_WORKER_URL;\n\tconst offlineUrl = options.contract?.offlineUrl ?? OFFLINE_URL;\n\tif (!existsSync(source)) {\n\t\tthrow new Error(`buildWorker(): ${options.file} does not exist.`);\n\t}\n\tconst assets = join(client, 'assets');\n\tconst files = await readdir(assets).catch(() => []);\n\tconst manifest = browserManifestFile(files);\n\tlet offline: PrecacheEntry | undefined;\n\tif (options.offline !== false) {\n\t\tconst page = urlInClient(client, offlineUrl);\n\t\tif (!existsSync(page)) {\n\t\t\tthrow new Error(\n\t\t\t\t`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()).`,\n\t\t\t);\n\t\t}\n\t\toffline = { url: offlineUrl, revision: revisionOf(await readFile(page)) };\n\t}\n\tconst precache = workerPrecache({\n\t\tmanifestSource: await readFile(join(assets, manifest), 'utf8'),\n\t\tassetFiles: files,\n\t\toffline,\n\t\tinclude: options.precacheAssets,\n\t});\n\tconst out = urlInClient(client, workerUrl);\n\tconst result = await Bun.build({\n\t\tentrypoints: [source],\n\t\toutdir: dirname(out),\n\t\tnaming: basename(out),\n\t\ttarget: 'browser',\n\t\tformat: 'iife',\n\t\tminify: options.minify ?? true,\n\t\tdefine: {\n\t\t\t// Workbox's production build: no logging, no dev assertions.\n\t\t\t'process.env.NODE_ENV': '\"production\"',\n\t\t\t[PRECACHE_DEFINE]: JSON.stringify(precache),\n\t\t},\n\t}).catch((error: unknown) => {\n\t\tconst detail =\n\t\t\terror instanceof AggregateError\n\t\t\t\t? error.errors.map(String).join('\\n')\n\t\t\t\t: String(error);\n\t\tthrow new Error(`buildWorker(): Bun.build failed:\\n${detail}`);\n\t});\n\tif (!result.success) {\n\t\tthrow new Error(\n\t\t\t`buildWorker(): Bun.build failed:\\n${result.logs.map(String).join('\\n')}`,\n\t\t);\n\t}\n\tlet precacheBytes = 0;\n\tfor (const { url } of precache) {\n\t\tprecacheBytes += (await stat(join(client, url))).size;\n\t}\n\treturn {\n\t\tfile: out,\n\t\tprecache,\n\t\tprecacheBytes,\n\t\tbytes: (await stat(out)).size,\n\t};\n}\n",
6
+ "import { createHash } from 'node:crypto';\nimport {\n\tassetPrecache,\n\ttype PrecacheEntry,\n\tparseBrowserManifest,\n\tprecacheList,\n} from '../model/precache-list';\n\n/** The one React Router browser manifest among the names in build/client/assets. */\nexport function browserManifestFile(files: readonly string[]): string {\n\tconst found = files.filter((file) => /^manifest-[\\w-]+\\.js$/.test(file));\n\tconst [only] = found;\n\tif (found.length !== 1 || only === undefined) {\n\t\tthrow new Error(\n\t\t\t`buildWorker(): expected one React Router browser manifest, build/client/assets/manifest-*.js, found ${found.length}. Run react-router build first.`,\n\t\t);\n\t}\n\treturn only;\n}\n\n/** A file's revision: the first 16 hex characters of its sha256. */\nexport function revisionOf(bytes: Uint8Array): string {\n\treturn createHash('sha256').update(bytes).digest('hex').slice(0, 16);\n}\n\n/**\n * The worker's precache list: the offline page (revisioned by its content),\n * every route's code, then the asset files `include` matches. Each URL once,\n * the first kept.\n */\nexport function workerPrecache(input: {\n\tmanifestSource: string;\n\tassetFiles: readonly string[];\n\toffline?: PrecacheEntry | undefined;\n\tinclude?: RegExp | undefined;\n}): PrecacheEntry[] {\n\tconst all = [\n\t\t...(input.offline ? [input.offline] : []),\n\t\t...precacheList(parseBrowserManifest(input.manifestSource)),\n\t\t...(input.include ? assetPrecache(input.assetFiles, input.include) : []),\n\t];\n\tconst seen = new Set<string>();\n\treturn all.filter(({ url }) => !seen.has(url) && seen.add(url));\n}\n"
7
+ ],
8
+ "mappings": ";;;;;;;;;;;;;;AAAA;AACA;AACA;;;ACFA;AASO,SAAS,mBAAmB,CAAC,OAAkC;AAAA,EACrE,MAAM,QAAQ,MAAM,OAAO,CAAC,SAAS,wBAAwB,KAAK,IAAI,CAAC;AAAA,EACvE,OAAO,QAAQ;AAAA,EACf,IAAI,MAAM,WAAW,KAAK,SAAS,WAAW;AAAA,IAC7C,MAAM,IAAI,MACT,uGAAuG,MAAM,uCAC9G;AAAA,EACD;AAAA,EACA,OAAO;AAAA;AAID,SAAS,UAAU,CAAC,OAA2B;AAAA,EACrD,OAAO,WAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK,EAAE,MAAM,GAAG,EAAE;AAAA;AAQ7D,SAAS,cAAc,CAAC,OAKX;AAAA,EACnB,MAAM,MAAM;AAAA,IACX,GAAI,MAAM,UAAU,CAAC,MAAM,OAAO,IAAI,CAAC;AAAA,IACvC,GAAG,cAAa,sBAAqB,MAAM,cAAc,CAAC;AAAA,IAC1D,GAAI,MAAM,UAAU,eAAc,MAAM,YAAY,MAAM,OAAO,IAAI,CAAC;AAAA,EACvE;AAAA,EACA,MAAM,OAAO,IAAI;AAAA,EACjB,OAAO,IAAI,OAAO,GAAG,UAAU,CAAC,KAAK,IAAI,GAAG,KAAK,KAAK,IAAI,GAAG,CAAC;AAAA;;;ADCxD,SAAS,SAAS,CACxB,QAAS,WAAiC,QAAQ,WAC3C;AAAA,EACP,IAAI;AAAA,IAAO;AAAA,EACX,MAAM,IAAI,MACT,8GACD;AAAA;AAQD,SAAS,WAAW,CAAC,QAAgB,KAAqB;AAAA,EACzD,MAAM,OAAO,KAAK,QAAQ,GAAG;AAAA,EAC7B,IACC,CAAC,IAAI,WAAW,GAAG,KACnB,6BAA6B,KAAK,GAAG,KACrC,CAAC,KAAK,WAAW,SAAS,GAAG,KAC7B,KAAK,SAAS,GAAG,GAChB;AAAA,IACD,MAAM,IAAI,MAAM,kBAAkB,kBAAkB,SAAS;AAAA,EAC9D;AAAA,EACA,OAAO;AAAA;AAUR,eAAsB,YAAW,CAChC,SACuB;AAAA,EACvB,UAAU;AAAA,EACV,MAAM,OAAO,QAAQ,QAAQ,QAAQ,QAAQ,IAAI,CAAC;AAAA,EAClD,MAAM,SAAS,QAAQ,MAAM,QAAQ,UAAU,cAAc;AAAA,EAC7D,MAAM,SAAS,QAAQ,MAAM,QAAQ,IAAI;AAAA,EACzC,MAAM,YAAY,QAAQ,UAAU,oBAAoB;AAAA,EACxD,MAAM,aAAa,QAAQ,UAAU,cAAc;AAAA,EACnD,IAAI,CAAC,WAAW,MAAM,GAAG;AAAA,IACxB,MAAM,IAAI,MAAM,kBAAkB,QAAQ,sBAAsB;AAAA,EACjE;AAAA,EACA,MAAM,SAAS,KAAK,QAAQ,QAAQ;AAAA,EACpC,MAAM,QAAQ,MAAM,QAAQ,MAAM,EAAE,MAAM,MAAM,CAAC,CAAC;AAAA,EAClD,MAAM,WAAW,oBAAoB,KAAK;AAAA,EAC1C,IAAI;AAAA,EACJ,IAAI,QAAQ,YAAY,OAAO;AAAA,IAC9B,MAAM,OAAO,YAAY,QAAQ,UAAU;AAAA,IAC3C,IAAI,CAAC,WAAW,IAAI,GAAG;AAAA,MACtB,MAAM,IAAI,MACT,kBAAkB,QAAQ,UAAU,iBAAiB,wCAAwC,mJAC9F;AAAA,IACD;AAAA,IACA,UAAU,EAAE,KAAK,YAAY,UAAU,WAAW,MAAM,SAAS,IAAI,CAAC,EAAE;AAAA,EACzE;AAAA,EACA,MAAM,WAAW,eAAe;AAAA,IAC/B,gBAAgB,MAAM,SAAS,KAAK,QAAQ,QAAQ,GAAG,MAAM;AAAA,IAC7D,YAAY;AAAA,IACZ;AAAA,IACA,SAAS,QAAQ;AAAA,EAClB,CAAC;AAAA,EACD,MAAM,MAAM,YAAY,QAAQ,SAAS;AAAA,EACzC,MAAM,SAAS,MAAM,IAAI,MAAM;AAAA,IAC9B,aAAa,CAAC,MAAM;AAAA,IACpB,QAAQ,QAAQ,GAAG;AAAA,IACnB,QAAQ,SAAS,GAAG;AAAA,IACpB,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ,QAAQ,UAAU;AAAA,IAC1B,QAAQ;AAAA,MAEP,wBAAwB;AAAA,OACvB,kBAAkB,KAAK,UAAU,QAAQ;AAAA,IAC3C;AAAA,EACD,CAAC,EAAE,MAAM,CAAC,UAAmB;AAAA,IAC5B,MAAM,SACL,iBAAiB,iBACd,MAAM,OAAO,IAAI,MAAM,EAAE,KAAK;AAAA,CAAI,IAClC,OAAO,KAAK;AAAA,IAChB,MAAM,IAAI,MAAM;AAAA,EAAqC,QAAQ;AAAA,GAC7D;AAAA,EACD,IAAI,CAAC,OAAO,SAAS;AAAA,IACpB,MAAM,IAAI,MACT;AAAA,EAAqC,OAAO,KAAK,IAAI,MAAM,EAAE,KAAK;AAAA,CAAI,GACvE;AAAA,EACD;AAAA,EACA,IAAI,gBAAgB;AAAA,EACpB,aAAa,SAAS,UAAU;AAAA,IAC/B,kBAAkB,MAAM,KAAK,KAAK,QAAQ,GAAG,CAAC,GAAG;AAAA,EAClD;AAAA,EACA,OAAO;AAAA,IACN,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA,QAAQ,MAAM,KAAK,GAAG,GAAG;AAAA,EAC1B;AAAA;",
9
+ "debugId": "763F0D0F0869B4B964756E2164756E21",
10
+ "names": []
11
+ }
@@ -0,0 +1,40 @@
1
+ // src/pwa/model/worker-contract.ts
2
+ var SERVICE_WORKER_URL2 = "/service-worker.js";
3
+ var OFFLINE_URL2 = "/offline.html";
4
+ var SKIP_WAITING2 = "SKIP_WAITING";
5
+ function defineWorkerContract2(options) {
6
+ const { cachePrefix } = options;
7
+ if (typeof cachePrefix !== "string" || cachePrefix === "") {
8
+ throw new TypeError(`defineWorkerContract(): cachePrefix must be a non-empty name, such as 'my-app'; got '${String(cachePrefix)}'.`);
9
+ }
10
+ if (cachePrefix.endsWith("-")) {
11
+ throw new TypeError(`defineWorkerContract(): cachePrefix must not end with '-', which the contract adds; got '${cachePrefix}'. Use '${cachePrefix.replace(/-+$/, "")}'.`);
12
+ }
13
+ const cacheVersion = options.cacheVersion ?? "v1";
14
+ if (typeof cacheVersion !== "string" || cacheVersion === "" || cacheVersion.includes("-")) {
15
+ throw new TypeError(`defineWorkerContract(): cacheVersion must be a non-empty name without '-', such as 'v2'; got '${String(cacheVersion)}'.`);
16
+ }
17
+ const cacheNames = Object.freeze({
18
+ precache: `${cachePrefix}-precache-${cacheVersion}`,
19
+ assets: `${cachePrefix}-assets-${cacheVersion}`,
20
+ static: `${cachePrefix}-static-${cacheVersion}`
21
+ });
22
+ const current = new Set(Object.values(cacheNames));
23
+ const shape = new RegExp(`^${cachePrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}-[a-z0-9]+-[^-]+$`);
24
+ const owned = (name) => shape.test(name);
25
+ const contract = {
26
+ cachePrefix,
27
+ cacheVersion,
28
+ serviceWorkerUrl: options.serviceWorkerUrl ?? SERVICE_WORKER_URL2,
29
+ offlineUrl: options.offlineUrl ?? OFFLINE_URL2,
30
+ cacheNames,
31
+ obsoleteCaches: (names) => names.filter((name) => owned(name) && !current.has(name)),
32
+ clearableCaches: (names) => names.filter((name) => owned(name) && name !== cacheNames.precache)
33
+ };
34
+ return Object.freeze(contract);
35
+ }
36
+
37
+ export { SERVICE_WORKER_URL2, OFFLINE_URL2, SKIP_WAITING2, defineWorkerContract2 };
38
+
39
+ //# debugId=5CED7E20833AD98364756E2164756E21
40
+ //# sourceMappingURL=index-rfr02vg4.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/pwa/model/worker-contract.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * What the service worker and the page agree on: the worker's address, the\n * offline page, the message that applies an update, and the names of the\n * caches the worker owns. Dependency-free: the worker bundles it, with no\n * DOM and no React.\n */\n\n/** Where the build writes the worker; registered with scope `/`. */\nexport const SERVICE_WORKER_URL = '/service-worker.js';\n\n/** The page served when a navigation fails for want of a network. */\nexport const OFFLINE_URL = '/offline.html';\n\n/** The message a page posts to a waiting worker to make it take over. */\nexport const SKIP_WAITING = 'SKIP_WAITING';\n\nexport interface WorkerContractOptions {\n\t/**\n\t * Every Cache Storage cache the worker owns is named\n\t * `<cachePrefix>-<kind>-<version>` (kind: letters and digits; version:\n\t * no `-`); a cache of another shape is never swept.\n\t * Required: two apps on one origin must not share it.\n\t */\n\tcachePrefix: string;\n\t/**\n\t * Bumped when a cache's content changes shape: the next worker deletes\n\t * every cache of the prefix that has another version when it activates.\n\t * `'v1'` by default; not empty, and without a `-`.\n\t */\n\tcacheVersion?: string;\n\t/** `SERVICE_WORKER_URL` by default. */\n\tserviceWorkerUrl?: string;\n\t/** `OFFLINE_URL` by default. */\n\tofflineUrl?: string;\n}\n\nexport type CacheNames = {\n\t/**\n\t * The offline page and the hashed scripts and stylesheets of every route.\n\t * `<prefix>-precache-<version>` is the name Workbox builds from\n\t * `setCacheNameDetails({ prefix, precache: 'precache', suffix: version })`.\n\t */\n\treadonly precache: string;\n\t/** `/assets/*` bundles: hashed names, so cache-first. */\n\treadonly assets: string;\n\t/** Icons, sprites, the logo: stale-while-revalidate. */\n\treadonly static: string;\n};\n\nexport interface WorkerContract {\n\treadonly cachePrefix: string;\n\treadonly cacheVersion: string;\n\treadonly serviceWorkerUrl: string;\n\treadonly offlineUrl: string;\n\treadonly cacheNames: CacheNames;\n\t/** The caches a worker deletes on activate: the prefix's, of another version. */\n\tobsoleteCaches(names: readonly string[]): string[];\n\t/**\n\t * The caches `clearAppCaches()` deletes: all of the prefix's but the\n\t * current precache, which holds the offline page and the app's code, never\n\t * a user's data.\n\t */\n\tclearableCaches(names: readonly string[]): string[];\n}\n\n/** Builds the contract of one app from its cache prefix. */\nexport function defineWorkerContract(\n\toptions: WorkerContractOptions,\n): WorkerContract {\n\tconst { cachePrefix } = options;\n\tif (typeof cachePrefix !== 'string' || cachePrefix === '') {\n\t\tthrow new TypeError(\n\t\t\t`defineWorkerContract(): cachePrefix must be a non-empty name, such as 'my-app'; got '${String(cachePrefix)}'.`,\n\t\t);\n\t}\n\tif (cachePrefix.endsWith('-')) {\n\t\tthrow new TypeError(\n\t\t\t`defineWorkerContract(): cachePrefix must not end with '-', which the contract adds; got '${cachePrefix}'. Use '${cachePrefix.replace(/-+$/, '')}'.`,\n\t\t);\n\t}\n\tconst cacheVersion = options.cacheVersion ?? 'v1';\n\tif (\n\t\ttypeof cacheVersion !== 'string' ||\n\t\tcacheVersion === '' ||\n\t\tcacheVersion.includes('-')\n\t) {\n\t\tthrow new TypeError(\n\t\t\t`defineWorkerContract(): cacheVersion must be a non-empty name without '-', such as 'v2'; got '${String(cacheVersion)}'.`,\n\t\t);\n\t}\n\tconst cacheNames: CacheNames = Object.freeze({\n\t\tprecache: `${cachePrefix}-precache-${cacheVersion}`,\n\t\tassets: `${cachePrefix}-assets-${cacheVersion}`,\n\t\tstatic: `${cachePrefix}-static-${cacheVersion}`,\n\t});\n\tconst current = new Set<string>(Object.values(cacheNames));\n\t// Exactly `<prefix>-<kind>-<version>`: the prefix 'app' must not claim\n\t// 'app-admin-assets-v1' (one segment too many), but still owns a retired\n\t// kind such as 'app-pages-v0'.\n\tconst shape = new RegExp(\n\t\t`^${cachePrefix.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}-[a-z0-9]+-[^-]+$`,\n\t);\n\tconst owned = (name: string) => shape.test(name);\n\tconst contract: WorkerContract = {\n\t\tcachePrefix,\n\t\tcacheVersion,\n\t\tserviceWorkerUrl: options.serviceWorkerUrl ?? SERVICE_WORKER_URL,\n\t\tofflineUrl: options.offlineUrl ?? OFFLINE_URL,\n\t\tcacheNames,\n\t\tobsoleteCaches: (names) =>\n\t\t\tnames.filter((name) => owned(name) && !current.has(name)),\n\t\tclearableCaches: (names) =>\n\t\t\tnames.filter((name) => owned(name) && name !== cacheNames.precache),\n\t};\n\treturn Object.freeze(contract);\n}\n"
6
+ ],
7
+ "mappings": ";AAQO,IAAM,sBAAqB;AAG3B,IAAM,eAAc;AAGpB,IAAM,gBAAe;AAoDrB,SAAS,qBAAoB,CACnC,SACiB;AAAA,EACjB,QAAQ,gBAAgB;AAAA,EACxB,IAAI,OAAO,gBAAgB,YAAY,gBAAgB,IAAI;AAAA,IAC1D,MAAM,IAAI,UACT,wFAAwF,OAAO,WAAW,KAC3G;AAAA,EACD;AAAA,EACA,IAAI,YAAY,SAAS,GAAG,GAAG;AAAA,IAC9B,MAAM,IAAI,UACT,4FAA4F,sBAAsB,YAAY,QAAQ,OAAO,EAAE,KAChJ;AAAA,EACD;AAAA,EACA,MAAM,eAAe,QAAQ,gBAAgB;AAAA,EAC7C,IACC,OAAO,iBAAiB,YACxB,iBAAiB,MACjB,aAAa,SAAS,GAAG,GACxB;AAAA,IACD,MAAM,IAAI,UACT,iGAAiG,OAAO,YAAY,KACrH;AAAA,EACD;AAAA,EACA,MAAM,aAAyB,OAAO,OAAO;AAAA,IAC5C,UAAU,GAAG,wBAAwB;AAAA,IACrC,QAAQ,GAAG,sBAAsB;AAAA,IACjC,QAAQ,GAAG,sBAAsB;AAAA,EAClC,CAAC;AAAA,EACD,MAAM,UAAU,IAAI,IAAY,OAAO,OAAO,UAAU,CAAC;AAAA,EAIzD,MAAM,QAAQ,IAAI,OACjB,IAAI,YAAY,QAAQ,uBAAuB,MAAM,oBACtD;AAAA,EACA,MAAM,QAAQ,CAAC,SAAiB,MAAM,KAAK,IAAI;AAAA,EAC/C,MAAM,WAA2B;AAAA,IAChC;AAAA,IACA;AAAA,IACA,kBAAkB,QAAQ,oBAAoB;AAAA,IAC9C,YAAY,QAAQ,cAAc;AAAA,IAClC;AAAA,IACA,gBAAgB,CAAC,UAChB,MAAM,OAAO,CAAC,SAAS,MAAM,IAAI,KAAK,CAAC,QAAQ,IAAI,IAAI,CAAC;AAAA,IACzD,iBAAiB,CAAC,UACjB,MAAM,OAAO,CAAC,SAAS,MAAM,IAAI,KAAK,SAAS,WAAW,QAAQ;AAAA,EACpE;AAAA,EACA,OAAO,OAAO,OAAO,QAAQ;AAAA;",
8
+ "debugId": "5CED7E20833AD98364756E2164756E21",
9
+ "names": []
10
+ }