@ecopages/react 0.2.0-alpha.1

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 (59) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/LICENSE +21 -0
  3. package/README.md +65 -0
  4. package/package.json +76 -0
  5. package/src/declarations.d.ts +6 -0
  6. package/src/react-hmr-strategy.d.ts +143 -0
  7. package/src/react-hmr-strategy.js +332 -0
  8. package/src/react-hmr-strategy.ts +444 -0
  9. package/src/react-renderer.d.ts +106 -0
  10. package/src/react-renderer.js +302 -0
  11. package/src/react-renderer.ts +403 -0
  12. package/src/react.plugin.d.ts +147 -0
  13. package/src/react.plugin.js +126 -0
  14. package/src/react.plugin.ts +241 -0
  15. package/src/router-adapter.d.ts +87 -0
  16. package/src/router-adapter.js +0 -0
  17. package/src/router-adapter.ts +95 -0
  18. package/src/services/react-bundle.service.d.ts +68 -0
  19. package/src/services/react-bundle.service.js +145 -0
  20. package/src/services/react-bundle.service.ts +212 -0
  21. package/src/services/react-hmr-page-metadata-cache.d.ts +17 -0
  22. package/src/services/react-hmr-page-metadata-cache.js +19 -0
  23. package/src/services/react-hmr-page-metadata-cache.ts +24 -0
  24. package/src/services/react-hydration-asset.service.d.ts +75 -0
  25. package/src/services/react-hydration-asset.service.js +198 -0
  26. package/src/services/react-hydration-asset.service.ts +260 -0
  27. package/src/services/react-page-module.service.d.ts +80 -0
  28. package/src/services/react-page-module.service.js +155 -0
  29. package/src/services/react-page-module.service.ts +214 -0
  30. package/src/services/react-runtime-bundle.service.d.ts +38 -0
  31. package/src/services/react-runtime-bundle.service.js +207 -0
  32. package/src/services/react-runtime-bundle.service.ts +271 -0
  33. package/src/utils/client-graph-boundary-plugin.d.ts +43 -0
  34. package/src/utils/client-graph-boundary-plugin.js +356 -0
  35. package/src/utils/client-graph-boundary-plugin.ts +590 -0
  36. package/src/utils/client-only.d.ts +8 -0
  37. package/src/utils/client-only.js +19 -0
  38. package/src/utils/client-only.ts +27 -0
  39. package/src/utils/declared-modules.d.ts +42 -0
  40. package/src/utils/declared-modules.js +56 -0
  41. package/src/utils/declared-modules.ts +99 -0
  42. package/src/utils/dynamic.d.ts +15 -0
  43. package/src/utils/dynamic.js +12 -0
  44. package/src/utils/dynamic.ts +27 -0
  45. package/src/utils/hmr-scripts.d.ts +18 -0
  46. package/src/utils/hmr-scripts.js +31 -0
  47. package/src/utils/hmr-scripts.ts +47 -0
  48. package/src/utils/html-boundary.d.ts +7 -0
  49. package/src/utils/html-boundary.js +55 -0
  50. package/src/utils/html-boundary.ts +66 -0
  51. package/src/utils/hydration-scripts.d.ts +71 -0
  52. package/src/utils/hydration-scripts.js +222 -0
  53. package/src/utils/hydration-scripts.ts +338 -0
  54. package/src/utils/reachability-analyzer.d.ts +55 -0
  55. package/src/utils/reachability-analyzer.js +243 -0
  56. package/src/utils/reachability-analyzer.ts +440 -0
  57. package/src/utils/react-mdx-loader-plugin.d.ts +3 -0
  58. package/src/utils/react-mdx-loader-plugin.js +37 -0
  59. package/src/utils/react-mdx-loader-plugin.ts +40 -0
