@nifrajs/web-solid 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +3 -3
  2. package/dist/await.d.ts +2 -2
  3. package/dist/await.js +3 -3
  4. package/dist/client.d.ts +4 -4
  5. package/dist/client.d.ts.map +1 -1
  6. package/dist/client.js +19 -11
  7. package/dist/client.js.map +1 -1
  8. package/dist/compose.d.ts +4 -1
  9. package/dist/compose.d.ts.map +1 -1
  10. package/dist/compose.js +21 -3
  11. package/dist/compose.js.map +1 -1
  12. package/dist/content.d.ts +2 -2
  13. package/dist/content.js +2 -2
  14. package/dist/error.d.ts +1 -1
  15. package/dist/error.js +2 -2
  16. package/dist/fetcher.d.ts +4 -4
  17. package/dist/fetcher.js +4 -4
  18. package/dist/fn.d.ts +29 -0
  19. package/dist/fn.d.ts.map +1 -0
  20. package/dist/fn.js +28 -0
  21. package/dist/fn.js.map +1 -0
  22. package/dist/i18n.d.ts +1 -1
  23. package/dist/i18n.js +1 -1
  24. package/dist/image.d.ts +3 -3
  25. package/dist/image.js +1 -1
  26. package/dist/index.d.ts +2 -2
  27. package/dist/index.js +7 -7
  28. package/dist/mdx-runtime.js +2 -2
  29. package/dist/mdx.js +5 -5
  30. package/dist/query.d.ts +2 -2
  31. package/dist/query.js +2 -2
  32. package/dist/router.d.ts +45 -0
  33. package/dist/router.d.ts.map +1 -0
  34. package/dist/router.js +60 -0
  35. package/dist/router.js.map +1 -0
  36. package/package.json +16 -5
  37. package/src/await.ts +3 -3
  38. package/src/client.ts +22 -12
  39. package/src/compose.ts +23 -3
  40. package/src/content.ts +2 -2
  41. package/src/error.ts +2 -2
  42. package/src/fetcher.ts +4 -4
  43. package/src/fn.ts +42 -0
  44. package/src/i18n.ts +1 -1
  45. package/src/image.ts +3 -3
  46. package/src/index.ts +7 -7
  47. package/src/mdx-runtime.ts +2 -2
  48. package/src/mdx.ts +5 -5
  49. package/src/query.ts +2 -2
  50. package/src/router.ts +85 -0
package/dist/query.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { createQueryClient } from "@nifrajs/web";
2
2
  /**
3
- * `@nifrajs/web-solid/query` — Solid bindings for the keyed query-cache. `createQuery(key, fn)` bridges a
3
+ * `@nifrajs/web-solid/query` - Solid bindings for the keyed query-cache. `createQuery(key, fn)` bridges a
4
4
  * query's `subscribe`/`snapshot` store into a Solid signal and fetches on mount; `useQueryClient`
5
5
  * exposes `invalidateQueries` for after a mutation. Imports only `solid-js` + `@nifrajs/web` (never
6
6
  * `solid-js/web`), so route components use it on the server *and* client. No JSX.
7
7
  *
8
- * The query client is created lazily **client-side only** (`typeof window`) — on the server the
8
+ * The query client is created lazily **client-side only** (`typeof window`) - on the server the
9
9
  * accessor returns the idle/pending state (queries are client-first; loaders are the SSR data source).
10
10
  * The `key` is captured once (Solid setup runs once); a reactive key is a documented future enhancement.
11
11
  */
