@alxia/react-router 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +301 -13
- package/dist/chunks/index-59vhfj6n.js +10 -0
- package/dist/chunks/index-59vhfj6n.js.map +10 -0
- package/dist/chunks/index-5gqcpyyg.js +34 -0
- package/dist/chunks/index-5gqcpyyg.js.map +10 -0
- package/dist/chunks/{index-zqa30gr7.js → index-9xv2a8et.js} +7 -38
- package/dist/chunks/index-9xv2a8et.js.map +11 -0
- package/dist/chunks/index-mppz0806.js +119 -0
- package/dist/chunks/index-mppz0806.js.map +11 -0
- package/dist/chunks/index-rfr02vg4.js +40 -0
- package/dist/chunks/index-rfr02vg4.js.map +10 -0
- package/dist/chunks/index-zdbqzcjp.js +97 -0
- package/dist/chunks/index-zdbqzcjp.js.map +12 -0
- package/dist/errors/api-error.d.ts +86 -0
- package/dist/errors/api-error.d.ts.map +1 -0
- package/dist/errors/api-errors.d.ts +96 -0
- package/dist/errors/api-errors.d.ts.map +1 -0
- package/dist/errors/call-failure.d.ts +45 -0
- package/dist/errors/call-failure.d.ts.map +1 -0
- package/dist/errors/catalogue-check.d.ts +24 -0
- package/dist/errors/catalogue-check.d.ts.map +1 -0
- package/dist/errors/catalogue-read.d.ts +33 -0
- package/dist/errors/catalogue-read.d.ts.map +1 -0
- package/dist/errors/catalogue.d.ts +59 -0
- package/dist/errors/catalogue.d.ts.map +1 -0
- package/dist/errors/code-resolver.d.ts +44 -0
- package/dist/errors/code-resolver.d.ts.map +1 -0
- package/dist/errors/describe-refusal.d.ts +6 -0
- package/dist/errors/describe-refusal.d.ts.map +1 -0
- package/dist/errors/describe.d.ts +3 -0
- package/dist/errors/describe.d.ts.map +1 -0
- package/dist/errors/fetcher-result.d.ts +21 -0
- package/dist/errors/fetcher-result.d.ts.map +1 -0
- package/dist/errors/form-errors.d.ts +8 -0
- package/dist/errors/form-errors.d.ts.map +1 -0
- package/dist/errors/index.d.ts +15 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +602 -0
- package/dist/errors/index.js.map +29 -0
- package/dist/errors/json-actions.d.ts +38 -0
- package/dist/errors/json-actions.d.ts.map +1 -0
- package/dist/errors/log.d.ts +8 -0
- package/dist/errors/log.d.ts.map +1 -0
- package/dist/errors/message-values.d.ts +11 -0
- package/dist/errors/message-values.d.ts.map +1 -0
- package/dist/errors/messages.d.ts +63 -0
- package/dist/errors/messages.d.ts.map +1 -0
- package/dist/errors/page-path.d.ts +9 -0
- package/dist/errors/page-path.d.ts.map +1 -0
- package/dist/errors/provider.d.ts +20 -0
- package/dist/errors/provider.d.ts.map +1 -0
- package/dist/errors/request-id.d.ts +5 -0
- package/dist/errors/request-id.d.ts.map +1 -0
- package/dist/errors/route-error.d.ts +31 -0
- package/dist/errors/route-error.d.ts.map +1 -0
- package/dist/errors/standard-schema.d.ts +34 -0
- package/dist/errors/standard-schema.d.ts.map +1 -0
- package/dist/errors/submit-form.d.ts +16 -0
- package/dist/errors/submit-form.d.ts.map +1 -0
- package/dist/errors/use-json-fetcher.d.ts +18 -0
- package/dist/errors/use-json-fetcher.d.ts.map +1 -0
- package/dist/pwa/build/build-worker.d.ts +41 -0
- package/dist/pwa/build/build-worker.d.ts.map +1 -0
- package/dist/pwa/build/index.d.ts +9 -0
- package/dist/pwa/build/index.d.ts.map +1 -0
- package/dist/pwa/build/index.js +9 -0
- package/dist/pwa/build/index.js.map +9 -0
- package/dist/pwa/build/precache.d.ts +17 -0
- package/dist/pwa/build/precache.d.ts.map +1 -0
- package/dist/pwa/index.js +34 -134
- package/dist/pwa/index.js.map +4 -8
- package/dist/pwa/react/index.js +1 -1
- package/dist/pwa/worker/index.d.ts +9 -0
- package/dist/pwa/worker/index.d.ts.map +1 -0
- package/dist/pwa/worker/index.js +144 -0
- package/dist/pwa/worker/index.js.map +12 -0
- package/dist/pwa/worker/injected.d.ts +15 -0
- package/dist/pwa/worker/injected.d.ts.map +1 -0
- package/dist/pwa/worker/plan.d.ts +65 -0
- package/dist/pwa/worker/plan.d.ts.map +1 -0
- package/dist/pwa/worker/scope.d.ts +18 -0
- package/dist/pwa/worker/scope.d.ts.map +1 -0
- package/dist/pwa/worker/start-worker.d.ts +17 -0
- package/dist/pwa/worker/start-worker.d.ts.map +1 -0
- package/dist/vite/index.d.ts +10 -0
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +49 -3
- package/dist/vite/index.js.map +6 -5
- package/dist/vite/runtime.d.ts +1 -1
- package/dist/vite/runtime.d.ts.map +1 -1
- package/dist/vite/worker.d.ts +16 -0
- package/dist/vite/worker.d.ts.map +1 -0
- package/docs/README.md +3 -2
- package/docs/guide/errors.md +838 -0
- package/docs/guide/how-it-works.md +7 -1
- package/docs/guide/pwa.md +372 -211
- package/docs/roadmap.md +38 -5
- package/docs/troubleshooting.md +689 -3
- package/package.json +56 -4
- 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
|
|
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). `/errors` imports `react-router`, a required peer that is already installed. 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
|
|
488
|
-
|
|
489
|
-
|
|
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
|
|
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,233 @@ export default function App() {
|
|
|
511
542
|
```ts
|
|
512
543
|
// file: app/server.ts
|
|
513
544
|
import { createServer } from '@alxia/react-router';
|
|
514
|
-
import {
|
|
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
|
-
//
|
|
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
|
|
525
|
-
rest.
|
|
621
|
+
has the worker's options, the offline page, the manifest route, the update
|
|
622
|
+
banner and the rest.
|
|
623
|
+
|
|
624
|
+
## An API's refusals, worded for the page
|
|
625
|
+
|
|
626
|
+
`@alxia/react-router/errors` has no UI; its one runtime import is `react-router`, never `@alxia/core`. It
|
|
627
|
+
holds the shape of a GraphQL API's refusal, declared structurally (no `@nxgt/graphql-client` at
|
|
628
|
+
runtime), the request id an API echoes, the page a request is for, the
|
|
629
|
+
catalogue of the API's messages an app keeps, the words of a form's checks,
|
|
630
|
+
the provider that fills the catalogue in the browser, the wording of a refusal, the loaders and actions that answer with it, the JSON actions (CSRF check) with their fetcher hook, and what a failed call to the API becomes (`apiCallFailure`):
|
|
631
|
+
|
|
632
|
+
```ts
|
|
633
|
+
import { isApiErrorLike, nextOf, requestIdOf } from '@alxia/react-router/errors';
|
|
634
|
+
|
|
635
|
+
export function describe(error: unknown, request: Request) {
|
|
636
|
+
return isApiErrorLike(error)
|
|
637
|
+
? { code: error.code, status: error.status, next: nextOf(request.url) }
|
|
638
|
+
: undefined;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export const idOf = (response: Response) => requestIdOf(response);
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
The catalogue is an instance made with the app's ICU formatter. A form's checks
|
|
645
|
+
read it through `apiCodeResolver`, and the provider fills it from the root's data:
|
|
646
|
+
|
|
647
|
+
```ts
|
|
648
|
+
import {
|
|
649
|
+
ApiMessagesProvider,
|
|
650
|
+
apiCodeResolver,
|
|
651
|
+
createApiMessages,
|
|
652
|
+
} from '@alxia/react-router/errors';
|
|
653
|
+
|
|
654
|
+
type Language = 'en' | 'fr';
|
|
655
|
+
|
|
656
|
+
export const apiMessages = createApiMessages<Language>({
|
|
657
|
+
format: (message) => message, // the app's ICU formatter goes here
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
export const resolver = apiCodeResolver(apiMessages, {
|
|
661
|
+
translate: (key) => key,
|
|
662
|
+
language: () => 'en',
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
// In the root: <ApiMessagesProvider messages={apiMessages} value={data.catalogue}>…
|
|
666
|
+
export const provider = ApiMessagesProvider({
|
|
667
|
+
messages: apiMessages,
|
|
668
|
+
value: { en: {}, fr: {} },
|
|
669
|
+
children: null,
|
|
670
|
+
});
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
A failed call is worded in the page's language by `createApiErrors`:
|
|
674
|
+
|
|
675
|
+
```ts
|
|
676
|
+
import { createApiErrors } from '@alxia/react-router/errors';
|
|
677
|
+
import { apiMessages } from './api-messages';
|
|
678
|
+
|
|
679
|
+
export const apiErrors = createApiErrors<'en' | 'fr'>({
|
|
680
|
+
messages: apiMessages,
|
|
681
|
+
unavailable: (language) => (language === 'en' ? 'Unavailable' : 'Indisponible'),
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
// In an action's catch: { message, status }, or undefined (rethrow it).
|
|
685
|
+
export const refusal = (error: unknown) => apiErrors.apiRefusal(error, 'en');
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
A loader throws the API's error as a route error, with the call's request id
|
|
689
|
+
for the error page; an action answers a refusal as data:
|
|
690
|
+
|
|
691
|
+
```ts
|
|
692
|
+
import { errorRequestId, routeError } from '@alxia/react-router/errors';
|
|
693
|
+
import { apiErrors } from './api-errors';
|
|
694
|
+
|
|
695
|
+
// In a loader's catch: a 401, 403, 404 or 503 ApiError becomes a route error.
|
|
696
|
+
export const thrown = (error: unknown) => routeError(error);
|
|
697
|
+
|
|
698
|
+
// In the root's ErrorBoundary: the reference the user can quote.
|
|
699
|
+
export const reference = (error: unknown) => errorRequestId(error);
|
|
700
|
+
|
|
701
|
+
// A fetcher's action: { ok: true, …value }, or { ok: false, error } with a status.
|
|
702
|
+
export const rename = (run: () => Promise<{ title: string }>) =>
|
|
703
|
+
apiErrors.fetcherResult('en', { intent: 'rename' }, run);
|
|
704
|
+
|
|
705
|
+
// A form's action: { ok: true, value }, or the form's errors with a status.
|
|
706
|
+
export const create = (run: () => Promise<{ id: string }>) =>
|
|
707
|
+
apiErrors.submitForm('en', run, (path) =>
|
|
708
|
+
path === 'todo.title' ? 'title' : undefined,
|
|
709
|
+
);
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
JSON actions refuse a post from another site, and read a Standard Schema's input:
|
|
713
|
+
|
|
714
|
+
```ts
|
|
715
|
+
import { jsonActions, useJsonFetcher } from '@alxia/react-router/errors';
|
|
716
|
+
import { z } from 'zod';
|
|
717
|
+
|
|
718
|
+
// Behind a TLS proxy named with createServer({ proxy }), nothing to set; else: origin: () => process.env.APP_ORIGIN
|
|
719
|
+
export const { readInput } = jsonActions();
|
|
720
|
+
|
|
721
|
+
const schema = z.object({ title: z.string().min(1) });
|
|
722
|
+
|
|
723
|
+
export async function action({ request }: { request: Request }) {
|
|
724
|
+
return readInput(request, schema); // 400 when the input is refused
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// In the component: const { post, busy } = useJsonFetcher<typeof action>();
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
A call that failed is turned into what a loader throws, and logged with the API's request id, by `apiCallFailure(error, { api, operation: operationName(document), requestId })`
|
|
731
|
+
(your own `execute` calls it in its `catch`):
|
|
732
|
+
|
|
733
|
+
```ts
|
|
734
|
+
import { apiCallFailure, operationName } from '@alxia/react-router/errors';
|
|
735
|
+
import type { DocumentNode } from 'graphql';
|
|
736
|
+
|
|
737
|
+
export function failed(error: unknown, document: DocumentNode, requestId?: string, signal?: AbortSignal) {
|
|
738
|
+
return apiCallFailure(error, { api: 'my-api', operation: operationName(document), requestId, signal });
|
|
739
|
+
}
|
|
740
|
+
// In execute(): catch (error) { throw failed(error, document, requestId, signal); }
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
The API's catalogue is read and kept fresh by `createErrorCatalogue`; the root's
|
|
744
|
+
middleware awaits `freshAll()`, its loader returns `clientMessages()` and
|
|
745
|
+
`server.ts` calls `warm()`:
|
|
746
|
+
|
|
747
|
+
```ts
|
|
748
|
+
import { createErrorCatalogue } from '@alxia/react-router/errors';
|
|
749
|
+
import { apiMessages } from './api-messages';
|
|
750
|
+
|
|
751
|
+
export const catalogue = createErrorCatalogue<'en' | 'fr'>({
|
|
752
|
+
messages: apiMessages,
|
|
753
|
+
languages: ['en', 'fr'],
|
|
754
|
+
read: async (language) => fetchCatalogue(language), // { version, messages }
|
|
755
|
+
versions: async () => fetchVersions(), // { en: '…', fr: '…' }
|
|
756
|
+
ttlMs: () => Number(process.env.CATALOGUE_TTL_MS ?? 60_000),
|
|
757
|
+
clientCodes: ['validation.errors.required'],
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
declare function fetchCatalogue(
|
|
761
|
+
language: string,
|
|
762
|
+
): Promise<{ version: string; messages: { code: string; message: string; params: { name: string; kind: 'TEXT' }[] }[] }>;
|
|
763
|
+
declare function fetchVersions(): Promise<{ en: string; fr: string }>;
|
|
764
|
+
|
|
765
|
+
// The root's middleware: await catalogue.freshAll();
|
|
766
|
+
// The root's loader: return { messages: catalogue.clientMessages() };
|
|
767
|
+
// server.ts, at startup: catalogue.warm();
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
The [errors guide](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/errors.md)
|
|
771
|
+
has the rest.
|
|
526
772
|
|
|
527
773
|
## Traps
|
|
528
774
|
|
|
@@ -574,8 +820,8 @@ From `@alxia/react-router/vite`:
|
|
|
574
820
|
|
|
575
821
|
| export | |
|
|
576
822
|
| --- | --- |
|
|
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 |
|
|
823
|
+
| `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` |
|
|
824
|
+
| `AlxiaOptions`, `AlxiaWorkerOptions` | its options, and the worker's (`buildWorker()`'s but `root`, `client` and `file`, which is optional) |
|
|
579
825
|
|
|
580
826
|
From `@alxia/react-router/pwa`:
|
|
581
827
|
|
|
@@ -623,6 +869,47 @@ From `@alxia/react-router/pwa/server` (`@alxia/core` is its only package import)
|
|
|
623
869
|
| `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
870
|
| `WORKER_POLICY` | `default-src 'none'; connect-src 'self'`: the worker runs no other script and fetches this site only |
|
|
625
871
|
|
|
872
|
+
From `@alxia/react-router/pwa/worker` (the six `workbox-*` packages, optional peers, are its only package imports):
|
|
873
|
+
|
|
874
|
+
| export | |
|
|
875
|
+
| --- | --- |
|
|
876
|
+
| `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 |
|
|
877
|
+
| `WorkerOptions` | `contract` (required), `caching`, `assets`, `static`, `navigationPreload`, `precache` |
|
|
878
|
+
| `ExpirationOptions` | `{ maxEntries?, maxAgeSeconds? }` |
|
|
879
|
+
|
|
880
|
+
From `@alxia/react-router/pwa/build` (Bun; Node's built-ins its only imports):
|
|
881
|
+
|
|
882
|
+
| export | |
|
|
883
|
+
| --- | --- |
|
|
884
|
+
| `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` |
|
|
885
|
+
| `BuildWorkerOptions` | `file` (required), `root`, `client`, `contract`, `offline`, `precacheAssets`, `minify` |
|
|
886
|
+
| `WorkerBuild` | `{ file, precache, precacheBytes, bytes }` |
|
|
887
|
+
|
|
888
|
+
From `@alxia/react-router/errors` (`react-router` is its only package import, and never `@alxia/core`):
|
|
889
|
+
|
|
890
|
+
| export | |
|
|
891
|
+
| --- | --- |
|
|
892
|
+
| `isApiErrorLike(error)` | whether `error` is an `@nxgt/graphql-client` `ApiError`, like the client's `isApiError`, plus a numeric `status`, which `ApiErrorLike` promises; it holds across two copies of the client |
|
|
893
|
+
| `ApiErrorLike`, `ApiErrorEntryLike`, `ApiErrorExtensionsLike`, `ApiErrorParamsLike`, `ApiFieldErrorLike` | the shape of the refusal: `errors`, `code`, `status`, `requestId?`, and each error's `extensions` (`code`, `params`, `fields`, `field`, `minutes`, `retryAfter`) |
|
|
894
|
+
| `waitMinutes(extensions)` | the minutes of a throttle's wait: `minutes` (at least one), else `retryAfter` rounded up |
|
|
895
|
+
| `REQUEST_ID_HEADER`, `requestIdOf(response)` | `x-request-id`, and the id a response carries, if the API sent one |
|
|
896
|
+
| `nextOf(url)` | the path and search of the page a request is for, without React Router's `.data`, `/_.data`, `_routes` and empty `index`; it never starts with two slashes, but an app still validates `next` before redirecting to it |
|
|
897
|
+
| `FormErrors` | what a form action answers when the API refused the input: `fieldErrors` by field and a `formError` (`undefined` when every message sits under a field), already translated; `submitForm` answers it |
|
|
898
|
+
| `routeError(error, { statuses? }?)`, `RouteErrorOptions` | what a loader throws for the API's error: an `ApiError` of 401, 403, 404 or 503, or such a `Response` carrying a request id, becomes a route error of that status with the `code`, `message` and `requestId`; any other `ApiError` with an id becomes a 500 carrying the id; anything else is returned as it is |
|
|
899
|
+
| `errorRequestId(error)`, `RouteErrorData` | the support reference an error page shows: the `requestId` a route error carries, else `undefined`; `RouteErrorData` is the data `routeError` carries (`code?`, `message?`, `requestId?`) |
|
|
900
|
+
| `createApiMessages({ format })`, `ApiMessages`, `ApiMessagesOptions` | the catalogue store of an app, an instance (`set`, `add`, `clear` (for tests), `messageOf`, `format`, `apiMessage(code, values, language)`); `format` is the app's ICU formatter, such as `@nxgt/material`'s `formatMessage` |
|
|
901
|
+
| `CatalogueMessage`, `CatalogueMessages`, `ParamKind`, `MessageFormat`, `MessageValues` | one API message with the kind of each argument (`TEXT`, `NUMBER`, `DATE`, `PLURAL`, `SELECT`), a language's messages by code, and the formatter's type |
|
|
902
|
+
| `apiCodeResolver(messages, { translate, language, codeOf?, isApiCode? })`, `ApiCodeResolver`, `ApiCodeResolverOptions`, `CodeIssueLike` | the `{ translate, codeOf }` a form's `useCodeResolver(schema, resolver)` takes: a check's API code is worded as the API words it, any other key by the app's translator; `CodeIssueLike` is the issue a custom `codeOf` receives |
|
|
903
|
+
| `ApiMessagesProvider({ messages, value, children })`, `ApiMessagesProviderProps` | `messages` is the store and `value` the root loader's data; fills the store in the browser from the root's data before the children render; a server render writes nothing. A function with no JSX, so no React import |
|
|
904
|
+
| `createApiErrors({ messages, unavailable, refused?, byCode?, isApiCode? })`, `ApiErrors`, `ApiErrorsOptions` | what a page says of a refusal, in a language: `apiErrorMessage(error, language)` (the catalogue's text of the code, else the API's message; `refused` replaces it for an error with no API code and `byCode` words given codes, such as a query's limits), `fieldErrors(error, language)` by input path, `translateCode(code, fallback, language, values?)`, `unavailableMessage(language)` and `apiRefusal(error, language)`; for an action, `fetcherResult(language, about, run, route?)` (`{ ok: true, …about, …value }`, or `{ ok: false, …about, error }` with the refusal's status), `fetcherRefusal(error, language, route?)` (the refusal, or the thrown route error) and `submitForm(language, run, fieldOf, route?)` (`{ ok: true, value }`, or a `FormErrors` with the refusal's status); `route` is the `RouteErrorOptions` of the `routeError` they throw through |
|
|
905
|
+
| `apiCallFailure(error, { api, operation?, requestId?, signal?, passedThrough?, log? })`, `CallFailureOptions` | what a failed call to the API becomes, for a loader or an action to throw, logged with the request id: an `ApiError` as it is (with `requestId`), a `Response` of its own status for a 401, 403 or 404 without a GraphQL body, a 503 `Response` with no body for an unreachable, slow, garbled or aborted call (the reason is in the log), any other error as it is; the log lines start with `api`, and a `log` that throws never replaces the outcome. It recognises `@nxgt/graphql-client`'s errors by `name`, not `instanceof` |
|
|
906
|
+
| `operationName(document)`, `DocumentLike` | the name of the operation a document holds, for the log (else `query`, `mutation` or `subscription`); a structural `{ definitions }`, so `graphql`'s `DocumentNode` fits |
|
|
907
|
+
| `createErrorCatalogue({ messages, languages, read, versions, ttlMs, clientCodes, retryMs?, firstReadWaitMs?, log? })`, `ErrorCatalogue`, `ErrorCatalogueOptions`, `CatalogueReply` | keeps the API's error catalogue in the store: `fresh(language, now?)` and `freshAll(now?)` (a cold language is awaited for `firstReadWaitMs`, 1500 by default, and read once however many requests ask; a held one is served, and its version checked in the background once per `ttlMs`, a number or a getter; only a language whose version moved is read again; a failure is retried after `retryMs`, 5000 by default), `warm()` for startup, `clientMessages()` (the `clientCodes` the browser words itself, by language, for the root's loader), and, for tests, `reset()`, `heldVersion(language)`, `settled()` and the `now` parameters. `read` and `versions` are the app's calls to its API |
|
|
908
|
+
| `ApiRefusal` | `{ message, status }`: a failed call as a page says it; what `apiRefusal` returns for an API refusal (its status) or an unreachable API (503), else `undefined` |
|
|
909
|
+
| `jsonActions({ origin? })`, `JsonActions`, `JsonActionsOptions` | the JSON helpers of the actions, bound to the app's origin (a string, or a getter read at each request; the request's own by default): `readJson(request)` (403 `Cross-site request refused` for another origin or a cross-site fetch, 415 `Expected a JSON body`, 400 `Malformed JSON body`), `requireJsonPost(request)` (a native post goes back to its page) and `readInput(request, schema)` (the body's `input` through a Standard Schema, async allowed; 400 `The form sent an input the API would refuse`) |
|
|
910
|
+
| `useJsonFetcher(key?, action?)`, `JsonBody` | a fetcher that posts JSON: `{ fetcher, post(body, to?), busy }`; `to` is the action of one call |
|
|
911
|
+
| `StandardSchemaV1` | the Standard Schema v1 type `readInput` takes, declared in `/errors` (type only, so no `@alxia/core`; `readInput`'s output is inferred from the schema's `~standard.types`); a Zod 4, Valibot or ArkType schema fits |
|
|
912
|
+
|
|
626
913
|
The `alxia-react-router` bin, run with `bunx`:
|
|
627
914
|
|
|
628
915
|
| command | |
|
|
@@ -637,7 +924,8 @@ The `alxia-react-router` bin, run with `bunx`:
|
|
|
637
924
|
- [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
925
|
- [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
926
|
- [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
|
|
927
|
+
- [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.
|
|
928
|
+
- [An API's refusals](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/errors.md): what `/errors` holds, recognising an API error, the request id, the page a request is for, the catalogue store, the form checks in the API's words, the provider, wording a refusal, loaders and the error page, actions, JSON actions and CSRF, and keeping the catalogue fresh.
|
|
641
929
|
- [Troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md): each message, and the traps that print none.
|
|
642
930
|
- [Roadmap](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/roadmap.md): what is coming, and what is not planned.
|
|
643
931
|
- [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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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 {
|
|
105
|
+
export { watchForUpdates2, createUpdater2, afterLoad2, registerServiceWorker2, UPDATE_LATER_KEY2, createUpdateNotice2, sessionStore2 };
|
|
137
106
|
|
|
138
|
-
//# debugId=
|
|
139
|
-
//# sourceMappingURL=index-
|
|
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
|
+
}
|