@ecopages/react 0.2.0-alpha.9 → 0.2.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 (43) hide show
  1. package/CHANGELOG.md +13 -11
  2. package/README.md +10 -0
  3. package/package.json +6 -6
  4. package/src/react-hmr-strategy.d.ts +4 -2
  5. package/src/react-hmr-strategy.js +36 -3
  6. package/src/react-renderer.d.ts +25 -37
  7. package/src/react-renderer.js +190 -142
  8. package/src/react.plugin.d.ts +0 -12
  9. package/src/react.plugin.js +2 -13
  10. package/src/services/react-bundle.service.d.ts +3 -1
  11. package/src/services/react-bundle.service.js +20 -2
  12. package/src/services/react-hmr-page-metadata-cache.d.ts +9 -0
  13. package/src/services/react-hmr-page-metadata-cache.js +18 -2
  14. package/src/services/react-hydration-asset.service.d.ts +7 -6
  15. package/src/services/react-hydration-asset.service.js +26 -14
  16. package/src/services/react-page-module.service.js +5 -2
  17. package/src/services/react-runtime-bundle.service.d.ts +2 -0
  18. package/src/services/react-runtime-bundle.service.js +5 -0
  19. package/src/utils/client-graph-boundary-plugin.js +2 -2
  20. package/src/utils/declared-modules.js +4 -1
  21. package/src/utils/hydration-scripts.d.ts +1 -3
  22. package/src/utils/hydration-scripts.js +31 -19
  23. package/src/react-hmr-strategy.ts +0 -386
  24. package/src/react-renderer.ts +0 -803
  25. package/src/react.plugin.ts +0 -276
  26. package/src/router-adapter.ts +0 -95
  27. package/src/services/react-bundle.service.ts +0 -108
  28. package/src/services/react-hmr-page-metadata-cache.ts +0 -24
  29. package/src/services/react-hydration-asset.service.ts +0 -263
  30. package/src/services/react-page-module.service.ts +0 -224
  31. package/src/services/react-runtime-bundle.service.ts +0 -172
  32. package/src/utils/client-graph-boundary-plugin.ts +0 -831
  33. package/src/utils/client-only.ts +0 -27
  34. package/src/utils/declared-modules.ts +0 -99
  35. package/src/utils/dynamic.ts +0 -27
  36. package/src/utils/hmr-scripts.ts +0 -47
  37. package/src/utils/html-boundary.ts +0 -66
  38. package/src/utils/hydration-scripts.ts +0 -459
  39. package/src/utils/reachability-analyzer.ts +0 -593
  40. package/src/utils/react-dom-runtime-interop-plugin.ts +0 -33
  41. package/src/utils/react-mdx-loader-plugin.ts +0 -63
  42. package/src/utils/react-runtime-specifier-map.ts +0 -45
  43. package/src/utils/use-sync-external-store-shim-plugin.ts +0 -45
@@ -23,21 +23,24 @@ export interface ReactHydrationAssetServiceConfig {
23
23
  bundleService: ReactBundleService;
24
24
  hmrPageMetadataCache?: ReactHmrPageMetadataCache;
25
25
  }
26
+ export declare function getReactIslandComponentKey(componentFile: string, config?: EcoComponentConfig): string;
26
27
  /**
27
28
  * Manages the creation of client-side hydration assets for React pages and component islands.
28
29
  */
