@alxia/react-router 0.10.0 → 0.12.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 (98) hide show
  1. package/README.md +370 -20
  2. package/dist/chunks/index-5rt6hnhn.js +42 -0
  3. package/dist/chunks/index-5rt6hnhn.js.map +11 -0
  4. package/dist/chunks/{index-mppz0806.js → index-6jf7zpxz.js} +5 -5
  5. package/dist/chunks/{index-mppz0806.js.map → index-6jf7zpxz.js.map} +1 -1
  6. package/dist/chunks/index-y6vpnjr3.js +123 -0
  7. package/dist/chunks/index-y6vpnjr3.js.map +14 -0
  8. package/dist/context.d.ts +0 -7
  9. package/dist/context.d.ts.map +1 -1
  10. package/dist/document/index.d.ts +11 -0
  11. package/dist/document/index.d.ts.map +1 -0
  12. package/dist/document/index.js +19 -0
  13. package/dist/document/index.js.map +9 -0
  14. package/dist/document/inline-script.d.ts +27 -0
  15. package/dist/document/inline-script.d.ts.map +1 -0
  16. package/dist/document/language-store.d.ts +8 -0
  17. package/dist/document/language-store.d.ts.map +1 -0
  18. package/dist/document/nonce.d.ts +34 -0
  19. package/dist/document/nonce.d.ts.map +1 -0
  20. package/dist/document/page-language.d.ts +39 -0
  21. package/dist/document/page-language.d.ts.map +1 -0
  22. package/dist/document/text-direction.d.ts +10 -0
  23. package/dist/document/text-direction.d.ts.map +1 -0
  24. package/dist/entry/abort.d.ts +21 -0
  25. package/dist/entry/abort.d.ts.map +1 -0
  26. package/dist/entry/create-entry.d.ts +32 -0
  27. package/dist/entry/create-entry.d.ts.map +1 -0
  28. package/dist/entry/index.d.ts +8 -0
  29. package/dist/entry/index.d.ts.map +1 -0
  30. package/dist/entry/index.js +128 -0
  31. package/dist/entry/index.js.map +12 -0
  32. package/dist/entry/whole-page.d.ts +10 -0
  33. package/dist/entry/whole-page.d.ts.map +1 -0
  34. package/dist/errors/api-error.d.ts +86 -0
  35. package/dist/errors/api-error.d.ts.map +1 -0
  36. package/dist/errors/api-errors.d.ts +96 -0
  37. package/dist/errors/api-errors.d.ts.map +1 -0
  38. package/dist/errors/call-failure.d.ts +45 -0
  39. package/dist/errors/call-failure.d.ts.map +1 -0
  40. package/dist/errors/catalogue-check.d.ts +24 -0
  41. package/dist/errors/catalogue-check.d.ts.map +1 -0
  42. package/dist/errors/catalogue-read.d.ts +33 -0
  43. package/dist/errors/catalogue-read.d.ts.map +1 -0
  44. package/dist/errors/catalogue.d.ts +59 -0
  45. package/dist/errors/catalogue.d.ts.map +1 -0
  46. package/dist/errors/code-resolver.d.ts +44 -0
  47. package/dist/errors/code-resolver.d.ts.map +1 -0
  48. package/dist/errors/describe-refusal.d.ts +6 -0
  49. package/dist/errors/describe-refusal.d.ts.map +1 -0
  50. package/dist/errors/describe.d.ts +3 -0
  51. package/dist/errors/describe.d.ts.map +1 -0
  52. package/dist/errors/fetcher-result.d.ts +21 -0
  53. package/dist/errors/fetcher-result.d.ts.map +1 -0
  54. package/dist/errors/form-errors.d.ts +8 -0
  55. package/dist/errors/form-errors.d.ts.map +1 -0
  56. package/dist/errors/index.d.ts +15 -0
  57. package/dist/errors/index.d.ts.map +1 -0
  58. package/dist/errors/index.js +602 -0
  59. package/dist/errors/index.js.map +29 -0
  60. package/dist/errors/json-actions.d.ts +38 -0
  61. package/dist/errors/json-actions.d.ts.map +1 -0
  62. package/dist/errors/log.d.ts +8 -0
  63. package/dist/errors/log.d.ts.map +1 -0
  64. package/dist/errors/message-values.d.ts +11 -0
  65. package/dist/errors/message-values.d.ts.map +1 -0
  66. package/dist/errors/messages.d.ts +63 -0
  67. package/dist/errors/messages.d.ts.map +1 -0
  68. package/dist/errors/page-path.d.ts +9 -0
  69. package/dist/errors/page-path.d.ts.map +1 -0
  70. package/dist/errors/provider.d.ts +20 -0
  71. package/dist/errors/provider.d.ts.map +1 -0
  72. package/dist/errors/request-id.d.ts +5 -0
  73. package/dist/errors/request-id.d.ts.map +1 -0
  74. package/dist/errors/route-error.d.ts +31 -0
  75. package/dist/errors/route-error.d.ts.map +1 -0
  76. package/dist/errors/standard-schema.d.ts +34 -0
  77. package/dist/errors/standard-schema.d.ts.map +1 -0
  78. package/dist/errors/submit-form.d.ts +16 -0
  79. package/dist/errors/submit-form.d.ts.map +1 -0
  80. package/dist/errors/use-json-fetcher.d.ts +18 -0
  81. package/dist/errors/use-json-fetcher.d.ts.map +1 -0
  82. package/dist/index.js +13 -34
  83. package/dist/index.js.map +3 -5
  84. package/dist/pwa/build/index.js +1 -1
  85. package/dist/pwa/index.js +6 -6
  86. package/dist/pwa/index.js.map +1 -1
  87. package/dist/vite/index.d.ts.map +1 -1
  88. package/dist/vite/index.js +29 -2
  89. package/dist/vite/index.js.map +5 -4
  90. package/dist/vite/inline.d.ts +18 -0
  91. package/dist/vite/inline.d.ts.map +1 -0
  92. package/docs/README.md +3 -1
  93. package/docs/guide/csp-nonce.md +35 -3
  94. package/docs/guide/document.md +310 -0
  95. package/docs/guide/errors.md +838 -0
  96. package/docs/roadmap.md +36 -1
  97. package/docs/troubleshooting.md +712 -1
  98. package/package.json +33 -3
