@nifrajs/web-solid 3.3.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.
@@ -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;;;GAGG;AACH,wBAAgB,UAAU,CACxB,QAAQ,EAAE,MAAM,WAAW,EAC3B,aAAa,EAAE,kBAAkB,CAAC,eAAe,CAAC,EAClD,WAAW,CAAC,EAAE,kBAAkB,CAAC,aAAa,CAAC,GAC9C,WAAW,CAyCb"}
@@ -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
@@ -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,EAGjB,MAAM,cAAc,CAAA;AASrB;;;;;;GAMG;AACH,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,OAAO,EAKb,MAAM,UAAU,CAAA;AAEjB,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,gBAAgB,EAAE,CAAA;AAiBxE,eAAO,MAAM,aAAa,wDACoE,CAAA;AAG9F;;;;;;;;;;;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"}
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
@@ -1 +1 @@
1
- {"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAYA,mFAAmF;AACnF,qEAAqE;AACrE,OAAO,EACL,kBAAkB,EAClB,YAAY,EACZ,eAAe,EACf,eAAe,GAChB,MAAM,qBAAqB,CAAA;AAC5B;;;;;;GAMG;AACH,OAAO,EAGL,aAAa,EACb,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;;;;;;;;;;;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"}
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": "3.3.0",
4
- "description": "Solid render adapter for @nifrajs/web - SSR + hydration + the Solid Babel build plugin.",
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": "3.3.0",
103
- "@nifrajs/i18n": "3.3.0",
104
- "@nifrajs/image": "3.3.0",
105
- "@nifrajs/web": "3.3.0",
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). `props` are getters over it; a
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. Cast bridges
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(routes[initial.routeId] ?? [], props)()
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
- if (snapshot.pending) return // skip the in-flight tick; act once settled
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
- const layoutData = props.layoutData?.[i] ?? null
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: layoutData,
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 inner()
43
+ return createComponent(RenderPropsContext.Provider, {
44
+ value: props,
45
+ get children() {
46
+ return inner()
47
+ },
48
+ })
41
49
  },
42
50
  })
43
51
  }
package/src/content.ts CHANGED
@@ -7,12 +7,14 @@
7
7
  * Security: injects raw HTML, so `html` MUST be trusted (your own Markdown/content, rendered at
8
8
  * build/server time) - never pass user-supplied HTML without sanitizing it first.
9
9
  */
10
+
11
+ import type { TrustedHtml } from "@nifrajs/web"
10
12
  import { createComponent, type JSX, mergeProps, splitProps } from "solid-js"
11
13
  import { Dynamic } from "solid-js/web"
12
14
 
13
15
  export interface ContentProps {
14
16
  /** Trusted, pre-rendered HTML (e.g. `entry.html`). */
15
- readonly html: string
17
+ readonly html: TrustedHtml
16
18
  /** Wrapper element (default `"div"`). */
17
19
  readonly as?: string
18
20
  /** DOM passthrough (`class`, `style`, `id`, `data-*`, handlers). */
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 { createFormatter, type Formatter, type Messages } from "@nifrajs/i18n"
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
- readonly messages: Messages
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`) to the subtree. Memoized on
27
- * `locale`/`messages`, so switching locale rebuilds it. */
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(() => createFormatter(props.locale, props.messages))
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) + the Solid Babel
3
- * Bun-plugin. Streaming SSR via `renderToStream` + `generateHydrationScript`; the layout-chain
4
- * fold is in `./compose`. Client hydration lives in `@nifrajs/web-solid/client` (Solid's browser
5
- * build).
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
- solidRenderToStream(compose(chain, props)).pipeTo(ts.writable)
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
- }
@@ -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>) => unknown> = {}
45
+ const components: Record<string, (props: Record<string, unknown>) => JSX.Element> = {}
46
46
  for (const tag of MARKDOWN_TAGS) {
47
- components[tag] = (props) => createComponent(Dynamic, Object.assign({ component: tag }, props))
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<string, (props: Record<string, unknown>) => unknown> {
54
+ export function useMDXComponents(): Record<
55
+ string,
56
+ (props: Record<string, unknown>) => JSX.Element
57
+ > {
53
58
  return components
54
59
  }