@nifrajs/web-solid 5.9.0 → 5.11.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,14 @@
1
+ import { createContext } from "solid-js";
2
+ /** Shared provider identities across the Bun server and Vite's SSR module graph. */
3
+ const SEARCH_CONTEXT_SLOT = Symbol.for("nifra.web-solid.search-context");
4
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: The namespaced Symbol.for key shares one matching Solid Context across Bun and Vite's duplicate module graphs in this SSR process.
5
+ const searchContextSlot = globalThis;
6
+ export const SearchContext = searchContextSlot[SEARCH_CONTEXT_SLOT] ?? createContext();
7
+ searchContextSlot[SEARCH_CONTEXT_SLOT] = SearchContext;
8
+ /** Props for the composed route chain; a global singleton for the same reason as SearchContext. */
9
+ const PROPS_CONTEXT_SLOT = Symbol.for("nifra.web-solid.props-context");
10
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: The namespaced Symbol.for key shares one matching Solid Context across Bun and Vite's duplicate module graphs in this SSR process.
11
+ const propsContextSlot = globalThis;
12
+ export const RenderPropsContext = propsContextSlot[PROPS_CONTEXT_SLOT] ?? createContext();
13
+ propsContextSlot[PROPS_CONTEXT_SLOT] = RenderPropsContext;
14
+ //# sourceMappingURL=router-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router-context.js","sourceRoot":"","sources":["../src/router-context.ts"],"names":[],"mappings":"AACA,OAAO,EAA+B,aAAa,EAAE,MAAM,UAAU,CAAA;AAErE,oFAAoF;AACpF,MAAM,mBAAmB,GAAG,MAAM,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAA;AACxE,oMAAoM;AACpM,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,mGAAmG;AACnG,MAAM,kBAAkB,GAAG,MAAM,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAA;AACtE,oMAAoM;AACpM,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"}
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, RenderProps, UIMatch } from "@nifrajs/web";
2
+ import type { Blocker, BlockerFunction, BlockerState, NavigateFunction, 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`
@@ -7,11 +7,14 @@ import type { Blocker, BlockerFunction, BlockerState, NavigateFunction, RenderPr
7
7
  * DOM-free bridges (`getBrowserNavigate` / `registerBlocker`, populated by `installHistory`); `useSearch`
8
8
  * reads the accessor `compose` provides on SSR + client mount alike. Imports only `solid-js`. No JSX.
9
9
  */
10
- import { type Accessor, type Context } from "solid-js";
10
+ import { type Accessor } from "solid-js";
11
+ import { RenderPropsContext, SearchContext } from "./router-context.js";
12
+ export { href, rawHref } from "@nifrajs/web/client";
11
13
  export { onPageHide, onPageShow, usePageVisibility } from "./page-visibility.js";
14
+ export type { RouteSlotController, RouteSlotProps } from "./route-slot.tsx";
15
+ export { RouteSlot, useRouteSlot } from "./route-slot.tsx";
12
16
  export type { Blocker, BlockerFunction, BlockerState, NavigateFunction, UIMatch };
13
- export declare const SearchContext: Context<Accessor<Record<string, unknown>> | undefined>;
14
- export declare const RenderPropsContext: Context<RenderProps | undefined>;
17
+ export { RenderPropsContext, SearchContext };
15
18
  /**
16
19
  * The rendered chain - each layout, then the page - with the URL prefix, params and loader data each
17
20
  * one owns, plus its `handle` export, as a reactive accessor. The same list on the server render and
@@ -38,7 +41,7 @@ export declare function useMatches(): Accessor<readonly UIMatch[]>;
38
41
  * ```
39
42
  */
40
43
  export declare function useSearch<Schema extends StandardSchemaV1 | undefined = undefined>(): Accessor<Schema extends StandardSchemaV1 ? InferOutput<Schema> : Record<string, unknown>>;
41
- /** Get the {@link NavigateFunction} (a string path, a history delta, or a typed `{ to, search }` object).
44
+ /** Get the {@link NavigateFunction} (an `Href` from `href()` or `rawHref()`, or a history delta).
42
45
  * Resolves the browser navigate at call time, so it works as soon as `installHistory` has run and no-ops
43
46
  * before then / on the server. */
44
47
  export declare function useNavigate(): NavigateFunction;
@@ -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,EAChB,WAAW,EACX,OAAO,EACR,MAAM,cAAc,CAAA;AAOrB;;;;;;GAMG;AACH,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,OAAO,EAMb,MAAM,UAAU,CAAA;AAGjB,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAChF,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,CAE9C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,OAAO,GAAG,eAAe,GAAG,QAAQ,CAAC,OAAO,CAAC,CAYpF"}
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,EAChB,OAAO,EACR,MAAM,cAAc,CAAA;AAOrB;;;;;;GAMG;AACH,OAAO,EAAE,KAAK,QAAQ,EAAmD,MAAM,UAAU,CAAA;AAEzF,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAEvE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AACnD,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAChF,YAAY,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAC3E,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC1D,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,gBAAgB,EAAE,OAAO,EAAE,CAAA;AACjF,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,CAAA;AAe5C;;;;;;;;;;;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,CAE9C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,OAAO,GAAG,eAAe,GAAG,QAAQ,CAAC,OAAO,CAAC,CAYpF"}
package/dist/router.js CHANGED
@@ -11,9 +11,13 @@ import { navigateNow } from "@nifrajs/web/internal/navigate-runtime";
11
11
  * DOM-free bridges (`getBrowserNavigate` / `registerBlocker`, populated by `installHistory`); `useSearch`
12
12
  * reads the accessor `compose` provides on SSR + client mount alike. Imports only `solid-js`. No JSX.
13
13
  */
14
- import { createContext, createMemo, createSignal, onCleanup, useContext, } from "solid-js";
14
+ import { createMemo, createSignal, onCleanup, useContext } from "solid-js";
15
15
  import { PageContext } from "./page-visibility.js";
16
+ import { RenderPropsContext, SearchContext } from "./router-context.js";
17
+ export { href, rawHref } from "@nifrajs/web/client";
16
18
  export { onPageHide, onPageShow, usePageVisibility } from "./page-visibility.js";
19
+ export { RouteSlot, useRouteSlot } from "./route-slot.jsx";
20
+ export { RenderPropsContext, SearchContext };
17
21
  // Frozen empty search + a stable accessor for a `useSearch` used outside a nifra route tree.
18
22
  const EMPTY_SEARCH = Object.freeze({});
19
23
  const EMPTY_SEARCH_ACCESSOR = () => EMPTY_SEARCH;
@@ -24,16 +28,6 @@ const EMPTY_SEARCH_ACCESSOR = () => EMPTY_SEARCH;
24
28
  * code under Bun provides; route modules through Vite's SSR runner read), and Solid matches provider
25
29
  * to reader by context object identity - two `createContext` results make `useSearch` SSR-render its
26
30
  * empty default in dev. */
27
- const SEARCH_CONTEXT_SLOT = Symbol.for("nifra.web-solid.search-context");
28
- const searchContextSlot = globalThis;
29
- export const SearchContext = searchContextSlot[SEARCH_CONTEXT_SLOT] ?? createContext();
30
- searchContextSlot[SEARCH_CONTEXT_SLOT] = SearchContext;
31
- /** The props `compose` rendered the chain with - getters over the router snapshot on the client - which
32
- * {@link useMatches} reads. A `globalThis` singleton for the same reason as {@link SearchContext}. */
33
- const PROPS_CONTEXT_SLOT = Symbol.for("nifra.web-solid.props-context");
34
- const propsContextSlot = globalThis;
35
- export const RenderPropsContext = propsContextSlot[PROPS_CONTEXT_SLOT] ?? createContext();
36
- propsContextSlot[PROPS_CONTEXT_SLOT] = RenderPropsContext;
37
31
  const NO_MATCHES = Object.freeze([]);
38
32
  /**
39
33
  * The rendered chain - each layout, then the page - with the URL prefix, params and loader data each
@@ -68,7 +62,7 @@ export function useMatches() {
68
62
  export function useSearch() {
69
63
  return (useContext(SearchContext) ?? EMPTY_SEARCH_ACCESSOR);
70
64
  }
71
- /** Get the {@link NavigateFunction} (a string path, a history delta, or a typed `{ to, search }` object).
65
+ /** Get the {@link NavigateFunction} (an `Href` from `href()` or `rawHref()`, or a history delta).
72
66
  * Resolves the browser navigate at call time, so it works as soon as `installHistory` has run and no-ops
73
67
  * before then / on the server. */
74
68
  export function useNavigate() {
@@ -1 +1 @@
1
- {"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAYA,mFAAmF;AACnF,qEAAqE;AACrE,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AACnE,6EAA6E;AAC7E,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAA;AACpE,OAAO,EAAE,WAAW,EAAE,MAAM,wCAAwC,CAAA;AACpE;;;;;;GAMG;AACH,OAAO,EAGL,aAAa,EACb,UAAU,EACV,YAAY,EACZ,SAAS,EACT,UAAU,GACX,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAElD,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAGhF,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,WAAW,CAAA;AACpB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU,CAAC,WAAsC;IAC/D,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,YAAY,CAAU,YAAY,CAAC,CAAA;IACjE,MAAM,IAAI,GAAG,UAAU,CAAC,WAAW,CAAC,CAAA;IACpC,SAAS,CACP,eAAe,CACb,CAAC,IAAI,EAAE,EAAE,CACP,IAAI,EAAE,MAAM,EAAE,KAAK,IAAI;QACvB,CAAC,OAAO,WAAW,KAAK,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,EACvE,UAAU,CACX,CACF,CAAA;IACD,OAAO,OAAO,CAAA;AAChB,CAAC"}
1
+ {"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAWA,mFAAmF;AACnF,qEAAqE;AACrE,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AACnE,6EAA6E;AAC7E,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAA;AACpE,OAAO,EAAE,WAAW,EAAE,MAAM,wCAAwC,CAAA;AACpE;;;;;;GAMG;AACH,OAAO,EAAiB,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AACzF,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAClD,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAEvE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AACnD,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAEhF,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAE1D,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,CAAA;AAE5C,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,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,WAAW,CAAA;AACpB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU,CAAC,WAAsC;IAC/D,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,YAAY,CAAU,YAAY,CAAC,CAAA;IACjE,MAAM,IAAI,GAAG,UAAU,CAAC,WAAW,CAAC,CAAA;IACpC,SAAS,CACP,eAAe,CACb,CAAC,IAAI,EAAE,EAAE,CACP,IAAI,EAAE,MAAM,EAAE,KAAK,IAAI;QACvB,CAAC,OAAO,WAAW,KAAK,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,EACvE,UAAU,CACX,CACF,CAAA;IACD,OAAO,OAAO,CAAA;AAChB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nifrajs/web-solid",
3
- "version": "5.9.0",
3
+ "version": "5.11.0",
4
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",
@@ -132,16 +132,16 @@
132
132
  "dependencies": {
133
133
  "@babel/core": "^7.26.0",
134
134
  "@babel/preset-typescript": "^7.26.0",
135
- "@nifrajs/core": "5.9.0",
136
- "@nifrajs/web": "5.9.0",
135
+ "@nifrajs/core": "5.11.0",
136
+ "@nifrajs/web": "5.11.0",
137
137
  "babel-preset-solid": "^1.9.3",
138
138
  "solid-refresh": "^0.7.8"
139
139
  },
140
140
  "peerDependencies": {
141
141
  "@mdx-js/mdx": "^3",
142
142
  "solid-js": "^1.9.0",
143
- "@nifrajs/i18n": "^5.9.0",
144
- "@nifrajs/image": "^5.9.0"
143
+ "@nifrajs/i18n": "^5.11.0",
144
+ "@nifrajs/image": "^5.11.0"
145
145
  },
146
146
  "peerDependenciesMeta": {
147
147
  "@mdx-js/mdx": {
package/src/client.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { MountRouterOptions, RenderProps } from "@nifrajs/web"
2
+ import { setRouteSlotRuntime } from "@nifrajs/web/client"
2
3
  /**
3
4
  * @nifrajs/web-solid/client - Solid client runtime. `hydrate` hydrates a single SSR'd route;
4
5
  * `mountRouter` hydrates the initial route then drives navigation through a Solid signal - so a
@@ -36,6 +37,7 @@ export function hydrate(chain: readonly unknown[], props: RenderProps, container
36
37
  */
37
38
  export function mountRouter(options: MountRouterOptions): void {
38
39
  const { router, routes, searchSchemas, matchChains, container } = options
40
+ setRouteSlotRuntime({ router, routes, searchSchemas, matchChains })
39
41
  setMountedRouter(router) // expose it to createFetcher/useFetchers (same page, client-only)
40
42
  const el = container as Element
41
43
 
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 { RenderPropsContext, SearchContext } from "./router.ts"
3
+ import { RenderPropsContext, SearchContext } from "./router-context.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({})
@@ -29,7 +29,9 @@ export function compose(chain: readonly unknown[], props: RenderProps): () => JS
29
29
  get data() {
30
30
  return props.layoutData?.[i] ?? null
31
31
  },
32
- ...(props.boundaries !== undefined ? { boundaries: props.boundaries } : {}),
32
+ get boundaries() {
33
+ return props.boundaries
34
+ },
33
35
  children: child(),
34
36
  })
35
37
  }
package/src/image.ts CHANGED
@@ -6,7 +6,14 @@
6
6
  * reactively (`splitProps`/`mergeProps`). Resizing is delegated to the `loader`; nifra bundles no codec.
7
7
  * No JSX (`createComponent`/`Dynamic`), so the package builds with plain `tsc`.
8
8
  */
9
- import { type ImageLoader, type ImageProps, resolveImage, toHtmlAttrs } from "@nifrajs/image"
9
+ import {
10
+ type ImageInputProps,
11
+ type ImageLoader,
12
+ type ImageProps,
13
+ resolveImage,
14
+ type StaticImageProps,
15
+ toHtmlAttrs,
16
+ } from "@nifrajs/image"
10
17
  import { createComponent, type JSX, mergeProps, splitProps } from "solid-js"
11
18
  import { Dynamic } from "solid-js/web"
12
19
 
@@ -29,11 +36,14 @@ export interface ImageComponentProps
29
36
  readonly loader?: ImageLoader
30
37
  }
31
38
 
39
+ export type StaticImageComponentProps = StaticImageProps &
40
+ Omit<ImageComponentProps, "src" | "width" | "height">
41
+
32
42
  /**
33
- * Render a responsive, CLS-safe `<img>`. `width`+`height` are required (reserve layout space);
43
+ * Render a responsive, CLS-safe `<img>`. String URLs require dimensions; asset imports infer them;
34
44
  * `priority` marks the LCP image (`eager` + `fetchpriority="high"`). Extra DOM props pass through.
35
45
  */
36
- export function Image(props: ImageComponentProps): JSX.Element {
46
+ export function Image(props: ImageComponentProps | StaticImageComponentProps): JSX.Element {
37
47
  // Separate the nifra-only props (not valid DOM attributes) from the DOM passthrough (`rest`), keeping
38
48
  // both reactive. `rest` carries class/style/id/handlers; the computed attrs are layered over it.
39
49
  const [, rest] = splitProps(props, [
@@ -51,10 +61,7 @@ export function Image(props: ImageComponentProps): JSX.Element {
51
61
  const attrs = (): ReturnType<typeof toHtmlAttrs> => {
52
62
  // Build ImageProps via conditional spreads (exactOptionalPropertyTypes: an unset optional is
53
63
  // absent, not `undefined`). resolveImage validates width/height > 0 (CLS contract).
54
- const input: ImageProps = {
55
- src: props.src,
56
- width: props.width,
57
- height: props.height,
64
+ const common = {
58
65
  alt: props.alt,
59
66
  ...(props.sizes !== undefined ? { sizes: props.sizes } : {}),
60
67
  ...(props.widths !== undefined ? { widths: props.widths } : {}),
@@ -62,6 +69,20 @@ export function Image(props: ImageComponentProps): JSX.Element {
62
69
  ...(props.loading !== undefined ? { loading: props.loading } : {}),
63
70
  ...(props.priority !== undefined ? { priority: props.priority } : {}),
64
71
  }
72
+ const input: ImageInputProps =
73
+ typeof props.src === "string"
74
+ ? {
75
+ ...common,
76
+ src: props.src,
77
+ width: props.width ?? Number.NaN,
78
+ height: props.height ?? Number.NaN,
79
+ }
80
+ : {
81
+ ...common,
82
+ src: props.src,
83
+ ...(props.width !== undefined ? { width: props.width } : {}),
84
+ ...(props.height !== undefined ? { height: props.height } : {}),
85
+ }
65
86
  return toHtmlAttrs(resolveImage(input, props.loader))
66
87
  }
67
88
  return createComponent(Dynamic, mergeProps(rest, attrs, { component: "img" }))
@@ -23,7 +23,7 @@ import {
23
23
  } from "solid-js"
24
24
  import { Dynamic } from "solid-js/web"
25
25
  import { PageContext, type PageSlot } from "./page-visibility.ts"
26
- import { RenderPropsContext, SearchContext } from "./router.ts"
26
+ import { RenderPropsContext, SearchContext } from "./router-context.ts"
27
27
 
28
28
  /** What the view renders with: one per mount, on the server or in the browser. */
29
29
  export interface KeepAliveViewMount {
package/src/ppr.ts CHANGED
@@ -38,7 +38,7 @@ function openHost<Data>(hole: HoleDefinition<Data>, fill: () => unknown): JSX.El
38
38
  }
39
39
 
40
40
  /**
41
- * Experimental: one `dynamic` or `shared` boundary of a partially prerendered page.
41
+ * One `dynamic` or `shared` boundary of a partially prerendered page.
42
42
  *
43
43
  * In a shared shell the hole is an empty host Solid never fills: the PPR runtime puts the server's
44
44
  * HTML into it, and an interactive hole then hydrates as its own root from the frame's props. When the
package/src/query.ts CHANGED
@@ -1,4 +1,16 @@
1
- import type { QueryClient, QueryOptions, QueryState } from "@nifrajs/web"
1
+ import type {
2
+ DehydratedState,
3
+ InfiniteData,
4
+ InfiniteQueryOptions,
5
+ MutationCallbacks,
6
+ MutationContext,
7
+ MutationHandle,
8
+ MutationState,
9
+ QueryClient,
10
+ QueryOptions,
11
+ QueryState,
12
+ } from "@nifrajs/web"
13
+ import { createMutation as createMutationHandle } from "@nifrajs/web/client"
2
14
  import {
3
15
  createNoClientRefetch,
4
16
  fetchOnMount,
@@ -12,15 +24,46 @@ import {
12
24
  * exposes `invalidateQueries` for after a mutation. Imports only `solid-js` + `@nifrajs/web` (never
13
25
  * `solid-js/web`), so route components use it on the server *and* client. No JSX.
14
26
  *
15
- * The query client is created lazily **client-side only** (`typeof window`) - on the server the
16
- * accessor returns the idle/pending state (queries are client-first; loaders are the SSR data source).
27
+ * Without a provider, the query client is created lazily **client-side only** (`typeof window`) - on
28
+ * the server the accessor returns idle state and route loaders remain the data source. A
29
+ * `QueryClientProvider` can supply a request-scoped hydrated client.
17
30
  * The `key` is captured once (Solid setup runs once); a reactive key is a documented future enhancement.
18
31
  */
19
- import { type Accessor, createSignal, onCleanup, onMount } from "solid-js"
32
+ import {
33
+ type Accessor,
34
+ createComponent,
35
+ createContext,
36
+ createSignal,
37
+ type JSX,
38
+ onCleanup,
39
+ onMount,
40
+ useContext,
41
+ } from "solid-js"
42
+
43
+ const QueryClientContext = createContext<QueryClient>()
44
+
45
+ function queryClientForCurrentTree(): QueryClient | undefined {
46
+ return useContext(QueryClientContext) ?? getQueryClientSingleton()
47
+ }
48
+
49
+ /** Provide a request-scoped client and optionally seed it before descendants run. */
50
+ export function QueryClientProvider(props: {
51
+ readonly client: QueryClient
52
+ readonly state?: DehydratedState
53
+ readonly children?: JSX.Element
54
+ }): JSX.Element {
55
+ if (props.state !== undefined) props.client.hydrate(props.state)
56
+ return createComponent(QueryClientContext.Provider, {
57
+ value: props.client,
58
+ get children() {
59
+ return props.children
60
+ },
61
+ })
62
+ }
20
63
 
21
64
  /** Access the query client to imperatively `invalidateQueries(keyOrPrefix)` (e.g. after a mutation). */
22
65
  export function useQueryClient(): Pick<QueryClient, "invalidateQueries"> {
23
- return getQueryClientSingleton() ?? { invalidateQueries: noop }
66
+ return queryClientForCurrentTree() ?? { invalidateQueries: noop }
24
67
  }
25
68
 
26
69
  /** A query's reactive state accessor plus `refetch`. */
@@ -42,7 +85,7 @@ export function createQuery<T>(
42
85
  fn: () => Promise<T>,
43
86
  options?: QueryOptions,
44
87
  ): CreateQueryResult<T> {
45
- const handle = getQueryClientSingleton()?.query<T>(key, fn, options)
88
+ const handle = queryClientForCurrentTree()?.query<T>(key, fn, options)
46
89
  const [state, setState] = createSignal<QueryState<T>>(
47
90
  handle ? handle.snapshot() : IDLE_QUERY_STATE,
48
91
  )
@@ -56,3 +99,99 @@ export function createQuery<T>(
56
99
  refetch: createNoClientRefetch<T>("[nifra/web-solid]", "createQuery.refetch"),
57
100
  }
58
101
  }
102
+
103
+ /** A mutation result expressed as Solid accessors and imperative controls. */
104
+ export interface CreateMutationResult<TData, TVariables> {
105
+ readonly state: Accessor<MutationState<TData, TVariables>>
106
+ readonly isIdle: Accessor<boolean>
107
+ readonly isPending: Accessor<boolean>
108
+ readonly isError: Accessor<boolean>
109
+ readonly isSuccess: Accessor<boolean>
110
+ readonly mutate: (variables: TVariables) => void
111
+ readonly mutateAsync: (variables: TVariables) => Promise<TData>
112
+ readonly reset: () => void
113
+ readonly cancel: () => void
114
+ }
115
+
116
+ /** Bind a mutation to a Solid owner. Identity changes discard late results and callbacks. */
117
+ export function createMutation<TData, TVariables = void>(
118
+ fn: (variables: TVariables, context: MutationContext) => Promise<TData>,
119
+ callbacks: MutationCallbacks<TData, TVariables> = {},
120
+ ): CreateMutationResult<TData, TVariables> {
121
+ const handle: MutationHandle<TData, TVariables> = createMutationHandle(fn, callbacks)
122
+ const [state, setState] = createSignal(handle.snapshot())
123
+ onCleanup(handle.subscribe(() => setState(() => handle.snapshot())))
124
+ return {
125
+ state,
126
+ isIdle: () => state().status === "idle",
127
+ isPending: () => state().status === "pending",
128
+ isError: () => state().status === "error",
129
+ isSuccess: () => state().status === "success",
130
+ mutate: (variables) => {
131
+ handle.mutate(variables).catch(noop)
132
+ },
133
+ mutateAsync: handle.mutate,
134
+ reset: handle.reset,
135
+ cancel: handle.cancel,
136
+ }
137
+ }
138
+
139
+ /** Options for a Solid infinite query. */
140
+ export interface CreateInfiniteQueryOptions<T, P> extends InfiniteQueryOptions<T, P> {
141
+ readonly enabled?: boolean
142
+ }
143
+
144
+ /** Infinite-query state and page controls expressed as Solid accessors. */
145
+ export interface CreateInfiniteQueryResult<T, P> {
146
+ readonly state: Accessor<QueryState<InfiniteData<T, P>>>
147
+ readonly isPending: Accessor<boolean>
148
+ readonly isError: Accessor<boolean>
149
+ readonly isSuccess: Accessor<boolean>
150
+ readonly hasNextPage: Accessor<boolean>
151
+ readonly hasPreviousPage: Accessor<boolean>
152
+ readonly fetchNextPage: () => Promise<InfiniteData<T, P>>
153
+ readonly fetchPreviousPage: () => Promise<InfiniteData<T, P>>
154
+ readonly refetch: () => Promise<InfiniteData<T, P>>
155
+ }
156
+
157
+ /** Bind a paged query to a Solid owner and fetch its first page on mount. */
158
+ export function createInfiniteQuery<T, P>(
159
+ key: unknown,
160
+ fn: (pageParam: P) => Promise<T>,
161
+ options: CreateInfiniteQueryOptions<T, P>,
162
+ ): CreateInfiniteQueryResult<T, P> {
163
+ const client = queryClientForCurrentTree()
164
+ const enabled = options.enabled !== false
165
+ const handle = client?.infiniteQuery<T, P>(key, fn, options)
166
+ const [state, setState] = createSignal<QueryState<InfiniteData<T, P>>>(
167
+ handle
168
+ ? handle.snapshot()
169
+ : // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: An idle query has no generic page data; the provider supplies the typed state before use.
170
+ (IDLE_QUERY_STATE as QueryState<InfiniteData<T, P>>),
171
+ )
172
+ if (handle) {
173
+ onCleanup(handle.subscribe(() => setState(() => handle.snapshot())))
174
+ onMount(fetchOnMount(enabled ? handle : undefined))
175
+ }
176
+ const noClientAction = createNoClientRefetch<InfiniteData<T, P>>(
177
+ "[nifra/web-solid]",
178
+ "infinite query action",
179
+ )
180
+ return {
181
+ state,
182
+ isPending: () => state().status === "pending",
183
+ isError: () => state().status === "error",
184
+ isSuccess: () => state().status === "success",
185
+ hasNextPage: () => {
186
+ state()
187
+ return handle?.hasNextPage() ?? false
188
+ },
189
+ hasPreviousPage: () => {
190
+ state()
191
+ return handle?.hasPreviousPage() ?? false
192
+ },
193
+ fetchNextPage: handle?.fetchNextPage ?? noClientAction,
194
+ fetchPreviousPage: handle?.fetchPreviousPage ?? noClientAction,
195
+ refetch: handle?.refetch ?? noClientAction,
196
+ }
197
+ }
@@ -0,0 +1,137 @@
1
+ import type { RenderProps, RouteSlotName, RouteSlotTarget } from "@nifrajs/web"
2
+ import {
3
+ getRouteSlotStore,
4
+ type LoadedRouteSlot,
5
+ type RouteSlotSnapshot,
6
+ type RouteSlotStatus,
7
+ rawHref,
8
+ routeSlotSyncKey,
9
+ routeSlotTargetFromPath,
10
+ setRouteSlot,
11
+ stripRouteSlots,
12
+ syncRouteSlotStore,
13
+ } from "@nifrajs/web/client"
14
+ import { navigateNow } from "@nifrajs/web/internal/navigate-runtime"
15
+ import {
16
+ createComponent,
17
+ createEffect,
18
+ createMemo,
19
+ createSignal,
20
+ type JSX,
21
+ onCleanup,
22
+ untrack,
23
+ useContext,
24
+ } from "solid-js"
25
+ import { Dynamic } from "solid-js/web"
26
+ import { compose } from "./compose.ts"
27
+ import { RenderPropsContext } from "./router-context.ts"
28
+
29
+ export interface RouteSlotController {
30
+ readonly target: () => string | undefined
31
+ readonly status: () => RouteSlotStatus
32
+ readonly content: () => LoadedRouteSlot | undefined
33
+ readonly error: () => string | undefined
34
+ open(target: RouteSlotTarget): void
35
+ close(): void
36
+ }
37
+
38
+ export interface RouteSlotHooks {
39
+ createEffect(effect: () => void): void
40
+ createMemo<T>(read: () => T): () => T
41
+ createSignal<T>(initial: T): readonly [() => T, (next: T | ((previous: T) => T)) => void]
42
+ onCleanup(cleanup: () => void): void
43
+ }
44
+
45
+ export interface RouteSlotProps {
46
+ readonly name: RouteSlotName
47
+ readonly fallback?: JSX.Element
48
+ readonly error?: JSX.Element | ((code: string) => JSX.Element)
49
+ }
50
+
51
+ /** Build the hook with its small reactive surface; the test seam avoids global module mocks. */
52
+ export function createRouteSlotBinding(
53
+ readProps: () => RenderProps | undefined,
54
+ navigate: (href: ReturnType<typeof rawHref>) => void,
55
+ hooks: RouteSlotHooks,
56
+ ): (name: RouteSlotName) => RouteSlotController {
57
+ return function useRouteSlot(name: RouteSlotName): RouteSlotController {
58
+ const props = readProps()
59
+ const path = (): string => props?.path ?? "/"
60
+ const pending = (): boolean => props?.pending === true
61
+ const parsed = hooks.createMemo(() => routeSlotTargetFromPath(path(), name))
62
+ const basePath = hooks.createMemo(() => stripRouteSlots(path()))
63
+ const store = hooks.createMemo(() => getRouteSlotStore(basePath(), name))
64
+ const [snapshot, setSnapshot] = hooks.createSignal<RouteSlotSnapshot>(
65
+ store().getServerSnapshot(),
66
+ )
67
+ const requestKey = hooks.createMemo(() =>
68
+ routeSlotSyncKey(basePath(), name, parsed().target, parsed().error, pending()),
69
+ )
70
+
71
+ hooks.createEffect(() => {
72
+ const currentStore = store()
73
+ setSnapshot(currentStore.getSnapshot())
74
+ const unsubscribe = currentStore.subscribe(() => setSnapshot(currentStore.getSnapshot()))
75
+ hooks.onCleanup(unsubscribe)
76
+ })
77
+
78
+ hooks.createEffect(() => {
79
+ const current = parsed()
80
+ syncRouteSlotStore(store(), basePath(), name, current.target, current.error, pending())
81
+ })
82
+
83
+ const current = hooks.createMemo(() => {
84
+ const value = snapshot()
85
+ return value.key === requestKey() ? value : undefined
86
+ })
87
+
88
+ return {
89
+ target: () => parsed().target,
90
+ status: () =>
91
+ current()?.status ??
92
+ (parsed().error !== undefined
93
+ ? "error"
94
+ : pending() || parsed().target === undefined
95
+ ? "idle"
96
+ : "loading"),
97
+ content: () => current()?.content,
98
+ error: () => current()?.error ?? parsed().error,
99
+ open: (target) => navigate(rawHref(setRouteSlot(path(), name, String(target)))),
100
+ close: () => navigate(rawHref(setRouteSlot(path(), name, null))),
101
+ }
102
+ }
103
+ }
104
+
105
+ /** Read or navigate one route slot without changing any sibling slot's URL state or component tree. */
106
+ export const useRouteSlot = createRouteSlotBinding(
107
+ () => useContext(RenderPropsContext),
108
+ navigateNow,
109
+ { createEffect, createMemo, createSignal, onCleanup },
110
+ )
111
+
112
+ /** Render the registered route's complete layout and page chain in this independently navigable region. */
113
+
114
+ export function RouteSlot(props: RouteSlotProps): JSX.Element {
115
+ const slot = useRouteSlot(props.name)
116
+ const view = createMemo(() => {
117
+ const content = slot.content()
118
+ if (content !== undefined) return untrack(() => compose(content.chain, content)())
119
+ if (slot.status() === "error") {
120
+ const code = slot.error() ?? "unavailable"
121
+ const error = props.error
122
+ if (typeof error === "function") return untrack(() => error(code))
123
+ if (error !== undefined) return error
124
+ return untrack(() =>
125
+ createComponent(Dynamic, {
126
+ component: "div",
127
+ role: "alert",
128
+ "data-route-slot-error": code,
129
+ children: "This panel is unavailable.",
130
+ }),
131
+ )
132
+ }
133
+ return slot.target() !== undefined ? (props.fallback ?? null) : null
134
+ })
135
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion lint/plugin/noChainedTypeAssertions: Solid inserts an accessor reactively as its child, while its JSX.Element type excludes functions.
136
+ return view as unknown as JSX.Element
137
+ }
@@ -0,0 +1,20 @@
1
+ import type { RenderProps } from "@nifrajs/web"
2
+ import { type Accessor, type Context, createContext } from "solid-js"
3
+
4
+ /** Shared provider identities across the Bun server and Vite's SSR module graph. */
5
+ const SEARCH_CONTEXT_SLOT = Symbol.for("nifra.web-solid.search-context")
6
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: The namespaced Symbol.for key shares one matching Solid Context across Bun and Vite's duplicate module graphs in this SSR process.
7
+ const searchContextSlot = globalThis as {
8
+ [SEARCH_CONTEXT_SLOT]?: Context<Accessor<Record<string, unknown>> | undefined>
9
+ }
10
+ export const SearchContext =
11
+ searchContextSlot[SEARCH_CONTEXT_SLOT] ?? createContext<Accessor<Record<string, unknown>>>()
12
+ searchContextSlot[SEARCH_CONTEXT_SLOT] = SearchContext
13
+
14
+ /** Props for the composed route chain; a global singleton for the same reason as SearchContext. */
15
+ const PROPS_CONTEXT_SLOT = Symbol.for("nifra.web-solid.props-context")
16
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: The namespaced Symbol.for key shares one matching Solid Context across Bun and Vite's duplicate module graphs in this SSR process.
17
+ const propsContextSlot = globalThis as { [PROPS_CONTEXT_SLOT]?: Context<RenderProps | undefined> }
18
+ export const RenderPropsContext =
19
+ propsContextSlot[PROPS_CONTEXT_SLOT] ?? createContext<RenderProps>()
20
+ propsContextSlot[PROPS_CONTEXT_SLOT] = RenderPropsContext