@ecopages/react 0.2.0-alpha.5 → 0.2.0-alpha.50
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 +152 -29
- package/package.json +16 -12
- package/src/eco-embed.d.ts +11 -0
- package/src/eco-embed.js +11 -0
- package/src/react-hmr-strategy.d.ts +60 -43
- package/src/react-hmr-strategy.js +297 -144
- package/src/react-renderer.d.ts +169 -42
- package/src/react-renderer.js +484 -164
- package/src/react.constants.d.ts +1 -0
- package/src/react.constants.js +4 -0
- package/src/react.plugin.d.ts +38 -111
- package/src/react.plugin.js +132 -61
- package/src/react.types.d.ts +88 -0
- package/src/react.types.js +0 -0
- package/src/router-adapter.d.ts +7 -14
- package/src/services/react-bundle.service.d.ts +19 -31
- package/src/services/react-bundle.service.js +51 -100
- package/src/services/react-hmr-page-metadata-cache.d.ts +9 -0
- package/src/services/react-hmr-page-metadata-cache.js +18 -2
- package/src/services/react-hydration-asset.service.d.ts +28 -19
- package/src/services/react-hydration-asset.service.js +85 -66
- package/src/services/react-mdx-config-dependency.service.d.ts +36 -0
- package/src/services/react-mdx-config-dependency.service.js +122 -0
- package/src/services/react-page-module.service.d.ts +10 -2
- package/src/services/react-page-module.service.js +47 -39
- package/src/services/react-page-payload.service.d.ts +46 -0
- package/src/services/react-page-payload.service.js +67 -0
- package/src/services/react-runtime-bundle.service.d.ts +15 -13
- package/src/services/react-runtime-bundle.service.js +103 -180
- package/src/utils/client-graph-boundary-plugin.d.ts +1 -1
- package/src/utils/client-graph-boundary-plugin.js +80 -3
- package/src/utils/component-config-traversal.d.ts +36 -0
- package/src/utils/component-config-traversal.js +54 -0
- package/src/utils/declared-modules.d.ts +1 -1
- package/src/utils/declared-modules.js +7 -16
- package/src/utils/dynamic.test.browser.d.ts +1 -0
- package/src/utils/dynamic.test.browser.js +33 -0
- package/src/utils/hydration-scripts.d.ts +27 -6
- package/src/utils/hydration-scripts.js +177 -44
- package/src/utils/hydration-scripts.test.browser.d.ts +1 -0
- package/src/utils/hydration-scripts.test.browser.js +198 -0
- package/src/utils/react-dom-runtime-interop-plugin.d.ts +5 -0
- package/src/utils/react-dom-runtime-interop-plugin.js +29 -0
- package/src/utils/react-mdx-loader-plugin.d.ts +1 -1
- package/src/utils/react-mdx-loader-plugin.js +13 -5
- package/src/utils/react-runtime-alias-map.d.ts +6 -0
- package/src/utils/react-runtime-alias-map.js +33 -0
- package/src/utils/use-sync-external-store-shim-plugin.d.ts +5 -0
- package/src/utils/use-sync-external-store-shim-plugin.js +41 -0
- package/CHANGELOG.md +0 -67
- package/src/react-hmr-strategy.ts +0 -455
- package/src/react-renderer.ts +0 -403
- package/src/react.plugin.ts +0 -241
- package/src/router-adapter.ts +0 -95
- package/src/services/react-bundle.service.ts +0 -217
- package/src/services/react-hmr-page-metadata-cache.ts +0 -24
- package/src/services/react-hydration-asset.service.ts +0 -260
- package/src/services/react-page-module.service.ts +0 -214
- package/src/services/react-runtime-bundle.service.ts +0 -271
- package/src/utils/client-graph-boundary-plugin.ts +0 -710
- package/src/utils/client-only.ts +0 -27
- package/src/utils/declared-modules.ts +0 -99
- package/src/utils/dynamic.ts +0 -27
- package/src/utils/hmr-scripts.ts +0 -47
- package/src/utils/html-boundary.ts +0 -66
- package/src/utils/hydration-scripts.ts +0 -338
- package/src/utils/reachability-analyzer.ts +0 -593
- package/src/utils/react-mdx-loader-plugin.ts +0 -40
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hydration script generators for React pages.
|
|
3
|
-
* These functions create the
|
|
3
|
+
* These functions create the page entry modules that hydrate React routes.
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
import type { ReactRouterAdapter } from '../router-adapter.js';
|
|
@@ -8,8 +8,12 @@ import type { ReactRouterAdapter } from '../router-adapter.js';
|
|
|
8
8
|
* Options for generating a hydration script.
|
|
9
9
|
*/
|
|
10
10
|
export type HydrationScriptOptions = {
|
|
11
|
-
/** The
|
|
11
|
+
/** The module path imported by the page entry module. */
|
|
12
12
|
importPath: string;
|
|
13
|
+
/** Browser expression that resolves to the page module URL the router should import. */
|
|
14
|
+
pageModuleUrlExpression?: string;
|
|
15
|
+
/** Stable id of the page entry script tag in the document. */
|
|
16
|
+
scriptId: string;
|
|
13
17
|
/** Direct import path for React runtime module */
|
|
14
18
|
reactImportPath: string;
|
|
15
19
|
/** Direct import path for react-dom/client runtime module */
|
|
@@ -26,14 +30,14 @@ export type HydrationScriptOptions = {
|
|
|
26
30
|
export type IslandHydrationScriptOptions = {
|
|
27
31
|
/** Bundled browser module path for the island component. */
|
|
28
32
|
importPath: string;
|
|
33
|
+
/** Stable id of the island bootstrap script tag in the document. */
|
|
34
|
+
scriptId: string;
|
|
29
35
|
/** Browser import path for React runtime. */
|
|
30
36
|
reactImportPath: string;
|
|
31
37
|
/** Browser import path for react-dom/client runtime. */
|
|
32
38
|
reactDomClientImportPath: string;
|
|
33
|
-
/** Selector that resolves to
|
|
39
|
+
/** Selector that resolves to all SSR root elements for this island component. */
|
|
34
40
|
targetSelector: string;
|
|
35
|
-
/** Serialized component props emitted at render time. */
|
|
36
|
-
props: Record<string, unknown>;
|
|
37
41
|
/** Optional stable component id used to resolve named exports reliably. */
|
|
38
42
|
componentRef?: string;
|
|
39
43
|
/** Optional source file hint used as fallback for component resolution. */
|
|
@@ -43,7 +47,18 @@ export type IslandHydrationScriptOptions = {
|
|
|
43
47
|
};
|
|
44
48
|
/**
|
|
45
49
|
* Creates a hydration script for client-side React hydration.
|
|
46
|
-
*
|
|
50
|
+
*
|
|
51
|
+
* Why this dispatcher exists:
|
|
52
|
+
* the runtime matrix is small but behaviorally different across development vs
|
|
53
|
+
* production and router vs non-router pages. Keeping that branch here preserves
|
|
54
|
+
* a compact public API while allowing each emitted script to stay focused.
|
|
55
|
+
*
|
|
56
|
+
* Selection rules:
|
|
57
|
+
* - development uses readable scripts with HMR hooks
|
|
58
|
+
* - production uses minified equivalents
|
|
59
|
+
* - router presence decides whether page updates flow through the router runtime
|
|
60
|
+
* or rebuild directly from the page module
|
|
61
|
+
*
|
|
47
62
|
* @param options - Configuration options for script generation
|
|
48
63
|
* @returns The generated hydration script as a string
|
|
49
64
|
*/
|
|
@@ -63,8 +78,14 @@ export declare function createHydrationScript(options: HydrationScriptOptions):
|
|
|
63
78
|
* - resolves the component export by metadata (`componentRef`, `componentFile`)
|
|
64
79
|
* before falling back to default/first function export
|
|
65
80
|
* - selects island root using `targetSelector`
|
|
81
|
+
* - replaces the SSR host with a dedicated client-owned container
|
|
66
82
|
* - creates a fresh React root and renders with serialized `props`
|
|
67
83
|
*
|
|
84
|
+
* Why it remounts instead of hydrating:
|
|
85
|
+
* island SSR intentionally avoids synthetic wrapper elements. The runtime swaps
|
|
86
|
+
* the authored SSR node for a dedicated client-owned container before mounting
|
|
87
|
+
* so the server markup stays clean while the client still gets a stable root.
|
|
88
|
+
*
|
|
68
89
|
* @param options Island script generation options.
|
|
69
90
|
* @returns Browser-executable JavaScript module source.
|
|
70
91
|
*/
|
|
@@ -9,8 +9,66 @@ function getHmrImportStatement(isMdx) {
|
|
|
9
9
|
function getComponentType(isMdx) {
|
|
10
10
|
return isMdx ? "MDX" : "React";
|
|
11
11
|
}
|
|
12
|
+
function getDevPageRootCleanupScript() {
|
|
13
|
+
return `window.__ECO_PAGES__ = window.__ECO_PAGES__ || {};
|
|
14
|
+
window.__ECO_PAGES__.react = window.__ECO_PAGES__.react || {};
|
|
15
|
+
window.__ECO_PAGES__.react.cleanupPageRoot = () => {
|
|
16
|
+
const activeRoot = window.__ECO_PAGES__.react?.pageRoot || root;
|
|
17
|
+
if (!activeRoot) {
|
|
18
|
+
window.__ECO_PAGES__.react.pageRoot = null;
|
|
19
|
+
window.__ECO_PAGES__?.navigation?.releaseOwnership?.("react-router");
|
|
20
|
+
delete window.__ECO_PAGES__.page;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
window.__ECO_PAGES__.react.pageRoot = null;
|
|
24
|
+
window.__ECO_PAGES__?.navigation?.releaseOwnership?.("react-router");
|
|
25
|
+
delete window.__ECO_PAGES__.page;
|
|
26
|
+
root = null;
|
|
27
|
+
activeRoot.unmount();
|
|
28
|
+
};`;
|
|
29
|
+
}
|
|
30
|
+
function getProdPageRootCleanupScript() {
|
|
31
|
+
return 'window.__ECO_PAGES__=window.__ECO_PAGES__||{};window.__ECO_PAGES__.react=window.__ECO_PAGES__.react||{};window.__ECO_PAGES__.react.cleanupPageRoot=()=>{const a=window.__ECO_PAGES__.react?.pageRoot||root;if(!a){window.__ECO_PAGES__.react.pageRoot=null;window.__ECO_PAGES__?.navigation?.releaseOwnership?.("react-router");delete window.__ECO_PAGES__.page;return}window.__ECO_PAGES__.react.pageRoot=null;window.__ECO_PAGES__?.navigation?.releaseOwnership?.("react-router");delete window.__ECO_PAGES__.page;root=null;a.unmount()};';
|
|
32
|
+
}
|
|
33
|
+
function getDevRouterBootstrapRegistrationScript() {
|
|
34
|
+
return `const currentOwnerState = window.__ECO_PAGES__?.navigation?.getOwnerState?.();
|
|
35
|
+
if (!(currentOwnerState?.owner === "react-router" && currentOwnerState.canHandleSpaNavigation)) {
|
|
36
|
+
window.__ECO_PAGES__?.navigation?.register({
|
|
37
|
+
owner: "react-router",
|
|
38
|
+
cleanupBeforeHandoff: async () => {
|
|
39
|
+
window.__ECO_PAGES__?.react?.cleanupPageRoot?.();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
window.__ECO_PAGES__?.navigation?.claimOwnership?.("react-router");
|
|
43
|
+
}`;
|
|
44
|
+
}
|
|
45
|
+
function getProdRouterBootstrapRegistrationScript() {
|
|
46
|
+
return 'const o=window.__ECO_PAGES__?.navigation?.getOwnerState?.();if(!(o?.owner==="react-router"&&o.canHandleSpaNavigation)){window.__ECO_PAGES__?.navigation?.register({owner:"react-router",cleanupBeforeHandoff:async()=>{window.__ECO_PAGES__?.react?.cleanupPageRoot?.()}});window.__ECO_PAGES__?.navigation?.claimOwnership?.("react-router")}';
|
|
47
|
+
}
|
|
48
|
+
function getDevReuseExistingRouterRootScript() {
|
|
49
|
+
return `const shouldReuseExistingRouterRoot = () => {
|
|
50
|
+
const ownerState = window.__ECO_PAGES__?.navigation?.getOwnerState?.();
|
|
51
|
+
return Boolean(
|
|
52
|
+
window.__ECO_PAGES__.react?.pageRoot &&
|
|
53
|
+
ownerState?.owner === "react-router" &&
|
|
54
|
+
ownerState.canHandleSpaNavigation
|
|
55
|
+
);
|
|
56
|
+
};`;
|
|
57
|
+
}
|
|
58
|
+
function getProdReuseExistingRouterRootScript() {
|
|
59
|
+
return 'const sr=()=>{const o=window.__ECO_PAGES__?.navigation?.getOwnerState?.();return!!(window.__ECO_PAGES__.react?.pageRoot&&o?.owner==="react-router"&&o.canHandleSpaNavigation)};';
|
|
60
|
+
}
|
|
61
|
+
function getDevRerunRegistrationScript(scriptId) {
|
|
62
|
+
return `window.__ECO_PAGES__ = window.__ECO_PAGES__ || {};
|
|
63
|
+
window.__ECO_PAGES__.rerunScripts = window.__ECO_PAGES__.rerunScripts || {};
|
|
64
|
+
window.__ECO_PAGES__.rerunScripts[${JSON.stringify(scriptId)}] = mount;`;
|
|
65
|
+
}
|
|
66
|
+
function getProdRerunRegistrationScript(scriptId) {
|
|
67
|
+
return `window.__ECO_PAGES__=window.__ECO_PAGES__||{};window.__ECO_PAGES__.rerunScripts=window.__ECO_PAGES__.rerunScripts||{};window.__ECO_PAGES__.rerunScripts[${JSON.stringify(scriptId)}]=m;`;
|
|
68
|
+
}
|
|
12
69
|
function createDevScriptWithRouter(options) {
|
|
13
|
-
const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath } = options;
|
|
70
|
+
const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath, scriptId } = options;
|
|
71
|
+
const pageModuleUrlExpression = options.pageModuleUrlExpression ?? "import.meta.url";
|
|
14
72
|
const { components, getRouterProps } = router;
|
|
15
73
|
if (!routerImportPath) {
|
|
16
74
|
throw new Error("routerImportPath is required when router adapter is configured");
|
|
@@ -20,11 +78,21 @@ import { hydrateRoot } from "${reactDomClientImportPath}";
|
|
|
20
78
|
import { createElement } from "${reactImportPath}";
|
|
21
79
|
import { ${components.router}, ${components.pageContent} } from "${routerImportPath}";
|
|
22
80
|
${getImportStatement(importPath, isMdx)}
|
|
81
|
+
const pageModuleUrl = ${pageModuleUrlExpression};
|
|
82
|
+
export default Page;
|
|
83
|
+
export const config = Page.config;
|
|
84
|
+
const isActivePageEntry = Boolean(document.querySelector('script[data-eco-script-id="${scriptId}"]'));
|
|
23
85
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
window.
|
|
27
|
-
|
|
86
|
+
if (isActivePageEntry) {
|
|
87
|
+
|
|
88
|
+
window.__ECO_PAGES__ = window.__ECO_PAGES__ || {};
|
|
89
|
+
window.__ECO_PAGES__.hmrHandlers = window.__ECO_PAGES__.hmrHandlers || {};
|
|
90
|
+
window.__ECO_PAGES__.react = window.__ECO_PAGES__.react || {};
|
|
91
|
+
window.__ECO_PAGES__.react.pageRoot = window.__ECO_PAGES__.react.pageRoot || null;
|
|
92
|
+
let root = window.__ECO_PAGES__.react.pageRoot;
|
|
93
|
+
${getDevPageRootCleanupScript()}
|
|
94
|
+
${getDevRouterBootstrapRegistrationScript()}
|
|
95
|
+
${getDevReuseExistingRouterRootScript()}
|
|
28
96
|
|
|
29
97
|
const getPageData = () => {
|
|
30
98
|
const el = document.getElementById("__ECO_PAGE_DATA__");
|
|
@@ -36,8 +104,8 @@ const getPageData = () => {
|
|
|
36
104
|
|
|
37
105
|
const props = getPageData();
|
|
38
106
|
|
|
39
|
-
window.
|
|
40
|
-
module:
|
|
107
|
+
window.__ECO_PAGES__.page = {
|
|
108
|
+
module: pageModuleUrl,
|
|
41
109
|
props
|
|
42
110
|
};
|
|
43
111
|
|
|
@@ -47,20 +115,46 @@ const createTree = (Component, props) => {
|
|
|
47
115
|
};
|
|
48
116
|
|
|
49
117
|
const mount = () => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
118
|
+
const props = getPageData();
|
|
119
|
+
window.__ECO_PAGES__.page = {
|
|
120
|
+
module: pageModuleUrl,
|
|
121
|
+
props
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (shouldReuseExistingRouterRoot()) {
|
|
125
|
+
root = window.__ECO_PAGES__.react.pageRoot;
|
|
126
|
+
} else if (window.__ECO_PAGES__.react?.pageRoot) {
|
|
127
|
+
root = window.__ECO_PAGES__.react.pageRoot;
|
|
128
|
+
root.render(createTree(Page, props));
|
|
129
|
+
} else {
|
|
130
|
+
root = hydrateRoot(document.body, createTree(Page, props), {
|
|
131
|
+
onRecoverableError: (err) => console.warn("[ecopages] Hydration error:", err)
|
|
132
|
+
});
|
|
133
|
+
window.__ECO_PAGES__.react.pageRoot = root;
|
|
134
|
+
}
|
|
135
|
+
window.__ECO_PAGES__.hmrHandlers["${importPath}"] = async (newUrl) => {
|
|
60
136
|
try {
|
|
61
137
|
const newModule = await import(newUrl);
|
|
138
|
+
const nextProps = getPageData();
|
|
62
139
|
${getHmrImportStatement(isMdx)}
|
|
63
|
-
|
|
140
|
+
const currentPageLayout = Page.config?.layout;
|
|
141
|
+
const nextPageLayout = NewPage.config?.layout;
|
|
142
|
+
|
|
143
|
+
if (window.__ECO_PAGES__?.navigation?.getOwnerState().owner === "react-router") {
|
|
144
|
+
await window.__ECO_PAGES__?.navigation?.reloadCurrentPage?.({
|
|
145
|
+
clearCache: currentPageLayout !== nextPageLayout,
|
|
146
|
+
moduleUrl: "${importPath}",
|
|
147
|
+
source: "react-router"
|
|
148
|
+
});
|
|
149
|
+
console.log("[ecopages] ${getComponentType(isMdx)} component updated via router");
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
window.__ECO_PAGES__.page = {
|
|
154
|
+
module: pageModuleUrl,
|
|
155
|
+
props: nextProps
|
|
156
|
+
};
|
|
157
|
+
root.render(createTree(NewPage, nextProps));
|
|
64
158
|
console.log("[ecopages] ${getComponentType(isMdx)} component updated");
|
|
65
159
|
} catch (e) {
|
|
66
160
|
console.error("[ecopages] Failed to hot-reload ${getComponentType(isMdx)} component:", e);
|
|
@@ -68,22 +162,36 @@ const mount = () => {
|
|
|
68
162
|
};
|
|
69
163
|
};
|
|
70
164
|
|
|
165
|
+
${getDevRerunRegistrationScript(scriptId)}
|
|
166
|
+
|
|
71
167
|
if (document.readyState === "loading") {
|
|
72
168
|
document.addEventListener("DOMContentLoaded", mount);
|
|
73
169
|
} else {
|
|
74
170
|
mount();
|
|
75
171
|
}
|
|
172
|
+
}
|
|
76
173
|
`.trim();
|
|
77
174
|
}
|
|
78
175
|
function createDevScriptWithoutRouter(options) {
|
|
79
|
-
const { importPath, isMdx, reactImportPath, reactDomClientImportPath } = options;
|
|
176
|
+
const { importPath, isMdx, reactImportPath, reactDomClientImportPath, scriptId } = options;
|
|
177
|
+
const pageModuleUrlExpression = options.pageModuleUrlExpression ?? "import.meta.url";
|
|
80
178
|
return `
|
|
81
179
|
import { hydrateRoot } from "${reactDomClientImportPath}";
|
|
82
180
|
import { createElement } from "${reactImportPath}";
|
|
83
181
|
${getImportStatement(importPath, isMdx)}
|
|
182
|
+
const pageModuleUrl = ${pageModuleUrlExpression};
|
|
183
|
+
export default Page;
|
|
184
|
+
export const config = Page.config;
|
|
185
|
+
const isActivePageEntry = Boolean(document.querySelector('script[data-eco-script-id="${scriptId}"]'));
|
|
186
|
+
|
|
187
|
+
if (isActivePageEntry) {
|
|
84
188
|
|
|
85
|
-
window.
|
|
86
|
-
|
|
189
|
+
window.__ECO_PAGES__ = window.__ECO_PAGES__ || {};
|
|
190
|
+
window.__ECO_PAGES__.hmrHandlers = window.__ECO_PAGES__.hmrHandlers || {};
|
|
191
|
+
window.__ECO_PAGES__.react = window.__ECO_PAGES__.react || {};
|
|
192
|
+
window.__ECO_PAGES__.react.pageRoot = window.__ECO_PAGES__.react.pageRoot || null;
|
|
193
|
+
let root = window.__ECO_PAGES__.react.pageRoot;
|
|
194
|
+
${getDevPageRootCleanupScript()}
|
|
87
195
|
|
|
88
196
|
const getPageData = () => {
|
|
89
197
|
const el = document.getElementById("__ECO_PAGE_DATA__");
|
|
@@ -95,22 +203,35 @@ const getPageData = () => {
|
|
|
95
203
|
|
|
96
204
|
const props = getPageData();
|
|
97
205
|
|
|
98
|
-
window.
|
|
99
|
-
module:
|
|
206
|
+
window.__ECO_PAGES__.page = {
|
|
207
|
+
module: pageModuleUrl,
|
|
100
208
|
props
|
|
101
209
|
};
|
|
102
210
|
|
|
103
211
|
const createTree = (Component, props) => {
|
|
104
212
|
const Layout = Component.config?.layout;
|
|
105
213
|
const pageElement = createElement(Component, props);
|
|
106
|
-
|
|
214
|
+
const layoutProps = props?.locals ? { locals: props.locals } : null;
|
|
215
|
+
return Layout ? createElement(Layout, layoutProps, pageElement) : pageElement;
|
|
107
216
|
};
|
|
108
217
|
|
|
109
218
|
const mount = () => {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
219
|
+
const props = getPageData();
|
|
220
|
+
window.__ECO_PAGES__.page = {
|
|
221
|
+
module: pageModuleUrl,
|
|
222
|
+
props
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
if (window.__ECO_PAGES__.react?.pageRoot) {
|
|
226
|
+
root = window.__ECO_PAGES__.react.pageRoot;
|
|
227
|
+
root.render(createTree(Page, props));
|
|
228
|
+
} else {
|
|
229
|
+
root = hydrateRoot(document.body, createTree(Page, props), {
|
|
230
|
+
onRecoverableError: (err) => console.warn("[ecopages] Hydration error:", err)
|
|
231
|
+
});
|
|
232
|
+
window.__ECO_PAGES__.react.pageRoot = root;
|
|
233
|
+
}
|
|
234
|
+
window.__ECO_PAGES__.hmrHandlers["${importPath}"] = async (newUrl) => {
|
|
114
235
|
try {
|
|
115
236
|
const newModule = await import(newUrl);
|
|
116
237
|
${getHmrImportStatement(isMdx)}
|
|
@@ -122,30 +243,35 @@ const mount = () => {
|
|
|
122
243
|
};
|
|
123
244
|
};
|
|
124
245
|
|
|
246
|
+
${getDevRerunRegistrationScript(scriptId)}
|
|
247
|
+
|
|
125
248
|
if (document.readyState === "loading") {
|
|
126
249
|
document.addEventListener("DOMContentLoaded", mount);
|
|
127
250
|
} else {
|
|
128
251
|
mount();
|
|
129
252
|
}
|
|
253
|
+
}
|
|
130
254
|
`.trim();
|
|
131
255
|
}
|
|
132
256
|
function createProdScriptWithRouter(options) {
|
|
133
|
-
const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath } = options;
|
|
257
|
+
const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath, scriptId } = options;
|
|
258
|
+
const pageModuleUrlExpression = options.pageModuleUrlExpression ?? "import.meta.url";
|
|
134
259
|
const { components, getRouterProps } = router;
|
|
135
260
|
if (!routerImportPath) {
|
|
136
261
|
throw new Error("routerImportPath is required when router adapter is configured");
|
|
137
262
|
}
|
|
138
263
|
if (isMdx) {
|
|
139
|
-
return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import{${components.router} as R,${components.pageContent} as PC}from"${routerImportPath}";import*as M from"${importPath}";const P=M.default;if(M.config)P.config=M.config;const gd=()=>{const e=document.getElementById("__ECO_PAGE_DATA__");if(e?.textContent){try{return JSON.parse(e.textContent)}catch{}}return{}};const
|
|
264
|
+
return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import{${components.router} as R,${components.pageContent} as PC}from"${routerImportPath}";import*as M from"${importPath}";const P=M.default;if(M.config)P.config=M.config;const u=${pageModuleUrlExpression};export default P;export const config=P.config;const a=!!document.querySelector('script[data-eco-script-id="${scriptId}"]');if(a){window.__ECO_PAGES__=window.__ECO_PAGES__||{};window.__ECO_PAGES__.react=window.__ECO_PAGES__.react||{};window.__ECO_PAGES__.react.pageRoot=window.__ECO_PAGES__.react.pageRoot||null;let root=window.__ECO_PAGES__.react.pageRoot;${getProdPageRootCleanupScript()}${getProdRouterBootstrapRegistrationScript()}${getProdReuseExistingRouterRootScript()}const gd=()=>{const e=document.getElementById("__ECO_PAGE_DATA__");if(e?.textContent){try{return JSON.parse(e.textContent)}catch{}}return{}};const ct=(C,p)=>ce(R,${getRouterProps("C", "p")},ce(PC));const m=()=>{const pr=gd();window.__ECO_PAGES__.page={module:u,props:pr};if(sr()){root=window.__ECO_PAGES__.react.pageRoot;return}if(window.__ECO_PAGES__.react?.pageRoot){root=window.__ECO_PAGES__.react.pageRoot;root.render(ct(P,pr));return}root=hr(document.body,ct(P,pr),{onRecoverableError:(e)=>console.warn("[ecopages] Hydration error:",e)});window.__ECO_PAGES__.react.pageRoot=root};${getProdRerunRegistrationScript(scriptId)}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m()}`;
|
|
140
265
|
}
|
|
141
|
-
return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import{${components.router} as R,${components.pageContent} as PC}from"${routerImportPath}";import P from"${importPath}";const gd=()=>{const e=document.getElementById("__ECO_PAGE_DATA__");if(e?.textContent){try{return JSON.parse(e.textContent)}catch{}}return{}};const
|
|
266
|
+
return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import{${components.router} as R,${components.pageContent} as PC}from"${routerImportPath}";import P from"${importPath}";const u=${pageModuleUrlExpression};export default P;export const config=P.config;const a=!!document.querySelector('script[data-eco-script-id="${scriptId}"]');if(a){window.__ECO_PAGES__=window.__ECO_PAGES__||{};window.__ECO_PAGES__.react=window.__ECO_PAGES__.react||{};window.__ECO_PAGES__.react.pageRoot=window.__ECO_PAGES__.react.pageRoot||null;let root=window.__ECO_PAGES__.react.pageRoot;${getProdPageRootCleanupScript()}${getProdRouterBootstrapRegistrationScript()}${getProdReuseExistingRouterRootScript()}const gd=()=>{const e=document.getElementById("__ECO_PAGE_DATA__");if(e?.textContent){try{return JSON.parse(e.textContent)}catch{}}return{}};const ct=(C,p)=>ce(R,${getRouterProps("C", "p")},ce(PC));const m=()=>{const pr=gd();window.__ECO_PAGES__.page={module:u,props:pr};if(sr()){root=window.__ECO_PAGES__.react.pageRoot;return}if(window.__ECO_PAGES__.react?.pageRoot){root=window.__ECO_PAGES__.react.pageRoot;root.render(ct(P,pr));return}root=hr(document.body,ct(P,pr),{onRecoverableError:(e)=>console.warn("[ecopages] Hydration error:",e)});window.__ECO_PAGES__.react.pageRoot=root};${getProdRerunRegistrationScript(scriptId)}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m()}`;
|
|
142
267
|
}
|
|
143
268
|
function createProdScriptWithoutRouter(options) {
|
|
144
|
-
const { importPath, isMdx, reactImportPath, reactDomClientImportPath } = options;
|
|
269
|
+
const { importPath, isMdx, reactImportPath, reactDomClientImportPath, scriptId } = options;
|
|
270
|
+
const pageModuleUrlExpression = options.pageModuleUrlExpression ?? "import.meta.url";
|
|
145
271
|
if (isMdx) {
|
|
146
|
-
return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import*as M from"${importPath}";const P=M.default;if(M.config)P.config=M.config;const gd=()=>{const e=document.getElementById("__ECO_PAGE_DATA__");if(e?.textContent){try{return JSON.parse(e.textContent)}catch{}}return{}};const
|
|
272
|
+
return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import*as M from"${importPath}";const P=M.default;if(M.config)P.config=M.config;const u=${pageModuleUrlExpression};export default P;export const config=P.config;const a=!!document.querySelector('script[data-eco-script-id="${scriptId}"]');if(a){window.__ECO_PAGES__=window.__ECO_PAGES__||{};window.__ECO_PAGES__.react=window.__ECO_PAGES__.react||{};window.__ECO_PAGES__.react.pageRoot=window.__ECO_PAGES__.react.pageRoot||null;let root=window.__ECO_PAGES__.react.pageRoot;${getProdPageRootCleanupScript()}const gd=()=>{const e=document.getElementById("__ECO_PAGE_DATA__");if(e?.textContent){try{return JSON.parse(e.textContent)}catch{}}return{}};const ct=(C,p)=>{const L=C.config?.layout;const pe=ce(C,p);const lp=p?.locals?{locals:p.locals}:null;return L?ce(L,lp,pe):pe};const m=()=>{const pr=gd();window.__ECO_PAGES__.page={module:u,props:pr};if(window.__ECO_PAGES__.react?.pageRoot){root=window.__ECO_PAGES__.react.pageRoot;root.render(ct(P,pr));return}root=hr(document.body,ct(P,pr),{onRecoverableError:(e)=>console.warn("[ecopages] Hydration error:",e)});window.__ECO_PAGES__.react.pageRoot=root};${getProdRerunRegistrationScript(scriptId)}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m()}`;
|
|
147
273
|
}
|
|
148
|
-
return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import P from"${importPath}";const gd=()=>{const e=document.getElementById("__ECO_PAGE_DATA__");if(e?.textContent){try{return JSON.parse(e.textContent)}catch{}}return{}};const
|
|
274
|
+
return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import P from"${importPath}";const u=${pageModuleUrlExpression};export default P;export const config=P.config;const a=!!document.querySelector('script[data-eco-script-id="${scriptId}"]');if(a){window.__ECO_PAGES__=window.__ECO_PAGES__||{};window.__ECO_PAGES__.react=window.__ECO_PAGES__.react||{};window.__ECO_PAGES__.react.pageRoot=window.__ECO_PAGES__.react.pageRoot||null;let root=window.__ECO_PAGES__.react.pageRoot;${getProdPageRootCleanupScript()}const gd=()=>{const e=document.getElementById("__ECO_PAGE_DATA__");if(e?.textContent){try{return JSON.parse(e.textContent)}catch{}}return{}};const ct=(C,p)=>{const L=C.config?.layout;const pe=ce(C,p);const lp=p?.locals?{locals:p.locals}:null;return L?ce(L,lp,pe):pe};const m=()=>{const pr=gd();window.__ECO_PAGES__.page={module:u,props:pr};if(window.__ECO_PAGES__.react?.pageRoot){root=window.__ECO_PAGES__.react.pageRoot;root.render(ct(P,pr));return}root=hr(document.body,ct(P,pr),{onRecoverableError:(e)=>console.warn("[ecopages] Hydration error:",e)});window.__ECO_PAGES__.react.pageRoot=root};${getProdRerunRegistrationScript(scriptId)}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m()}`;
|
|
149
275
|
}
|
|
150
276
|
function createHydrationScript(options) {
|
|
151
277
|
const { isDevelopment, router } = options;
|
|
@@ -156,10 +282,9 @@ function createHydrationScript(options) {
|
|
|
156
282
|
}
|
|
157
283
|
function createIslandHydrationScript(options) {
|
|
158
284
|
const targetSelector = JSON.stringify(options.targetSelector);
|
|
159
|
-
const serializedProps = JSON.stringify(options.props ?? {});
|
|
160
285
|
const componentRef = JSON.stringify(options.componentRef ?? "");
|
|
161
286
|
const componentFile = JSON.stringify(options.componentFile ?? "");
|
|
162
|
-
const
|
|
287
|
+
const scriptId = options.scriptId;
|
|
163
288
|
if (options.isDevelopment) {
|
|
164
289
|
return `
|
|
165
290
|
import { createRoot } from "${options.reactDomClientImportPath}";
|
|
@@ -195,18 +320,26 @@ const resolveComponent = () => {
|
|
|
195
320
|
};
|
|
196
321
|
|
|
197
322
|
const mount = () => {
|
|
198
|
-
const
|
|
323
|
+
const targets = document.querySelectorAll(${targetSelector});
|
|
199
324
|
const Component = resolveComponent();
|
|
200
|
-
if (!
|
|
325
|
+
if (!Component || targets.length === 0) {
|
|
201
326
|
return;
|
|
202
327
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
328
|
+
targets.forEach((target) => {
|
|
329
|
+
if (!(target instanceof HTMLElement)) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const props = JSON.parse(atob(target.getAttribute("data-eco-props") || "e30="));
|
|
333
|
+
const container = document.createElement("eco-island");
|
|
334
|
+
container.style.display = "block";
|
|
335
|
+
target.replaceWith(container);
|
|
336
|
+
const root = createRoot(container);
|
|
337
|
+
root.render(createElement(Component, props));
|
|
338
|
+
});
|
|
207
339
|
};
|
|
208
340
|
|
|
209
|
-
|
|
341
|
+
${getDevRerunRegistrationScript(scriptId)}
|
|
342
|
+
|
|
210
343
|
if (document.readyState === "loading") {
|
|
211
344
|
document.addEventListener("DOMContentLoaded", mount, { once: true });
|
|
212
345
|
} else {
|
|
@@ -214,7 +347,7 @@ if (document.readyState === "loading") {
|
|
|
214
347
|
}
|
|
215
348
|
`.trim();
|
|
216
349
|
}
|
|
217
|
-
return `import{createRoot as cr}from"${options.reactDomClientImportPath}";import{createElement as ce}from"${options.reactImportPath}";import*as M from"${options.importPath}";const r=${componentRef};const f=${componentFile};const mv=Object.values(M);const c=mv.find((e)=>{if(typeof e!=="function")return false;const ec=e.config?.__eco;if(!ec)return false;if(r&&ec.id===r)return true;if(f&&ec.file===f)return true;return false;})??(typeof M.default==="function"?M.default:mv.find((e)=>typeof e==="function")??null);const m=()=>{const
|
|
350
|
+
return `import{createRoot as cr}from"${options.reactDomClientImportPath}";import{createElement as ce}from"${options.reactImportPath}";import*as M from"${options.importPath}";const r=${componentRef};const f=${componentFile};const mv=Object.values(M);const c=mv.find((e)=>{if(typeof e!=="function")return false;const ec=e.config?.__eco;if(!ec)return false;if(r&&ec.id===r)return true;if(f&&ec.file===f)return true;return false;})??(typeof M.default==="function"?M.default:mv.find((e)=>typeof e==="function")??null);const m=()=>{const ts=document.querySelectorAll(${targetSelector});if(!c||ts.length===0)return;ts.forEach((t)=>{if(!(t instanceof HTMLElement))return;const p=JSON.parse(atob(t.getAttribute("data-eco-props")||"e30="));const ct=document.createElement("eco-island");ct.style.display="block";t.replaceWith(ct);cr(ct).render(ce(c,p))})};${getProdRerunRegistrationScript(scriptId)}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m,{once:true}):m()`;
|
|
218
351
|
}
|
|
219
352
|
export {
|
|
220
353
|
createHydrationScript,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { createHydrationScript } from "./hydration-scripts.js";
|
|
3
|
+
const routerAdapter = {
|
|
4
|
+
name: "eco-router",
|
|
5
|
+
bundle: {
|
|
6
|
+
importPath: "/assets/router.js",
|
|
7
|
+
outputName: "router",
|
|
8
|
+
externals: []
|
|
9
|
+
},
|
|
10
|
+
components: {
|
|
11
|
+
router: "EcoRouter",
|
|
12
|
+
pageContent: "PageContent"
|
|
13
|
+
},
|
|
14
|
+
getRouterProps: (page, props) => `{ page: ${page}, pageProps: ${props} }`
|
|
15
|
+
};
|
|
16
|
+
function createModuleUrl(source) {
|
|
17
|
+
return `data:text/javascript;base64,${btoa(source)}`;
|
|
18
|
+
}
|
|
19
|
+
async function importModule(moduleUrl, scriptId) {
|
|
20
|
+
let marker;
|
|
21
|
+
if (scriptId) {
|
|
22
|
+
marker = document.createElement("script");
|
|
23
|
+
marker.setAttribute("data-eco-script-id", scriptId);
|
|
24
|
+
document.head.appendChild(marker);
|
|
25
|
+
}
|
|
26
|
+
await import(
|
|
27
|
+
/* @vite-ignore */
|
|
28
|
+
moduleUrl
|
|
29
|
+
);
|
|
30
|
+
marker?.remove();
|
|
31
|
+
}
|
|
32
|
+
function createRuntimeModules() {
|
|
33
|
+
const reactImportPath = createModuleUrl("export const createElement = (...args) => ({ args });");
|
|
34
|
+
const reactDomClientImportPath = createModuleUrl(`
|
|
35
|
+
export const hydrateRoot = (container, tree, options) => {
|
|
36
|
+
const runtime = window.__ECO_REACT_HYDRATION_TEST__;
|
|
37
|
+
runtime.hydrateCalls.push({
|
|
38
|
+
containerTag: container.tagName,
|
|
39
|
+
hasRecoverableErrorHandler: typeof options?.onRecoverableError === "function",
|
|
40
|
+
tree,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
render() {},
|
|
45
|
+
unmount() {
|
|
46
|
+
runtime.unmountCount += 1;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
`);
|
|
51
|
+
const importPath = createModuleUrl("export default function Page() { return null; }");
|
|
52
|
+
const routerImportPath = createModuleUrl(`
|
|
53
|
+
export function EcoRouter(props) {
|
|
54
|
+
return props;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function PageContent() {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
`);
|
|
61
|
+
return {
|
|
62
|
+
importPath,
|
|
63
|
+
reactImportPath,
|
|
64
|
+
reactDomClientImportPath,
|
|
65
|
+
routerImportPath
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
describe("createHydrationScript browser execution", () => {
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
document.body.innerHTML = "";
|
|
71
|
+
const testWindow = window;
|
|
72
|
+
delete testWindow.__ECO_PAGES__;
|
|
73
|
+
delete testWindow.__ECO_REACT_HYDRATION_TEST__;
|
|
74
|
+
});
|
|
75
|
+
it("registers router ownership and cleanup when the browser hydration bootstrap runs", async () => {
|
|
76
|
+
const runtimeModules = createRuntimeModules();
|
|
77
|
+
const testWindow = window;
|
|
78
|
+
testWindow.__ECO_REACT_HYDRATION_TEST__ = {
|
|
79
|
+
hydrateCalls: [],
|
|
80
|
+
renderCalls: [],
|
|
81
|
+
claimedOwners: [],
|
|
82
|
+
releasedOwners: [],
|
|
83
|
+
registrations: [],
|
|
84
|
+
unmountCount: 0
|
|
85
|
+
};
|
|
86
|
+
testWindow.__ECO_PAGES__ = {
|
|
87
|
+
navigation: {
|
|
88
|
+
getOwnerState: () => ({
|
|
89
|
+
owner: "html",
|
|
90
|
+
canHandleSpaNavigation: false
|
|
91
|
+
}),
|
|
92
|
+
register: (registration) => {
|
|
93
|
+
testWindow.__ECO_REACT_HYDRATION_TEST__?.registrations.push(registration);
|
|
94
|
+
},
|
|
95
|
+
claimOwnership: (owner) => {
|
|
96
|
+
testWindow.__ECO_REACT_HYDRATION_TEST__?.claimedOwners.push(owner);
|
|
97
|
+
},
|
|
98
|
+
releaseOwnership: (owner) => {
|
|
99
|
+
testWindow.__ECO_REACT_HYDRATION_TEST__?.releasedOwners.push(owner);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
document.body.innerHTML = `<script id="__ECO_PAGE_DATA__" type="application/json">${JSON.stringify({
|
|
104
|
+
title: "Hello React",
|
|
105
|
+
locals: { theme: "dark" }
|
|
106
|
+
})}<\/script>`;
|
|
107
|
+
const script = createHydrationScript({
|
|
108
|
+
...runtimeModules,
|
|
109
|
+
scriptId: "ecopages-react-page",
|
|
110
|
+
isDevelopment: true,
|
|
111
|
+
isMdx: false,
|
|
112
|
+
router: routerAdapter
|
|
113
|
+
});
|
|
114
|
+
const moduleUrl = createModuleUrl(script);
|
|
115
|
+
await importModule(moduleUrl, "ecopages-react-page");
|
|
116
|
+
expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.hydrateCalls).toHaveLength(1);
|
|
117
|
+
expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.hydrateCalls[0]?.containerTag).toBe("BODY");
|
|
118
|
+
expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.hydrateCalls[0]?.hasRecoverableErrorHandler).toBe(true);
|
|
119
|
+
expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.claimedOwners).toEqual(["react-router"]);
|
|
120
|
+
expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.registrations).toHaveLength(1);
|
|
121
|
+
expect(typeof testWindow.__ECO_PAGES__?.react?.cleanupPageRoot).toBe("function");
|
|
122
|
+
expect(testWindow.__ECO_PAGES__?.page).toEqual({
|
|
123
|
+
module: moduleUrl,
|
|
124
|
+
props: {
|
|
125
|
+
title: "Hello React",
|
|
126
|
+
locals: { theme: "dark" }
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
await testWindow.__ECO_PAGES__?.react?.cleanupPageRoot?.();
|
|
130
|
+
expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.unmountCount).toBe(1);
|
|
131
|
+
expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.releasedOwners).toEqual(["react-router"]);
|
|
132
|
+
expect(testWindow.__ECO_PAGES__?.page).toBeUndefined();
|
|
133
|
+
expect(testWindow.__ECO_PAGES__?.react?.pageRoot).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
it("reuses an existing router-owned page root during rerun bootstrap execution", async () => {
|
|
136
|
+
const runtimeModules = createRuntimeModules();
|
|
137
|
+
const testWindow = window;
|
|
138
|
+
testWindow.__ECO_REACT_HYDRATION_TEST__ = {
|
|
139
|
+
hydrateCalls: [],
|
|
140
|
+
renderCalls: [],
|
|
141
|
+
claimedOwners: [],
|
|
142
|
+
releasedOwners: [],
|
|
143
|
+
registrations: [],
|
|
144
|
+
unmountCount: 0
|
|
145
|
+
};
|
|
146
|
+
const existingRoot = {
|
|
147
|
+
render: (tree) => {
|
|
148
|
+
testWindow.__ECO_REACT_HYDRATION_TEST__?.renderCalls.push(tree);
|
|
149
|
+
},
|
|
150
|
+
unmount: () => {
|
|
151
|
+
testWindow.__ECO_REACT_HYDRATION_TEST__.unmountCount += 1;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
testWindow.__ECO_PAGES__ = {
|
|
155
|
+
navigation: {
|
|
156
|
+
getOwnerState: () => ({
|
|
157
|
+
owner: "react-router",
|
|
158
|
+
canHandleSpaNavigation: true
|
|
159
|
+
}),
|
|
160
|
+
register: (registration) => {
|
|
161
|
+
testWindow.__ECO_REACT_HYDRATION_TEST__?.registrations.push(registration);
|
|
162
|
+
},
|
|
163
|
+
claimOwnership: (owner) => {
|
|
164
|
+
testWindow.__ECO_REACT_HYDRATION_TEST__?.claimedOwners.push(owner);
|
|
165
|
+
},
|
|
166
|
+
releaseOwnership: (owner) => {
|
|
167
|
+
testWindow.__ECO_REACT_HYDRATION_TEST__?.releasedOwners.push(owner);
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
react: {
|
|
171
|
+
pageRoot: existingRoot
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
document.body.innerHTML = `<script id="__ECO_PAGE_DATA__" type="application/json">${JSON.stringify({
|
|
175
|
+
title: "Rerun"
|
|
176
|
+
})}<\/script>`;
|
|
177
|
+
const script = createHydrationScript({
|
|
178
|
+
...runtimeModules,
|
|
179
|
+
scriptId: "ecopages-react-page-rerun",
|
|
180
|
+
isDevelopment: true,
|
|
181
|
+
isMdx: false,
|
|
182
|
+
router: routerAdapter
|
|
183
|
+
});
|
|
184
|
+
const moduleUrl = createModuleUrl(script);
|
|
185
|
+
await importModule(moduleUrl, "ecopages-react-page-rerun");
|
|
186
|
+
expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.hydrateCalls).toHaveLength(0);
|
|
187
|
+
expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.renderCalls).toHaveLength(0);
|
|
188
|
+
expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.claimedOwners).toHaveLength(0);
|
|
189
|
+
expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.registrations).toHaveLength(0);
|
|
190
|
+
expect(testWindow.__ECO_PAGES__?.react?.pageRoot).toBe(existingRoot);
|
|
191
|
+
expect(testWindow.__ECO_PAGES__?.page).toEqual({
|
|
192
|
+
module: moduleUrl,
|
|
193
|
+
props: {
|
|
194
|
+
title: "Rerun"
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
function createReactDomRuntimeInteropPlugin(options) {
|
|
4
|
+
const reactDomFileFilter = /[\\/]react-dom[\\/].*\.js$/;
|
|
5
|
+
const reactRequirePattern = /\brequire\((['"])react\1\)/g;
|
|
6
|
+
const reactSpecifier = options?.reactSpecifier ?? "react";
|
|
7
|
+
return {
|
|
8
|
+
name: options?.name ?? "react-dom-runtime-interop",
|
|
9
|
+
setup(build) {
|
|
10
|
+
build.onLoad({ filter: reactDomFileFilter }, (args) => {
|
|
11
|
+
const content = fs.readFileSync(args.path, "utf-8");
|
|
12
|
+
if (!reactRequirePattern.test(content)) {
|
|
13
|
+
return void 0;
|
|
14
|
+
}
|
|
15
|
+
reactRequirePattern.lastIndex = 0;
|
|
16
|
+
const rewritten = content.replace(reactRequirePattern, "__ecopages_react_runtime");
|
|
17
|
+
return {
|
|
18
|
+
contents: `import * as __ecopages_react_runtime from '${reactSpecifier}';
|
|
19
|
+
${rewritten}`,
|
|
20
|
+
loader: "js",
|
|
21
|
+
resolveDir: path.dirname(args.path)
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export {
|
|
28
|
+
createReactDomRuntimeInteropPlugin
|
|
29
|
+
};
|