@ecopages/react 0.2.0-alpha.5 → 0.2.0-alpha.51

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