@@ -0,0 +1,222 @@
1
+ function getImportStatement(importPath, isMdx) {
2
+ return isMdx ? `import * as MDXModule from "${importPath}";
3
+ const Page = MDXModule.default;
4
+ if (MDXModule.config) Page.config = MDXModule.config;` : `import Page from "${importPath}";`;
5
+ }
6
+ function getHmrImportStatement(isMdx) {
7
+ return isMdx ? "const NewPage = newModule.default; if (newModule.config) NewPage.config = newModule.config;" : "const NewPage = newModule.default;";
8
+ }
9
+ function getComponentType(isMdx) {
10
+ return isMdx ? "MDX" : "React";
11
+ }
12
+ function createDevScriptWithRouter(options) {
13
+ const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath } = options;
14
+ const { components, getRouterProps } = router;
15
+ if (!routerImportPath) {
16
+ throw new Error("routerImportPath is required when router adapter is configured");
17
+ }
18
+ return `
19
+ import { hydrateRoot } from "${reactDomClientImportPath}";
20
+ import { createElement } from "${reactImportPath}";
21
+ import { ${components.router}, ${components.pageContent} } from "${routerImportPath}";
22
+ ${getImportStatement(importPath, isMdx)}
23
+
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;
28
+
29
+ const getPageData = () => {
30
+ const el = document.getElementById("__ECO_PAGE_DATA__");
31
+ if (el?.textContent) {
32
+ try { return JSON.parse(el.textContent); } catch {}
33
+ }
34
+ return {};
35
+ };
36
+
37
+ const props = getPageData();
38
+
39
+ window.__ECO_PAGE__ = {
40
+ module: "${importPath}",
41
+ props
42
+ };
43
+
44
+ const createTree = (Component, props) => {
45
+ const pageContent = createElement(${components.pageContent});
46
+ return createElement(${components.router}, ${getRouterProps("Component", "props")}, pageContent);
47
+ };
48
+
49
+ 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
+ }
60
+ try {
61
+ const newModule = await import(newUrl);
62
+ ${getHmrImportStatement(isMdx)}
63
+ root.render(createTree(NewPage, props));
64
+ console.log("[ecopages] ${getComponentType(isMdx)} component updated");
65
+ } catch (e) {
66
+ console.error("[ecopages] Failed to hot-reload ${getComponentType(isMdx)} component:", e);
67
+ }
68
+ };
69
+ };
70
+
71
+ if (document.readyState === "loading") {
72
+ document.addEventListener("DOMContentLoaded", mount);
73
+ } else {
74
+ mount();
75
+ }
76
+ `.trim();
77
+ }
78
+ function createDevScriptWithoutRouter(options) {
79
+ const { importPath, isMdx, reactImportPath, reactDomClientImportPath } = options;
80
+ return `
81
+ import { hydrateRoot } from "${reactDomClientImportPath}";
82
+ import { createElement } from "${reactImportPath}";
83
+ ${getImportStatement(importPath, isMdx)}
84
+
85
+ window.__ecopages_hmr_handlers__ = window.__ecopages_hmr_handlers__ || {};
86
+ let root = null;
87
+
88
+ const getPageData = () => {
89
+ const el = document.getElementById("__ECO_PAGE_DATA__");
90
+ if (el?.textContent) {
91
+ try { return JSON.parse(el.textContent); } catch {}
92
+ }
93
+ return {};
94
+ };
95
+
96
+ const props = getPageData();
97
+
98
+ window.__ECO_PAGE__ = {
99
+ module: "${importPath}",
100
+ props
101
+ };
102
+
103
+ const createTree = (Component, props) => {
104
+ const Layout = Component.config?.layout;
105
+ const pageElement = createElement(Component, props);
106
+ return Layout ? createElement(Layout, null, pageElement) : pageElement;
107
+ };
108
+
109
+ 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) => {
114
+ try {
115
+ const newModule = await import(newUrl);
116
+ ${getHmrImportStatement(isMdx)}
117
+ root.render(createTree(NewPage, props));
118
+ console.log("[ecopages] ${getComponentType(isMdx)} component updated");
119
+ } catch (e) {
120
+ console.error("[ecopages] Failed to hot-reload ${getComponentType(isMdx)} component:", e);
121
+ }
122
+ };
123
+ };
124
+
125
+ if (document.readyState === "loading") {
126
+ document.addEventListener("DOMContentLoaded", mount);
127
+ } else {
128
+ mount();
129
+ }
130
+ `.trim();
131
+ }
132
+ function createProdScriptWithRouter(options) {
133
+ const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath } = options;
134
+ const { components, getRouterProps } = router;
135
+ if (!routerImportPath) {
136
+ throw new Error("routerImportPath is required when router adapter is configured");
137
+ }
138
+ 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()`;
140
+ }
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()`;
142
+ }
143
+ function createProdScriptWithoutRouter(options) {
144
+ const { importPath, isMdx, reactImportPath, reactDomClientImportPath } = options;
145
+ 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()`;
147
+ }
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()`;
149
+ }
150
+ function createHydrationScript(options) {
151
+ const { isDevelopment, router } = options;
152
+ if (isDevelopment) {
153
+ return router ? createDevScriptWithRouter(options) : createDevScriptWithoutRouter(options);
154
+ }
155
+ return router ? createProdScriptWithRouter(options) : createProdScriptWithoutRouter(options);
156
+ }
157
+ function createIslandHydrationScript(options) {
158
+ const targetSelector = JSON.stringify(options.targetSelector);
159
+ const serializedProps = JSON.stringify(options.props ?? {});
160
+ const componentRef = JSON.stringify(options.componentRef ?? "");
161
+ const componentFile = JSON.stringify(options.componentFile ?? "");
162
+ const mountedAttribute = "data-eco-react-mounted";
163
+ if (options.isDevelopment) {
164
+ return `
165
+ import { createRoot } from "${options.reactDomClientImportPath}";
166
+ import { createElement } from "${options.reactImportPath}";
167
+ import * as ComponentModule from "${options.importPath}";
168
+
169
+ const resolveComponent = () => {
170
+ const id = ${componentRef};
171
+ const file = ${componentFile};
172
+ const moduleValues = Object.values(ComponentModule);
173
+
174
+ const matchByMetadata = moduleValues.find((entry) => {
175
+ if (typeof entry !== "function") return false;
176
+ const config = entry.config;
177
+ const eco = config?.__eco;
178
+ if (!eco) return false;
179
+ if (id && eco.id === id) return true;
180
+ if (file && eco.file === file) return true;
181
+ return false;
182
+ });
183
+
184
+ if (matchByMetadata && typeof matchByMetadata === "function") {
185
+ return matchByMetadata;
186
+ }
187
+
188
+ const defaultExport = ComponentModule.default;
189
+ if (typeof defaultExport === "function") {
190
+ return defaultExport;
191
+ }
192
+
193
+ const firstFunction = moduleValues.find((entry) => typeof entry === "function");
194
+ return typeof firstFunction === "function" ? firstFunction : null;
195
+ };
196
+
197
+ const mount = () => {
198
+ const target = document.querySelector(${targetSelector});
199
+ const Component = resolveComponent();
200
+ if (!target || !Component || target.hasAttribute("${mountedAttribute}")) {
201
+ return;
202
+ }
203
+ const props = ${serializedProps};
204
+ target.setAttribute("${mountedAttribute}", "true");
205
+ const root = createRoot(target);
206
+ root.render(createElement(Component, props));
207
+ };
208
+
209
+ document.addEventListener("eco:after-swap", mount);
210
+ if (document.readyState === "loading") {
211
+ document.addEventListener("DOMContentLoaded", mount, { once: true });
212
+ } else {
213
+ mount();
214
+ }
215
+ `.trim();
216
+ }
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()`;
218
+ }
219
+ export {
220
+ createHydrationScript,
221
+ createIslandHydrationScript
222
+ };
@@ -0,0 +1,338 @@
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
+ * Creates development hydration script with router support.
77
+ * Includes HMR handlers for hot module replacement.
78
+ * Layout is NOT applied here since PageContent handles it.
79
+ */
80
+ function createDevScriptWithRouter(options: HydrationScriptOptions): string {
81
+ const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath } = options;
82
+ const { components, getRouterProps } = router!;
83
+ if (!routerImportPath) {
84
+ throw new Error('routerImportPath is required when router adapter is configured');
85
+ }
86
+
87
+ return `
88
+ import { hydrateRoot } from "${reactDomClientImportPath}";
89
+ import { createElement } from "${reactImportPath}";
90
+ import { ${components.router}, ${components.pageContent} } from "${routerImportPath}";
91
+ ${getImportStatement(importPath, isMdx)}
92
+
93
+ window.__ecopages_hmr_handlers__ = window.__ecopages_hmr_handlers__ || {};
94
+ window.__ecopages_router_active__ = false;
95
+ window.__ecopages_reload_current_page__ = null;
96
+ let root = null;
97
+
98
+ const getPageData = () => {
99
+ const el = document.getElementById("__ECO_PAGE_DATA__");
100
+ if (el?.textContent) {
101
+ try { return JSON.parse(el.textContent); } catch {}
102
+ }
103
+ return {};
104
+ };
105
+
106
+ const props = getPageData();
107
+
108
+ window.__ECO_PAGE__ = {
109
+ module: "${importPath}",
110
+ props
111
+ };
112
+
113
+ const createTree = (Component, props) => {
114
+ const pageContent = createElement(${components.pageContent});
115
+ return createElement(${components.router}, ${getRouterProps('Component', 'props')}, pageContent);
116
+ };
117
+
118
+ const mount = () => {
119
+ root = hydrateRoot(document, createTree(Page, props), {
120
+ onRecoverableError: (err) => console.warn("[ecopages] Hydration error:", err)
121
+ });
122
+ window.__ecopages_router_active__ = true;
123
+ window.__ecopages_hmr_handlers__["${importPath}"] = async (newUrl) => {
124
+ if (window.__ecopages_router_active__ && window.__ecopages_reload_current_page__) {
125
+ await window.__ecopages_reload_current_page__();
126
+ console.log("[ecopages] ${getComponentType(isMdx)} component updated via router");
127
+ return;
128
+ }
129
+ try {
130
+ const newModule = await import(newUrl);
131
+ ${getHmrImportStatement(isMdx)}
132
+ root.render(createTree(NewPage, props));
133
+ console.log("[ecopages] ${getComponentType(isMdx)} component updated");
134
+ } catch (e) {
135
+ console.error("[ecopages] Failed to hot-reload ${getComponentType(isMdx)} component:", e);
136
+ }
137
+ };
138
+ };
139
+
140
+ if (document.readyState === "loading") {
141
+ document.addEventListener("DOMContentLoaded", mount);
142
+ } else {
143
+ mount();
144
+ }
145
+ `.trim();
146
+ }
147
+
148
+ /**
149
+ * Creates development hydration script without router.
150
+ * Includes HMR handlers for hot module replacement.
151
+ */
152
+ function createDevScriptWithoutRouter(options: HydrationScriptOptions): string {
153
+ const { importPath, isMdx, reactImportPath, reactDomClientImportPath } = options;
154
+
155
+ return `
156
+ import { hydrateRoot } from "${reactDomClientImportPath}";
157
+ import { createElement } from "${reactImportPath}";
158
+ ${getImportStatement(importPath, isMdx)}
159
+
160
+ window.__ecopages_hmr_handlers__ = window.__ecopages_hmr_handlers__ || {};
161
+ let root = null;
162
+
163
+ const getPageData = () => {
164
+ const el = document.getElementById("__ECO_PAGE_DATA__");
165
+ if (el?.textContent) {
166
+ try { return JSON.parse(el.textContent); } catch {}
167
+ }
168
+ return {};
169
+ };
170
+
171
+ const props = getPageData();
172
+
173
+ window.__ECO_PAGE__ = {
174
+ module: "${importPath}",
175
+ props
176
+ };
177
+
178
+ const createTree = (Component, props) => {
179
+ const Layout = Component.config?.layout;
180
+ const pageElement = createElement(Component, props);
181
+ return Layout ? createElement(Layout, null, pageElement) : pageElement;
182
+ };
183
+
184
+ const mount = () => {
185
+ root = hydrateRoot(document, createTree(Page, props), {
186
+ onRecoverableError: (err) => console.warn("[ecopages] Hydration error:", err)
187
+ });
188
+ window.__ecopages_hmr_handlers__["${importPath}"] = async (newUrl) => {
189
+ try {
190
+ const newModule = await import(newUrl);
191
+ ${getHmrImportStatement(isMdx)}
192
+ root.render(createTree(NewPage, props));
193
+ console.log("[ecopages] ${getComponentType(isMdx)} component updated");
194
+ } catch (e) {
195
+ console.error("[ecopages] Failed to hot-reload ${getComponentType(isMdx)} component:", e);
196
+ }
197
+ };
198
+ };
199
+
200
+ if (document.readyState === "loading") {
201
+ document.addEventListener("DOMContentLoaded", mount);
202
+ } else {
203
+ mount();
204
+ }
205
+ `.trim();
206
+ }
207
+
208
+ /**
209
+ * Creates minified production hydration script with router support.
210
+ * Layout is NOT applied here since PageContent handles it.
211
+ */
212
+ function createProdScriptWithRouter(options: HydrationScriptOptions): string {
213
+ const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath } = options;
214
+ const { components, getRouterProps } = router!;
215
+ if (!routerImportPath) {
216
+ throw new Error('routerImportPath is required when router adapter is configured');
217
+ }
218
+
219
+ if (isMdx) {
220
+ 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()`;
221
+ }
222
+
223
+ 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()`;
224
+ }
225
+
226
+ /**
227
+ * Creates minified production hydration script without router.
228
+ */
229
+ function createProdScriptWithoutRouter(options: HydrationScriptOptions): string {
230
+ const { importPath, isMdx, reactImportPath, reactDomClientImportPath } = options;
231
+
232
+ if (isMdx) {
233
+ 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()`;
234
+ }
235
+
236
+ 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()`;
237
+ }
238
+
239
+ /**
240
+ * Creates a hydration script for client-side React hydration.
241
+ * Generates appropriate script based on environment and router configuration.
242
+ * @param options - Configuration options for script generation
243
+ * @returns The generated hydration script as a string
244
+ */
245
+ export function createHydrationScript(options: HydrationScriptOptions): string {
246
+ const { isDevelopment, router } = options;
247
+
248
+ if (isDevelopment) {
249
+ return router ? createDevScriptWithRouter(options) : createDevScriptWithoutRouter(options);
250
+ }
251
+
252
+ return router ? createProdScriptWithRouter(options) : createProdScriptWithoutRouter(options);
253
+ }
254
+
255
+ /**
256
+ * Creates the client bootstrap for component-level React islands.
257
+ *
258
+ * The island runtime intentionally uses `createRoot()` (not `hydrateRoot()`) and
259
+ * mounts into the SSR element identified by `targetSelector`.
260
+ *
261
+ * Rationale:
262
+ * - No synthetic wrapper element is introduced in SSR output.
263
+ * - DOM structure remains identical to authored component markup.
264
+ * - Runtime ownership is isolated per island instance.
265
+ *
266
+ * Generated script behavior:
267
+ * - resolves the component export by metadata (`componentRef`, `componentFile`)
268
+ * before falling back to default/first function export
269
+ * - selects island root using `targetSelector`
270
+ * - creates a fresh React root and renders with serialized `props`
271
+ *
272
+ * @param options Island script generation options.
273
+ * @returns Browser-executable JavaScript module source.
274
+ */
275
+ export function createIslandHydrationScript(options: IslandHydrationScriptOptions): string {
276
+ const targetSelector = JSON.stringify(options.targetSelector);
277
+ const serializedProps = JSON.stringify(options.props ?? {});
278
+ const componentRef = JSON.stringify(options.componentRef ?? '');
279
+ const componentFile = JSON.stringify(options.componentFile ?? '');
280
+ const mountedAttribute = 'data-eco-react-mounted';
281
+
282
+ if (options.isDevelopment) {
283
+ return `
284
+ import { createRoot } from "${options.reactDomClientImportPath}";
285
+ import { createElement } from "${options.reactImportPath}";
286
+ import * as ComponentModule from "${options.importPath}";
287
+
288
+ const resolveComponent = () => {
289
+ const id = ${componentRef};
290
+ const file = ${componentFile};
291
+ const moduleValues = Object.values(ComponentModule);
292
+
293
+ const matchByMetadata = moduleValues.find((entry) => {
294
+ if (typeof entry !== "function") return false;
295
+ const config = entry.config;
296
+ const eco = config?.__eco;
297
+ if (!eco) return false;
298
+ if (id && eco.id === id) return true;
299
+ if (file && eco.file === file) return true;
300
+ return false;
301
+ });
302
+
303
+ if (matchByMetadata && typeof matchByMetadata === "function") {
304
+ return matchByMetadata;
305
+ }
306
+
307
+ const defaultExport = ComponentModule.default;
308
+ if (typeof defaultExport === "function") {
309
+ return defaultExport;
310
+ }
311
+
312
+ const firstFunction = moduleValues.find((entry) => typeof entry === "function");
313
+ return typeof firstFunction === "function" ? firstFunction : null;
314
+ };
315
+
316
+ const mount = () => {
317
+ const target = document.querySelector(${targetSelector});
318
+ const Component = resolveComponent();
319
+ if (!target || !Component || target.hasAttribute("${mountedAttribute}")) {
320
+ return;
321
+ }
322
+ const props = ${serializedProps};
323
+ target.setAttribute("${mountedAttribute}", "true");
324
+ const root = createRoot(target);
325
+ root.render(createElement(Component, props));
326
+ };
327
+
328
+ document.addEventListener("eco:after-swap", mount);
329
+ if (document.readyState === "loading") {
330
+ document.addEventListener("DOMContentLoaded", mount, { once: true });
331
+ } else {
332
+ mount();
333
+ }
334
+ `.trim();
335
+ }
336
+
337
+ 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()`;
338
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @module ReachabilityAnalyzer
3
+ *
4
+ * This module is responsible for performing static analysis on Ecopages client components
5
+ * using the Oxc AST parser. It computes a strict "reachability graph" of all JavaScript/TypeScript
6
+ * dependencies (imports, variables, functions, and classes) that begin from explicit client roots.
7
+ *
8
+ * In Ecopages, "client roots" are defined as the `render`, `errorBoundary`, or `loadingFallback`
9
+ * properties passed into `eco.page()` or `eco.component()`. By tracing the execution path from
10
+ * these roots, the analyzer determines exactly which modules and bindings are actually needed
11
+ * by the browser to hydrate the page, and which imports are unused on the client (and thus can be pruned).
12
+ */
13
+ import { parseSync } from 'oxc-parser';
14
+ type ParserLanguage = 'js' | 'jsx' | 'ts' | 'tsx';
15
+ /**
16
+ * Determines the appropriate parser language configuration for a given file name.
17
+ *
18
+ * @param filename - The absolute or relative path to the file.
19
+ * @returns The Oxc parser language dialect to use ('js', 'jsx', 'ts', or 'tsx').
20
+ */
21
+ export declare function parserLanguageForFile(filename: string): ParserLanguage;
22
+ /**
23
+ * Represents the computed results of a reachability analysis pass.
24
+ */
25
+ export type ReachabilityResult = {
26
+ /**
27
+ * Map from import specifier (e.g. 'node:fs', '@/components/Button')
28
+ * to a Set of imported bindings, or '*' for namespace imports.
29
+ */
30
+ reachableImports: Map<string, Set<string> | '*'>;
31
+ /**
32
+ * AST nodes of top-level declarations that are reachable.
33
+ */
34
+ reachableDeclarations: Set<unknown>;
35
+ unreachableSideEffectImports: unknown[];
36
+ /**
37
+ * Indicates whether the file had explicit eco client roots, or fell back to treating all exports as roots.
38
+ */
39
+ isFallbackRoots: boolean;
40
+ /**
41
+ * Whether the file was successfully parsed and analyzed.
42
+ */
43
+ analyzed: boolean;
44
+ };
45
+ /**
46
+ * Analyzes a module using Oxc AST and extracts a strict reachability graph
47
+ * starting from client roots (`render`, `errorBoundary`, `loadingFallback` of `eco.page` or `eco.component`).
48
+ *
49
+ * @param source - Raw source string of the module.
50
+ * @param filename - Absolute or relative path to the module file.
51
+ * @param program - Optional pre-parsed Oxc program AST. When supplied, the
52
+ * internal `parseSync` call is skipped entirely (avoids double-parsing).
53
+ */
54
+ export declare function analyzeReachability(source: string, filename: string, program?: ReturnType<typeof parseSync>['program']): ReachabilityResult;
55
+ export {};