@@ -0,0 +1,45 @@
1
+ import type { InferOutput, StandardSchemaV1 } from "@nifrajs/core/server";
2
+ import { type Blocker, type BlockerFunction, type NavigateFunction } from "@nifrajs/web";
3
+ /**
4
+ * `@nifrajs/web-solid/router` - Solid routing bindings over the agnostic `@nifrajs/web` history layer:
5
+ * `useNavigate` (programmatic navigation), `useBlocker` (the unsaved-changes guard), and `useSearch`
6
+ * (the route's typed, validated search, as a reactive accessor). Navigation goes through `@nifrajs/web`'s
7
+ * DOM-free bridges (`getBrowserNavigate` / `registerBlocker`, populated by `installHistory`); `useSearch`
8
+ * reads the accessor `compose` provides on SSR + client mount alike. Imports only `solid-js`. No JSX.
9
+ */
10
+ import { type Accessor } from "solid-js";
11
+ export type { Blocker, BlockerFunction, BlockerState, NavigateFunction } from "@nifrajs/web";
12
+ /** The current route's validated search accessor, provided by `compose` on SSR + client mount alike
13
+ * (derived from the URL via the shared `searchOfChain`). Read via {@link useSearch}. */
14
+ export declare const SearchContext: import("solid-js").Context<Accessor<Record<string, unknown>> | undefined>;
15
+ /**
16
+ * The route's typed, validated search params as a reactive accessor - the SAME value the loader received
17
+ * as `ctx.search`. SSR-correct: `compose` provides it from the URL server-side and from the identical
18
+ * client-mount derivation, so a value rendered from it doesn't flash on hydration. Call it to read the
19
+ * current search (`useSearch()().page`); it updates in place on a same-route navigation. Pass the route's
20
+ * `searchSchema` as the type argument for its output type.
21
+ *
22
+ * ```tsx
23
+ * const search = useSearch<typeof searchSchema>() // Accessor<{ page: number }>
24
+ * return <span>{search().page}</span>
25
+ * ```
26
+ */
27
+ export declare function useSearch<Schema extends StandardSchemaV1 | undefined = undefined>(): Accessor<Schema extends StandardSchemaV1 ? InferOutput<Schema> : Record<string, unknown>>;
28
+ /** Get the {@link NavigateFunction} (a string path, a history delta, or a typed `{ to, search }` object).
29
+ * Resolves the browser navigate at call time, so it works as soon as `installHistory` has run and no-ops
30
+ * before then / on the server. */
31
+ export declare function useNavigate(): NavigateFunction;
32
+ /**
33
+ * Guard navigation away from a page with unsaved work, confirming with your OWN async UI. Mirrors
34
+ * react-router's `useBlocker`: pass a boolean or a `({ currentLocation, nextLocation }) => boolean`
35
+ * predicate, and get back a reactive {@link Blocker} accessor. When a navigation (an anchor click,
36
+ * `useNavigate`, or a browser back/forward) is intercepted, `blocker().state` becomes `"blocked"` and
37
+ * `proceed`/`reset` go live - show a dialog and call `proceed()` to continue or `reset()` to stay put.
38
+ * It also arms the browser's native "Leave site?" prompt on tab close / reload. Idle on the server and
39
+ * before hydration.
40
+ *
41
+ * The component body runs once, so to track a CHANGING flag pass a function -
42
+ * `useBlocker(() => isDirty())` - not a bare signal accessor's current value. A constant boolean is fine.
43
+ */
44
+ export declare function useBlocker(shouldBlock: boolean | BlockerFunction): Accessor<Blocker>;
45
+ //# sourceMappingURL=router.d.ts.map
@@ -0,0 +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;AACzE,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,eAAe,EAGpB,KAAK,gBAAgB,EAKtB,MAAM,cAAc,CAAA;AACrB;;;;;;GAMG;AACH,OAAO,EAAE,KAAK,QAAQ,EAAsD,MAAM,UAAU,CAAA;AAE5F,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAM5F;wFACwF;AACxF,eAAO,MAAM,aAAa,2EAAqD,CAAA;AAE/E;;;;;;;;;;;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 ADDED
@@ -0,0 +1,60 @@
1
+ import { getBrowserNavigate, IDLE_BLOCKER, registerBlocker, resolveNavigate, } from "@nifrajs/web";
2
+ /**
3
+ * `@nifrajs/web-solid/router` - Solid routing bindings over the agnostic `@nifrajs/web` history layer:
4
+ * `useNavigate` (programmatic navigation), `useBlocker` (the unsaved-changes guard), and `useSearch`
5
+ * (the route's typed, validated search, as a reactive accessor). Navigation goes through `@nifrajs/web`'s
6
+ * DOM-free bridges (`getBrowserNavigate` / `registerBlocker`, populated by `installHistory`); `useSearch`
7
+ * reads the accessor `compose` provides on SSR + client mount alike. Imports only `solid-js`. No JSX.
8
+ */
9
+ import { createContext, createSignal, onCleanup, useContext } from "solid-js";
10
+ // Frozen empty search + a stable accessor for a `useSearch` used outside a nifra route tree.
11
+ const EMPTY_SEARCH = Object.freeze({});
12
+ const EMPTY_SEARCH_ACCESSOR = () => EMPTY_SEARCH;
13
+ /** The current route's validated search accessor, provided by `compose` on SSR + client mount alike
14
+ * (derived from the URL via the shared `searchOfChain`). Read via {@link useSearch}. */
15
+ export const SearchContext = createContext();
16
+ /**
17
+ * The route's typed, validated search params as a reactive accessor - the SAME value the loader received
18
+ * as `ctx.search`. SSR-correct: `compose` provides it from the URL server-side and from the identical
19
+ * client-mount derivation, so a value rendered from it doesn't flash on hydration. Call it to read the
20
+ * current search (`useSearch()().page`); it updates in place on a same-route navigation. Pass the route's
21
+ * `searchSchema` as the type argument for its output type.
22
+ *
23
+ * ```tsx
24
+ * const search = useSearch<typeof searchSchema>() // Accessor<{ page: number }>
25
+ * return <span>{search().page}</span>
26
+ * ```
27
+ */
28
+ export function useSearch() {
29
+ return (useContext(SearchContext) ?? EMPTY_SEARCH_ACCESSOR);
30
+ }
31
+ /** Get the {@link NavigateFunction} (a string path, a history delta, or a typed `{ to, search }` object).
32
+ * Resolves the browser navigate at call time, so it works as soon as `installHistory` has run and no-ops
33
+ * before then / on the server. */
34
+ export function useNavigate() {
35
+ return ((to, options) => {
36
+ const navigate = getBrowserNavigate();
37
+ if (navigate === undefined)
38
+ return;
39
+ const resolved = resolveNavigate(to, options);
40
+ navigate(resolved.to, resolved.options);
41
+ });
42
+ }
43
+ /**
44
+ * Guard navigation away from a page with unsaved work, confirming with your OWN async UI. Mirrors
45
+ * react-router's `useBlocker`: pass a boolean or a `({ currentLocation, nextLocation }) => boolean`
46
+ * predicate, and get back a reactive {@link Blocker} accessor. When a navigation (an anchor click,
47
+ * `useNavigate`, or a browser back/forward) is intercepted, `blocker().state` becomes `"blocked"` and
48
+ * `proceed`/`reset` go live - show a dialog and call `proceed()` to continue or `reset()` to stay put.
49
+ * It also arms the browser's native "Leave site?" prompt on tab close / reload. Idle on the server and
50
+ * before hydration.
51
+ *
52
+ * The component body runs once, so to track a CHANGING flag pass a function -
53
+ * `useBlocker(() => isDirty())` - not a bare signal accessor's current value. A constant boolean is fine.
54
+ */
55
+ export function useBlocker(shouldBlock) {
56
+ const [blocker, setBlocker] = createSignal(IDLE_BLOCKER);
57
+ onCleanup(registerBlocker((args) => (typeof shouldBlock === "function" ? shouldBlock(args) : shouldBlock), setBlocker));
58
+ return blocker;
59
+ }
60
+ //# sourceMappingURL=router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,kBAAkB,EAClB,YAAY,EAIZ,eAAe,EACf,eAAe,GAChB,MAAM,cAAc,CAAA;AACrB;;;;;;GAMG;AACH,OAAO,EAAiB,aAAa,EAAE,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAI5F,6FAA6F;AAC7F,MAAM,YAAY,GAAsC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;AACzE,MAAM,qBAAqB,GAAsC,GAAG,EAAE,CAAC,YAAY,CAAA;AAEnF;wFACwF;AACxF,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,EAAqC,CAAA;AAE/E;;;;;;;;;;;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": "2.1.0",
4
- "description": "Solid render adapter for @nifrajs/web — SSR + hydration + the Solid Babel build plugin.",
3
+ "version": "2.3.0",
4
+ "description": "Solid render adapter for @nifrajs/web - SSR + hydration + the Solid Babel build plugin.",
5
5
  "keywords": [
6
6
  "bun",
7
7
  "typescript",
@@ -36,6 +36,11 @@
36
36
  "types": "./dist/fetcher.d.ts",
37
37
  "default": "./dist/fetcher.js"
38
38
  },
