@ecopages/react 0.2.0-alpha.9 → 0.2.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -13
- package/package.json +23 -12
- package/src/eco-embed.d.ts +11 -0
- package/src/eco-embed.js +11 -0
- package/src/react-hmr-strategy.d.ts +102 -18
- package/src/react-hmr-strategy.js +427 -50
- package/src/react-renderer.d.ts +100 -92
- package/src/react-renderer.js +356 -340
- package/src/react.constants.d.ts +1 -0
- package/src/react.constants.js +4 -0
- package/src/react.plugin.d.ts +25 -107
- package/src/react.plugin.js +109 -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/runtime/use-sync-external-store-with-selector.d.ts +3 -0
- package/src/runtime/use-sync-external-store-with-selector.js +56 -0
- package/src/services/pages-index.d.ts +64 -0
- package/src/services/pages-index.js +73 -0
- package/src/services/react-bundle.service.d.ts +24 -9
- package/src/services/react-bundle.service.js +35 -24
- package/src/services/react-hmr-page-metadata-cache.d.ts +10 -1
- 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 +83 -64
- 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 +8 -3
- package/src/services/react-page-module.service.js +33 -26
- 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 +9 -2
- package/src/services/react-runtime-bundle.service.js +77 -16
- package/src/utils/client-graph-boundary-cache.d.ts +108 -0
- package/src/utils/client-graph-boundary-cache.js +116 -0
- package/src/utils/client-graph-boundary-plugin.d.ts +13 -5
- package/src/utils/client-graph-boundary-plugin.js +63 -5
- 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 +9 -5
- package/src/utils/hydration-scripts.js +119 -34
- 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 +1 -1
- package/src/utils/react-dom-runtime-interop-plugin.js +9 -0
- package/src/utils/react-mdx-loader-plugin.d.ts +1 -1
- package/src/utils/{react-runtime-specifier-map.d.ts → react-runtime-alias-map.d.ts} +3 -1
- package/src/utils/react-runtime-alias-map.js +90 -0
- package/CHANGELOG.md +0 -27
- package/src/react-hmr-strategy.ts +0 -386
- package/src/react-renderer.ts +0 -803
- package/src/react.plugin.ts +0 -276
- package/src/router-adapter.ts +0 -95
- package/src/services/react-bundle.service.ts +0 -108
- package/src/services/react-hmr-page-metadata-cache.ts +0 -24
- package/src/services/react-hydration-asset.service.ts +0 -263
- package/src/services/react-page-module.service.ts +0 -224
- package/src/services/react-runtime-bundle.service.ts +0 -172
- package/src/utils/client-graph-boundary-plugin.ts +0 -831
- 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 -459
- package/src/utils/reachability-analyzer.ts +0 -593
- package/src/utils/react-dom-runtime-interop-plugin.ts +0 -33
- package/src/utils/react-mdx-loader-plugin.ts +0 -63
- package/src/utils/react-runtime-specifier-map.js +0 -37
- package/src/utils/react-runtime-specifier-map.ts +0 -45
- package/src/utils/use-sync-external-store-shim-plugin.d.ts +0 -5
- package/src/utils/use-sync-external-store-shim-plugin.js +0 -41
- package/src/utils/use-sync-external-store-shim-plugin.ts +0 -45
|
@@ -1,459 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hydration script generators for React pages.
|
|
3
|
-
* These functions create the client-side scripts that hydrate React components.
|
|
4
|
-
* @module
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { ReactRouterAdapter } from '../router-adapter.ts';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Options for generating a hydration script.
|
|
11
|
-
*/
|
|
12
|
-
export type HydrationScriptOptions = {
|
|
13
|
-
/** The import path for the bundled page component */
|
|
14
|
-
importPath: string;
|
|
15
|
-
/** Direct import path for React runtime module */
|
|
16
|
-
reactImportPath: string;
|
|
17
|
-
/** Direct import path for react-dom/client runtime module */
|
|
18
|
-
reactDomClientImportPath: string;
|
|
19
|
-
/** Direct import path for router runtime module */
|
|
20
|
-
routerImportPath?: string;
|
|
21
|
-
/** Whether running in development mode with HMR support */
|
|
22
|
-
isDevelopment: boolean;
|
|
23
|
-
/** Whether the source file is an MDX file */
|
|
24
|
-
isMdx: boolean;
|
|
25
|
-
/** Optional router adapter for SPA navigation */
|
|
26
|
-
router?: ReactRouterAdapter;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export type IslandHydrationScriptOptions = {
|
|
30
|
-
/** Bundled browser module path for the island component. */
|
|
31
|
-
importPath: string;
|
|
32
|
-
/** Browser import path for React runtime. */
|
|
33
|
-
reactImportPath: string;
|
|
34
|
-
/** Browser import path for react-dom/client runtime. */
|
|
35
|
-
reactDomClientImportPath: string;
|
|
36
|
-
/** Selector that resolves to the SSR root element for this island instance. */
|
|
37
|
-
targetSelector: string;
|
|
38
|
-
/** Serialized component props emitted at render time. */
|
|
39
|
-
props: Record<string, unknown>;
|
|
40
|
-
/** Optional stable component id used to resolve named exports reliably. */
|
|
41
|
-
componentRef?: string;
|
|
42
|
-
/** Optional source file hint used as fallback for component resolution. */
|
|
43
|
-
componentFile?: string;
|
|
44
|
-
/** Enables development-oriented non-minified output. */
|
|
45
|
-
isDevelopment: boolean;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Generates the import statement for the page component.
|
|
50
|
-
* MDX files use namespace imports to access the config export.
|
|
51
|
-
*/
|
|
52
|
-
function getImportStatement(importPath: string, isMdx: boolean): string {
|
|
53
|
-
return isMdx
|
|
54
|
-
? `import * as MDXModule from "${importPath}";\nconst Page = MDXModule.default;\nif (MDXModule.config) Page.config = MDXModule.config;`
|
|
55
|
-
: `import Page from "${importPath}";`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Generates the HMR import statement for hot-reloading.
|
|
60
|
-
* MDX files need to extract config from the new module.
|
|
61
|
-
*/
|
|
62
|
-
function getHmrImportStatement(isMdx: boolean): string {
|
|
63
|
-
return isMdx
|
|
64
|
-
? 'const NewPage = newModule.default; if (newModule.config) NewPage.config = newModule.config;'
|
|
65
|
-
: 'const NewPage = newModule.default;';
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Returns the component type label for logging.
|
|
70
|
-
*/
|
|
71
|
-
function getComponentType(isMdx: boolean): string {
|
|
72
|
-
return isMdx ? 'MDX' : 'React';
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Generates the development cleanup hook for the page-level React root.
|
|
77
|
-
*
|
|
78
|
-
* Why this exists:
|
|
79
|
-
* browser-router and React-router hand document ownership back and forth. The
|
|
80
|
-
* client runtime therefore needs a single cleanup entry point that can unmount
|
|
81
|
-
* the active React root, clear ownership flags, and discard serialized page data
|
|
82
|
-
* before a non-React renderer or a fresh React bootstrap takes over.
|
|
83
|
-
*
|
|
84
|
-
* Why it is emitted as a string:
|
|
85
|
-
* this module generates browser bootstraps, so the cleanup behavior must be
|
|
86
|
-
* embedded directly into the emitted module source.
|
|
87
|
-
*/
|
|
88
|
-
function getDevPageRootCleanupScript(): string {
|
|
89
|
-
return `window.__ECO_PAGES__ = window.__ECO_PAGES__ || {};
|
|
90
|
-
window.__ECO_PAGES__.react = window.__ECO_PAGES__.react || {};
|
|
91
|
-
window.__ECO_PAGES__.react.cleanupPageRoot = () => {
|
|
92
|
-
const activeRoot = window.__ECO_PAGES__.react?.pageRoot || root;
|
|
93
|
-
if (!activeRoot) {
|
|
94
|
-
window.__ECO_PAGES__.react.pageRoot = null;
|
|
95
|
-
window.__ECO_PAGES__?.navigation?.releaseOwnership?.("react-router");
|
|
96
|
-
delete window.__ECO_PAGES__.page;
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
window.__ECO_PAGES__.react.pageRoot = null;
|
|
100
|
-
window.__ECO_PAGES__?.navigation?.releaseOwnership?.("react-router");
|
|
101
|
-
delete window.__ECO_PAGES__.page;
|
|
102
|
-
root = null;
|
|
103
|
-
activeRoot.unmount();
|
|
104
|
-
};`;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Minified production variant of the page-root cleanup hook.
|
|
109
|
-
*
|
|
110
|
-
* It mirrors the development behavior exactly so navigation ownership semantics
|
|
111
|
-
* remain identical across environments while keeping the emitted payload small.
|
|
112
|
-
*/
|
|
113
|
-
function getProdPageRootCleanupScript(): string {
|
|
114
|
-
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()};';
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function getDevRouterBootstrapRegistrationScript(): string {
|
|
118
|
-
return `window.__ECO_PAGES__?.navigation?.register({
|
|
119
|
-
owner: "react-router",
|
|
120
|
-
cleanupBeforeHandoff: async () => {
|
|
121
|
-
window.__ECO_PAGES__?.react?.cleanupPageRoot?.();
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
window.__ECO_PAGES__?.navigation?.claimOwnership?.("react-router");`;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function getProdRouterBootstrapRegistrationScript(): string {
|
|
128
|
-
return 'window.__ECO_PAGES__?.navigation?.register({owner:"react-router",cleanupBeforeHandoff:async()=>{window.__ECO_PAGES__?.react?.cleanupPageRoot?.()}});window.__ECO_PAGES__?.navigation?.claimOwnership?.("react-router");';
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Creates development hydration script with router support.
|
|
133
|
-
*
|
|
134
|
-
* Why this branch exists:
|
|
135
|
-
* router-managed React pages keep a long-lived root across client-side
|
|
136
|
-
* navigations. The bootstrap therefore hydrates once, reuses that root for
|
|
137
|
-
* future renders, exposes cleanup for ownership handoff, and lets the router
|
|
138
|
-
* adapter reconstruct page content instead of rebuilding layout trees here.
|
|
139
|
-
*
|
|
140
|
-
* How it works:
|
|
141
|
-
* - imports the page module and router runtime pieces
|
|
142
|
-
* - reads serialized page props from the server payload
|
|
143
|
-
* - hydrates or re-renders the shared page root
|
|
144
|
-
* - installs HMR handlers that either ask the router to reload the active page
|
|
145
|
-
* or patch the current root directly when the router is inactive
|
|
146
|
-
*/
|
|
147
|
-
function createDevScriptWithRouter(options: HydrationScriptOptions): string {
|
|
148
|
-
const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath } = options;
|
|
149
|
-
const { components, getRouterProps } = router!;
|
|
150
|
-
if (!routerImportPath) {
|
|
151
|
-
throw new Error('routerImportPath is required when router adapter is configured');
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return `
|
|
155
|
-
import { hydrateRoot } from "${reactDomClientImportPath}";
|
|
156
|
-
import { createElement } from "${reactImportPath}";
|
|
157
|
-
import { ${components.router}, ${components.pageContent} } from "${routerImportPath}";
|
|
158
|
-
${getImportStatement(importPath, isMdx)}
|
|
159
|
-
|
|
160
|
-
window.__ECO_PAGES__ = window.__ECO_PAGES__ || {};
|
|
161
|
-
window.__ECO_PAGES__.hmrHandlers = window.__ECO_PAGES__.hmrHandlers || {};
|
|
162
|
-
window.__ECO_PAGES__.react = window.__ECO_PAGES__.react || {};
|
|
163
|
-
window.__ECO_PAGES__.react.pageRoot = window.__ECO_PAGES__.react.pageRoot || null;
|
|
164
|
-
let root = window.__ECO_PAGES__.react.pageRoot;
|
|
165
|
-
${getDevPageRootCleanupScript()}
|
|
166
|
-
${getDevRouterBootstrapRegistrationScript()}
|
|
167
|
-
|
|
168
|
-
const getPageData = () => {
|
|
169
|
-
const el = document.getElementById("__ECO_PAGE_DATA__");
|
|
170
|
-
if (el?.textContent) {
|
|
171
|
-
try { return JSON.parse(el.textContent); } catch {}
|
|
172
|
-
}
|
|
173
|
-
return {};
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
const props = getPageData();
|
|
177
|
-
|
|
178
|
-
window.__ECO_PAGES__.page = {
|
|
179
|
-
module: "${importPath}",
|
|
180
|
-
props
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
const createTree = (Component, props) => {
|
|
184
|
-
const pageContent = createElement(${components.pageContent});
|
|
185
|
-
return createElement(${components.router}, ${getRouterProps('Component', 'props')}, pageContent);
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
const mount = () => {
|
|
189
|
-
if (window.__ECO_PAGES__.react?.pageRoot) {
|
|
190
|
-
root = window.__ECO_PAGES__.react.pageRoot;
|
|
191
|
-
root.render(createTree(Page, props));
|
|
192
|
-
} else {
|
|
193
|
-
root = hydrateRoot(document.body, createTree(Page, props), {
|
|
194
|
-
onRecoverableError: (err) => console.warn("[ecopages] Hydration error:", err)
|
|
195
|
-
});
|
|
196
|
-
window.__ECO_PAGES__.react.pageRoot = root;
|
|
197
|
-
}
|
|
198
|
-
window.__ECO_PAGES__.hmrHandlers["${importPath}"] = async (newUrl) => {
|
|
199
|
-
if (window.__ECO_PAGES__?.navigation?.getOwnerState().owner === "react-router") {
|
|
200
|
-
await window.__ECO_PAGES__?.navigation?.reloadCurrentPage?.({ clearCache: false, source: "react-router" });
|
|
201
|
-
console.log("[ecopages] ${getComponentType(isMdx)} component updated via router");
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
try {
|
|
205
|
-
const newModule = await import(newUrl);
|
|
206
|
-
${getHmrImportStatement(isMdx)}
|
|
207
|
-
root.render(createTree(NewPage, props));
|
|
208
|
-
console.log("[ecopages] ${getComponentType(isMdx)} component updated");
|
|
209
|
-
} catch (e) {
|
|
210
|
-
console.error("[ecopages] Failed to hot-reload ${getComponentType(isMdx)} component:", e);
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
if (document.readyState === "loading") {
|
|
216
|
-
document.addEventListener("DOMContentLoaded", mount);
|
|
217
|
-
} else {
|
|
218
|
-
mount();
|
|
219
|
-
}
|
|
220
|
-
`.trim();
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Creates development hydration script without router.
|
|
225
|
-
*
|
|
226
|
-
* Why this branch exists:
|
|
227
|
-
* non-router React pages rebuild their layout tree directly from the page
|
|
228
|
-
* module on the client. That means the bootstrap must recreate the page and its
|
|
229
|
-
* optional layout so hydration matches the server HTML exactly.
|
|
230
|
-
*
|
|
231
|
-
* How it works:
|
|
232
|
-
* - imports the page module directly
|
|
233
|
-
* - reconstructs the layout wrapper from `Page.config?.layout`
|
|
234
|
-
* - hydrates a single document root
|
|
235
|
-
* - patches that root during HMR without involving a router adapter
|
|
236
|
-
*/
|
|
237
|
-
function createDevScriptWithoutRouter(options: HydrationScriptOptions): string {
|
|
238
|
-
const { importPath, isMdx, reactImportPath, reactDomClientImportPath } = options;
|
|
239
|
-
|
|
240
|
-
return `
|
|
241
|
-
import { hydrateRoot } from "${reactDomClientImportPath}";
|
|
242
|
-
import { createElement } from "${reactImportPath}";
|
|
243
|
-
${getImportStatement(importPath, isMdx)}
|
|
244
|
-
|
|
245
|
-
window.__ECO_PAGES__ = window.__ECO_PAGES__ || {};
|
|
246
|
-
window.__ECO_PAGES__.hmrHandlers = window.__ECO_PAGES__.hmrHandlers || {};
|
|
247
|
-
window.__ECO_PAGES__.react = window.__ECO_PAGES__.react || {};
|
|
248
|
-
window.__ECO_PAGES__.react.pageRoot = window.__ECO_PAGES__.react.pageRoot || null;
|
|
249
|
-
let root = window.__ECO_PAGES__.react.pageRoot;
|
|
250
|
-
${getDevPageRootCleanupScript()}
|
|
251
|
-
|
|
252
|
-
const getPageData = () => {
|
|
253
|
-
const el = document.getElementById("__ECO_PAGE_DATA__");
|
|
254
|
-
if (el?.textContent) {
|
|
255
|
-
try { return JSON.parse(el.textContent); } catch {}
|
|
256
|
-
}
|
|
257
|
-
return {};
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
const props = getPageData();
|
|
261
|
-
|
|
262
|
-
window.__ECO_PAGES__.page = {
|
|
263
|
-
module: "${importPath}",
|
|
264
|
-
props
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
const createTree = (Component, props) => {
|
|
268
|
-
const Layout = Component.config?.layout;
|
|
269
|
-
const pageElement = createElement(Component, props);
|
|
270
|
-
const layoutProps = props?.locals ? { locals: props.locals } : null;
|
|
271
|
-
return Layout ? createElement(Layout, layoutProps, pageElement) : pageElement;
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
const mount = () => {
|
|
275
|
-
if (window.__ECO_PAGES__.react?.pageRoot) {
|
|
276
|
-
root = window.__ECO_PAGES__.react.pageRoot;
|
|
277
|
-
root.render(createTree(Page, props));
|
|
278
|
-
} else {
|
|
279
|
-
root = hydrateRoot(document.body, createTree(Page, props), {
|
|
280
|
-
onRecoverableError: (err) => console.warn("[ecopages] Hydration error:", err)
|
|
281
|
-
});
|
|
282
|
-
window.__ECO_PAGES__.react.pageRoot = root;
|
|
283
|
-
}
|
|
284
|
-
window.__ECO_PAGES__.hmrHandlers["${importPath}"] = async (newUrl) => {
|
|
285
|
-
try {
|
|
286
|
-
const newModule = await import(newUrl);
|
|
287
|
-
${getHmrImportStatement(isMdx)}
|
|
288
|
-
root.render(createTree(NewPage, props));
|
|
289
|
-
console.log("[ecopages] ${getComponentType(isMdx)} component updated");
|
|
290
|
-
} catch (e) {
|
|
291
|
-
console.error("[ecopages] Failed to hot-reload ${getComponentType(isMdx)} component:", e);
|
|
292
|
-
}
|
|
293
|
-
};
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
if (document.readyState === "loading") {
|
|
297
|
-
document.addEventListener("DOMContentLoaded", mount);
|
|
298
|
-
} else {
|
|
299
|
-
mount();
|
|
300
|
-
}
|
|
301
|
-
`.trim();
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Creates minified production hydration script with router support.
|
|
306
|
-
*
|
|
307
|
-
* This is the production counterpart to `createDevScriptWithRouter()`. The
|
|
308
|
-
* ownership and hydration behavior is the same; only the emitted source is
|
|
309
|
-
* compressed for delivery.
|
|
310
|
-
*/
|
|
311
|
-
function createProdScriptWithRouter(options: HydrationScriptOptions): string {
|
|
312
|
-
const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath } = options;
|
|
313
|
-
const { components, getRouterProps } = router!;
|
|
314
|
-
if (!routerImportPath) {
|
|
315
|
-
throw new Error('routerImportPath is required when router adapter is configured');
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (isMdx) {
|
|
319
|
-
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:"${importPath}",props:pr};const ct=(C,p)=>ce(R,${getRouterProps('C', 'p')},ce(PC));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()`;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
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:"${importPath}",props:pr};const ct=(C,p)=>ce(R,${getRouterProps('C', 'p')},ce(PC));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()`;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Creates the minified production hydration script for non-router pages.
|
|
327
|
-
*
|
|
328
|
-
* In this mode the page module is responsible for reconstructing its own layout
|
|
329
|
-
* tree. If the server serialized request `locals`, the script forwards those
|
|
330
|
-
* values to the layout as well as the page so hydration matches the server HTML.
|
|
331
|
-
* The runtime semantics mirror the development path; only the emitted source is
|
|
332
|
-
* condensed.
|
|
333
|
-
*/
|
|
334
|
-
function createProdScriptWithoutRouter(options: HydrationScriptOptions): string {
|
|
335
|
-
const { importPath, isMdx, reactImportPath, reactDomClientImportPath } = options;
|
|
336
|
-
|
|
337
|
-
if (isMdx) {
|
|
338
|
-
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:"${importPath}",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()`;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
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:"${importPath}",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()`;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Creates a hydration script for client-side React hydration.
|
|
346
|
-
*
|
|
347
|
-
* Why this dispatcher exists:
|
|
348
|
-
* the runtime matrix is small but behaviorally different across development vs
|
|
349
|
-
* production and router vs non-router pages. Keeping that branch here preserves
|
|
350
|
-
* a compact public API while allowing each emitted script to stay focused.
|
|
351
|
-
*
|
|
352
|
-
* Selection rules:
|
|
353
|
-
* - development uses readable scripts with HMR hooks
|
|
354
|
-
* - production uses minified equivalents
|
|
355
|
-
* - router presence decides whether page updates flow through the router runtime
|
|
356
|
-
* or rebuild directly from the page module
|
|
357
|
-
*
|
|
358
|
-
* @param options - Configuration options for script generation
|
|
359
|
-
* @returns The generated hydration script as a string
|
|
360
|
-
*/
|
|
361
|
-
export function createHydrationScript(options: HydrationScriptOptions): string {
|
|
362
|
-
const { isDevelopment, router } = options;
|
|
363
|
-
|
|
364
|
-
if (isDevelopment) {
|
|
365
|
-
return router ? createDevScriptWithRouter(options) : createDevScriptWithoutRouter(options);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
return router ? createProdScriptWithRouter(options) : createProdScriptWithoutRouter(options);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Creates the client bootstrap for component-level React islands.
|
|
373
|
-
*
|
|
374
|
-
* The island runtime intentionally uses `createRoot()` (not `hydrateRoot()`) and
|
|
375
|
-
* mounts into the SSR element identified by `targetSelector`.
|
|
376
|
-
*
|
|
377
|
-
* Rationale:
|
|
378
|
-
* - No synthetic wrapper element is introduced in SSR output.
|
|
379
|
-
* - DOM structure remains identical to authored component markup.
|
|
380
|
-
* - Runtime ownership is isolated per island instance.
|
|
381
|
-
*
|
|
382
|
-
* Generated script behavior:
|
|
383
|
-
* - resolves the component export by metadata (`componentRef`, `componentFile`)
|
|
384
|
-
* before falling back to default/first function export
|
|
385
|
-
* - selects island root using `targetSelector`
|
|
386
|
-
* - replaces the SSR host with a dedicated client-owned container
|
|
387
|
-
* - creates a fresh React root and renders with serialized `props`
|
|
388
|
-
*
|
|
389
|
-
* Why it remounts instead of hydrating:
|
|
390
|
-
* island SSR intentionally avoids synthetic wrapper elements. The runtime swaps
|
|
391
|
-
* the authored SSR node for a dedicated client-owned container before mounting
|
|
392
|
-
* so the server markup stays clean while the client still gets a stable root.
|
|
393
|
-
*
|
|
394
|
-
* @param options Island script generation options.
|
|
395
|
-
* @returns Browser-executable JavaScript module source.
|
|
396
|
-
*/
|
|
397
|
-
export function createIslandHydrationScript(options: IslandHydrationScriptOptions): string {
|
|
398
|
-
const targetSelector = JSON.stringify(options.targetSelector);
|
|
399
|
-
const componentRef = JSON.stringify(options.componentRef ?? '');
|
|
400
|
-
const componentFile = JSON.stringify(options.componentFile ?? '');
|
|
401
|
-
|
|
402
|
-
if (options.isDevelopment) {
|
|
403
|
-
return `
|
|
404
|
-
import { createRoot } from "${options.reactDomClientImportPath}";
|
|
405
|
-
import { createElement } from "${options.reactImportPath}";
|
|
406
|
-
import * as ComponentModule from "${options.importPath}";
|
|
407
|
-
|
|
408
|
-
const resolveComponent = () => {
|
|
409
|
-
const id = ${componentRef};
|
|
410
|
-
const file = ${componentFile};
|
|
411
|
-
const moduleValues = Object.values(ComponentModule);
|
|
412
|
-
|
|
413
|
-
const matchByMetadata = moduleValues.find((entry) => {
|
|
414
|
-
if (typeof entry !== "function") return false;
|
|
415
|
-
const config = entry.config;
|
|
416
|
-
const eco = config?.__eco;
|
|
417
|
-
if (!eco) return false;
|
|
418
|
-
if (id && eco.id === id) return true;
|
|
419
|
-
if (file && eco.file === file) return true;
|
|
420
|
-
return false;
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
if (matchByMetadata && typeof matchByMetadata === "function") {
|
|
424
|
-
return matchByMetadata;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const defaultExport = ComponentModule.default;
|
|
428
|
-
if (typeof defaultExport === "function") {
|
|
429
|
-
return defaultExport;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const firstFunction = moduleValues.find((entry) => typeof entry === "function");
|
|
433
|
-
return typeof firstFunction === "function" ? firstFunction : null;
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
const mount = () => {
|
|
437
|
-
const target = document.querySelector(${targetSelector});
|
|
438
|
-
const Component = resolveComponent();
|
|
439
|
-
if (!target || !Component) {
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
const props = JSON.parse(atob(target.getAttribute("data-eco-props") || "e30="));
|
|
443
|
-
const container = document.createElement("eco-island");
|
|
444
|
-
container.style.display = "block";
|
|
445
|
-
target.replaceWith(container);
|
|
446
|
-
const root = createRoot(container);
|
|
447
|
-
root.render(createElement(Component, props));
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
if (document.readyState === "loading") {
|
|
451
|
-
document.addEventListener("DOMContentLoaded", mount, { once: true });
|
|
452
|
-
} else {
|
|
453
|
-
mount();
|
|
454
|
-
}
|
|
455
|
-
`.trim();
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
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 t=document.querySelector(${targetSelector});if(!t||!c)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()`;
|
|
459
|
-
}
|