@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
package/CHANGELOG.md CHANGED
@@ -4,24 +4,26 @@ All notable changes to `@ecopages/react` are documented here.
4
4
 
5
5
  > **Note:** Changelog tracking begins at version `0.2.0`. Changes prior to this release are not recorded here but are available in the git history.
6
6
 
7
- ## [UNRELEASED] — TBD
7
+ ## [0.2.1] — 2026-04-16
8
8
 
9
- ### Features & Performance
9
+ ### Bug Fixes
10
10
 
11
- - **Performance Hydration**: Introduced static reachability analysis to enforce explicit hydration boundaries and optimized HMR via metadata caching.
12
- - **Service-Oriented Internals**: Refactored the integration into focused core-backed services for bundling, hydration, and page-module loading.
13
- - **React MDX**: Inlined MDX support directly into the React integration for a zero-config setup, including Node-native compatibility for experimental startup.
11
+ - Fixed React hydration, Fast Refresh, module loading, doctype handling, island asset reuse, and mixed-renderer boundary resolution across Bun, Vite, and Nitro flows.
14
12
 
15
- ### Bug Fixes & Refactoring
13
+ ### Features
16
14
 
17
- - **Handoff Stability**: Standardized router-backed page payloads and document owner markers for mixed-router stability during navigation.
18
- - **Hydration Hardening**: Fixed island remount races, prop collisions, and layout metadata resolution during development and route handoffs.
19
- - **Architecture**: Centralized runtime specifiers and consolidated browser-side integration state under `window.__ECO_PAGES__`.
15
+ - Added built-in React MDX support and reachability-based hydration analysis for React page bundles.
16
+
17
+ ### Refactoring
18
+
19
+ - Consolidated React bundling, hydration, and runtime state behind shared service boundaries and `window.__ECO_PAGES__`.
20
+
21
+ ### Documentation
22
+
23
+ - Updated the README to document React-owned mixed boundaries and React MDX setup.
20
24
 
21
25
  ---
22
26
 
23
27
  ## Migration Notes
24
28
 
25
- - The React integration now requires explicit client boundary declarations for client-rendered components.
26
29
  - React MDX support is built in and no longer requires installing `@ecopages/mdx` just to enable React MDX routes.
27
- - The internal service layer is not part of the public API and may change between releases.
package/README.md CHANGED
@@ -60,6 +60,16 @@ const config = await new ConfigBuilder()
60
60
  export default config;
61
61
  ```
62
62
 
63
+ ## Mixed Rendering
64
+
65
+ The React integration can participate in mixed-renderer apps in three ways:
66
+
67
+ - React can own the page or view directly.
68
+ - React can render nested component boundaries inside pages owned by another integration.
69
+ - React can render through non-React page, layout, or document shells when those shell components return strings.
70
+
71
+ When a non-React render pass enters a React-owned boundary, Ecopages hands that boundary back to the React renderer. When React renders through a non-React shell, that shell must serialize to HTML so React can insert the result into the final response without escaping it.
72
+
63
73
  ## Server and Client Graph Contract
64
74
 
65
75
  The React integration supports Node.js modules and server-only code **only on the server execution graph**.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/react",
3
- "version": "0.2.0-alpha.9",
3
+ "version": "0.2.1",
4
4
  "description": "React integration for Ecopages",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -53,19 +53,19 @@
53
53
  "directory": "packages/integrations/react"
54
54
  },
55
55
  "peerDependencies": {
56
- "@ecopages/core": "0.2.0-alpha.9",
56
+ "@ecopages/core": "0.2.1",
57
57
  "@types/react": "^19",
58
58
  "@types/react-dom": "^19",
59
59
  "react": "^19",
60
60
  "react-dom": "^19"
61
61
  },
62
62
  "dependencies": {
63
- "@ecopages/file-system": "0.2.0-alpha.9",
64
- "@ecopages/logger": "latest",
63
+ "@ecopages/file-system": "0.2.1",
64
+ "@ecopages/logger": "^0.2.3",
65
65
  "@mdx-js/esbuild": "^3.0.1",
66
66
  "@mdx-js/mdx": "^3.1.0",
67
- "oxc-parser": "^0.114.0",
68
- "oxc-transform": "^0.114.0",
67
+ "oxc-parser": "^0.124.0",
68
+ "oxc-transform": "^0.124.0",
69
69
  "source-map": "^0.7.6",
70
70
  "vfile": "^6.0.3"
71
71
  },
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @module
8
8
  */
9
- import { HmrStrategy, HmrStrategyType, type HmrAction } from '@ecopages/core/hmr/hmr-strategy';
9
+ import { HmrStrategy, type HmrAction } from '@ecopages/core/hmr/hmr-strategy';
10
10
  import type { DefaultHmrContext } from '@ecopages/core';
11
11
  import type { CompileOptions } from '@mdx-js/mdx';
12
12
  import type { ReactHmrPageMetadataCache } from './services/react-hmr-page-metadata-cache.js';
@@ -49,7 +49,7 @@ import type { ReactHmrPageMetadataCache } from './services/react-hmr-page-metada
49
49
  * ```