package/README.md CHANGED
@@ -95,7 +95,7 @@ client's files and the app's middlewares. So does React Router's prerendering.
95
95
  optional peer, for `/vite`; `react` 19 is an optional peer, for `/pwa/react`. The six `workbox-*`
96
96
  packages (`workbox-core`, `workbox-expiration`, `workbox-navigation-preload`,
97
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.
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.
99
99
 
100
100
  Its `typescript` peer is 6 or 7. The template's TypeScript 5.9 typechecks,
101
101
  but `bun add` warns about it; `bun add -d typescript@^6` silences the
@@ -255,7 +255,7 @@ for the types.
255
255
 
256
256
  With `secureHeaders({ nonce: true })` from `@alxia/secure-headers` in
257
257
  `configure`, each request's policy names a fresh nonce, and the context
258
- carries it. `nonceOf(loadContext)` reads it in `entry.server.tsx`, and React
258
+ carries it. `@alxia/react-router/entry` reads it with `nonceOf(loadContext)`, and React
259
259
  Router puts it on every script it renders, so `script-src` needs no
260
260
  `'unsafe-inline'`:
261
261
 
@@ -271,33 +271,185 @@ configure: (app) =>
271
271
  ),
272
272
  ```
273
273
 
274
- The template has no `entry.server.tsx`; reveal React Router's, then add
275
- three lines:
274
+ The template has no `entry.server.tsx`; the package's own is one line:
276
275
 
277
- ```sh
278
- bunx react-router reveal entry.server
276
+ ```ts
277
+ // app/entry.server.tsx
278
+ export { handleRequest as default, streamTimeout } from '@alxia/react-router/entry';
279
279
  ```
280
280
 