39
+ "./router": {
40
+ "bun": "./src/router.ts",
41
+ "types": "./dist/router.d.ts",
42
+ "default": "./dist/router.js"
43
+ },
39
44
  "./query": {
40
45
  "bun": "./src/query.ts",
41
46
  "types": "./dist/query.d.ts",
@@ -70,6 +75,11 @@
70
75
  "bun": "./src/mdx-runtime.ts",
71
76
  "types": "./dist/mdx-runtime.d.ts",
72
77
  "default": "./dist/mdx-runtime.js"
78
+ },
79
+ "./fn": {
80
+ "bun": "./src/fn.ts",
81
+ "types": "./dist/fn.d.ts",
82
+ "default": "./dist/fn.js"
73
83
  }
74
84
  },
75
85
  "files": [
@@ -87,9 +97,10 @@
87
97
  "dependencies": {
88
98
  "@babel/core": "^7.26.0",
89
99
  "@babel/preset-typescript": "^7.26.0",
90
- "@nifrajs/i18n": "2.1.0",
91
- "@nifrajs/image": "2.1.0",
92
- "@nifrajs/web": "2.1.0",
100
+ "@nifrajs/core": "2.3.0",
101
+ "@nifrajs/i18n": "2.3.0",
102
+ "@nifrajs/image": "2.3.0",
103
+ "@nifrajs/web": "2.3.0",
93
104
  "babel-preset-solid": "^1.9.3"
94
105
  },
95
106
  "peerDependencies": {
package/src/await.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import type { Deferred } from "@nifrajs/web"
2
2
  /**
3
- * `@nifrajs/web-solid/await` — the `<Await>` primitive for deferred loader data (`defer()`). Isomorphic
3
+ * `@nifrajs/web-solid/await` - the `<Await>` primitive for deferred loader data (`defer()`). Isomorphic
4
4
  * and imports only `solid-js` + a type from `@nifrajs/web` (never `solid-js/web`'s server or the Babel
5
5
  * plugin), so route components can use it without dragging server/build code into the client bundle.
6
6
  * No JSX (the package builds with plain `tsc`).
7
7
  *
8
8
  * Solid streams the boundary natively: on the server `createResource` awaits the deferred promise,
9
9
  * `<Suspense>` flushes the fallback then the resolved content mid-stream, and Solid serializes the
10
- * resource value into its hydration data — so the client hydrates the streamed content without a
10
+ * resource value into its hydration data - so the client hydrates the streamed content without a
11
11
  * re-fetch (it reads `_$HY`, not the registry promise).
12
12
  */
13
13
  import { createComponent, createResource, type JSX, Suspense } from "solid-js"
@@ -46,7 +46,7 @@ export function Await<T>(props: AwaitProps<T>): JSX.Element {
46
46
  get children() {
47
47
  // Errored? Render `errorFallback`, else rethrow to an outer <ErrorBoundary>. Check `data.error`
48
48
  // BEFORE `data()` (which would rethrow). Otherwise: `data()` suspends until the resource
49
- // settles (on the client Solid reads the serialized value — no re-fetch).
49
+ // settles (on the client Solid reads the serialized value - no re-fetch).
50
50
  if (data.error !== undefined) {
51
51
  if (props.errorFallback) return props.errorFallback(data.error)
52
52
  throw data.error
package/src/client.ts CHANGED
@@ -1,10 +1,10 @@
1
- import type { MountRouterOptions, RenderProps } from "@nifrajs/web"
1
+ import { type MountRouterOptions, type RenderProps, searchOfChain } from "@nifrajs/web"
2
2
  /**
3
- * @nifrajs/web-solid/client — Solid client runtime. `hydrate` hydrates a single SSR'd route;
4
- * `mountRouter` hydrates the initial route then drives navigation through a Solid signal — so a
3
+ * @nifrajs/web-solid/client - Solid client runtime. `hydrate` hydrates a single SSR'd route;
4
+ * `mountRouter` hydrates the initial route then drives navigation through a Solid signal - so a
5
5
  * same-route update (revalidation, a param change, an optimistic submit) updates **in place** via
6
6
  * fine-grained reactivity, preserving the layout chain + focus + scroll. Runs against Solid's client
7
- * build; kept in its own entry so SSR code never ships to the client bundle. No JSX — Solid's package
7
+ * build; kept in its own entry so SSR code never ships to the client bundle. No JSX - Solid's package
8
8
  * build is plain `tsc`, so this uses Solid's runtime functions directly, like `compose`.
9
9
  */
10
10
  import { createSignal } from "solid-js"
@@ -12,7 +12,7 @@ import { hydrate as solidHydrate, render as solidRender } from "solid-js/web"
12
12
  import { compose } from "./compose.ts"
13
13
  import { setMountedRouter } from "./fetcher.ts"
14
14
 
15
- // The `_error` boundary chain element — defined in its own module, re-exported here so nifra's client
15
+ // The `_error` boundary chain element - defined in its own module, re-exported here so nifra's client
16
16
  // codegen resolves it from `@nifrajs/web-solid/client` alongside `mountRouter`.
17
17
  export { errorBoundary } from "./error.ts"
18
18
 
@@ -25,22 +25,22 @@ export function hydrate(chain: readonly unknown[], props: RenderProps, container
25
25
  * Mount a Solid Router driven by the agnostic store. The first render *hydrates* the SSR'd chain. The
26
26
  * route's props are exposed as **getters over a signal** created inside the render root, so:
27
27
  *
28
- * - **Same route** (revalidation, a param change, an optimistic submit) — the snapshot signal is
28
+ * - **Same route** (revalidation, a param change, an optimistic submit) - the snapshot signal is
29
29
  * updated and only the components reading the changed props re-run; the layout chain stays mounted,
30
30
  * so **focus and scroll are preserved**. This is the case the old "dispose + re-render on every
31
31
  * navigation" implementation regressed.
32
- * - **Different route** — the matched chain genuinely changed (different page + layouts), so the root
32
+ * - **Different route** - the matched chain genuinely changed (different page + layouts), so the root
33
33
  * is disposed and the new chain is rendered. (Getter-props are a transparent reactive boundary, so
34
- * they don't shift Solid's structural hydration keys — unlike a `<Show>`, which would.)
34
+ * they don't shift Solid's structural hydration keys - unlike a `<Show>`, which would.)
35
35
  */
36
36
  export function mountRouter(options: MountRouterOptions): void {
37
- const { router, routes, container } = options
37
+ const { router, routes, searchSchemas, container } = options
38
38
  setMountedRouter(router) // expose it to createFetcher/useFetchers (same page, client-only)
39
39
  const el = container as Element
40
40
 
41
41
  let dispose: (() => void) | undefined
42
42
  let mountedRouteId: string | null = null
43
- // The setter for the currently-mounted route's reactive props — same-route settles push through it.
43
+ // The setter for the currently-mounted route's reactive props - same-route settles push through it.
44
44
  let update: ((snapshot: ReturnType<typeof router.snapshot>) => void) | null = null
45
45
 
46
46
  const mount = (first: boolean): void => {
@@ -49,9 +49,9 @@ export function mountRouter(options: MountRouterOptions): void {
49
49
  const renderer = first ? solidHydrate : solidRender
50
50
  dispose = renderer(() => {
51
51
  // Signal owned by THIS render root (so it's never ownerless). `props` are getters over it; a
52
- // same-route settle calls `update(snapshot)` and the reading components update in place — no
52
+ // same-route settle calls `update(snapshot)` and the reading components update in place - no
53
53
  // recompose, so the layout chain + focus + scroll survive. Cast bridges
54
- // `exactOptionalPropertyTypes` (a getter is always present, returning `undefined` when idle —
54
+ // `exactOptionalPropertyTypes` (a getter is always present, returning `undefined` when idle -
55
55
  // the documented "absent on idle" semantics for `submission`).
56
56
  const [snapshot, setSnapshot] = createSignal(initial)
57
57
  update = setSnapshot
@@ -65,6 +65,16 @@ export function mountRouter(options: MountRouterOptions): void {
65
65
  get pending() {
66
66
  return snapshot().pending
67
67
  },
68
+ get search() {
69
+ // The SAME searchOfChain the server ran, over this snapshot's URL - a same-route search change
70
+ // updates in place (fine-grained), matching the SSR value on hydration.
71
+ const s = snapshot()
72
+ const idx = s.path.indexOf("?")
73
+ return searchOfChain(
74
+ searchSchemas?.[s.routeId] ?? [],
75
+ idx === -1 ? "" : s.path.slice(idx),
76
+ )
77
+ },
68
78
  get submission() {
69
79
  return snapshot().submission
70
80
  },
package/src/compose.ts CHANGED
@@ -1,10 +1,17 @@
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"
4
+
5
+ // Frozen empty search so a render with no search context has a stable provider value.
6
+ const EMPTY_SEARCH: Readonly<Record<string, unknown>> = Object.freeze({})
3
7
 
4
8
  /**
5
9
  * Fold a layout chain (outermost layout → page) into a single Solid tree: the page
6
10
  * (innermost) receives `props` (the loader data); each layout wraps the child via its
7
- * `children`. Shared by the server adapter (renderToString) and the client (hydrate).
11
+ * `children`. The whole tree is wrapped in a {@link SearchContext} provider carrying an accessor of the
12
+ * validated `search` (threaded through `RenderProps` identically on SSR + client), so `useSearch` reads
13
+ * the same value on both sides - no hydration mismatch. Shared by the server adapter (renderToString) and
14
+ * the client (hydrate).
8
15
  */
9
16
  export function compose(chain: readonly unknown[], props: RenderProps): () => JSX.Element {
10
17
  const last = chain.length - 1
@@ -12,7 +19,20 @@ export function compose(chain: readonly unknown[], props: RenderProps): () => JS
12
19
  for (let i = last - 1; i >= 0; i--) {
13
20
  const Layout = chain[i] as Component<{ children: JSX.Element }>
14
21
  const child = node
15
- node = () => createComponent(Layout, { children: child() })
22
+ // Each layout receives its own loader data at its own index. Layouts are the chain's leading
23
+ // prefix, so `layoutData[i]` belongs to `chain[i]`; anything past that end (a client-only `_error`
24
+ // boundary marker, the page) reads `undefined` and is unaffected.
25
+ const layoutData = props.layoutData?.[i] ?? null
26
+ node = () => createComponent(Layout, { data: layoutData, children: child() })
16
27
  }
17
- return node
28
+ const inner = node
29
+ // The provider `value` is an ACCESSOR reading `props.search` - a getter over the mount's snapshot on
30
+ // the client (so a same-route search change updates in place), a static value on SSR.
31
+ return () =>
32
+ createComponent(SearchContext.Provider, {
33
+ value: () => (props.search ?? EMPTY_SEARCH) as Record<string, unknown>,
34
+ get children() {
35
+ return inner()
36
+ },
37
+ })
18
38
  }
package/src/content.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * `@nifrajs/web-solid/content` — render pre-rendered HTML (e.g. a `@nifrajs/content` collection entry's
2
+ * `@nifrajs/web-solid/content` - render pre-rendered HTML (e.g. a `@nifrajs/content` collection entry's
3
3
  * `.html`) into the DOM. Solid sets `innerHTML` reactively on a `<Dynamic>` element. Pairs with content
4
4
  * collections: `<Content html={entry.html} />`. No JSX (`createComponent`/`Dynamic`), so it builds with
5
5
  * plain `tsc`.
6
6
  *
7
7
  * Security: injects raw HTML, so `html` MUST be trusted (your own Markdown/content, rendered at
8
- * build/server time) — never pass user-supplied HTML without sanitizing it first.
8
+ * build/server time) - never pass user-supplied HTML without sanitizing it first.
9
9
  */
10
10
  import { createComponent, type JSX, mergeProps, splitProps } from "solid-js"
11
11
  import { Dynamic } from "solid-js/web"
package/src/error.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `errorBoundary` — the Solid error-boundary chain element for nifra's `_error.tsx`. Its own module
2
+ * `errorBoundary` - the Solid error-boundary chain element for nifra's `_error.tsx`. Its own module
3
3
  * (imports only `solid-js`) so the client codegen can import it from `@nifrajs/web-solid/client` (which
4
4
  * re-exports it). No JSX (`createComponent`), so the package builds with plain `tsc`.
5
5
  */
@@ -9,7 +9,7 @@ import { createComponent, ErrorBoundary, type JSX } from "solid-js"
9
9
  * Build an error-boundary chain element bound to `fallback` (a route's `_error` component). nifra's
10
10
  * client codegen inserts it before the page in the matched chain; a render error in the subtree renders
11
11
  * `fallback` with `{ data: { name, message } }` (via Solid's `<ErrorBoundary>`) instead of crashing.
12
- * DOM-transparent (renders its children directly — no wrapper element), so it never disturbs hydration.
12
+ * DOM-transparent (renders its children directly - no wrapper element), so it never disturbs hydration.
13
13
  */
14
14
  export function errorBoundary(fallback: unknown): unknown {
15
15
  const Fallback = fallback as (props: { data: { name: string; message: string } }) => JSX.Element
package/src/fetcher.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { ClientRouter, Fetcher, FetcherState } from "@nifrajs/web"
2
2
  /**
3
- * `@nifrajs/web-solid/fetcher` — Solid bindings for concurrent fetchers. `createFetcher(key)` bridges an
3
+ * `@nifrajs/web-solid/fetcher` - Solid bindings for concurrent fetchers. `createFetcher(key)` bridges an
4
4
  * independent {@link Fetcher}'s `subscribe`/`snapshot` store into a Solid signal (the pattern the F16
5
5
  * spike confirmed) and returns its reactive state accessor plus `load`/`submit`; `useFetchers()`
6
6
  * bridges the whole live collection. Imports only `solid-js` (never `solid-js/web`), so route
@@ -8,7 +8,7 @@ import type { ClientRouter, Fetcher, FetcherState } from "@nifrajs/web"
8
8
  * bundle. No JSX (the package builds with plain `tsc`).
9
9
  *
10
10
  * The router that owns the fetchers registers itself via `setMountedRouter` from `mountRouter`. On the
11
- * server (no mount) the accessors return an idle state — fetchers are client-only — so SSR markup is
11
+ * server (no mount) the accessors return an idle state - fetchers are client-only - so SSR markup is
12
12
  * unchanged and the first client render after `mountRouter` takes over cleanly.
13
13
  */
14
14
  import { type Accessor, createSignal, onCleanup } from "solid-js"
@@ -16,7 +16,7 @@ import { type Accessor, createSignal, onCleanup } from "solid-js"
16
16
  // The active client router (set by `mountRouter`). Module-scoped: one app per page, client-only.
17
17
  let mountedRouter: ClientRouter | undefined
18
18
 
19
- /** Register (or clear) the router that owns fetchers — called by `mountRouter`. Not for app use. */
19
+ /** Register (or clear) the router that owns fetchers - called by `mountRouter`. Not for app use. */
20
20
  export function setMountedRouter(router: ClientRouter | undefined): void {
21
21
  mountedRouter = router
22
22
  }
@@ -50,7 +50,7 @@ export function createFetcher(key: string): FetcherHandle {
50
50
  }
51
51
 
52
52
  /**
53
- * Bind the whole live fetcher collection — for a global busy view. Returns a reactive accessor; read
53
+ * Bind the whole live fetcher collection - for a global busy view. Returns a reactive accessor; read
54
54
  * each entry's `.snapshot()` for its state. Updates whenever any fetcher transitions or one is created.
55
55
  */
56
56
  export function useFetchers(): Accessor<readonly Fetcher[]> {
package/src/fn.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `@nifrajs/web-solid/fn` - Solid binding for a server function's pending/error state.
3
+ *
4
+ * Calling a server function needs no hook: the client stub is `(input) => Promise<Output>`. This adds
5
+ * the part a component usually wants around it - in flight, last result, last error - and nothing more.
6
+ *
7
+ * The state machine is `@nifrajs/web`'s `createServerFnStore`, shared with every other adapter, so
8
+ * "is it pending" has one answer rather than five that drift. This file contributes only a signal fed by the store.
9
+ */
10
+
11
+ import type { ClientServerFn, ServerFnReference } from "@nifrajs/web/fn"
12
+ import { createServerFnStore, type ServerFnState } from "@nifrajs/web/fn-state"
13
+ import { type Accessor, createSignal, onCleanup } from "solid-js"
14
+
15
+ /** A server function's state accessor plus the call itself. */
16
+ export interface ServerFnHandle<Input, Output> {
17
+ /** Reactive state - read it inside JSX so Solid tracks it. */
18
+ readonly state: Accessor<ServerFnState<Output>>
19
+ /** Invoke it. Records state, and rejects on failure - attach `.catch` if you only render state. */
20
+ readonly call: (input: Input) => Promise<Output>
21
+ /** Back to idle. */
22
+ readonly reset: () => void
23
+ }
24
+
25
+ /**
26
+ * Track one server function's call state.
27
+ *
28
+ * const addTodo = useServerFn(fns.addTodo)
29
+ * <button disabled={addTodo.state().pending} onClick={() => addTodo.call({ text }).catch(() => {})}>
30
+ */
31
+ export function useServerFn<Input, Output>(
32
+ fn: ServerFnReference<Input, Output>,
33
+ ): ServerFnHandle<Input, Output> {
34
+ const store = createServerFnStore(fn as ClientServerFn<Input, Output>)
35
+ // Seeded from the store, not from the idle constant. They are the same value here - the store is
36
+ // created one line up and cannot have moved - so this is correctness that does not depend on that
37
+ // staying true. Svelte had exactly this line reading the constant, and when its subscription turned
38
+ // out to be lazy the first render showed idle for a call that had already finished.
39
+ const [state, setState] = createSignal<ServerFnState<Output>>(store.snapshot())
40
+ onCleanup(store.subscribe(() => setState(() => store.snapshot())))
41
+ return { state, call: store.call, reset: store.reset }
42
+ }
package/src/i18n.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `@nifrajs/web-solid/i18n` — Solid bindings for `@nifrajs/i18n`. `<I18nProvider locale messages>` builds a
2
+ * `@nifrajs/web-solid/i18n` - Solid bindings for `@nifrajs/i18n`. `<I18nProvider locale messages>` builds a
3
3
  * `Formatter` (a `createMemo`, so switching locale rebuilds it) and provides it; `useT()` reads it.
4
4
  * Both `locale` + `messages` are serializable, so a loader returns them, the page renders with the
5
5
  * negotiated catalog on the server, and the client rebuilds the same formatter on hydrate (no
package/src/image.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `@nifrajs/web-solid/image` — a CLS-safe responsive Solid `<Image>`. A thin wrapper over `resolveImage`
2
+ * `@nifrajs/web-solid/image` - a CLS-safe responsive Solid `<Image>`. A thin wrapper over `resolveImage`
3
3
  * from `@nifrajs/image`: it computes the responsive `<img>` attributes (`src`/`srcset`/`sizes`/`width`/
4
4
  * `height`/`loading`/`decoding`/`fetchpriority` via `toHtmlAttrs`) and renders an `<img>` through
5
5
  * Solid's `<Dynamic>`. Extra DOM props (`class`, `style`, `id`, `data-*`, handlers) pass through
@@ -10,7 +10,7 @@ import { type ImageLoader, type ImageProps, resolveImage, toHtmlAttrs } from "@n
10
10
  import { createComponent, type JSX, mergeProps, splitProps } from "solid-js"
11
11
  import { Dynamic } from "solid-js/web"
12
12
 
13
- /** Attributes `resolveImage` computes — callers can't override them via DOM passthrough. */
13
+ /** Attributes `resolveImage` computes - callers can't override them via DOM passthrough. */
14
14
  type ComputedAttrs =
15
15
  | "src"
16
16
  | "srcset"
@@ -25,7 +25,7 @@ type ComputedAttrs =
25
25
  export interface ImageComponentProps
26
26
  extends ImageProps,
27
27
  Omit<JSX.ImgHTMLAttributes<HTMLImageElement>, ComputedAttrs | keyof ImageProps> {
28
- /** CDN URL builder. Defaults to the identity loader (no transform — still CLS-safe + lazy). */
28
+ /** CDN URL builder. Defaults to the identity loader (no transform - still CLS-safe + lazy). */
29
29
  readonly loader?: ImageLoader
30
30
  }
31
31
 
package/src/index.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  /**
2
- * @nifrajs/web-solid — the Solid render adapter for @nifrajs/web (server side) + the Solid Babel
2
+ * @nifrajs/web-solid - the Solid render adapter for @nifrajs/web (server side) + the Solid Babel
3
3
  * Bun-plugin. Streaming SSR via `renderToStream` + `generateHydrationScript`; the layout-chain
4
4
  * fold is in `./compose`. Client hydration lives in `@nifrajs/web-solid/client` (Solid's browser
5
5
  * build).
6
6
  */
7
7
  import { transformAsync } from "@babel/core"
8
- // @ts-expect-error — no type declarations published
8
+ // @ts-expect-error - no type declarations published
9
9
  import presetTypeScript from "@babel/preset-typescript"
10
10
  import type { RenderAdapter } from "@nifrajs/web"
11
- // @ts-expect-error — no type declarations published
11
+ // @ts-expect-error - no type declarations published
12
12
  import presetSolid from "babel-preset-solid"
13
13
  import type { BunPlugin } from "bun"
14
14
  import {
@@ -20,19 +20,19 @@ import { compose } from "./compose.ts"
20
20
 
21
21
  const HYDRATION_HEAD = generateHydrationScript()
22
22
 
23
- /** The Solid server render adapter — pass to @nifrajs/web's `renderPage`. */
23
+ /** The Solid server render adapter - pass to @nifrajs/web's `renderPage`. */
24
24
  export const solidAdapter: RenderAdapter = {
25
25
  // Synchronous one-pass render for non-deferred pages (renderPage's buffered fast path). Solid's
26
26
  // `renderToString` emits the same hydratable markup (same `data-hk` keys seeded by
27
27
  // `generateHydrationScript()`) as `renderToStream`, but skips the TransformStream + Solid's
28
- // streaming machinery — the heaviest of the five renderers on Bun. A page that defer()s/Suspends
28
+ // streaming machinery - the heaviest of the five renderers on Bun. A page that defer()s/Suspends
29
29
  // takes `renderToStream` below (progressive resolution needs it).
30
30
  renderToString(chain, props) {
31
31
  return solidRenderToString(compose(chain, props))
32
32
  },
33
33
  renderToStream(chain, props) {
34
34
  // Solid's `renderToStream` streams `Uint8Array` chunks into a Web `WritableStream` via
35
- // `pipeTo` (fire-and-forget — returns void); pipe into a TransformStream and hand back the
35
+ // `pipeTo` (fire-and-forget - returns void); pipe into a TransformStream and hand back the
36
36
  // readable side. Suspense boundaries stream as they resolve; `generateHydrationScript()` (in
37
37
  // <head>) seeds client hydration. A render failure errors `ts.readable`, which `renderPage`
38
38
  // surfaces on the response body.
@@ -46,7 +46,7 @@ export const solidAdapter: RenderAdapter = {
46
46
  }
47
47
 
48
48
  /**
49
- * Bun build/runtime plugin that compiles Solid components with Babel — `generate: "ssr"`
49
+ * Bun build/runtime plugin that compiles Solid components with Babel - `generate: "ssr"`
50
50
  * for the server, `"dom"` for the client, `hydratable` so SSR and hydrate align. Solid's
51
51
  * reactive-JSX compiler ships only as a Babel plugin (no swc/native port); this runs at
52
52
  * build time, on `.tsx` files only.
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Solid MDX runtime — the `useMDXComponents` provider that `@nifrajs/web-solid/mdx`'s compiled MDX imports
2
+ * Solid MDX runtime - the `useMDXComponents` provider that `@nifrajs/web-solid/mdx`'s compiled MDX imports
3
3
  * (via `@mdx-js`'s `providerImportSource`). MDX emits intrinsic elements (`h1`, `p`, …) as *string*
4
- * component references, but Solid can't `createComponent("h1")` — its JSX is compile-time. So we map
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
7
  import { createComponent } from "solid-js"
package/src/mdx.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  /**
2
- * `solidMdxBunPlugin` — compile `.mdx` routes/files to **Solid** components. MDX is JSX-oriented but
2
+ * `solidMdxBunPlugin` - compile `.mdx` routes/files to **Solid** components. MDX is JSX-oriented but
3
3
  * Solid's JSX is compile-time, so this: (1) compiles MDX → JSX with `@mdx-js` (keeping JSX), pointing
4
4
  * intrinsic elements at `@nifrajs/web-solid/mdx-runtime` (which renders them via Solid's `<Dynamic>`), then
5
- * (2) runs `babel-preset-solid` — the same transform `solidBunPlugin` applies to `.tsx`. Pass it to
5
+ * (2) runs `babel-preset-solid` - the same transform `solidBunPlugin` applies to `.tsx`. Pass it to
6
6
  * `buildClient`/`buildServer`'s `plugins` (`"dom"` for the client, `"ssr"` for the server), like
7
7
  * `solidBunPlugin`. `@mdx-js/mdx` is an optional peer (lazy-loaded at build time).
8
8
  */
9
9
  import { transformAsync } from "@babel/core"
10
- // @ts-expect-error — no type declarations published
10
+ // @ts-expect-error - no type declarations published
11
11
  import presetTypeScript from "@babel/preset-typescript"
12
- // @ts-expect-error — no type declarations published
12
+ // @ts-expect-error - no type declarations published
13
13
  import presetSolid from "babel-preset-solid"
14
14
  import type { BunPlugin } from "bun"
15
15
 
@@ -31,7 +31,7 @@ export function solidMdxBunPlugin(generate: "dom" | "ssr"): BunPlugin {
31
31
  compile = ((await import(MDX_MODULE)) as MdxCompiler).compile
32
32
  } catch {
33
33
  throw new Error(
34
- "@nifrajs/web-solid/mdx: compiling `.mdx` needs the `@mdx-js/mdx` package — install it (`bun add @mdx-js/mdx`).",
34
+ "@nifrajs/web-solid/mdx: compiling `.mdx` needs the `@mdx-js/mdx` package - install it (`bun add @mdx-js/mdx`).",
35
35
  )
36
36
  }
37
37
  build.onLoad({ filter: /\.mdx(\?|$)/ }, async (args) => {
package/src/query.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import { createQueryClient, type QueryClient, type QueryState } from "@nifrajs/web"
2
2
  /**
3
- * `@nifrajs/web-solid/query` — Solid bindings for the keyed query-cache. `createQuery(key, fn)` bridges a
3
+ * `@nifrajs/web-solid/query` - Solid bindings for the keyed query-cache. `createQuery(key, fn)` bridges a
4
4
  * query's `subscribe`/`snapshot` store into a Solid signal and fetches on mount; `useQueryClient`
5
5
  * exposes `invalidateQueries` for after a mutation. Imports only `solid-js` + `@nifrajs/web` (never
6
6
  * `solid-js/web`), so route components use it on the server *and* client. No JSX.
7
7
  *
8
- * The query client is created lazily **client-side only** (`typeof window`) — on the server the
8
+ * The query client is created lazily **client-side only** (`typeof window`) - on the server the
9
9
  * accessor returns the idle/pending state (queries are client-first; loaders are the SSR data source).
10
10
  * The `key` is captured once (Solid setup runs once); a reactive key is a documented future enhancement.
11
11
  */