@nifrajs/web-solid 3.5.0 → 4.0.2
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/dist/client.d.ts.map +1 -1
- package/dist/client.js +9 -35
- package/dist/client.js.map +1 -1
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +12 -4
- package/dist/compose.js.map +1 -1
- package/dist/i18n.d.ts +18 -6
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +28 -5
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +6 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -91
- package/dist/index.js.map +1 -1
- package/dist/mdx-runtime.d.ts +8 -1
- package/dist/mdx-runtime.d.ts.map +1 -1
- package/dist/mdx-runtime.js +4 -2
- package/dist/mdx-runtime.js.map +1 -1
- package/dist/plugin.d.ts +12 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +87 -0
- package/dist/plugin.js.map +1 -0
- package/dist/route-props.d.ts +7 -0
- package/dist/route-props.d.ts.map +1 -0
- package/dist/route-props.js +50 -0
- package/dist/route-props.js.map +1 -0
- package/dist/router.d.ts +16 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +28 -1
- package/dist/router.js.map +1 -1
- package/package.json +14 -6
- package/src/client.ts +11 -37
- package/src/compose.ts +12 -4
- package/src/i18n.ts +51 -7
- package/src/index.ts +12 -96
- package/src/mdx-runtime.ts +9 -4
- package/src/plugin.ts +93 -0
- package/src/route-props.ts +55 -0
- package/src/router.ts +34 -1
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nifrajs/web-solid/plugin` - the Solid Babel Bun plugin, in its OWN module so the adapter root (which
|
|
3
|
+
* every Solid SSR server and edge worker bundles) never links build-time code. Mirrors
|
|
4
|
+
* `@nifrajs/web-vue/plugin`: pass `"dom"` for the client bundle and `"ssr"` for the server.
|
|
5
|
+
*/
|
|
6
|
+
import { transformAsync } from "@babel/core"
|
|
7
|
+
// @ts-expect-error - no type declarations published
|
|
8
|
+
import presetTypeScript from "@babel/preset-typescript"
|
|
9
|
+
import { devServerCompile, normalizeFilePath, rewriteSsrImports } from "@nifrajs/web/plugins/kit"
|
|
10
|
+
// @ts-expect-error - no type declarations published
|
|
11
|
+
import presetSolid from "babel-preset-solid"
|
|
12
|
+
import type { BunPlugin } from "bun"
|
|
13
|
+
import solidRefreshBunHot, { SOLID_HOT_MODULE } from "./refresh-babel.ts"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Bun build/runtime plugin that compiles Solid components with Babel - `generate: "ssr"`
|
|
17
|
+
* for the server, `"dom"` for the client, `hydratable` so SSR and hydrate align. Solid's
|
|
18
|
+
* reactive-JSX compiler ships only as a Babel plugin (no swc/native port); this runs at
|
|
19
|
+
* build time, on `.tsx` files only.
|
|
20
|
+
*
|
|
21
|
+
* Under a dev server the client half additionally runs `solid-refresh`, which is what makes an edit
|
|
22
|
+
* patch the running component tree instead of reloading the page - see {@link setupSolidRefresh}.
|
|
23
|
+
*/
|
|
24
|
+
export function solidBunPlugin(generate: "dom" | "ssr"): BunPlugin {
|
|
25
|
+
return {
|
|
26
|
+
name: `nifra-solid-${generate}`,
|
|
27
|
+
setup(build) {
|
|
28
|
+
const refresh =
|
|
29
|
+
generate === "dom" && devServerCompile() ? setupSolidRefresh(build) : undefined
|
|
30
|
+
// Match `.tsx`, tolerating a `?query` suffix (dev servers append one to bust Bun's import
|
|
31
|
+
// cache); strip it before reading the file off disk.
|
|
32
|
+
build.onLoad({ filter: /\.tsx(\?|$)/ }, async (args) => {
|
|
33
|
+
const path = normalizeFilePath(args.path)
|
|
34
|
+
const source = await Bun.file(path).text()
|
|
35
|
+
const result = await transformAsync(source, {
|
|
36
|
+
filename: path,
|
|
37
|
+
// babel applies presets last→first: strip TS first, then Solid transforms JSX.
|
|
38
|
+
presets: [
|
|
39
|
+
[presetSolid, { generate, hydratable: true }],
|
|
40
|
+
[presetTypeScript, { onlyRemoveTypeImports: true }],
|
|
41
|
+
],
|
|
42
|
+
...(refresh !== undefined
|
|
43
|
+
? { plugins: [[await refresh.babel(), REFRESH_OPTIONS], solidRefreshBunHot] }
|
|
44
|
+
: {}),
|
|
45
|
+
})
|
|
46
|
+
return { contents: rewriteSsrImports(result?.code ?? "", path, generate), loader: "js" }
|
|
47
|
+
})
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* `"esm"` is the closest of `solid-refresh`'s bundler modes to what Bun implements: one
|
|
54
|
+
* `accept(mod => …)` per module, falling back to `invalidate()` when a component's signature changed too
|
|
55
|
+
* much to patch in place. `"vite"` differs only by also calling a bare `accept()` first, which would make
|
|
56
|
+
* every module self-accepting twice over. The call it emits is still Vite-shaped, and
|
|
57
|
+
* `solidRefreshBunHot` translates it.
|
|
58
|
+
*
|
|
59
|
+
* `jsx: false` turns OFF the pass that lifts a component's returned JSX into a second, nested component.
|
|
60
|
+
* That extra component is an extra hydration level on the client and none on the server, so with it on,
|
|
61
|
+
* every hydratable page dies on `Unable to find DOM nodes for hydration key` before a single edit. It buys
|
|
62
|
+
* finer-grained patching of markup-only edits; correct hydration is worth more.
|
|
63
|
+
*/
|
|
64
|
+
const REFRESH_OPTIONS = { bundler: "esm", jsx: false } as const
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Wire `solid-refresh` into a dev-server client compile.
|
|
68
|
+
*
|
|
69
|
+
* Solid components are compiled reactive closures, not functions re-run on render, so a new module
|
|
70
|
+
* version cannot simply replace the old one - `solid-refresh` wraps every component in a registry and, on
|
|
71
|
+
* an update, patches the live instances, keeping signal state. Without it the update finds no accepting
|
|
72
|
+
* module, bubbles up to the generated entry, and Bun reloads the whole page.
|
|
73
|
+
*
|
|
74
|
+
* The Babel half loads lazily: it is dev-only, and every production build loads this plugin too.
|
|
75
|
+
*
|
|
76
|
+
* Both runtime specifiers need pinning, because the transform emits them INTO the app's own files, where
|
|
77
|
+
* a bare specifier resolves against the app. `solid-refresh` is only a transitive dependency there and
|
|
78
|
+
* need not be hoisted; `nifra:solid-hot` is this package's own bridge module and has no app-side name at
|
|
79
|
+
* all. Resolving both here is what keeps the emitted imports working without an app depending on either.
|
|
80
|
+
*/
|
|
81
|
+
function setupSolidRefresh(build: Parameters<BunPlugin["setup"]>[0]): {
|
|
82
|
+
babel: () => Promise<unknown>
|
|
83
|
+
} {
|
|
84
|
+
const runtime = Bun.resolveSync("solid-refresh", import.meta.dir)
|
|
85
|
+
build.onResolve({ filter: /^solid-refresh$/ }, () => ({ path: runtime }))
|
|
86
|
+
// `.ts` from source (Bun takes the package's `bun` export condition), `.js` from the published build.
|
|
87
|
+
const bridge = `${import.meta.dir}/refresh-hot${import.meta.path.endsWith(".ts") ? ".ts" : ".js"}`
|
|
88
|
+
build.onResolve({ filter: new RegExp(`^${SOLID_HOT_MODULE}$`) }, () => ({ path: bridge }))
|
|
89
|
+
let babel: Promise<unknown> | undefined
|
|
90
|
+
return {
|
|
91
|
+
babel: () => (babel ??= import("solid-refresh/babel").then((m) => m.default)),
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { MountRouterOptions, RenderProps, RouterState } from "@nifrajs/web"
|
|
2
|
+
// `/client`, not the root: the root's graph carries the server, and Vite's dev server evaluates it
|
|
3
|
+
// instead of tree-shaking it - which broke hydration before the browser ran a line of app code.
|
|
4
|
+
import { searchOfChain } from "@nifrajs/web/client"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The client half of the server's `RenderProps`, as getters so a settle re-renders only what reads the
|
|
8
|
+
* changed field. Cast: a getter always exists where `exactOptionalPropertyTypes` expects absence.
|
|
9
|
+
*/
|
|
10
|
+
export function routeProps(
|
|
11
|
+
snapshot: () => RouterState,
|
|
12
|
+
searchSchemas: MountRouterOptions["searchSchemas"],
|
|
13
|
+
matchChains?: MountRouterOptions["matchChains"],
|
|
14
|
+
): RenderProps {
|
|
15
|
+
return {
|
|
16
|
+
get data() {
|
|
17
|
+
return snapshot().data
|
|
18
|
+
},
|
|
19
|
+
// Each layout's own loader data. Without it a layout renders `data: null` on the client while the
|
|
20
|
+
// server rendered it with its data.
|
|
21
|
+
get layoutData() {
|
|
22
|
+
return snapshot().layoutData
|
|
23
|
+
},
|
|
24
|
+
get actionData() {
|
|
25
|
+
return snapshot().actionData
|
|
26
|
+
},
|
|
27
|
+
get pending() {
|
|
28
|
+
return snapshot().pending
|
|
29
|
+
},
|
|
30
|
+
// The same `params`/`path` the server render received, so `useMatches` reports the same chain.
|
|
31
|
+
get params() {
|
|
32
|
+
return snapshot().params
|
|
33
|
+
},
|
|
34
|
+
get path() {
|
|
35
|
+
return snapshot().path
|
|
36
|
+
},
|
|
37
|
+
get search() {
|
|
38
|
+
// The SAME searchOfChain the server ran, over this snapshot's URL - a same-route search change
|
|
39
|
+
// updates in place (fine-grained), matching the SSR value on hydration.
|
|
40
|
+
const s = snapshot()
|
|
41
|
+
const idx = s.path.indexOf("?")
|
|
42
|
+
return searchOfChain(searchSchemas?.[s.routeId] ?? [], idx === -1 ? "" : s.path.slice(idx))
|
|
43
|
+
},
|
|
44
|
+
get submission() {
|
|
45
|
+
return snapshot().submission
|
|
46
|
+
},
|
|
47
|
+
get boundaries() {
|
|
48
|
+
return snapshot().boundaries
|
|
49
|
+
},
|
|
50
|
+
// The chain `useMatches` reports - the server passed the same one to the SSR render.
|
|
51
|
+
get matchChain() {
|
|
52
|
+
return matchChains?.[snapshot().routeId]
|
|
53
|
+
},
|
|
54
|
+
} as RenderProps
|
|
55
|
+
}
|
package/src/router.ts
CHANGED
|
@@ -9,6 +9,8 @@ import type {
|
|
|
9
9
|
NavigateFunction,
|
|
10
10
|
NavigateOptions,
|
|
11
11
|
NavigateTargetInput,
|
|
12
|
+
RenderProps,
|
|
13
|
+
UIMatch,
|
|
12
14
|
} from "@nifrajs/web"
|
|
13
15
|
// `/client`, not the root - these are DOM values, and the root's graph carries the
|
|
14
16
|
// server, which Vite's dev server evaluates rather than tree-shakes.
|
|
@@ -18,6 +20,8 @@ import {
|
|
|
18
20
|
registerBlocker,
|
|
19
21
|
resolveNavigate,
|
|
20
22
|
} from "@nifrajs/web/client"
|
|
23
|
+
// Its own subpath, so an app that never calls `useMatches` never bundles it.
|
|
24
|
+
import { chainMatches } from "@nifrajs/web/internal/matches-runtime"
|
|
21
25
|
/**
|
|
22
26
|
* `@nifrajs/web-solid/router` - Solid routing bindings over the agnostic `@nifrajs/web` history layer:
|
|
23
27
|
* `useNavigate` (programmatic navigation), `useBlocker` (the unsaved-changes guard), and `useSearch`
|
|
@@ -29,12 +33,13 @@ import {
|
|
|
29
33
|
type Accessor,
|
|
30
34
|
type Context,
|
|
31
35
|
createContext,
|
|
36
|
+
createMemo,
|
|
32
37
|
createSignal,
|
|
33
38
|
onCleanup,
|
|
34
39
|
useContext,
|
|
35
40
|
} from "solid-js"
|
|
36
41
|
|
|
37
|
-
export type { Blocker, BlockerFunction, BlockerState, NavigateFunction }
|
|
42
|
+
export type { Blocker, BlockerFunction, BlockerState, NavigateFunction, UIMatch }
|
|
38
43
|
|
|
39
44
|
// Frozen empty search + a stable accessor for a `useSearch` used outside a nifra route tree.
|
|
40
45
|
const EMPTY_SEARCH: Readonly<Record<string, unknown>> = Object.freeze({})
|
|
@@ -55,6 +60,34 @@ export const SearchContext =
|
|
|
55
60
|
searchContextSlot[SEARCH_CONTEXT_SLOT] ?? createContext<Accessor<Record<string, unknown>>>()
|
|
56
61
|
searchContextSlot[SEARCH_CONTEXT_SLOT] = SearchContext
|
|
57
62
|
|
|
63
|
+
/** The props `compose` rendered the chain with - getters over the router snapshot on the client - which
|
|
64
|
+
* {@link useMatches} reads. A `globalThis` singleton for the same reason as {@link SearchContext}. */
|
|
65
|
+
const PROPS_CONTEXT_SLOT = Symbol.for("nifra.web-solid.props-context")
|
|
66
|
+
const propsContextSlot = globalThis as { [PROPS_CONTEXT_SLOT]?: Context<RenderProps | undefined> }
|
|
67
|
+
export const RenderPropsContext =
|
|
68
|
+
propsContextSlot[PROPS_CONTEXT_SLOT] ?? createContext<RenderProps>()
|
|
69
|
+
propsContextSlot[PROPS_CONTEXT_SLOT] = RenderPropsContext
|
|
70
|
+
|
|
71
|
+
const NO_MATCHES: readonly UIMatch[] = Object.freeze([])
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The rendered chain - each layout, then the page - with the URL prefix, params and loader data each
|
|
75
|
+
* one owns, plus its `handle` export, as a reactive accessor. The same list on the server render and
|
|
76
|
+
* the client mount, so a layout can render breadcrumbs from its children's `handle`s without a
|
|
77
|
+
* hydration mismatch; it updates in place when a same-route navigation settles.
|
|
78
|
+
*
|
|
79
|
+
* ```tsx
|
|
80
|
+
* export const handle = { crumb: "Settings" }
|
|
81
|
+
* const matches = useMatches()
|
|
82
|
+
* const crumbs = () => matches().flatMap((m) => (m.handle as { crumb?: string } | undefined)?.crumb ?? [])
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export function useMatches(): Accessor<readonly UIMatch[]> {
|
|
86
|
+
const props = useContext(RenderPropsContext)
|
|
87
|
+
if (props === undefined) return () => NO_MATCHES
|
|
88
|
+
return createMemo(() => chainMatches(props))
|
|
89
|
+
}
|
|
90
|
+
|
|
58
91
|
/**
|
|
59
92
|
* The route's typed, validated search params as a reactive accessor - the SAME value the loader received
|
|
60
93
|
* as `ctx.search`. SSR-correct: `compose` provides it from the URL server-side and from the identical
|