@ecopages/react 0.2.0-alpha.2 → 0.2.0-alpha.20

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 (66) hide show
  1. package/CHANGELOG.md +19 -39
  2. package/README.md +160 -18
  3. package/package.json +6 -6
  4. package/src/react-hmr-strategy.d.ts +26 -21
  5. package/src/react-hmr-strategy.js +91 -110
  6. package/src/react-renderer.d.ts +165 -41
  7. package/src/react-renderer.js +451 -158
  8. package/src/react.constants.d.ts +1 -0
  9. package/src/react.constants.js +4 -0
  10. package/src/react.plugin.d.ts +37 -108
  11. package/src/react.plugin.js +125 -54
  12. package/src/react.types.d.ts +88 -0
  13. package/src/react.types.js +0 -0
  14. package/src/router-adapter.d.ts +2 -2
  15. package/src/services/react-bundle.service.d.ts +4 -25
  16. package/src/services/react-bundle.service.js +39 -91
  17. package/src/services/react-hmr-page-metadata-cache.d.ts +9 -0
  18. package/src/services/react-hmr-page-metadata-cache.js +18 -2
  19. package/src/services/react-hydration-asset.service.d.ts +7 -6
  20. package/src/services/react-hydration-asset.service.js +29 -17
  21. package/src/services/react-mdx-config-dependency.service.d.ts +36 -0
  22. package/src/services/react-mdx-config-dependency.service.js +122 -0
  23. package/src/services/react-page-module.service.d.ts +8 -2
  24. package/src/services/react-page-module.service.js +44 -37
  25. package/src/services/react-page-payload.service.d.ts +46 -0
  26. package/src/services/react-page-payload.service.js +67 -0
  27. package/src/services/react-runtime-bundle.service.d.ts +14 -12
  28. package/src/services/react-runtime-bundle.service.js +103 -180
  29. package/src/utils/client-graph-boundary-plugin.js +149 -11
  30. package/src/utils/component-config-traversal.d.ts +36 -0
  31. package/src/utils/component-config-traversal.js +54 -0
  32. package/src/utils/declared-modules.d.ts +1 -1
  33. package/src/utils/declared-modules.js +7 -16
  34. package/src/utils/dynamic.test.browser.d.ts +1 -0
  35. package/src/utils/dynamic.test.browser.js +33 -0
  36. package/src/utils/hydration-scripts.d.ts +19 -4
  37. package/src/utils/hydration-scripts.js +102 -39
  38. package/src/utils/hydration-scripts.test.browser.d.ts +1 -0
  39. package/src/utils/hydration-scripts.test.browser.js +126 -0
  40. package/src/utils/reachability-analyzer.d.ts +12 -1
  41. package/src/utils/reachability-analyzer.js +101 -5
  42. package/src/utils/react-dom-runtime-interop-plugin.d.ts +5 -0
  43. package/src/utils/react-dom-runtime-interop-plugin.js +29 -0
  44. package/src/utils/react-mdx-loader-plugin.js +13 -5
  45. package/src/utils/react-runtime-specifier-map.d.ts +6 -0
  46. package/src/utils/react-runtime-specifier-map.js +37 -0
  47. package/src/utils/use-sync-external-store-shim-plugin.d.ts +5 -0
  48. package/src/utils/use-sync-external-store-shim-plugin.js +41 -0
  49. package/src/react-hmr-strategy.ts +0 -444
  50. package/src/react-renderer.ts +0 -403
  51. package/src/react.plugin.ts +0 -241
  52. package/src/router-adapter.ts +0 -95
  53. package/src/services/react-bundle.service.ts +0 -212
  54. package/src/services/react-hmr-page-metadata-cache.ts +0 -24
  55. package/src/services/react-hydration-asset.service.ts +0 -260
  56. package/src/services/react-page-module.service.ts +0 -214
  57. package/src/services/react-runtime-bundle.service.ts +0 -271
  58. package/src/utils/client-graph-boundary-plugin.ts +0 -590
  59. package/src/utils/client-only.ts +0 -27
  60. package/src/utils/declared-modules.ts +0 -99
  61. package/src/utils/dynamic.ts +0 -27
  62. package/src/utils/hmr-scripts.ts +0 -47
  63. package/src/utils/html-boundary.ts +0 -66
  64. package/src/utils/hydration-scripts.ts +0 -338
  65. package/src/utils/reachability-analyzer.ts +0 -440
  66. package/src/utils/react-mdx-loader-plugin.ts +0 -40