29
30
  export declare class ReactHydrationAssetService {
30
31
  private readonly config;
31
32
  constructor(config: ReactHydrationAssetServiceConfig);
33
+ private getIslandBundleName;
34
+ private getIslandHydrationName;
32
35
  /**
33
36
  * Resolves the import path for the bundled page component.
34
37
  * Uses HMR manager for development or constructs static path for production.
35
38
  *
36
39
  * @param pagePath - Absolute path to the page source file
37
- * @param componentName - Generated unique component name
40
+ * @param assetName - Generated asset name
38
41
  * @returns The resolved import path for the bundled component
39
42
  */
40
- resolveAssetImportPath(pagePath: string, componentName: string): Promise<string>;
43
+ resolveAssetImportPath(pagePath: string, assetName: string): Promise<string>;
41
44
  /**
42
45
  * Creates the asset dependencies for a page: the bundled component and hydration script.
43
46
  *
@@ -54,15 +57,13 @@ export declare class ReactHydrationAssetService {
54
57
  /**
55
58
  * Builds client-side assets for a React component island.
56
59
  *
57
- * Includes the bundled component entry and an inline hydration bootstrap script.
60
+ * Includes the bundled component entry and a shared hydration bootstrap script.
58
61
  *
59
62
  * @param componentFile - Absolute path to the component source file
60
- * @param componentInstanceId - Unique instance ID for DOM targeting
61
- * @param props - Serialized props for client-side hydration
62
63
  * @param config - Optional component config with `__eco` metadata
63
64
  * @returns Processed assets ready for injection
64
65
  */
65
- buildComponentRenderAssets(componentFile: string, componentInstanceId: string, props: Record<string, unknown>, config?: EcoComponentConfig): Promise<ProcessedAsset[]>;
66
+ buildComponentRenderAssets(componentFile: string, config?: EcoComponentConfig): Promise<ProcessedAsset[]>;
66
67
  /**
67
68
  * Builds all client-side route assets for a page.
68
69
  *
@@ -6,25 +6,34 @@ import {
6
6
  } from "@ecopages/core/services/asset-processing-service";
7
7
  import { createHydrationScript, createIslandHydrationScript } from "../utils/hydration-scripts.js";
8
8
  import { collectDeclaredModulesInConfig } from "../utils/declared-modules.js";
9
+ function getReactIslandComponentKey(componentFile, config) {
10
+ return rapidhash(`${componentFile}:${config?.__eco?.id ?? ""}`).toString();
11
+ }
9
12
  class ReactHydrationAssetService {
10
13
  config;
11
14
  constructor(config) {
12
15
  this.config = config;
13
16
  }
17
+ getIslandBundleName(componentFile) {
18
+ return `ecopages-react-island-${rapidhash(componentFile)}`;
19
+ }
20
+ getIslandHydrationName(bundleName, componentKey) {
21
+ return `${bundleName}-hydration-${componentKey}`;
22
+ }
14
23
  /**
15
24
  * Resolves the import path for the bundled page component.
16
25
  * Uses HMR manager for development or constructs static path for production.
17
26
  *
18
27
  * @param pagePath - Absolute path to the page source file
19
- * @param componentName - Generated unique component name
28
+ * @param assetName - Generated asset name
20
29
  * @returns The resolved import path for the bundled component
21
30
  */
22
- async resolveAssetImportPath(pagePath, componentName) {
31
+ async resolveAssetImportPath(pagePath, assetName) {
23
32
  const hmrManager = this.config.assetProcessingService?.getHmrManager();
24
33
  if (hmrManager?.isEnabled()) {
25
34
  return hmrManager.registerEntrypoint(pagePath);
26
35
  }
27
- return `/${path.join(RESOLVED_ASSETS_DIR, path.relative(this.config.srcDir, pagePath)).replace(path.basename(pagePath), `${componentName}.js`).replace(/\\/g, "/")}`;
36
+ return `/${path.join(RESOLVED_ASSETS_DIR, path.relative(this.config.srcDir, pagePath)).replace(path.basename(pagePath), `${assetName}.js`).replace(/\\/g, "/")}`;
28
37
  }
29
38
  /**
30
39
  * Creates the asset dependencies for a page: the bundled component and hydration script.
@@ -96,19 +105,22 @@ class ReactHydrationAssetService {
96
105
  /**
97
106
  * Builds client-side assets for a React component island.
98
107
  *
99
- * Includes the bundled component entry and an inline hydration bootstrap script.
108
+ * Includes the bundled component entry and a shared hydration bootstrap script.
100
109
  *
101
110
  * @param componentFile - Absolute path to the component source file
102
- * @param componentInstanceId - Unique instance ID for DOM targeting
103
- * @param props - Serialized props for client-side hydration
104
111
  * @param config - Optional component config with `__eco` metadata
105
112
  * @returns Processed assets ready for injection
106
113
  */
107
- async buildComponentRenderAssets(componentFile, componentInstanceId, props, config) {
108
- const componentName = `ecopages-react-island-${rapidhash(`${componentFile}:${componentInstanceId}`)}`;
109
- const importPath = await this.resolveAssetImportPath(componentFile, componentName);
114
+ async buildComponentRenderAssets(componentFile, config) {
115
+ const componentName = this.getIslandBundleName(componentFile);
116
+ const componentKey = getReactIslandComponentKey(componentFile, config);
117
+ const hydrationName = this.getIslandHydrationName(componentName, componentKey);
110
118
  const hmrManager = this.config.assetProcessingService?.getHmrManager();
111
119
  const isDevelopment = hmrManager?.isEnabled() ?? false;
120
+ if (isDevelopment) {
121
+ this.config.hmrPageMetadataCache?.markOwnedEntrypoint(componentFile);
122
+ }
123
+ const importPath = await this.resolveAssetImportPath(componentFile, componentName);
112
124
  const declaredModules = collectDeclaredModulesInConfig(config);
113
125
  const bundleOptions = await this.config.bundleService.createBundleOptions(
114
126
  componentName,
@@ -136,19 +148,18 @@ class ReactHydrationAssetService {
136
148
  importPath,
137
149
  reactImportPath: runtimeImports.react,
138
150
  reactDomClientImportPath: runtimeImports.reactDomClient,
139
- targetSelector: `[data-eco-component-id="${componentInstanceId}"]`,
140
- props,
151
+ targetSelector: `[data-eco-component-key="${componentKey}"]`,
141
152
  componentRef: config?.__eco?.id,
142
153
  componentFile,
143
154
  isDevelopment
144
155
  }),
145
- name: `${componentName}-hydration`,
156
+ name: hydrationName,
146
157
  bundle: false,
147
158
  attributes: {
148
159
  type: "module",
149
160
  defer: "",
150
161
  "data-eco-rerun": "true",
151
- "data-eco-script-id": `${componentName}-hydration`,
162
+ "data-eco-script-id": hydrationName,
152
163
  "data-eco-persist": "true"
153
164
  }
154
165
  })
@@ -194,5 +205,6 @@ class ReactHydrationAssetService {
194
205
  }
195
206
  }
196
207
  export {
197
- ReactHydrationAssetService
208
+ ReactHydrationAssetService,
209
+ getReactIslandComponentKey
198
210
  };
@@ -42,7 +42,7 @@ class ReactPageModuleService {
42
42
  entrypoints: [filePath],
43
43
  root: this.config.rootDir,
44
44
  outdir,
45
- target: "node",
45
+ target: "es2022",
46
46
  format: "esm",
47
47
  sourcemap: "none",
48
48
  splitting: false,
@@ -62,7 +62,10 @@ class ReactPageModuleService {
62
62
  if (!compiledOutput) {
63
63
  throw new Error(`No compiled MDX output generated for page: ${filePath}`);
64
64
  }
65
- return await import(pathToFileURL(compiledOutput).href);
65
+ return await import(
66
+ /* @vite-ignore */
67
+ pathToFileURL(compiledOutput).href
68
+ );
66
69
  }
67
70
  /**
68
71
  * Ensures that an EcoComponentConfig has proper `__eco` metadata attached.
@@ -19,11 +19,13 @@ export type ReactRuntimeImports = {
19
19
  };
20
20
  export interface ReactRuntimeBundleServiceConfig {
21
21
  routerAdapter?: ReactRouterAdapter;
22
+ rootDir?: string;
22
23
  }
23
24
  type RuntimeMode = 'development' | 'production';
24
25
  export declare class ReactRuntimeBundleService {
25
26
  private readonly config;
26
27
  constructor(config: ReactRuntimeBundleServiceConfig);
28
+ setRootDir(rootDir: string | undefined): void;
27
29
  private get isDevelopment();
28
30
  private getCurrentRuntimeMode;
29
31
  private createRuntimeDefines;
@@ -11,6 +11,9 @@ class ReactRuntimeBundleService {
11
11
  constructor(config) {
12
12
  this.config = config;
13
13
  }
14
+ setRootDir(rootDir) {
15
+ this.config.rootDir = rootDir;
16
+ }
14
17
  get isDevelopment() {
15
18
  return process.env.NODE_ENV === "development";
16
19
  }
@@ -79,6 +82,7 @@ class ReactRuntimeBundleService {
79
82
  name: "react",
80
83
  fileName: this.getReactVendorFileName(mode),
81
84
  cacheDirName: `ecopages-react-runtime-${mode}`,
85
+ rootDir: this.config.rootDir,
82
86
  bundleOptions: {
83
87
  define: this.createRuntimeDefines(mode)
84
88
  }
@@ -88,6 +92,7 @@ class ReactRuntimeBundleService {
88
92
  name: "react-dom",
89
93
  fileName: this.getReactDomVendorFileName(mode),
90
94
  cacheDirName: `ecopages-react-runtime-${mode}`,
95
+ rootDir: this.config.rootDir,
91
96
  bundleOptions: {
92
97
  define: this.createRuntimeDefines(mode),
93
98
  plugins: reactDomBundlePlugins
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, extname, resolve } from "node:path";
3
3
  import { parseSync } from "oxc-parser";
4
- import { analyzeReachability } from "./reachability-analyzer";
4
+ import { analyzeReachability } from "./reachability-analyzer.js";
5
5
  const SOURCE_FILE_FILTER = /\.(tsx?|jsx?)$/;
6
6
  const SERVER_ONLY_ECO_PAGE_OPTION_KEYS = /* @__PURE__ */ new Set([
7
7
  "cache",
@@ -484,7 +484,7 @@ function createClientGraphBoundaryPlugin(options) {
484
484
  }
485
485
  if (!modified) return void 0;
486
486
  const ext = extname(args.path).slice(1);
487
- return { contents: transformed, loader: ext };
487
+ return { contents: transformed, loader: ext, resolveDir: dirname(args.path) };
488
488
  });
489
489
  }
490
490
  };
