@nifrajs/web-solid 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +18 -0
- package/dist/await.d.ts +29 -0
- package/dist/await.d.ts.map +1 -0
- package/dist/await.js +46 -0
- package/dist/await.js.map +1 -0
- package/dist/client.d.ts +18 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +85 -0
- package/dist/client.js.map +1 -0
- package/dist/compose.d.ts +9 -0
- package/dist/compose.d.ts.map +1 -0
- package/dist/compose.js +17 -0
- package/dist/compose.js.map +1 -0
- package/dist/content.d.ts +21 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +18 -0
- package/dist/content.js.map +1 -0
- package/dist/error.d.ts +8 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +25 -0
- package/dist/error.js.map +1 -0
- package/dist/fetcher.d.ts +37 -0
- package/dist/fetcher.d.ts.map +1 -0
- package/dist/fetcher.js +48 -0
- package/dist/fetcher.js.map +1 -0
- package/dist/i18n.d.ts +21 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +31 -0
- package/dist/i18n.js.map +1 -0
- package/dist/image.d.ts +23 -0
- package/dist/image.d.ts.map +1 -0
- package/dist/image.js +49 -0
- package/dist/image.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +67 -0
- package/dist/index.js.map +1 -0
- package/dist/mdx-runtime.d.ts +4 -0
- package/dist/mdx-runtime.d.ts.map +1 -0
- package/dist/mdx-runtime.js +52 -0
- package/dist/mdx-runtime.js.map +1 -0
- package/dist/mdx.d.ts +5 -0
- package/dist/mdx.d.ts.map +1 -0
- package/dist/mdx.js +59 -0
- package/dist/mdx.js.map +1 -0
- package/dist/query.d.ts +28 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +54 -0
- package/dist/query.js.map +1 -0
- package/package.json +112 -0
- package/src/await.ts +58 -0
- package/src/client.ts +89 -0
- package/src/compose.ts +18 -0
- package/src/content.ts +30 -0
- package/src/error.ts +26 -0
- package/src/fetcher.ts +63 -0
- package/src/i18n.ts +46 -0
- package/src/image.ts +68 -0
- package/src/index.ts +75 -0
- package/src/mdx-runtime.ts +54 -0
- package/src/mdx.ts +60 -0
- package/src/query.ts +63 -0
package/src/error.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `errorBoundary` — the Solid error-boundary chain element for nifra's `_error.tsx`. Its own module
|
|
3
|
+
* (imports only `solid-js`) so the client codegen can import it from `@nifrajs/web-solid/client` (which
|
|
4
|
+
* re-exports it). No JSX (`createComponent`), so the package builds with plain `tsc`.
|
|
5
|
+
*/
|
|
6
|
+
import { createComponent, ErrorBoundary, type JSX } from "solid-js"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build an error-boundary chain element bound to `fallback` (a route's `_error` component). nifra's
|
|
10
|
+
* client codegen inserts it before the page in the matched chain; a render error in the subtree renders
|
|
11
|
+
* `fallback` with `{ data: { name, message } }` (via Solid's `<ErrorBoundary>`) instead of crashing.
|
|
12
|
+
* DOM-transparent (renders its children directly — no wrapper element), so it never disturbs hydration.
|
|
13
|
+
*/
|
|
14
|
+
export function errorBoundary(fallback: unknown): unknown {
|
|
15
|
+
const Fallback = fallback as (props: { data: { name: string; message: string } }) => JSX.Element
|
|
16
|
+
return (props: { children?: JSX.Element }): JSX.Element =>
|
|
17
|
+
createComponent(ErrorBoundary, {
|
|
18
|
+
fallback: (err: unknown) => {
|
|
19
|
+
const e = err instanceof Error ? err : new Error(String(err))
|
|
20
|
+
return createComponent(Fallback, { data: { name: e.name, message: e.message } })
|
|
21
|
+
},
|
|
22
|
+
get children() {
|
|
23
|
+
return props.children
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
}
|
package/src/fetcher.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ClientRouter, Fetcher, FetcherState } from "@nifrajs/web"
|
|
2
|
+
/**
|
|
3
|
+
* `@nifrajs/web-solid/fetcher` — Solid bindings for concurrent fetchers. `createFetcher(key)` bridges an
|
|
4
|
+
* independent {@link Fetcher}'s `subscribe`/`snapshot` store into a Solid signal (the pattern the F16
|
|
5
|
+
* spike confirmed) and returns its reactive state accessor plus `load`/`submit`; `useFetchers()`
|
|
6
|
+
* bridges the whole live collection. Imports only `solid-js` (never `solid-js/web`), so route
|
|
7
|
+
* components can use it on the server *and* client without pulling server/build code into the wrong
|
|
8
|
+
* bundle. No JSX (the package builds with plain `tsc`).
|
|
9
|
+
*
|
|
10
|
+
* The router that owns the fetchers registers itself via `setMountedRouter` from `mountRouter`. On the
|
|
11
|
+
* server (no mount) the accessors return an idle state — fetchers are client-only — so SSR markup is
|
|
12
|
+
* unchanged and the first client render after `mountRouter` takes over cleanly.
|
|
13
|
+
*/
|
|
14
|
+
import { type Accessor, createSignal, onCleanup } from "solid-js"
|
|
15
|
+
|
|
16
|
+
// The active client router (set by `mountRouter`). Module-scoped: one app per page, client-only.
|
|
17
|
+
let mountedRouter: ClientRouter | undefined
|
|
18
|
+
|
|
19
|
+
/** Register (or clear) the router that owns fetchers — called by `mountRouter`. Not for app use. */
|
|
20
|
+
export function setMountedRouter(router: ClientRouter | undefined): void {
|
|
21
|
+
mountedRouter = router
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const IDLE: FetcherState = { pending: false, data: undefined }
|
|
25
|
+
const noopAsync = async (): Promise<void> => {}
|
|
26
|
+
|
|
27
|
+
/** A fetcher's reactive state accessor plus its imperative `load`/`submit`. */
|
|
28
|
+
export interface FetcherHandle {
|
|
29
|
+
/** Reactive accessor for the fetcher's {@link FetcherState} (`pending`/`data`/`actionData`/...). */
|
|
30
|
+
readonly state: Accessor<FetcherState>
|
|
31
|
+
/** Load a route path's loader data into this fetcher (concurrent; doesn't touch the active view). */
|
|
32
|
+
readonly load: (path: string) => Promise<void>
|
|
33
|
+
/** Submit an action into this fetcher (concurrent); honors `X-Nifra-Revalidate`. */
|
|
34
|
+
readonly submit: (action: string, body: NonNullable<RequestInit["body"]>) => Promise<void>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Bind the independent fetcher for `key` (created lazily, stable). Returns a reactive `state()`
|
|
39
|
+
* accessor + `load`/`submit`. Multiple `createFetcher` calls with different keys run concurrently
|
|
40
|
+
* without disturbing the active route or each other. Call inside a component (owns the subscription).
|
|
41
|
+
*/
|
|
42
|
+
export function createFetcher(key: string): FetcherHandle {
|
|
43
|
+
const fetcher = mountedRouter?.fetcher(key)
|
|
44
|
+
const [state, setState] = createSignal<FetcherState>(fetcher ? fetcher.snapshot() : IDLE)
|
|
45
|
+
if (fetcher !== undefined) {
|
|
46
|
+
onCleanup(fetcher.subscribe(() => setState(fetcher.snapshot())))
|
|
47
|
+
return { state, load: fetcher.load, submit: fetcher.submit }
|
|
48
|
+
}
|
|
49
|
+
return { state, load: noopAsync, submit: noopAsync }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Bind the whole live fetcher collection — for a global busy view. Returns a reactive accessor; read
|
|
54
|
+
* each entry's `.snapshot()` for its state. Updates whenever any fetcher transitions or one is created.
|
|
55
|
+
*/
|
|
56
|
+
export function useFetchers(): Accessor<readonly Fetcher[]> {
|
|
57
|
+
const router = mountedRouter
|
|
58
|
+
const [list, setList] = createSignal<readonly Fetcher[]>(router ? router.fetchers() : [])
|
|
59
|
+
if (router !== undefined) {
|
|
60
|
+
onCleanup(router.subscribeFetchers(() => setList(router.fetchers())))
|
|
61
|
+
}
|
|
62
|
+
return list
|
|
63
|
+
}
|
package/src/i18n.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nifrajs/web-solid/i18n` — Solid bindings for `@nifrajs/i18n`. `<I18nProvider locale messages>` builds a
|
|
3
|
+
* `Formatter` (a `createMemo`, so switching locale rebuilds it) and provides it; `useT()` reads it.
|
|
4
|
+
* Both `locale` + `messages` are serializable, so a loader returns them, the page renders with the
|
|
5
|
+
* negotiated catalog on the server, and the client rebuilds the same formatter on hydrate (no
|
|
6
|
+
* mismatch). Imports only `solid-js` + `@nifrajs/i18n`; no JSX (`createComponent`).
|
|
7
|
+
*/
|
|
8
|
+
import { createFormatter, type Formatter, type Messages } from "@nifrajs/i18n"
|
|
9
|
+
import {
|
|
10
|
+
type Accessor,
|
|
11
|
+
createComponent,
|
|
12
|
+
createContext,
|
|
13
|
+
createMemo,
|
|
14
|
+
type JSX,
|
|
15
|
+
useContext,
|
|
16
|
+
} from "solid-js"
|
|
17
|
+
|
|
18
|
+
const I18nContext = createContext<Accessor<Formatter>>()
|
|
19
|
+
|
|
20
|
+
export interface I18nProviderProps {
|
|
21
|
+
readonly locale: string
|
|
22
|
+
readonly messages: Messages
|
|
23
|
+
readonly children?: JSX.Element
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Provide a {@link Formatter} (built from `locale` + `messages`) to the subtree. Memoized on
|
|
27
|
+
* `locale`/`messages`, so switching locale rebuilds it. */
|
|
28
|
+
export function I18nProvider(props: I18nProviderProps): JSX.Element {
|
|
29
|
+
const formatter = createMemo(() => createFormatter(props.locale, props.messages))
|
|
30
|
+
return createComponent(I18nContext.Provider, {
|
|
31
|
+
value: formatter,
|
|
32
|
+
get children() {
|
|
33
|
+
return props.children
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Read the current {@link Formatter} (`{ locale, t, n, d }`). Throws if no `<I18nProvider>` is above.
|
|
39
|
+
* nifra switches locale by re-navigating, which re-runs the consuming component with the new catalog. */
|
|
40
|
+
export function useT(): Formatter {
|
|
41
|
+
const formatter = useContext(I18nContext)
|
|
42
|
+
if (formatter === undefined) {
|
|
43
|
+
throw new Error("[nifra/web-solid] useT() must be used within an <I18nProvider>")
|
|
44
|
+
}
|
|
45
|
+
return formatter()
|
|
46
|
+
}
|
package/src/image.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nifrajs/web-solid/image` — a CLS-safe responsive Solid `<Image>`. A thin wrapper over `resolveImage`
|
|
3
|
+
* from `@nifrajs/image`: it computes the responsive `<img>` attributes (`src`/`srcset`/`sizes`/`width`/
|
|
4
|
+
* `height`/`loading`/`decoding`/`fetchpriority` via `toHtmlAttrs`) and renders an `<img>` through
|
|
5
|
+
* Solid's `<Dynamic>`. Extra DOM props (`class`, `style`, `id`, `data-*`, handlers) pass through
|
|
6
|
+
* reactively (`splitProps`/`mergeProps`). Resizing is delegated to the `loader`; nifra bundles no codec.
|
|
7
|
+
* No JSX (`createComponent`/`Dynamic`), so the package builds with plain `tsc`.
|
|
8
|
+
*/
|
|
9
|
+
import { type ImageLoader, type ImageProps, resolveImage, toHtmlAttrs } from "@nifrajs/image"
|
|
10
|
+
import { createComponent, type JSX, mergeProps, splitProps } from "solid-js"
|
|
11
|
+
import { Dynamic } from "solid-js/web"
|
|
12
|
+
|
|
13
|
+
/** Attributes `resolveImage` computes — callers can't override them via DOM passthrough. */
|
|
14
|
+
type ComputedAttrs =
|
|
15
|
+
| "src"
|
|
16
|
+
| "srcset"
|
|
17
|
+
| "sizes"
|
|
18
|
+
| "width"
|
|
19
|
+
| "height"
|
|
20
|
+
| "alt"
|
|
21
|
+
| "loading"
|
|
22
|
+
| "decoding"
|
|
23
|
+
| "fetchpriority"
|
|
24
|
+
|
|
25
|
+
export interface ImageComponentProps
|
|
26
|
+
extends ImageProps,
|
|
27
|
+
Omit<JSX.ImgHTMLAttributes<HTMLImageElement>, ComputedAttrs | keyof ImageProps> {
|
|
28
|
+
/** CDN URL builder. Defaults to the identity loader (no transform — still CLS-safe + lazy). */
|
|
29
|
+
readonly loader?: ImageLoader
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Render a responsive, CLS-safe `<img>`. `width`+`height` are required (reserve layout space);
|
|
34
|
+
* `priority` marks the LCP image (`eager` + `fetchpriority="high"`). Extra DOM props pass through.
|
|
35
|
+
*/
|
|
36
|
+
export function Image(props: ImageComponentProps): JSX.Element {
|
|
37
|
+
// Separate the nifra-only props (not valid DOM attributes) from the DOM passthrough (`rest`), keeping
|
|
38
|
+
// both reactive. `rest` carries class/style/id/handlers; the computed attrs are layered over it.
|
|
39
|
+
const [, rest] = splitProps(props, [
|
|
40
|
+
"loader",
|
|
41
|
+
"widths",
|
|
42
|
+
"quality",
|
|
43
|
+
"priority",
|
|
44
|
+
"src",
|
|
45
|
+
"width",
|
|
46
|
+
"height",
|
|
47
|
+
"alt",
|
|
48
|
+
"sizes",
|
|
49
|
+
"loading",
|
|
50
|
+
])
|
|
51
|
+
const attrs = (): ReturnType<typeof toHtmlAttrs> => {
|
|
52
|
+
// Build ImageProps via conditional spreads (exactOptionalPropertyTypes: an unset optional is
|
|
53
|
+
// 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,
|
|
58
|
+
alt: props.alt,
|
|
59
|
+
...(props.sizes !== undefined ? { sizes: props.sizes } : {}),
|
|
60
|
+
...(props.widths !== undefined ? { widths: props.widths } : {}),
|
|
61
|
+
...(props.quality !== undefined ? { quality: props.quality } : {}),
|
|
62
|
+
...(props.loading !== undefined ? { loading: props.loading } : {}),
|
|
63
|
+
...(props.priority !== undefined ? { priority: props.priority } : {}),
|
|
64
|
+
}
|
|
65
|
+
return toHtmlAttrs(resolveImage(input, props.loader))
|
|
66
|
+
}
|
|
67
|
+
return createComponent(Dynamic, mergeProps(rest, attrs, { component: "img" }))
|
|
68
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nifrajs/web-solid — the Solid render adapter for @nifrajs/web (server side) + the Solid Babel
|
|
3
|
+
* Bun-plugin. Streaming SSR via `renderToStream` + `generateHydrationScript`; the layout-chain
|
|
4
|
+
* fold is in `./compose`. Client hydration lives in `@nifrajs/web-solid/client` (Solid's browser
|
|
5
|
+
* build).
|
|
6
|
+
*/
|
|
7
|
+
import { transformAsync } from "@babel/core"
|
|
8
|
+
// @ts-expect-error — no type declarations published
|
|
9
|
+
import presetTypeScript from "@babel/preset-typescript"
|
|
10
|
+
import type { RenderAdapter } from "@nifrajs/web"
|
|
11
|
+
// @ts-expect-error — no type declarations published
|
|
12
|
+
import presetSolid from "babel-preset-solid"
|
|
13
|
+
import type { BunPlugin } from "bun"
|
|
14
|
+
import {
|
|
15
|
+
generateHydrationScript,
|
|
16
|
+
renderToStream as solidRenderToStream,
|
|
17
|
+
renderToString as solidRenderToString,
|
|
18
|
+
} from "solid-js/web"
|
|
19
|
+
import { compose } from "./compose.ts"
|
|
20
|
+
|
|
21
|
+
const HYDRATION_HEAD = generateHydrationScript()
|
|
22
|
+
|
|
23
|
+
/** The Solid server render adapter — pass to @nifrajs/web's `renderPage`. */
|
|
24
|
+
export const solidAdapter: RenderAdapter = {
|
|
25
|
+
// Synchronous one-pass render for non-deferred pages (renderPage's buffered fast path). Solid's
|
|
26
|
+
// `renderToString` emits the same hydratable markup (same `data-hk` keys seeded by
|
|
27
|
+
// `generateHydrationScript()`) as `renderToStream`, but skips the TransformStream + Solid's
|
|
28
|
+
// streaming machinery — the heaviest of the five renderers on Bun. A page that defer()s/Suspends
|
|
29
|
+
// takes `renderToStream` below (progressive resolution needs it).
|
|
30
|
+
renderToString(chain, props) {
|
|
31
|
+
return solidRenderToString(compose(chain, props))
|
|
32
|
+
},
|
|
33
|
+
renderToStream(chain, props) {
|
|
34
|
+
// Solid's `renderToStream` streams `Uint8Array` chunks into a Web `WritableStream` via
|
|
35
|
+
// `pipeTo` (fire-and-forget — returns void); pipe into a TransformStream and hand back the
|
|
36
|
+
// readable side. Suspense boundaries stream as they resolve; `generateHydrationScript()` (in
|
|
37
|
+
// <head>) seeds client hydration. A render failure errors `ts.readable`, which `renderPage`
|
|
38
|
+
// surfaces on the response body.
|
|
39
|
+
const ts = new TransformStream<Uint8Array, Uint8Array>()
|
|
40
|
+
solidRenderToStream(compose(chain, props)).pipeTo(ts.writable)
|
|
41
|
+
return ts.readable
|
|
42
|
+
},
|
|
43
|
+
hydrationHead() {
|
|
44
|
+
return HYDRATION_HEAD
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Bun build/runtime plugin that compiles Solid components with Babel — `generate: "ssr"`
|
|
50
|
+
* for the server, `"dom"` for the client, `hydratable` so SSR and hydrate align. Solid's
|
|
51
|
+
* reactive-JSX compiler ships only as a Babel plugin (no swc/native port); this runs at
|
|
52
|
+
* build time, on `.tsx` files only.
|
|
53
|
+
*/
|
|
54
|
+
export function solidBunPlugin(generate: "dom" | "ssr"): BunPlugin {
|
|
55
|
+
return {
|
|
56
|
+
name: `nifra-solid-${generate}`,
|
|
57
|
+
setup(build) {
|
|
58
|
+
// Match `.tsx`, tolerating a `?query` suffix (dev servers append one to bust Bun's import
|
|
59
|
+
// cache); strip it before reading the file off disk.
|
|
60
|
+
build.onLoad({ filter: /\.tsx(\?|$)/ }, async (args) => {
|
|
61
|
+
const path = args.path.split("?")[0] ?? args.path
|
|
62
|
+
const source = await Bun.file(path).text()
|
|
63
|
+
const result = await transformAsync(source, {
|
|
64
|
+
filename: path,
|
|
65
|
+
// babel applies presets last→first: strip TS first, then Solid transforms JSX.
|
|
66
|
+
presets: [
|
|
67
|
+
[presetSolid, { generate, hydratable: true }],
|
|
68
|
+
[presetTypeScript, { onlyRemoveTypeImports: true }],
|
|
69
|
+
],
|
|
70
|
+
})
|
|
71
|
+
return { contents: result?.code ?? "", loader: "js" }
|
|
72
|
+
})
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Solid MDX runtime — the `useMDXComponents` provider that `@nifrajs/web-solid/mdx`'s compiled MDX imports
|
|
3
|
+
* (via `@mdx-js`'s `providerImportSource`). MDX emits intrinsic elements (`h1`, `p`, …) as *string*
|
|
4
|
+
* component references, but Solid can't `createComponent("h1")` — its JSX is compile-time. So we map
|
|
5
|
+
* each Markdown-output tag to a Solid component that renders it via `<Dynamic component={tag}>`.
|
|
6
|
+
*/
|
|
7
|
+
import { createComponent } from "solid-js"
|
|
8
|
+
import { Dynamic } from "solid-js/web"
|
|
9
|
+
|
|
10
|
+
// The HTML element set CommonMark + GFM produce. An explicit map (not a Proxy): `@mdx-js` *spreads* the
|
|
11
|
+
// provider's result into a plain object, so only own-enumerable keys survive.
|
|
12
|
+
const MARKDOWN_TAGS = [
|
|
13
|
+
"a",
|
|
14
|
+
"blockquote",
|
|
15
|
+
"br",
|
|
16
|
+
"code",
|
|
17
|
+
"del",
|
|
18
|
+
"em",
|
|
19
|
+
"h1",
|
|
20
|
+
"h2",
|
|
21
|
+
"h3",
|
|
22
|
+
"h4",
|
|
23
|
+
"h5",
|
|
24
|
+
"h6",
|
|
25
|
+
"hr",
|
|
26
|
+
"img",
|
|
27
|
+
"input",
|
|
28
|
+
"li",
|
|
29
|
+
"ol",
|
|
30
|
+
"p",
|
|
31
|
+
"pre",
|
|
32
|
+
"section",
|
|
33
|
+
"span",
|
|
34
|
+
"strong",
|
|
35
|
+
"sup",
|
|
36
|
+
"table",
|
|
37
|
+
"tbody",
|
|
38
|
+
"td",
|
|
39
|
+
"th",
|
|
40
|
+
"thead",
|
|
41
|
+
"tr",
|
|
42
|
+
"ul",
|
|
43
|
+
] as const
|
|
44
|
+
|
|
45
|
+
const components: Record<string, (props: Record<string, unknown>) => unknown> = {}
|
|
46
|
+
for (const tag of MARKDOWN_TAGS) {
|
|
47
|
+
components[tag] = (props) => createComponent(Dynamic, Object.assign({ component: tag }, props))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Returns the intrinsic-element → Solid-component map MDX content uses. Merge in your own overrides by
|
|
51
|
+
* passing `components` to the MDX content component (they take precedence). */
|
|
52
|
+
export function useMDXComponents(): Record<string, (props: Record<string, unknown>) => unknown> {
|
|
53
|
+
return components
|
|
54
|
+
}
|
package/src/mdx.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `solidMdxBunPlugin` — compile `.mdx` routes/files to **Solid** components. MDX is JSX-oriented but
|
|
3
|
+
* Solid's JSX is compile-time, so this: (1) compiles MDX → JSX with `@mdx-js` (keeping JSX), pointing
|
|
4
|
+
* intrinsic elements at `@nifrajs/web-solid/mdx-runtime` (which renders them via Solid's `<Dynamic>`), then
|
|
5
|
+
* (2) runs `babel-preset-solid` — the same transform `solidBunPlugin` applies to `.tsx`. Pass it to
|
|
6
|
+
* `buildClient`/`buildServer`'s `plugins` (`"dom"` for the client, `"ssr"` for the server), like
|
|
7
|
+
* `solidBunPlugin`. `@mdx-js/mdx` is an optional peer (lazy-loaded at build time).
|
|
8
|
+
*/
|
|
9
|
+
import { transformAsync } from "@babel/core"
|
|
10
|
+
// @ts-expect-error — no type declarations published
|
|
11
|
+
import presetTypeScript from "@babel/preset-typescript"
|
|
12
|
+
// @ts-expect-error — no type declarations published
|
|
13
|
+
import presetSolid from "babel-preset-solid"
|
|
14
|
+
import type { BunPlugin } from "bun"
|
|
15
|
+
|
|
16
|
+
// Non-literal so tsc/consumers don't need `@mdx-js/mdx`'s types to typecheck this file (optional peer).
|
|
17
|
+
const MDX_MODULE = "@mdx-js/mdx"
|
|
18
|
+
|
|
19
|
+
interface MdxCompiler {
|
|
20
|
+
compile(source: string, options?: Record<string, unknown>): Promise<{ toString(): string }>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Build a `Bun.build` plugin that loads `.mdx` files as Solid components. `generate`: `"ssr"` for the
|
|
24
|
+
* server build, `"dom"` for the client (matches `solidBunPlugin`). */
|
|
25
|
+
export function solidMdxBunPlugin(generate: "dom" | "ssr"): BunPlugin {
|
|
26
|
+
return {
|
|
27
|
+
name: `nifra-solid-mdx-${generate}`,
|
|
28
|
+
async setup(build) {
|
|
29
|
+
let compile: MdxCompiler["compile"]
|
|
30
|
+
try {
|
|
31
|
+
compile = ((await import(MDX_MODULE)) as MdxCompiler).compile
|
|
32
|
+
} catch {
|
|
33
|
+
throw new Error(
|
|
34
|
+
"@nifrajs/web-solid/mdx: compiling `.mdx` needs the `@mdx-js/mdx` package — install it (`bun add @mdx-js/mdx`).",
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
build.onLoad({ filter: /\.mdx(\?|$)/ }, async (args) => {
|
|
38
|
+
const path = args.path.split("?")[0] ?? args.path
|
|
39
|
+
// 1. MDX → JSX, with intrinsics provided by the Solid runtime (rendered via `<Dynamic>`).
|
|
40
|
+
const jsx = String(
|
|
41
|
+
await compile(await Bun.file(path).text(), {
|
|
42
|
+
jsx: true,
|
|
43
|
+
providerImportSource: "@nifrajs/web-solid/mdx-runtime",
|
|
44
|
+
}),
|
|
45
|
+
)
|
|
46
|
+
// 2. babel-preset-solid (TS stripped first), exactly like `solidBunPlugin`.
|
|
47
|
+
const result = await transformAsync(jsx, {
|
|
48
|
+
filename: `${path}.tsx`,
|
|
49
|
+
presets: [
|
|
50
|
+
[presetSolid, { generate, hydratable: true }],
|
|
51
|
+
[presetTypeScript, { onlyRemoveTypeImports: true }],
|
|
52
|
+
],
|
|
53
|
+
babelrc: false,
|
|
54
|
+
configFile: false,
|
|
55
|
+
})
|
|
56
|
+
return { contents: result?.code ?? "", loader: "js" }
|
|
57
|
+
})
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/query.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createQueryClient, type QueryClient, type QueryState } from "@nifrajs/web"
|
|
2
|
+
/**
|
|
3
|
+
* `@nifrajs/web-solid/query` — Solid bindings for the keyed query-cache. `createQuery(key, fn)` bridges a
|
|
4
|
+
* query's `subscribe`/`snapshot` store into a Solid signal and fetches on mount; `useQueryClient`
|
|
5
|
+
* exposes `invalidateQueries` for after a mutation. Imports only `solid-js` + `@nifrajs/web` (never
|
|
6
|
+
* `solid-js/web`), so route components use it on the server *and* client. No JSX.
|
|
7
|
+
*
|
|
8
|
+
* The query client is created lazily **client-side only** (`typeof window`) — on the server the
|
|
9
|
+
* accessor returns the idle/pending state (queries are client-first; loaders are the SSR data source).
|
|
10
|
+
* The `key` is captured once (Solid setup runs once); a reactive key is a documented future enhancement.
|
|
11
|
+
*/
|
|
12
|
+
import { type Accessor, createSignal, onCleanup, onMount } from "solid-js"
|
|
13
|
+
|
|
14
|
+
let client: QueryClient | undefined
|
|
15
|
+
function getClient(): QueryClient | undefined {
|
|
16
|
+
if (typeof window === "undefined") return undefined // SSR → no client (queries render idle)
|
|
17
|
+
if (client === undefined) client = createQueryClient({ now: () => Date.now() })
|
|
18
|
+
return client
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Access the query client to imperatively `invalidateQueries(keyOrPrefix)` (e.g. after a mutation). */
|
|
22
|
+
export function useQueryClient(): Pick<QueryClient, "invalidateQueries"> {
|
|
23
|
+
return getClient() ?? { invalidateQueries: () => {} }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const IDLE: QueryState<never> = Object.freeze({
|
|
27
|
+
status: "pending",
|
|
28
|
+
data: undefined,
|
|
29
|
+
error: undefined,
|
|
30
|
+
isFetching: false,
|
|
31
|
+
updatedAt: Number.NEGATIVE_INFINITY,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
/** A query's reactive state accessor plus `refetch`. */
|
|
35
|
+
export interface CreateQueryResult<T> {
|
|
36
|
+
/** Reactive accessor for the {@link QueryState} (`status`/`data`/`error`/`isFetching`/`updatedAt`). */
|
|
37
|
+
readonly state: Accessor<QueryState<T>>
|
|
38
|
+
/** Force a refetch (ignores `staleTime`). */
|
|
39
|
+
readonly refetch: () => Promise<T>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Bind the keyed query for `key`, fetched via `fn`. Returns a reactive `state()` accessor + `refetch`.
|
|
44
|
+
* Concurrent `createQuery`s with the same key share one cache entry + one in-flight fetch (dedup).
|
|
45
|
+
* Fetches on mount; SSR-idle. Call inside a component (owns the subscription).
|
|
46
|
+
*/
|
|
47
|
+
export function createQuery<T>(key: unknown, fn: () => Promise<T>): CreateQueryResult<T> {
|
|
48
|
+
const handle = getClient()?.query<T>(key, fn)
|
|
49
|
+
const [state, setState] = createSignal<QueryState<T>>(handle ? handle.snapshot() : IDLE)
|
|
50
|
+
if (handle !== undefined) {
|
|
51
|
+
onCleanup(handle.subscribe(() => setState(() => handle.snapshot())))
|
|
52
|
+
onMount(() => {
|
|
53
|
+
handle.fetch().catch(() => {})
|
|
54
|
+
})
|
|
55
|
+
return { state, refetch: handle.refetch }
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
state,
|
|
59
|
+
refetch: async () => {
|
|
60
|
+
throw new Error("[nifra/web-solid] createQuery.refetch called with no query client (server?)")
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|