50
50
  */
51
51
  export declare class ReactHmrStrategy extends HmrStrategy {
52
- readonly type = HmrStrategyType.INTEGRATION;
52
+ readonly type: 100;
53
53
  private mdxCompilerOptions?;
54
54
  private readonly ownedTemplateExtensions;
55
55
  private readonly allTemplateExtensions;
@@ -89,6 +89,7 @@ export declare class ReactHmrStrategy extends HmrStrategy {
89
89
  */
90
90
  private isRouteTemplate;
91
91
  private resolveTemplateExtension;
92
+ private ownsWatchedEntrypoint;
92
93
  /**
93
94
  * Determines if the file is a React/MDX entrypoint that's registered for HMR.
94
95
  *
@@ -128,6 +129,7 @@ export declare class ReactHmrStrategy extends HmrStrategy {
128
129
  * @returns True if bundling was successful
129
130
  */
130
131
  private bundleReactEntrypoint;
132
+ private resolveTempOutputPath;
131
133
  /**
132
134
  * Encodes dynamic route segments (brackets) in file paths.
133
135
  * Converts `[slug]` to `_slug_` to avoid filesystem issues.
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { HmrStrategy, HmrStrategyType } from "@ecopages/core/hmr/hmr-strategy";
3
+ import { rewriteRuntimeSpecifierAliases } from "@ecopages/core/build/runtime-specifier-aliases";
3
4
  import { createRuntimeSpecifierAliasPlugin } from "@ecopages/core/build/runtime-specifier-alias-plugin";
4
5
  import { FileNotFoundError, fileSystem } from "@ecopages/file-system";
5
6
  import { Logger } from "@ecopages/logger";
@@ -49,8 +50,9 @@ class ReactHmrStrategy extends HmrStrategy {
49
50
  * (including `node:*`) from breaking the browser bundle.
50
51
  */
51
52
  getBuildPlugins(declaredModules) {
52
- const allowSpecifiers = getReactClientGraphAllowSpecifiers(this.context.getSpecifierMap().keys());
53
- const runtimeAliasPlugin = createRuntimeSpecifierAliasPlugin(this.context.getSpecifierMap(), {
53
+ const runtimeSpecifierMap = this.context.getSpecifierMap();
54
+ const allowSpecifiers = getReactClientGraphAllowSpecifiers(runtimeSpecifierMap.keys());
55
+ const runtimeAliasPlugin = createRuntimeSpecifierAliasPlugin(runtimeSpecifierMap, {
54
56
  name: "react-hmr-runtime-specifier-alias"
55
57
  });
56
58
  return [
@@ -97,6 +99,9 @@ class ReactHmrStrategy extends HmrStrategy {
97
99
  resolveTemplateExtension(filePath) {
98
100
  return this.allTemplateExtensions.find((extension) => filePath.endsWith(extension));
99
101
  }
102
+ ownsWatchedEntrypoint(filePath) {
103
+ return this.pageMetadataCache.ownsEntrypoint(filePath);
104
+ }
100
105
  /**
101
106
  * Determines if the file is a React/MDX entrypoint that's registered for HMR.
102
107
  *
@@ -109,6 +114,9 @@ class ReactHmrStrategy extends HmrStrategy {
109
114
  if (watchedFiles.size === 0) {
110
115
  return false;
111
116
  }
117
+ if (watchedFiles.has(filePath)) {
118
+ return this.ownsWatchedEntrypoint(filePath);
119
+ }
112
120
  return this.isReactEntrypoint(filePath);
113
121
  }
114
122
  /**
@@ -148,6 +156,10 @@ class ReactHmrStrategy extends HmrStrategy {
148
156
  appLogger.debug(`Detected layout file change: ${_filePath}`);
149
157
  }
150
158
  const changedEntrypointOutput = watchedFiles.get(_filePath);
159
+ if (changedEntrypointOutput && !this.ownsWatchedEntrypoint(_filePath)) {
160
+ appLogger.debug(`Skipping non-React watched entrypoint: ${_filePath}`);
161
+ return { type: "none" };
162
+ }
151
163
  const entrypointsToBuild = changedEntrypointOutput ? [[_filePath, changedEntrypointOutput]] : watchedFiles.entries();
152
164
  const updates = [];
153
165
  for (const [entrypoint, outputUrl] of entrypointsToBuild) {
@@ -225,13 +237,33 @@ class ReactHmrStrategy extends HmrStrategy {
225
237
  appLogger.error(`No output file generated for ${entrypointPath}`);
226
238
  return false;
227
239
  }
228
- const processed = await this.processOutput(tempFile, outputPath, outputUrl);
240
+ const resolvedTempFile = await this.resolveTempOutputPath(tempFile);
241
+ if (!resolvedTempFile) {
242
+ appLogger.debug(`Skipping stale temp output for ${outputUrl}: ${tempFile}`);
243
+ return false;
244
+ }
245
+ const processed = await this.processOutput(resolvedTempFile, outputPath, outputUrl);
229
246
  return processed;
230
247
  } catch (error) {
231
248
  appLogger.error(`Error bundling ${entrypointPath}:`, error);
232
249
  return false;
233
250
  }
234
251
  }
252
+ async resolveTempOutputPath(tempPath) {
253
+ if (fileSystem.exists(tempPath)) {
254
+ return tempPath;
255
+ }
256
+ if (!tempPath.includes("[hash]")) {
257
+ return tempPath;
258
+ }
259
+ const directory = path.dirname(tempPath);
260
+ const pattern = path.basename(tempPath).replaceAll("[hash]", "*");
261
+ const matches = await fileSystem.glob([pattern], { cwd: directory });
262
+ if (matches.length === 0) {
263
+ return null;
264
+ }
265
+ return path.isAbsolute(matches[0]) ? matches[0] : path.join(directory, matches[0]);
266
+ }
235
267
  /**
236
268
  * Encodes dynamic route segments (brackets) in file paths.
237
269
  * Converts `[slug]` to `_slug_` to avoid filesystem issues.
@@ -255,6 +287,7 @@ class ReactHmrStrategy extends HmrStrategy {
255
287
  }
256
288
  try {
257
289
  let code = await fileSystem.readFile(tempPath);
290
+ code = rewriteRuntimeSpecifierAliases(code, this.context.getSpecifierMap());
258
291
  code = injectHmrHandler(code);
259
292
  await fileSystem.writeAsync(finalPath, code);
260
293
  await fileSystem.removeAsync(tempPath).catch(() => {
@@ -2,7 +2,7 @@
2
2
  * This module contains the React renderer
3
3
  * @module
4
4
  */
5
- import type { ComponentRenderInput, ComponentRenderResult, EcoComponent, EcoComponentConfig, EcoPageFile, IntegrationRendererRenderOptions, RouteRendererBody } from '@ecopages/core';
5
+ import type { ComponentRenderInput, ComponentRenderResult, EcoComponent, EcoPageFile, IntegrationRendererRenderOptions, RouteRendererBody } from '@ecopages/core';
6
6
  import { IntegrationRenderer, type RenderToResponseContext } from '@ecopages/core/route-renderer/integration-renderer';
7
7
  import type { ProcessedAsset } from '@ecopages/core/services/asset-processing-service';
8
8
  import { type ReactNode } from 'react';
@@ -12,11 +12,6 @@ import { ReactBundleService } from './services/react-bundle.service.js';
12
12
  import { ReactHmrPageMetadataCache } from './services/react-hmr-page-metadata-cache.js';
13
13
  import { ReactPageModuleService } from './services/react-page-module.service.js';
14
14
  import { ReactHydrationAssetService } from './services/react-hydration-asset.service.js';
15
- type ReactPageModule = EcoPageFile<{
16
- config?: EcoComponentConfig;
17
- }> & {
18
- config?: EcoComponentConfig;
19
- };
20
15
  /**
21
16
  * Error thrown when an error occurs while rendering a React component.
22
17
  */
@@ -82,7 +77,7 @@ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
82
77
  *
83
78
  * React pages embedded in a non-React HTML shell still need to expose the same
84
79
  * page-data contract as fully React-owned documents so navigation and hydration
85
- * can read one marker consistently.
80
+ * can read one shared document payload consistently.
86
81
  */
87
82
  private buildRouterPageDataScript;
88
83
  private getRouterDocumentAttributes;
@@ -116,14 +111,6 @@ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
116
111
  * HTML transformer state.
117
112
  */
118
113
  private appendHydrationAssetsForFile;
119
- /**
120
- * Resolves metadata for direct `renderToResponse()` calls.
121
- *
122
- * View rendering bypasses the normal route-file pipeline, so metadata has to be
123
- * evaluated here from either the component-level generator or the application
124
- * default.
125
- */
126
- private resolveViewMetadata;
127
114
  /**
128
115
  * Renders a non-React layout or HTML template and enforces that mixed shells
129
116
  * return serialized HTML.
@@ -133,21 +120,23 @@ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
133
120
  */
134
121
  private renderNonReactShellComponent;
135
122
  /**
136
- * Produces the page body before the final HTML template is applied.
123
+ * Renders one React component boundary while preserving already-resolved child HTML.
137
124
  *
138
- * This method owns the React/non-React layout split. React-managed layouts stay
139
- * as React elements so they can stream normally; non-React layouts are rendered
140
- * to HTML first and then passed through as serialized content.
141
- */
142
- private composePageContent;
143
- /**
144
- * Wraps composed page content in the final document template.
125
+ * When nested boundary resolution has already produced child HTML for this
126
+ * boundary, the child payload must remain raw SSR output rather than a React
127
+ * string child, otherwise React would escape it. This helper renders a unique
128
+ * token through React and swaps that token back to the resolved HTML
129
+ * afterward.
145
130
  *
146
- * React-owned HTML templates stream directly. Non-React templates receive
147
- * pre-rendered page HTML plus the canonical React page-data payload so the
148
- * client runtime can recover page data after cross-integration handoff.
131
+ * @param input Component render input for the current boundary.
132
+ * @param context React-specific render context for stable token generation.
133
+ * @returns Serialized component HTML with resolved child markup preserved.
149
134
  */
150
- private renderDocument;
135
+ private renderComponentHtml;
136
+ private restoreRuntimeChildHtml;
137
+ private renderQueuedChildrenToHtml;
138
+ private resolveQueuedBoundaryHtml;
139
+ private buildHydrationProps;
151
140
  /**
152
141
  * Renders a React component for component-level orchestration.
153
142
  *
@@ -156,17 +145,26 @@ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
156
145
  * - When an explicit component instance id is provided, a stable
157
146
  * `data-eco-component-id` attribute is attached so island hydration can target it.
158
147
  * - Without an explicit instance id, component renders remain plain SSR output.
148
+ * - When resolved child HTML is provided, that boundary is treated as a pure SSR
149
+ * composition step and does not emit hydration assets for the parent wrapper.
159
150
  *
160
151
  * This preserves DOM shape for global CSS/layout selectors while keeping a
161
152
  * deterministic mount target per component instance.
162
153
  */
163
154
  renderComponent(input: ComponentRenderInput): Promise<ComponentRenderResult>;
155
+ protected createComponentBoundaryRuntime(options: {
156
+ boundaryInput: ComponentRenderInput;
157
+ rendererCache: Map<string, IntegrationRenderer<any>>;
158
+ }): import("@ecopages/core").ComponentBoundaryRuntime;
164
159
  /**
165
160
  * Checks if the given file path corresponds to an MDX file based on configured extensions.
166
161
  * @param filePath - The file path to check
167
162
  * @returns True if the file is an MDX file
168
163
  */
169
164
  isMdxFile(filePath: string): boolean;
165
+ protected usesIntegrationPageImporter(file: string): boolean;
166
+ protected importIntegrationPageFile(file: string): Promise<EcoPageFile>;
167
+ protected normalizeImportedPageFile<TPageModule extends EcoPageFile>(file: string, pageModule: TPageModule): TPageModule;
170
168
  /**
171
169
  * Processes MDX-specific configuration dependencies including layout dependencies.
172
170
  * @param pagePath - Absolute path to the MDX page file
@@ -176,15 +174,6 @@ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
176
174
  private processDeclaredMdxSsrLazyDependencies;
177
175
  private collectDeclaredMdxSsrLazyDependencies;
178
176
  buildRouteRenderAssets(pagePath: string): Promise<ProcessedAsset[]>;
179
- /**
180
- * Imports a page module while normalizing React MDX modules to the same shape
181
- * as ordinary React page files.
182
- *
183
- * MDX page imports can expose `config` separately from the default export. The
184
- * React renderer reattaches that config to the page component so downstream
185
- * layout, dependency, and hydration logic can treat MDX and TSX pages the same.
186
- */
187
- protected importPageFile(file: string): Promise<ReactPageModule>;
188
177
  /**
189
178
  * Renders a full route response for the filesystem page pipeline.
190
179
  *
@@ -222,4 +211,3 @@ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
222
211
  */
223
212
  renderToResponse<P = Record<string, unknown>>(view: EcoComponent<P>, props: P, ctx: RenderToResponseContext): Promise<Response>;
224
213
  }
225
- export {};