@ecopages/react 0.2.0-alpha.4 → 0.2.0-alpha.41
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 +161 -18
- 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 +42 -32
- package/src/react-hmr-strategy.js +103 -124
- 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 +15 -26
- package/src/services/react-bundle.service.js +45 -93
- 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 +26 -19
- package/src/services/react-hydration-asset.service.js +72 -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 +149 -11
- 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 +25 -6
- package/src/utils/hydration-scripts.js +150 -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/reachability-analyzer.d.ts +12 -1
- package/src/utils/reachability-analyzer.js +101 -5
- 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 -62
- package/src/react-hmr-strategy.ts +0 -444
- 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 -212
- 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 -590
- 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 -440
- package/src/utils/react-mdx-loader-plugin.ts +0 -40
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectFromConfigTree } from "./component-config-traversal.js";
|
|
1
2
|
function parseDeclaredModuleSource(value) {
|
|
2
3
|
const source = value.trim();
|
|
3
4
|
if (source.length === 0) return void 0;
|
|
@@ -16,21 +17,8 @@ function normalizeDeclaredModuleSources(modules) {
|
|
|
16
17
|
}
|
|
17
18
|
return Array.from(seen);
|
|
18
19
|
}
|
|
19
|
-
function collectDeclaredModulesInConfig(config
|
|
20
|
-
|
|
21
|
-
return [];
|
|
22
|
-
}
|
|
23
|
-
visited.add(config);
|
|
24
|
-
const declarations = normalizeDeclaredModuleSources(config.dependencies?.modules);
|
|
25
|
-
if (config.layout?.config) {
|
|
26
|
-
declarations.push(...collectDeclaredModulesInConfig(config.layout.config, visited));
|
|
27
|
-
}
|
|
28
|
-
for (const component of config.dependencies?.components ?? []) {
|
|
29
|
-
if (component.config) {
|
|
30
|
-
declarations.push(...collectDeclaredModulesInConfig(component.config, visited));
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return declarations;
|
|
20
|
+
function collectDeclaredModulesInConfig(config) {
|
|
21
|
+
return collectFromConfigTree(config, (node) => normalizeDeclaredModuleSources(node.dependencies?.modules));
|
|
34
22
|
}
|
|
35
23
|
function collectPageDeclaredModulesFromModule(pageModule) {
|
|
36
24
|
const declarations = [
|
|
@@ -41,7 +29,10 @@ function collectPageDeclaredModulesFromModule(pageModule) {
|
|
|
41
29
|
}
|
|
42
30
|
async function collectPageDeclaredModules(pagePath) {
|
|
43
31
|
try {
|
|
44
|
-
const pageModule = await import(
|
|
32
|
+
const pageModule = await import(
|
|
33
|
+
/* @vite-ignore */
|
|
34
|
+
pagePath
|
|
35
|
+
);
|
|
45
36
|
return collectPageDeclaredModulesFromModule(pageModule);
|
|
46
37
|
} catch {
|
|
47
38
|
return [];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Suspense } from "react";
|
|
3
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
4
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
5
|
+
import { dynamic } from "./dynamic.js";
|
|
6
|
+
function createDeferredImport() {
|
|
7
|
+
let resolve;
|
|
8
|
+
const promise = new Promise((innerResolve) => {
|
|
9
|
+
resolve = innerResolve;
|
|
10
|
+
});
|
|
11
|
+
return {
|
|
12
|
+
promise,
|
|
13
|
+
resolve
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
describe("dynamic", () => {
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
cleanup();
|
|
19
|
+
});
|
|
20
|
+
it("returns a browser lazy component that resolves through Suspense", async () => {
|
|
21
|
+
const deferredImport = createDeferredImport();
|
|
22
|
+
const DynamicComponent = dynamic(() => deferredImport.promise);
|
|
23
|
+
render(
|
|
24
|
+
/* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx("span", { children: "Loading dynamic component" }), children: /* @__PURE__ */ jsx(DynamicComponent, {}) })
|
|
25
|
+
);
|
|
26
|
+
expect(screen.getByText("Loading dynamic component")).toBeTruthy();
|
|
27
|
+
deferredImport.resolve({
|
|
28
|
+
default: () => /* @__PURE__ */ jsx("span", { children: "Dynamic content" })
|
|
29
|
+
});
|
|
30
|
+
expect(await screen.findByText("Dynamic content")).toBeTruthy();
|
|
31
|
+
expect(screen.queryByText("Loading dynamic component")).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -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 */
|
|
@@ -30,10 +34,8 @@ export type IslandHydrationScriptOptions = {
|
|
|
30
34
|
reactImportPath: string;
|
|
31
35
|
/** Browser import path for react-dom/client runtime. */
|
|
32
36
|
reactDomClientImportPath: string;
|
|
33
|
-
/** Selector that resolves to
|
|
37
|
+
/** Selector that resolves to all SSR root elements for this island component. */
|
|
34
38
|
targetSelector: string;
|
|
35
|
-
/** Serialized component props emitted at render time. */
|
|
36
|
-
props: Record<string, unknown>;
|
|
37
39
|
/** Optional stable component id used to resolve named exports reliably. */
|
|
38
40
|
componentRef?: string;
|
|
39
41
|
/** Optional source file hint used as fallback for component resolution. */
|
|
@@ -43,7 +45,18 @@ export type IslandHydrationScriptOptions = {
|
|
|
43
45
|
};
|
|
44
46
|
/**
|
|
45
47
|
* Creates a hydration script for client-side React hydration.
|
|
46
|
-
*
|
|
48
|
+
*
|
|
49
|
+
* Why this dispatcher exists:
|
|
50
|
+
* the runtime matrix is small but behaviorally different across development vs
|
|
51
|
+
* production and router vs non-router pages. Keeping that branch here preserves
|
|
52
|
+
* a compact public API while allowing each emitted script to stay focused.
|
|
53
|
+
*
|
|
54
|
+
* Selection rules:
|
|
55
|
+
* - development uses readable scripts with HMR hooks
|
|
56
|
+
* - production uses minified equivalents
|
|
57
|
+
* - router presence decides whether page updates flow through the router runtime
|
|
58
|
+
* or rebuild directly from the page module
|
|
59
|
+
*
|
|
47
60
|
* @param options - Configuration options for script generation
|
|
48
61
|
* @returns The generated hydration script as a string
|
|
49
62
|
*/
|
|
@@ -63,8 +76,14 @@ export declare function createHydrationScript(options: HydrationScriptOptions):
|
|
|
63
76
|
* - resolves the component export by metadata (`componentRef`, `componentFile`)
|
|
64
77
|
* before falling back to default/first function export
|
|
65
78
|
* - selects island root using `targetSelector`
|
|
79
|
+
* - replaces the SSR host with a dedicated client-owned container
|
|
66
80
|
* - creates a fresh React root and renders with serialized `props`
|
|
67
81
|
*
|
|
82
|
+
* Why it remounts instead of hydrating:
|
|
83
|
+
* island SSR intentionally avoids synthetic wrapper elements. The runtime swaps
|
|
84
|
+
* the authored SSR node for a dedicated client-owned container before mounting
|
|
85
|
+
* so the server markup stays clean while the client still gets a stable root.
|
|
86
|
+
*
|
|
68
87
|
* @param options Island script generation options.
|
|
69
88
|
* @returns Browser-executable JavaScript module source.
|
|
70
89
|
*/
|
|
@@ -9,8 +9,58 @@ 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
|
+
}
|
|
12
61
|
function createDevScriptWithRouter(options) {
|
|
13
|
-
const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath } = options;
|
|
62
|
+
const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath, scriptId } = options;
|
|
63
|
+
const pageModuleUrlExpression = options.pageModuleUrlExpression ?? "import.meta.url";
|
|
14
64
|
const { components, getRouterProps } = router;
|
|
15
65
|
if (!routerImportPath) {
|
|
16
66
|
throw new Error("routerImportPath is required when router adapter is configured");
|
|
@@ -20,11 +70,21 @@ import { hydrateRoot } from "${reactDomClientImportPath}";
|
|
|
20
70
|
import { createElement } from "${reactImportPath}";
|
|
21
71
|
import { ${components.router}, ${components.pageContent} } from "${routerImportPath}";
|
|
22
72
|
${getImportStatement(importPath, isMdx)}
|
|
73
|
+
const pageModuleUrl = ${pageModuleUrlExpression};
|
|
74
|
+
export default Page;
|
|
75
|
+
export const config = Page.config;
|
|
76
|
+
const isActivePageEntry = Boolean(document.querySelector('script[data-eco-script-id="${scriptId}"]'));
|
|
77
|
+
|
|
78
|
+
if (isActivePageEntry) {
|
|
23
79
|
|
|
24
|
-
window.
|
|
25
|
-
window.
|
|
26
|
-
window.
|
|
27
|
-
|
|
80
|
+
window.__ECO_PAGES__ = window.__ECO_PAGES__ || {};
|
|
81
|
+
window.__ECO_PAGES__.hmrHandlers = window.__ECO_PAGES__.hmrHandlers || {};
|
|
82
|
+
window.__ECO_PAGES__.react = window.__ECO_PAGES__.react || {};
|
|
83
|
+
window.__ECO_PAGES__.react.pageRoot = window.__ECO_PAGES__.react.pageRoot || null;
|
|
84
|
+
let root = window.__ECO_PAGES__.react.pageRoot;
|
|
85
|
+
${getDevPageRootCleanupScript()}
|
|
86
|
+
${getDevRouterBootstrapRegistrationScript()}
|
|
87
|
+
${getDevReuseExistingRouterRootScript()}
|
|
28
88
|
|
|
29
89
|
const getPageData = () => {
|
|
30
90
|
const el = document.getElementById("__ECO_PAGE_DATA__");
|
|
@@ -36,8 +96,8 @@ const getPageData = () => {
|
|
|
36
96
|
|
|
37
97
|
const props = getPageData();
|
|
38
98
|
|
|
39
|
-
window.
|
|
40
|
-
module:
|
|
99
|
+
window.__ECO_PAGES__.page = {
|
|
100
|
+
module: pageModuleUrl,
|
|
41
101
|
props
|
|
42
102
|
};
|
|
43
103
|
|
|
@@ -47,20 +107,40 @@ const createTree = (Component, props) => {
|
|
|
47
107
|
};
|
|
48
108
|
|
|
49
109
|
const mount = () => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
console.
|
|
58
|
-
|
|
59
|
-
|
|
110
|
+
if (shouldReuseExistingRouterRoot()) {
|
|
111
|
+
root = window.__ECO_PAGES__.react.pageRoot;
|
|
112
|
+
} else if (window.__ECO_PAGES__.react?.pageRoot) {
|
|
113
|
+
root = window.__ECO_PAGES__.react.pageRoot;
|
|
114
|
+
root.render(createTree(Page, props));
|
|
115
|
+
} else {
|
|
116
|
+
root = hydrateRoot(document.body, createTree(Page, props), {
|
|
117
|
+
onRecoverableError: (err) => console.warn("[ecopages] Hydration error:", err)
|
|
118
|
+
});
|
|
119
|
+
window.__ECO_PAGES__.react.pageRoot = root;
|
|
120
|
+
}
|
|
121
|
+
window.__ECO_PAGES__.hmrHandlers["${importPath}"] = async (newUrl) => {
|
|
60
122
|
try {
|
|
61
123
|
const newModule = await import(newUrl);
|
|
124
|
+
const nextProps = getPageData();
|
|
62
125
|
${getHmrImportStatement(isMdx)}
|
|
63
|
-
|
|
126
|
+
const currentPageLayout = Page.config?.layout;
|
|
127
|
+
const nextPageLayout = NewPage.config?.layout;
|
|
128
|
+
|
|
129
|
+
if (window.__ECO_PAGES__?.navigation?.getOwnerState().owner === "react-router") {
|
|
130
|
+
await window.__ECO_PAGES__?.navigation?.reloadCurrentPage?.({
|
|
131
|
+
clearCache: currentPageLayout !== nextPageLayout,
|
|
132
|
+
moduleUrl: "${importPath}",
|
|
133
|
+
source: "react-router"
|
|
134
|
+
});
|
|
135
|
+
console.log("[ecopages] ${getComponentType(isMdx)} component updated via router");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
window.__ECO_PAGES__.page = {
|
|
140
|
+
module: pageModuleUrl,
|
|
141
|
+
props: nextProps
|
|
142
|
+
};
|
|
143
|
+
root.render(createTree(NewPage, nextProps));
|
|
64
144
|
console.log("[ecopages] ${getComponentType(isMdx)} component updated");
|
|
65
145
|
} catch (e) {
|
|
66
146
|
console.error("[ecopages] Failed to hot-reload ${getComponentType(isMdx)} component:", e);
|
|
@@ -73,17 +153,29 @@ if (document.readyState === "loading") {
|
|
|
73
153
|
} else {
|
|
74
154
|
mount();
|
|
75
155
|
}
|
|
156
|
+
}
|
|
76
157
|
`.trim();
|
|
77
158
|
}
|
|
78
159
|
function createDevScriptWithoutRouter(options) {
|
|
79
|
-
const { importPath, isMdx, reactImportPath, reactDomClientImportPath } = options;
|
|
160
|
+
const { importPath, isMdx, reactImportPath, reactDomClientImportPath, scriptId } = options;
|
|
161
|
+
const pageModuleUrlExpression = options.pageModuleUrlExpression ?? "import.meta.url";
|
|
80
162
|
return `
|
|
81
163
|
import { hydrateRoot } from "${reactDomClientImportPath}";
|
|
82
164
|
import { createElement } from "${reactImportPath}";
|
|
83
165
|
${getImportStatement(importPath, isMdx)}
|
|
166
|
+
const pageModuleUrl = ${pageModuleUrlExpression};
|
|
167
|
+
export default Page;
|
|
168
|
+
export const config = Page.config;
|
|
169
|
+
const isActivePageEntry = Boolean(document.querySelector('script[data-eco-script-id="${scriptId}"]'));
|
|
170
|
+
|
|
171
|
+
if (isActivePageEntry) {
|
|
84
172
|
|
|
85
|
-
window.
|
|
86
|
-
|
|
173
|
+
window.__ECO_PAGES__ = window.__ECO_PAGES__ || {};
|
|
174
|
+
window.__ECO_PAGES__.hmrHandlers = window.__ECO_PAGES__.hmrHandlers || {};
|
|
175
|
+
window.__ECO_PAGES__.react = window.__ECO_PAGES__.react || {};
|
|
176
|
+
window.__ECO_PAGES__.react.pageRoot = window.__ECO_PAGES__.react.pageRoot || null;
|
|
177
|
+
let root = window.__ECO_PAGES__.react.pageRoot;
|
|
178
|
+
${getDevPageRootCleanupScript()}
|
|
87
179
|
|
|
88
180
|
const getPageData = () => {
|
|
89
181
|
const el = document.getElementById("__ECO_PAGE_DATA__");
|
|
@@ -95,22 +187,29 @@ const getPageData = () => {
|
|
|
95
187
|
|
|
96
188
|
const props = getPageData();
|
|
97
189
|
|
|
98
|
-
window.
|
|
99
|
-
module:
|
|
190
|
+
window.__ECO_PAGES__.page = {
|
|
191
|
+
module: pageModuleUrl,
|
|
100
192
|
props
|
|
101
193
|
};
|
|
102
194
|
|
|
103
195
|
const createTree = (Component, props) => {
|
|
104
196
|
const Layout = Component.config?.layout;
|
|
105
197
|
const pageElement = createElement(Component, props);
|
|
106
|
-
|
|
198
|
+
const layoutProps = props?.locals ? { locals: props.locals } : null;
|
|
199
|
+
return Layout ? createElement(Layout, layoutProps, pageElement) : pageElement;
|
|
107
200
|
};
|
|
108
201
|
|
|
109
202
|
const mount = () => {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
203
|
+
if (window.__ECO_PAGES__.react?.pageRoot) {
|
|
204
|
+
root = window.__ECO_PAGES__.react.pageRoot;
|
|
205
|
+
root.render(createTree(Page, props));
|
|
206
|
+
} else {
|
|
207
|
+
root = hydrateRoot(document.body, createTree(Page, props), {
|
|
208
|
+
onRecoverableError: (err) => console.warn("[ecopages] Hydration error:", err)
|
|
209
|
+
});
|
|
210
|
+
window.__ECO_PAGES__.react.pageRoot = root;
|
|
211
|
+
}
|
|
212
|
+
window.__ECO_PAGES__.hmrHandlers["${importPath}"] = async (newUrl) => {
|
|
114
213
|
try {
|
|
115
214
|
const newModule = await import(newUrl);
|
|
116
215
|
${getHmrImportStatement(isMdx)}
|
|
@@ -127,25 +226,28 @@ if (document.readyState === "loading") {
|
|
|
127
226
|
} else {
|
|
128
227
|
mount();
|
|
129
228
|
}
|
|
229
|
+
}
|
|
130
230
|
`.trim();
|
|
131
231
|
}
|
|
132
232
|
function createProdScriptWithRouter(options) {
|
|
133
|
-
const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath } = options;
|
|
233
|
+
const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath, scriptId } = options;
|
|
234
|
+
const pageModuleUrlExpression = options.pageModuleUrlExpression ?? "import.meta.url";
|
|
134
235
|
const { components, getRouterProps } = router;
|
|
135
236
|
if (!routerImportPath) {
|
|
136
237
|
throw new Error("routerImportPath is required when router adapter is configured");
|
|
137
238
|
}
|
|
138
239
|
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 pr=gd();window.
|
|
240
|
+
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 pr=gd();window.__ECO_PAGES__.page={module:u,props:pr};const ct=(C,p)=>ce(R,${getRouterProps("C", "p")},ce(PC));const m=()=>{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};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m()}`;
|
|
140
241
|
}
|
|
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 pr=gd();window.
|
|
242
|
+
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 pr=gd();window.__ECO_PAGES__.page={module:u,props:pr};const ct=(C,p)=>ce(R,${getRouterProps("C", "p")},ce(PC));const m=()=>{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};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m()}`;
|
|
142
243
|
}
|
|
143
244
|
function createProdScriptWithoutRouter(options) {
|
|
144
|
-
const { importPath, isMdx, reactImportPath, reactDomClientImportPath } = options;
|
|
245
|
+
const { importPath, isMdx, reactImportPath, reactDomClientImportPath, scriptId } = options;
|
|
246
|
+
const pageModuleUrlExpression = options.pageModuleUrlExpression ?? "import.meta.url";
|
|
145
247
|
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 pr=gd();window.
|
|
248
|
+
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 pr=gd();window.__ECO_PAGES__.page={module:u,props:pr};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=()=>{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};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m()}`;
|
|
147
249
|
}
|
|
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 pr=gd();window.
|
|
250
|
+
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 pr=gd();window.__ECO_PAGES__.page={module:u,props:pr};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=()=>{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};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m()}`;
|
|
149
251
|
}
|
|
150
252
|
function createHydrationScript(options) {
|
|
151
253
|
const { isDevelopment, router } = options;
|
|
@@ -156,10 +258,8 @@ function createHydrationScript(options) {
|
|
|
156
258
|
}
|
|
157
259
|
function createIslandHydrationScript(options) {
|
|
158
260
|
const targetSelector = JSON.stringify(options.targetSelector);
|
|
159
|
-
const serializedProps = JSON.stringify(options.props ?? {});
|
|
160
261
|
const componentRef = JSON.stringify(options.componentRef ?? "");
|
|
161
262
|
const componentFile = JSON.stringify(options.componentFile ?? "");
|
|
162
|
-
const mountedAttribute = "data-eco-react-mounted";
|
|
163
263
|
if (options.isDevelopment) {
|
|
164
264
|
return `
|
|
165
265
|
import { createRoot } from "${options.reactDomClientImportPath}";
|
|
@@ -195,18 +295,24 @@ const resolveComponent = () => {
|
|
|
195
295
|
};
|
|
196
296
|
|
|
197
297
|
const mount = () => {
|
|
198
|
-
const
|
|
298
|
+
const targets = document.querySelectorAll(${targetSelector});
|
|
199
299
|
const Component = resolveComponent();
|
|
200
|
-
if (!
|
|
300
|
+
if (!Component || targets.length === 0) {
|
|
201
301
|
return;
|
|
202
302
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
303
|
+
targets.forEach((target) => {
|
|
304
|
+
if (!(target instanceof HTMLElement)) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const props = JSON.parse(atob(target.getAttribute("data-eco-props") || "e30="));
|
|
308
|
+
const container = document.createElement("eco-island");
|
|
309
|
+
container.style.display = "block";
|
|
310
|
+
target.replaceWith(container);
|
|
311
|
+
const root = createRoot(container);
|
|
312
|
+
root.render(createElement(Component, props));
|
|
313
|
+
});
|
|
207
314
|
};
|
|
208
315
|
|
|
209
|
-
document.addEventListener("eco:after-swap", mount);
|
|
210
316
|
if (document.readyState === "loading") {
|
|
211
317
|
document.addEventListener("DOMContentLoaded", mount, { once: true });
|
|
212
318
|
} else {
|
|
@@ -214,7 +320,7 @@ if (document.readyState === "loading") {
|
|
|
214
320
|
}
|
|
215
321
|
`.trim();
|
|
216
322
|
}
|
|
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
|
|
323
|
+
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))})};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m,{once:true}):m()`;
|
|
218
324
|
}
|
|
219
325
|
export {
|
|
220
326
|
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
|
+
});
|