@nifrajs/web-preact 0.1.0-alpha.1 → 0.1.0-alpha.3
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 +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/dist/preact-render.d.ts +44 -0
- package/dist/preact-render.d.ts.map +1 -0
- package/dist/preact-render.js +93 -0
- package/dist/preact-render.js.map +1 -0
- package/package.json +4 -4
- package/src/index.ts +9 -5
- package/src/preact-render.ts +111 -0
package/README.md
CHANGED
|
@@ -16,3 +16,11 @@ bun add @nifrajs/web-preact
|
|
|
16
16
|
- AI-readable: <https://nifra.dev/llms.txt>
|
|
17
17
|
|
|
18
18
|
MIT
|
|
19
|
+
|
|
20
|
+
## For AI agents
|
|
21
|
+
|
|
22
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
23
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
24
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
25
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
26
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAWjD,8EAA8E;AAC9E,eAAO,MAAM,aAAa,EAAE,aAqB3B,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -5,19 +5,23 @@
|
|
|
5
5
|
* `@nifrajs/web-preact/client`. Preact uses `h()` render functions, so no JSX/build plugin is needed
|
|
6
6
|
* (contrast `@nifrajs/web-solid`'s Babel plugin).
|
|
7
7
|
*/
|
|
8
|
-
import { renderToString as preactRenderToString } from "preact-render-to-string";
|
|
9
|
-
import { renderToReadableStream } from "preact-render-to-string/stream";
|
|
10
8
|
import { compose } from "./compose.js";
|
|
9
|
+
import { preactRenderToStream, preactRenderToString } from "./preact-render.js";
|
|
11
10
|
/** The Preact server render adapter — pass to @nifrajs/web's `renderPage`. */
|
|
12
11
|
export const preactAdapter = {
|
|
13
12
|
// Synchronous one-pass render for non-deferred pages (renderPage's buffered fast path). The sync
|
|
14
13
|
// `renderToString` emits the same markup as the stream renderer without the chunking machinery.
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
//
|
|
15
|
+
// Async because the renderer module is resolved lazily (from the consumer app's node_modules under Bun
|
|
16
|
+
// runtime SSR — see ./preact-render). `RenderAdapter.renderToString` permits a Promise; renderPage awaits.
|
|
17
|
+
async renderToString(chain, props) {
|
|
18
|
+
const { renderToString } = await preactRenderToString();
|
|
19
|
+
return renderToString(compose(chain, props));
|
|
17
20
|
},
|
|
18
|
-
renderToStream(chain, props) {
|
|
21
|
+
async renderToStream(chain, props) {
|
|
19
22
|
// `renderToReadableStream` yields a Web ReadableStream<Uint8Array>; <Suspense> boundaries
|
|
20
23
|
// (preact/compat) stream as they resolve, mirroring the React adapter.
|
|
24
|
+
const { renderToReadableStream } = await preactRenderToStream();
|
|
21
25
|
return renderToReadableStream(compose(chain, props));
|
|
22
26
|
},
|
|
23
27
|
// Preact reconciles against the existing DOM on hydrate (like React), so there's no per-document
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA;;;;;;GAMG;AACH,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA;;;;;;GAMG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAE/E,8EAA8E;AAC9E,MAAM,CAAC,MAAM,aAAa,GAAkB;IAC1C,iGAAiG;IACjG,gGAAgG;IAChG,EAAE;IACF,uGAAuG;IACvG,2GAA2G;IAC3G,KAAK,CAAC,cAAc,CAAC,KAAK,EAAE,KAAK;QAC/B,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,oBAAoB,EAAE,CAAA;QACvD,OAAO,cAAc,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAA;IAC9C,CAAC;IACD,KAAK,CAAC,cAAc,CAAC,KAAK,EAAE,KAAK;QAC/B,0FAA0F;QAC1F,uEAAuE;QACvE,MAAM,EAAE,sBAAsB,EAAE,GAAG,MAAM,oBAAoB,EAAE,CAAA;QAC/D,OAAO,sBAAsB,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAA;IACtD,CAAC;IACD,iGAAiG;IACjG,kGAAkG;IAClG,aAAa;QACX,OAAO,EAAE,CAAA;IACX,CAAC;CACF,CAAA"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve `preact-render-to-string` (+ its `/stream` subpath) so the renderer shares ONE `preact`
|
|
3
|
+
* module-global with the route components — the fix for the dual-Preact SSR crash (`undefined is not an
|
|
4
|
+
* object (evaluating '…__H')` / silently-empty hook output).
|
|
5
|
+
*
|
|
6
|
+
* WHY this exists: `preact-render-to-string` imports `options` and `h` from `preact` and mutates the
|
|
7
|
+
* shared `options` hooks; `preact/hooks` registers its render hook on the SAME `options` global. Under Bun
|
|
8
|
+
* **runtime** SSR (`nifra dev`, `nifra start`, `nifra_render`, all in-process), a static
|
|
9
|
+
* `import "preact-render-to-string"` in this adapter is resolved from THIS package's own (symlinked)
|
|
10
|
+
* node_modules — which can be a DIFFERENT physical `preact` than the consumer app's components import. Two
|
|
11
|
+
* `preact` copies → two `options` globals → the renderer walks one while `preact/hooks` wrote the other →
|
|
12
|
+
* the vnode never gets its hook-state list → the crash. Resolving the renderer from the consumer **app
|
|
13
|
+
* root** makes it pull the app's `preact` transitively, matching the components: one `options`, one core,
|
|
14
|
+
* no crash. (Empirically verified against a two-copy install fixture — see test/dual-preact.test.ts.)
|
|
15
|
+
*
|
|
16
|
+
* Guarding precisely so the PRODUCTION/BUILT path is untouched: when `Bun.resolveSync` is unavailable
|
|
17
|
+
* (Node / Deno / Cloudflare / Vercel / any bundled output), the build has already bundled a single
|
|
18
|
+
* `preact` + renderer, so the static `import` is correct AND the only thing that works without a Bun
|
|
19
|
+
* resolver. We fall back to it there. The app-root resolution runs ONLY under the Bun runtime.
|
|
20
|
+
*/
|
|
21
|
+
import type { VNode } from "preact";
|
|
22
|
+
/** The slice of `preact-render-to-string` this adapter uses. */
|
|
23
|
+
export interface PreactRenderToString {
|
|
24
|
+
renderToString(node: VNode): string;
|
|
25
|
+
}
|
|
26
|
+
/** The slice of `preact-render-to-string/stream`. */
|
|
27
|
+
export interface PreactRenderToStream {
|
|
28
|
+
renderToReadableStream(node: VNode): ReadableStream<Uint8Array>;
|
|
29
|
+
}
|
|
30
|
+
/** Get `preact-render-to-string` bound to the consumer app's `preact`. Cached after the first call. */
|
|
31
|
+
export declare function preactRenderToString(): Promise<PreactRenderToString>;
|
|
32
|
+
/** Get `preact-render-to-string/stream` bound to the consumer app's `preact`. Cached after the first call. */
|
|
33
|
+
export declare function preactRenderToStream(): Promise<PreactRenderToStream>;
|
|
34
|
+
/** A `Bun.resolveSync`-shaped function (specifier, from) → absolute path. */
|
|
35
|
+
type ResolveSync = (specifier: string, from: string) => string;
|
|
36
|
+
/**
|
|
37
|
+
* Load a renderer subpath, preferring the app-root-resolved copy under the Bun runtime. Exported for unit
|
|
38
|
+
* tests: `resolve` defaults to the ambient `Bun.resolveSync` (undefined on non-Bun hosts), and a test can
|
|
39
|
+
* inject a stub that succeeds (re-root branch) or throws (fallback branch) to cover both deterministically
|
|
40
|
+
* without depending on the machine's node_modules layout.
|
|
41
|
+
*/
|
|
42
|
+
export declare function load<T>(specifier: "preact-render-to-string" | "preact-render-to-string/stream", resolve?: ResolveSync | undefined): Promise<T>;
|
|
43
|
+
export {};
|
|
44
|
+
//# sourceMappingURL=preact-render.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preact-render.d.ts","sourceRoot":"","sources":["../src/preact-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AAEnC,gEAAgE;AAChE,MAAM,WAAW,oBAAoB;IACnC,cAAc,CAAC,IAAI,EAAE,KAAK,GAAG,MAAM,CAAA;CACpC;AACD,qDAAqD;AACrD,MAAM,WAAW,oBAAoB;IACnC,sBAAsB,CAAC,IAAI,EAAE,KAAK,GAAG,cAAc,CAAC,UAAU,CAAC,CAAA;CAChE;AAyBD,uGAAuG;AACvG,wBAAgB,oBAAoB,IAAI,OAAO,CAAC,oBAAoB,CAAC,CAIpE;AAED,8GAA8G;AAC9G,wBAAgB,oBAAoB,IAAI,OAAO,CAAC,oBAAoB,CAAC,CAIpE;AAED,6EAA6E;AAC7E,KAAK,WAAW,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;AAE9D;;;;;GAKG;AACH,wBAAsB,IAAI,CAAC,CAAC,EAC1B,SAAS,EAAE,yBAAyB,GAAG,gCAAgC,EACvE,OAAO,GAAE,WAAW,GAAG,SAA2B,GACjD,OAAO,CAAC,CAAC,CAAC,CAsBZ"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve `preact-render-to-string` (+ its `/stream` subpath) so the renderer shares ONE `preact`
|
|
3
|
+
* module-global with the route components — the fix for the dual-Preact SSR crash (`undefined is not an
|
|
4
|
+
* object (evaluating '…__H')` / silently-empty hook output).
|
|
5
|
+
*
|
|
6
|
+
* WHY this exists: `preact-render-to-string` imports `options` and `h` from `preact` and mutates the
|
|
7
|
+
* shared `options` hooks; `preact/hooks` registers its render hook on the SAME `options` global. Under Bun
|
|
8
|
+
* **runtime** SSR (`nifra dev`, `nifra start`, `nifra_render`, all in-process), a static
|
|
9
|
+
* `import "preact-render-to-string"` in this adapter is resolved from THIS package's own (symlinked)
|
|
10
|
+
* node_modules — which can be a DIFFERENT physical `preact` than the consumer app's components import. Two
|
|
11
|
+
* `preact` copies → two `options` globals → the renderer walks one while `preact/hooks` wrote the other →
|
|
12
|
+
* the vnode never gets its hook-state list → the crash. Resolving the renderer from the consumer **app
|
|
13
|
+
* root** makes it pull the app's `preact` transitively, matching the components: one `options`, one core,
|
|
14
|
+
* no crash. (Empirically verified against a two-copy install fixture — see test/dual-preact.test.ts.)
|
|
15
|
+
*
|
|
16
|
+
* Guarding precisely so the PRODUCTION/BUILT path is untouched: when `Bun.resolveSync` is unavailable
|
|
17
|
+
* (Node / Deno / Cloudflare / Vercel / any bundled output), the build has already bundled a single
|
|
18
|
+
* `preact` + renderer, so the static `import` is correct AND the only thing that works without a Bun
|
|
19
|
+
* resolver. We fall back to it there. The app-root resolution runs ONLY under the Bun runtime.
|
|
20
|
+
*/
|
|
21
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
22
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
23
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
24
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return path;
|
|
28
|
+
};
|
|
29
|
+
const bunResolver = globalThis.Bun;
|
|
30
|
+
const hasBunResolveSync = typeof bunResolver?.resolveSync === "function";
|
|
31
|
+
// Cache the in-flight Promises (not the modules) so concurrent first renders share one import each.
|
|
32
|
+
let cachedSync;
|
|
33
|
+
let cachedStream;
|
|
34
|
+
/**
|
|
35
|
+
* The consumer app root used to re-root the renderer. `nifra dev` / `nifra start` run with the CLI's
|
|
36
|
+
* `process.cwd()` set to the app directory, and `nifra_render`'s subprocess `process.chdir`es to the app
|
|
37
|
+
* dir before SSR, so `process.cwd()` is the app root on every Bun runtime SSR path. Read lazily so a host
|
|
38
|
+
* that `chdir`s after import still sees the right root.
|
|
39
|
+
*/
|
|
40
|
+
function appRoot() {
|
|
41
|
+
return process.cwd();
|
|
42
|
+
}
|
|
43
|
+
/** Get `preact-render-to-string` bound to the consumer app's `preact`. Cached after the first call. */
|
|
44
|
+
export function preactRenderToString() {
|
|
45
|
+
if (cachedSync !== undefined)
|
|
46
|
+
return cachedSync;
|
|
47
|
+
cachedSync = load("preact-render-to-string");
|
|
48
|
+
return cachedSync;
|
|
49
|
+
}
|
|
50
|
+
/** Get `preact-render-to-string/stream` bound to the consumer app's `preact`. Cached after the first call. */
|
|
51
|
+
export function preactRenderToStream() {
|
|
52
|
+
if (cachedStream !== undefined)
|
|
53
|
+
return cachedStream;
|
|
54
|
+
cachedStream = load("preact-render-to-string/stream");
|
|
55
|
+
return cachedStream;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Load a renderer subpath, preferring the app-root-resolved copy under the Bun runtime. Exported for unit
|
|
59
|
+
* tests: `resolve` defaults to the ambient `Bun.resolveSync` (undefined on non-Bun hosts), and a test can
|
|
60
|
+
* inject a stub that succeeds (re-root branch) or throws (fallback branch) to cover both deterministically
|
|
61
|
+
* without depending on the machine's node_modules layout.
|
|
62
|
+
*/
|
|
63
|
+
export async function load(specifier, resolve = bunResolverFn()) {
|
|
64
|
+
if (resolve !== undefined) {
|
|
65
|
+
// Bun runtime SSR: re-root to the app's copy so the renderer shares the components' `preact`.
|
|
66
|
+
// `preact-render-to-string` is a direct DEPENDENCY of this adapter (not a peer), so whether it sits at
|
|
67
|
+
// the app root depends on the installer's hoisting. When it's hoisted, app-root resolution re-roots
|
|
68
|
+
// its transitive `preact` to the app's copy (the fix). When it isn't resolvable from the app root, we
|
|
69
|
+
// fall through to this module's own (always-present) copy below rather than crashing.
|
|
70
|
+
try {
|
|
71
|
+
const resolved = resolve(specifier, appRoot());
|
|
72
|
+
return (await import(__rewriteRelativeImportExtension(resolved)));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// App-root resolution failed; use the specifier resolved from this module's own location below.
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Built bundle / Node / Deno / edge (or the Bun-resolve fallback above): the static specifier is
|
|
79
|
+
// bundled (or is the only copy present). A constant bare-specifier dynamic import lets the bundler
|
|
80
|
+
// include it; at runtime it resolves the copy visible to this package. Two explicit branches (not a
|
|
81
|
+
// variable specifier) keep the bundler's static analysis intact.
|
|
82
|
+
if (specifier === "preact-render-to-string/stream") {
|
|
83
|
+
return (await import("preact-render-to-string/stream"));
|
|
84
|
+
}
|
|
85
|
+
return (await import("preact-render-to-string"));
|
|
86
|
+
}
|
|
87
|
+
/** The ambient `Bun.resolveSync`, or undefined on a non-Bun host (where the static import is the only path). */
|
|
88
|
+
function bunResolverFn() {
|
|
89
|
+
return hasBunResolveSync && bunResolver?.resolveSync !== undefined
|
|
90
|
+
? bunResolver.resolveSync.bind(bunResolver)
|
|
91
|
+
: undefined;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=preact-render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preact-render.js","sourceRoot":"","sources":["../src/preact-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;;;;;;;;;AAmBH,MAAM,WAAW,GAAI,UAA6C,CAAC,GAAG,CAAA;AACtE,MAAM,iBAAiB,GAAG,OAAO,WAAW,EAAE,WAAW,KAAK,UAAU,CAAA;AAExE,oGAAoG;AACpG,IAAI,UAAqD,CAAA;AACzD,IAAI,YAAuD,CAAA;AAE3D;;;;;GAKG;AACH,SAAS,OAAO;IACd,OAAO,OAAO,CAAC,GAAG,EAAE,CAAA;AACtB,CAAC;AAED,uGAAuG;AACvG,MAAM,UAAU,oBAAoB;IAClC,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,UAAU,CAAA;IAC/C,UAAU,GAAG,IAAI,CAAuB,yBAAyB,CAAC,CAAA;IAClE,OAAO,UAAU,CAAA;AACnB,CAAC;AAED,8GAA8G;AAC9G,MAAM,UAAU,oBAAoB;IAClC,IAAI,YAAY,KAAK,SAAS;QAAE,OAAO,YAAY,CAAA;IACnD,YAAY,GAAG,IAAI,CAAuB,gCAAgC,CAAC,CAAA;IAC3E,OAAO,YAAY,CAAA;AACrB,CAAC;AAKD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,IAAI,CACxB,SAAuE,EACvE,UAAmC,aAAa,EAAE;IAElD,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,8FAA8F;QAC9F,uGAAuG;QACvG,oGAAoG;QACpG,sGAAsG;QACtG,sFAAsF;QACtF,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,OAAO,CAAC,SAAS,EAAE,OAAO,EAAE,CAAC,CAAA;YAC9C,OAAO,CAAC,MAAM,MAAM,kCAAC,QAAQ,EAAC,CAAM,CAAA;QACtC,CAAC;QAAC,MAAM,CAAC;YACP,gGAAgG;QAClG,CAAC;IACH,CAAC;IACD,iGAAiG;IACjG,mGAAmG;IACnG,oGAAoG;IACpG,iEAAiE;IACjE,IAAI,SAAS,KAAK,gCAAgC,EAAE,CAAC;QACnD,OAAO,CAAC,MAAM,MAAM,CAAC,gCAAgC,CAAC,CAAM,CAAA;IAC9D,CAAC;IACD,OAAO,CAAC,MAAM,MAAM,CAAC,yBAAyB,CAAC,CAAM,CAAA;AACvD,CAAC;AAED,gHAAgH;AAChH,SAAS,aAAa;IACpB,OAAO,iBAAiB,IAAI,WAAW,EAAE,WAAW,KAAK,SAAS;QAChE,CAAC,CAAC,WAAW,CAAC,WAAW,CAAC,IAAI,CAAC,WAAW,CAAC;QAC3C,CAAC,CAAC,SAAS,CAAA;AACf,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/web-preact",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.3",
|
|
4
4
|
"description": "Preact render adapter for @nifrajs/web — streaming SSR + hydration (no build plugin).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -70,9 +70,9 @@
|
|
|
70
70
|
"access": "public"
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|
|
73
|
-
"@nifrajs/i18n": "
|
|
74
|
-
"@nifrajs/image": "
|
|
75
|
-
"@nifrajs/web": "
|
|
73
|
+
"@nifrajs/i18n": "0.1.0-alpha.2",
|
|
74
|
+
"@nifrajs/image": "0.1.0-alpha.2",
|
|
75
|
+
"@nifrajs/web": "0.1.0-alpha.3",
|
|
76
76
|
"preact-render-to-string": "^6.5.0"
|
|
77
77
|
},
|
|
78
78
|
"peerDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -6,20 +6,24 @@ import type { RenderAdapter } from "@nifrajs/web"
|
|
|
6
6
|
* `@nifrajs/web-preact/client`. Preact uses `h()` render functions, so no JSX/build plugin is needed
|
|
7
7
|
* (contrast `@nifrajs/web-solid`'s Babel plugin).
|
|
8
8
|
*/
|
|
9
|
-
import { renderToString as preactRenderToString } from "preact-render-to-string"
|
|
10
|
-
import { renderToReadableStream } from "preact-render-to-string/stream"
|
|
11
9
|
import { compose } from "./compose.ts"
|
|
10
|
+
import { preactRenderToStream, preactRenderToString } from "./preact-render.ts"
|
|
12
11
|
|
|
13
12
|
/** The Preact server render adapter — pass to @nifrajs/web's `renderPage`. */
|
|
14
13
|
export const preactAdapter: RenderAdapter = {
|
|
15
14
|
// Synchronous one-pass render for non-deferred pages (renderPage's buffered fast path). The sync
|
|
16
15
|
// `renderToString` emits the same markup as the stream renderer without the chunking machinery.
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
//
|
|
17
|
+
// Async because the renderer module is resolved lazily (from the consumer app's node_modules under Bun
|
|
18
|
+
// runtime SSR — see ./preact-render). `RenderAdapter.renderToString` permits a Promise; renderPage awaits.
|
|
19
|
+
async renderToString(chain, props) {
|
|
20
|
+
const { renderToString } = await preactRenderToString()
|
|
21
|
+
return renderToString(compose(chain, props))
|
|
19
22
|
},
|
|
20
|
-
renderToStream(chain, props) {
|
|
23
|
+
async renderToStream(chain, props) {
|
|
21
24
|
// `renderToReadableStream` yields a Web ReadableStream<Uint8Array>; <Suspense> boundaries
|
|
22
25
|
// (preact/compat) stream as they resolve, mirroring the React adapter.
|
|
26
|
+
const { renderToReadableStream } = await preactRenderToStream()
|
|
23
27
|
return renderToReadableStream(compose(chain, props))
|
|
24
28
|
},
|
|
25
29
|
// Preact reconciles against the existing DOM on hydrate (like React), so there's no per-document
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve `preact-render-to-string` (+ its `/stream` subpath) so the renderer shares ONE `preact`
|
|
3
|
+
* module-global with the route components — the fix for the dual-Preact SSR crash (`undefined is not an
|
|
4
|
+
* object (evaluating '…__H')` / silently-empty hook output).
|
|
5
|
+
*
|
|
6
|
+
* WHY this exists: `preact-render-to-string` imports `options` and `h` from `preact` and mutates the
|
|
7
|
+
* shared `options` hooks; `preact/hooks` registers its render hook on the SAME `options` global. Under Bun
|
|
8
|
+
* **runtime** SSR (`nifra dev`, `nifra start`, `nifra_render`, all in-process), a static
|
|
9
|
+
* `import "preact-render-to-string"` in this adapter is resolved from THIS package's own (symlinked)
|
|
10
|
+
* node_modules — which can be a DIFFERENT physical `preact` than the consumer app's components import. Two
|
|
11
|
+
* `preact` copies → two `options` globals → the renderer walks one while `preact/hooks` wrote the other →
|
|
12
|
+
* the vnode never gets its hook-state list → the crash. Resolving the renderer from the consumer **app
|
|
13
|
+
* root** makes it pull the app's `preact` transitively, matching the components: one `options`, one core,
|
|
14
|
+
* no crash. (Empirically verified against a two-copy install fixture — see test/dual-preact.test.ts.)
|
|
15
|
+
*
|
|
16
|
+
* Guarding precisely so the PRODUCTION/BUILT path is untouched: when `Bun.resolveSync` is unavailable
|
|
17
|
+
* (Node / Deno / Cloudflare / Vercel / any bundled output), the build has already bundled a single
|
|
18
|
+
* `preact` + renderer, so the static `import` is correct AND the only thing that works without a Bun
|
|
19
|
+
* resolver. We fall back to it there. The app-root resolution runs ONLY under the Bun runtime.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { VNode } from "preact"
|
|
23
|
+
|
|
24
|
+
/** The slice of `preact-render-to-string` this adapter uses. */
|
|
25
|
+
export interface PreactRenderToString {
|
|
26
|
+
renderToString(node: VNode): string
|
|
27
|
+
}
|
|
28
|
+
/** The slice of `preact-render-to-string/stream`. */
|
|
29
|
+
export interface PreactRenderToStream {
|
|
30
|
+
renderToReadableStream(node: VNode): ReadableStream<Uint8Array>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Narrow only the one Bun method we need (it isn't in the ambient lib types) so no `any` is introduced.
|
|
34
|
+
// `resolveSync(specifier, from)` is the only lever that re-roots a BARE specifier — a runtime
|
|
35
|
+
// `Bun.plugin` onResolve does NOT fire for bare specifiers like `preact-render-to-string` (verified).
|
|
36
|
+
interface BunResolver {
|
|
37
|
+
resolveSync(specifier: string, from: string): string
|
|
38
|
+
}
|
|
39
|
+
const bunResolver = (globalThis as { Bun?: Partial<BunResolver> }).Bun
|
|
40
|
+
const hasBunResolveSync = typeof bunResolver?.resolveSync === "function"
|
|
41
|
+
|
|
42
|
+
// Cache the in-flight Promises (not the modules) so concurrent first renders share one import each.
|
|
43
|
+
let cachedSync: Promise<PreactRenderToString> | undefined
|
|
44
|
+
let cachedStream: Promise<PreactRenderToStream> | undefined
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The consumer app root used to re-root the renderer. `nifra dev` / `nifra start` run with the CLI's
|
|
48
|
+
* `process.cwd()` set to the app directory, and `nifra_render`'s subprocess `process.chdir`es to the app
|
|
49
|
+
* dir before SSR, so `process.cwd()` is the app root on every Bun runtime SSR path. Read lazily so a host
|
|
50
|
+
* that `chdir`s after import still sees the right root.
|
|
51
|
+
*/
|
|
52
|
+
function appRoot(): string {
|
|
53
|
+
return process.cwd()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Get `preact-render-to-string` bound to the consumer app's `preact`. Cached after the first call. */
|
|
57
|
+
export function preactRenderToString(): Promise<PreactRenderToString> {
|
|
58
|
+
if (cachedSync !== undefined) return cachedSync
|
|
59
|
+
cachedSync = load<PreactRenderToString>("preact-render-to-string")
|
|
60
|
+
return cachedSync
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Get `preact-render-to-string/stream` bound to the consumer app's `preact`. Cached after the first call. */
|
|
64
|
+
export function preactRenderToStream(): Promise<PreactRenderToStream> {
|
|
65
|
+
if (cachedStream !== undefined) return cachedStream
|
|
66
|
+
cachedStream = load<PreactRenderToStream>("preact-render-to-string/stream")
|
|
67
|
+
return cachedStream
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A `Bun.resolveSync`-shaped function (specifier, from) → absolute path. */
|
|
71
|
+
type ResolveSync = (specifier: string, from: string) => string
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Load a renderer subpath, preferring the app-root-resolved copy under the Bun runtime. Exported for unit
|
|
75
|
+
* tests: `resolve` defaults to the ambient `Bun.resolveSync` (undefined on non-Bun hosts), and a test can
|
|
76
|
+
* inject a stub that succeeds (re-root branch) or throws (fallback branch) to cover both deterministically
|
|
77
|
+
* without depending on the machine's node_modules layout.
|
|
78
|
+
*/
|
|
79
|
+
export async function load<T>(
|
|
80
|
+
specifier: "preact-render-to-string" | "preact-render-to-string/stream",
|
|
81
|
+
resolve: ResolveSync | undefined = bunResolverFn(),
|
|
82
|
+
): Promise<T> {
|
|
83
|
+
if (resolve !== undefined) {
|
|
84
|
+
// Bun runtime SSR: re-root to the app's copy so the renderer shares the components' `preact`.
|
|
85
|
+
// `preact-render-to-string` is a direct DEPENDENCY of this adapter (not a peer), so whether it sits at
|
|
86
|
+
// the app root depends on the installer's hoisting. When it's hoisted, app-root resolution re-roots
|
|
87
|
+
// its transitive `preact` to the app's copy (the fix). When it isn't resolvable from the app root, we
|
|
88
|
+
// fall through to this module's own (always-present) copy below rather than crashing.
|
|
89
|
+
try {
|
|
90
|
+
const resolved = resolve(specifier, appRoot())
|
|
91
|
+
return (await import(resolved)) as T
|
|
92
|
+
} catch {
|
|
93
|
+
// App-root resolution failed; use the specifier resolved from this module's own location below.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Built bundle / Node / Deno / edge (or the Bun-resolve fallback above): the static specifier is
|
|
97
|
+
// bundled (or is the only copy present). A constant bare-specifier dynamic import lets the bundler
|
|
98
|
+
// include it; at runtime it resolves the copy visible to this package. Two explicit branches (not a
|
|
99
|
+
// variable specifier) keep the bundler's static analysis intact.
|
|
100
|
+
if (specifier === "preact-render-to-string/stream") {
|
|
101
|
+
return (await import("preact-render-to-string/stream")) as T
|
|
102
|
+
}
|
|
103
|
+
return (await import("preact-render-to-string")) as T
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The ambient `Bun.resolveSync`, or undefined on a non-Bun host (where the static import is the only path). */
|
|
107
|
+
function bunResolverFn(): ResolveSync | undefined {
|
|
108
|
+
return hasBunResolveSync && bunResolver?.resolveSync !== undefined
|
|
109
|
+
? bunResolver.resolveSync.bind(bunResolver)
|
|
110
|
+
: undefined
|
|
111
|
+
}
|