@nifrajs/web-react 2.1.0 → 2.2.0
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/compose.d.ts.map +1 -1
- package/dist/compose.js +4 -1
- package/dist/compose.js.map +1 -1
- package/dist/react-dom-server.d.ts +21 -0
- package/dist/react-dom-server.d.ts.map +1 -1
- package/dist/react-dom-server.js +62 -1
- package/dist/react-dom-server.js.map +1 -1
- package/package.json +5 -5
- package/src/compose.ts +8 -1
- package/src/react-dom-server.ts +74 -1
package/dist/compose.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAyC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAM7E;;;;;;;;;;GAUG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,EAAE,KAAK,EAAE,WAAW,GAAG,SAAS,
|
|
1
|
+
{"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAyC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAM7E;;;;;;;;;;GAUG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,EAAE,KAAK,EAAE,WAAW,GAAG,SAAS,CA0BhF"}
|
package/dist/compose.js
CHANGED
|
@@ -18,7 +18,10 @@ export function compose(chain, props) {
|
|
|
18
18
|
let node = createElement(chain[last], props);
|
|
19
19
|
for (let i = last - 1; i >= 0; i--) {
|
|
20
20
|
// children passed as the 3rd arg (not a `children` prop) — React's canonical form.
|
|
21
|
-
|
|
21
|
+
// Each layout receives its own loader data at its own index. Layouts are the chain's leading
|
|
22
|
+
// prefix, so `layoutData[i]` belongs to `chain[i]`; anything past that end (a client-only `_error`
|
|
23
|
+
// boundary marker, the page) reads `undefined` and is unaffected.
|
|
24
|
+
node = createElement(chain[i], { data: props.layoutData?.[i] ?? null }, node);
|
|
22
25
|
}
|
|
23
26
|
return createElement(RouterContext.Provider, {
|
|
24
27
|
value: {
|
package/dist/compose.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compose.js","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAA0C,MAAM,OAAO,CAAA;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAE3C,oGAAoG;AACpG,MAAM,YAAY,GAAqC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;AAExE;;;;;;;;;;GAUG;AACH,MAAM,UAAU,OAAO,CAAC,KAAyB,EAAE,KAAkB;IACnE,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAA;IAC7B,IAAI,IAAI,GAAc,aAAa,CAAC,KAAK,CAAC,IAAI,CAAmC,EAAE,KAAK,CAAC,CAAA;IACzF,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACnC,mFAAmF;QACnF,IAAI,GAAG,aAAa,
|
|
1
|
+
{"version":3,"file":"compose.js","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAA0C,MAAM,OAAO,CAAA;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAE3C,oGAAoG;AACpG,MAAM,YAAY,GAAqC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;AAExE;;;;;;;;;;GAUG;AACH,MAAM,UAAU,OAAO,CAAC,KAAyB,EAAE,KAAkB;IACnE,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAA;IAC7B,IAAI,IAAI,GAAc,aAAa,CAAC,KAAK,CAAC,IAAI,CAAmC,EAAE,KAAK,CAAC,CAAA;IACzF,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACnC,mFAAmF;QACnF,6FAA6F;QAC7F,mGAAmG;QACnG,kEAAkE;QAClE,IAAI,GAAG,aAAa,CAClB,KAAK,CAAC,CAAC,CAAyC,EAChD,EAAE,IAAI,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,EACvC,IAAI,CACL,CAAA;IACH,CAAC;IACD,OAAO,aAAa,CAClB,aAAa,CAAC,QAAQ,EACtB;QACE,KAAK,EAAE;YACL,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,YAAY;YACpC,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE;YACtB,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,KAAK;YAC/B,WAAW,EAAE,KAAK,CAAC,WAAW;SAC/B;KACF,EACD,IAAI,CACL,CAAA;AACH,CAAC"}
|
|
@@ -37,6 +37,27 @@ export interface ReactDomServer {
|
|
|
37
37
|
export declare function reactDomServer(): Promise<ReactDomServer>;
|
|
38
38
|
/** A `Bun.resolveSync`-shaped function (specifier, from) → absolute path. */
|
|
39
39
|
type ResolveSync = (specifier: string, from: string) => string;
|
|
40
|
+
/**
|
|
41
|
+
* The dual-React crash, caught on the RESOLVED graph instead of after it detonates.
|
|
42
|
+
*
|
|
43
|
+
* The re-root above makes `react-dom/server` load from the app root, which is right almost always. What it
|
|
44
|
+
* cannot guarantee is that react-dom then pulls in the SAME physical `react` the route components import -
|
|
45
|
+
* a nested `react` under react-dom, or a components tree resolving react elsewhere, still yields two cores.
|
|
46
|
+
* Two cores is two hook dispatchers, and the renderer sees a null (or foreign) one: the SSR throws
|
|
47
|
+
* `resolveDispatcher().useState is null` from deep inside react-dom-server, naming a React internal and
|
|
48
|
+
* nothing about the two directories that actually caused it. That is hours of inference from a message
|
|
49
|
+
* that points nowhere useful.
|
|
50
|
+
*
|
|
51
|
+
* `nifra doctor` checks what is INSTALLED; this checks what SSR actually RESOLVED, which is the only thing
|
|
52
|
+
* that can catch a duplicate the two dev pipelines introduce (Bun for SSR, Vite for the client) rather
|
|
53
|
+
* than the install. It compares the realpath of the `react` react-dom will render with against the
|
|
54
|
+
* `react` the components import, and if they differ throws with BOTH paths - turning a five-hour hunt into
|
|
55
|
+
* a five-second read. Silent when they agree, which is the single-copy common case.
|
|
56
|
+
*
|
|
57
|
+
* Never manufactures a failure: if either side cannot be resolved it returns, because a resolver that
|
|
58
|
+
* cannot answer is not evidence of a duplicate. Exported for direct unit testing.
|
|
59
|
+
*/
|
|
60
|
+
export declare function assertSingleReactCore(reactDomServerPath: string, resolve: ResolveSync, realpath?: (path: string) => string): void;
|
|
40
61
|
/**
|
|
41
62
|
* Load `react-dom/server`, preferring the app-root-resolved copy under the Bun runtime. Exported for unit
|
|
42
63
|
* tests: `resolve` defaults to the ambient `Bun.resolveSync` (undefined on non-Bun hosts), and a test can
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react-dom-server.d.ts","sourceRoot":"","sources":["../src/react-dom-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC;wFACwF;AACxF,MAAM,WAAW,cAAc;IAC7B,cAAc,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAAA;IACvC,sBAAsB,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAAA;CAC7E;
|
|
1
|
+
{"version":3,"file":"react-dom-server.d.ts","sourceRoot":"","sources":["../src/react-dom-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC;wFACwF;AACxF,MAAM,WAAW,cAAc;IAC7B,cAAc,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAAA;IACvC,sBAAsB,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAAA;CAC7E;AA6CD;;;;GAIG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAAC,cAAc,CAAC,CAIxD;AAED,6EAA6E;AAC7E,KAAK,WAAW,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;AAE9D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,qBAAqB,CACnC,kBAAkB,EAAE,MAAM,EAC1B,OAAO,EAAE,WAAW,EACpB,QAAQ,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAwB,GACnD,IAAI,CAsBN;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,GAAE,WAAW,GAAG,SAA2B,GACjD,OAAO,CAAC,cAAc,CAAC,CAyBzB;AAED;;;;;;;;kEAQkE;AAClE,wBAAgB,aAAa,IAAI,WAAW,GAAG,SAAS,CAKvD"}
|
package/dist/react-dom-server.js
CHANGED
|
@@ -30,6 +30,15 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
30
30
|
}
|
|
31
31
|
return path;
|
|
32
32
|
};
|
|
33
|
+
function dirnamePortable(path) {
|
|
34
|
+
const slash = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
35
|
+
return slash <= 0 ? "." : path.slice(0, slash);
|
|
36
|
+
}
|
|
37
|
+
function runtimeRealpath(path) {
|
|
38
|
+
const runtimeProcess = globalThis.process;
|
|
39
|
+
const fs = runtimeProcess?.getBuiltinModule?.("node:fs");
|
|
40
|
+
return fs?.realpathSync?.(path) ?? path;
|
|
41
|
+
}
|
|
33
42
|
const bunResolver = globalThis.Bun;
|
|
34
43
|
const hasBunResolveSync = typeof bunResolver?.resolveSync === "function";
|
|
35
44
|
// Resolve once and cache: the module identity is stable for the process lifetime, and re-resolving per
|
|
@@ -57,6 +66,49 @@ export function reactDomServer() {
|
|
|
57
66
|
cached = loadReactDomServer();
|
|
58
67
|
return cached;
|
|
59
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* The dual-React crash, caught on the RESOLVED graph instead of after it detonates.
|
|
71
|
+
*
|
|
72
|
+
* The re-root above makes `react-dom/server` load from the app root, which is right almost always. What it
|
|
73
|
+
* cannot guarantee is that react-dom then pulls in the SAME physical `react` the route components import -
|
|
74
|
+
* a nested `react` under react-dom, or a components tree resolving react elsewhere, still yields two cores.
|
|
75
|
+
* Two cores is two hook dispatchers, and the renderer sees a null (or foreign) one: the SSR throws
|
|
76
|
+
* `resolveDispatcher().useState is null` from deep inside react-dom-server, naming a React internal and
|
|
77
|
+
* nothing about the two directories that actually caused it. That is hours of inference from a message
|
|
78
|
+
* that points nowhere useful.
|
|
79
|
+
*
|
|
80
|
+
* `nifra doctor` checks what is INSTALLED; this checks what SSR actually RESOLVED, which is the only thing
|
|
81
|
+
* that can catch a duplicate the two dev pipelines introduce (Bun for SSR, Vite for the client) rather
|
|
82
|
+
* than the install. It compares the realpath of the `react` react-dom will render with against the
|
|
83
|
+
* `react` the components import, and if they differ throws with BOTH paths - turning a five-hour hunt into
|
|
84
|
+
* a five-second read. Silent when they agree, which is the single-copy common case.
|
|
85
|
+
*
|
|
86
|
+
* Never manufactures a failure: if either side cannot be resolved it returns, because a resolver that
|
|
87
|
+
* cannot answer is not evidence of a duplicate. Exported for direct unit testing.
|
|
88
|
+
*/
|
|
89
|
+
export function assertSingleReactCore(reactDomServerPath, resolve, realpath = runtimeRealpath) {
|
|
90
|
+
let rendererReact;
|
|
91
|
+
let componentsReact;
|
|
92
|
+
try {
|
|
93
|
+
// The `react` react-dom/server itself resolves - the core whose dispatcher the renderer sets.
|
|
94
|
+
rendererReact = realpath(resolve("react", dirnamePortable(reactDomServerPath)));
|
|
95
|
+
// The `react` the app's route components import - the core they call hooks on.
|
|
96
|
+
componentsReact = realpath(resolve("react", appRoot()));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (rendererReact === componentsReact)
|
|
102
|
+
return;
|
|
103
|
+
throw new Error("[nifra/web-react] two copies of React reached SSR, so hooks render against a null dispatcher " +
|
|
104
|
+
"(the `resolveDispatcher().useState is null` crash). react-dom renders with a DIFFERENT React " +
|
|
105
|
+
"than your components import:\n" +
|
|
106
|
+
` react-dom's react: ${rendererReact}\n` +
|
|
107
|
+
` components' react: ${componentsReact}\n` +
|
|
108
|
+
"Module identity is path-based, so two copies fail even at the same version. Dedupe react to one " +
|
|
109
|
+
"physical copy - usually a single hoisted install at the workspace root (`nifra doctor` locates " +
|
|
110
|
+
"the duplicate). A Vite `resolve.dedupe`/alias fixes only the client bundle, not this SSR path.");
|
|
111
|
+
}
|
|
60
112
|
/**
|
|
61
113
|
* Load `react-dom/server`, preferring the app-root-resolved copy under the Bun runtime. Exported for unit
|
|
62
114
|
* tests: `resolve` defaults to the ambient `Bun.resolveSync` (undefined on non-Bun hosts), and a test can
|
|
@@ -71,9 +123,18 @@ export async function loadReactDomServer(resolve = bunResolverFn()) {
|
|
|
71
123
|
// rather than crashing — degraded (possible duplicate) but never a hard failure.
|
|
72
124
|
try {
|
|
73
125
|
const resolved = resolve("react-dom/server", appRoot());
|
|
126
|
+
// Before returning the module, verify the react-dom we just re-rooted shares the components' React.
|
|
127
|
+
// The re-root fixes the common case; this catches the residual duplicate loudly, with both paths,
|
|
128
|
+
// instead of leaving it to throw a dispatcher-null error on the first hook render. Runs once (this
|
|
129
|
+
// whole function is cached), dev/unbundled Bun only (the branch `resolve` gates).
|
|
130
|
+
assertSingleReactCore(resolved, resolve);
|
|
74
131
|
return (await import(__rewriteRelativeImportExtension(resolved)));
|
|
75
132
|
}
|
|
76
|
-
catch {
|
|
133
|
+
catch (err) {
|
|
134
|
+
// A genuine duplicate is surfaced (re-thrown with both paths); any other failure (react-dom not at
|
|
135
|
+
// the app root, an odd nested layout) falls through to the bundled specifier rather than crashing.
|
|
136
|
+
if (err instanceof Error && err.message.includes("two copies of React"))
|
|
137
|
+
throw err;
|
|
77
138
|
// App-root resolution failed; use the specifier resolved from this module's own location below.
|
|
78
139
|
}
|
|
79
140
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react-dom-server.js","sourceRoot":"","sources":["../src/react-dom-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"react-dom-server.js","sourceRoot":"","sources":["../src/react-dom-server.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/web-react",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "React render adapter for @nifrajs/web — SSR + hydration (Bun-native JSX, no Babel).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -80,10 +80,10 @@
|
|
|
80
80
|
"access": "public"
|
|
81
81
|
},
|
|
82
82
|
"dependencies": {
|
|
83
|
-
"@nifrajs/core": "2.
|
|
84
|
-
"@nifrajs/i18n": "2.
|
|
85
|
-
"@nifrajs/image": "2.
|
|
86
|
-
"@nifrajs/web": "2.
|
|
83
|
+
"@nifrajs/core": "2.2.0",
|
|
84
|
+
"@nifrajs/i18n": "2.2.0",
|
|
85
|
+
"@nifrajs/image": "2.2.0",
|
|
86
|
+
"@nifrajs/web": "2.2.0"
|
|
87
87
|
},
|
|
88
88
|
"peerDependencies": {
|
|
89
89
|
"react": "^19.0.0",
|
package/src/compose.ts
CHANGED
|
@@ -21,7 +21,14 @@ export function compose(chain: readonly unknown[], props: RenderProps): ReactNod
|
|
|
21
21
|
let node: ReactNode = createElement(chain[last] as FunctionComponent<RenderProps>, props)
|
|
22
22
|
for (let i = last - 1; i >= 0; i--) {
|
|
23
23
|
// children passed as the 3rd arg (not a `children` prop) — React's canonical form.
|
|
24
|
-
|
|
24
|
+
// Each layout receives its own loader data at its own index. Layouts are the chain's leading
|
|
25
|
+
// prefix, so `layoutData[i]` belongs to `chain[i]`; anything past that end (a client-only `_error`
|
|
26
|
+
// boundary marker, the page) reads `undefined` and is unaffected.
|
|
27
|
+
node = createElement(
|
|
28
|
+
chain[i] as FunctionComponent<{ data: unknown }>,
|
|
29
|
+
{ data: props.layoutData?.[i] ?? null },
|
|
30
|
+
node,
|
|
31
|
+
)
|
|
25
32
|
}
|
|
26
33
|
return createElement(
|
|
27
34
|
RouterContext.Provider,
|
package/src/react-dom-server.ts
CHANGED
|
@@ -32,6 +32,23 @@ export interface ReactDomServer {
|
|
|
32
32
|
renderToReadableStream(node: ReactNode): Promise<ReadableStream<Uint8Array>>
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
interface RuntimeProcess {
|
|
36
|
+
getBuiltinModule?(specifier: string): unknown
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function dirnamePortable(path: string): string {
|
|
40
|
+
const slash = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"))
|
|
41
|
+
return slash <= 0 ? "." : path.slice(0, slash)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function runtimeRealpath(path: string): string {
|
|
45
|
+
const runtimeProcess = (globalThis as { process?: RuntimeProcess }).process
|
|
46
|
+
const fs = runtimeProcess?.getBuiltinModule?.("node:fs") as
|
|
47
|
+
| { realpathSync?: (value: string) => string }
|
|
48
|
+
| undefined
|
|
49
|
+
return fs?.realpathSync?.(path) ?? path
|
|
50
|
+
}
|
|
51
|
+
|
|
35
52
|
// `globalThis.Bun` isn't in the ambient lib types; narrow exactly the one method we need so we never
|
|
36
53
|
// reach for `any`. `resolveSync(specifier, from)` returns the absolute path the specifier resolves to
|
|
37
54
|
// when required from `from` — Bun's runtime resolver, the only lever that re-roots a BARE specifier
|
|
@@ -72,6 +89,54 @@ export function reactDomServer(): Promise<ReactDomServer> {
|
|
|
72
89
|
/** A `Bun.resolveSync`-shaped function (specifier, from) → absolute path. */
|
|
73
90
|
type ResolveSync = (specifier: string, from: string) => string
|
|
74
91
|
|
|
92
|
+
/**
|
|
93
|
+
* The dual-React crash, caught on the RESOLVED graph instead of after it detonates.
|
|
94
|
+
*
|
|
95
|
+
* The re-root above makes `react-dom/server` load from the app root, which is right almost always. What it
|
|
96
|
+
* cannot guarantee is that react-dom then pulls in the SAME physical `react` the route components import -
|
|
97
|
+
* a nested `react` under react-dom, or a components tree resolving react elsewhere, still yields two cores.
|
|
98
|
+
* Two cores is two hook dispatchers, and the renderer sees a null (or foreign) one: the SSR throws
|
|
99
|
+
* `resolveDispatcher().useState is null` from deep inside react-dom-server, naming a React internal and
|
|
100
|
+
* nothing about the two directories that actually caused it. That is hours of inference from a message
|
|
101
|
+
* that points nowhere useful.
|
|
102
|
+
*
|
|
103
|
+
* `nifra doctor` checks what is INSTALLED; this checks what SSR actually RESOLVED, which is the only thing
|
|
104
|
+
* that can catch a duplicate the two dev pipelines introduce (Bun for SSR, Vite for the client) rather
|
|
105
|
+
* than the install. It compares the realpath of the `react` react-dom will render with against the
|
|
106
|
+
* `react` the components import, and if they differ throws with BOTH paths - turning a five-hour hunt into
|
|
107
|
+
* a five-second read. Silent when they agree, which is the single-copy common case.
|
|
108
|
+
*
|
|
109
|
+
* Never manufactures a failure: if either side cannot be resolved it returns, because a resolver that
|
|
110
|
+
* cannot answer is not evidence of a duplicate. Exported for direct unit testing.
|
|
111
|
+
*/
|
|
112
|
+
export function assertSingleReactCore(
|
|
113
|
+
reactDomServerPath: string,
|
|
114
|
+
resolve: ResolveSync,
|
|
115
|
+
realpath: (path: string) => string = runtimeRealpath,
|
|
116
|
+
): void {
|
|
117
|
+
let rendererReact: string
|
|
118
|
+
let componentsReact: string
|
|
119
|
+
try {
|
|
120
|
+
// The `react` react-dom/server itself resolves - the core whose dispatcher the renderer sets.
|
|
121
|
+
rendererReact = realpath(resolve("react", dirnamePortable(reactDomServerPath)))
|
|
122
|
+
// The `react` the app's route components import - the core they call hooks on.
|
|
123
|
+
componentsReact = realpath(resolve("react", appRoot()))
|
|
124
|
+
} catch {
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
if (rendererReact === componentsReact) return
|
|
128
|
+
throw new Error(
|
|
129
|
+
"[nifra/web-react] two copies of React reached SSR, so hooks render against a null dispatcher " +
|
|
130
|
+
"(the `resolveDispatcher().useState is null` crash). react-dom renders with a DIFFERENT React " +
|
|
131
|
+
"than your components import:\n" +
|
|
132
|
+
` react-dom's react: ${rendererReact}\n` +
|
|
133
|
+
` components' react: ${componentsReact}\n` +
|
|
134
|
+
"Module identity is path-based, so two copies fail even at the same version. Dedupe react to one " +
|
|
135
|
+
"physical copy - usually a single hoisted install at the workspace root (`nifra doctor` locates " +
|
|
136
|
+
"the duplicate). A Vite `resolve.dedupe`/alias fixes only the client bundle, not this SSR path.",
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
75
140
|
/**
|
|
76
141
|
* Load `react-dom/server`, preferring the app-root-resolved copy under the Bun runtime. Exported for unit
|
|
77
142
|
* tests: `resolve` defaults to the ambient `Bun.resolveSync` (undefined on non-Bun hosts), and a test can
|
|
@@ -88,8 +153,16 @@ export async function loadReactDomServer(
|
|
|
88
153
|
// rather than crashing — degraded (possible duplicate) but never a hard failure.
|
|
89
154
|
try {
|
|
90
155
|
const resolved = resolve("react-dom/server", appRoot())
|
|
156
|
+
// Before returning the module, verify the react-dom we just re-rooted shares the components' React.
|
|
157
|
+
// The re-root fixes the common case; this catches the residual duplicate loudly, with both paths,
|
|
158
|
+
// instead of leaving it to throw a dispatcher-null error on the first hook render. Runs once (this
|
|
159
|
+
// whole function is cached), dev/unbundled Bun only (the branch `resolve` gates).
|
|
160
|
+
assertSingleReactCore(resolved, resolve)
|
|
91
161
|
return (await import(resolved)) as ReactDomServer
|
|
92
|
-
} catch {
|
|
162
|
+
} catch (err) {
|
|
163
|
+
// A genuine duplicate is surfaced (re-thrown with both paths); any other failure (react-dom not at
|
|
164
|
+
// the app root, an odd nested layout) falls through to the bundled specifier rather than crashing.
|
|
165
|
+
if (err instanceof Error && err.message.includes("two copies of React")) throw err
|
|
93
166
|
// App-root resolution failed; use the specifier resolved from this module's own location below.
|
|
94
167
|
}
|
|
95
168
|
}
|