@@ -9,6 +9,42 @@ 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
+ }
12
48
  function createDevScriptWithRouter(options) {
13
49
  const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath } = options;
14
50
  const { components, getRouterProps } = router;
@@ -21,10 +57,13 @@ import { createElement } from "${reactImportPath}";
21
57
  import { ${components.router}, ${components.pageContent} } from "${routerImportPath}";
22
58
  ${getImportStatement(importPath, isMdx)}
23
59
 
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;
60
+ window.__ECO_PAGES__ = window.__ECO_PAGES__ || {};
61
+ window.__ECO_PAGES__.hmrHandlers = window.__ECO_PAGES__.hmrHandlers || {};
62
+ window.__ECO_PAGES__.react = window.__ECO_PAGES__.react || {};
63
+ window.__ECO_PAGES__.react.pageRoot = window.__ECO_PAGES__.react.pageRoot || null;
64
+ let root = window.__ECO_PAGES__.react.pageRoot;
65
+ ${getDevPageRootCleanupScript()}
66
+ ${getDevRouterBootstrapRegistrationScript()}
28
67
 
29
68
  const getPageData = () => {
30
69
  const el = document.getElementById("__ECO_PAGE_DATA__");
@@ -36,7 +75,7 @@ const getPageData = () => {
36
75
 
37
76
  const props = getPageData();
38
77
 
39
- window.__ECO_PAGE__ = {
78
+ window.__ECO_PAGES__.page = {
40
79
  module: "${importPath}",
41
80
  props
42
81
  };
@@ -47,21 +86,30 @@ const createTree = (Component, props) => {
47
86
  };
48
87
 
49
88
  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
- }
89
+ if (window.__ECO_PAGES__.react?.pageRoot) {
90
+ root = window.__ECO_PAGES__.react.pageRoot;
91
+ root.render(createTree(Page, props));
92
+ } else {
93
+ root = hydrateRoot(document.body, createTree(Page, props), {
94
+ onRecoverableError: (err) => console.warn("[ecopages] Hydration error:", err)
95
+ });
96
+ window.__ECO_PAGES__.react.pageRoot = root;
97
+ }
98
+ window.__ECO_PAGES__.hmrHandlers["${importPath}"] = async (newUrl) => {
60
99
  try {
61
100
  const newModule = await import(newUrl);
101
+ const nextProps = getPageData();
62
102
  ${getHmrImportStatement(isMdx)}
63
- root.render(createTree(NewPage, props));
64
- console.log("[ecopages] ${getComponentType(isMdx)} component updated");
103
+ window.__ECO_PAGES__.page = {
104
+ module: "${importPath}",
105
+ props: nextProps
106
+ };
107
+ root.render(createTree(NewPage, nextProps));
108
+ if (window.__ECO_PAGES__?.navigation?.getOwnerState().owner === "react-router") {
109
+ console.log("[ecopages] ${getComponentType(isMdx)} component updated via router");
110
+ } else {
111
+ console.log("[ecopages] ${getComponentType(isMdx)} component updated");
112
+ }
65
113
  } catch (e) {
66
114
  console.error("[ecopages] Failed to hot-reload ${getComponentType(isMdx)} component:", e);
67
115
  }
@@ -82,8 +130,12 @@ import { hydrateRoot } from "${reactDomClientImportPath}";
82
130
  import { createElement } from "${reactImportPath}";
83
131
  ${getImportStatement(importPath, isMdx)}
84
132
 
85
- window.__ecopages_hmr_handlers__ = window.__ecopages_hmr_handlers__ || {};
86
- let root = null;
133
+ window.__ECO_PAGES__ = window.__ECO_PAGES__ || {};
134
+ window.__ECO_PAGES__.hmrHandlers = window.__ECO_PAGES__.hmrHandlers || {};
135
+ window.__ECO_PAGES__.react = window.__ECO_PAGES__.react || {};
136
+ window.__ECO_PAGES__.react.pageRoot = window.__ECO_PAGES__.react.pageRoot || null;
137
+ let root = window.__ECO_PAGES__.react.pageRoot;
138
+ ${getDevPageRootCleanupScript()}
87
139
 
88
140
  const getPageData = () => {
89
141
  const el = document.getElementById("__ECO_PAGE_DATA__");
@@ -95,7 +147,7 @@ const getPageData = () => {
95
147
 
96
148
  const props = getPageData();
97
149
 
98
- window.__ECO_PAGE__ = {
150
+ window.__ECO_PAGES__.page = {
99
151
  module: "${importPath}",
100
152
  props
101
153
  };
@@ -103,14 +155,21 @@ window.__ECO_PAGE__ = {
103
155
  const createTree = (Component, props) => {
104
156
  const Layout = Component.config?.layout;
105
157
  const pageElement = createElement(Component, props);
106
- return Layout ? createElement(Layout, null, pageElement) : pageElement;
158
+ const layoutProps = props?.locals ? { locals: props.locals } : null;
159
+ return Layout ? createElement(Layout, layoutProps, pageElement) : pageElement;
107
160
  };
108
161
 
109
162
  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) => {
163
+ if (window.__ECO_PAGES__.react?.pageRoot) {
164
+ root = window.__ECO_PAGES__.react.pageRoot;
165
+ root.render(createTree(Page, props));
166
+ } else {
167
+ root = hydrateRoot(document.body, createTree(Page, props), {
168
+ onRecoverableError: (err) => console.warn("[ecopages] Hydration error:", err)
169
+ });
170
+ window.__ECO_PAGES__.react.pageRoot = root;
171
+ }
172
+ window.__ECO_PAGES__.hmrHandlers["${importPath}"] = async (newUrl) => {
114
173
  try {
115
174
  const newModule = await import(newUrl);
116
175
  ${getHmrImportStatement(isMdx)}
@@ -136,16 +195,16 @@ function createProdScriptWithRouter(options) {
136
195
  throw new Error("routerImportPath is required when router adapter is configured");
137
196
  }
138
197
  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()`;
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:"${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()`;
140
199
  }
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()`;
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:"${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()`;
142
201
  }
143
202
  function createProdScriptWithoutRouter(options) {
144
203
  const { importPath, isMdx, reactImportPath, reactDomClientImportPath } = options;
145
204
  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()`;
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:"${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()`;
147
206
  }
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()`;
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:"${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()`;
149
208
  }
150
209
  function createHydrationScript(options) {
151
210
  const { isDevelopment, router } = options;
@@ -156,10 +215,8 @@ function createHydrationScript(options) {
156
215
  }
157
216
  function createIslandHydrationScript(options) {
158
217
  const targetSelector = JSON.stringify(options.targetSelector);
159
- const serializedProps = JSON.stringify(options.props ?? {});
160
218
  const componentRef = JSON.stringify(options.componentRef ?? "");
161
219
  const componentFile = JSON.stringify(options.componentFile ?? "");
162
- const mountedAttribute = "data-eco-react-mounted";
163
220
  if (options.isDevelopment) {
164
221
  return `
165
222
  import { createRoot } from "${options.reactDomClientImportPath}";
@@ -195,18 +252,24 @@ const resolveComponent = () => {
195
252
  };
196
253
 
197
254
  const mount = () => {
198
- const target = document.querySelector(${targetSelector});
255
+ const targets = document.querySelectorAll(${targetSelector});
199
256
  const Component = resolveComponent();
200
- if (!target || !Component || target.hasAttribute("${mountedAttribute}")) {
257
+ if (!Component || targets.length === 0) {
201
258
  return;
202
259
  }
203
- const props = ${serializedProps};
204
- target.setAttribute("${mountedAttribute}", "true");
205
- const root = createRoot(target);
206
- root.render(createElement(Component, props));
260
+ targets.forEach((target) => {
261
+ if (!(target instanceof HTMLElement)) {
262
+ return;
263
+ }
264
+ const props = JSON.parse(atob(target.getAttribute("data-eco-props") || "e30="));
265
+ const container = document.createElement("eco-island");
266
+ container.style.display = "block";
267
+ target.replaceWith(container);
268
+ const root = createRoot(container);
269
+ root.render(createElement(Component, props));
270
+ });
207
271
  };
208
272
 
209
- document.addEventListener("eco:after-swap", mount);
210
273
  if (document.readyState === "loading") {
211
274
  document.addEventListener("DOMContentLoaded", mount, { once: true });
212
275
  } else {
@@ -214,7 +277,7 @@ if (document.readyState === "loading") {
214
277
  }
215
278
  `.trim();
216
279
  }
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()`;
280
+ return `import{createRoot as cr}from"${options.reactDomClientImportPath}";import{createElement as ce}from"${options.reactImportPath}";import*as M from"${options.importPath}";const r=${componentRef};const f=${componentFile};const mv=Object.values(M);const c=mv.find((e)=>{if(typeof e!=="function")return false;const ec=e.config?.__eco;if(!ec)return false;if(r&&ec.id===r)return true;if(f&&ec.file===f)return true;return false;})??(typeof M.default==="function"?M.default:mv.find((e)=>typeof e==="function")??null);const m=()=>{const ts=document.querySelectorAll(${targetSelector});if(!c||ts.length===0)return;ts.forEach((t)=>{if(!(t instanceof HTMLElement))return;const p=JSON.parse(atob(t.getAttribute("data-eco-props")||"e30="));const ct=document.createElement("eco-island");ct.style.display="block";t.replaceWith(ct);cr(ct).render(ce(c,p))})};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m,{once:true}):m()`;
218
281
  }
219
282
  export {
220
283
  createHydrationScript,
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,126 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import { createHydrationScript } from "./hydration-scripts.js";
3
+ const routerAdapter = {
4
+ name: "eco-router",
5
+ bundle: {
6
+ importPath: "/assets/router.js",
7
+ outputName: "router",
8
+ externals: []
9
+ },
10
+ importMapKey: "@ecopages/react-router",
11
+ components: {
12
+ router: "EcoRouter",
13
+ pageContent: "PageContent"
14
+ },
15
+ getRouterProps: (page, props) => `{ page: ${page}, pageProps: ${props} }`
16
+ };
17
+ function createModuleUrl(source) {
18
+ return `data:text/javascript;base64,${btoa(source)}`;
19
+ }
20
+ async function importModule(moduleUrl) {
21
+ await import(
22
+ /* @vite-ignore */
23
+ moduleUrl
24
+ );
25
+ }
26
+ function createRuntimeModules() {
27
+ const reactImportPath = createModuleUrl("export const createElement = (...args) => ({ args });");
28
+ const reactDomClientImportPath = createModuleUrl(`
29
+ export const hydrateRoot = (container, tree, options) => {
30
+ const runtime = window.__ECO_REACT_HYDRATION_TEST__;
31
+ runtime.hydrateCalls.push({
32
+ containerTag: container.tagName,
33
+ hasRecoverableErrorHandler: typeof options?.onRecoverableError === "function",
34
+ tree,
35
+ });
36
+
37
+ return {
38
+ render() {},
39
+ unmount() {
40
+ runtime.unmountCount += 1;
41
+ },
42
+ };
43
+ };
44
+ `);
45
+ const importPath = createModuleUrl("export default function Page() { return null; }");
46
+ const routerImportPath = createModuleUrl(`
47
+ export function EcoRouter(props) {
48
+ return props;
49
+ }
50
+
51
+ export function PageContent() {
52
+ return null;
53
+ }
54
+ `);
55
+ return {
56
+ importPath,
57
+ reactImportPath,
58
+ reactDomClientImportPath,
59
+ routerImportPath
60
+ };
61
+ }
62
+ describe("createHydrationScript browser execution", () => {
63
+ afterEach(() => {
64
+ document.body.innerHTML = "";
65
+ const testWindow = window;
66
+ delete testWindow.__ECO_PAGES__;
67
+ delete testWindow.__ECO_REACT_HYDRATION_TEST__;
68
+ });
69
+ it("registers router ownership and cleanup when the browser hydration bootstrap runs", async () => {
70
+ const runtimeModules = createRuntimeModules();
71
+ const testWindow = window;
72
+ testWindow.__ECO_REACT_HYDRATION_TEST__ = {
73
+ hydrateCalls: [],
74
+ claimedOwners: [],
75
+ releasedOwners: [],
76
+ registrations: [],
77
+ unmountCount: 0
78
+ };
79
+ testWindow.__ECO_PAGES__ = {
80
+ navigation: {
81
+ getOwnerState: () => ({
82
+ owner: "html",
83
+ canHandleSpaNavigation: false
84
+ }),
85
+ register: (registration) => {
86
+ testWindow.__ECO_REACT_HYDRATION_TEST__?.registrations.push(registration);
87
+ },
88
+ claimOwnership: (owner) => {
89
+ testWindow.__ECO_REACT_HYDRATION_TEST__?.claimedOwners.push(owner);
90
+ },
91
+ releaseOwnership: (owner) => {
92
+ testWindow.__ECO_REACT_HYDRATION_TEST__?.releasedOwners.push(owner);
93
+ }
94
+ }
95
+ };
96
+ document.body.innerHTML = `<script id="__ECO_PAGE_DATA__" type="application/json">${JSON.stringify({
97
+ title: "Hello React",
98
+ locals: { theme: "dark" }
99
+ })}<\/script>`;
100
+ const script = createHydrationScript({
101
+ ...runtimeModules,
102
+ isDevelopment: true,
103
+ isMdx: false,
104
+ router: routerAdapter
105
+ });
106
+ await importModule(createModuleUrl(script));
107
+ expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.hydrateCalls).toHaveLength(1);
108
+ expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.hydrateCalls[0]?.containerTag).toBe("BODY");
109
+ expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.hydrateCalls[0]?.hasRecoverableErrorHandler).toBe(true);
110
+ expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.claimedOwners).toEqual(["react-router"]);
111
+ expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.registrations).toHaveLength(1);
112
+ expect(typeof testWindow.__ECO_PAGES__?.react?.cleanupPageRoot).toBe("function");
113
+ expect(testWindow.__ECO_PAGES__?.page).toEqual({
114
+ module: runtimeModules.importPath,
115
+ props: {
116
+ title: "Hello React",
117
+ locals: { theme: "dark" }
118
+ }
119
+ });
120
+ await testWindow.__ECO_PAGES__?.react?.cleanupPageRoot?.();
121
+ expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.unmountCount).toBe(1);
122
+ expect(testWindow.__ECO_REACT_HYDRATION_TEST__?.releasedOwners).toEqual(["react-router"]);
123
+ expect(testWindow.__ECO_PAGES__?.page).toBeUndefined();
124
+ expect(testWindow.__ECO_PAGES__?.react?.pageRoot).toBeNull();
125
+ });
126
+ });
@@ -42,6 +42,15 @@ export type ReachabilityResult = {
42
42
  */
43
43
  analyzed: boolean;
44
44
  };
45
+ /**
46
+ * Optional export filter supplied by the client graph boundary when a local
47
+ * module is imported through a narrower named-export surface.
48
+ *
49
+ * `'*'` means the whole module namespace is considered reachable, while a
50
+ * `Set` restricts analysis to the named exports that are actually requested by
51
+ * downstream client-reachable modules.
52
+ */
53
+ type ExplicitlyRequestedExports = Set<string> | '*';
45
54
  /**
46
55
  * Analyzes a module using Oxc AST and extracts a strict reachability graph
47
56
  * starting from client roots (`render`, `errorBoundary`, `loadingFallback` of `eco.page` or `eco.component`).
@@ -50,6 +59,8 @@ export type ReachabilityResult = {
50
59
  * @param filename - Absolute or relative path to the module file.
51
60
  * @param program - Optional pre-parsed Oxc program AST. When supplied, the
52
61
  * internal `parseSync` call is skipped entirely (avoids double-parsing).
62
+ * @param explicitlyRequestedExports - Optional named export filter propagated
63
+ * from a downstream importer when this module is only partially reachable.
53
64
  */
54
- export declare function analyzeReachability(source: string, filename: string, program?: ReturnType<typeof parseSync>['program']): ReachabilityResult;
65
+ export declare function analyzeReachability(source: string, filename: string, program?: ReturnType<typeof parseSync>['program'], explicitlyRequestedExports?: ExplicitlyRequestedExports): ReachabilityResult;
55
66
  export {};
@@ -7,7 +7,7 @@ function parserLanguageForFile(filename) {
7
7
  if (extension === ".jsx") return "jsx";
8
8
  return "js";
9
9
  }
10
- function analyzeReachability(source, filename, program) {
10
+ function analyzeReachability(source, filename, program, explicitlyRequestedExports) {
11
11
  let resolvedProgram;
12
12
  if (program) {
13
13
  resolvedProgram = program;
@@ -114,12 +114,86 @@ function analyzeReachability(source, filename, program) {
114
114
  potentialClientRoots.push(node);
115
115
  }
116
116
  }
117
+ function getExportedName(specifier) {
118
+ if (specifier?.exported?.type === "Identifier") return specifier.exported.name;
119
+ if (typeof specifier?.exported?.value === "string") return specifier.exported.value;
120
+ if (specifier?.local?.type === "Identifier") return specifier.local.name;
121
+ if (typeof specifier?.local?.value === "string") return specifier.local.value;
122
+ return void 0;
123
+ }
124
+ function getReexportedImportName(specifier) {
125
+ if (specifier?.local?.type === "Identifier") return specifier.local.name;
126
+ if (typeof specifier?.local?.value === "string") return specifier.local.value;
127
+ if (specifier?.imported?.type === "Identifier") return specifier.imported.name;
128
+ if (typeof specifier?.imported?.value === "string") return specifier.imported.value;
129
+ return getExportedName(specifier);
130
+ }
131
+ function getLocalExportName(specifier) {
132
+ if (specifier?.local?.type === "Identifier") return specifier.local.name;
133
+ if (typeof specifier?.local?.value === "string") return specifier.local.value;
134
+ return void 0;
135
+ }
136
+ function isExplicitlyRequestedExport(name) {
137
+ if (explicitlyRequestedExports === "*") return true;
138
+ return explicitlyRequestedExports?.has(name) ?? false;
139
+ }
117
140
  let isFallbackRoots = false;
118
141
  if (potentialClientRoots.length === 0) {
119
- isFallbackRoots = true;
120
- for (const node of resolvedProgram.body) {
121
- if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration" || node.type === "ExportAllDeclaration") {
122
- potentialClientRoots.push(node);
142
+ if (explicitlyRequestedExports) {
143
+ for (const node of resolvedProgram.body) {
144
+ if (node.type === "ExportNamedDeclaration") {
145
+ const exportNode = node;
146
+ if (exportNode.source && exportNode.specifiers?.length) {
147
+ const hasRequestedReexport = exportNode.specifiers.some((specifier) => {
148
+ const exportedName = getExportedName(specifier);
149
+ return exportedName ? isExplicitlyRequestedExport(exportedName) : false;
150
+ });
151
+ if (hasRequestedReexport) {
152
+ potentialClientRoots.push(node);
153
+ }
154
+ continue;
155
+ }
156
+ if (exportNode.declaration?.type === "FunctionDeclaration" || exportNode.declaration?.type === "ClassDeclaration") {
157
+ const declarationName = exportNode.declaration.id?.name;
158
+ if (declarationName && isExplicitlyRequestedExport(declarationName)) {
159
+ potentialClientRoots.push(node);
160
+ }
161
+ continue;
162
+ }
163
+ if (exportNode.declaration?.type === "VariableDeclaration") {
164
+ const hasRequestedDeclaration = exportNode.declaration.declarations.some(
165
+ (declaration) => declaration.id?.type === "Identifier" && isExplicitlyRequestedExport(declaration.id.name)
166
+ );
167
+ if (hasRequestedDeclaration) {
168
+ potentialClientRoots.push(node);
169
+ }
170
+ continue;
171
+ }
172
+ if (exportNode.specifiers?.length) {
173
+ const hasRequestedSpecifier = exportNode.specifiers.some((specifier) => {
174
+ const exportedName = getExportedName(specifier);
175
+ return exportedName ? isExplicitlyRequestedExport(exportedName) : false;
176
+ });
177
+ if (hasRequestedSpecifier) {
178
+ potentialClientRoots.push(node);
179
+ }
180
+ }
181
+ } else if (node.type === "ExportDefaultDeclaration") {
182
+ if (isExplicitlyRequestedExport("default")) {
183
+ potentialClientRoots.push(node);
184
+ }
185
+ } else if (node.type === "ExportAllDeclaration") {
186
+ if (explicitlyRequestedExports === "*") {
187
+ potentialClientRoots.push(node);
188
+ }
189
+ }
190
+ }
191
+ } else {
192
+ isFallbackRoots = true;
193
+ for (const node of resolvedProgram.body) {
194
+ if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration" || node.type === "ExportAllDeclaration") {
195
+ potentialClientRoots.push(node);
196
+ }
123
197
  }
124
198
  }
125
199
  }
@@ -167,6 +241,28 @@ function analyzeReachability(source, filename, program) {
167
241
  markImportReachable(node.source.value, "*");
168
242
  return;
169
243
  }
244
+ if (node.type === "ExportNamedDeclaration" && typeof node.source?.value === "string") {
245
+ for (const specifier of node.specifiers ?? []) {
246
+ const importedName = getReexportedImportName(specifier);
247
+ if (importedName) {
248
+ markImportReachable(node.source.value, importedName);
249
+ }
250
+ }
251
+ return;
252
+ }
253
+ if (node.type === "ExportNamedDeclaration" && !node.source && explicitlyRequestedExports && node.specifiers?.length) {
254
+ for (const specifier of node.specifiers) {
255
+ const exportedName = getExportedName(specifier);
256
+ if (!exportedName || !isExplicitlyRequestedExport(exportedName)) {
257
+ continue;
258
+ }
259
+ const localName = getLocalExportName(specifier);
260
+ if (localName && !currentScope.has(localName)) {
261
+ checkIdentifier(localName);
262
+ }
263
+ }
264
+ return;
265
+ }
170
266
  if (node.type === "Identifier" || node.type === "JSXIdentifier" && /^[A-Z]/.test(node.name)) {
171
267
  if (!currentScope.has(node.name)) {
172
268
  checkIdentifier(node.name);
@@ -0,0 +1,5 @@
1
+ import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
2
+ export declare function createReactDomRuntimeInteropPlugin(options?: {
3
+ name?: string;
4
+ reactSpecifier?: string;
5
+ }): EcoBuildPlugin;
@@ -0,0 +1,29 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ function createReactDomRuntimeInteropPlugin(options) {
4
+ const reactDomFileFilter = /[\\/]react-dom[\\/].*\.js$/;
5
+ const reactRequirePattern = /\brequire\((['"])react\1\)/g;
6
+ const reactSpecifier = options?.reactSpecifier ?? "react";
7
+ return {
8
+ name: options?.name ?? "react-dom-runtime-interop",
9
+ setup(build) {
10
+ build.onLoad({ filter: reactDomFileFilter }, (args) => {
11
+ const content = fs.readFileSync(args.path, "utf-8");
12
+ if (!reactRequirePattern.test(content)) {
13
+ return void 0;
14
+ }
15
+ reactRequirePattern.lastIndex = 0;
16
+ const rewritten = content.replace(reactRequirePattern, "__ecopages_react_runtime");
17
+ return {
18
+ contents: `import * as __ecopages_react_runtime from '${reactSpecifier}';
19
+ ${rewritten}`,
20
+ loader: "js",
21
+ resolveDir: path.dirname(args.path)
22
+ };
23
+ });
24
+ }
25
+ };
26
+ }
27
+ export {
28
+ createReactDomRuntimeInteropPlugin
29
+ };
@@ -1,11 +1,18 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { compile } from "@mdx-js/mdx";
4
- import { SourceMapGenerator } from "source-map";
4
+ import sourceMap from "source-map";
5
5
  import { VFile } from "vfile";
6
+ function resolveCompileFormat(filePath, compilerOptions) {
7
+ const configuredFormat = compilerOptions?.format;
8
+ if (configuredFormat && configuredFormat !== "detect") {
9
+ return configuredFormat;
10
+ }
11
+ return path.extname(filePath).toLowerCase() === ".md" ? "mdx" : configuredFormat;
12
+ }
6
13
  function createReactMdxLoaderPlugin(compilerOptions) {
7
14
  const mdxExtensions = compilerOptions?.mdxExtensions ?? [".mdx"];
8
- const mdExtensions = compilerOptions?.mdExtensions ?? [".md"];
15
+ const mdExtensions = compilerOptions?.mdExtensions ?? [];
9
16
  const allExtensions = [...mdxExtensions, ...mdExtensions];
10
17
  const escapedExts = allExtensions.map((ext) => ext.replace(".", "\\."));
11
18
  const filter = new RegExp(`(${escapedExts.join("|")})(\\?.*)?$`);
@@ -18,13 +25,14 @@ function createReactMdxLoaderPlugin(compilerOptions) {
18
25
  const file = new VFile({ path: filePath, value: source });
19
26
  const compiled = await compile(file, {
20
27
  ...compilerOptions,
21
- SourceMapGenerator
28
+ format: resolveCompileFormat(filePath, compilerOptions),
29
+ SourceMapGenerator: sourceMap.SourceMapGenerator
22
30
  });
23
- const sourceMap = compiled.map ? `
31
+ const inlineSourceMap = compiled.map ? `
24
32
  //# sourceMappingURL=data:application/json;base64,${Buffer.from(JSON.stringify(compiled.map)).toString("base64")}
25
33
  ` : "";
26
34
  return {
27
- contents: `${String(compiled.value)}${sourceMap}`,
35
+ contents: `${String(compiled.value)}${inlineSourceMap}`,
28
36
  loader: compilerOptions?.jsx ? "jsx" : "js",
29
37
  resolveDir: path.dirname(args.path)
30
38
  };
@@ -0,0 +1,6 @@
1
+ import type { ReactRouterAdapter } from '../router-adapter.js';
2
+ import type { ReactRuntimeImports } from '../services/react-runtime-bundle.service.js';
3
+ export declare const REACT_RUNTIME_SPECIFIERS: readonly ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime", "react-dom/client"];
4
+ export declare function buildReactRuntimeSpecifierMap(runtimeImports: ReactRuntimeImports, routerAdapter?: ReactRouterAdapter): Record<string, string>;
5
+ export declare function getReactRuntimeExternalSpecifiers(): string[];
6
+ export declare function getReactClientGraphAllowSpecifiers(runtimeSpecifiers: Iterable<string>, routerAdapter?: ReactRouterAdapter): string[];
@@ -0,0 +1,37 @@
1
+ const REACT_RUNTIME_SPECIFIERS = [
2
+ "react",
3
+ "react-dom",
4
+ "react/jsx-runtime",
5
+ "react/jsx-dev-runtime",
6
+ "react-dom/client"
7
+ ];
8
+ function buildReactRuntimeSpecifierMap(runtimeImports, routerAdapter) {
9
+ const map = {
10
+ react: runtimeImports.react,
11
+ "react/jsx-runtime": runtimeImports.reactJsxRuntime,
12
+ "react/jsx-dev-runtime": runtimeImports.reactJsxDevRuntime,
13
+ "react-dom": runtimeImports.reactDom,
14
+ "react-dom/client": runtimeImports.reactDomClient
15
+ };
16
+ if (routerAdapter && runtimeImports.router) {
17
+ map[routerAdapter.importMapKey] = runtimeImports.router;
18
+ }
19
+ return map;
20
+ }
21
+ function getReactRuntimeExternalSpecifiers() {
22
+ return [...REACT_RUNTIME_SPECIFIERS];
23
+ }
24
+ function getReactClientGraphAllowSpecifiers(runtimeSpecifiers, routerAdapter) {
25
+ return [
26
+ "@ecopages/core",
27
+ ...REACT_RUNTIME_SPECIFIERS,
28
+ ...routerAdapter ? [routerAdapter.importMapKey] : [],
29
+ ...Array.from(runtimeSpecifiers)
30
+ ];
31
+ }
32
+ export {
33
+ REACT_RUNTIME_SPECIFIERS,
34
+ buildReactRuntimeSpecifierMap,
35
+ getReactClientGraphAllowSpecifiers,
36
+ getReactRuntimeExternalSpecifiers
37
+ };