@nifrajs/web-solid 2.2.0 → 2.4.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/README.md +3 -3
- package/dist/await.d.ts +2 -2
- package/dist/await.js +3 -3
- package/dist/client.d.ts +4 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +19 -11
- package/dist/client.js.map +1 -1
- package/dist/compose.d.ts +4 -1
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +16 -2
- package/dist/compose.js.map +1 -1
- package/dist/content.d.ts +2 -2
- package/dist/content.js +2 -2
- package/dist/error.d.ts +1 -1
- package/dist/error.js +2 -2
- package/dist/fetcher.d.ts +4 -4
- package/dist/fetcher.js +4 -4
- package/dist/fn.d.ts +29 -0
- package/dist/fn.d.ts.map +1 -0
- package/dist/fn.js +28 -0
- package/dist/fn.js.map +1 -0
- package/dist/i18n.d.ts +1 -1
- package/dist/i18n.js +1 -1
- package/dist/image.d.ts +3 -3
- package/dist/image.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +7 -7
- package/dist/mdx-runtime.js +2 -2
- package/dist/mdx.js +5 -5
- package/dist/query.d.ts +2 -2
- package/dist/query.js +2 -2
- package/dist/router.d.ts +45 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +60 -0
- package/dist/router.js.map +1 -0
- package/package.json +16 -5
- package/src/await.ts +3 -3
- package/src/client.ts +22 -12
- package/src/compose.ts +18 -2
- package/src/content.ts +2 -2
- package/src/error.ts +2 -2
- package/src/fetcher.ts +4 -4
- package/src/fn.ts +42 -0
- package/src/i18n.ts +1 -1
- package/src/image.ts +3 -3
- package/src/index.ts +7 -7
- package/src/mdx-runtime.ts +2 -2
- package/src/mdx.ts +5 -5
- package/src/query.ts +2 -2
- package/src/router.ts +85 -0
package/dist/router.d.ts
ADDED
|
@@ -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.
|
|
4
|
-
"description": "Solid render adapter for @nifrajs/web
|
|
3
|
+
"version": "2.4.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/
|
|
91
|
-
"@nifrajs/
|
|
92
|
-
"@nifrajs/
|
|
100
|
+
"@nifrajs/core": "2.4.0",
|
|
101
|
+
"@nifrajs/i18n": "2.4.0",
|
|
102
|
+
"@nifrajs/image": "2.4.0",
|
|
103
|
+
"@nifrajs/web": "2.4.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`
|
|
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
|
|
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
|
|
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
|
|
1
|
+
import { type MountRouterOptions, type RenderProps, searchOfChain } from "@nifrajs/web"
|
|
2
2
|
/**
|
|
3
|
-
* @nifrajs/web-solid/client
|
|
4
|
-
* `mountRouter` hydrates the initial route then drives navigation through a Solid signal
|
|
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
|
|
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
|
|
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)
|
|
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**
|
|
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
|
|
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
|
|
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
|
|
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`.
|
|
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
|
|
@@ -18,5 +25,14 @@ export function compose(chain: readonly unknown[], props: RenderProps): () => JS
|
|
|
18
25
|
const layoutData = props.layoutData?.[i] ?? null
|
|
19
26
|
node = () => createComponent(Layout, { data: layoutData, children: child() })
|
|
20
27
|
}
|
|
21
|
-
|
|
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
|
+
})
|
|
22
38
|
}
|
package/src/content.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@nifrajs/web-solid/content`
|
|
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)
|
|
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`
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
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`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
package/src/mdx-runtime.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Solid MDX runtime
|
|
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")`
|
|
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`
|
|
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`
|
|
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
|
|
10
|
+
// @ts-expect-error - no type declarations published
|
|
11
11
|
import presetTypeScript from "@babel/preset-typescript"
|
|
12
|
-
// @ts-expect-error
|
|
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
|
|
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`
|
|
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`)
|
|
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
|
*/
|