@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.
- package/CHANGELOG.md +58 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -0
- package/dist/client.js.map +1 -1
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +4 -2
- package/dist/compose.js.map +1 -1
- package/dist/image.d.ts +4 -3
- package/dist/image.d.ts.map +1 -1
- package/dist/image.js +16 -6
- package/dist/image.js.map +1 -1
- package/dist/keep-alive-view.js +1 -1
- package/dist/keep-alive-view.js.map +1 -1
- package/dist/ppr.d.ts +1 -1
- package/dist/ppr.js +1 -1
- package/dist/query.d.ts +43 -4
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +73 -5
- package/dist/query.js.map +1 -1
- package/dist/route-slot.d.ts +29 -0
- package/dist/route-slot.d.ts.map +1 -0
- package/dist/route-slot.jsx +75 -0
- package/dist/route-slot.jsx.map +1 -0
- package/dist/router-context.d.ts +5 -0
- package/dist/router-context.d.ts.map +1 -0
- package/dist/router-context.js +14 -0
- package/dist/router-context.js.map +1 -0
- package/dist/router.d.ts +8 -5
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +6 -12
- package/dist/router.js.map +1 -1
- package/package.json +5 -5
- package/src/client.ts +2 -0
- package/src/compose.ts +4 -2
- package/src/image.ts +28 -7
- package/src/keep-alive-view.ts +1 -1
- package/src/ppr.ts +1 -1
- package/src/query.ts +145 -6
- package/src/route-slot.tsx +137 -0
- package/src/router-context.ts +20 -0
- package/src/router.ts +8 -28
|
@@ -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,
|
|
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
|
|
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
|
|
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} (
|
|
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;
|
package/dist/router.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAIzE,OAAO,KAAK,EACV,OAAO,EACP,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,
|
|
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 {
|
|
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} (
|
|
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() {
|
package/dist/router.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"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.
|
|
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.
|
|
136
|
-
"@nifrajs/web": "5.
|
|
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.
|
|
144
|
-
"@nifrajs/image": "^5.
|
|
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
|
-
|
|
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 {
|
|
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>`.
|
|
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
|
|
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" }))
|
package/src/keep-alive-view.ts
CHANGED
|
@@ -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
|
-
*
|
|
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 {
|
|
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
|
-
*
|
|
16
|
-
* accessor returns
|
|
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 {
|
|
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
|
|
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 =
|
|
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
|