@nifrajs/web-solid 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 +9 -35
- package/dist/client.js.map +1 -1
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +12 -4
- package/dist/compose.js.map +1 -1
- package/dist/i18n.d.ts +18 -6
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +28 -5
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +6 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -91
- package/dist/index.js.map +1 -1
- package/dist/mdx-runtime.d.ts +8 -1
- package/dist/mdx-runtime.d.ts.map +1 -1
- package/dist/mdx-runtime.js +4 -2
- package/dist/mdx-runtime.js.map +1 -1
- package/dist/plugin.d.ts +12 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +87 -0
- package/dist/plugin.js.map +1 -0
- package/dist/route-props.d.ts +7 -0
- package/dist/route-props.d.ts.map +1 -0
- package/dist/route-props.js +50 -0
- package/dist/route-props.js.map +1 -0
- package/dist/router.d.ts +16 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +28 -1
- package/dist/router.js.map +1 -1
- package/package.json +14 -6
- package/src/client.ts +11 -37
- package/src/compose.ts +12 -4
- package/src/i18n.ts +51 -7
- package/src/index.ts +12 -96
- package/src/mdx-runtime.ts +9 -4
- package/src/plugin.ts +93 -0
- package/src/route-props.ts +55 -0
- package/src/router.ts +34 -1
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
/**
|
|
5
|
+
* The client half of the server's `RenderProps`, as getters so a settle re-renders only what reads the
|
|
6
|
+
* changed field. Cast: a getter always exists where `exactOptionalPropertyTypes` expects absence.
|
|
7
|
+
*/
|
|
8
|
+
export function routeProps(snapshot, searchSchemas, matchChains) {
|
|
9
|
+
return {
|
|
10
|
+
get data() {
|
|
11
|
+
return snapshot().data;
|
|
12
|
+
},
|
|
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.
|
|
15
|
+
get layoutData() {
|
|
16
|
+
return snapshot().layoutData;
|
|
17
|
+
},
|
|
18
|
+
get actionData() {
|
|
19
|
+
return snapshot().actionData;
|
|
20
|
+
},
|
|
21
|
+
get pending() {
|
|
22
|
+
return snapshot().pending;
|
|
23
|
+
},
|
|
24
|
+
// The same `params`/`path` the server render received, so `useMatches` reports the same chain.
|
|
25
|
+
get params() {
|
|
26
|
+
return snapshot().params;
|
|
27
|
+
},
|
|
28
|
+
get path() {
|
|
29
|
+
return snapshot().path;
|
|
30
|
+
},
|
|
31
|
+
get search() {
|
|
32
|
+
// The SAME searchOfChain the server ran, over this snapshot's URL - a same-route search change
|
|
33
|
+
// updates in place (fine-grained), matching the SSR value on hydration.
|
|
34
|
+
const s = snapshot();
|
|
35
|
+
const idx = s.path.indexOf("?");
|
|
36
|
+
return searchOfChain(searchSchemas?.[s.routeId] ?? [], idx === -1 ? "" : s.path.slice(idx));
|
|
37
|
+
},
|
|
38
|
+
get submission() {
|
|
39
|
+
return snapshot().submission;
|
|
40
|
+
},
|
|
41
|
+
get boundaries() {
|
|
42
|
+
return snapshot().boundaries;
|
|
43
|
+
},
|
|
44
|
+
// The chain `useMatches` reports - the server passed the same one to the SSR render.
|
|
45
|
+
get matchChain() {
|
|
46
|
+
return matchChains?.[snapshot().routeId];
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//# 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;;;GAGG;AACH,MAAM,UAAU,UAAU,CACxB,QAA2B,EAC3B,aAAkD,EAClD,WAA+C;IAE/C,OAAO;QACL,IAAI,IAAI;YACN,OAAO,QAAQ,EAAE,CAAC,IAAI,CAAA;QACxB,CAAC;QACD,kGAAkG;QAClG,oCAAoC;QACpC,IAAI,UAAU;YACZ,OAAO,QAAQ,EAAE,CAAC,UAAU,CAAA;QAC9B,CAAC;QACD,IAAI,UAAU;YACZ,OAAO,QAAQ,EAAE,CAAC,UAAU,CAAA;QAC9B,CAAC;QACD,IAAI,OAAO;YACT,OAAO,QAAQ,EAAE,CAAC,OAAO,CAAA;QAC3B,CAAC;QACD,+FAA+F;QAC/F,IAAI,MAAM;YACR,OAAO,QAAQ,EAAE,CAAC,MAAM,CAAA;QAC1B,CAAC;QACD,IAAI,IAAI;YACN,OAAO,QAAQ,EAAE,CAAC,IAAI,CAAA;QACxB,CAAC;QACD,IAAI,MAAM;YACR,+FAA+F;YAC/F,wEAAwE;YACxE,MAAM,CAAC,GAAG,QAAQ,EAAE,CAAA;YACpB,MAAM,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;YAC/B,OAAO,aAAa,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAA;QAC7F,CAAC;QACD,IAAI,UAAU;YACZ,OAAO,QAAQ,EAAE,CAAC,UAAU,CAAA;QAC9B,CAAC;QACD,IAAI,UAAU;YACZ,OAAO,QAAQ,EAAE,CAAC,UAAU,CAAA;QAC9B,CAAC;QACD,qFAAqF;QACrF,IAAI,UAAU;YACZ,OAAO,WAAW,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAA;QAC1C,CAAC;KACa,CAAA;AAClB,CAAC"}
|
package/dist/router.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { InferOutput, StandardSchemaV1 } from "@nifrajs/core/server";
|
|
2
|
-
import type { Blocker, BlockerFunction, BlockerState, NavigateFunction } from "@nifrajs/web";
|
|
2
|
+
import type { Blocker, BlockerFunction, BlockerState, NavigateFunction, RenderProps, UIMatch } from "@nifrajs/web";
|
|
3
3
|
/**
|
|
4
4
|
* `@nifrajs/web-solid/router` - Solid routing bindings over the agnostic `@nifrajs/web` history layer:
|
|
5
5
|
* `useNavigate` (programmatic navigation), `useBlocker` (the unsaved-changes guard), and `useSearch`
|
|
@@ -8,8 +8,22 @@ import type { Blocker, BlockerFunction, BlockerState, NavigateFunction } from "@
|
|
|
8
8
|
* reads the accessor `compose` provides on SSR + client mount alike. Imports only `solid-js`. No JSX.
|
|
9
9
|
*/
|
|
10
10
|
import { type Accessor, type Context } from "solid-js";
|
|
11
|
-
export type { Blocker, BlockerFunction, BlockerState, NavigateFunction };
|
|
11
|
+
export type { Blocker, BlockerFunction, BlockerState, NavigateFunction, UIMatch };
|
|
12
12
|
export declare const SearchContext: Context<Accessor<Record<string, unknown>> | undefined>;
|
|
13
|
+
export declare const RenderPropsContext: Context<RenderProps | undefined>;
|
|
14
|
+
/**
|
|
15
|
+
* The rendered chain - each layout, then the page - with the URL prefix, params and loader data each
|
|
16
|
+
* one owns, plus its `handle` export, as a reactive accessor. The same list on the server render and
|
|
17
|
+
* the client mount, so a layout can render breadcrumbs from its children's `handle`s without a
|
|
18
|
+
* hydration mismatch; it updates in place when a same-route navigation settles.
|
|
19
|
+
*
|
|
20
|
+
* ```tsx
|
|
21
|
+
* export const handle = { crumb: "Settings" }
|
|
22
|
+
* const matches = useMatches()
|
|
23
|
+
* const crumbs = () => matches().flatMap((m) => (m.handle as { crumb?: string } | undefined)?.crumb ?? [])
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function useMatches(): Accessor<readonly UIMatch[]>;
|
|
13
27
|
/**
|
|
14
28
|
* The route's typed, validated search params as a reactive accessor - the SAME value the loader received
|
|
15
29
|
* as `ctx.search`. SSR-correct: `compose` provides it from the URL server-side and from the identical
|
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;AAWrB;;;;;;GAMG;AACH,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,OAAO,EAMb,MAAM,UAAU,CAAA;AAEjB,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,gBAAgB,EAAE,OAAO,EAAE,CAAA;AAiBjF,eAAO,MAAM,aAAa,wDACoE,CAAA;AAO9F,eAAO,MAAM,kBAAkB,kCACuC,CAAA;AAKtE;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,IAAI,QAAQ,CAAC,SAAS,OAAO,EAAE,CAAC,CAIzD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,MAAM,SAAS,gBAAgB,GAAG,SAAS,GAAG,SAAS,KAAK,QAAQ,CAC5F,MAAM,SAAS,gBAAgB,GAAG,WAAW,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAChF,CAIA;AAED;;kCAEkC;AAClC,wBAAgB,WAAW,IAAI,gBAAgB,CAO9C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,OAAO,GAAG,eAAe,GAAG,QAAQ,CAAC,OAAO,CAAC,CASpF"}
|
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-solid/router` - Solid routing bindings over the agnostic `@nifrajs/web` history layer:
|
|
6
8
|
* `useNavigate` (programmatic navigation), `useBlocker` (the unsaved-changes guard), and `useSearch`
|
|
@@ -8,7 +10,7 @@ import { getBrowserNavigate, IDLE_BLOCKER, registerBlocker, resolveNavigate, } f
|
|
|
8
10
|
* DOM-free bridges (`getBrowserNavigate` / `registerBlocker`, populated by `installHistory`); `useSearch`
|
|
9
11
|
* reads the accessor `compose` provides on SSR + client mount alike. Imports only `solid-js`. No JSX.
|
|
10
12
|
*/
|
|
11
|
-
import { createContext, createSignal, onCleanup, useContext, } from "solid-js";
|
|
13
|
+
import { createContext, createMemo, createSignal, onCleanup, useContext, } from "solid-js";
|
|
12
14
|
// Frozen empty search + a stable accessor for a `useSearch` used outside a nifra route tree.
|
|
13
15
|
const EMPTY_SEARCH = Object.freeze({});
|
|
14
16
|
const EMPTY_SEARCH_ACCESSOR = () => EMPTY_SEARCH;
|
|
@@ -23,6 +25,31 @@ const SEARCH_CONTEXT_SLOT = Symbol.for("nifra.web-solid.search-context");
|
|
|
23
25
|
const searchContextSlot = globalThis;
|
|
24
26
|
export const SearchContext = searchContextSlot[SEARCH_CONTEXT_SLOT] ?? createContext();
|
|
25
27
|
searchContextSlot[SEARCH_CONTEXT_SLOT] = SearchContext;
|
|
28
|
+
/** The props `compose` rendered the chain with - getters over the router snapshot on the client - which
|
|
29
|
+
* {@link useMatches} reads. A `globalThis` singleton for the same reason as {@link SearchContext}. */
|
|
30
|
+
const PROPS_CONTEXT_SLOT = Symbol.for("nifra.web-solid.props-context");
|
|
31
|
+
const propsContextSlot = globalThis;
|
|
32
|
+
export const RenderPropsContext = propsContextSlot[PROPS_CONTEXT_SLOT] ?? createContext();
|
|
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, as a reactive accessor. The same list on the server render and
|
|
38
|
+
* the client mount, so a layout can render breadcrumbs from its children's `handle`s without a
|
|
39
|
+
* hydration mismatch; it updates in place when a same-route navigation settles.
|
|
40
|
+
*
|
|
41
|
+
* ```tsx
|
|
42
|
+
* export const handle = { crumb: "Settings" }
|
|
43
|
+
* const matches = useMatches()
|
|
44
|
+
* const crumbs = () => matches().flatMap((m) => (m.handle as { crumb?: string } | undefined)?.crumb ?? [])
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function useMatches() {
|
|
48
|
+
const props = useContext(RenderPropsContext);
|
|
49
|
+
if (props === undefined)
|
|
50
|
+
return () => NO_MATCHES;
|
|
51
|
+
return createMemo(() => chainMatches(props));
|
|
52
|
+
}
|
|
26
53
|
/**
|
|
27
54
|
* The route's typed, validated search params as a reactive accessor - the SAME value the loader received
|
|
28
55
|
* as `ctx.search`. SSR-correct: `compose` provides it from the URL server-side and from the identical
|
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":"AAcA,mFAAmF;AACnF,qEAAqE;AACrE,OAAO,EACL,kBAAkB,EAClB,YAAY,EACZ,eAAe,EACf,eAAe,GAChB,MAAM,qBAAqB,CAAA;AAC5B,6EAA6E;AAC7E,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAA;AACpE;;;;;;GAMG;AACH,OAAO,EAGL,aAAa,EACb,UAAU,EACV,YAAY,EACZ,SAAS,EACT,UAAU,GACX,MAAM,UAAU,CAAA;AAIjB,6FAA6F;AAC7F,MAAM,YAAY,GAAsC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;AACzE,MAAM,qBAAqB,GAAsC,GAAG,EAAE,CAAC,YAAY,CAAA;AAEnF;;;;;;2BAM2B;AAC3B,MAAM,mBAAmB,GAAG,MAAM,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAA;AACxE,MAAM,iBAAiB,GAAG,UAEzB,CAAA;AACD,MAAM,CAAC,MAAM,aAAa,GACxB,iBAAiB,CAAC,mBAAmB,CAAC,IAAI,aAAa,EAAqC,CAAA;AAC9F,iBAAiB,CAAC,mBAAmB,CAAC,GAAG,aAAa,CAAA;AAEtD;sGACsG;AACtG,MAAM,kBAAkB,GAAG,MAAM,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAA;AACtE,MAAM,gBAAgB,GAAG,UAAyE,CAAA;AAClG,MAAM,CAAC,MAAM,kBAAkB,GAC7B,gBAAgB,CAAC,kBAAkB,CAAC,IAAI,aAAa,EAAe,CAAA;AACtE,gBAAgB,CAAC,kBAAkB,CAAC,GAAG,kBAAkB,CAAA;AAEzD,MAAM,UAAU,GAAuB,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;AAExD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU;IACxB,MAAM,KAAK,GAAG,UAAU,CAAC,kBAAkB,CAAC,CAAA;IAC5C,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,GAAG,EAAE,CAAC,UAAU,CAAA;IAChD,OAAO,UAAU,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAA;AAC9C,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,SAAS;IAGvB,OAAO,CAAC,UAAU,CAAC,aAAa,CAAC,IAAI,qBAAqB,CAEzD,CAAA;AACH,CAAC;AAED;;kCAEkC;AAClC,MAAM,UAAU,WAAW;IACzB,OAAO,CAAC,CAAC,EAAyC,EAAE,OAAyB,EAAE,EAAE;QAC/E,MAAM,QAAQ,GAAG,kBAAkB,EAAE,CAAA;QACrC,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAM;QAClC,MAAM,QAAQ,GAAG,eAAe,CAAC,EAAE,EAAE,OAAO,CAAC,CAAA;QAC7C,QAAQ,CAAC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAA;IACzC,CAAC,CAAqB,CAAA;AACxB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU,CAAC,WAAsC;IAC/D,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,YAAY,CAAU,YAAY,CAAC,CAAA;IACjE,SAAS,CACP,eAAe,CACb,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,OAAO,WAAW,KAAK,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,EAC/E,UAAU,CACX,CACF,CAAA;IACD,OAAO,OAAO,CAAA;AAChB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/web-solid",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Solid render adapter for @nifrajs/web - SSR + hydration
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "Solid render adapter for @nifrajs/web - SSR + hydration, with the Solid Babel build plugin (@nifrajs/web-solid/plugin).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
7
7
|
"typescript",
|
|
@@ -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",
|
|
@@ -28,6 +31,11 @@
|
|
|
28
31
|
"types": "./dist/client.d.ts",
|
|
29
32
|
"default": "./dist/client.js"
|
|
30
33
|
},
|
|
34
|
+
"./plugin": {
|
|
35
|
+
"bun": "./src/plugin.ts",
|
|
36
|
+
"types": "./dist/plugin.d.ts",
|
|
37
|
+
"default": "./dist/plugin.js"
|
|
38
|
+
},
|
|
31
39
|
"./await": {
|
|
32
40
|
"bun": "./src/await.ts",
|
|
33
41
|
"types": "./dist/await.d.ts",
|
|
@@ -99,10 +107,10 @@
|
|
|
99
107
|
"dependencies": {
|
|
100
108
|
"@babel/core": "^7.26.0",
|
|
101
109
|
"@babel/preset-typescript": "^7.26.0",
|
|
102
|
-
"@nifrajs/core": "
|
|
103
|
-
"@nifrajs/i18n": "
|
|
104
|
-
"@nifrajs/image": "
|
|
105
|
-
"@nifrajs/web": "
|
|
110
|
+
"@nifrajs/core": "4.0.0",
|
|
111
|
+
"@nifrajs/i18n": "4.0.0",
|
|
112
|
+
"@nifrajs/image": "4.0.0",
|
|
113
|
+
"@nifrajs/web": "4.0.0",
|
|
106
114
|
"babel-preset-solid": "^1.9.3",
|
|
107
115
|
"solid-refresh": "^0.7.8"
|
|
108
116
|
},
|
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-solid/client - Solid client runtime. `hydrate` hydrates a single SSR'd route;
|
|
7
4
|
* `mountRouter` hydrates the initial route then drives navigation through a Solid signal - so a
|
|
@@ -14,6 +11,7 @@ import { createSignal } from "solid-js"
|
|
|
14
11
|
import { hydrate as solidHydrate, render as solidRender } from "solid-js/web"
|
|
15
12
|
import { compose } from "./compose.ts"
|
|
16
13
|
import { setMountedRouter } from "./fetcher.ts"
|
|
14
|
+
import { routeProps } from "./route-props.ts"
|
|
17
15
|
|
|
18
16
|
// The `_error` boundary chain element - defined in its own module, re-exported here so nifra's client
|
|
19
17
|
// codegen resolves it from `@nifrajs/web-solid/client` alongside `mountRouter`.
|
|
@@ -37,7 +35,7 @@ export function hydrate(chain: readonly unknown[], props: RenderProps, container
|
|
|
37
35
|
* they don't shift Solid's structural hydration keys - unlike a `<Show>`, which would.)
|
|
38
36
|
*/
|
|
39
37
|
export function mountRouter(options: MountRouterOptions): void {
|
|
40
|
-
const { router, routes, searchSchemas, container } = options
|
|
38
|
+
const { router, routes, searchSchemas, matchChains, container } = options
|
|
41
39
|
setMountedRouter(router) // expose it to createFetcher/useFetchers (same page, client-only)
|
|
42
40
|
const el = container as Element
|
|
43
41
|
|
|
@@ -51,50 +49,26 @@ export function mountRouter(options: MountRouterOptions): void {
|
|
|
51
49
|
mountedRouteId = initial.routeId
|
|
52
50
|
const renderer = first ? solidHydrate : solidRender
|
|
53
51
|
dispose = renderer(() => {
|
|
54
|
-
// Signal owned by THIS render root (so it's never ownerless). `
|
|
52
|
+
// Signal owned by THIS render root (so it's never ownerless). `routeProps` are getters over it; a
|
|
55
53
|
// same-route settle calls `update(snapshot)` and the reading components update in place - no
|
|
56
|
-
// recompose, so the layout chain + focus + scroll survive.
|
|
57
|
-
// `exactOptionalPropertyTypes` (a getter is always present, returning `undefined` when idle -
|
|
58
|
-
// the documented "absent on idle" semantics for `submission`).
|
|
54
|
+
// recompose, so the layout chain + focus + scroll survive.
|
|
59
55
|
const [snapshot, setSnapshot] = createSignal(initial)
|
|
60
56
|
update = setSnapshot
|
|
61
|
-
const props = {
|
|
62
|
-
get data() {
|
|
63
|
-
return snapshot().data
|
|
64
|
-
},
|
|
65
|
-
get actionData() {
|
|
66
|
-
return snapshot().actionData
|
|
67
|
-
},
|
|
68
|
-
get pending() {
|
|
69
|
-
return snapshot().pending
|
|
70
|
-
},
|
|
71
|
-
get search() {
|
|
72
|
-
// The SAME searchOfChain the server ran, over this snapshot's URL - a same-route search change
|
|
73
|
-
// updates in place (fine-grained), matching the SSR value on hydration.
|
|
74
|
-
const s = snapshot()
|
|
75
|
-
const idx = s.path.indexOf("?")
|
|
76
|
-
return searchOfChain(
|
|
77
|
-
searchSchemas?.[s.routeId] ?? [],
|
|
78
|
-
idx === -1 ? "" : s.path.slice(idx),
|
|
79
|
-
)
|
|
80
|
-
},
|
|
81
|
-
get submission() {
|
|
82
|
-
return snapshot().submission
|
|
83
|
-
},
|
|
84
|
-
get boundaries() {
|
|
85
|
-
return snapshot().boundaries
|
|
86
|
-
},
|
|
87
|
-
} as RenderProps
|
|
88
57
|
// The matched chain is fixed for this mount (routeId is constant here); a route *change* disposes
|
|
89
58
|
// and re-mounts below, so the root render fn reads no signal and never re-runs on its own.
|
|
90
|
-
return compose(
|
|
59
|
+
return compose(
|
|
60
|
+
routes[initial.routeId] ?? [],
|
|
61
|
+
routeProps(snapshot, searchSchemas, matchChains),
|
|
62
|
+
)()
|
|
91
63
|
}, el)
|
|
92
64
|
}
|
|
93
65
|
|
|
94
66
|
mount(true)
|
|
95
67
|
router.subscribe(() => {
|
|
96
68
|
const snapshot = router.snapshot()
|
|
97
|
-
|
|
69
|
+
// Skip the in-flight tick and act once settled - unless the store names a different chain while
|
|
70
|
+
// pending, which is a `_loading` page taking the page slot for the navigation in flight.
|
|
71
|
+
if (snapshot.pending && snapshot.routeId === mountedRouteId) return
|
|
98
72
|
if (snapshot.routeId === mountedRouteId) {
|
|
99
73
|
update?.(snapshot) // same route → reactive in-place update (preserves layout/focus/scroll)
|
|
100
74
|
} else {
|
package/src/compose.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { RenderProps } from "@nifrajs/web"
|
|
2
2
|
import { type Component, createComponent, type JSX } from "solid-js"
|
|
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({})
|
|
@@ -22,10 +22,13 @@ export function compose(chain: readonly unknown[], props: RenderProps): () => JS
|
|
|
22
22
|
// Each layout receives its own loader data at its own index. Layouts are the chain's leading
|
|
23
23
|
// prefix, so `layoutData[i]` belongs to `chain[i]`; anything past that end (a client-only `_error`
|
|
24
24
|
// boundary marker, the page) reads `undefined` and is unaffected.
|
|
25
|
-
|
|
25
|
+
// Read through a getter: on the client `props` are getters over the router snapshot, so a
|
|
26
|
+
// same-route settle that brings new layout data updates the layout in place.
|
|
26
27
|
node = () =>
|
|
27
28
|
createComponent(Layout, {
|
|
28
|
-
data
|
|
29
|
+
get data() {
|
|
30
|
+
return props.layoutData?.[i] ?? null
|
|
31
|
+
},
|
|
29
32
|
...(props.boundaries !== undefined ? { boundaries: props.boundaries } : {}),
|
|
30
33
|
children: child(),
|
|
31
34
|
})
|
|
@@ -37,7 +40,12 @@ export function compose(chain: readonly unknown[], props: RenderProps): () => JS
|
|
|
37
40
|
createComponent(SearchContext.Provider, {
|
|
38
41
|
value: () => (props.search ?? EMPTY_SEARCH) as Record<string, unknown>,
|
|
39
42
|
get children() {
|
|
40
|
-
return
|
|
43
|
+
return createComponent(RenderPropsContext.Provider, {
|
|
44
|
+
value: props,
|
|
45
|
+
get children() {
|
|
46
|
+
return inner()
|
|
47
|
+
},
|
|
48
|
+
})
|
|
41
49
|
},
|
|
42
50
|
})
|
|
43
51
|
}
|
package/src/i18n.ts
CHANGED
|
@@ -5,7 +5,15 @@
|
|
|
5
5
|
* negotiated catalog on the server, and the client rebuilds the same formatter on hydrate (no
|
|
6
6
|
* mismatch). Imports only `solid-js` + `@nifrajs/i18n`; no JSX (`createComponent`).
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
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"
|
|
9
17
|
import {
|
|
10
18
|
type Accessor,
|
|
11
19
|
createComponent,
|
|
@@ -14,19 +22,29 @@ import {
|
|
|
14
22
|
type JSX,
|
|
15
23
|
useContext,
|
|
16
24
|
} from "solid-js"
|
|
25
|
+
import { Dynamic } from "solid-js/web"
|
|
17
26
|
|
|
18
27
|
const I18nContext = createContext<Accessor<Formatter>>()
|
|
19
28
|
|
|
20
|
-
export interface I18nProviderProps {
|
|
29
|
+
export interface I18nProviderProps extends FormatterOptions {
|
|
21
30
|
readonly locale: string
|
|
22
|
-
|
|
31
|
+
/** The catalog for `locale`, checked against the registered catalog type when one is declared. */
|
|
32
|
+
readonly messages: Translation
|
|
23
33
|
readonly children?: JSX.Element
|
|
24
34
|
}
|
|
25
35
|
|
|
26
|
-
/** Provide a {@link Formatter} (built from `locale` + `messages
|
|
27
|
-
* `
|
|
36
|
+
/** Provide a {@link Formatter} (built from `locale` + `messages`, with the optional `fallback`,
|
|
37
|
+
* `onMissing`, `timeZone` and `numberingSystem` of `createFormatter`) to the subtree. Memoized
|
|
38
|
+
* on those props, so switching locale rebuilds it. */
|
|
28
39
|
export function I18nProvider(props: I18nProviderProps): JSX.Element {
|
|
29
|
-
const formatter = createMemo(() =>
|
|
40
|
+
const formatter = createMemo(() =>
|
|
41
|
+
createFormatter(props.locale, props.messages, {
|
|
42
|
+
fallback: props.fallback,
|
|
43
|
+
onMissing: props.onMissing,
|
|
44
|
+
timeZone: props.timeZone,
|
|
45
|
+
numberingSystem: props.numberingSystem,
|
|
46
|
+
}),
|
|
47
|
+
)
|
|
30
48
|
return createComponent(I18nContext.Provider, {
|
|
31
49
|
value: formatter,
|
|
32
50
|
get children() {
|
|
@@ -35,7 +53,7 @@ export function I18nProvider(props: I18nProviderProps): JSX.Element {
|
|
|
35
53
|
})
|
|
36
54
|
}
|
|
37
55
|
|
|
38
|
-
/** Read the current {@link Formatter} (`{ locale, t, n, d }`). Throws if no `<I18nProvider>` is above.
|
|
56
|
+
/** Read the current {@link Formatter} (`{ locale, t, get, n, d }`). Throws if no `<I18nProvider>` is above.
|
|
39
57
|
* nifra switches locale by re-navigating, which re-runs the consuming component with the new catalog. */
|
|
40
58
|
export function useT(): Formatter {
|
|
41
59
|
const formatter = useContext(I18nContext)
|
|
@@ -44,3 +62,29 @@ export function useT(): Formatter {
|
|
|
44
62
|
}
|
|
45
63
|
return formatter()
|
|
46
64
|
}
|
|
65
|
+
|
|
66
|
+
/** Tag handlers for {@link rich}, by tag name: each receives its tag's content as one node. */
|
|
67
|
+
export type RichTags = Readonly<Record<string, (content: JSX.Element) => JSX.Element>>
|
|
68
|
+
|
|
69
|
+
const join = (chunks: RichChunks<JSX.Element>): JSX.Element =>
|
|
70
|
+
chunks.length === 0 ? undefined : chunks.length === 1 ? chunks[0] : [...chunks]
|
|
71
|
+
const SOLID_RICH: RichRenderer<JSX.Element> = {
|
|
72
|
+
join,
|
|
73
|
+
lineBreak: () => createComponent(Dynamic, { component: "br" }),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The message at `key` with its tags rendered by `tags`, as Solid nodes - no HTML, no `innerHTML`.
|
|
78
|
+
* `"Read the <link>terms</link>"` with `{ link: (content) => <a href="/terms">{content}</a> }`
|
|
79
|
+
* renders the link around "terms"; a tag with no handler renders its content as text, `<br/>` is a
|
|
80
|
+
* `<br>`, and interpolated values are always text. It runs once: wrap the call in a function
|
|
81
|
+
* (`{() => rich(t, key, tags, { n: count() })}`) to track signals in `vars`. See `@nifrajs/i18n/rich`.
|
|
82
|
+
*/
|
|
83
|
+
export function rich<M extends object = RegisteredMessages>(
|
|
84
|
+
formatter: Formatter<M>,
|
|
85
|
+
key: MessageKey<M>,
|
|
86
|
+
tags?: RichTags,
|
|
87
|
+
vars?: Readonly<Record<string, unknown>>,
|
|
88
|
+
): JSX.Element {
|
|
89
|
+
return renderRich(SOLID_RICH, formatter, key, tags, vars)
|
|
90
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,24 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @nifrajs/web-solid - the Solid render adapter for @nifrajs/web (server side)
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* @nifrajs/web-solid - the Solid render adapter for @nifrajs/web (server side). Streaming SSR via
|
|
3
|
+
* `renderToStream` + `generateHydrationScript`; the layout-chain fold is in `./compose`. Client
|
|
4
|
+
* hydration lives in `@nifrajs/web-solid/client` (Solid's browser build), and the Solid Babel
|
|
5
|
+
* Bun-plugin in `@nifrajs/web-solid/plugin`.
|
|
6
6
|
*/
|
|
7
|
-
import { transformAsync } from "@babel/core"
|
|
8
|
-
// @ts-expect-error - no type declarations published
|
|
9
|
-
import presetTypeScript from "@babel/preset-typescript"
|
|
10
7
|
import type { RenderAdapter } from "@nifrajs/web"
|
|
11
|
-
import { devServerCompile, normalizeFilePath, rewriteSsrImports } from "@nifrajs/web/plugins/kit"
|
|
12
|
-
// @ts-expect-error - no type declarations published
|
|
13
|
-
import presetSolid from "babel-preset-solid"
|
|
14
|
-
import type { BunPlugin } from "bun"
|
|
15
8
|
import {
|
|
16
9
|
generateHydrationScript,
|
|
17
10
|
renderToStream as solidRenderToStream,
|
|
18
11
|
renderToString as solidRenderToString,
|
|
19
12
|
} from "solid-js/web"
|
|
20
13
|
import { compose } from "./compose.ts"
|
|
21
|
-
import solidRefreshBunHot, { SOLID_HOT_MODULE } from "./refresh-babel.ts"
|
|
22
14
|
|
|
23
15
|
const HYDRATION_HEAD = generateHydrationScript()
|
|
24
16
|
|
|
@@ -32,98 +24,22 @@ export const solidAdapter: RenderAdapter = {
|
|
|
32
24
|
renderToString(chain, props) {
|
|
33
25
|
return solidRenderToString(compose(chain, props))
|
|
34
26
|
},
|
|
35
|
-
renderToStream(chain, props) {
|
|
27
|
+
renderToStream(chain, props, options) {
|
|
36
28
|
// Solid's `renderToStream` streams `Uint8Array` chunks into a Web `WritableStream` via
|
|
37
29
|
// `pipeTo` (fire-and-forget - returns void); pipe into a TransformStream and hand back the
|
|
38
30
|
// readable side. Suspense boundaries stream as they resolve; `generateHydrationScript()` (in
|
|
39
31
|
// <head>) seeds client hydration. A render failure errors `ts.readable`, which `renderPage`
|
|
40
|
-
// surfaces on the response body.
|
|
32
|
+
// surfaces on the response body. The nonce reaches the resource and boundary scripts Solid streams.
|
|
41
33
|
const ts = new TransformStream<Uint8Array, Uint8Array>()
|
|
42
|
-
|
|
34
|
+
const app = compose(chain, props)
|
|
35
|
+
const stream =
|
|
36
|
+
options?.nonce === undefined
|
|
37
|
+
? solidRenderToStream(app)
|
|
38
|
+
: solidRenderToStream(app, { nonce: options.nonce })
|
|
39
|
+
stream.pipeTo(ts.writable)
|
|
43
40
|
return ts.readable
|
|
44
41
|
},
|
|
45
42
|
hydrationHead() {
|
|
46
43
|
return HYDRATION_HEAD
|
|
47
44
|
},
|
|
48
45
|
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Bun build/runtime plugin that compiles Solid components with Babel - `generate: "ssr"`
|
|
52
|
-
* for the server, `"dom"` for the client, `hydratable` so SSR and hydrate align. Solid's
|
|
53
|
-
* reactive-JSX compiler ships only as a Babel plugin (no swc/native port); this runs at
|
|
54
|
-
* build time, on `.tsx` files only.
|
|
55
|
-
*
|
|
56
|
-
* Under a dev server the client half additionally runs `solid-refresh`, which is what makes an edit
|
|
57
|
-
* patch the running component tree instead of reloading the page - see {@link setupSolidRefresh}.
|
|
58
|
-
*/
|
|
59
|
-
export function solidBunPlugin(generate: "dom" | "ssr"): BunPlugin {
|
|
60
|
-
return {
|
|
61
|
-
name: `nifra-solid-${generate}`,
|
|
62
|
-
setup(build) {
|
|
63
|
-
const refresh =
|
|
64
|
-
generate === "dom" && devServerCompile() ? setupSolidRefresh(build) : undefined
|
|
65
|
-
// Match `.tsx`, tolerating a `?query` suffix (dev servers append one to bust Bun's import
|
|
66
|
-
// cache); strip it before reading the file off disk.
|
|
67
|
-
build.onLoad({ filter: /\.tsx(\?|$)/ }, async (args) => {
|
|
68
|
-
const path = normalizeFilePath(args.path)
|
|
69
|
-
const source = await Bun.file(path).text()
|
|
70
|
-
const result = await transformAsync(source, {
|
|
71
|
-
filename: path,
|
|
72
|
-
// babel applies presets last→first: strip TS first, then Solid transforms JSX.
|
|
73
|
-
presets: [
|
|
74
|
-
[presetSolid, { generate, hydratable: true }],
|
|
75
|
-
[presetTypeScript, { onlyRemoveTypeImports: true }],
|
|
76
|
-
],
|
|
77
|
-
...(refresh !== undefined
|
|
78
|
-
? { plugins: [[await refresh.babel(), REFRESH_OPTIONS], solidRefreshBunHot] }
|
|
79
|
-
: {}),
|
|
80
|
-
})
|
|
81
|
-
return { contents: rewriteSsrImports(result?.code ?? "", path, generate), loader: "js" }
|
|
82
|
-
})
|
|
83
|
-
},
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* `"esm"` is the closest of `solid-refresh`'s bundler modes to what Bun implements: one
|
|
89
|
-
* `accept(mod => …)` per module, falling back to `invalidate()` when a component's signature changed too
|
|
90
|
-
* much to patch in place. `"vite"` differs only by also calling a bare `accept()` first, which would make
|
|
91
|
-
* every module self-accepting twice over. The call it emits is still Vite-shaped, and
|
|
92
|
-
* `solidRefreshBunHot` translates it.
|
|
93
|
-
*
|
|
94
|
-
* `jsx: false` turns OFF the pass that lifts a component's returned JSX into a second, nested component.
|
|
95
|
-
* That extra component is an extra hydration level on the client and none on the server, so with it on,
|
|
96
|
-
* every hydratable page dies on `Unable to find DOM nodes for hydration key` before a single edit. It buys
|
|
97
|
-
* finer-grained patching of markup-only edits; correct hydration is worth more.
|
|
98
|
-
*/
|
|
99
|
-
const REFRESH_OPTIONS = { bundler: "esm", jsx: false } as const
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Wire `solid-refresh` into a dev-server client compile.
|
|
103
|
-
*
|
|
104
|
-
* Solid components are compiled reactive closures, not functions re-run on render, so a new module
|
|
105
|
-
* version cannot simply replace the old one - `solid-refresh` wraps every component in a registry and, on
|
|
106
|
-
* an update, patches the live instances, keeping signal state. Without it the update finds no accepting
|
|
107
|
-
* module, bubbles up to the generated entry, and Bun reloads the whole page.
|
|
108
|
-
*
|
|
109
|
-
* The Babel half loads lazily: it is dev-only, and this module is the adapter root every Solid SSR
|
|
110
|
-
* server imports.
|
|
111
|
-
*
|
|
112
|
-
* Both runtime specifiers need pinning, because the transform emits them INTO the app's own files, where
|
|
113
|
-
* a bare specifier resolves against the app. `solid-refresh` is only a transitive dependency there and
|
|
114
|
-
* need not be hoisted; `nifra:solid-hot` is this package's own bridge module and has no app-side name at
|
|
115
|
-
* all. Resolving both here is what keeps the emitted imports working without an app depending on either.
|
|
116
|
-
*/
|
|
117
|
-
function setupSolidRefresh(build: Parameters<BunPlugin["setup"]>[0]): {
|
|
118
|
-
babel: () => Promise<unknown>
|
|
119
|
-
} {
|
|
120
|
-
const runtime = Bun.resolveSync("solid-refresh", import.meta.dir)
|
|
121
|
-
build.onResolve({ filter: /^solid-refresh$/ }, () => ({ path: runtime }))
|
|
122
|
-
// `.ts` from source (Bun takes the package's `bun` export condition), `.js` from the published build.
|
|
123
|
-
const bridge = `${import.meta.dir}/refresh-hot${import.meta.path.endsWith(".ts") ? ".ts" : ".js"}`
|
|
124
|
-
build.onResolve({ filter: new RegExp(`^${SOLID_HOT_MODULE}$`) }, () => ({ path: bridge }))
|
|
125
|
-
let babel: Promise<unknown> | undefined
|
|
126
|
-
return {
|
|
127
|
-
babel: () => (babel ??= import("solid-refresh/babel").then((m) => m.default)),
|
|
128
|
-
}
|
|
129
|
-
}
|
package/src/mdx-runtime.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* component references, but Solid can't `createComponent("h1")` - its JSX is compile-time. So we map
|
|
5
5
|
* each Markdown-output tag to a Solid component that renders it via `<Dynamic component={tag}>`.
|
|
6
6
|
*/
|
|
7
|
-
import { createComponent } from "solid-js"
|
|
7
|
+
import { createComponent, type JSX, mergeProps } from "solid-js"
|
|
8
8
|
import { Dynamic } from "solid-js/web"
|
|
9
9
|
|
|
10
10
|
// The HTML element set CommonMark + GFM produce. An explicit map (not a Proxy): `@mdx-js` *spreads* the
|
|
@@ -42,13 +42,18 @@ const MARKDOWN_TAGS = [
|
|
|
42
42
|
"ul",
|
|
43
43
|
] as const
|
|
44
44
|
|
|
45
|
-
const components: Record<string, (props: Record<string, unknown>) =>
|
|
45
|
+
const components: Record<string, (props: Record<string, unknown>) => JSX.Element> = {}
|
|
46
46
|
for (const tag of MARKDOWN_TAGS) {
|
|
47
|
-
|
|
47
|
+
// The tag merges last, so a `component` prop in the content cannot swap the element, and
|
|
48
|
+
// `mergeProps` keeps Solid's prop getters live where a plain copy would read them once.
|
|
49
|
+
components[tag] = (props) => createComponent(Dynamic, mergeProps(props, { component: tag }))
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
/** Returns the intrinsic-element → Solid-component map MDX content uses. Merge in your own overrides by
|
|
51
53
|
* passing `components` to the MDX content component (they take precedence). */
|
|
52
|
-
export function useMDXComponents(): Record<
|
|
54
|
+
export function useMDXComponents(): Record<
|
|
55
|
+
string,
|
|
56
|
+
(props: Record<string, unknown>) => JSX.Element
|
|
57
|
+
> {
|
|
53
58
|
return components
|
|
54
59
|
}
|