/** * Resolve `react-dom/server` so it shares ONE React core with the route components - the fix for the * dual-React SSR crash (`resolveDispatcher().useState` is null / "Invalid hook call: mismatching versions * of React and the renderer"). * * WHY this exists: under Bun **runtime** SSR (`nifra dev`, `nifra start`, `nifra_render`, all in-process), * a static `import "react-dom/server"` in this adapter is resolved by Bun from THIS package's own * (symlinked) node_modules - which can be a DIFFERENT physical `react` than the one the consumer app's * route components import. Two React cores → two hook dispatchers → the renderer's dispatcher is the wrong * (or null) one → the crash. Resolving `react-dom/server` from the consumer **app root** instead makes * react-dom pull the app's `react` transitively, matching the components' `react`: a single core, a single * dispatcher, no crash. (Empirically verified against a two-copy install fixture - see * test/dual-react.test.ts.) * * Guarding precisely so the BUILT path is untouched: a bundle is detected two ways - `Bun.resolveSync` is * unavailable (Node / Deno / Cloudflare / Vercel), OR `buildServer` tagged the output with * `process.env.NIFRA_SSR_BUNDLED` (a `target:"bun"` bundle DOES keep `Bun.resolveSync` under the Bun * runtime, so the resolver test alone can't see it). In either case the build already bundled+deduped a * single `react-dom` (buildServer's `reactDedupePlugin` pins `react`), so the static `import` is correct. * Re-rooting a bundle would instead re-import a SECOND react-dom from disk - a second React core whose hook * dispatcher is null for the bundled components → the `…H.useRef of null` SSR crash. The app-root re-root * therefore runs ONLY under an UNBUNDLED Bun runtime (nifra dev/start, nifra_render), where the duplication * can occur, `Bun.resolveSync` exists, and no bundle marker is present. */ import type { ReactNode } from "react"; /** The slice of `react-dom/server` this adapter uses. Typed locally so the dynamic import (which Bun * resolves to an absolute path string) stays strict - no `any` crosses the boundary. */ export interface ReactDomServer { renderToString(node: ReactNode): string; renderToReadableStream(node: ReactNode): Promise>; } /** * Get `react-dom/server` bound to the consumer app's React. Cached after the first call. Under the Bun * runtime, dynamically imports the copy resolved from the app root; otherwise (built/bundled, or a non-Bun * host) loads the statically-bundled `react-dom/server`. */ export declare function reactDomServer(): Promise; /** A `Bun.resolveSync`-shaped function (specifier, from) → absolute path. */ type ResolveSync = (specifier: string, from: string) => string; /** * The dual-React crash, caught on the RESOLVED graph instead of after it detonates. * * The re-root above makes `react-dom/server` load from the app root, which is right almost always. What it * cannot guarantee is that react-dom then pulls in the SAME physical `react` the route components import - * a nested `react` under react-dom, or a components tree resolving react elsewhere, still yields two cores. * Two cores is two hook dispatchers, and the renderer sees a null (or foreign) one: the SSR throws * `resolveDispatcher().useState is null` from deep inside react-dom-server, naming a React internal and * nothing about the two directories that actually caused it. That is hours of inference from a message * that points nowhere useful. * * `nifra doctor` checks what is INSTALLED; this checks what SSR actually RESOLVED, which is the only thing * that can catch a duplicate the two dev pipelines introduce (Bun for SSR, Vite for the client) rather * than the install. It compares the realpath of the `react` react-dom will render with against the * `react` the components import, and if they differ throws with BOTH paths - turning a five-hour hunt into * a five-second read. Silent when they agree, which is the single-copy common case. * * Never manufactures a failure: if either side cannot be resolved it returns, because a resolver that * cannot answer is not evidence of a duplicate. Exported for direct unit testing. */ export declare function assertSingleReactCore(reactDomServerPath: string, resolve: ResolveSync, realpath?: (path: string) => string): void; /** * Load `react-dom/server`, preferring the app-root-resolved copy under the Bun runtime. Exported for unit * tests: `resolve` defaults to the ambient `Bun.resolveSync` (undefined on non-Bun hosts), and a test can * inject a stub that succeeds (re-root branch) or throws (fallback branch) to cover both deterministically * without depending on the machine's node_modules layout. */ export declare function loadReactDomServer(resolve?: ResolveSync | undefined): Promise; /** * Whether this module is executing from inside a bundle rather than as its own file. Bundling rewrites * `import.meta.url` to the OUTPUT file (`server-bun.js`, `server.mjs`, …), so when the basename is no * longer `react-dom-server.*` (the `.ts` source under the Bun workspace runtime, the `.js` in the * published dist), this module has been concatenated into a bundle. The second, marker-free layer of * bundle detection: it catches a server bundled by hand (`bun build --target bun` without `buildServer`), * which keeps `Bun.resolveSync` but never defines `NIFRA_SSR_BUNDLED`. `url` is injectable for tests; * runtime callers pass nothing. */ export declare function moduleLooksBundled(url?: string): boolean; /** * The resolver `loadReactDomServer` uses by default, or `undefined` when re-rooting must NOT happen - a * non-Bun host (no `Bun.resolveSync`; the static import is the only path) OR a BUNDLED SSR output. * `buildServer` defines `process.env.NIFRA_SSR_BUNDLED` to `"1"` in every bundle, where react-dom is * already inlined + deduped to the components' React (reactDedupePlugin); re-rooting there would re-import * a SECOND react-dom from disk (a `target:"bun"` bundle still has `Bun.resolveSync`), giving the bundled * components a foreign/null hook dispatcher → the `…H.useRef of null` crash. The marker is read here (per * call, not at module load) so it stays driveable from a test. Unbundled Bun runtimes don't set it, so * dev/start still re-root. Exported for unit testing the gate. * * A bundle produced WITHOUT `buildServer` has no marker, so `moduleLooksBundled` backstops it: re-rooting * inside such a bundle re-imports react-dom from disk, where the dev/prod switch reads the RUNTIME * `NODE_ENV` - with hooks that is the dual-core crash, without hooks it silently renders with development * React (a large SSR slowdown that looks like a runtime regression). Bundled output takes the static * import, whose react-dom the bundle already inlined and deduped. */ export declare function bunResolverFn(): ResolveSync | undefined; export {}; //# sourceMappingURL=react-dom-server.d.ts.map