@ecopages/react 0.2.0-alpha.8 → 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.
- package/CHANGELOG.md +13 -11
- package/README.md +10 -0
- package/package.json +6 -6
- package/src/react-hmr-strategy.d.ts +4 -2
- package/src/react-hmr-strategy.js +36 -3
- package/src/react-renderer.d.ts +25 -37
- package/src/react-renderer.js +190 -142
- package/src/react.plugin.d.ts +0 -12
- package/src/react.plugin.js +2 -13
- package/src/services/react-bundle.service.d.ts +3 -1
- package/src/services/react-bundle.service.js +20 -2
- package/src/services/react-hmr-page-metadata-cache.d.ts +9 -0
- package/src/services/react-hmr-page-metadata-cache.js +18 -2
- package/src/services/react-hydration-asset.service.d.ts +7 -6
- package/src/services/react-hydration-asset.service.js +26 -14
- package/src/services/react-page-module.service.js +5 -2
- package/src/services/react-runtime-bundle.service.d.ts +2 -0
- package/src/services/react-runtime-bundle.service.js +5 -0
- package/src/utils/client-graph-boundary-plugin.js +2 -2
- package/src/utils/declared-modules.js +4 -1
- package/src/utils/hydration-scripts.d.ts +1 -3
- package/src/utils/hydration-scripts.js +31 -19
- package/src/react-hmr-strategy.ts +0 -386
- package/src/react-renderer.ts +0 -803
- package/src/react.plugin.ts +0 -276
- package/src/router-adapter.ts +0 -95
- package/src/services/react-bundle.service.ts +0 -108
- package/src/services/react-hmr-page-metadata-cache.ts +0 -24
- package/src/services/react-hydration-asset.service.ts +0 -263
- package/src/services/react-page-module.service.ts +0 -224
- package/src/services/react-runtime-bundle.service.ts +0 -172
- package/src/utils/client-graph-boundary-plugin.ts +0 -831
- package/src/utils/client-only.ts +0 -27
- package/src/utils/declared-modules.ts +0 -99
- package/src/utils/dynamic.ts +0 -27
- package/src/utils/hmr-scripts.ts +0 -47
- package/src/utils/html-boundary.ts +0 -66
- package/src/utils/hydration-scripts.ts +0 -459
- package/src/utils/reachability-analyzer.ts +0 -593
- package/src/utils/react-dom-runtime-interop-plugin.ts +0 -33
- package/src/utils/react-mdx-loader-plugin.ts +0 -63
- package/src/utils/react-runtime-specifier-map.ts +0 -45
- 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
|
-
## [
|
|
7
|
+
## [0.2.1] — 2026-04-16
|
|
8
8
|
|
|
9
|
-
###
|
|
9
|
+
### Bug Fixes
|
|
10
10
|
|
|
11
|
-
-
|
|
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
|
-
###
|
|
13
|
+
### Features
|
|
16
14
|
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
64
|
-
"@ecopages/logger": "
|
|
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.
|
|
68
|
-
"oxc-transform": "^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,
|
|
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
|
|
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
|
|
53
|
-
const
|
|
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
|
|
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(() => {
|
package/src/react-renderer.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* This module contains the React renderer
|
|
3
3
|
* @module
|
|
4
4
|
*/
|
|
5
|
-
import type { ComponentRenderInput, ComponentRenderResult, EcoComponent,
|
|
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
|
|
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
|
-
*
|
|
123
|
+
* Renders one React component boundary while preserving already-resolved child HTML.
|
|
137
124
|
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
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
|
|
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 {};
|