@nifrajs/web-preact 0.1.0-alpha.1 → 0.1.0-alpha.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/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.
@@ -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;AAYjD,8EAA8E;AAC9E,eAAO,MAAM,aAAa,EAAE,aAgB3B,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
- renderToString(chain, props) {
16
- return preactRenderToString(compose(chain, props));
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,cAAc,IAAI,oBAAoB,EAAE,MAAM,yBAAyB,CAAA;AAChF,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAA;AACvE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAEtC,8EAA8E;AAC9E,MAAM,CAAC,MAAM,aAAa,GAAkB;IAC1C,iGAAiG;IACjG,gGAAgG;IAChG,cAAc,CAAC,KAAK,EAAE,KAAK;QACzB,OAAO,oBAAoB,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAA;IACpD,CAAC;IACD,cAAc,CAAC,KAAK,EAAE,KAAK;QACzB,0FAA0F;QAC1F,uEAAuE;QACvE,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"}
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.1",
3
+ "version": "0.1.0-alpha.2",
4
4
  "description": "Preact render adapter for @nifrajs/web — streaming SSR + hydration (no build plugin).",
5
5
  "keywords": [
6
6
  "bun",
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
- renderToString(chain, props) {
18
- return preactRenderToString(compose(chain, props))
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
+ }