@alxia/react-router 0.11.0 → 0.12.1
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 +201 -19
- package/dist/chunks/index-5rt6hnhn.js +42 -0
- package/dist/chunks/index-5rt6hnhn.js.map +11 -0
- package/dist/chunks/{index-mppz0806.js → index-6jf7zpxz.js} +5 -5
- package/dist/chunks/{index-mppz0806.js.map → index-6jf7zpxz.js.map} +1 -1
- package/dist/chunks/index-y6vpnjr3.js +123 -0
- package/dist/chunks/index-y6vpnjr3.js.map +14 -0
- package/dist/context.d.ts +0 -7
- package/dist/context.d.ts.map +1 -1
- package/dist/document/index.d.ts +11 -0
- package/dist/document/index.d.ts.map +1 -0
- package/dist/document/index.js +19 -0
- package/dist/document/index.js.map +9 -0
- package/dist/document/inline-script.d.ts +27 -0
- package/dist/document/inline-script.d.ts.map +1 -0
- package/dist/document/language-store.d.ts +8 -0
- package/dist/document/language-store.d.ts.map +1 -0
- package/dist/document/nonce.d.ts +34 -0
- package/dist/document/nonce.d.ts.map +1 -0
- package/dist/document/page-language.d.ts +39 -0
- package/dist/document/page-language.d.ts.map +1 -0
- package/dist/document/text-direction.d.ts +10 -0
- package/dist/document/text-direction.d.ts.map +1 -0
- package/dist/entry/abort.d.ts +21 -0
- package/dist/entry/abort.d.ts.map +1 -0
- package/dist/entry/create-entry.d.ts +32 -0
- package/dist/entry/create-entry.d.ts.map +1 -0
- package/dist/entry/index.d.ts +8 -0
- package/dist/entry/index.d.ts.map +1 -0
- package/dist/entry/index.js +128 -0
- package/dist/entry/index.js.map +12 -0
- package/dist/entry/whole-page.d.ts +10 -0
- package/dist/entry/whole-page.d.ts.map +1 -0
- package/dist/index.js +13 -34
- package/dist/index.js.map +3 -5
- package/dist/pwa/build/index.js +1 -1
- package/dist/pwa/index.js +6 -6
- package/dist/pwa/index.js.map +1 -1
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +29 -2
- package/dist/vite/index.js.map +5 -4
- package/dist/vite/inline.d.ts +18 -0
- package/dist/vite/inline.d.ts.map +1 -0
- package/docs/README.md +1 -0
- package/docs/guide/csp-nonce.md +37 -3
- package/docs/guide/document.md +321 -0
- package/docs/roadmap.md +17 -1
- package/docs/troubleshooting.md +266 -1
- package/package.json +22 -2
package/README.md
CHANGED
|
@@ -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. `
|
|
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,192 @@ configure: (app) =>
|
|
|
271
271
|
),
|
|
272
272
|
```
|
|
273
273
|
|
|
274
|
-
The template has no `entry.server.tsx`;
|
|
275
|
-
three lines:
|
|
274
|
+
The template has no `entry.server.tsx`; the package's own is one line:
|
|
276
275
|
|
|
277
|
-
```
|
|
278
|
-
|
|
276
|
+
```ts
|
|
277
|
+
// app/entry.server.tsx
|
|
278
|
+
export { handleRequest as default, streamTimeout } from '@alxia/react-router/entry';
|
|
279
279
|
```
|
|
280
280
|
|
|
281
|
-
|
|
282
|
-
|
|
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 {
|
|
304
|
+
CspNonceMeta,
|
|
305
|
+
InlineScript,
|
|
306
|
+
useNonce,
|
|
307
|
+
} from '@alxia/react-router/document';
|
|
308
|
+
import { themeBootScript } from '<your theme library>';
|
|
309
|
+
|
|
310
|
+
const THEME_BOOT = themeBootScript();
|
|
311
|
+
|
|
312
|
+
export function Layout({ children }: { children: React.ReactNode }) {
|
|
313
|
+
const nonce = useNonce();
|
|
314
|
+
return (
|
|
315
|
+
<html lang="en" suppressHydrationWarning>
|
|
316
|
+
<head>
|
|
317
|
+
<meta charSet="utf-8" />
|
|
318
|
+
<InlineScript code={THEME_BOOT} />
|
|
319
|
+
<CspNonceMeta />
|
|
320
|
+
<Meta />
|
|
321
|
+
<Links nonce={nonce} />
|
|
322
|
+
</head>
|
|
323
|
+
<body>
|
|
324
|
+
{children}
|
|
325
|
+
<ScrollRestoration />
|
|
326
|
+
<Scripts />
|
|
327
|
+
</body>
|
|
328
|
+
</html>
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export default function App() {
|
|
333
|
+
return <Outlet />;
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
`THEME_BOOT` is your theme library's own boot script. With `/entry`,
|
|
338
|
+
`<Scripts>` and `<ScrollRestoration>` take the nonce from `<ServerRouter nonce>`
|
|
339
|
+
and need no prop. `<Links>` does not suppress the hydration warning for its
|
|
340
|
+
critical-CSS `<style nonce>` in development, so pass it `useNonce()`, as above;
|
|
341
|
+
`useNonce()` also serves `InlineScript`, `CspNonceMeta` and any script or style
|
|
342
|
+
the app inserts. An entry of your own
|
|
343
|
+
wraps the router:
|
|
344
|
+
|
|
345
|
+
```tsx
|
|
346
|
+
// app/entry.server.tsx (inside handleRequest)
|
|
347
|
+
import { nonceOf } from '@alxia/react-router';
|
|
348
|
+
import { DocumentNonce } from '@alxia/react-router/document';
|
|
349
|
+
import { ServerRouter } from 'react-router';
|
|
350
|
+
|
|
351
|
+
<DocumentNonce nonce={nonceOf(loadContext)}>
|
|
352
|
+
<ServerRouter context={routerContext} url={request.url} />
|
|
353
|
+
</DocumentNonce>;
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
> The `InlineScript` code is written unescaped: keep it a constant of the
|
|
357
|
+
> app, never user input. It throws a `TypeError` on `</script` or `<!--`
|
|
358
|
+
> ([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)).
|
|
359
|
+
> A nonced page must never be made `public` by a route's `headers` export: a
|
|
360
|
+
> shared cache would hand the same nonce to every visitor.
|
|
361
|
+
|
|
362
|
+
### The page's language
|
|
363
|
+
|
|
364
|
+
`createPageLanguage` gives the root the page's language for `<html lang>` and
|
|
365
|
+
a route's `meta`, and `textDirection` the `dir` that goes with it. The server
|
|
366
|
+
side, negotiating the request's language from a cookie and `Accept-Language`,
|
|
367
|
+
is [`@alxia/language`](https://github.com/softistx/alxia/blob/develop/packages/language/README.md):
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
// app/server.ts
|
|
371
|
+
import { language } from '@alxia/language';
|
|
372
|
+
import { createServer } from '@alxia/react-router';
|
|
373
|
+
|
|
374
|
+
const server = createServer({
|
|
375
|
+
configure: (app) =>
|
|
376
|
+
app.use(language({ supported: ['en', 'fr', 'ar'], fallback: 'en', order: ['cookie', 'header'] })),
|
|
377
|
+
});
|
|
378
|
+
export default server;
|
|
379
|
+
declare module '@alxia/react-router' {
|
|
380
|
+
interface Register { server: typeof server }
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
// app/language.ts: one instance, imported by the root and the routes
|
|
386
|
+
import { createPageLanguage } from '@alxia/react-router/document';
|
|
387
|
+
|
|
388
|
+
export const { pageLanguage, metaLanguage, chooseLanguage, useChosenLanguage } =
|
|
389
|
+
createPageLanguage({ languages: ['en', 'fr', 'ar'], fallback: 'en' });
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
```tsx
|
|
393
|
+
// app/root.tsx
|
|
394
|
+
import { type AlxiaArgs, withAlxia } from '@alxia/react-router';
|
|
395
|
+
import { textDirection } from '@alxia/react-router/document';
|
|
396
|
+
import { useRouteLoaderData } from 'react-router';
|
|
397
|
+
import { metaLanguage, pageLanguage, useChosenLanguage } from './language';
|
|
398
|
+
import type { Route } from './+types/root';
|
|
399
|
+
|
|
400
|
+
const titles = { en: 'Home', fr: 'Accueil', ar: 'الرئيسية' };
|
|
401
|
+
|
|
402
|
+
// the root loader returns alxia.language, set by the middleware
|
|
403
|
+
export const loader = withAlxia(({ alxia }: Route.LoaderArgs & AlxiaArgs) => ({ language: alxia.language }));
|
|
404
|
+
|
|
405
|
+
export function Layout({ children }: { children: React.ReactNode }) {
|
|
406
|
+
const data = useRouteLoaderData<typeof loader>('root');
|
|
407
|
+
const language = pageLanguage(data?.language);
|
|
408
|
+
useChosenLanguage(); // re-renders the shell when the menu picks a language
|
|
409
|
+
return (
|
|
410
|
+
<html lang={language} dir={textDirection(language)} suppressHydrationWarning>
|
|
411
|
+
{/* … */}
|
|
412
|
+
</html>
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export const meta: Route.MetaFunction = ({ matches }) => [
|
|
417
|
+
{ title: titles[metaLanguage(matches)] },
|
|
418
|
+
];
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
- `pageLanguage(language)` is the supported language, else the fallback. In
|
|
422
|
+
the browser the one picked with `chooseLanguage(language)` wins, and the
|
|
423
|
+
last one seen stands in when the root's data is gone. On the server it
|
|
424
|
+
remembers nothing: one module serves every request.
|
|
425
|
+
- `metaLanguage(matches)` reads `matches[0].loaderData.language`.
|
|
426
|
+
- `useChosenLanguage()` is the picked language, or `undefined`; it
|
|
427
|
+
re-renders on a pick, and is `undefined` in the server render so
|
|
428
|
+
hydration matches.
|
|
429
|
+
- `textDirection(tag)` is `'rtl'` for `ar`, `fa`, `he`, `ur`, `ps`, `yi`,
|
|
430
|
+
`dv`, `ckb`, `sd` and `ug` (and the legacy `iw` and `ji`), else `'ltr'`, reading the base tag whatever its
|
|
431
|
+
case (`ar-EG`, `AR`). A script subtag is not read: `az-Arab` is `'ltr'`.
|
|
432
|
+
- A fallback outside `languages` throws
|
|
433
|
+
([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-)).
|
|
434
|
+
An error page keeps the root's data, so it keeps the language, except
|
|
435
|
+
for a URL that matches no route or a root loader that threw: there it
|
|
436
|
+
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)).
|
|
437
|
+
|
|
438
|
+
[More](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/document.md)
|
|
439
|
+
|
|
440
|
+
## The entry
|
|
441
|
+
|
|
442
|
+
`@alxia/react-router/entry` is `entry.server.tsx` for an app on Bun, server
|
|
443
|
+
only: it streams the shell, waits for the whole page for a bot and in SPA
|
|
444
|
+
mode, answers HEAD with no body, and gives the router the response's CSP
|
|
445
|
+
nonce (`nonceOf`) and `DocumentNonce`. It renders with the web stream
|
|
446
|
+
renderer, so no `@react-router/node` and no `node:stream`. `react-dom` and
|
|
447
|
+
`isbot` (needed only without an `isBot` option) are optional peers, which React Router's templates install.
|
|
448
|
+
|
|
449
|
+
```tsx
|
|
450
|
+
// app/entry.server.tsx
|
|
451
|
+
export { handleRequest as default, streamTimeout } from '@alxia/react-router/entry';
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
To change `streamTimeout`, the nonce, the bot check or the error report,
|
|
455
|
+
`createEntry({ streamTimeout, nonce, isBot, onError })` returns
|
|
456
|
+
`{ handleRequest, streamTimeout }`.
|
|
457
|
+
|
|
458
|
+
[More](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/document.md#the-entry)
|
|
459
|
+
|
|
301
460
|
## WebSockets
|
|
302
461
|
|
|
303
462
|
A `ws` route in `configure` connects under `react-router dev`, under
|
|
@@ -410,6 +569,9 @@ build, it adds:
|
|
|
410
569
|
- `build.target: 'esnext'`, where Vite's default targets browsers;
|
|
411
570
|
- in the build only, `resolve.noExternal: true`: every package is bundled
|
|
412
571
|
into `build/server/index.js`, so `build/` runs with no `node_modules`.
|
|
572
|
+
- `/entry` and `/document` inlined, in dev by an alias and in the build, so
|
|
573
|
+
they share the app's `react-router`, even when the package is listed in
|
|
574
|
+
`ssr.external`.
|
|
413
575
|
|
|
414
576
|
```ts
|
|
415
577
|
// app/server.ts: Bun's modules, bundled as they are
|
|
@@ -910,6 +1072,25 @@ From `@alxia/react-router/errors` (`react-router` is its only package import, an
|
|
|
910
1072
|
| `useJsonFetcher(key?, action?)`, `JsonBody` | a fetcher that posts JSON: `{ fetcher, post(body, to?), busy }`; `to` is the action of one call |
|
|
911
1073
|
| `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
1074
|
|
|
1075
|
+
From `@alxia/react-router/document`:
|
|
1076
|
+
|
|
1077
|
+
| export | |
|
|
1078
|
+
| --- | --- |
|
|
1079
|
+
| `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` |
|
|
1080
|
+
| `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 |
|
|
1081
|
+
| `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 |
|
|
1082
|
+
| `CspNonceMeta()` | `<meta property="csp-nonce" nonce>`, the nonce for what a UI library adds at runtime |
|
|
1083
|
+
| `createPageLanguage({ languages, fallback, browser? })` | `PageLanguage`: `pageLanguage`, `metaLanguage`, `chooseLanguage`, `useChosenLanguage`, for `<html lang>`, a route's `meta` and a language menu |
|
|
1084
|
+
| `PageLanguage`, `PageLanguageOptions` | the types of the instance and of its options |
|
|
1085
|
+
| `textDirection(language)` | `'ltr'` or `'rtl'` for `<html dir>`, from the base language |
|
|
1086
|
+
|
|
1087
|
+
From `@alxia/react-router/entry`:
|
|
1088
|
+
|
|
1089
|
+
| export | |
|
|
1090
|
+
| --- | --- |
|
|
1091
|
+
| `createEntry({ streamTimeout?, nonce?, isBot?, onError? })`, `EntryOptions`, `Entry` | `/entry`: `{ handleRequest, streamTimeout }` (`Entry`), `entry.server.tsx` for Bun on the web stream renderer |
|
|
1092
|
+
| `handleRequest`, `streamTimeout`, `HandleRequest` | `/entry`: the default entry (`createEntry()`), its `5_000` and the type of `handleRequest` (assignable to React Router's `ServerEntryModule['default']`) |
|
|
1093
|
+
|
|
913
1094
|
The `alxia-react-router` bin, run with `bunx`:
|
|
914
1095
|
|
|
915
1096
|
| command | |
|
|
@@ -922,6 +1103,7 @@ The `alxia-react-router` bin, run with `bunx`:
|
|
|
922
1103
|
- [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.
|
|
923
1104
|
- [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.
|
|
924
1105
|
- [Customising the server](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/customising-the-server.md): `createServer()`, and the escape hatches.
|
|
1106
|
+
- [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.
|
|
925
1107
|
- [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.
|
|
926
1108
|
- [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.
|
|
927
1109
|
- [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.
|
|
@@ -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=
|
|
119
|
-
//# sourceMappingURL=index-
|
|
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": "
|
|
9
|
+
"debugId": "705C996D7C991BF164756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// src/document/inline-script.ts
|
|
2
|
+
import { createElement as createElement2 } from "react";
|
|
3
|
+
|
|
4
|
+
// src/document/nonce.ts
|
|
5
|
+
import {
|
|
6
|
+
createContext,
|
|
7
|
+
createElement,
|
|
8
|
+
useContext
|
|
9
|
+
} from "react";
|
|
10
|
+
var KEY = Symbol.for("@alxia/react-router:document-nonce@1");
|
|
11
|
+
var registry = globalThis;
|
|
12
|
+
registry[KEY] ??= createContext(undefined);
|
|
13
|
+
var NonceContext = registry[KEY];
|
|
14
|
+
function DocumentNonce2({
|
|
15
|
+
nonce,
|
|
16
|
+
children
|
|
17
|
+
}) {
|
|
18
|
+
return createElement(NonceContext, { value: nonce }, children);
|
|
19
|
+
}
|
|
20
|
+
var HEAD_NONCE = 'head script[nonce], head meta[property="csp-nonce"]';
|
|
21
|
+
var documentNonce;
|
|
22
|
+
function useNonce2() {
|
|
23
|
+
const serverNonce = useContext(NonceContext);
|
|
24
|
+
if (typeof document === "undefined")
|
|
25
|
+
return serverNonce;
|
|
26
|
+
documentNonce ??= (document.querySelector(HEAD_NONCE) ?? document.querySelector("script[nonce]"))?.nonce ?? "";
|
|
27
|
+
return documentNonce || undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/document/inline-script.ts
|
|
31
|
+
var BREAKS_OUT = /<\/script|<!--/i;
|
|
32
|
+
function InlineScript2({ code, nonce }) {
|
|
33
|
+
if (BREAKS_OUT.test(code)) {
|
|
34
|
+
throw new TypeError('InlineScript(): the code contains "</script" or "<!--", which ends or confuses the script element; escape it, such as "<\\/script" inside a string');
|
|
35
|
+
}
|
|
36
|
+
const fromDocument = useNonce2();
|
|
37
|
+
return createElement2("script", {
|
|
38
|
+
nonce: nonce ?? fromDocument,
|
|
39
|
+
dangerouslySetInnerHTML: { __html: code }
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function CspNonceMeta2() {
|
|
43
|
+
return createElement2("meta", {
|
|
44
|
+
property: "csp-nonce",
|
|
45
|
+
nonce: useNonce2()
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
// src/document/page-language.ts
|
|
49
|
+
import { useSyncExternalStore } from "react";
|
|
50
|
+
|
|
51
|
+
// src/document/language-store.ts
|
|
52
|
+
function createLanguageStore() {
|
|
53
|
+
const listeners = new Set;
|
|
54
|
+
let chosen;
|
|
55
|
+
return {
|
|
56
|
+
subscribe(listener) {
|
|
57
|
+
listeners.add(listener);
|
|
58
|
+
return () => {
|
|
59
|
+
listeners.delete(listener);
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
getSnapshot: () => chosen,
|
|
63
|
+
choose(language) {
|
|
64
|
+
chosen = language;
|
|
65
|
+
for (const listener of listeners)
|
|
66
|
+
listener();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/document/page-language.ts
|
|
72
|
+
function createPageLanguage2(options) {
|
|
73
|
+
const { languages, fallback } = options;
|
|
74
|
+
if (!languages.includes(fallback)) {
|
|
75
|
+
throw new TypeError(`createPageLanguage(): the fallback "${fallback}" is not one of the languages; got ${JSON.stringify(languages)}.`);
|
|
76
|
+
}
|
|
77
|
+
const isBrowser = options.browser ?? (() => typeof document !== "undefined");
|
|
78
|
+
const store = createLanguageStore();
|
|
79
|
+
let last;
|
|
80
|
+
function pageLanguage(language) {
|
|
81
|
+
const supported = languages.find((known) => known === language);
|
|
82
|
+
if (!isBrowser())
|
|
83
|
+
return supported ?? fallback;
|
|
84
|
+
const current = store.getSnapshot() ?? supported;
|
|
85
|
+
if (current)
|
|
86
|
+
last = current;
|
|
87
|
+
return last ?? fallback;
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
pageLanguage,
|
|
91
|
+
metaLanguage(matches) {
|
|
92
|
+
const root = matches[0];
|
|
93
|
+
return pageLanguage(root?.loaderData?.language);
|
|
94
|
+
},
|
|
95
|
+
chooseLanguage: store.choose,
|
|
96
|
+
useChosenLanguage: () => useSyncExternalStore(store.subscribe, store.getSnapshot, () => {
|
|
97
|
+
return;
|
|
98
|
+
})
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// src/document/text-direction.ts
|
|
102
|
+
var RTL = new Set([
|
|
103
|
+
"ar",
|
|
104
|
+
"fa",
|
|
105
|
+
"he",
|
|
106
|
+
"ur",
|
|
107
|
+
"ps",
|
|
108
|
+
"yi",
|
|
109
|
+
"dv",
|
|
110
|
+
"ckb",
|
|
111
|
+
"sd",
|
|
112
|
+
"ug",
|
|
113
|
+
"iw",
|
|
114
|
+
"ji"
|
|
115
|
+
]);
|
|
116
|
+
function textDirection2(language) {
|
|
117
|
+
const base = language.split(/[-_]/, 1)[0]?.toLowerCase() ?? "";
|
|
118
|
+
return RTL.has(base) ? "rtl" : "ltr";
|
|
119
|
+
}
|
|
120
|
+
export { DocumentNonce2, useNonce2, InlineScript2, CspNonceMeta2, createPageLanguage2, textDirection2 };
|
|
121
|
+
|
|
122
|
+
//# debugId=49CF940BC2A4321264756E2164756E21
|
|
123
|
+
//# sourceMappingURL=index-y6vpnjr3.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/document/inline-script.ts", "../src/document/nonce.ts", "../src/document/page-language.ts", "../src/document/language-store.ts", "../src/document/text-direction.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { createElement, type ReactElement } from 'react';\nimport { useNonce } from './nonce';\n\nconst BREAKS_OUT = /<\\/script|<!--/i;\n\nexport interface InlineScriptProps {\n\t/**\n\t * The script's text, rendered as it is: never escaped, so a `</script`\n\t * or a `<!--` in it is refused (see `InlineScript`).\n\t */\n\tcode: string;\n\t/** The script's nonce; `useNonce()`'s by default. */\n\tnonce?: string | undefined;\n}\n\n/**\n * An inline `<script>` carrying the page's CSP nonce, on the server and in\n * the browser, so hydration finds the attribute it rendered. The code is a\n * constant of the app (a theme's boot script): never put user input in it.\n * With no nonce anywhere, it renders no `nonce` attribute, and the policy\n * must then allow inline scripts some other way.\n *\n * Throws a `TypeError` when the code holds `</script` or `<!--`, which end or\n * confuse the element whatever the nonce: escape them in a string (`<\\/script`).\n */\nexport function InlineScript({ code, nonce }: InlineScriptProps): ReactElement {\n\tif (BREAKS_OUT.test(code)) {\n\t\tthrow new TypeError(\n\t\t\t'InlineScript(): the code contains \"</script\" or \"<!--\", which ends or confuses the script element; escape it, such as \"<\\\\/script\" inside a string',\n\t\t);\n\t}\n\tconst fromDocument = useNonce();\n\treturn createElement('script', {\n\t\tnonce: nonce ?? fromDocument,\n\t\tdangerouslySetInnerHTML: { __html: code },\n\t});\n}\n\n/**\n * `<meta property=\"csp-nonce\" nonce=\"…\">`: the page's nonce for the scripts\n * and styles a library adds at runtime (a UI kit reads it from the DOM).\n */\nexport function CspNonceMeta(): ReactElement {\n\treturn createElement('meta', {\n\t\tproperty: 'csp-nonce',\n\t\tnonce: useNonce(),\n\t});\n}\n",
|
|
6
|
+
"import {\n\tcreateContext,\n\tcreateElement,\n\ttype ReactNode,\n\tuseContext,\n} from 'react';\n\n/**\n * One context per process, whichever copy of this module asks: a package\n * loaded natively that imports `/document` would otherwise get a second\n * context, and its `useNonce()` would never see the entry's `DocumentNonce`.\n * `/document` imports `react` alone, so the few lines of `src/context.ts`'s\n * singleton are written here. The `@1` suffix is the shape of what is stored\n * under the key: a version that changes it bumps the suffix, never the name.\n */\nconst KEY = Symbol.for('@alxia/react-router:document-nonce@1');\nconst registry = globalThis as Record<symbol, unknown>;\nregistry[KEY] ??= createContext<string | undefined>(undefined);\nconst NonceContext = registry[KEY] as ReturnType<\n\ttypeof createContext<string | undefined>\n>;\n\nexport interface DocumentNonceProps {\n\t/** This response's CSP nonce: `nonceOf(loadContext)` in the entry. */\n\tnonce: string | undefined;\n\tchildren?: ReactNode | undefined;\n}\n\n/**\n * Gives the server render this response's CSP nonce, for `useNonce()` and\n * the components built on it. The entry wraps the router in it, so every\n * boundary reads the nonce, an error page after an action included (it\n * runs no loader). Never put the nonce in loader data: the browser hides\n * it from the page, and loader data would print it in the hydration script\n * and every `.data` answer.\n */\nexport function DocumentNonce({\n\tnonce,\n\tchildren,\n}: DocumentNonceProps): ReactNode {\n\treturn createElement(NonceContext, { value: nonce }, children);\n}\n\nconst HEAD_NONCE = 'head script[nonce], head meta[property=\"csp-nonce\"]';\n\nlet documentNonce: string | undefined;\n\n/**\n * The page's CSP nonce, for what the root renders: an inline script, and\n * React Router's `<Links>`, `<Scripts>` and `<ScrollRestoration>`. On the\n * server, the one `DocumentNonce` provides. In the browser, the one the\n * document carries: React Router's client knows no nonce, and React would\n * find `nonce` on the server's elements and none in its own (a hydration\n * mismatch). It is read from the DOM property of the first script or\n * `csp-nonce` meta in the `<head>` (the browser hides the attribute, not\n * the property), else of the first `script[nonce]` anywhere, once for the\n * document. That first read is cached per module copy, so a test with a\n * DOM keeps it across tests: reset modules, or render the server path with\n * `DocumentNonce`.\n *\n * \"In the browser\" means a global `document`. A server that installs a DOM\n * shim on `globalThis` takes the browser path and finds no nonce: it must not\n * render this hook that way.\n */\nexport function useNonce(): string | undefined {\n\tconst serverNonce = useContext(NonceContext);\n\tif (typeof document === 'undefined') return serverNonce;\n\tdocumentNonce ??=\n\t\t(\n\t\t\tdocument.querySelector<HTMLElement>(HEAD_NONCE) ??\n\t\t\tdocument.querySelector<HTMLElement>('script[nonce]')\n\t\t)?.nonce ?? '';\n\treturn documentNonce || undefined;\n}\n",
|
|
7
|
+
"import { useSyncExternalStore } from 'react';\nimport { createLanguageStore } from './language-store';\n\nexport interface PageLanguageOptions<L extends string> {\n\t/** The languages the app serves. */\n\treadonly languages: readonly L[];\n\t/** The language of a page whose root data gives none; one of `languages`. */\n\treadonly fallback: NoInfer<L>;\n\t/**\n\t * Whether this is a browser. Default: `typeof document !== 'undefined'`,\n\t * a global `document`: a server with a DOM shim on `globalThis` must pass\n\t * `() => false`, or the instance remembers a language across requests.\n\t */\n\treadonly browser?: (() => boolean) | undefined;\n}\n\nexport interface PageLanguage<L extends string> {\n\t/**\n\t * The page's language from the root loader's `language`. In the browser,\n\t * the one picked with `chooseLanguage` wins, and the last one seen stands\n\t * in when the root's data is gone (a failed navigation takes the root's\n\t * with it): the error page keeps the page's language, not the fallback's.\n\t * On the server it remembers nothing, since one module serves every request.\n\t */\n\tpageLanguage(language: string | undefined): L;\n\t/** The page's language in a route's `meta`, from `matches[0].loaderData.language`. */\n\tmetaLanguage(matches: ReadonlyArray<unknown>): L;\n\t/**\n\t * Records the language picked in a language menu: from then on the page's\n\t * language in the browser, until the next document. Re-renders every\n\t * `useChosenLanguage()`.\n\t */\n\tchooseLanguage(language: L): void;\n\t/** The language picked in this document, if any; re-renders on a new pick. */\n\tuseChosenLanguage(): L | undefined;\n}\n\n/**\n * The page's language for a root route: `<html lang>`, a route's `meta`, and\n * the language menu. An instance, not module state: keep one per app, in a\n * module both the root and the routes import.\n */\nexport function createPageLanguage<const L extends string>(\n\toptions: PageLanguageOptions<L>,\n): PageLanguage<L> {\n\tconst { languages, fallback } = options;\n\tif (!languages.includes(fallback)) {\n\t\tthrow new TypeError(\n\t\t\t`createPageLanguage(): the fallback \"${fallback}\" is not one of the languages; got ${JSON.stringify(languages)}.`,\n\t\t);\n\t}\n\tconst isBrowser = options.browser ?? (() => typeof document !== 'undefined');\n\tconst store = createLanguageStore<L>();\n\tlet last: L | undefined;\n\n\tfunction pageLanguage(language: string | undefined): L {\n\t\tconst supported = languages.find((known) => known === language);\n\t\tif (!isBrowser()) return supported ?? fallback;\n\t\tconst current = store.getSnapshot() ?? supported;\n\t\tif (current) last = current;\n\t\treturn last ?? fallback;\n\t}\n\n\treturn {\n\t\tpageLanguage,\n\t\tmetaLanguage(matches) {\n\t\t\tconst root = matches[0] as\n\t\t\t\t| { loaderData?: { language?: string } }\n\t\t\t\t| undefined;\n\t\t\treturn pageLanguage(root?.loaderData?.language);\n\t\t},\n\t\tchooseLanguage: store.choose,\n\t\tuseChosenLanguage: () =>\n\t\t\tuseSyncExternalStore(store.subscribe, store.getSnapshot, () => undefined),\n\t};\n}\n",
|
|
8
|
+
"/** The picked language and who listens to it: internal to `createPageLanguage`. */\nexport interface LanguageStore<L extends string> {\n\tsubscribe(listener: () => void): () => void;\n\tgetSnapshot(): L | undefined;\n\tchoose(language: L): void;\n}\n\nexport function createLanguageStore<L extends string>(): LanguageStore<L> {\n\tconst listeners = new Set<() => void>();\n\tlet chosen: L | undefined;\n\treturn {\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\tgetSnapshot: () => chosen,\n\t\tchoose(language) {\n\t\t\tchosen = language;\n\t\t\tfor (const listener of listeners) listener();\n\t\t},\n\t};\n}\n",
|
|
9
|
+
"/**\n * Base languages written right to left. A fixed list: a language is added\n * here when it needs it.\n */\nconst RTL = new Set([\n\t'ar',\n\t'fa',\n\t'he',\n\t'ur',\n\t'ps',\n\t'yi',\n\t'dv',\n\t'ckb',\n\t'sd',\n\t'ug',\n\t// Legacy codes of Hebrew and Yiddish.\n\t'iw',\n\t'ji',\n]);\n\n/**\n * The `dir` of a language tag: `'rtl'` for a right-to-left base language,\n * `'ltr'` for the rest. The base tag is read whatever its case, so `ar-EG`\n * and `AR` are `'rtl'` and `en-US` is `'ltr'`.\n *\n * Known gap: a script subtag is not read, so `az-Arab` (Azerbaijani in the\n * Arabic script) is `'ltr'`, as `az` is.\n */\nexport function textDirection(language: string): 'ltr' | 'rtl' {\n\tconst base = language.split(/[-_]/, 1)[0]?.toLowerCase() ?? '';\n\treturn RTL.has(base) ? 'rtl' : 'ltr';\n}\n"
|
|
10
|
+
],
|
|
11
|
+
"mappings": "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",
|
|
12
|
+
"debugId": "49CF940BC2A4321264756E2164756E21",
|
|
13
|
+
"names": []
|
|
14
|
+
}
|
package/dist/context.d.ts
CHANGED
|
@@ -5,13 +5,6 @@
|
|
|
5
5
|
import type { Alxia, AnyAlxia, ContextOf, RegisteredBase } from '@alxia/core';
|
|
6
6
|
import { RouterContextProvider } from 'react-router';
|
|
7
7
|
import type { ReactRouterServer } from './server';
|
|
8
|
-
/**
|
|
9
|
-
* The key `reactRouter()` sets on React Router's context provider, on every
|
|
10
|
-
* request, to what alxia's middlewares built for it. It lives in this package, so
|
|
11
|
-
* it is one object whichever way the server was built or loaded: a key made
|
|
12
|
-
* in the app's own `app/` folder is copied into React Router's build, and
|
|
13
|
-
* the server that imports it separately sets a different one.
|
|
14
|
-
*/
|
|
15
8
|
export declare const alxiaContext: import("react-router").RouterContext<unknown>;
|
|
16
9
|
/**
|
|
17
10
|
* Names the server whose context `alxiaOf(context)` reads when given no
|
package/dist/context.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAiB,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAiB,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAkClD,eAAO,MAAM,YAAY,+CAAiB,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,WAAW,QAAQ;CAAG;AAE5B,iDAAiD;AACjD,MAAM,MAAM,KAAK,CAAC,MAAM,IACvB,MAAM,SAAS,iBAAiB,CAAC,MAAM,GAAG,CAAC,GAAG,GAAG,GAAG,MAAM,CAAC;AAE5D;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,KAAK,CAClC;IACC,QAAQ,CAAC,6EAA6E,EAAE,KAAK,CAAC;CAC9F,EACD,EAAE,CACF,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,YAAY,CACvB,CAAC,EACD,IAAI,SAAS,QAAQ,GAAG,cAAc,IACnC,CAAC,SAAS;IACb,QAAQ,CAAC,MAAM,EAAE,MAAM,MAAM,CAAC;CAC9B,GACE,MAAM,SAAS,QAAQ,GAAG,iBAAiB,CAAC,QAAQ,CAAC,GACpD,KAAK,CAAC,MAAM,CAAC,GACb,eAAe,GAChB,IAAI,CAAC;AAER,6HAA6H;AAC7H,MAAM,MAAM,aAAa,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;AAEnD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,OAAO,CACtB,GAAG,SAAS,QAAQ,GAAG,iBAAiB,CAAC,QAAQ,CAAC,GAAG,aAAa,EACjE,OAAO,EAAE,YAAY,GAAG,cAAc,CAAC,GAAG,CAAC,CAQ5C;AAED;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,QAAQ,CAAC,qBAAqB,CAAC,EAAE,KAAK,CAAC,CAAC;AAExE,2FAA2F;AAC3F,MAAM,MAAM,cAAc,CACzB,GAAG,SAAS,QAAQ,GAAG,iBAAiB,CAAC,QAAQ,CAAC,GAAG,aAAa,IAC/D,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG;IAC3B,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,OAAO,QAAQ,cAAc,CAAC;IAC7B,UAAU,qBAAqB;QAC9B;;;;;;;;;;;;;;;;WAgBG;QACH,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;KAC/B;CACD"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@alxia/react-router/document`: what a root route renders for the page's
|
|
3
|
+
* CSP nonce, on the server and in the browser. `react` is its only import
|
|
4
|
+
* (an optional peer of the package), and nothing runs at the top level but
|
|
5
|
+
* `createContext`.
|
|
6
|
+
*/
|
|
7
|
+
export { CspNonceMeta, InlineScript, type InlineScriptProps, } from './inline-script';
|
|
8
|
+
export { DocumentNonce, type DocumentNonceProps, useNonce, } from './nonce';
|
|
9
|
+
export { createPageLanguage, type PageLanguage, type PageLanguageOptions, } from './page-language';
|
|
10
|
+
export { textDirection } from './text-direction';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/document/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACN,YAAY,EACZ,YAAY,EACZ,KAAK,iBAAiB,GACtB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACN,aAAa,EACb,KAAK,kBAAkB,EACvB,QAAQ,GACR,MAAM,SAAS,CAAC;AACjB,OAAO,EACN,kBAAkB,EAClB,KAAK,YAAY,EACjB,KAAK,mBAAmB,GACxB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DocumentNonce2,
|
|
3
|
+
useNonce2,
|
|
4
|
+
InlineScript2,
|
|
5
|
+
CspNonceMeta2,
|
|
6
|
+
createPageLanguage2,
|
|
7
|
+
textDirection2
|
|
8
|
+
} from "../chunks/index-y6vpnjr3.js";
|
|
9
|
+
export {
|
|
10
|
+
CspNonceMeta2 as CspNonceMeta,
|
|
11
|
+
DocumentNonce2 as DocumentNonce,
|
|
12
|
+
InlineScript2 as InlineScript,
|
|
13
|
+
createPageLanguage2 as createPageLanguage,
|
|
14
|
+
textDirection2 as textDirection,
|
|
15
|
+
useNonce2 as useNonce
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
//# debugId=3F5BF8C287845B0F64756E2164756E21
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type ReactElement } from 'react';
|
|
2
|
+
export interface InlineScriptProps {
|
|
3
|
+
/**
|
|
4
|
+
* The script's text, rendered as it is: never escaped, so a `</script`
|
|
5
|
+
* or a `<!--` in it is refused (see `InlineScript`).
|
|
6
|
+
*/
|
|
7
|
+
code: string;
|
|
8
|
+
/** The script's nonce; `useNonce()`'s by default. */
|
|
9
|
+
nonce?: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* An inline `<script>` carrying the page's CSP nonce, on the server and in
|
|
13
|
+
* the browser, so hydration finds the attribute it rendered. The code is a
|
|
14
|
+
* constant of the app (a theme's boot script): never put user input in it.
|
|
15
|
+
* With no nonce anywhere, it renders no `nonce` attribute, and the policy
|
|
16
|
+
* must then allow inline scripts some other way.
|
|
17
|
+
*
|
|
18
|
+
* Throws a `TypeError` when the code holds `</script` or `<!--`, which end or
|
|
19
|
+
* confuse the element whatever the nonce: escape them in a string (`<\/script`).
|
|
20
|
+
*/
|
|
21
|
+
export declare function InlineScript({ code, nonce }: InlineScriptProps): ReactElement;
|
|
22
|
+
/**
|
|
23
|
+
* `<meta property="csp-nonce" nonce="…">`: the page's nonce for the scripts
|
|
24
|
+
* and styles a library adds at runtime (a UI kit reads it from the DOM).
|
|
25
|
+
*/
|
|
26
|
+
export declare function CspNonceMeta(): ReactElement;
|
|
27
|
+
//# sourceMappingURL=inline-script.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inline-script.d.ts","sourceRoot":"","sources":["../../src/document/inline-script.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,OAAO,CAAC;AAKzD,MAAM,WAAW,iBAAiB;IACjC;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,iBAAiB,GAAG,YAAY,CAW7E;AAED;;;GAGG;AACH,wBAAgB,YAAY,IAAI,YAAY,CAK3C"}
|