281
- ```diff
282
- // app/entry.server.tsx, as reveal writes it
283
- import { PassThrough } from "node:stream";
284
-
285
- +import { nonceOf } from "@alxia/react-router";
286
- import type { EntryContext, RouterContextProvider } from "react-router";
287
- …
288
- const { pipe, abort } = renderToPipeableStream(
289
- - <ServerRouter context={routerContext} url={request.url} />,
290
- + <ServerRouter context={routerContext} url={request.url} nonce={nonceOf(loadContext)} />,
291
- {
292
- + nonce: nonceOf(loadContext),
293
- [readyOption]() {
294
- ```
281
+ To customise the entry, reveal React Router's and add the nonce yourself
282
+ ([an entry of your own](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/csp-nonce.md#an-entry-of-your-own)).
295
283
 
296
284
  `nonceOf` returns `undefined` when no middleware set a nonce, so the entry works
297
285
  with or without secure-headers: neither package depends on the other. Any
298
286
  `derive` that returns a string `nonce` works the same.
299
287
  [More](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/csp-nonce.md#a-csp-nonce)
300
288
 
289
+ ## The document: nonce and boot scripts
290
+
291
+ `@alxia/react-router/document` renders the CSP nonce in the root, on the
292
+ server and in the browser, with `react` as its only import. The nonce comes
293
+ from `secureHeaders({ nonce: true })` ([A CSP nonce](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/csp-nonce.md)).
294
+ The server render provides it around the router; `useNonce()` reads it there,
295
+ and in the browser reads the one the document's scripts carry, so hydration
296
+ finds the attribute it rendered. `InlineScript` is an inline script with
297
+ that nonce, for a theme's boot script, which must run before the first
298
+ paint:
299
+
300
+ ```tsx
301
+ // app/root.tsx
302
+ import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router';
303
+ import { CspNonceMeta, InlineScript } from '@alxia/react-router/document';
304
+ import { themeBootScript } from '<your theme library>';
305
+
306
+ const THEME_BOOT = themeBootScript();
307
+
308
+ export function Layout({ children }: { children: React.ReactNode }) {
309
+ return (
310
+ <html lang="en" suppressHydrationWarning>
311
+ <head>
312
+ <meta charSet="utf-8" />
313
+ <InlineScript code={THEME_BOOT} />
314
+ <CspNonceMeta />
315
+ <Meta />
316
+ <Links />
317
+ </head>
318
+ <body>
319
+ {children}
320
+ <ScrollRestoration />
321
+ <Scripts />
322
+ </body>
323
+ </html>
324
+ );
325
+ }
326
+
327
+ export default function App() {
328
+ return <Outlet />;
329
+ }
330
+ ```
331
+
332
+ `THEME_BOOT` is your theme library's own boot script. With `/entry`, `<Links>`,
333
+ `<Scripts>` and `<ScrollRestoration>` take the nonce from `<ServerRouter nonce>`,
334
+ so their `nonce` props are optional; `useNonce()` stays for `InlineScript`,
335
+ `CspNonceMeta` and any script or style the app inserts. An entry of your own
336
+ wraps the router:
337
+
338
+ ```tsx
339
+ // app/entry.server.tsx (inside handleRequest)
340
+ import { nonceOf } from '@alxia/react-router';
341
+ import { DocumentNonce } from '@alxia/react-router/document';
342
+ import { ServerRouter } from 'react-router';
343
+
344
+ <DocumentNonce nonce={nonceOf(loadContext)}>
345
+ <ServerRouter context={routerContext} url={request.url} />
346
+ </DocumentNonce>;
347
+ ```
348
+
349
+ > The `InlineScript` code is written unescaped: keep it a constant of the
350
+ > app, never user input. It throws a `TypeError` on `</script` or `<!--`
351
+ > ([the message](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md#typeerror-inlinescript-the-code-contains-script-or----which-ends-or-confuses-the-script-element-escape-it-such-as-script-inside-a-string)).
352
+ > A nonced page must never be made `public` by a route's `headers` export: a
353
+ > shared cache would hand the same nonce to every visitor.
354
+
355
+ ### The page's language
356
+
357
+ `createPageLanguage` gives the root the page's language for `<html lang>` and
358
+ a route's `meta`, and `textDirection` the `dir` that goes with it. The server
359
+ side, negotiating the request's language from a cookie and `Accept-Language`,
360
+ is [`@alxia/language`](https://github.com/softistx/alxia/blob/develop/packages/language/README.md):
361
+
362
+ ```ts
363
+ // app/server.ts
364
+ import { language } from '@alxia/language';
365
+ import { createServer } from '@alxia/react-router';
366
+
367
+ const server = createServer({
368
+ configure: (app) =>
369
+ app.use(language({ supported: ['en', 'fr', 'ar'], fallback: 'en', order: ['cookie', 'header'] })),
370
+ });
371
+ export default server;
372
+ declare module '@alxia/react-router' {
373
+ interface Register { server: typeof server }
374
+ }
375
+ ```
376
+
377
+ ```ts
378
+ // app/language.ts: one instance, imported by the root and the routes
379
+ import { createPageLanguage } from '@alxia/react-router/document';
380
+
381
+ export const { pageLanguage, metaLanguage, chooseLanguage, useChosenLanguage } =
382
+ createPageLanguage({ languages: ['en', 'fr', 'ar'], fallback: 'en' });
383
+ ```
384
+
385
+ ```tsx
386
+ // app/root.tsx
387
+ import { type AlxiaArgs, withAlxia } from '@alxia/react-router';
388
+ import { textDirection } from '@alxia/react-router/document';
389
+ import { useRouteLoaderData } from 'react-router';
390
+ import { metaLanguage, pageLanguage, useChosenLanguage } from './language';
391
+ import type { Route } from './+types/root';
392
+
393
+ const titles = { en: 'Home', fr: 'Accueil', ar: 'الرئيسية' };
394
+
395
+ // the root loader returns alxia.language, set by the middleware
396
+ export const loader = withAlxia(({ alxia }: Route.LoaderArgs & AlxiaArgs) => ({ language: alxia.language }));
397
+
398
+ export function Layout({ children }: { children: React.ReactNode }) {
399
+ const data = useRouteLoaderData<typeof loader>('root');
400
+ const language = pageLanguage(data?.language);
401
+ useChosenLanguage(); // re-renders the shell when the menu picks a language
402
+ return (
403
+ <html lang={language} dir={textDirection(language)} suppressHydrationWarning>
404
+ {/* … */}
405
+ </html>
406
+ );
407
+ }
408
+
409
+ export const meta: Route.MetaFunction = ({ matches }) => [
410
+ { title: titles[metaLanguage(matches)] },
411
+ ];
412
+ ```
413
+
414
+ - `pageLanguage(language)` is the supported language, else the fallback. In
415
+ the browser the one picked with `chooseLanguage(language)` wins, and the
416
+ last one seen stands in when the root's data is gone. On the server it
417
+ remembers nothing: one module serves every request.
418
+ - `metaLanguage(matches)` reads `matches[0].loaderData.language`.
419
+ - `useChosenLanguage()` is the picked language, or `undefined`; it
420
+ re-renders on a pick, and is `undefined` in the server render so
421
+ hydration matches.
422
+ - `textDirection(tag)` is `'rtl'` for `ar`, `fa`, `he`, `ur`, `ps`, `yi`,
423
+ `dv`, `ckb`, `sd` and `ug` (and the legacy `iw` and `ji`), else `'ltr'`, reading the base tag whatever its
424
+ case (`ar-EG`, `AR`). A script subtag is not read: `az-Arab` is `'ltr'`.
425
+ - A fallback outside `languages` throws
426
+ ([the message](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md#typeerror-createpagelanguage-the-fallback--is-not-one-of-the-languages-got-)).
427
+ An error page keeps the root's data, so it keeps the language, except
428
+ for a URL that matches no route or a root loader that threw: there it
429
+ shows the last language seen in the browser, or the fallback ([why](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md#the-error-page-shows-in-the-fallback-language)).
430
+
431
+ [More](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/document.md)
432
+
433
+ ## The entry
434
+
435
+ `@alxia/react-router/entry` is `entry.server.tsx` for an app on Bun, server
436
+ only: it streams the shell, waits for the whole page for a bot and in SPA
437
+ mode, answers HEAD with no body, and gives the router the response's CSP
438
+ nonce (`nonceOf`) and `DocumentNonce`. It renders with the web stream
439
+ renderer, so no `@react-router/node` and no `node:stream`. `react-dom` and
440
+ `isbot` (needed only without an `isBot` option) are optional peers, which React Router's templates install.
441
+
442
+ ```tsx
443
+ // app/entry.server.tsx
444
+ export { handleRequest as default, streamTimeout } from '@alxia/react-router/entry';
445
+ ```
446
+
447
+ To change `streamTimeout`, the nonce, the bot check or the error report,
448
+ `createEntry({ streamTimeout, nonce, isBot, onError })` returns
449
+ `{ handleRequest, streamTimeout }`.
450
+
451
+ [More](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/document.md#the-entry)
452
+
301
453
  ## WebSockets
302
454
 
303
455
  A `ws` route in `configure` connects under `react-router dev`, under
@@ -410,6 +562,9 @@ build, it adds:
410
562
  - `build.target: 'esnext'`, where Vite's default targets browsers;
411
563
  - in the build only, `resolve.noExternal: true`: every package is bundled
412
564
  into `build/server/index.js`, so `build/` runs with no `node_modules`.
565
+ - `/entry` and `/document` inlined, in dev by an alias and in the build, so
566
+ they share the app's `react-router`, even when the package is listed in
567
+ `ssr.external`.
413
568
 
414
569
  ```ts
415
570
  // app/server.ts: Bun's modules, bundled as they are
@@ -621,6 +776,155 @@ The [PWA guide](https://github.com/softistx/alxia/blob/develop/packages/react-ro
621
776
  has the worker's options, the offline page, the manifest route, the update
622
777
  banner and the rest.
623
778
 
779
+ ## An API's refusals, worded for the page
780
+
781
+ `@alxia/react-router/errors` has no UI; its one runtime import is `react-router`, never `@alxia/core`. It
782
+ holds the shape of a GraphQL API's refusal, declared structurally (no `@nxgt/graphql-client` at
783
+ runtime), the request id an API echoes, the page a request is for, the
784
+ catalogue of the API's messages an app keeps, the words of a form's checks,
785
+ 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`):
786
+
787
+ ```ts
788
+ import { isApiErrorLike, nextOf, requestIdOf } from '@alxia/react-router/errors';
789
+
790
+ export function describe(error: unknown, request: Request) {
791
+ return isApiErrorLike(error)
792
+ ? { code: error.code, status: error.status, next: nextOf(request.url) }
793
+ : undefined;
794
+ }
795
+
796
+ export const idOf = (response: Response) => requestIdOf(response);
797
+ ```
798
+
799
+ The catalogue is an instance made with the app's ICU formatter. A form's checks
800
+ read it through `apiCodeResolver`, and the provider fills it from the root's data:
801
+
802
+ ```ts
803
+ import {
804
+ ApiMessagesProvider,
805
+ apiCodeResolver,
806
+ createApiMessages,
807
+ } from '@alxia/react-router/errors';
808
+
809
+ type Language = 'en' | 'fr';
810
+
811
+ export const apiMessages = createApiMessages<Language>({
812
+ format: (message) => message, // the app's ICU formatter goes here
813
+ });
814
+
815
+ export const resolver = apiCodeResolver(apiMessages, {
816
+ translate: (key) => key,
817
+ language: () => 'en',
818
+ });
819
+
820
+ // In the root: <ApiMessagesProvider messages={apiMessages} value={data.catalogue}>…
821
+ export const provider = ApiMessagesProvider({
822
+ messages: apiMessages,
823
+ value: { en: {}, fr: {} },
824
+ children: null,
825
+ });
826
+ ```
827
+
828
+ A failed call is worded in the page's language by `createApiErrors`:
829
+
830
+ ```ts
831
+ import { createApiErrors } from '@alxia/react-router/errors';
832
+ import { apiMessages } from './api-messages';
833
+
834
+ export const apiErrors = createApiErrors<'en' | 'fr'>({
835
+ messages: apiMessages,
836
+ unavailable: (language) => (language === 'en' ? 'Unavailable' : 'Indisponible'),
837
+ });
838
+
839
+ // In an action's catch: { message, status }, or undefined (rethrow it).
840
+ export const refusal = (error: unknown) => apiErrors.apiRefusal(error, 'en');
841
+ ```
842
+
843
+ A loader throws the API's error as a route error, with the call's request id
844
+ for the error page; an action answers a refusal as data:
845
+
846
+ ```ts
847
+ import { errorRequestId, routeError } from '@alxia/react-router/errors';
848
+ import { apiErrors } from './api-errors';
849
+
850
+ // In a loader's catch: a 401, 403, 404 or 503 ApiError becomes a route error.
851
+ export const thrown = (error: unknown) => routeError(error);
852
+
853
+ // In the root's ErrorBoundary: the reference the user can quote.
854
+ export const reference = (error: unknown) => errorRequestId(error);
855
+
856
+ // A fetcher's action: { ok: true, …value }, or { ok: false, error } with a status.
857
+ export const rename = (run: () => Promise<{ title: string }>) =>
858
+ apiErrors.fetcherResult('en', { intent: 'rename' }, run);
859
+
860
+ // A form's action: { ok: true, value }, or the form's errors with a status.
861
+ export const create = (run: () => Promise<{ id: string }>) =>
862
+ apiErrors.submitForm('en', run, (path) =>
863
+ path === 'todo.title' ? 'title' : undefined,
864
+ );
865
+ ```
866
+
867
+ JSON actions refuse a post from another site, and read a Standard Schema's input:
868
+
869
+ ```ts
870
+ import { jsonActions, useJsonFetcher } from '@alxia/react-router/errors';
871
+ import { z } from 'zod';
872
+
873
+ // Behind a TLS proxy named with createServer({ proxy }), nothing to set; else: origin: () => process.env.APP_ORIGIN
874
+ export const { readInput } = jsonActions();
875
+
876
+ const schema = z.object({ title: z.string().min(1) });
877
+
878
+ export async function action({ request }: { request: Request }) {
879
+ return readInput(request, schema); // 400 when the input is refused
880
+ }
881
+
882
+ // In the component: const { post, busy } = useJsonFetcher<typeof action>();
883
+ ```
884
+
885
+ 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 })`
886
+ (your own `execute` calls it in its `catch`):
887
+
888
+ ```ts
889
+ import { apiCallFailure, operationName } from '@alxia/react-router/errors';
890
+ import type { DocumentNode } from 'graphql';
891
+
892
+ export function failed(error: unknown, document: DocumentNode, requestId?: string, signal?: AbortSignal) {
893
+ return apiCallFailure(error, { api: 'my-api', operation: operationName(document), requestId, signal });
894
+ }
895
+ // In execute(): catch (error) { throw failed(error, document, requestId, signal); }
896
+ ```
897
+
898
+ The API's catalogue is read and kept fresh by `createErrorCatalogue`; the root's
899
+ middleware awaits `freshAll()`, its loader returns `clientMessages()` and
900
+ `server.ts` calls `warm()`:
901
+
902
+ ```ts
903
+ import { createErrorCatalogue } from '@alxia/react-router/errors';
904
+ import { apiMessages } from './api-messages';
905
+
906
+ export const catalogue = createErrorCatalogue<'en' | 'fr'>({
907
+ messages: apiMessages,
908
+ languages: ['en', 'fr'],
909
+ read: async (language) => fetchCatalogue(language), // { version, messages }
910
+ versions: async () => fetchVersions(), // { en: '…', fr: '…' }
911
+ ttlMs: () => Number(process.env.CATALOGUE_TTL_MS ?? 60_000),
912
+ clientCodes: ['validation.errors.required'],
913
+ });
914
+
915
+ declare function fetchCatalogue(
916
+ language: string,
917
+ ): Promise<{ version: string; messages: { code: string; message: string; params: { name: string; kind: 'TEXT' }[] }[] }>;
918
+ declare function fetchVersions(): Promise<{ en: string; fr: string }>;
919
+
920
+ // The root's middleware: await catalogue.freshAll();
921
+ // The root's loader: return { messages: catalogue.clientMessages() };
922
+ // server.ts, at startup: catalogue.warm();
923
+ ```
924
+
925
+ The [errors guide](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/errors.md)
926
+ has the rest.
927
+
624
928
  ## Traps
625
929
 
626
930
  - **A context key made in `app/` reaches the loaders only through the
@@ -736,6 +1040,50 @@ From `@alxia/react-router/pwa/build` (Bun; Node's built-ins its only imports):
736
1040
  | `BuildWorkerOptions` | `file` (required), `root`, `client`, `contract`, `offline`, `precacheAssets`, `minify` |
737
1041
  | `WorkerBuild` | `{ file, precache, precacheBytes, bytes }` |
738
1042
 
1043
+ From `@alxia/react-router/errors` (`react-router` is its only package import, and never `@alxia/core`):
1044
+
1045
+ | export | |
1046
+ | --- | --- |
1047
+ | `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 |
1048
+ | `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`) |
1049
+ | `waitMinutes(extensions)` | the minutes of a throttle's wait: `minutes` (at least one), else `retryAfter` rounded up |
1050
+ | `REQUEST_ID_HEADER`, `requestIdOf(response)` | `x-request-id`, and the id a response carries, if the API sent one |
1051
+ | `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 |
1052
+ | `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 |
1053
+ | `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 |
1054
+ | `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?`) |
1055
+ | `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` |
1056
+ | `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 |
1057
+ | `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 |
1058
+ | `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 |
1059
+ | `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 |
1060
+ | `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` |
1061
+ | `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 |
1062
+ | `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 |
1063
+ | `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` |
1064
+ | `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`) |
1065
+ | `useJsonFetcher(key?, action?)`, `JsonBody` | a fetcher that posts JSON: `{ fetcher, post(body, to?), busy }`; `to` is the action of one call |
1066
+ | `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 |
1067
+
1068
+ From `@alxia/react-router/document`:
1069
+
1070
+ | export | |
1071
+ | --- | --- |
1072
+ | `DocumentNonce({ nonce, children })`, `DocumentNonceProps` | the provider of this response's CSP nonce, for the server render: wrap the router with `nonceOf(loadContext)` in `entry.server.tsx` |
1073
+ | `useNonce()` | the page's CSP nonce: the provider's on the server, the first script or `csp-nonce` meta of the `<head>`'s (else the first `script[nonce]`'s) in the browser (read once and cached), `undefined` when there is none |
1074
+ | `InlineScript({ code, nonce? })`, `InlineScriptProps` | an inline `<script>` with `nonce ?? useNonce()`; the code is not escaped, so it is a constant of yours, and `</script` or `<!--` in it throws a `TypeError`; no `nonce` attribute when there is no nonce |
1075
+ | `CspNonceMeta()` | `<meta property="csp-nonce" nonce>`, the nonce for what a UI library adds at runtime |
1076
+ | `createPageLanguage({ languages, fallback, browser? })` | `PageLanguage`: `pageLanguage`, `metaLanguage`, `chooseLanguage`, `useChosenLanguage`, for `<html lang>`, a route's `meta` and a language menu |
1077
+ | `PageLanguage`, `PageLanguageOptions` | the types of the instance and of its options |
1078
+ | `textDirection(language)` | `'ltr'` or `'rtl'` for `<html dir>`, from the base language |
1079
+
1080
+ From `@alxia/react-router/entry`:
1081
+
1082
+ | export | |
1083
+ | --- | --- |
1084
+ | `createEntry({ streamTimeout?, nonce?, isBot?, onError? })`, `EntryOptions`, `Entry` | `/entry`: `{ handleRequest, streamTimeout }` (`Entry`), `entry.server.tsx` for Bun on the web stream renderer |
1085
+ | `handleRequest`, `streamTimeout`, `HandleRequest` | `/entry`: the default entry (`createEntry()`), its `5_000` and the type of `handleRequest` (assignable to React Router's `ServerEntryModule['default']`) |
1086
+
739
1087
  The `alxia-react-router` bin, run with `bunx`:
740
1088
 
741
1089
  | command | |
@@ -748,9 +1096,11 @@ The `alxia-react-router` bin, run with `bunx`:
748
1096
  - [Getting started](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/getting-started.md): a new app, the official template or an existing app.
749
1097
  - [How it works](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/how-it-works.md): dev, the build and `vite preview`, and what the build sets for Bun.
750
1098
  - [Customising the server](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/customising-the-server.md): `createServer()`, and the escape hatches.
1099
+ - [The document](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/document.md): the CSP nonce in the root, `InlineScript` for a theme's boot script with no flash, `CspNonceMeta`, and the entry.
751
1100
  - [Typing the loaders](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/typing-the-loaders.md): `Register`, and the app's own context keys.
752
1101
  - [A CSP nonce](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/csp-nonce.md), [Beside the pages](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/beside-the-pages.md) (middlewares, routes, WebSockets, the client's files), [OpenAPI](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/openapi.md), [Testing](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/testing.md) and [Deploying](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/deploying.md), with Docker.
753
1102
  - [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.
1103
+ - [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.
754
1104
  - [Troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md): each message, and the traps that print none.
755
1105
  - [Roadmap](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/roadmap.md): what is coming, and what is not planned.
756
1106
  - [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,42 @@
1
+ // src/context.ts
2
+ import { createContext, RouterContextProvider } from "react-router";
3
+ function singleton(name, make) {
4
+ const registry = globalThis;
5
+ const key = Symbol.for(`@alxia/react-router:${name}@1`);
6
+ registry[key] ??= make();
7
+ return registry[key];
8
+ }
9
+ var shared = singleton("context", () => {
10
+ const missing = Symbol("alxia context missing");
11
+ return { missing, context: createContext(missing) };
12
+ });
13
+ var MISSING = shared.missing;
14
+ var alxiaContext2 = shared.context;
15
+ function alxiaOf2(context) {
16
+ const value = context.get(alxiaContext2);
17
+ if (value === MISSING) {
18
+ throw new Error("alxiaOf(): this request has no alxia context. Serve the React Router app through alxia: add alxia() from @alxia/react-router/vite to vite.config.ts's plugins, or, with a server of your own, serve the build through reactRouter() from @alxia/react-router.");
19
+ }
20
+ return value;
21
+ }
22
+ Object.defineProperty(RouterContextProvider.prototype, "alxia", {
23
+ configurable: true,
24
+ enumerable: false,
25
+ get() {
26
+ return alxiaOf2(this);
27
+ }
28
+ });
29
+
30
+ // src/nonce.ts
31
+ function nonceOf2(context) {
32
+ const value = context.get(alxiaContext2);
33
+ if (typeof value !== "object" || value === null || !("nonce" in value)) {
34
+ return;
35
+ }
36
+ return typeof value.nonce === "string" ? value.nonce : undefined;
37
+ }
38
+
39
+ export { alxiaContext2, alxiaOf2, nonceOf2 };
40
+
41
+ //# debugId=BC95EB0B0221673364756E2164756E21
42
+ //# sourceMappingURL=index-5rt6hnhn.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/context.ts", "../src/nonce.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * The key under which `reactRouter()` hands every loader, action and\n * middleware the request's alxia context, and the typed way to read it.\n */\nimport type { Alxia, AnyAlxia, ContextOf, RegisteredBase } from '@alxia/core';\nimport { createContext, RouterContextProvider } from 'react-router';\nimport type { ReactRouterServer } from './server';\n\n/**\n * One value per process, whichever copy of this module asks: the server\n * build bundles `/entry` and the chunk it shares with `.` (see\n * `vite/inline.ts`), and the server imports `.` outside the bundle.\n *\n * The key is shared across versions of this package. Its name ends in a\n * shape version (`@1`): a version that changes what is stored under it bumps\n * the suffix, so two copies of different shapes never read each other's.\n */\nfunction singleton<T>(name: string, make: () => T): T {\n\tconst registry = globalThis as Record<symbol, unknown>;\n\tconst key = Symbol.for(`@alxia/react-router:${name}@1`);\n\tregistry[key] ??= make();\n\treturn registry[key] as T;\n}\n\n/**\n * The key `reactRouter()` sets on React Router's context provider, on every\n * request, to what alxia's middlewares built for it, and its default, the\n * \"missing\" sentinel no catch-all replaced. Both are one pair per process,\n * so that a second copy's `alxiaOf` compares with the sentinel the key holds.\n * It lives in this package, so it is one object whichever way the server was\n * built or loaded: a key made in the app's own `app/` folder is copied into\n * React Router's build, and the server that imports it separately sets a\n * different one.\n */\nconst shared = singleton('context', () => {\n\t// `as never` gives the `unique symbol` type without a `declare const`.\n\tconst missing: unique symbol = Symbol('alxia context missing') as never;\n\treturn { missing, context: createContext<unknown>(missing) };\n});\nconst MISSING = shared.missing;\nexport const alxiaContext = shared.context;\n\n/**\n * Names the server whose context `alxiaOf(context)` reads when given no\n * type argument. One React Router build has one server, so it may be\n * declared once, beside it:\n *\n * ```ts\n * // app/server.ts\n * const server = createServer({ configure: (app) => app.use(session) });\n * export default server;\n *\n * declare module '@alxia/react-router' {\n * interface Register {\n * server: typeof server;\n * }\n * }\n * ```\n *\n * Unregistered here, `alxiaOf(context)` reads the app `@alxia/core`'s own\n * `Register` names as its `context`, and `BaseContext` when neither is\n * declared. When both are, this one wins: the server's app is the whole\n * app the pages run behind, the base's context and all `configure` adds.\n */\n// biome-ignore lint/suspicious/noEmptyInterface: an app augments it\nexport interface Register {}\n\n/** The app a server makes, or the app itself. */\nexport type AppOf<Server> =\n\tServer extends ReactRouterServer<infer App> ? App : Server;\n\n/**\n * What `Register` names when it is neither a server nor an app — the\n * module, say, rather than its default export: an app whose context has\n * nothing but this key, so that reading anything of it is a compile error.\n */\nexport type InvalidRegister = Alxia<\n\t{\n\t\treadonly 'Register.server must be typeof server, the default export of createServer()': never;\n\t},\n\t''\n>;\n\n/**\n * The app `alxiaOf` reads for a `Register` interface: its server's, or\n * `InvalidRegister`; with no server, `Core`, the app `@alxia/core`'s\n * `Register` names (a fresh one when it names none).\n */\nexport type RegisteredOf<\n\tR,\n\tCore extends AnyAlxia = RegisteredBase,\n> = R extends {\n\treadonly server: infer Server;\n}\n\t? Server extends AnyAlxia | ReactRouterServer<AnyAlxia>\n\t\t? AppOf<Server>\n\t\t: InvalidRegister\n\t: Core;\n\n/** What `alxiaOf` reads with no type argument: the registered server's app, else core's registered app, else a fresh one. */\nexport type RegisteredApp = RegisteredOf<Register>;\n\n/**\n * What alxia's middlewares built for this request, read in a loader, an action or\n * a middleware, typed by the app: the server `Register` names, or the one\n * given as the type argument — `typeof server`, or an app *before* the\n * catch-all. With neither, the app `@alxia/core`'s `Register` names, and\n * `BaseContext` when nothing is registered.\n *\n * ```ts\n * export async function loader({ context }: Route.LoaderArgs) {\n * const { user, log } = alxiaOf(context);\n * }\n * ```\n *\n * Throws when the request did not come through `reactRouter()`: under\n * `react-router dev` without the plugin, or a test that calls a loader\n * directly.\n */\nexport function alxiaOf<\n\tApp extends AnyAlxia | ReactRouterServer<AnyAlxia> = RegisteredApp,\n>(context: ProviderLike): AlxiaContextOf<App> {\n\tconst value = context.get(alxiaContext);\n\tif (value === MISSING) {\n\t\tthrow new Error(\n\t\t\t\"alxiaOf(): this request has no alxia context. Serve the React Router app through alxia: add alxia() from @alxia/react-router/vite to vite.config.ts's plugins, or, with a server of your own, serve the build through reactRouter() from @alxia/react-router.\",\n\t\t);\n\t}\n\treturn value as AlxiaContextOf<App>;\n}\n\n/**\n * What `alxiaOf` and `nonceOf` read of a provider: its `get`. Not the whole\n * class, which this package gives an `alxia`: the generated route types'\n * `context` is another declaration of it, without one, and must still pass.\n */\nexport type ProviderLike = Pick<Readonly<RouterContextProvider>, 'get'>;\n\n/** What `alxiaOf<App>(context)` returns, and `context.alxia` is for the registered app. */\nexport type AlxiaContextOf<\n\tApp extends AnyAlxia | ReactRouterServer<AnyAlxia> = RegisteredApp,\n> = ContextOf<AppOf<App>> & {\n\t/** The catch-all's route, which every request reaching React Router matched. */\n\treadonly route: string;\n};\n\ndeclare module 'react-router' {\n\tinterface RouterContextProvider {\n\t\t/**\n\t\t * What alxia's middlewares built for this request — `alxiaOf(context)`,\n\t\t * typed by the app `Register` names (`BaseContext` with none):\n\t\t *\n\t\t * ```ts\n\t\t * export async function loader({ context }: LoaderFunctionArgs) {\n\t\t * const { user, server } = context.alxia;\n\t\t * }\n\t\t * ```\n\t\t *\n\t\t * Typed with `react-router`'s own argument types. The generated\n\t\t * `Route.LoaderArgs` reads a second declaration of this class (its\n\t\t * `./internal` types point at react-router's development build), which\n\t\t * no augmentation reaches: read `alxiaOf(context)` there. Throws as\n\t\t * `alxiaOf` does on a request that did not come through\n\t\t * `reactRouter()`. Server-side only: a `clientLoader` has none.\n\t\t */\n\t\treadonly alxia: AlxiaContextOf;\n\t}\n}\n\n// A getter on the prototype rather than a value set on each provider: it\n// reads `alxiaContext`, so it holds exactly when that key is set — a\n// provider React Router copies keeps it — and on a provider no\n// `reactRouter()` filled, a test's, it throws `alxiaOf`'s error rather than\n// reading `undefined`.\nObject.defineProperty(RouterContextProvider.prototype, 'alxia', {\n\tconfigurable: true,\n\tenumerable: false,\n\tget(this: Readonly<RouterContextProvider>) {\n\t\treturn alxiaOf(this);\n\t},\n});\n",
6
+ "/** `nonceOf`: the request's CSP nonce, read from alxia's context if a middleware set one. */\nimport { alxiaContext, type ProviderLike } from './context';\n\n/**\n * This request's CSP nonce, for `entry.server.tsx`: the `nonce` alxia's\n * middlewares put on the context — `secureHeaders({ nonce: true })` from\n * `@alxia/secure-headers`, or a `derive` of your own — or `undefined` when\n * none did, or when the request did not come through alxia. It reads the\n * key if present: neither package depends on the other.\n *\n * ```tsx\n * const nonce = nonceOf(loadContext);\n * <ServerRouter context={routerContext} url={request.url} nonce={nonce} />\n * ```\n */\nexport function nonceOf(context: ProviderLike): string | undefined {\n\tconst value = context.get(alxiaContext);\n\tif (typeof value !== 'object' || value === null || !('nonce' in value)) {\n\t\treturn undefined;\n\t}\n\treturn typeof value.nonce === 'string' ? value.nonce : undefined;\n}\n"
7
+ ],
8
+ "mappings": ";AAKA;AAYA,SAAS,SAAY,CAAC,MAAc,MAAkB;AAAA,EACrD,MAAM,WAAW;AAAA,EACjB,MAAM,MAAM,OAAO,IAAI,uBAAuB,QAAQ;AAAA,EACtD,SAAS,SAAS,KAAK;AAAA,EACvB,OAAO,SAAS;AAAA;AAajB,IAAM,SAAS,UAAU,WAAW,MAAM;AAAA,EAEzC,MAAM,UAAyB,OAAO,uBAAuB;AAAA,EAC7D,OAAO,EAAE,SAAS,SAAS,cAAuB,OAAO,EAAE;AAAA,CAC3D;AACD,IAAM,UAAU,OAAO;AAChB,IAAM,gBAAe,OAAO;AA+E5B,SAAS,QAEf,CAAC,SAA4C;AAAA,EAC7C,MAAM,QAAQ,QAAQ,IAAI,aAAY;AAAA,EACtC,IAAI,UAAU,SAAS;AAAA,IACtB,MAAM,IAAI,MACT,+PACD;AAAA,EACD;AAAA,EACA,OAAO;AAAA;AA8CR,OAAO,eAAe,sBAAsB,WAAW,SAAS;AAAA,EAC/D,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,GAAG,GAAwC;AAAA,IAC1C,OAAO,SAAQ,IAAI;AAAA;AAErB,CAAC;;;ACrKM,SAAS,QAAO,CAAC,SAA2C;AAAA,EAClE,MAAM,QAAQ,QAAQ,IAAI,aAAY;AAAA,EACtC,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,EAAE,WAAW,QAAQ;AAAA,IACvE;AAAA,EACD;AAAA,EACA,OAAO,OAAO,MAAM,UAAU,WAAW,MAAM,QAAQ;AAAA;",
9
+ "debugId": "BC95EB0B0221673364756E2164756E21",
10
+ "names": []
11
+ }
@@ -1,11 +1,11 @@
1
+ import {
2
+ PRECACHE_DEFINE
3
+ } from "./index-59vhfj6n.js";
1
4
  import {
2
5
  parseBrowserManifest2,
3
6
  precacheList2,
4
7
  assetPrecache2
5
8
  } from "./index-5gqcpyyg.js";
6
- import {
7
- PRECACHE_DEFINE
8
- } from "./index-59vhfj6n.js";
9
9
  import {
10
10
  SERVICE_WORKER_URL2,
11
11
  OFFLINE_URL2
@@ -115,5 +115,5 @@ ${result.logs.map(String).join(`
115
115
  }
116
116
  export { buildWorker2 };
117
117
 
118
- //# debugId=763F0D0F0869B4B964756E2164756E21
119
- //# sourceMappingURL=index-mppz0806.js.map
118
+ //# debugId=705C996D7C991BF164756E2164756E21
119
+ //# sourceMappingURL=index-6jf7zpxz.js.map
@@ -6,6 +6,6 @@
6
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
7
  ],
8
8
  "mappings": "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",
9
- "debugId": "763F0D0F0869B4B964756E2164756E21",
9
+ "debugId": "705C996D7C991BF164756E2164756E21",
10
10
  "names": []
11
11
  }