@ecopages/react 0.2.0-alpha.3 → 0.2.0-alpha.30
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 +27 -39
- package/README.md +161 -18
- package/package.json +6 -6
- package/src/react-hmr-strategy.d.ts +42 -32
- package/src/react-hmr-strategy.js +99 -123
- package/src/react-renderer.d.ts +168 -41
- package/src/react-renderer.js +466 -163
- package/src/react.constants.d.ts +1 -0
- package/src/react.constants.js +4 -0
- package/src/react.plugin.d.ts +38 -111
- package/src/react.plugin.js +132 -61
- package/src/react.types.d.ts +88 -0
- package/src/react.types.js +0 -0
- package/src/router-adapter.d.ts +7 -14
- package/src/services/react-bundle.service.d.ts +15 -26
- package/src/services/react-bundle.service.js +44 -92
- 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 +17 -18
- package/src/services/react-hydration-asset.service.js +59 -65
- package/src/services/react-mdx-config-dependency.service.d.ts +36 -0
- package/src/services/react-mdx-config-dependency.service.js +122 -0
- package/src/services/react-page-module.service.d.ts +10 -2
- package/src/services/react-page-module.service.js +44 -37
- package/src/services/react-page-payload.service.d.ts +46 -0
- package/src/services/react-page-payload.service.js +67 -0
- package/src/services/react-runtime-bundle.service.d.ts +15 -13
- package/src/services/react-runtime-bundle.service.js +103 -180
- package/src/utils/client-graph-boundary-plugin.d.ts +1 -1
- package/src/utils/client-graph-boundary-plugin.js +149 -11
- package/src/utils/component-config-traversal.d.ts +36 -0
- package/src/utils/component-config-traversal.js +54 -0
- package/src/utils/declared-modules.d.ts +1 -1
- package/src/utils/declared-modules.js +7 -16
- package/src/utils/dynamic.test.browser.d.ts +1 -0
- package/src/utils/dynamic.test.browser.js +33 -0
- package/src/utils/hydration-scripts.d.ts +25 -6
- package/src/utils/hydration-scripts.js +150 -44
- package/src/utils/hydration-scripts.test.browser.d.ts +1 -0
- package/src/utils/hydration-scripts.test.browser.js +198 -0
- package/src/utils/reachability-analyzer.d.ts +12 -1
- package/src/utils/reachability-analyzer.js +101 -5
- package/src/utils/react-dom-runtime-interop-plugin.d.ts +5 -0
- package/src/utils/react-dom-runtime-interop-plugin.js +29 -0
- package/src/utils/react-mdx-loader-plugin.d.ts +1 -1
- package/src/utils/react-mdx-loader-plugin.js +13 -5
- package/src/utils/react-runtime-alias-map.d.ts +6 -0
- package/src/utils/react-runtime-alias-map.js +33 -0
- package/src/utils/use-sync-external-store-shim-plugin.d.ts +5 -0
- package/src/utils/use-sync-external-store-shim-plugin.js +41 -0
- package/src/react-hmr-strategy.ts +0 -444
- package/src/react-renderer.ts +0 -403
- package/src/react.plugin.ts +0 -241
- package/src/router-adapter.ts +0 -95
- package/src/services/react-bundle.service.ts +0 -212
- package/src/services/react-hmr-page-metadata-cache.ts +0 -24
- package/src/services/react-hydration-asset.service.ts +0 -260
- package/src/services/react-page-module.service.ts +0 -214
- package/src/services/react-runtime-bundle.service.ts +0 -271
- package/src/utils/client-graph-boundary-plugin.ts +0 -590
- 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 -338
- package/src/utils/reachability-analyzer.ts +0 -440
- package/src/utils/react-mdx-loader-plugin.ts +0 -40
|
@@ -1,96 +1,43 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { pathToFileURL } from "node:url";
|
|
3
2
|
import { HmrStrategy, HmrStrategyType } from "@ecopages/core/hmr/hmr-strategy";
|
|
4
|
-
import {
|
|
3
|
+
import { rewriteRuntimeSpecifierAliases } from "@ecopages/core/build/runtime-specifier-aliases";
|
|
4
|
+
import { createRuntimeSpecifierAliasPlugin } from "@ecopages/core/build/runtime-specifier-alias-plugin";
|
|
5
5
|
import { FileNotFoundError, fileSystem } from "@ecopages/file-system";
|
|
6
6
|
import { Logger } from "@ecopages/logger";
|
|
7
7
|
import { injectHmrHandler } from "./utils/hmr-scripts.js";
|
|
8
8
|
import { createClientGraphBoundaryPlugin } from "./utils/client-graph-boundary-plugin.js";
|
|
9
9
|
import { collectPageDeclaredModules, collectPageDeclaredModulesFromModule } from "./utils/declared-modules.js";
|
|
10
|
+
import { getReactClientGraphAllowSpecifiers } from "./utils/react-runtime-alias-map.js";
|
|
11
|
+
import { createUseSyncExternalStoreShimPlugin } from "./utils/use-sync-external-store-shim-plugin.js";
|
|
10
12
|
const appLogger = new Logger("[ReactHmrStrategy]");
|
|
11
13
|
class ReactHmrStrategy extends HmrStrategy {
|
|
14
|
+
type = HmrStrategyType.INTEGRATION;
|
|
15
|
+
mdxCompilerOptions;
|
|
16
|
+
ownedTemplateExtensions;
|
|
17
|
+
allTemplateExtensions;
|
|
18
|
+
async importNodePageModule(entrypointPath) {
|
|
19
|
+
return await this.context.importServerModule(entrypointPath);
|
|
20
|
+
}
|
|
12
21
|
/**
|
|
13
22
|
* Creates a new React HMR strategy instance.
|
|
14
23
|
*
|
|
15
|
-
* @param
|
|
16
|
-
* and the layouts directory for detecting layout file changes that require full
|
|
17
|
-
* page reloads instead of module-level HMR updates.
|
|
18
|
-
* @param pageMetadataCache - React-only cache of declared browser modules discovered during
|
|
19
|
-
* server rendering. This avoids re-importing unchanged page modules
|
|
20
|
-
* during save-time Fast Refresh rebuilds.
|
|
21
|
-
* @param mdxCompilerOptions - Optional MDX compiler options for processing .mdx files
|
|
22
|
-
* @param explicitGraphEnabled - Enables explicit graph mode for React HMR bundling.
|
|
23
|
-
* In explicit mode, HMR builds omit AST server-only stripping plugins in React paths.
|
|
24
|
+
* @param options - React HMR runtime services and behavior flags.
|
|
24
25
|
*/
|
|
25
|
-
|
|
26
|
+
context;
|
|
27
|
+
pageMetadataCache;
|
|
28
|
+
explicitGraphEnabled;
|
|
29
|
+
runtimeAliasMap;
|
|
30
|
+
constructor(options) {
|
|
26
31
|
super();
|
|
27
|
-
this.context = context;
|
|
28
|
-
this.pageMetadataCache = pageMetadataCache;
|
|
29
|
-
this.
|
|
30
|
-
this.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const srcDir = this.context.getSrcDir();
|
|
37
|
-
const rootDir = path.dirname(srcDir);
|
|
38
|
-
const outdir = path.join(path.resolve(this.context.getDistDir(), "..", ".."), ".server-modules");
|
|
39
|
-
const fileBaseName = path.basename(entrypointPath, path.extname(entrypointPath));
|
|
40
|
-
const fileHash = fileSystem.hash(entrypointPath);
|
|
41
|
-
const outputFileName = `${fileBaseName}-${fileHash}.js`;
|
|
42
|
-
const buildResult = await defaultBuildAdapter.build({
|
|
43
|
-
entrypoints: [entrypointPath],
|
|
44
|
-
root: rootDir,
|
|
45
|
-
outdir,
|
|
46
|
-
target: "node",
|
|
47
|
-
format: "esm",
|
|
48
|
-
sourcemap: "none",
|
|
49
|
-
splitting: false,
|
|
50
|
-
minify: false,
|
|
51
|
-
naming: outputFileName
|
|
52
|
-
});
|
|
53
|
-
if (!buildResult.success) {
|
|
54
|
-
const details = buildResult.logs.map((log) => log.message).join(" | ");
|
|
55
|
-
throw new Error(`Error transpiling React HMR page module: ${details}`);
|
|
56
|
-
}
|
|
57
|
-
const preferredOutputPath = path.join(outdir, outputFileName);
|
|
58
|
-
const compiledOutput = buildResult.outputs.find((output) => output.path === preferredOutputPath)?.path ?? buildResult.outputs.find((output) => output.path.endsWith(".js"))?.path;
|
|
59
|
-
if (!compiledOutput) {
|
|
60
|
-
throw new Error(`No transpiled output generated for React HMR page module: ${entrypointPath}`);
|
|
61
|
-
}
|
|
62
|
-
return await import(pathToFileURL(compiledOutput).href);
|
|
63
|
-
}
|
|
64
|
-
createUseSyncExternalStoreShimPlugin() {
|
|
65
|
-
return {
|
|
66
|
-
name: "react-hmr-use-sync-external-store-shim",
|
|
67
|
-
setup(build) {
|
|
68
|
-
build.onResolve({ filter: /^use-sync-external-store\/shim(?:\/index\.js)?$/ }, () => ({
|
|
69
|
-
path: "use-sync-external-store/shim",
|
|
70
|
-
namespace: "ecopages-react-hmr-shim"
|
|
71
|
-
}));
|
|
72
|
-
build.onLoad(
|
|
73
|
-
{ filter: /^use-sync-external-store\/shim$/, namespace: "ecopages-react-hmr-shim" },
|
|
74
|
-
() => ({
|
|
75
|
-
contents: "export { useSyncExternalStore } from 'react';",
|
|
76
|
-
loader: "js"
|
|
77
|
-
})
|
|
78
|
-
);
|
|
79
|
-
build.onLoad({ filter: /[\\/]use-sync-external-store[\\/]shim[\\/]index\.js$/ }, () => ({
|
|
80
|
-
contents: "export { useSyncExternalStore } from 'react';",
|
|
81
|
-
loader: "js"
|
|
82
|
-
}));
|
|
83
|
-
build.onLoad(
|
|
84
|
-
{
|
|
85
|
-
filter: /[\\/]use-sync-external-store[\\/]cjs[\\/]use-sync-external-store-shim\.development\.js$/
|
|
86
|
-
},
|
|
87
|
-
() => ({
|
|
88
|
-
contents: "export { useSyncExternalStore } from 'react';",
|
|
89
|
-
loader: "js"
|
|
90
|
-
})
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
};
|
|
32
|
+
this.context = options.context;
|
|
33
|
+
this.pageMetadataCache = options.pageMetadataCache;
|
|
34
|
+
this.runtimeAliasMap = options.runtimeAliasMap;
|
|
35
|
+
this.explicitGraphEnabled = options.explicitGraphEnabled ?? false;
|
|
36
|
+
this.mdxCompilerOptions = options.mdxCompilerOptions;
|
|
37
|
+
this.ownedTemplateExtensions = new Set(options.ownedTemplateExtensions ?? [".tsx"]);
|
|
38
|
+
this.allTemplateExtensions = [...options.allTemplateExtensions ?? [".tsx"]].sort(
|
|
39
|
+
(a, b) => b.length - a.length
|
|
40
|
+
);
|
|
94
41
|
}
|
|
95
42
|
/**
|
|
96
43
|
* Returns build plugins for React HMR bundling.
|
|
@@ -99,30 +46,56 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
99
46
|
* (including `node:*`) from breaking the browser bundle.
|
|
100
47
|
*/
|
|
101
48
|
getBuildPlugins(declaredModules) {
|
|
102
|
-
const allowSpecifiers =
|
|
103
|
-
|
|
104
|
-
"react"
|
|
105
|
-
|
|
106
|
-
"react/jsx-runtime",
|
|
107
|
-
"react/jsx-dev-runtime",
|
|
108
|
-
"react-dom/client",
|
|
109
|
-
...Array.from(this.context.getSpecifierMap().keys())
|
|
110
|
-
];
|
|
49
|
+
const allowSpecifiers = getReactClientGraphAllowSpecifiers(this.runtimeAliasMap.keys());
|
|
50
|
+
const runtimeAliasPlugin = createRuntimeSpecifierAliasPlugin(this.runtimeAliasMap, {
|
|
51
|
+
name: "react-hmr-runtime-specifier-alias"
|
|
52
|
+
});
|
|
111
53
|
return [
|
|
112
54
|
createClientGraphBoundaryPlugin({
|
|
113
55
|
absWorkingDir: path.dirname(this.context.getSrcDir()),
|
|
114
56
|
alwaysAllowSpecifiers: allowSpecifiers,
|
|
115
57
|
declaredModules
|
|
116
58
|
}),
|
|
59
|
+
...runtimeAliasPlugin ? [runtimeAliasPlugin] : [],
|
|
117
60
|
...this.context.getPlugins(),
|
|
118
|
-
|
|
61
|
+
createUseSyncExternalStoreShimPlugin({
|
|
62
|
+
name: "react-hmr-use-sync-external-store-shim",
|
|
63
|
+
namespace: "ecopages-react-hmr-shim"
|
|
64
|
+
})
|
|
119
65
|
];
|
|
120
66
|
}
|
|
121
67
|
isReactEntrypoint(filePath) {
|
|
122
|
-
if (filePath.endsWith(".
|
|
68
|
+
if (filePath.endsWith(".mdx")) {
|
|
69
|
+
return this.mdxCompilerOptions !== void 0;
|
|
70
|
+
}
|
|
71
|
+
if (!filePath.endsWith(".tsx")) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
if (!this.isRouteTemplate(filePath)) {
|
|
123
75
|
return true;
|
|
124
76
|
}
|
|
125
|
-
|
|
77
|
+
const templateExtension = this.resolveTemplateExtension(filePath);
|
|
78
|
+
if (!templateExtension) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return this.ownedTemplateExtensions.has(templateExtension);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Returns true when a route file uses a compound extension like `page.foo.tsx`.
|
|
85
|
+
*
|
|
86
|
+
* @remarks
|
|
87
|
+
* React integration owns plain `.tsx` route templates. Compound extensions in
|
|
88
|
+
* pages/layouts are integration-specific route templates and should not be
|
|
89
|
+
* claimed by React HMR strategy.
|
|
90
|
+
*/
|
|
91
|
+
isRouteTemplate(filePath) {
|
|
92
|
+
return filePath.startsWith(this.context.getPagesDir()) || filePath.startsWith(this.context.getLayoutsDir());
|
|
93
|
+
}
|
|
94
|
+
resolveTemplateExtension(filePath) {
|
|
95
|
+
return this.allTemplateExtensions.find((extension) => filePath.endsWith(extension));
|
|
96
|
+
}
|
|
97
|
+
ownsWatchedEntrypoint(filePath) {
|
|
98
|
+
return this.pageMetadataCache.ownsEntrypoint(filePath);
|
|
126
99
|
}
|
|
127
100
|
/**
|
|
128
101
|
* Determines if the file is a React/MDX entrypoint that's registered for HMR.
|
|
@@ -136,6 +109,9 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
136
109
|
if (watchedFiles.size === 0) {
|
|
137
110
|
return false;
|
|
138
111
|
}
|
|
112
|
+
if (watchedFiles.has(filePath)) {
|
|
113
|
+
return this.ownsWatchedEntrypoint(filePath);
|
|
114
|
+
}
|
|
139
115
|
return this.isReactEntrypoint(filePath);
|
|
140
116
|
}
|
|
141
117
|
/**
|
|
@@ -174,8 +150,12 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
174
150
|
if (isLayout) {
|
|
175
151
|
appLogger.debug(`Detected layout file change: ${_filePath}`);
|
|
176
152
|
}
|
|
177
|
-
const
|
|
178
|
-
this.
|
|
153
|
+
const changedEntrypointOutput = watchedFiles.get(_filePath);
|
|
154
|
+
if (changedEntrypointOutput && !this.ownsWatchedEntrypoint(_filePath)) {
|
|
155
|
+
appLogger.debug(`Skipping non-React watched entrypoint: ${_filePath}`);
|
|
156
|
+
return { type: "none" };
|
|
157
|
+
}
|
|
158
|
+
const entrypointsToBuild = changedEntrypointOutput ? [[_filePath, changedEntrypointOutput]] : watchedFiles.entries();
|
|
179
159
|
const updates = [];
|
|
180
160
|
for (const [entrypoint, outputUrl] of entrypointsToBuild) {
|
|
181
161
|
if (!this.isReactEntrypoint(entrypoint)) {
|
|
@@ -235,16 +215,13 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
235
215
|
const mdxPlugin = createReactMdxLoaderPlugin(this.mdxCompilerOptions);
|
|
236
216
|
plugins.unshift(mdxPlugin);
|
|
237
217
|
}
|
|
238
|
-
const result = await
|
|
218
|
+
const result = await this.context.getBrowserBundleService().bundle({
|
|
219
|
+
profile: "hmr-entrypoint",
|
|
239
220
|
entrypoints: [entrypointPath],
|
|
240
221
|
outdir: tempDir,
|
|
241
222
|
naming: `[name].[hash].tmp`,
|
|
242
|
-
target: "browser",
|
|
243
|
-
format: "esm",
|
|
244
|
-
sourcemap: "none",
|
|
245
223
|
plugins,
|
|
246
|
-
minify: false
|
|
247
|
-
external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime", "react-dom/client"]
|
|
224
|
+
minify: false
|
|
248
225
|
});
|
|
249
226
|
if (!result.success) {
|
|
250
227
|
appLogger.error(`Failed to build ${entrypointPath}:`, result.logs);
|
|
@@ -255,13 +232,33 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
255
232
|
appLogger.error(`No output file generated for ${entrypointPath}`);
|
|
256
233
|
return false;
|
|
257
234
|
}
|
|
258
|
-
const
|
|
235
|
+
const resolvedTempFile = await this.resolveTempOutputPath(tempFile);
|
|
236
|
+
if (!resolvedTempFile) {
|
|
237
|
+
appLogger.debug(`Skipping stale temp output for ${outputUrl}: ${tempFile}`);
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
const processed = await this.processOutput(resolvedTempFile, outputPath, outputUrl);
|
|
259
241
|
return processed;
|
|
260
242
|
} catch (error) {
|
|
261
243
|
appLogger.error(`Error bundling ${entrypointPath}:`, error);
|
|
262
244
|
return false;
|
|
263
245
|
}
|
|
264
246
|
}
|
|
247
|
+
async resolveTempOutputPath(tempPath) {
|
|
248
|
+
if (fileSystem.exists(tempPath)) {
|
|
249
|
+
return tempPath;
|
|
250
|
+
}
|
|
251
|
+
if (!tempPath.includes("[hash]")) {
|
|
252
|
+
return tempPath;
|
|
253
|
+
}
|
|
254
|
+
const directory = path.dirname(tempPath);
|
|
255
|
+
const pattern = path.basename(tempPath).replaceAll("[hash]", "*");
|
|
256
|
+
const matches = await fileSystem.glob([pattern], { cwd: directory });
|
|
257
|
+
if (matches.length === 0) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
return path.isAbsolute(matches[0]) ? matches[0] : path.join(directory, matches[0]);
|
|
261
|
+
}
|
|
265
262
|
/**
|
|
266
263
|
* Encodes dynamic route segments (brackets) in file paths.
|
|
267
264
|
* Converts `[slug]` to `_slug_` to avoid filesystem issues.
|
|
@@ -270,7 +267,7 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
270
267
|
return filepath.replace(/\[([^\]]+)\]/g, "_$1_");
|
|
271
268
|
}
|
|
272
269
|
/**
|
|
273
|
-
* Processes bundled output
|
|
270
|
+
* Processes bundled output and injects the React HMR handler.
|
|
274
271
|
* Writes to temp file first, then renames atomically to avoid conflicts.
|
|
275
272
|
*
|
|
276
273
|
* @param tempPath - Path to the temporary bundled file
|
|
@@ -285,7 +282,7 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
285
282
|
}
|
|
286
283
|
try {
|
|
287
284
|
let code = await fileSystem.readFile(tempPath);
|
|
288
|
-
code = this.
|
|
285
|
+
code = rewriteRuntimeSpecifierAliases(code, this.runtimeAliasMap);
|
|
289
286
|
code = injectHmrHandler(code);
|
|
290
287
|
await fileSystem.writeAsync(finalPath, code);
|
|
291
288
|
await fileSystem.removeAsync(tempPath).catch(() => {
|
|
@@ -305,27 +302,6 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
305
302
|
return false;
|
|
306
303
|
}
|
|
307
304
|
}
|
|
308
|
-
/**
|
|
309
|
-
* Replaces bare specifiers with runtime URLs.
|
|
310
|
-
*
|
|
311
|
-
* Handles both static imports and dynamic imports.
|
|
312
|
-
*
|
|
313
|
-
* @param code - The bundled code to transform
|
|
314
|
-
* @returns The transformed code with runtime URLs
|
|
315
|
-
*/
|
|
316
|
-
replaceBareSpecifiers(code) {
|
|
317
|
-
const specifierMap = this.context.getSpecifierMap();
|
|
318
|
-
if (specifierMap.size === 0) {
|
|
319
|
-
return code;
|
|
320
|
-
}
|
|
321
|
-
let result = code;
|
|
322
|
-
for (const [bareSpec, runtimeUrl] of specifierMap.entries()) {
|
|
323
|
-
const escaped = bareSpec.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
324
|
-
result = result.replace(new RegExp(`from\\s*["']${escaped}["']`, "g"), `from "${runtimeUrl}"`);
|
|
325
|
-
result = result.replace(new RegExp(`import\\(["']${escaped}["']\\)`, "g"), `import("${runtimeUrl}")`);
|
|
326
|
-
}
|
|
327
|
-
return result;
|
|
328
|
-
}
|
|
329
305
|
}
|
|
330
306
|
export {
|
|
331
307
|
ReactHmrStrategy
|
package/src/react-renderer.d.ts
CHANGED
|
@@ -2,16 +2,24 @@
|
|
|
2
2
|
* This module contains the React renderer
|
|
3
3
|
* @module
|
|
4
4
|
*/
|
|
5
|
-
import type { ComponentRenderInput, ComponentRenderResult, EcoComponent,
|
|
6
|
-
import { IntegrationRenderer, type RenderToResponseContext } from '@ecopages/core/route-renderer/integration-renderer';
|
|
5
|
+
import type { ComponentRenderInput, ComponentRenderResult, EcoComponent, EcoPageFile, IntegrationRendererRenderOptions, RouteRendererBody } from '@ecopages/core';
|
|
6
|
+
import { IntegrationRenderer, type RenderToResponseContext, type RouteModuleLoadOptions } from '@ecopages/core/route-renderer/integration-renderer';
|
|
7
7
|
import type { ProcessedAsset } from '@ecopages/core/services/asset-processing-service';
|
|
8
|
-
import {
|
|
9
|
-
import type {
|
|
10
|
-
import type { ReactRouterAdapter } from './router-adapter.js';
|
|
8
|
+
import type { ReactNode } from 'react';
|
|
9
|
+
import type { ReactRendererConfig } from './react.types.js';
|
|
11
10
|
import { ReactBundleService } from './services/react-bundle.service.js';
|
|
12
|
-
import {
|
|
11
|
+
import { ReactMdxConfigDependencyService } from './services/react-mdx-config-dependency.service.js';
|
|
13
12
|
import { ReactPageModuleService } from './services/react-page-module.service.js';
|
|
13
|
+
import { ReactPagePayloadService } from './services/react-page-payload.service.js';
|
|
14
14
|
import { ReactHydrationAssetService } from './services/react-hydration-asset.service.js';
|
|
15
|
+
export type { ReactRendererConfig } from './react.types.js';
|
|
16
|
+
type ReactRuntimeModules = {
|
|
17
|
+
react: Pick<typeof import('react'), 'createElement' | 'Fragment'>;
|
|
18
|
+
reactDomServer: Pick<typeof import('react-dom/server'), 'renderToReadableStream' | 'renderToString'>;
|
|
19
|
+
};
|
|
20
|
+
export type ReactRendererOptions = ConstructorParameters<typeof IntegrationRenderer>[0] & {
|
|
21
|
+
reactConfig?: ReactRendererConfig;
|
|
22
|
+
};
|
|
15
23
|
/**
|
|
16
24
|
* Error thrown when an error occurs while rendering a React component.
|
|
17
25
|
*/
|
|
@@ -32,75 +40,194 @@ export declare class BundleError extends Error {
|
|
|
32
40
|
export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
|
|
33
41
|
name: string;
|
|
34
42
|
componentDirectory: string;
|
|
35
|
-
private
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
private reactRuntimeModules?;
|
|
44
|
+
private readonly routerAdapter?;
|
|
45
|
+
private readonly mdxCompilerOptions?;
|
|
46
|
+
private readonly mdxExtensions;
|
|
47
|
+
private readonly hmrPageMetadataCache?;
|
|
40
48
|
/**
|
|
41
49
|
* Enables explicit graph behavior for React page-entry bundling.
|
|
42
50
|
*
|
|
43
51
|
* When true, page-entry bundles disable AST server-only stripping and rely
|
|
44
52
|
* on explicit dependency declarations for browser graph composition.
|
|
45
53
|
*/
|
|
46
|
-
|
|
54
|
+
private readonly explicitGraphEnabled;
|
|
47
55
|
/** @internal */
|
|
48
56
|
readonly bundleService: ReactBundleService;
|
|
49
57
|
/** @internal */
|
|
50
58
|
readonly pageModuleService: ReactPageModuleService;
|
|
51
59
|
/** @internal */
|
|
52
60
|
readonly hydrationAssetService: ReactHydrationAssetService;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
});
|
|
61
|
+
/** @internal */
|
|
62
|
+
readonly pagePayloadService: ReactPagePayloadService;
|
|
63
|
+
/** @internal */
|
|
64
|
+
readonly mdxConfigDependencyService: ReactMdxConfigDependencyService;
|
|
65
|
+
constructor(options: ReactRendererOptions);
|
|
59
66
|
protected shouldRenderPageComponent(): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Reads the declared integration name for a component or layout.
|
|
69
|
+
*
|
|
70
|
+
* We honor both the explicit `config.integration` override and injected
|
|
71
|
+
* `config.__eco.integration` metadata because pages can arrive here through
|
|
72
|
+
* authored config as well as build-time component metadata.
|
|
73
|
+
*/
|
|
74
|
+
private getComponentIntegration;
|
|
75
|
+
/**
|
|
76
|
+
* Returns whether a component should stay inside the React render lane.
|
|
77
|
+
*
|
|
78
|
+
* Components without explicit integration metadata are treated as React-owned
|
|
79
|
+
* here because this renderer only receives them after the route pipeline has
|
|
80
|
+
* already selected the React integration.
|
|
81
|
+
*/
|
|
82
|
+
private isReactManagedComponent;
|
|
83
|
+
private getComponentRequires;
|
|
84
|
+
private getRouterDocumentAttributes;
|
|
85
|
+
/**
|
|
86
|
+
* Commits a framework-agnostic component to React semantics.
|
|
87
|
+
*
|
|
88
|
+
* This is one of the two real cast boundaries in this file. Core keeps
|
|
89
|
+
* `EcoComponent` broad so integrations can share the same public surface; once
|
|
90
|
+
* the React renderer is executing, `createElement()` needs a concrete React
|
|
91
|
+
* component signature.
|
|
92
|
+
*/
|
|
93
|
+
private asReactComponent;
|
|
94
|
+
/**
|
|
95
|
+
* Commits a mixed-shell component to the string-returning contract required by
|
|
96
|
+
* non-React layouts and HTML templates.
|
|
97
|
+
*
|
|
98
|
+
* This is the second real cast boundary: once we decide a shell is not managed
|
|
99
|
+
* by React, we call it directly and require serialized HTML back.
|
|
100
|
+
*/
|
|
101
|
+
private asNonReactShellComponent;
|
|
102
|
+
protected resolveReactRuntimeModules(): ReactRuntimeModules;
|
|
103
|
+
private getReactRuntimeModules;
|
|
104
|
+
/**
|
|
105
|
+
* Appends route hydration assets for a concrete page/view file to the current
|
|
106
|
+
* HTML transformer state.
|
|
107
|
+
*/
|
|
108
|
+
private appendHydrationAssetsForFile;
|
|
109
|
+
/**
|
|
110
|
+
* Renders a non-React layout or HTML template and enforces that mixed shells
|
|
111
|
+
* return serialized HTML.
|
|
112
|
+
*
|
|
113
|
+
* The React renderer can compose through another integration's shell, but only
|
|
114
|
+
* if that shell yields a string that can be inserted into the final document.
|
|
115
|
+
*/
|
|
116
|
+
private renderNonReactShellComponent;
|
|
117
|
+
/**
|
|
118
|
+
* Renders one React component while preserving already-resolved child HTML.
|
|
119
|
+
*
|
|
120
|
+
* When nested foreign-subtree resolution has already produced child HTML for this
|
|
121
|
+
* component, the child payload must remain raw SSR output rather than a React
|
|
122
|
+
* string child, otherwise React would escape it. This helper renders a unique
|
|
123
|
+
* token through React and swaps that token back to the resolved HTML
|
|
124
|
+
* afterward.
|
|
125
|
+
*
|
|
126
|
+
* @param input Component render input for the current render step.
|
|
127
|
+
* @param context React-specific render context for stable token generation.
|
|
128
|
+
* @returns Serialized component HTML with resolved child markup preserved.
|
|
129
|
+
*/
|
|
130
|
+
private renderComponentHtml;
|
|
131
|
+
/**
|
|
132
|
+
* Restores raw child HTML that was temporarily replaced by a token during React SSR.
|
|
133
|
+
*
|
|
134
|
+
* Queued foreign-subtree resolution may render children through a fragment path before all
|
|
135
|
+
* nested integration tokens are resolved. When that happens, React must never see
|
|
136
|
+
* the resolved child HTML as a normal string child or it would escape it. The
|
|
137
|
+
* runtime context stores the placeholder token and the raw child HTML so the
|
|
138
|
+
* fragment render path can reinsert it before foreign-subtree tokens are handled.
|
|
139
|
+
*/
|
|
140
|
+
private restoreRuntimeChildHtml;
|
|
141
|
+
/**
|
|
142
|
+
* Renders queued child content through React and then resolves nested foreign-subtree tokens.
|
|
143
|
+
*
|
|
144
|
+
* This path is only used for children that were deferred while React rendered the
|
|
145
|
+
* parent component. It first restores any raw child HTML placeholders owned by the
|
|
146
|
+
* current runtime context, then asks the shared queued foreign-subtree resolver to swap
|
|
147
|
+
* foreign integration tokens with their resolved HTML.
|
|
148
|
+
*/
|
|
149
|
+
private renderQueuedChildrenToHtml;
|
|
150
|
+
/**
|
|
151
|
+
* Resolves queued renderer-owned foreign-subtree tokens produced during React component rendering.
|
|
152
|
+
*
|
|
153
|
+
* React components can enqueue nested foreign subtrees while the parent HTML is being
|
|
154
|
+
* rendered. This delegates to the shared renderer-owned queue resolver but keeps
|
|
155
|
+
* the React-specific child rendering behavior local so raw child HTML and React's
|
|
156
|
+
* fragment rendering semantics stay coordinated.
|
|
157
|
+
*/
|
|
158
|
+
private resolveQueuedForeignSubtreeHtml;
|
|
159
|
+
private buildHydrationProps;
|
|
160
|
+
/**
|
|
161
|
+
* Builds the extra document props needed when React renders through a non-React HTML shell.
|
|
162
|
+
*
|
|
163
|
+
* Router-backed React pages still need to publish the canonical page-data script
|
|
164
|
+
* even when the outer document shell belongs to another integration.
|
|
165
|
+
*/
|
|
166
|
+
private buildNonReactDocumentProps;
|
|
167
|
+
/**
|
|
168
|
+
* Renders a foreign integration component that participates in React composition.
|
|
169
|
+
*
|
|
170
|
+
* Non-React components must resolve to serialized HTML so React can embed them as
|
|
171
|
+
* mixed-shell children. Any component-owned dependencies still need to flow
|
|
172
|
+
* through the shared dependency resolver before queued foreign-subtree tokens are finalized.
|
|
173
|
+
*/
|
|
174
|
+
private renderForeignComponentWithSerializedHtml;
|
|
175
|
+
/**
|
|
176
|
+
* Renders a React-owned component and attaches island hydration metadata when possible.
|
|
177
|
+
*
|
|
178
|
+
* This path keeps React-owned SSR, queued foreign-subtree resolution, and optional
|
|
179
|
+
* island hydration wiring together so the public `renderComponent()` method can
|
|
180
|
+
* read as orchestration rather than implementation detail.
|
|
181
|
+
*/
|
|
182
|
+
private renderReactManagedComponent;
|
|
60
183
|
/**
|
|
61
184
|
* Renders a React component for component-level orchestration.
|
|
62
185
|
*
|
|
63
186
|
* Behavior:
|
|
64
187
|
* - SSR always returns the component's own root HTML (no synthetic wrapper).
|
|
65
|
-
* -
|
|
66
|
-
*
|
|
67
|
-
* -
|
|
188
|
+
* - When an explicit component instance id is provided, a stable
|
|
189
|
+
* `data-eco-component-id` attribute is attached so island hydration can target it.
|
|
190
|
+
* - Without an explicit instance id, component renders remain plain SSR output.
|
|
191
|
+
* - When resolved child HTML is provided, that foreign subtree is treated as a pure SSR
|
|
192
|
+
* composition step and does not emit hydration assets for the parent wrapper.
|
|
68
193
|
*
|
|
69
194
|
* This preserves DOM shape for global CSS/layout selectors while keeping a
|
|
70
195
|
* deterministic mount target per component instance.
|
|
71
196
|
*/
|
|
72
197
|
renderComponent(input: ComponentRenderInput): Promise<ComponentRenderResult>;
|
|
198
|
+
protected createForeignChildRuntime(options: {
|
|
199
|
+
renderInput: ComponentRenderInput;
|
|
200
|
+
rendererCache: Map<string, IntegrationRenderer<any>>;
|
|
201
|
+
}): import("@ecopages/core").ForeignChildRuntime;
|
|
73
202
|
/**
|
|
74
203
|
* Checks if the given file path corresponds to an MDX file based on configured extensions.
|
|
75
204
|
* @param filePath - The file path to check
|
|
76
205
|
* @returns True if the file is an MDX file
|
|
77
206
|
*/
|
|
78
207
|
isMdxFile(filePath: string): boolean;
|
|
208
|
+
protected usesIntegrationPageImporter(file: string): boolean;
|
|
209
|
+
protected importIntegrationPageFile(file: string, options?: RouteModuleLoadOptions): Promise<EcoPageFile>;
|
|
210
|
+
protected normalizeImportedPageFile<TPageModule extends EcoPageFile>(file: string, pageModule: TPageModule): TPageModule;
|
|
211
|
+
buildPageBrowserGraph(pagePath: string): Promise<{
|
|
212
|
+
assets: ProcessedAsset[];
|
|
213
|
+
}>;
|
|
79
214
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
215
|
+
* Renders a full route response for the filesystem page pipeline.
|
|
216
|
+
*
|
|
217
|
+
* This path receives already-resolved route metadata, layout, locals, and HTML
|
|
218
|
+
* template instances from the shared renderer orchestration. Its main job is to
|
|
219
|
+
* serialize only the browser-safe page payload, compose the mixed React/non-
|
|
220
|
+
* React shell tree, and hand the result back as a document body.
|
|
83
221
|
*/
|
|
84
|
-
private processMdxConfigDependencies;
|
|
85
|
-
buildRouteRenderAssets(pagePath: string): Promise<ProcessedAsset[]>;
|
|
86
|
-
protected importPageFile(file: string): Promise<EcoPageFile<{
|
|
87
|
-
config?: EcoComponentConfig;
|
|
88
|
-
}>>;
|
|
89
222
|
render({ params, query, props, locals, pageLocals, metadata, Page, Layout, HtmlTemplate, pageProps, }: IntegrationRendererRenderOptions<ReactNode>): Promise<RouteRendererBody>;
|
|
223
|
+
protected getDocumentAttributes(): Record<string, string> | undefined;
|
|
90
224
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* On dynamic pages with `cache: 'dynamic'`, middleware populates `locals` with
|
|
94
|
-
* request-scoped data (e.g., session). This data needs to be serialized to the
|
|
95
|
-
* client for hydration to match the server-rendered output.
|
|
96
|
-
*
|
|
97
|
-
* On static pages, `locals` is a Proxy that throws `LocalsAccessError` on access
|
|
98
|
-
* to prevent accidental use. This method safely detects that case and returns
|
|
99
|
-
* `undefined` instead of throwing.
|
|
225
|
+
* Renders an arbitrary React view through the application's HTML shell.
|
|
100
226
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
227
|
+
* Unlike route rendering, this path starts from a single component rather than a
|
|
228
|
+
* page module discovered by the router. It still needs to resolve metadata,
|
|
229
|
+
* layout dependencies, and hydration assets so direct `ctx.render()` calls match
|
|
230
|
+
* normal page responses.
|
|
103
231
|
*/
|
|
104
|
-
private getSerializableLocals;
|
|
105
232
|
renderToResponse<P = Record<string, unknown>>(view: EcoComponent<P>, props: P, ctx: RenderToResponseContext): Promise<Response>;
|
|
106
233
|
}
|