@@ -41,7 +41,10 @@ function collectPageDeclaredModulesFromModule(pageModule) {
41
41
  }
42
42
  async function collectPageDeclaredModules(pagePath) {
43
43
  try {
44
- const pageModule = await import(pagePath);
44
+ const pageModule = await import(
45
+ /* @vite-ignore */
46
+ pagePath
47
+ );
45
48
  return collectPageDeclaredModulesFromModule(pageModule);
46
49
  } catch {
47
50
  return [];
@@ -30,10 +30,8 @@ export type IslandHydrationScriptOptions = {
30
30
  reactImportPath: string;
31
31
  /** Browser import path for react-dom/client runtime. */
32
32
  reactDomClientImportPath: string;
33
- /** Selector that resolves to the SSR root element for this island instance. */
33
+ /** Selector that resolves to all SSR root elements for this island component. */
34
34
  targetSelector: string;
35
- /** Serialized component props emitted at render time. */
36
- props: Record<string, unknown>;
37
35
  /** Optional stable component id used to resolve named exports reliably. */
38
36
  componentRef?: string;
39
37
  /** Optional source file hint used as fallback for component resolution. */
@@ -31,16 +31,19 @@ function getProdPageRootCleanupScript() {
31
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
32
  }
33
33
  function getDevRouterBootstrapRegistrationScript() {
34
- return `window.__ECO_PAGES__?.navigation?.register({
34
+ return `const currentOwnerState = window.__ECO_PAGES__?.navigation?.getOwnerState?.();
35
+ if (!(currentOwnerState?.owner === "react-router" && currentOwnerState.canHandleSpaNavigation)) {
36
+ window.__ECO_PAGES__?.navigation?.register({
35
37
  owner: "react-router",
36
38
  cleanupBeforeHandoff: async () => {
37
39
  window.__ECO_PAGES__?.react?.cleanupPageRoot?.();
38
40
  }
39
41
  });
40
- window.__ECO_PAGES__?.navigation?.claimOwnership?.("react-router");`;
42
+ window.__ECO_PAGES__?.navigation?.claimOwnership?.("react-router");
43
+ }`;
41
44
  }
42
45
  function getProdRouterBootstrapRegistrationScript() {
43
- return 'window.__ECO_PAGES__?.navigation?.register({owner:"react-router",cleanupBeforeHandoff:async()=>{window.__ECO_PAGES__?.react?.cleanupPageRoot?.()}});window.__ECO_PAGES__?.navigation?.claimOwnership?.("react-router");';
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")}';
44
47
  }
45
48
  function createDevScriptWithRouter(options) {
46
49
  const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath } = options;
@@ -93,16 +96,20 @@ const mount = () => {
93
96
  window.__ECO_PAGES__.react.pageRoot = root;
94
97
  }
95
98
  window.__ECO_PAGES__.hmrHandlers["${importPath}"] = async (newUrl) => {
96
- if (window.__ECO_PAGES__?.navigation?.getOwnerState().owner === "react-router") {
97
- await window.__ECO_PAGES__?.navigation?.reloadCurrentPage?.({ clearCache: false, source: "react-router" });
98
- console.log("[ecopages] ${getComponentType(isMdx)} component updated via router");
99
- return;
100
- }
101
99
  try {
102
100
  const newModule = await import(newUrl);
101
+ const nextProps = getPageData();
103
102
  ${getHmrImportStatement(isMdx)}
104
- root.render(createTree(NewPage, props));
105
- 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
+ }
106
113
  } catch (e) {
107
114
  console.error("[ecopages] Failed to hot-reload ${getComponentType(isMdx)} component:", e);
108
115
  }
@@ -245,17 +252,22 @@ const resolveComponent = () => {
245
252
  };
246
253
 
247
254
  const mount = () => {
248
- const target = document.querySelector(${targetSelector});
255
+ const targets = document.querySelectorAll(${targetSelector});
249
256
  const Component = resolveComponent();
250
- if (!target || !Component) {
257
+ if (!Component || targets.length === 0) {
251
258
  return;
252
259
  }
253
- const props = JSON.parse(atob(target.getAttribute("data-eco-props") || "e30="));
254
- const container = document.createElement("eco-island");
255
- container.style.display = "block";
256
- target.replaceWith(container);
257
- const root = createRoot(container);
258
- 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
+ });
259
271
  };
260
272
 
261
273
  if (document.readyState === "loading") {
@@ -265,7 +277,7 @@ if (document.readyState === "loading") {
265
277
  }
266
278
  `.trim();
267
279
  }
268
- return `import{createRoot as cr}from"${options.reactDomClientImportPath}";import{createElement as ce}from"${options.reactImportPath}";import*as M from"${options.importPath}";const r=${componentRef};const f=${componentFile};const mv=Object.values(M);const c=mv.find((e)=>{if(typeof e!=="function")return false;const ec=e.config?.__eco;if(!ec)return false;if(r&&ec.id===r)return true;if(f&&ec.file===f)return true;return false;})??(typeof M.default==="function"?M.default:mv.find((e)=>typeof e==="function")??null);const m=()=>{const t=document.querySelector(${targetSelector});if(!t||!c)return;const p=JSON.parse(atob(t.getAttribute("data-eco-props")||"e30="));const ct=document.createElement("eco-island");ct.style.display="block";t.replaceWith(ct);cr(ct).render(ce(c,p))};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m,{once:true}):m()`;
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()`;
269
281
  }
270
282
  export {
271
283
  createHydrationScript,