@ecopages/react 0.2.0-alpha.23 → 0.2.0-alpha.25
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/CHANGELOG.md +5 -0
- package/package.json +3 -3
- package/src/react-renderer.d.ts +1 -0
- package/src/react-renderer.js +7 -5
- package/src/services/react-bundle.service.d.ts +11 -1
- package/src/services/react-bundle.service.js +11 -7
- package/src/services/react-hydration-asset.service.d.ts +8 -10
- package/src/services/react-hydration-asset.service.js +21 -46
- package/src/services/react-page-module.service.d.ts +3 -1
- package/src/utils/hydration-scripts.d.ts +4 -2
- package/src/utils/hydration-scripts.js +56 -17
- package/src/utils/hydration-scripts.test.browser.js +76 -3
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ All notable changes to `@ecopages/react` are documented here.
|
|
|
8
8
|
|
|
9
9
|
### Bug Fixes
|
|
10
10
|
|
|
11
|
+
- Fixed router-managed React HMR page entries to reload the active route with a cleared persisted-layout cache so shared layout edits apply while the current page stays mounted.
|
|
12
|
+
- Fixed router-managed React HMR handlers to forward the active page HMR entry when reloading the current route through React Router.
|
|
13
|
+
- Fixed React route hydration bundles to resolve the router through the published import-map key and keep rerun navigation on the shared runtime graph.
|
|
14
|
+
- Removed the redundant React page props bootstrap script so route hydration relies on the canonical `__ECO_PAGE_DATA__` payload.
|
|
11
15
|
- Fixed React hydration, Fast Refresh, module loading, doctype handling, island asset reuse, and mixed-renderer boundary resolution across Bun, Vite, and Nitro flows.
|
|
12
16
|
- Restored direct `ReactPlugin` construction so the exported class still accepts the public plugin options shape.
|
|
13
17
|
- Fixed React boundary payload compatibility coverage and removed the plugin/renderer integration-name import cycle.
|
|
@@ -18,6 +22,7 @@ All notable changes to `@ecopages/react` are documented here.
|
|
|
18
22
|
|
|
19
23
|
### Refactoring
|
|
20
24
|
|
|
25
|
+
- Collapsed React route hydration into one page-owned entry module that re-exports the page component and bundles runtime dependencies in production.
|
|
21
26
|
- Consolidated React bundling, hydration, and runtime state behind shared service boundaries and `window.__ECO_PAGES__`.
|
|
22
27
|
- Moved React plugin option/default resolution into the factory and replaced renderer static config with instance-owned runtime wiring.
|
|
23
28
|
- Extracted React page-payload and locals serialization into a dedicated service to keep the renderer focused on orchestration.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecopages/react",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.25",
|
|
4
4
|
"description": "React integration for Ecopages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ecopages",
|
|
@@ -53,14 +53,14 @@
|
|
|
53
53
|
"directory": "packages/integrations/react"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@ecopages/core": "0.2.0-alpha.
|
|
56
|
+
"@ecopages/core": "0.2.0-alpha.25",
|
|
57
57
|
"@types/react": "^19",
|
|
58
58
|
"@types/react-dom": "^19",
|
|
59
59
|
"react": "^19",
|
|
60
60
|
"react-dom": "^19"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@ecopages/file-system": "0.2.0-alpha.
|
|
63
|
+
"@ecopages/file-system": "0.2.0-alpha.25",
|
|
64
64
|
"@ecopages/logger": "^0.2.3",
|
|
65
65
|
"@mdx-js/esbuild": "^3.0.1",
|
|
66
66
|
"@mdx-js/mdx": "^3.1.0",
|
package/src/react-renderer.d.ts
CHANGED
|
@@ -80,6 +80,7 @@ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
|
|
|
80
80
|
* already selected the React integration.
|
|
81
81
|
*/
|
|
82
82
|
private isReactManagedComponent;
|
|
83
|
+
private getComponentRequires;
|
|
83
84
|
private getRouterDocumentAttributes;
|
|
84
85
|
/**
|
|
85
86
|
* Commits a framework-agnostic component to React semantics.
|
package/src/react-renderer.js
CHANGED
|
@@ -118,6 +118,9 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
118
118
|
const integration = this.getComponentIntegration(component);
|
|
119
119
|
return integration === void 0 || integration === this.name;
|
|
120
120
|
}
|
|
121
|
+
getComponentRequires(component) {
|
|
122
|
+
return component?.requires;
|
|
123
|
+
}
|
|
121
124
|
getRouterDocumentAttributes() {
|
|
122
125
|
if (!this.routerAdapter) {
|
|
123
126
|
return void 0;
|
|
@@ -351,7 +354,9 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
351
354
|
*/
|
|
352
355
|
async renderReactComponentBoundary(input, runtimeContext) {
|
|
353
356
|
const componentConfig = input.component.config;
|
|
354
|
-
const context =
|
|
357
|
+
const context = {
|
|
358
|
+
componentInstanceId: input.integrationContext?.componentInstanceId
|
|
359
|
+
};
|
|
355
360
|
const hasResolvedChildHtml = input.children !== void 0;
|
|
356
361
|
let html = this.renderComponentHtml(input, context, runtimeContext);
|
|
357
362
|
const queuedBoundaryResolution = await this.resolveQueuedBoundaryHtml(html, runtimeContext);
|
|
@@ -498,10 +503,7 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
498
503
|
pageProps
|
|
499
504
|
}) {
|
|
500
505
|
try {
|
|
501
|
-
const safeLocals = this.pagePayloadService.getSerializableLocals(
|
|
502
|
-
locals,
|
|
503
|
-
Page.requires
|
|
504
|
-
);
|
|
506
|
+
const safeLocals = this.pagePayloadService.getSerializableLocals(locals, this.getComponentRequires(Page));
|
|
505
507
|
const allPageProps = this.pagePayloadService.buildSerializedPageProps({
|
|
506
508
|
pageProps,
|
|
507
509
|
params,
|
|
@@ -19,6 +19,16 @@ export interface ReactBundleServiceConfig {
|
|
|
19
19
|
nonReactExtensions?: string[];
|
|
20
20
|
jsxImportSource?: string;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Optional flags that adjust how a React client entry is bundled.
|
|
24
|
+
*/
|
|
25
|
+
export interface ReactClientBundleOptions {
|
|
26
|
+
/**
|
|
27
|
+
* When `true`, bundle React runtime dependencies into the emitted entry instead of
|
|
28
|
+
* rewriting them to external runtime specifiers.
|
|
29
|
+
*/
|
|
30
|
+
includeRuntime?: boolean;
|
|
31
|
+
}
|
|
22
32
|
/**
|
|
23
33
|
* Manages esbuild bundle configuration and plugin creation for React page/component builds.
|
|
24
34
|
*/
|
|
@@ -38,7 +48,7 @@ export declare class ReactBundleService {
|
|
|
38
48
|
* @param declaredModules - Explicitly declared browser module specifiers
|
|
39
49
|
* @returns Bundle options object for the build adapter
|
|
40
50
|
*/
|
|
41
|
-
createBundleOptions(componentName: string, isMdx: boolean, declaredModules: string[]): Promise<Record<string, unknown>>;
|
|
51
|
+
createBundleOptions(componentName: string, isMdx: boolean, declaredModules: string[], bundleOptions?: ReactClientBundleOptions): Promise<Record<string, unknown>>;
|
|
42
52
|
/**
|
|
43
53
|
* Creates the esbuild plugin that rewrites bare React specifiers
|
|
44
54
|
* to their runtime asset URLs.
|
|
@@ -32,11 +32,8 @@ class ReactBundleService {
|
|
|
32
32
|
* @param declaredModules - Explicitly declared browser module specifiers
|
|
33
33
|
* @returns Bundle options object for the build adapter
|
|
34
34
|
*/
|
|
35
|
-
async createBundleOptions(componentName, isMdx, declaredModules) {
|
|
36
|
-
const runtimeImports = this.getRuntimeImports();
|
|
37
|
-
const runtimeSpecifierMap = buildReactRuntimeSpecifierMap(runtimeImports, this.config.routerAdapter);
|
|
35
|
+
async createBundleOptions(componentName, isMdx, declaredModules, bundleOptions = {}) {
|
|
38
36
|
const options = {
|
|
39
|
-
external: getReactRuntimeExternalSpecifiers(),
|
|
40
37
|
mainFields: ["module", "browser", "main"],
|
|
41
38
|
naming: `${componentName}.[ext]`,
|
|
42
39
|
...import.meta.env?.NODE_ENV === "production" && {
|
|
@@ -45,6 +42,9 @@ class ReactBundleService {
|
|
|
45
42
|
treeshaking: true
|
|
46
43
|
}
|
|
47
44
|
};
|
|
45
|
+
if (!bundleOptions.includeRuntime) {
|
|
46
|
+
options.external = getReactRuntimeExternalSpecifiers();
|
|
47
|
+
}
|
|
48
48
|
const graphBoundaryPlugin = createClientGraphBoundaryPlugin({
|
|
49
49
|
absWorkingDir: this.config.rootDir,
|
|
50
50
|
declaredModules,
|
|
@@ -55,18 +55,22 @@ class ReactBundleService {
|
|
|
55
55
|
hostJsxImportSource: this.config.jsxImportSource ?? "react",
|
|
56
56
|
foreignExtensions: this.config.nonReactExtensions ?? []
|
|
57
57
|
});
|
|
58
|
-
const runtimeAliasPlugin = this.createRuntimeAliasPlugin(runtimeSpecifierMap);
|
|
59
58
|
const useSyncExternalStoreShimPlugin = createUseSyncExternalStoreShimPlugin({
|
|
60
59
|
name: "react-renderer-use-sync-external-store-shim",
|
|
61
60
|
namespace: "ecopages-react-renderer-shim"
|
|
62
61
|
});
|
|
62
|
+
const runtimePlugins = bundleOptions.includeRuntime ? [] : [
|
|
63
|
+
this.createRuntimeAliasPlugin(
|
|
64
|
+
buildReactRuntimeSpecifierMap(this.getRuntimeImports(), this.config.routerAdapter)
|
|
65
|
+
)
|
|
66
|
+
];
|
|
63
67
|
if (isMdx && this.config.mdxCompilerOptions) {
|
|
64
68
|
const { createReactMdxLoaderPlugin } = await import("../utils/react-mdx-loader-plugin.js");
|
|
65
69
|
const mdxPlugin = createReactMdxLoaderPlugin(this.config.mdxCompilerOptions);
|
|
66
70
|
options.plugins = [
|
|
67
71
|
foreignJsxOverridePlugin,
|
|
68
72
|
graphBoundaryPlugin,
|
|
69
|
-
|
|
73
|
+
...runtimePlugins,
|
|
70
74
|
mdxPlugin,
|
|
71
75
|
useSyncExternalStoreShimPlugin
|
|
72
76
|
];
|
|
@@ -74,7 +78,7 @@ class ReactBundleService {
|
|
|
74
78
|
options.plugins = [
|
|
75
79
|
foreignJsxOverridePlugin,
|
|
76
80
|
graphBoundaryPlugin,
|
|
77
|
-
|
|
81
|
+
...runtimePlugins,
|
|
78
82
|
useSyncExternalStoreShimPlugin
|
|
79
83
|
];
|
|
80
84
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hydration asset creation service for React integration.
|
|
3
3
|
*
|
|
4
|
-
* Builds the asset definitions
|
|
5
|
-
*
|
|
6
|
-
* island level.
|
|
4
|
+
* Builds the asset definitions required for client-side React rendering — both at
|
|
5
|
+
* the page level and the component island level.
|
|
7
6
|
*
|
|
8
7
|
* @module
|
|
9
8
|
*/
|
|
@@ -33,27 +32,26 @@ export declare class ReactHydrationAssetService {
|
|
|
33
32
|
private getIslandBundleName;
|
|
34
33
|
private getIslandHydrationName;
|
|
35
34
|
/**
|
|
36
|
-
* Resolves the import path for
|
|
35
|
+
* Resolves the browser import path used for a React-owned page or island module.
|
|
37
36
|
* Uses HMR manager for development or constructs static path for production.
|
|
38
37
|
*
|
|
39
38
|
* @param pagePath - Absolute path to the page source file
|
|
40
39
|
* @param assetName - Generated asset name
|
|
41
|
-
* @returns The resolved import path for the
|
|
40
|
+
* @returns The resolved browser import path for the module
|
|
42
41
|
*/
|
|
43
42
|
resolveAssetImportPath(pagePath: string, assetName: string): Promise<string>;
|
|
44
43
|
/**
|
|
45
|
-
* Creates the
|
|
44
|
+
* Creates the page-owned route entry asset for hydration and client navigation.
|
|
46
45
|
*
|
|
47
46
|
* @param pagePath - Absolute path to the page source file
|
|
48
47
|
* @param componentName - Generated unique component name
|
|
49
|
-
* @param importPath - Resolved import path
|
|
48
|
+
* @param importPath - Resolved browser import path used by development HMR
|
|
50
49
|
* @param bundleOptions - Bundle configuration options
|
|
51
50
|
* @param isDevelopment - Whether running in development mode with HMR
|
|
52
51
|
* @param isMdx - Whether the source file is an MDX file
|
|
53
|
-
* @
|
|
54
|
-
* @returns Array of asset definitions for processing
|
|
52
|
+
* @returns One page-owned asset definition for processing
|
|
55
53
|
*/
|
|
56
|
-
createPageDependencies(pagePath: string, componentName: string, importPath: string, bundleOptions: Record<string, unknown>, isDevelopment: boolean, isMdx: boolean
|
|
54
|
+
createPageDependencies(pagePath: string, componentName: string, importPath: string, bundleOptions: Record<string, unknown>, isDevelopment: boolean, isMdx: boolean): AssetDefinition[];
|
|
57
55
|
/**
|
|
58
56
|
* Builds client-side assets for a React component island.
|
|
59
57
|
*
|
|
@@ -21,12 +21,12 @@ class ReactHydrationAssetService {
|
|
|
21
21
|
return `${bundleName}-hydration-${componentKey}`;
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
* Resolves the import path for
|
|
24
|
+
* Resolves the browser import path used for a React-owned page or island module.
|
|
25
25
|
* Uses HMR manager for development or constructs static path for production.
|
|
26
26
|
*
|
|
27
27
|
* @param pagePath - Absolute path to the page source file
|
|
28
28
|
* @param assetName - Generated asset name
|
|
29
|
-
* @returns The resolved import path for the
|
|
29
|
+
* @returns The resolved browser import path for the module
|
|
30
30
|
*/
|
|
31
31
|
async resolveAssetImportPath(pagePath, assetName) {
|
|
32
32
|
const hmrManager = this.config.assetProcessingService?.getHmrManager();
|
|
@@ -36,71 +36,44 @@ class ReactHydrationAssetService {
|
|
|
36
36
|
return `/${path.join(RESOLVED_ASSETS_DIR, path.relative(this.config.srcDir, pagePath)).replace(path.basename(pagePath), `${assetName}.js`).replace(/\\/g, "/")}`;
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
|
-
* Creates the
|
|
39
|
+
* Creates the page-owned route entry asset for hydration and client navigation.
|
|
40
40
|
*
|
|
41
41
|
* @param pagePath - Absolute path to the page source file
|
|
42
42
|
* @param componentName - Generated unique component name
|
|
43
|
-
* @param importPath - Resolved import path
|
|
43
|
+
* @param importPath - Resolved browser import path used by development HMR
|
|
44
44
|
* @param bundleOptions - Bundle configuration options
|
|
45
45
|
* @param isDevelopment - Whether running in development mode with HMR
|
|
46
46
|
* @param isMdx - Whether the source file is an MDX file
|
|
47
|
-
* @
|
|
48
|
-
* @returns Array of asset definitions for processing
|
|
47
|
+
* @returns One page-owned asset definition for processing
|
|
49
48
|
*/
|
|
50
|
-
createPageDependencies(pagePath, componentName, importPath, bundleOptions, isDevelopment, isMdx
|
|
49
|
+
createPageDependencies(pagePath, componentName, importPath, bundleOptions, isDevelopment, isMdx) {
|
|
51
50
|
const runtimeImports = this.config.bundleService.getRuntimeImports();
|
|
52
|
-
|
|
53
|
-
AssetFactory.createFileScript({
|
|
54
|
-
position: "head",
|
|
55
|
-
filepath: pagePath,
|
|
56
|
-
name: componentName,
|
|
57
|
-
excludeFromHtml: true,
|
|
58
|
-
bundle: true,
|
|
59
|
-
bundleOptions,
|
|
60
|
-
attributes: {
|
|
61
|
-
type: "module",
|
|
62
|
-
defer: "",
|
|
63
|
-
"data-eco-persist": "true"
|
|
64
|
-
}
|
|
65
|
-
})
|
|
66
|
-
];
|
|
67
|
-
if (props && Object.keys(props).length > 0) {
|
|
68
|
-
dependencies.push(
|
|
69
|
-
AssetFactory.createContentScript({
|
|
70
|
-
position: "head",
|
|
71
|
-
content: `window.__ECO_PAGES__=window.__ECO_PAGES__||{};window.__ECO_PAGES__.page={module:"${importPath}",props:${JSON.stringify(props)}};`,
|
|
72
|
-
name: `${componentName}-props`,
|
|
73
|
-
bundle: false,
|
|
74
|
-
attributes: {
|
|
75
|
-
type: "module"
|
|
76
|
-
}
|
|
77
|
-
})
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
dependencies.push(
|
|
51
|
+
return [
|
|
81
52
|
AssetFactory.createContentScript({
|
|
82
53
|
position: "head",
|
|
83
54
|
content: createHydrationScript({
|
|
84
|
-
importPath,
|
|
85
|
-
reactImportPath: runtimeImports.react,
|
|
86
|
-
reactDomClientImportPath: runtimeImports.reactDomClient,
|
|
87
|
-
routerImportPath: runtimeImports.router,
|
|
55
|
+
importPath: isDevelopment ? importPath : pagePath,
|
|
56
|
+
reactImportPath: isDevelopment ? runtimeImports.react : "react",
|
|
57
|
+
reactDomClientImportPath: isDevelopment ? runtimeImports.reactDomClient : "react-dom/client",
|
|
58
|
+
routerImportPath: isDevelopment ? runtimeImports.router : this.config.routerAdapter?.importMapKey,
|
|
88
59
|
isDevelopment,
|
|
89
60
|
isMdx,
|
|
90
|
-
router: this.config.routerAdapter
|
|
61
|
+
router: this.config.routerAdapter,
|
|
62
|
+
scriptId: componentName
|
|
91
63
|
}),
|
|
92
|
-
name:
|
|
93
|
-
|
|
64
|
+
name: componentName,
|
|
65
|
+
packageRole: "page-script",
|
|
66
|
+
bundle: !isDevelopment,
|
|
67
|
+
bundleOptions,
|
|
94
68
|
attributes: {
|
|
95
69
|
type: "module",
|
|
96
70
|
defer: "",
|
|
97
71
|
"data-eco-rerun": "true",
|
|
98
|
-
"data-eco-script-id":
|
|
72
|
+
"data-eco-script-id": componentName,
|
|
99
73
|
"data-eco-persist": "true"
|
|
100
74
|
}
|
|
101
75
|
})
|
|
102
|
-
|
|
103
|
-
return dependencies;
|
|
76
|
+
];
|
|
104
77
|
}
|
|
105
78
|
/**
|
|
106
79
|
* Builds client-side assets for a React component island.
|
|
@@ -133,6 +106,7 @@ class ReactHydrationAssetService {
|
|
|
133
106
|
position: "head",
|
|
134
107
|
filepath: componentFile,
|
|
135
108
|
name: componentName,
|
|
109
|
+
packageRole: "dynamic-chunk",
|
|
136
110
|
excludeFromHtml: true,
|
|
137
111
|
bundle: true,
|
|
138
112
|
bundleOptions,
|
|
@@ -154,6 +128,7 @@ class ReactHydrationAssetService {
|
|
|
154
128
|
isDevelopment
|
|
155
129
|
}),
|
|
156
130
|
name: hydrationName,
|
|
131
|
+
packageRole: "keep-separate",
|
|
157
132
|
bundle: false,
|
|
158
133
|
attributes: {
|
|
159
134
|
type: "module",
|
|
@@ -46,7 +46,9 @@ export declare class ReactPageModuleService {
|
|
|
46
46
|
importMdxPageFile(filePath: string, options?: {
|
|
47
47
|
bypassCache?: boolean;
|
|
48
48
|
cacheScope?: string;
|
|
49
|
-
}): Promise<
|
|
49
|
+
}): Promise<EcoPageFile<{
|
|
50
|
+
config?: EcoComponentConfig;
|
|
51
|
+
}>>;
|
|
50
52
|
/**
|
|
51
53
|
* Ensures that an EcoComponentConfig has proper `__eco` metadata attached.
|
|
52
54
|
* Resolves the file path from dependency declarations when not already set.
|
|
@@ -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,10 @@ 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
|
+
/** Stable id of the page entry script tag in the document. */
|
|
14
|
+
scriptId: string;
|
|
13
15
|
/** Direct import path for React runtime module */
|
|
14
16
|
reactImportPath: string;
|
|
15
17
|
/** Direct import path for react-dom/client runtime module */
|
|
@@ -45,8 +45,21 @@ window.__ECO_PAGES__?.navigation?.claimOwnership?.("react-router");
|
|
|
45
45
|
function getProdRouterBootstrapRegistrationScript() {
|
|
46
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
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
|
+
}
|
|
48
61
|
function createDevScriptWithRouter(options) {
|
|
49
|
-
const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath } = options;
|
|
62
|
+
const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath, scriptId } = options;
|
|
50
63
|
const { components, getRouterProps } = router;
|
|
51
64
|
if (!routerImportPath) {
|
|
52
65
|
throw new Error("routerImportPath is required when router adapter is configured");
|
|
@@ -56,6 +69,12 @@ import { hydrateRoot } from "${reactDomClientImportPath}";
|
|
|
56
69
|
import { createElement } from "${reactImportPath}";
|
|
57
70
|
import { ${components.router}, ${components.pageContent} } from "${routerImportPath}";
|
|
58
71
|
${getImportStatement(importPath, isMdx)}
|
|
72
|
+
const pageModuleUrl = import.meta.url;
|
|
73
|
+
export default Page;
|
|
74
|
+
export const config = Page.config;
|
|
75
|
+
const isActivePageEntry = Boolean(document.querySelector('script[data-eco-script-id="${scriptId}"]'));
|
|
76
|
+
|
|
77
|
+
if (isActivePageEntry) {
|
|
59
78
|
|
|
60
79
|
window.__ECO_PAGES__ = window.__ECO_PAGES__ || {};
|
|
61
80
|
window.__ECO_PAGES__.hmrHandlers = window.__ECO_PAGES__.hmrHandlers || {};
|
|
@@ -64,6 +83,7 @@ window.__ECO_PAGES__.react.pageRoot = window.__ECO_PAGES__.react.pageRoot || nul
|
|
|
64
83
|
let root = window.__ECO_PAGES__.react.pageRoot;
|
|
65
84
|
${getDevPageRootCleanupScript()}
|
|
66
85
|
${getDevRouterBootstrapRegistrationScript()}
|
|
86
|
+
${getDevReuseExistingRouterRootScript()}
|
|
67
87
|
|
|
68
88
|
const getPageData = () => {
|
|
69
89
|
const el = document.getElementById("__ECO_PAGE_DATA__");
|
|
@@ -76,7 +96,7 @@ const getPageData = () => {
|
|
|
76
96
|
const props = getPageData();
|
|
77
97
|
|
|
78
98
|
window.__ECO_PAGES__.page = {
|
|
79
|
-
module:
|
|
99
|
+
module: pageModuleUrl,
|
|
80
100
|
props
|
|
81
101
|
};
|
|
82
102
|
|
|
@@ -86,7 +106,9 @@ const createTree = (Component, props) => {
|
|
|
86
106
|
};
|
|
87
107
|
|
|
88
108
|
const mount = () => {
|
|
89
|
-
if (
|
|
109
|
+
if (shouldReuseExistingRouterRoot()) {
|
|
110
|
+
root = window.__ECO_PAGES__.react.pageRoot;
|
|
111
|
+
} else if (window.__ECO_PAGES__.react?.pageRoot) {
|
|
90
112
|
root = window.__ECO_PAGES__.react.pageRoot;
|
|
91
113
|
root.render(createTree(Page, props));
|
|
92
114
|
} else {
|
|
@@ -100,16 +122,25 @@ const mount = () => {
|
|
|
100
122
|
const newModule = await import(newUrl);
|
|
101
123
|
const nextProps = getPageData();
|
|
102
124
|
${getHmrImportStatement(isMdx)}
|
|
125
|
+
const currentPageLayout = Page.config?.layout;
|
|
126
|
+
const nextPageLayout = NewPage.config?.layout;
|
|
127
|
+
|
|
128
|
+
if (window.__ECO_PAGES__?.navigation?.getOwnerState().owner === "react-router") {
|
|
129
|
+
await window.__ECO_PAGES__?.navigation?.reloadCurrentPage?.({
|
|
130
|
+
clearCache: currentPageLayout !== nextPageLayout,
|
|
131
|
+
moduleUrl: "${importPath}",
|
|
132
|
+
source: "react-router"
|
|
133
|
+
});
|
|
134
|
+
console.log("[ecopages] ${getComponentType(isMdx)} component updated via router");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
103
138
|
window.__ECO_PAGES__.page = {
|
|
104
|
-
module:
|
|
139
|
+
module: pageModuleUrl,
|
|
105
140
|
props: nextProps
|
|
106
141
|
};
|
|
107
142
|
root.render(createTree(NewPage, nextProps));
|
|
108
|
-
|
|
109
|
-
console.log("[ecopages] ${getComponentType(isMdx)} component updated via router");
|
|
110
|
-
} else {
|
|
111
|
-
console.log("[ecopages] ${getComponentType(isMdx)} component updated");
|
|
112
|
-
}
|
|
143
|
+
console.log("[ecopages] ${getComponentType(isMdx)} component updated");
|
|
113
144
|
} catch (e) {
|
|
114
145
|
console.error("[ecopages] Failed to hot-reload ${getComponentType(isMdx)} component:", e);
|
|
115
146
|
}
|
|
@@ -121,14 +152,21 @@ if (document.readyState === "loading") {
|
|
|
121
152
|
} else {
|
|
122
153
|
mount();
|
|
123
154
|
}
|
|
155
|
+
}
|
|
124
156
|
`.trim();
|
|
125
157
|
}
|
|
126
158
|
function createDevScriptWithoutRouter(options) {
|
|
127
|
-
const { importPath, isMdx, reactImportPath, reactDomClientImportPath } = options;
|
|
159
|
+
const { importPath, isMdx, reactImportPath, reactDomClientImportPath, scriptId } = options;
|
|
128
160
|
return `
|
|
129
161
|
import { hydrateRoot } from "${reactDomClientImportPath}";
|
|
130
162
|
import { createElement } from "${reactImportPath}";
|
|
131
163
|
${getImportStatement(importPath, isMdx)}
|
|
164
|
+
const pageModuleUrl = import.meta.url;
|
|
165
|
+
export default Page;
|
|
166
|
+
export const config = Page.config;
|
|
167
|
+
const isActivePageEntry = Boolean(document.querySelector('script[data-eco-script-id="${scriptId}"]'));
|
|
168
|
+
|
|
169
|
+
if (isActivePageEntry) {
|
|
132
170
|
|
|
133
171
|
window.__ECO_PAGES__ = window.__ECO_PAGES__ || {};
|
|
134
172
|
window.__ECO_PAGES__.hmrHandlers = window.__ECO_PAGES__.hmrHandlers || {};
|
|
@@ -148,7 +186,7 @@ const getPageData = () => {
|
|
|
148
186
|
const props = getPageData();
|
|
149
187
|
|
|
150
188
|
window.__ECO_PAGES__.page = {
|
|
151
|
-
module:
|
|
189
|
+
module: pageModuleUrl,
|
|
152
190
|
props
|
|
153
191
|
};
|
|
154
192
|
|
|
@@ -186,25 +224,26 @@ if (document.readyState === "loading") {
|
|
|
186
224
|
} else {
|
|
187
225
|
mount();
|
|
188
226
|
}
|
|
227
|
+
}
|
|
189
228
|
`.trim();
|
|
190
229
|
}
|
|
191
230
|
function createProdScriptWithRouter(options) {
|
|
192
|
-
const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath } = options;
|
|
231
|
+
const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath, scriptId } = options;
|
|
193
232
|
const { components, getRouterProps } = router;
|
|
194
233
|
if (!routerImportPath) {
|
|
195
234
|
throw new Error("routerImportPath is required when router adapter is configured");
|
|
196
235
|
}
|
|
197
236
|
if (isMdx) {
|
|
198
|
-
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;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()}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:
|
|
237
|
+
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=import.meta.url;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()}`;
|
|
199
238
|
}
|
|
200
|
-
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}";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()}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:
|
|
239
|
+
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=import.meta.url;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()}`;
|
|
201
240
|
}
|
|
202
241
|
function createProdScriptWithoutRouter(options) {
|
|
203
|
-
const { importPath, isMdx, reactImportPath, reactDomClientImportPath } = options;
|
|
242
|
+
const { importPath, isMdx, reactImportPath, reactDomClientImportPath, scriptId } = options;
|
|
204
243
|
if (isMdx) {
|
|
205
|
-
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;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:
|
|
244
|
+
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=import.meta.url;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()}`;
|
|
206
245
|
}
|
|
207
|
-
return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import P from"${importPath}";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:
|
|
246
|
+
return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import P from"${importPath}";const u=import.meta.url;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()}`;
|
|
208
247
|
}
|
|
209
248
|
function createHydrationScript(options) {
|
|
210
249
|
const { isDevelopment, router } = options;
|
|
@@ -17,11 +17,18 @@ const routerAdapter = {
|
|
|
17
17
|
function createModuleUrl(source) {
|
|
18
18
|
return `data:text/javascript;base64,${btoa(source)}`;
|
|
19
19
|
}
|
|
20
|
-
async function importModule(moduleUrl) {
|
|
20
|
+
async function importModule(moduleUrl, scriptId) {
|
|
21
|
+
let marker;
|
|
22
|
+
if (scriptId) {
|
|
23
|
+
marker = document.createElement("script");
|
|
24
|
+
marker.setAttribute("data-eco-script-id", scriptId);
|
|
25
|
+
document.head.appendChild(marker);
|
|
26
|
+
}
|
|
21
27
|
await import(
|
|
22
28
|
/* @vite-ignore */
|
|
23
29
|
moduleUrl
|
|
24
30
|
);
|
|
31
|
+
marker?.remove();
|
|
25
32
|
}
|
|
26
33
|
function createRuntimeModules() {
|
|
27
34
|
const reactImportPath = createModuleUrl("export const createElement = (...args) => ({ args });");
|
|
@@ -71,6 +78,7 @@ describe("createHydrationScript browser execution", () => {
|
|
|
71
78
|
const testWindow = window;
|
|
72
79
|
testWindow.__ECO_REACT_HYDRATION_TEST__ = {
|
|
73
80
|
hydrateCalls: [],
|
|
81
|
+
renderCalls: [],
|
|
74
82
|
claimedOwners: [],
|
|
75
83
|
releasedOwners: [],
|
|
76
84
|
registrations: [],
|
|
@@ -99,11 +107,13 @@ describe("createHydrationScript browser execution", () => {
|
|
|
99
107
|
})}<\/script>`;
|
|
100
108
|
const script = createHydrationScript({
|
|
101
109
|
...runtimeModules,
|
|
110
|
+
scriptId: "ecopages-react-page",
|
|
102
111
|
isDevelopment: true,
|
|
103
112
|
isMdx: false,
|
|
104
113
|
router: routerAdapter
|
|
105
114
|
});
|
|
106
|
-
|
|
115
|
+
const moduleUrl = createModuleUrl(script);
|
|
116
|
+
await importModule(moduleUrl, "ecopages-react-page");
|
|
107
117
|
expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.hydrateCalls).toHaveLength(1);
|
|
108
118
|
expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.hydrateCalls[0]?.containerTag).toBe("BODY");
|
|
109
119
|
expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.hydrateCalls[0]?.hasRecoverableErrorHandler).toBe(true);
|
|
@@ -111,7 +121,7 @@ describe("createHydrationScript browser execution", () => {
|
|
|
111
121
|
expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.registrations).toHaveLength(1);
|
|
112
122
|
expect(typeof testWindow.__ECO_PAGES__?.react?.cleanupPageRoot).toBe("function");
|
|
113
123
|
expect(testWindow.__ECO_PAGES__?.page).toEqual({
|
|
114
|
-
module:
|
|
124
|
+
module: moduleUrl,
|
|
115
125
|
props: {
|
|
116
126
|
title: "Hello React",
|
|
117
127
|
locals: { theme: "dark" }
|
|
@@ -123,4 +133,67 @@ describe("createHydrationScript browser execution", () => {
|
|
|
123
133
|
expect(testWindow.__ECO_PAGES__?.page).toBeUndefined();
|
|
124
134
|
expect(testWindow.__ECO_PAGES__?.react?.pageRoot).toBeNull();
|
|
125
135
|
});
|
|
136
|
+
it("reuses an existing router-owned page root during rerun bootstrap execution", async () => {
|
|
137
|
+
const runtimeModules = createRuntimeModules();
|
|
138
|
+
const testWindow = window;
|
|
139
|
+
testWindow.__ECO_REACT_HYDRATION_TEST__ = {
|
|
140
|
+
hydrateCalls: [],
|
|
141
|
+
renderCalls: [],
|
|
142
|
+
claimedOwners: [],
|
|
143
|
+
releasedOwners: [],
|
|
144
|
+
registrations: [],
|
|
145
|
+
unmountCount: 0
|
|
146
|
+
};
|
|
147
|
+
const existingRoot = {
|
|
148
|
+
render: (tree) => {
|
|
149
|
+
testWindow.__ECO_REACT_HYDRATION_TEST__?.renderCalls.push(tree);
|
|
150
|
+
},
|
|
151
|
+
unmount: () => {
|
|
152
|
+
testWindow.__ECO_REACT_HYDRATION_TEST__.unmountCount += 1;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
testWindow.__ECO_PAGES__ = {
|
|
156
|
+
navigation: {
|
|
157
|
+
getOwnerState: () => ({
|
|
158
|
+
owner: "react-router",
|
|
159
|
+
canHandleSpaNavigation: true
|
|
160
|
+
}),
|
|
161
|
+
register: (registration) => {
|
|
162
|
+
testWindow.__ECO_REACT_HYDRATION_TEST__?.registrations.push(registration);
|
|
163
|
+
},
|
|
164
|
+
claimOwnership: (owner) => {
|
|
165
|
+
testWindow.__ECO_REACT_HYDRATION_TEST__?.claimedOwners.push(owner);
|
|
166
|
+
},
|
|
167
|
+
releaseOwnership: (owner) => {
|
|
168
|
+
testWindow.__ECO_REACT_HYDRATION_TEST__?.releasedOwners.push(owner);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
react: {
|
|
172
|
+
pageRoot: existingRoot
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
document.body.innerHTML = `<script id="__ECO_PAGE_DATA__" type="application/json">${JSON.stringify({
|
|
176
|
+
title: "Rerun"
|
|
177
|
+
})}<\/script>`;
|
|
178
|
+
const script = createHydrationScript({
|
|
179
|
+
...runtimeModules,
|
|
180
|
+
scriptId: "ecopages-react-page-rerun",
|
|
181
|
+
isDevelopment: true,
|
|
182
|
+
isMdx: false,
|
|
183
|
+
router: routerAdapter
|
|
184
|
+
});
|
|
185
|
+
const moduleUrl = createModuleUrl(script);
|
|
186
|
+
await importModule(moduleUrl, "ecopages-react-page-rerun");
|
|
187
|
+
expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.hydrateCalls).toHaveLength(0);
|
|
188
|
+
expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.renderCalls).toHaveLength(0);
|
|
189
|
+
expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.claimedOwners).toHaveLength(0);
|
|
190
|
+
expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.registrations).toHaveLength(0);
|
|
191
|
+
expect(testWindow.__ECO_PAGES__?.react?.pageRoot).toBe(existingRoot);
|
|
192
|
+
expect(testWindow.__ECO_PAGES__?.page).toEqual({
|
|
193
|
+
module: moduleUrl,
|
|
194
|
+
props: {
|
|
195
|
+
title: "Rerun"
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
});
|
|
126
199
|
});
|