@nifrajs/web-preact 3.5.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +3 -17
- package/dist/client.js.map +1 -1
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +3 -2
- package/dist/compose.js.map +1 -1
- package/dist/i18n.d.ts +19 -6
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +29 -6
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -2
- package/dist/index.js.map +1 -1
- package/dist/route-props.d.ts +4 -0
- package/dist/route-props.d.ts.map +1 -0
- package/dist/route-props.js +29 -0
- package/dist/route-props.js.map +1 -0
- package/dist/router.d.ts +14 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +24 -1
- package/dist/router.js.map +1 -1
- package/package.json +8 -5
- package/src/client.ts +3 -17
- package/src/compose.ts +11 -2
- package/src/i18n.ts +51 -9
- package/src/index.ts +41 -2
- package/src/route-props.ts +34 -0
- package/src/router.ts +33 -2
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAgBnE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE1C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,OAAO,CAAA;AAExC,oGAAoG;AACpG,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,GAAG,IAAI,CAE/F;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAQ7D"}
|
package/dist/client.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// `/client`, not the root: the root's graph carries the server, and Vite's dev server evaluates it
|
|
2
|
-
// instead of tree-shaking it - which broke hydration before the browser ran a line of app code.
|
|
3
|
-
import { searchOfChain } from "@nifrajs/web/client";
|
|
4
1
|
/**
|
|
5
2
|
* @nifrajs/web-preact/client - Preact client runtime. `hydrate` hydrates a single SSR'd route;
|
|
6
3
|
* `mountRouter` hydrates a stateful Router that subscribes to the agnostic store (via
|
|
@@ -12,6 +9,7 @@ import { h, hydrate as preactHydrate } from "preact";
|
|
|
12
9
|
import { useSyncExternalStore } from "preact/compat";
|
|
13
10
|
import { compose } from "./compose.js";
|
|
14
11
|
import { setMountedRouter } from "./fetcher.js";
|
|
12
|
+
import { routeProps } from "./route-props.js";
|
|
15
13
|
// The `_error` boundary chain element - defined in its own (DOM-free) module, re-exported here so
|
|
16
14
|
// nifra's client codegen resolves it from `@nifrajs/web-preact/client` alongside `mountRouter`.
|
|
17
15
|
export { errorBoundary } from "./error.js";
|
|
@@ -39,23 +37,11 @@ export function hydrate(chain, props, container) {
|
|
|
39
37
|
* (no `getServerSnapshot`); `router.snapshot` is deterministic, so hydration matches the SSR markup.
|
|
40
38
|
*/
|
|
41
39
|
export function mountRouter(options) {
|
|
42
|
-
const { router, routes, searchSchemas, container } = options;
|
|
40
|
+
const { router, routes, searchSchemas, matchChains, container } = options;
|
|
43
41
|
setMountedRouter(router); // expose it to useFetcher/useFetchers (same page, client-only)
|
|
44
42
|
const Router = () => {
|
|
45
43
|
const state = useSyncExternalStore(router.subscribe, router.snapshot);
|
|
46
|
-
|
|
47
|
-
// the server ran), so `useSearch` reads an identical value and hydrates with no drift.
|
|
48
|
-
const q = state.path.indexOf("?");
|
|
49
|
-
const rawSearch = q === -1 ? "" : state.path.slice(q);
|
|
50
|
-
return compose(routes[state.routeId] ?? [], {
|
|
51
|
-
data: state.data,
|
|
52
|
-
actionData: state.actionData,
|
|
53
|
-
pending: state.pending,
|
|
54
|
-
search: searchOfChain(searchSchemas?.[state.routeId] ?? [], rawSearch),
|
|
55
|
-
// The in-flight submission (for optimistic UI) - spread only when present.
|
|
56
|
-
...(state.submission ? { submission: state.submission } : {}),
|
|
57
|
-
...(state.boundaries !== undefined ? { boundaries: state.boundaries } : {}),
|
|
58
|
-
});
|
|
44
|
+
return compose(routes[state.routeId] ?? [], routeProps(state, searchSchemas, matchChains));
|
|
59
45
|
};
|
|
60
46
|
preactHydrate(h(Router, null), container);
|
|
61
47
|
}
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA;;;;;;GAMG;AACH,OAAO,EAA0B,CAAC,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,QAAQ,CAAA;AAC5E,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAE7C,kGAAkG;AAClG,gGAAgG;AAChG,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE1C;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,CAAA;AAExC,oGAAoG;AACpG,MAAM,UAAU,OAAO,CAAC,KAAyB,EAAE,KAAkB,EAAE,SAAkB;IACvF,aAAa,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,SAAoB,CAAC,CAAA;AAC5D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,OAA2B;IACrD,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,OAAO,CAAA;IACzE,gBAAgB,CAAC,MAAM,CAAC,CAAA,CAAC,+DAA+D;IACxF,MAAM,MAAM,GAAsB,GAAG,EAAE;QACrC,MAAM,KAAK,GAAG,oBAAoB,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAA;QACrE,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,UAAU,CAAC,KAAK,EAAE,aAAa,EAAE,WAAW,CAAC,CAAC,CAAA;IAC5F,CAAC,CAAA;IACD,aAAa,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,SAAoB,CAAC,CAAA;AACtD,CAAC"}
|
package/dist/compose.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAgE,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAA;
|
|
1
|
+
{"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAgE,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAA;AAoBjG;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,EAAE,KAAK,EAAE,WAAW,GAAG,KAAK,CA2B5E"}
|
package/dist/compose.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createElement, h } from "preact";
|
|
2
|
-
import { SearchContext } from "./router.js";
|
|
2
|
+
import { RenderPropsContext, SearchContext } from "./router.js";
|
|
3
3
|
// Frozen empty search so a render with no search context has a stable provider value.
|
|
4
4
|
const EMPTY_SEARCH = Object.freeze({});
|
|
5
5
|
// Preact's `createElement` overloads drop a typed `Context.Provider`'s required `value` under
|
|
6
6
|
// exactOptionalPropertyTypes (same issue i18n's provider hits); invoke it through a precisely-typed
|
|
7
7
|
// factory that states the real `{ value }` + children contract rather than scatter `any`.
|
|
8
8
|
const createSearchProvider = createElement;
|
|
9
|
+
const createPropsProvider = createElement;
|
|
9
10
|
/**
|
|
10
11
|
* Fold a layout chain (outermost layout → page) into a single Preact tree: the page (innermost)
|
|
11
12
|
* receives `props` (the loader data); each layout wraps the child via its `children` (Preact passes
|
|
@@ -32,6 +33,6 @@ export function compose(chain, props) {
|
|
|
32
33
|
...(props.boundaries !== undefined ? { boundaries: props.boundaries } : {}),
|
|
33
34
|
}, node);
|
|
34
35
|
}
|
|
35
|
-
return createSearchProvider(SearchContext.Provider, { value: props.search ?? EMPTY_SEARCH }, node);
|
|
36
|
+
return createSearchProvider(SearchContext.Provider, { value: props.search ?? EMPTY_SEARCH }, createPropsProvider(RenderPropsContext.Provider, { value: props }, node));
|
|
36
37
|
}
|
|
37
38
|
//# sourceMappingURL=compose.js.map
|
package/dist/compose.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compose.js","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"AACA,OAAO,EAA8C,aAAa,EAAE,CAAC,EAAc,MAAM,QAAQ,CAAA;AACjG,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"compose.js","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"AACA,OAAO,EAA8C,aAAa,EAAE,CAAC,EAAc,MAAM,QAAQ,CAAA;AACjG,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAE/D,sFAAsF;AACtF,MAAM,YAAY,GAAsC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;AAEzE,8FAA8F;AAC9F,oGAAoG;AACpG,0FAA0F;AAC1F,MAAM,oBAAoB,GAAG,aAInB,CAAA;AACV,MAAM,mBAAmB,GAAG,aAIlB,CAAA;AAEV;;;;;;;GAOG;AACH,MAAM,UAAU,OAAO,CAAC,KAAyB,EAAE,KAAkB;IACnE,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAA;IAC7B,iGAAiG;IACjG,8FAA8F;IAC9F,mGAAmG;IACnG,gGAAgG;IAChG,2FAA2F;IAC3F,IAAI,IAAI,GAAU,CAAC,CAAC,KAAK,CAAC,IAAI,CAA+B,EAAE,KAAK,CAAU,CAAA;IAC9E,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACnC,wFAAwF;QACxF,6FAA6F;QAC7F,mGAAmG;QACnG,kEAAkE;QAClE,IAAI,GAAG,CAAC,CACN,KAAK,CAAC,CAAC,CAAqC,EAC5C;YACE,IAAI,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI;YACnC,GAAG,CAAC,KAAK,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC5E,EACD,IAAI,CACI,CAAA;IACZ,CAAC;IACD,OAAO,oBAAoB,CACzB,aAAa,CAAC,QAAQ,EACtB,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,IAAI,YAAY,EAAE,EACvC,mBAAmB,CAAC,kBAAkB,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,IAAI,CAAC,CACzE,CAAA;AACH,CAAC"}
|
package/dist/i18n.d.ts
CHANGED
|
@@ -5,16 +5,29 @@
|
|
|
5
5
|
* and the client rebuilds the same formatter from the same props (no mismatch). Uses `preact` +
|
|
6
6
|
* `preact/hooks` - no JSX.
|
|
7
7
|
*/
|
|
8
|
-
import { type Formatter, type
|
|
8
|
+
import { type Formatter, type FormatterOptions, type MessageKey, type RegisteredMessages, type Translation } from "@nifrajs/i18n";
|
|
9
9
|
import { type ComponentChildren, type VNode } from "preact";
|
|
10
|
-
export interface I18nProviderProps {
|
|
10
|
+
export interface I18nProviderProps extends FormatterOptions {
|
|
11
11
|
readonly locale: string;
|
|
12
|
-
|
|
12
|
+
/** The catalog for `locale`, checked against the registered catalog type when one is declared. */
|
|
13
|
+
readonly messages: Translation;
|
|
13
14
|
readonly children?: ComponentChildren;
|
|
14
15
|
}
|
|
15
|
-
/** Provide a {@link Formatter} (built from `locale` + `messages
|
|
16
|
-
* `
|
|
16
|
+
/** Provide a {@link Formatter} (built from `locale` + `messages`, with the optional `fallback`,
|
|
17
|
+
* `onMissing`, `timeZone` and `numberingSystem` of `createFormatter`) to the subtree. Memoized
|
|
18
|
+
* on those props, so switching locale rebuilds it and re-renders consumers; formatters are cached by
|
|
19
|
+
* catalog identity, so an inline `fallback={[en]}` still yields the same instance. */
|
|
17
20
|
export declare function I18nProvider(props: I18nProviderProps): VNode;
|
|
18
|
-
/** Read the current {@link Formatter} (`{ locale, t, n, d }`). Throws if no `<I18nProvider>` is above. */
|
|
21
|
+
/** Read the current {@link Formatter} (`{ locale, t, get, n, d }`). Throws if no `<I18nProvider>` is above. */
|
|
19
22
|
export declare function useT(): Formatter;
|
|
23
|
+
/** Tag handlers for {@link rich}, by tag name: each receives its tag's content as one node. */
|
|
24
|
+
export type RichTags = Readonly<Record<string, (content: ComponentChildren) => ComponentChildren>>;
|
|
25
|
+
/**
|
|
26
|
+
* The message at `key` with its tags rendered by `tags`, as Preact nodes - no HTML, no
|
|
27
|
+
* `dangerouslySetInnerHTML`. `"Read the <link>terms</link>"` with
|
|
28
|
+
* `{ link: (content) => <a href="/terms">{content}</a> }` renders the link around "terms"; a tag with
|
|
29
|
+
* no handler renders its content as text, `<br/>` is a `<br>`, and interpolated values are always
|
|
30
|
+
* text. See `@nifrajs/i18n/rich`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function rich<M extends object = RegisteredMessages>(formatter: Formatter<M>, key: MessageKey<M>, tags?: RichTags, vars?: Readonly<Record<string, unknown>>): ComponentChildren;
|
|
20
33
|
//# sourceMappingURL=i18n.d.ts.map
|
package/dist/i18n.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,KAAK,WAAW,EACjB,MAAM,eAAe,CAAA;AAEtB,OAAO,EAAE,KAAK,iBAAiB,EAA0C,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAA;AAenG,MAAM,WAAW,iBAAkB,SAAQ,gBAAgB;IACzD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,kGAAkG;IAClG,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAA;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,iBAAiB,CAAA;CACtC;AAED;;;sFAGsF;AACtF,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,KAAK,CAO5D;AAED,+GAA+G;AAC/G,wBAAgB,IAAI,IAAI,SAAS,CAMhC;AAED,+FAA+F;AAC/F,MAAM,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,iBAAiB,CAAC,CAAC,CAAA;AAalG;;;;;;GAMG;AACH,wBAAgB,IAAI,CAAC,CAAC,SAAS,MAAM,GAAG,kBAAkB,EACxD,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,EACvB,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC,EAClB,IAAI,CAAC,EAAE,QAAQ,EACf,IAAI,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACvC,iBAAiB,CAEnB"}
|
package/dist/i18n.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* and the client rebuilds the same formatter from the same props (no mismatch). Uses `preact` +
|
|
6
6
|
* `preact/hooks` - no JSX.
|
|
7
7
|
*/
|
|
8
|
-
import { createFormatter } from "@nifrajs/i18n";
|
|
9
|
-
import {
|
|
8
|
+
import { createFormatter, } from "@nifrajs/i18n";
|
|
9
|
+
import { renderRich } from "@nifrajs/i18n/rich";
|
|
10
|
+
import { createContext, createElement, Fragment } from "preact";
|
|
10
11
|
import { useContext, useMemo } from "preact/hooks";
|
|
11
12
|
const I18nContext = createContext(null);
|
|
12
13
|
// Preact's `createElement` overloads don't reconcile a typed `Context.Provider` (required `value`)
|
|
@@ -14,13 +15,16 @@ const I18nContext = createContext(null);
|
|
|
14
15
|
// The call is correct at runtime (proven by the tests), so invoke it through a precisely-typed element
|
|
15
16
|
// factory that states the real contract (`{ value }` + children), rather than scatter `any`.
|
|
16
17
|
const createProvider = createElement;
|
|
17
|
-
/** Provide a {@link Formatter} (built from `locale` + `messages
|
|
18
|
-
* `
|
|
18
|
+
/** Provide a {@link Formatter} (built from `locale` + `messages`, with the optional `fallback`,
|
|
19
|
+
* `onMissing`, `timeZone` and `numberingSystem` of `createFormatter`) to the subtree. Memoized
|
|
20
|
+
* on those props, so switching locale rebuilds it and re-renders consumers; formatters are cached by
|
|
21
|
+
* catalog identity, so an inline `fallback={[en]}` still yields the same instance. */
|
|
19
22
|
export function I18nProvider(props) {
|
|
20
|
-
const
|
|
23
|
+
const { locale, messages, fallback, onMissing, timeZone, numberingSystem } = props;
|
|
24
|
+
const formatter = useMemo(() => createFormatter(locale, messages, { fallback, onMissing, timeZone, numberingSystem }), [locale, messages, fallback, onMissing, timeZone, numberingSystem]);
|
|
21
25
|
return createProvider(I18nContext.Provider, { value: formatter }, props.children);
|
|
22
26
|
}
|
|
23
|
-
/** Read the current {@link Formatter} (`{ locale, t, n, d }`). Throws if no `<I18nProvider>` is above. */
|
|
27
|
+
/** Read the current {@link Formatter} (`{ locale, t, get, n, d }`). Throws if no `<I18nProvider>` is above. */
|
|
24
28
|
export function useT() {
|
|
25
29
|
const formatter = useContext(I18nContext);
|
|
26
30
|
if (formatter === null) {
|
|
@@ -28,4 +32,23 @@ export function useT() {
|
|
|
28
32
|
}
|
|
29
33
|
return formatter;
|
|
30
34
|
}
|
|
35
|
+
const join = (chunks) => chunks.length === 0
|
|
36
|
+
? null
|
|
37
|
+
: chunks.length === 1
|
|
38
|
+
? chunks[0]
|
|
39
|
+
: createElement(Fragment, null, ...chunks);
|
|
40
|
+
const PREACT_RICH = {
|
|
41
|
+
join,
|
|
42
|
+
lineBreak: () => createElement("br", null),
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* The message at `key` with its tags rendered by `tags`, as Preact nodes - no HTML, no
|
|
46
|
+
* `dangerouslySetInnerHTML`. `"Read the <link>terms</link>"` with
|
|
47
|
+
* `{ link: (content) => <a href="/terms">{content}</a> }` renders the link around "terms"; a tag with
|
|
48
|
+
* no handler renders its content as text, `<br/>` is a `<br>`, and interpolated values are always
|
|
49
|
+
* text. See `@nifrajs/i18n/rich`.
|
|
50
|
+
*/
|
|
51
|
+
export function rich(formatter, key, tags, vars) {
|
|
52
|
+
return renderRich(PREACT_RICH, formatter, key, tags, vars);
|
|
53
|
+
}
|
|
31
54
|
//# sourceMappingURL=i18n.js.map
|
package/dist/i18n.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,eAAe,GAMhB,MAAM,eAAe,CAAA;AACtB,OAAO,EAAsC,UAAU,EAAE,MAAM,oBAAoB,CAAA;AACnF,OAAO,EAA0B,aAAa,EAAE,aAAa,EAAE,QAAQ,EAAc,MAAM,QAAQ,CAAA;AACnG,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAElD,MAAM,WAAW,GAAG,aAAa,CAAmB,IAAI,CAAC,CAAA;AAEzD,mGAAmG;AACnG,uGAAuG;AACvG,uGAAuG;AACvG,6FAA6F;AAC7F,MAAM,cAAc,GAAG,aAIb,CAAA;AASV;;;sFAGsF;AACtF,MAAM,UAAU,YAAY,CAAC,KAAwB;IACnD,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,eAAe,EAAE,GAAG,KAAK,CAAA;IAClF,MAAM,SAAS,GAAG,OAAO,CACvB,GAAG,EAAE,CAAC,eAAe,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,eAAe,EAAE,CAAC,EAC3F,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,eAAe,CAAC,CACnE,CAAA;IACD,OAAO,cAAc,CAAC,WAAW,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAA;AACnF,CAAC;AAED,+GAA+G;AAC/G,MAAM,UAAU,IAAI;IAClB,MAAM,SAAS,GAAG,UAAU,CAAC,WAAW,CAAC,CAAA;IACzC,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,iEAAiE,CAAC,CAAA;IACpF,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAKD,MAAM,IAAI,GAAG,CAAC,MAAqC,EAAqB,EAAE,CACxE,MAAM,CAAC,MAAM,KAAK,CAAC;IACjB,CAAC,CAAC,IAAI;IACN,CAAC,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;QACnB,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;QACX,CAAC,CAAC,aAAa,CAAC,QAAQ,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC,CAAA;AAChD,MAAM,WAAW,GAAoC;IACnD,IAAI;IACJ,SAAS,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC;CAC3C,CAAA;AAED;;;;;;GAMG;AACH,MAAM,UAAU,IAAI,CAClB,SAAuB,EACvB,GAAkB,EAClB,IAAe,EACf,IAAwC;IAExC,OAAO,UAAU,CAAC,WAAW,EAAE,SAAS,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,CAAA;AAC5D,CAAC"}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAiDjD,8EAA8E;AAC9E,eAAO,MAAM,aAAa,EAAE,aA0B3B,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,39 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { compose } from "./compose.js";
|
|
9
9
|
import { preactRenderToStream, preactRenderToString } from "./preact-render.js";
|
|
10
|
+
const ISLAND_RUNTIME_OPEN = "<script>(function(){";
|
|
11
|
+
const ISLAND_RUNTIME_OPEN_BYTES = new TextEncoder().encode(ISLAND_RUNTIME_OPEN);
|
|
12
|
+
const startsWithBytes = (chunk, prefix) => {
|
|
13
|
+
if (chunk.length < prefix.length)
|
|
14
|
+
return false;
|
|
15
|
+
for (let i = 0; i < prefix.length; i++)
|
|
16
|
+
if (chunk[i] !== prefix[i])
|
|
17
|
+
return false;
|
|
18
|
+
return true;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* `preact-render-to-string` streams the runtime that moves resolved `<Suspense>` content into place as
|
|
22
|
+
* one attribute-less `<script>` chunk, and takes no nonce option. Stamp the document nonce on exactly
|
|
23
|
+
* that chunk (it defines the `preact-island` element); every other chunk passes through untouched, so
|
|
24
|
+
* an app-rendered `<script>` never inherits the nonce.
|
|
25
|
+
*/
|
|
26
|
+
function withIslandRuntimeNonce(stream, nonce) {
|
|
27
|
+
const decoder = new TextDecoder();
|
|
28
|
+
const encoder = new TextEncoder();
|
|
29
|
+
const nonced = `<script nonce="${nonce.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<")}">(function(){`;
|
|
30
|
+
return stream.pipeThrough(new TransformStream({
|
|
31
|
+
transform(chunk, controller) {
|
|
32
|
+
if (startsWithBytes(chunk, ISLAND_RUNTIME_OPEN_BYTES)) {
|
|
33
|
+
const text = decoder.decode(chunk);
|
|
34
|
+
if (text.includes('"preact-island"')) {
|
|
35
|
+
controller.enqueue(encoder.encode(nonced + text.slice(ISLAND_RUNTIME_OPEN.length)));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
controller.enqueue(chunk);
|
|
40
|
+
},
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
10
43
|
/** The Preact server render adapter - pass to @nifrajs/web's `renderPage`. */
|
|
11
44
|
export const preactAdapter = {
|
|
12
45
|
// Synchronous one-pass render for non-deferred pages (renderPage's buffered fast path). The sync
|
|
@@ -22,11 +55,12 @@ export const preactAdapter = {
|
|
|
22
55
|
}
|
|
23
56
|
return loaded.renderToString(compose(chain, props));
|
|
24
57
|
},
|
|
25
|
-
async renderToStream(chain, props) {
|
|
58
|
+
async renderToStream(chain, props, options) {
|
|
26
59
|
// `renderToReadableStream` yields a Web ReadableStream<Uint8Array>; <Suspense> boundaries
|
|
27
60
|
// (preact/compat) stream as they resolve, mirroring the React adapter.
|
|
28
61
|
const { renderToReadableStream } = await preactRenderToStream();
|
|
29
|
-
|
|
62
|
+
const stream = renderToReadableStream(compose(chain, props));
|
|
63
|
+
return options?.nonce === undefined ? stream : withIslandRuntimeNonce(stream, options.nonce);
|
|
30
64
|
},
|
|
31
65
|
// Preact reconciles against the existing DOM on hydrate (like React), so there's no per-document
|
|
32
66
|
// bootstrap script - the seam allows the empty string (contrast Solid's generateHydrationScript).
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA;;;;;;GAMG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAE/E,8EAA8E;AAC9E,MAAM,CAAC,MAAM,aAAa,GAAkB;IAC1C,iGAAiG;IACjG,gGAAgG;IAChG,EAAE;IACF,gGAAgG;IAChG,oGAAoG;IACpG,kEAAkE;IAClE,cAAc,CAAC,KAAK,EAAE,KAAK;QACzB,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAA;QACrC,IAAI,MAAM,YAAY,OAAO,EAAE,CAAC;YAC9B,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,cAAc,EAAE,EAAE,EAAE,CAAC,cAAc,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAA;QACnF,CAAC;QACD,OAAO,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAA;IACrD,CAAC;IACD,KAAK,CAAC,cAAc,CAAC,KAAK,EAAE,KAAK;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { MountRouterOptions, RenderProps, RouterState } from "@nifrajs/web";
|
|
2
|
+
/** The client half of the server's `RenderProps`, so the first render matches the SSR markup. */
|
|
3
|
+
export declare function routeProps(state: RouterState, searchSchemas: MountRouterOptions["searchSchemas"], matchChains?: MountRouterOptions["matchChains"]): RenderProps;
|
|
4
|
+
//# sourceMappingURL=route-props.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-props.d.ts","sourceRoot":"","sources":["../src/route-props.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAKhF,iGAAiG;AACjG,wBAAgB,UAAU,CACxB,KAAK,EAAE,WAAW,EAClB,aAAa,EAAE,kBAAkB,CAAC,eAAe,CAAC,EAClD,WAAW,CAAC,EAAE,kBAAkB,CAAC,aAAa,CAAC,GAC9C,WAAW,CAuBb"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// `/client`, not the root: the root's graph carries the server, and Vite's dev server evaluates it
|
|
2
|
+
// instead of tree-shaking it - which broke hydration before the browser ran a line of app code.
|
|
3
|
+
import { searchOfChain } from "@nifrajs/web/client";
|
|
4
|
+
/** The client half of the server's `RenderProps`, so the first render matches the SSR markup. */
|
|
5
|
+
export function routeProps(state, searchSchemas, matchChains) {
|
|
6
|
+
const matchChain = matchChains?.[state.routeId];
|
|
7
|
+
// Derive this route's typed `search` from the URL + the route's schema chain (the SAME `searchOfChain`
|
|
8
|
+
// the server ran), so `useSearch` reads an identical value and hydrates with no drift.
|
|
9
|
+
const q = state.path.indexOf("?");
|
|
10
|
+
const rawSearch = q === -1 ? "" : state.path.slice(q);
|
|
11
|
+
return {
|
|
12
|
+
data: state.data,
|
|
13
|
+
// Each layout's own loader data. Without it a layout renders `data: null` on the client while the
|
|
14
|
+
// server rendered it with its data - a hydration mismatch on the first paint.
|
|
15
|
+
...(state.layoutData !== undefined ? { layoutData: state.layoutData } : {}),
|
|
16
|
+
actionData: state.actionData,
|
|
17
|
+
pending: state.pending,
|
|
18
|
+
// The same `params`/`path` the server render received, so `useMatches` reports the same chain.
|
|
19
|
+
params: state.params,
|
|
20
|
+
path: state.path,
|
|
21
|
+
search: searchOfChain(searchSchemas?.[state.routeId] ?? [], rawSearch),
|
|
22
|
+
// The in-flight submission (for optimistic UI) - spread only when present.
|
|
23
|
+
...(state.submission ? { submission: state.submission } : {}),
|
|
24
|
+
...(state.boundaries !== undefined ? { boundaries: state.boundaries } : {}),
|
|
25
|
+
// The chain `useMatches` reports - the server passed the same one to the SSR render.
|
|
26
|
+
...(matchChain !== undefined ? { matchChain } : {}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=route-props.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-props.js","sourceRoot":"","sources":["../src/route-props.ts"],"names":[],"mappings":"AACA,mGAAmG;AACnG,gGAAgG;AAChG,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAEnD,iGAAiG;AACjG,MAAM,UAAU,UAAU,CACxB,KAAkB,EAClB,aAAkD,EAClD,WAA+C;IAE/C,MAAM,UAAU,GAAG,WAAW,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IAC/C,uGAAuG;IACvG,uFAAuF;IACvF,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IACjC,MAAM,SAAS,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IACrD,OAAO;QACL,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,kGAAkG;QAClG,8EAA8E;QAC9E,GAAG,CAAC,KAAK,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3E,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,+FAA+F;QAC/F,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,MAAM,EAAE,aAAa,CAAC,aAAa,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,SAAS,CAAC;QACtE,2EAA2E;QAC3E,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7D,GAAG,CAAC,KAAK,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3E,qFAAqF;QACrF,GAAG,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACpD,CAAA;AACH,CAAC"}
|
package/dist/router.d.ts
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import type { InferOutput, StandardSchemaV1 } from "@nifrajs/core/server";
|
|
2
|
-
import type { Blocker, BlockerFunction, BlockerState, NavigateFunction } from "@nifrajs/web";
|
|
3
|
-
export type { Blocker, BlockerFunction, BlockerState, NavigateFunction };
|
|
2
|
+
import type { Blocker, BlockerFunction, BlockerState, NavigateFunction, RenderProps, UIMatch } from "@nifrajs/web";
|
|
3
|
+
export type { Blocker, BlockerFunction, BlockerState, NavigateFunction, UIMatch };
|
|
4
4
|
export declare const SearchContext: import("preact").Context<Record<string, unknown>>;
|
|
5
|
+
export declare const RenderPropsContext: import("preact").Context<RenderProps | undefined>;
|
|
6
|
+
/**
|
|
7
|
+
* The rendered chain - each layout, then the page - with the URL prefix, params and loader data each
|
|
8
|
+
* one owns, plus its `handle` export. The same list on the server render and the client mount, so a
|
|
9
|
+
* layout can render breadcrumbs from its children's `handle`s without a hydration mismatch.
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* export const handle = { crumb: "Settings" }
|
|
13
|
+
* const crumbs = useMatches().flatMap((m) => (m.handle as { crumb?: string } | undefined)?.crumb ?? [])
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare function useMatches(): readonly UIMatch[];
|
|
5
17
|
/**
|
|
6
18
|
* The route's typed, validated search params - the SAME value the loader received as `ctx.search`.
|
|
7
19
|
* SSR-correct: `compose` provides it from the URL server-side and from the identical derivation on the
|
package/dist/router.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAIzE,OAAO,KAAK,EACV,OAAO,EACP,eAAe,EACf,YAAY,EACZ,gBAAgB,
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAIzE,OAAO,KAAK,EACV,OAAO,EACP,eAAe,EACf,YAAY,EACZ,gBAAgB,EAGhB,WAAW,EACX,OAAO,EACR,MAAM,cAAc,CAAA;AAqBrB,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,gBAAgB,EAAE,OAAO,EAAE,CAAA;AAgBjF,eAAO,MAAM,aAAa,mDACsE,CAAA;AAShG,eAAO,MAAM,kBAAkB,mDAC4D,CAAA;AAK3F;;;;;;;;;GASG;AACH,wBAAgB,UAAU,IAAI,SAAS,OAAO,EAAE,CAG/C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CACvB,MAAM,SAAS,gBAAgB,GAAG,SAAS,GAAG,SAAS,KACpD,MAAM,SAAS,gBAAgB,GAAG,WAAW,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAInF;AAED;;sEAEsE;AACtE,wBAAgB,WAAW,IAAI,gBAAgB,CAO9C;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,OAAO,GAAG,eAAe,GAAG,OAAO,CAe1E"}
|
package/dist/router.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// `/client`, not the root - these are DOM values, and the root's graph carries the
|
|
2
2
|
// server, which Vite's dev server evaluates rather than tree-shakes.
|
|
3
3
|
import { getBrowserNavigate, IDLE_BLOCKER, registerBlocker, resolveNavigate, } from "@nifrajs/web/client";
|
|
4
|
+
// Its own subpath, so an app that never calls `useMatches` never bundles it.
|
|
5
|
+
import { chainMatches } from "@nifrajs/web/internal/matches-runtime";
|
|
4
6
|
/**
|
|
5
7
|
* `@nifrajs/web-preact/router` - Preact routing bindings over the agnostic `@nifrajs/web` history layer:
|
|
6
8
|
* `useNavigate` (programmatic navigation), `useBlocker` (the unsaved-changes guard), and `useSearch`
|
|
@@ -9,7 +11,7 @@ import { getBrowserNavigate, IDLE_BLOCKER, registerBlocker, resolveNavigate, } f
|
|
|
9
11
|
* `compose` provides on SSR + client mount alike. Client-safe (no `preact-render-to-string`). No JSX.
|
|
10
12
|
*/
|
|
11
13
|
import { createContext } from "preact";
|
|
12
|
-
import { useCallback, useContext, useEffect, useRef, useState } from "preact/compat";
|
|
14
|
+
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/compat";
|
|
13
15
|
// Frozen empty search so the default context value has a stable reference.
|
|
14
16
|
const EMPTY_SEARCH = Object.freeze({});
|
|
15
17
|
/** The current route's validated search, provided by `compose` on SSR + client mount alike (derived from
|
|
@@ -23,6 +25,27 @@ const SEARCH_CONTEXT_SLOT = Symbol.for("nifra.web-preact.search-context");
|
|
|
23
25
|
const searchContextSlot = globalThis;
|
|
24
26
|
export const SearchContext = searchContextSlot[SEARCH_CONTEXT_SLOT] ?? createContext(EMPTY_SEARCH);
|
|
25
27
|
searchContextSlot[SEARCH_CONTEXT_SLOT] = SearchContext;
|
|
28
|
+
/** The props `compose` rendered the chain with - what {@link useMatches} reads. A `globalThis`
|
|
29
|
+
* singleton for the same reason as {@link SearchContext}. */
|
|
30
|
+
const PROPS_CONTEXT_SLOT = Symbol.for("nifra.web-preact.props-context");
|
|
31
|
+
const propsContextSlot = globalThis;
|
|
32
|
+
export const RenderPropsContext = propsContextSlot[PROPS_CONTEXT_SLOT] ?? createContext(undefined);
|
|
33
|
+
propsContextSlot[PROPS_CONTEXT_SLOT] = RenderPropsContext;
|
|
34
|
+
const NO_MATCHES = Object.freeze([]);
|
|
35
|
+
/**
|
|
36
|
+
* The rendered chain - each layout, then the page - with the URL prefix, params and loader data each
|
|
37
|
+
* one owns, plus its `handle` export. The same list on the server render and the client mount, so a
|
|
38
|
+
* layout can render breadcrumbs from its children's `handle`s without a hydration mismatch.
|
|
39
|
+
*
|
|
40
|
+
* ```tsx
|
|
41
|
+
* export const handle = { crumb: "Settings" }
|
|
42
|
+
* const crumbs = useMatches().flatMap((m) => (m.handle as { crumb?: string } | undefined)?.crumb ?? [])
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function useMatches() {
|
|
46
|
+
const props = useContext(RenderPropsContext);
|
|
47
|
+
return useMemo(() => (props === undefined ? NO_MATCHES : chainMatches(props)), [props]);
|
|
48
|
+
}
|
|
26
49
|
/**
|
|
27
50
|
* The route's typed, validated search params - the SAME value the loader received as `ctx.search`.
|
|
28
51
|
* SSR-correct: `compose` provides it from the URL server-side and from the identical derivation on the
|
package/dist/router.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/web-preact",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Preact render adapter for @nifrajs/web - streaming SSR + hydration (no build plugin).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
"full-stack"
|
|
15
15
|
],
|
|
16
16
|
"type": "module",
|
|
17
|
+
"nifra": {
|
|
18
|
+
"environment": "library"
|
|
19
|
+
},
|
|
17
20
|
"main": "./dist/index.js",
|
|
18
21
|
"module": "./dist/index.js",
|
|
19
22
|
"types": "./dist/index.d.ts",
|
|
@@ -82,10 +85,10 @@
|
|
|
82
85
|
"access": "public"
|
|
83
86
|
},
|
|
84
87
|
"dependencies": {
|
|
85
|
-
"@nifrajs/core": "
|
|
86
|
-
"@nifrajs/i18n": "
|
|
87
|
-
"@nifrajs/image": "
|
|
88
|
-
"@nifrajs/web": "
|
|
88
|
+
"@nifrajs/core": "4.0.0",
|
|
89
|
+
"@nifrajs/i18n": "4.0.0",
|
|
90
|
+
"@nifrajs/image": "4.0.0",
|
|
91
|
+
"@nifrajs/web": "4.0.0",
|
|
89
92
|
"preact-render-to-string": "^6.5.0"
|
|
90
93
|
},
|
|
91
94
|
"peerDependencies": {
|
package/src/client.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import type { MountRouterOptions, RenderProps } from "@nifrajs/web"
|
|
2
|
-
// `/client`, not the root: the root's graph carries the server, and Vite's dev server evaluates it
|
|
3
|
-
// instead of tree-shaking it - which broke hydration before the browser ran a line of app code.
|
|
4
|
-
import { searchOfChain } from "@nifrajs/web/client"
|
|
5
2
|
/**
|
|
6
3
|
* @nifrajs/web-preact/client - Preact client runtime. `hydrate` hydrates a single SSR'd route;
|
|
7
4
|
* `mountRouter` hydrates a stateful Router that subscribes to the agnostic store (via
|
|
@@ -13,6 +10,7 @@ import { type FunctionComponent, h, hydrate as preactHydrate } from "preact"
|
|
|
13
10
|
import { useSyncExternalStore } from "preact/compat"
|
|
14
11
|
import { compose } from "./compose.ts"
|
|
15
12
|
import { setMountedRouter } from "./fetcher.ts"
|
|
13
|
+
import { routeProps } from "./route-props.ts"
|
|
16
14
|
|
|
17
15
|
// The `_error` boundary chain element - defined in its own (DOM-free) module, re-exported here so
|
|
18
16
|
// nifra's client codegen resolves it from `@nifrajs/web-preact/client` alongside `mountRouter`.
|
|
@@ -44,23 +42,11 @@ export function hydrate(chain: readonly unknown[], props: RenderProps, container
|
|
|
44
42
|
* (no `getServerSnapshot`); `router.snapshot` is deterministic, so hydration matches the SSR markup.
|
|
45
43
|
*/
|
|
46
44
|
export function mountRouter(options: MountRouterOptions): void {
|
|
47
|
-
const { router, routes, searchSchemas, container } = options
|
|
45
|
+
const { router, routes, searchSchemas, matchChains, container } = options
|
|
48
46
|
setMountedRouter(router) // expose it to useFetcher/useFetchers (same page, client-only)
|
|
49
47
|
const Router: FunctionComponent = () => {
|
|
50
48
|
const state = useSyncExternalStore(router.subscribe, router.snapshot)
|
|
51
|
-
|
|
52
|
-
// the server ran), so `useSearch` reads an identical value and hydrates with no drift.
|
|
53
|
-
const q = state.path.indexOf("?")
|
|
54
|
-
const rawSearch = q === -1 ? "" : state.path.slice(q)
|
|
55
|
-
return compose(routes[state.routeId] ?? [], {
|
|
56
|
-
data: state.data,
|
|
57
|
-
actionData: state.actionData,
|
|
58
|
-
pending: state.pending,
|
|
59
|
-
search: searchOfChain(searchSchemas?.[state.routeId] ?? [], rawSearch),
|
|
60
|
-
// The in-flight submission (for optimistic UI) - spread only when present.
|
|
61
|
-
...(state.submission ? { submission: state.submission } : {}),
|
|
62
|
-
...(state.boundaries !== undefined ? { boundaries: state.boundaries } : {}),
|
|
63
|
-
})
|
|
49
|
+
return compose(routes[state.routeId] ?? [], routeProps(state, searchSchemas, matchChains))
|
|
64
50
|
}
|
|
65
51
|
preactHydrate(h(Router, null), container as Element)
|
|
66
52
|
}
|
package/src/compose.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { RenderProps } from "@nifrajs/web"
|
|
2
2
|
import { type ComponentChildren, type ComponentType, createElement, h, type VNode } from "preact"
|
|
3
|
-
import { SearchContext } from "./router.ts"
|
|
3
|
+
import { RenderPropsContext, SearchContext } from "./router.ts"
|
|
4
4
|
|
|
5
5
|
// Frozen empty search so a render with no search context has a stable provider value.
|
|
6
6
|
const EMPTY_SEARCH: Readonly<Record<string, unknown>> = Object.freeze({})
|
|
@@ -13,6 +13,11 @@ const createSearchProvider = createElement as (
|
|
|
13
13
|
props: { value: Record<string, unknown> },
|
|
14
14
|
children?: ComponentChildren,
|
|
15
15
|
) => VNode
|
|
16
|
+
const createPropsProvider = createElement as (
|
|
17
|
+
type: typeof RenderPropsContext.Provider,
|
|
18
|
+
props: { value: RenderProps },
|
|
19
|
+
children?: ComponentChildren,
|
|
20
|
+
) => VNode
|
|
16
21
|
|
|
17
22
|
/**
|
|
18
23
|
* Fold a layout chain (outermost layout → page) into a single Preact tree: the page (innermost)
|
|
@@ -44,5 +49,9 @@ export function compose(chain: readonly unknown[], props: RenderProps): VNode {
|
|
|
44
49
|
node,
|
|
45
50
|
) as VNode
|
|
46
51
|
}
|
|
47
|
-
return createSearchProvider(
|
|
52
|
+
return createSearchProvider(
|
|
53
|
+
SearchContext.Provider,
|
|
54
|
+
{ value: props.search ?? EMPTY_SEARCH },
|
|
55
|
+
createPropsProvider(RenderPropsContext.Provider, { value: props }, node),
|
|
56
|
+
)
|
|
48
57
|
}
|
package/src/i18n.ts
CHANGED
|
@@ -5,8 +5,16 @@
|
|
|
5
5
|
* and the client rebuilds the same formatter from the same props (no mismatch). Uses `preact` +
|
|
6
6
|
* `preact/hooks` - no JSX.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
9
|
-
|
|
8
|
+
import {
|
|
9
|
+
createFormatter,
|
|
10
|
+
type Formatter,
|
|
11
|
+
type FormatterOptions,
|
|
12
|
+
type MessageKey,
|
|
13
|
+
type RegisteredMessages,
|
|
14
|
+
type Translation,
|
|
15
|
+
} from "@nifrajs/i18n"
|
|
16
|
+
import { type RichChunks, type RichRenderer, renderRich } from "@nifrajs/i18n/rich"
|
|
17
|
+
import { type ComponentChildren, createContext, createElement, Fragment, type VNode } from "preact"
|
|
10
18
|
import { useContext, useMemo } from "preact/hooks"
|
|
11
19
|
|
|
12
20
|
const I18nContext = createContext<Formatter | null>(null)
|
|
@@ -21,23 +29,27 @@ const createProvider = createElement as (
|
|
|
21
29
|
children?: ComponentChildren,
|
|
22
30
|
) => VNode
|
|
23
31
|
|
|
24
|
-
export interface I18nProviderProps {
|
|
32
|
+
export interface I18nProviderProps extends FormatterOptions {
|
|
25
33
|
readonly locale: string
|
|
26
|
-
|
|
34
|
+
/** The catalog for `locale`, checked against the registered catalog type when one is declared. */
|
|
35
|
+
readonly messages: Translation
|
|
27
36
|
readonly children?: ComponentChildren
|
|
28
37
|
}
|
|
29
38
|
|
|
30
|
-
/** Provide a {@link Formatter} (built from `locale` + `messages
|
|
31
|
-
* `
|
|
39
|
+
/** Provide a {@link Formatter} (built from `locale` + `messages`, with the optional `fallback`,
|
|
40
|
+
* `onMissing`, `timeZone` and `numberingSystem` of `createFormatter`) to the subtree. Memoized
|
|
41
|
+
* on those props, so switching locale rebuilds it and re-renders consumers; formatters are cached by
|
|
42
|
+
* catalog identity, so an inline `fallback={[en]}` still yields the same instance. */
|
|
32
43
|
export function I18nProvider(props: I18nProviderProps): VNode {
|
|
44
|
+
const { locale, messages, fallback, onMissing, timeZone, numberingSystem } = props
|
|
33
45
|
const formatter = useMemo(
|
|
34
|
-
() => createFormatter(
|
|
35
|
-
[
|
|
46
|
+
() => createFormatter(locale, messages, { fallback, onMissing, timeZone, numberingSystem }),
|
|
47
|
+
[locale, messages, fallback, onMissing, timeZone, numberingSystem],
|
|
36
48
|
)
|
|
37
49
|
return createProvider(I18nContext.Provider, { value: formatter }, props.children)
|
|
38
50
|
}
|
|
39
51
|
|
|
40
|
-
/** Read the current {@link Formatter} (`{ locale, t, n, d }`). Throws if no `<I18nProvider>` is above. */
|
|
52
|
+
/** Read the current {@link Formatter} (`{ locale, t, get, n, d }`). Throws if no `<I18nProvider>` is above. */
|
|
41
53
|
export function useT(): Formatter {
|
|
42
54
|
const formatter = useContext(I18nContext)
|
|
43
55
|
if (formatter === null) {
|
|
@@ -45,3 +57,33 @@ export function useT(): Formatter {
|
|
|
45
57
|
}
|
|
46
58
|
return formatter
|
|
47
59
|
}
|
|
60
|
+
|
|
61
|
+
/** Tag handlers for {@link rich}, by tag name: each receives its tag's content as one node. */
|
|
62
|
+
export type RichTags = Readonly<Record<string, (content: ComponentChildren) => ComponentChildren>>
|
|
63
|
+
|
|
64
|
+
const join = (chunks: RichChunks<ComponentChildren>): ComponentChildren =>
|
|
65
|
+
chunks.length === 0
|
|
66
|
+
? null
|
|
67
|
+
: chunks.length === 1
|
|
68
|
+
? chunks[0]
|
|
69
|
+
: createElement(Fragment, null, ...chunks)
|
|
70
|
+
const PREACT_RICH: RichRenderer<ComponentChildren> = {
|
|
71
|
+
join,
|
|
72
|
+
lineBreak: () => createElement("br", null),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The message at `key` with its tags rendered by `tags`, as Preact nodes - no HTML, no
|
|
77
|
+
* `dangerouslySetInnerHTML`. `"Read the <link>terms</link>"` with
|
|
78
|
+
* `{ link: (content) => <a href="/terms">{content}</a> }` renders the link around "terms"; a tag with
|
|
79
|
+
* no handler renders its content as text, `<br/>` is a `<br>`, and interpolated values are always
|
|
80
|
+
* text. See `@nifrajs/i18n/rich`.
|
|
81
|
+
*/
|
|
82
|
+
export function rich<M extends object = RegisteredMessages>(
|
|
83
|
+
formatter: Formatter<M>,
|
|
84
|
+
key: MessageKey<M>,
|
|
85
|
+
tags?: RichTags,
|
|
86
|
+
vars?: Readonly<Record<string, unknown>>,
|
|
87
|
+
): ComponentChildren {
|
|
88
|
+
return renderRich(PREACT_RICH, formatter, key, tags, vars)
|
|
89
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,44 @@ import type { RenderAdapter } from "@nifrajs/web"
|
|
|
9
9
|
import { compose } from "./compose.ts"
|
|
10
10
|
import { preactRenderToStream, preactRenderToString } from "./preact-render.ts"
|
|
11
11
|
|
|
12
|
+
const ISLAND_RUNTIME_OPEN = "<script>(function(){"
|
|
13
|
+
const ISLAND_RUNTIME_OPEN_BYTES = new TextEncoder().encode(ISLAND_RUNTIME_OPEN)
|
|
14
|
+
|
|
15
|
+
const startsWithBytes = (chunk: Uint8Array, prefix: Uint8Array): boolean => {
|
|
16
|
+
if (chunk.length < prefix.length) return false
|
|
17
|
+
for (let i = 0; i < prefix.length; i++) if (chunk[i] !== prefix[i]) return false
|
|
18
|
+
return true
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* `preact-render-to-string` streams the runtime that moves resolved `<Suspense>` content into place as
|
|
23
|
+
* one attribute-less `<script>` chunk, and takes no nonce option. Stamp the document nonce on exactly
|
|
24
|
+
* that chunk (it defines the `preact-island` element); every other chunk passes through untouched, so
|
|
25
|
+
* an app-rendered `<script>` never inherits the nonce.
|
|
26
|
+
*/
|
|
27
|
+
function withIslandRuntimeNonce(
|
|
28
|
+
stream: ReadableStream<Uint8Array>,
|
|
29
|
+
nonce: string,
|
|
30
|
+
): ReadableStream<Uint8Array> {
|
|
31
|
+
const decoder = new TextDecoder()
|
|
32
|
+
const encoder = new TextEncoder()
|
|
33
|
+
const nonced = `<script nonce="${nonce.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<")}">(function(){`
|
|
34
|
+
return stream.pipeThrough(
|
|
35
|
+
new TransformStream<Uint8Array, Uint8Array>({
|
|
36
|
+
transform(chunk, controller) {
|
|
37
|
+
if (startsWithBytes(chunk, ISLAND_RUNTIME_OPEN_BYTES)) {
|
|
38
|
+
const text = decoder.decode(chunk)
|
|
39
|
+
if (text.includes('"preact-island"')) {
|
|
40
|
+
controller.enqueue(encoder.encode(nonced + text.slice(ISLAND_RUNTIME_OPEN.length)))
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
controller.enqueue(chunk)
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
12
50
|
/** The Preact server render adapter - pass to @nifrajs/web's `renderPage`. */
|
|
13
51
|
export const preactAdapter: RenderAdapter = {
|
|
14
52
|
// Synchronous one-pass render for non-deferred pages (renderPage's buffered fast path). The sync
|
|
@@ -24,11 +62,12 @@ export const preactAdapter: RenderAdapter = {
|
|
|
24
62
|
}
|
|
25
63
|
return loaded.renderToString(compose(chain, props))
|
|
26
64
|
},
|
|
27
|
-
async renderToStream(chain, props) {
|
|
65
|
+
async renderToStream(chain, props, options) {
|
|
28
66
|
// `renderToReadableStream` yields a Web ReadableStream<Uint8Array>; <Suspense> boundaries
|
|
29
67
|
// (preact/compat) stream as they resolve, mirroring the React adapter.
|
|
30
68
|
const { renderToReadableStream } = await preactRenderToStream()
|
|
31
|
-
|
|
69
|
+
const stream = renderToReadableStream(compose(chain, props))
|
|
70
|
+
return options?.nonce === undefined ? stream : withIslandRuntimeNonce(stream, options.nonce)
|
|
32
71
|
},
|
|
33
72
|
// Preact reconciles against the existing DOM on hydrate (like React), so there's no per-document
|
|
34
73
|
// bootstrap script - the seam allows the empty string (contrast Solid's generateHydrationScript).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { MountRouterOptions, RenderProps, RouterState } from "@nifrajs/web"
|
|
2
|
+
// `/client`, not the root: the root's graph carries the server, and Vite's dev server evaluates it
|
|
3
|
+
// instead of tree-shaking it - which broke hydration before the browser ran a line of app code.
|
|
4
|
+
import { searchOfChain } from "@nifrajs/web/client"
|
|
5
|
+
|
|
6
|
+
/** The client half of the server's `RenderProps`, so the first render matches the SSR markup. */
|
|
7
|
+
export function routeProps(
|
|
8
|
+
state: RouterState,
|
|
9
|
+
searchSchemas: MountRouterOptions["searchSchemas"],
|
|
10
|
+
matchChains?: MountRouterOptions["matchChains"],
|
|
11
|
+
): RenderProps {
|
|
12
|
+
const matchChain = matchChains?.[state.routeId]
|
|
13
|
+
// Derive this route's typed `search` from the URL + the route's schema chain (the SAME `searchOfChain`
|
|
14
|
+
// the server ran), so `useSearch` reads an identical value and hydrates with no drift.
|
|
15
|
+
const q = state.path.indexOf("?")
|
|
16
|
+
const rawSearch = q === -1 ? "" : state.path.slice(q)
|
|
17
|
+
return {
|
|
18
|
+
data: state.data,
|
|
19
|
+
// Each layout's own loader data. Without it a layout renders `data: null` on the client while the
|
|
20
|
+
// server rendered it with its data - a hydration mismatch on the first paint.
|
|
21
|
+
...(state.layoutData !== undefined ? { layoutData: state.layoutData } : {}),
|
|
22
|
+
actionData: state.actionData,
|
|
23
|
+
pending: state.pending,
|
|
24
|
+
// The same `params`/`path` the server render received, so `useMatches` reports the same chain.
|
|
25
|
+
params: state.params,
|
|
26
|
+
path: state.path,
|
|
27
|
+
search: searchOfChain(searchSchemas?.[state.routeId] ?? [], rawSearch),
|
|
28
|
+
// The in-flight submission (for optimistic UI) - spread only when present.
|
|
29
|
+
...(state.submission ? { submission: state.submission } : {}),
|
|
30
|
+
...(state.boundaries !== undefined ? { boundaries: state.boundaries } : {}),
|
|
31
|
+
// The chain `useMatches` reports - the server passed the same one to the SSR render.
|
|
32
|
+
...(matchChain !== undefined ? { matchChain } : {}),
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/router.ts
CHANGED
|
@@ -9,6 +9,8 @@ import type {
|
|
|
9
9
|
NavigateFunction,
|
|
10
10
|
NavigateOptions,
|
|
11
11
|
NavigateTargetInput,
|
|
12
|
+
RenderProps,
|
|
13
|
+
UIMatch,
|
|
12
14
|
} from "@nifrajs/web"
|
|
13
15
|
// `/client`, not the root - these are DOM values, and the root's graph carries the
|
|
14
16
|
// server, which Vite's dev server evaluates rather than tree-shakes.
|
|
@@ -18,6 +20,8 @@ import {
|
|
|
18
20
|
registerBlocker,
|
|
19
21
|
resolveNavigate,
|
|
20
22
|
} from "@nifrajs/web/client"
|
|
23
|
+
// Its own subpath, so an app that never calls `useMatches` never bundles it.
|
|
24
|
+
import { chainMatches } from "@nifrajs/web/internal/matches-runtime"
|
|
21
25
|
/**
|
|
22
26
|
* `@nifrajs/web-preact/router` - Preact routing bindings over the agnostic `@nifrajs/web` history layer:
|
|
23
27
|
* `useNavigate` (programmatic navigation), `useBlocker` (the unsaved-changes guard), and `useSearch`
|
|
@@ -26,9 +30,9 @@ import {
|
|
|
26
30
|
* `compose` provides on SSR + client mount alike. Client-safe (no `preact-render-to-string`). No JSX.
|
|
27
31
|
*/
|
|
28
32
|
import { createContext } from "preact"
|
|
29
|
-
import { useCallback, useContext, useEffect, useRef, useState } from "preact/compat"
|
|
33
|
+
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/compat"
|
|
30
34
|
|
|
31
|
-
export type { Blocker, BlockerFunction, BlockerState, NavigateFunction }
|
|
35
|
+
export type { Blocker, BlockerFunction, BlockerState, NavigateFunction, UIMatch }
|
|
32
36
|
|
|
33
37
|
// Frozen empty search so the default context value has a stable reference.
|
|
34
38
|
const EMPTY_SEARCH: Readonly<Record<string, unknown>> = Object.freeze({})
|
|
@@ -48,6 +52,33 @@ export const SearchContext =
|
|
|
48
52
|
searchContextSlot[SEARCH_CONTEXT_SLOT] ?? createContext<Record<string, unknown>>(EMPTY_SEARCH)
|
|
49
53
|
searchContextSlot[SEARCH_CONTEXT_SLOT] = SearchContext
|
|
50
54
|
|
|
55
|
+
/** The props `compose` rendered the chain with - what {@link useMatches} reads. A `globalThis`
|
|
56
|
+
* singleton for the same reason as {@link SearchContext}. */
|
|
57
|
+
const PROPS_CONTEXT_SLOT = Symbol.for("nifra.web-preact.props-context")
|
|
58
|
+
const propsContextSlot = globalThis as {
|
|
59
|
+
[PROPS_CONTEXT_SLOT]?: ReturnType<typeof createContext<RenderProps | undefined>>
|
|
60
|
+
}
|
|
61
|
+
export const RenderPropsContext =
|
|
62
|
+
propsContextSlot[PROPS_CONTEXT_SLOT] ?? createContext<RenderProps | undefined>(undefined)
|
|
63
|
+
propsContextSlot[PROPS_CONTEXT_SLOT] = RenderPropsContext
|
|
64
|
+
|
|
65
|
+
const NO_MATCHES: readonly UIMatch[] = Object.freeze([])
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The rendered chain - each layout, then the page - with the URL prefix, params and loader data each
|
|
69
|
+
* one owns, plus its `handle` export. The same list on the server render and the client mount, so a
|
|
70
|
+
* layout can render breadcrumbs from its children's `handle`s without a hydration mismatch.
|
|
71
|
+
*
|
|
72
|
+
* ```tsx
|
|
73
|
+
* export const handle = { crumb: "Settings" }
|
|
74
|
+
* const crumbs = useMatches().flatMap((m) => (m.handle as { crumb?: string } | undefined)?.crumb ?? [])
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function useMatches(): readonly UIMatch[] {
|
|
78
|
+
const props = useContext(RenderPropsContext)
|
|
79
|
+
return useMemo(() => (props === undefined ? NO_MATCHES : chainMatches(props)), [props])
|
|
80
|
+
}
|
|
81
|
+
|
|
51
82
|
/**
|
|
52
83
|
* The route's typed, validated search params - the SAME value the loader received as `ctx.search`.
|
|
53
84
|
* SSR-correct: `compose` provides it from the URL server-side and from the identical derivation on the
|