@ecopages/react 0.2.0-alpha.5 → 0.2.0-alpha.51
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/README.md +152 -29
- package/package.json +24 -12
- package/src/eco-embed.d.ts +11 -0
- package/src/eco-embed.js +11 -0
- package/src/react-hmr-strategy.d.ts +65 -43
- package/src/react-hmr-strategy.js +298 -145
- package/src/react-renderer.d.ts +169 -42
- package/src/react-renderer.js +484 -164
- package/src/react.constants.d.ts +1 -0
- package/src/react.constants.js +4 -0
- package/src/react.plugin.d.ts +40 -111
- package/src/react.plugin.js +136 -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/runtime/use-sync-external-store-with-selector.d.ts +3 -0
- package/src/runtime/use-sync-external-store-with-selector.js +56 -0
- package/src/services/react-bundle.service.d.ts +22 -35
- package/src/services/react-bundle.service.js +41 -105
- 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 +28 -19
- package/src/services/react-hydration-asset.service.js +85 -66
- 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 +47 -39
- 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 +20 -13
- package/src/services/react-runtime-bundle.service.js +146 -179
- package/src/utils/client-graph-boundary-plugin.d.ts +1 -1
- package/src/utils/client-graph-boundary-plugin.js +80 -3
- 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 +27 -6
- package/src/utils/hydration-scripts.js +177 -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/react-dom-runtime-interop-plugin.d.ts +5 -0
- package/src/utils/react-dom-runtime-interop-plugin.js +38 -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 +8 -0
- package/src/utils/react-runtime-alias-map.js +90 -0
- package/CHANGELOG.md +0 -67
- package/src/react-hmr-strategy.ts +0 -455
- 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 -217
- 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 -710
- 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 -593
- package/src/utils/react-mdx-loader-plugin.ts +0 -40
|
@@ -1,139 +1,107 @@
|
|
|
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 { RESOLVED_ASSETS_DIR } from "@ecopages/core/constants";
|
|
4
|
+
import { createBrowserRuntimeImportRewritePlugin } from "@ecopages/core/build/browser-runtime-import-rewrite-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 { createReactMdxLoaderPlugin } from "./utils/react-mdx-loader-plugin.js";
|
|
11
|
+
import { getReactClientGraphAllowSpecifiers } from "./utils/react-runtime-alias-map.js";
|
|
10
12
|
const appLogger = new Logger("[ReactHmrStrategy]");
|
|
11
13
|
class ReactHmrStrategy extends HmrStrategy {
|
|
12
|
-
/**
|
|
13
|
-
* Creates a new React HMR strategy instance.
|
|
14
|
-
*
|
|
15
|
-
* @param context - The HMR context providing access to watched files, plugins, build directories,
|
|
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
|
-
*/
|
|
25
|
-
constructor(context, pageMetadataCache, mdxCompilerOptions, explicitGraphEnabled = false) {
|
|
26
|
-
super();
|
|
27
|
-
this.context = context;
|
|
28
|
-
this.pageMetadataCache = pageMetadataCache;
|
|
29
|
-
this.explicitGraphEnabled = explicitGraphEnabled;
|
|
30
|
-
this.mdxCompilerOptions = mdxCompilerOptions;
|
|
31
|
-
}
|
|
32
14
|
type = HmrStrategyType.INTEGRATION;
|
|
33
15
|
mdxCompilerOptions;
|
|
34
|
-
|
|
16
|
+
ownedTemplateExtensions;
|
|
17
|
+
allTemplateExtensions;
|
|
35
18
|
async importNodePageModule(entrypointPath) {
|
|
36
|
-
|
|
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);
|
|
19
|
+
return await this.context.importServerModule(entrypointPath);
|
|
63
20
|
}
|
|
64
21
|
/**
|
|
65
|
-
*
|
|
66
|
-
* `useSyncExternalStore`.
|
|
22
|
+
* Creates a new React HMR strategy instance.
|
|
67
23
|
*
|
|
68
|
-
*
|
|
69
|
-
* dependency to support React 16/17. On React 18+ the `/shim` export is
|
|
70
|
-
* already a pass-through, but without this plugin esbuild would bundle
|
|
71
|
-
* the full CJS shim (including `process.env` branching) into the browser
|
|
72
|
-
* bundle. The plugin short-circuits the resolution so only a single clean
|
|
73
|
-
* ESM re-export is emitted.
|
|
24
|
+
* @param options - React HMR runtime services and behavior flags.
|
|
74
25
|
*/
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
build.onLoad({ filter: /[\\/]use-sync-external-store[\\/]shim[\\/]index\.js$/ }, () => ({
|
|
91
|
-
contents: "export { useSyncExternalStore } from 'react';",
|
|
92
|
-
loader: "js"
|
|
93
|
-
}));
|
|
94
|
-
build.onLoad(
|
|
95
|
-
{
|
|
96
|
-
filter: /[\\/]use-sync-external-store[\\/]cjs[\\/]use-sync-external-store-shim\.development\.js$/
|
|
97
|
-
},
|
|
98
|
-
() => ({
|
|
99
|
-
contents: "export { useSyncExternalStore } from 'react';",
|
|
100
|
-
loader: "js"
|
|
101
|
-
})
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
};
|
|
26
|
+
context;
|
|
27
|
+
pageMetadataCache;
|
|
28
|
+
explicitGraphEnabled;
|
|
29
|
+
runtimeManifest;
|
|
30
|
+
constructor(options) {
|
|
31
|
+
super();
|
|
32
|
+
this.context = options.context;
|
|
33
|
+
this.pageMetadataCache = options.pageMetadataCache;
|
|
34
|
+
this.runtimeManifest = options.runtimeManifest;
|
|
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
|
+
);
|
|
105
41
|
}
|
|
106
42
|
/**
|
|
107
43
|
* Returns build plugins for React HMR bundling.
|
|
108
44
|
*
|
|
109
45
|
* Includes the client graph boundary plugin to prevent undeclared imports
|
|
110
46
|
* (including `node:*`) from breaking the browser bundle.
|
|
47
|
+
*
|
|
48
|
+
* @remarks
|
|
49
|
+
* HMR builds receive the React runtime manifest and rewrite manifest-owned
|
|
50
|
+
* runtime imports to concrete asset URLs before module resolution.
|
|
111
51
|
*/
|
|
112
52
|
getBuildPlugins(declaredModules) {
|
|
113
|
-
const allowSpecifiers =
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
"react
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
...Array.from(this.context.getSpecifierMap().keys())
|
|
121
|
-
];
|
|
53
|
+
const allowSpecifiers = getReactClientGraphAllowSpecifiers(
|
|
54
|
+
this.runtimeManifest.assets.map((asset) => asset.specifier)
|
|
55
|
+
);
|
|
56
|
+
const runtimeRewritePlugin = createBrowserRuntimeImportRewritePlugin({
|
|
57
|
+
name: "react-hmr-runtime-import-rewrite",
|
|
58
|
+
manifest: this.runtimeManifest
|
|
59
|
+
});
|
|
122
60
|
return [
|
|
123
61
|
createClientGraphBoundaryPlugin({
|
|
124
62
|
absWorkingDir: path.dirname(this.context.getSrcDir()),
|
|
125
63
|
alwaysAllowSpecifiers: allowSpecifiers,
|
|
126
64
|
declaredModules
|
|
127
65
|
}),
|
|
128
|
-
...
|
|
129
|
-
this.
|
|
66
|
+
...runtimeRewritePlugin ? [runtimeRewritePlugin] : [],
|
|
67
|
+
...this.context.getPlugins()
|
|
130
68
|
];
|
|
131
69
|
}
|
|
132
70
|
isReactEntrypoint(filePath) {
|
|
133
|
-
if (filePath.endsWith(".
|
|
71
|
+
if (filePath.endsWith(".mdx")) {
|
|
72
|
+
return this.mdxCompilerOptions !== void 0;
|
|
73
|
+
}
|
|
74
|
+
if (!filePath.endsWith(".tsx")) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const templateExtension = this.resolveTemplateExtension(filePath);
|
|
78
|
+
if (templateExtension && templateExtension !== ".tsx") {
|
|
79
|
+
return this.ownedTemplateExtensions.has(templateExtension);
|
|
80
|
+
}
|
|
81
|
+
if (!this.isRouteTemplate(filePath)) {
|
|
134
82
|
return true;
|
|
135
83
|
}
|
|
136
|
-
|
|
84
|
+
if (!templateExtension) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
return this.ownedTemplateExtensions.has(templateExtension);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Returns true when a route file uses a compound extension like `page.foo.tsx`.
|
|
91
|
+
*
|
|
92
|
+
* @remarks
|
|
93
|
+
* React integration owns plain `.tsx` route templates. Compound extensions in
|
|
94
|
+
* pages/layouts are integration-specific route templates and should not be
|
|
95
|
+
* claimed by React HMR strategy.
|
|
96
|
+
*/
|
|
97
|
+
isRouteTemplate(filePath) {
|
|
98
|
+
return filePath.startsWith(this.context.getPagesDir()) || filePath.startsWith(this.context.getLayoutsDir());
|
|
99
|
+
}
|
|
100
|
+
resolveTemplateExtension(filePath) {
|
|
101
|
+
return this.allTemplateExtensions.find((extension) => filePath.endsWith(extension));
|
|
102
|
+
}
|
|
103
|
+
ownsWatchedEntrypoint(filePath) {
|
|
104
|
+
return this.pageMetadataCache.ownsEntrypoint(filePath);
|
|
137
105
|
}
|
|
138
106
|
/**
|
|
139
107
|
* Determines if the file is a React/MDX entrypoint that's registered for HMR.
|
|
@@ -147,6 +115,9 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
147
115
|
if (watchedFiles.size === 0) {
|
|
148
116
|
return false;
|
|
149
117
|
}
|
|
118
|
+
if (watchedFiles.has(filePath)) {
|
|
119
|
+
return this.ownsWatchedEntrypoint(filePath);
|
|
120
|
+
}
|
|
150
121
|
return this.isReactEntrypoint(filePath);
|
|
151
122
|
}
|
|
152
123
|
/**
|
|
@@ -163,6 +134,96 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
163
134
|
isLayoutFile(filePath) {
|
|
164
135
|
return filePath.startsWith(this.context.getLayoutsDir());
|
|
165
136
|
}
|
|
137
|
+
isPageEntrypoint(filePath) {
|
|
138
|
+
return filePath.startsWith(this.context.getPagesDir()) && this.isReactEntrypoint(filePath);
|
|
139
|
+
}
|
|
140
|
+
getEntrypointOutput(entrypointPath) {
|
|
141
|
+
const srcDir = this.context.getSrcDir();
|
|
142
|
+
const relativePath = path.relative(srcDir, entrypointPath);
|
|
143
|
+
const relativePathJs = relativePath.replace(/\.(tsx?|jsx?|mdx)$/, ".js");
|
|
144
|
+
const encodedPathJs = this.encodeDynamicSegments(relativePathJs);
|
|
145
|
+
const outputPath = path.join(this.context.getDistDir(), encodedPathJs);
|
|
146
|
+
const outputUrl = `/${path.join(RESOLVED_ASSETS_DIR, "_hmr", encodedPathJs).split(path.sep).join("/")}`;
|
|
147
|
+
return { outputPath, outputUrl };
|
|
148
|
+
}
|
|
149
|
+
getGroupedTempOutputPattern(entrypointPath) {
|
|
150
|
+
const srcDir = this.context.getSrcDir();
|
|
151
|
+
const relativePath = path.relative(srcDir, entrypointPath);
|
|
152
|
+
const relativePathJs = relativePath.replace(/\.(tsx?|jsx?|mdx)$/, ".js");
|
|
153
|
+
return {
|
|
154
|
+
outputDir: path.join(this.context.getDistDir(), path.dirname(relativePathJs)),
|
|
155
|
+
outputBaseName: path.basename(relativePathJs, ".js")
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
async collectReactPageBuildTargets() {
|
|
159
|
+
const pagesDir = this.context.getPagesDir();
|
|
160
|
+
const scannedFiles = await fileSystem.glob(
|
|
161
|
+
this.allTemplateExtensions.map((extension) => `**/*${extension}`),
|
|
162
|
+
{ cwd: pagesDir }
|
|
163
|
+
);
|
|
164
|
+
const targets = /* @__PURE__ */ new Map();
|
|
165
|
+
for (const file of scannedFiles) {
|
|
166
|
+
if (file.includes(".ecopages-node.")) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const entrypointPath = path.join(pagesDir, file);
|
|
170
|
+
if (!this.isPageEntrypoint(entrypointPath)) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
this.pageMetadataCache.markOwnedEntrypoint(entrypointPath);
|
|
174
|
+
targets.set(entrypointPath, {
|
|
175
|
+
entrypointPath,
|
|
176
|
+
outputUrl: this.getEntrypointOutput(entrypointPath).outputUrl
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return Array.from(targets.values()).sort(
|
|
180
|
+
(left, right) => left.entrypointPath.localeCompare(right.entrypointPath)
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
getRequestedTargets(changedFilePath, changedEntrypointOutput, watchedFiles) {
|
|
184
|
+
const requestedEntries = changedEntrypointOutput ? [[changedFilePath, changedEntrypointOutput]] : Array.from(watchedFiles.entries());
|
|
185
|
+
return requestedEntries.map(([entrypointPath, outputUrl]) => ({
|
|
186
|
+
entrypointPath,
|
|
187
|
+
outputUrl
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Expands one HMR request into the full React page build cohort when needed.
|
|
192
|
+
*
|
|
193
|
+
* @remarks
|
|
194
|
+
* Page and layout changes need one shared rebuild pass so sibling routes keep
|
|
195
|
+
* a consistent client module graph. Non-page changes that do not touch a page
|
|
196
|
+
* cohort can stay scoped to the originally requested targets.
|
|
197
|
+
*/
|
|
198
|
+
async resolveBuildTargets(requestedTargets, changedFilePath) {
|
|
199
|
+
const requestedPageTargets = requestedTargets.filter((target) => this.isPageEntrypoint(target.entrypointPath));
|
|
200
|
+
const shouldGroupPageBuilds = this.isLayoutFile(changedFilePath) || requestedPageTargets.length > 0;
|
|
201
|
+
if (!shouldGroupPageBuilds) {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
const groupedTargets = new Map(requestedPageTargets.map((target) => [target.entrypointPath, target]));
|
|
205
|
+
for (const target of await this.collectReactPageBuildTargets()) {
|
|
206
|
+
groupedTargets.set(target.entrypointPath, target);
|
|
207
|
+
}
|
|
208
|
+
return Array.from(groupedTargets.values()).sort(
|
|
209
|
+
(left, right) => left.entrypointPath.localeCompare(right.entrypointPath)
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
partitionBuildTargets(requestedTargets, groupedPageTargets) {
|
|
213
|
+
if (groupedPageTargets.length === 0) {
|
|
214
|
+
return {
|
|
215
|
+
pageTargets: [],
|
|
216
|
+
nonPageTargets: requestedTargets
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
const groupedPageEntrypoints = new Set(groupedPageTargets.map((target) => target.entrypointPath));
|
|
220
|
+
return {
|
|
221
|
+
pageTargets: groupedPageTargets,
|
|
222
|
+
nonPageTargets: requestedTargets.filter(
|
|
223
|
+
(target) => !groupedPageEntrypoints.has(target.entrypointPath) && !this.isPageEntrypoint(target.entrypointPath)
|
|
224
|
+
)
|
|
225
|
+
};
|
|
226
|
+
}
|
|
166
227
|
/**
|
|
167
228
|
* Processes a React file change by rebuilding all React entrypoints.
|
|
168
229
|
*
|
|
@@ -185,16 +246,40 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
185
246
|
if (isLayout) {
|
|
186
247
|
appLogger.debug(`Detected layout file change: ${_filePath}`);
|
|
187
248
|
}
|
|
188
|
-
const
|
|
189
|
-
this.
|
|
249
|
+
const changedEntrypointOutput = watchedFiles.get(_filePath);
|
|
250
|
+
if (changedEntrypointOutput && !this.ownsWatchedEntrypoint(_filePath)) {
|
|
251
|
+
appLogger.debug(`Skipping non-React watched entrypoint: ${_filePath}`);
|
|
252
|
+
return { type: "none" };
|
|
253
|
+
}
|
|
254
|
+
const requestedTargets = this.getRequestedTargets(_filePath, changedEntrypointOutput, watchedFiles);
|
|
255
|
+
const groupedPageTargets = await this.resolveBuildTargets(requestedTargets, _filePath);
|
|
256
|
+
const { pageTargets, nonPageTargets } = this.partitionBuildTargets(requestedTargets, groupedPageTargets);
|
|
190
257
|
const updates = [];
|
|
191
|
-
|
|
192
|
-
|
|
258
|
+
const requestedOutputUrls = new Set(requestedTargets.map((target) => target.outputUrl));
|
|
259
|
+
if (pageTargets.length > 1) {
|
|
260
|
+
appLogger.debug(`Bundling ${pageTargets.length} React page entrypoints together`);
|
|
261
|
+
const rebuiltOutputs = await this.bundleReactEntrypoints(pageTargets);
|
|
262
|
+
for (const outputUrl of rebuiltOutputs) {
|
|
263
|
+
if (requestedOutputUrls.has(outputUrl)) {
|
|
264
|
+
updates.push(outputUrl);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
for (const { entrypointPath, outputUrl } of pageTargets) {
|
|
269
|
+
appLogger.debug(`Bundling ${entrypointPath}`);
|
|
270
|
+
const success = await this.bundleReactEntrypoint(entrypointPath, outputUrl);
|
|
271
|
+
if (success && requestedOutputUrls.has(outputUrl)) {
|
|
272
|
+
updates.push(outputUrl);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
for (const { entrypointPath, outputUrl } of nonPageTargets) {
|
|
277
|
+
if (!this.isReactEntrypoint(entrypointPath)) {
|
|
193
278
|
continue;
|
|
194
279
|
}
|
|
195
|
-
appLogger.debug(`Bundling ${
|
|
196
|
-
const success = await this.bundleReactEntrypoint(
|
|
197
|
-
if (success) {
|
|
280
|
+
appLogger.debug(`Bundling ${entrypointPath}`);
|
|
281
|
+
const success = await this.bundleReactEntrypoint(entrypointPath, outputUrl);
|
|
282
|
+
if (success && requestedOutputUrls.has(outputUrl)) {
|
|
198
283
|
updates.push(outputUrl);
|
|
199
284
|
}
|
|
200
285
|
}
|
|
@@ -233,29 +318,21 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
233
318
|
async bundleReactEntrypoint(entrypointPath, outputUrl) {
|
|
234
319
|
try {
|
|
235
320
|
const isMdx = entrypointPath.endsWith(".mdx");
|
|
236
|
-
const
|
|
237
|
-
const relativePath = path.relative(srcDir, entrypointPath);
|
|
238
|
-
const relativePathJs = relativePath.replace(/\.(tsx?|jsx?|mdx)$/, ".js");
|
|
239
|
-
const encodedPathJs = this.encodeDynamicSegments(relativePathJs);
|
|
240
|
-
const outputPath = path.join(this.context.getDistDir(), encodedPathJs);
|
|
321
|
+
const { outputPath } = this.getEntrypointOutput(entrypointPath);
|
|
241
322
|
const tempDir = path.dirname(outputPath);
|
|
242
323
|
const declaredModules = this.pageMetadataCache.getDeclaredModules(entrypointPath) ? this.pageMetadataCache.getDeclaredModules(entrypointPath) : isMdx ? await collectPageDeclaredModules(entrypointPath) : collectPageDeclaredModulesFromModule(await this.importNodePageModule(entrypointPath));
|
|
243
324
|
const plugins = this.getBuildPlugins(declaredModules);
|
|
244
325
|
if (isMdx && this.mdxCompilerOptions) {
|
|
245
|
-
const { createReactMdxLoaderPlugin } = await import("./utils/react-mdx-loader-plugin.js");
|
|
246
326
|
const mdxPlugin = createReactMdxLoaderPlugin(this.mdxCompilerOptions);
|
|
247
327
|
plugins.unshift(mdxPlugin);
|
|
248
328
|
}
|
|
249
|
-
const result = await
|
|
329
|
+
const result = await this.context.getBrowserBundleService().bundle({
|
|
330
|
+
profile: "hmr-entrypoint",
|
|
250
331
|
entrypoints: [entrypointPath],
|
|
251
332
|
outdir: tempDir,
|
|
252
333
|
naming: `[name].[hash].tmp`,
|
|
253
|
-
target: "browser",
|
|
254
|
-
format: "esm",
|
|
255
|
-
sourcemap: "none",
|
|
256
334
|
plugins,
|
|
257
|
-
minify: false
|
|
258
|
-
external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime", "react-dom/client"]
|
|
335
|
+
minify: false
|
|
259
336
|
});
|
|
260
337
|
if (!result.success) {
|
|
261
338
|
appLogger.error(`Failed to build ${entrypointPath}:`, result.logs);
|
|
@@ -266,13 +343,88 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
266
343
|
appLogger.error(`No output file generated for ${entrypointPath}`);
|
|
267
344
|
return false;
|
|
268
345
|
}
|
|
269
|
-
const
|
|
346
|
+
const resolvedTempFile = await this.resolveTempOutputPath(tempFile);
|
|
347
|
+
if (!resolvedTempFile) {
|
|
348
|
+
appLogger.debug(`Skipping stale temp output for ${outputUrl}: ${tempFile}`);
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
const processed = await this.processOutput(resolvedTempFile, outputPath, outputUrl);
|
|
270
352
|
return processed;
|
|
271
353
|
} catch (error) {
|
|
272
354
|
appLogger.error(`Error bundling ${entrypointPath}:`, error);
|
|
273
355
|
return false;
|
|
274
356
|
}
|
|
275
357
|
}
|
|
358
|
+
async bundleReactEntrypoints(entrypoints) {
|
|
359
|
+
try {
|
|
360
|
+
const declaredModules = /* @__PURE__ */ new Set();
|
|
361
|
+
let shouldEnableMdx = false;
|
|
362
|
+
for (const { entrypointPath } of entrypoints) {
|
|
363
|
+
const entrypointDeclaredModules = this.pageMetadataCache.getDeclaredModules(entrypointPath) ? this.pageMetadataCache.getDeclaredModules(entrypointPath) : entrypointPath.endsWith(".mdx") ? await collectPageDeclaredModules(entrypointPath) : collectPageDeclaredModulesFromModule(await this.importNodePageModule(entrypointPath));
|
|
364
|
+
this.pageMetadataCache.setDeclaredModules(entrypointPath, entrypointDeclaredModules);
|
|
365
|
+
for (const declaredModule of entrypointDeclaredModules) {
|
|
366
|
+
declaredModules.add(declaredModule);
|
|
367
|
+
}
|
|
368
|
+
if (entrypointPath.endsWith(".mdx")) {
|
|
369
|
+
shouldEnableMdx = true;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const plugins = this.getBuildPlugins([...declaredModules]);
|
|
373
|
+
if (shouldEnableMdx && this.mdxCompilerOptions) {
|
|
374
|
+
plugins.unshift(createReactMdxLoaderPlugin(this.mdxCompilerOptions));
|
|
375
|
+
}
|
|
376
|
+
const result = await this.context.getBrowserBundleService().bundle({
|
|
377
|
+
profile: "hmr-entrypoint",
|
|
378
|
+
entrypoints: entrypoints.map(({ entrypointPath }) => entrypointPath),
|
|
379
|
+
outdir: this.context.getDistDir(),
|
|
380
|
+
outbase: this.context.getSrcDir(),
|
|
381
|
+
naming: "[dir]/[name].[hash].tmp",
|
|
382
|
+
splitting: true,
|
|
383
|
+
plugins,
|
|
384
|
+
minify: false
|
|
385
|
+
});
|
|
386
|
+
if (!result.success) {
|
|
387
|
+
appLogger.error(`Failed to build grouped React entrypoints:`, result.logs);
|
|
388
|
+
return [];
|
|
389
|
+
}
|
|
390
|
+
const updatedOutputs = [];
|
|
391
|
+
for (const { entrypointPath, outputUrl } of entrypoints) {
|
|
392
|
+
const { outputPath } = this.getEntrypointOutput(entrypointPath);
|
|
393
|
+
const { outputDir, outputBaseName } = this.getGroupedTempOutputPattern(entrypointPath);
|
|
394
|
+
const tempOutput = result.outputs.find((output) => {
|
|
395
|
+
return path.dirname(output.path) === outputDir && path.basename(output.path).startsWith(`${outputBaseName}.`) && path.basename(output.path).includes(".tmp");
|
|
396
|
+
})?.path;
|
|
397
|
+
const resolvedTempOutput = tempOutput ? await this.resolveTempOutputPath(tempOutput) : await this.resolveTempOutputPath(path.join(outputDir, `${outputBaseName}.[hash].tmp.js`));
|
|
398
|
+
if (!resolvedTempOutput) {
|
|
399
|
+
appLogger.debug(`Missing grouped temp output for ${outputUrl}`);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
const processed = await this.processOutput(resolvedTempOutput, outputPath, outputUrl);
|
|
403
|
+
if (processed) {
|
|
404
|
+
updatedOutputs.push(outputUrl);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return updatedOutputs;
|
|
408
|
+
} catch (error) {
|
|
409
|
+
appLogger.error(`Error bundling grouped React entrypoints:`, error);
|
|
410
|
+
return [];
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async resolveTempOutputPath(tempPath) {
|
|
414
|
+
if (fileSystem.exists(tempPath)) {
|
|
415
|
+
return tempPath;
|
|
416
|
+
}
|
|
417
|
+
if (!tempPath.includes("[hash]")) {
|
|
418
|
+
return tempPath;
|
|
419
|
+
}
|
|
420
|
+
const directory = path.dirname(tempPath);
|
|
421
|
+
const pattern = path.basename(tempPath).replaceAll("[hash]", "*");
|
|
422
|
+
const matches = await fileSystem.glob([pattern], { cwd: directory });
|
|
423
|
+
if (matches.length === 0) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
return path.isAbsolute(matches[0]) ? matches[0] : path.join(directory, matches[0]);
|
|
427
|
+
}
|
|
276
428
|
/**
|
|
277
429
|
* Encodes dynamic route segments (brackets) in file paths.
|
|
278
430
|
* Converts `[slug]` to `_slug_` to avoid filesystem issues.
|
|
@@ -280,8 +432,30 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
280
432
|
encodeDynamicSegments(filepath) {
|
|
281
433
|
return filepath.replace(/\[([^\]]+)\]/g, "_$1_");
|
|
282
434
|
}
|
|
435
|
+
rewriteChunkImportUrls(code) {
|
|
436
|
+
const hmrChunkBaseUrl = `/${path.join(RESOLVED_ASSETS_DIR, "_hmr").split(path.sep).join("/")}`;
|
|
437
|
+
return code.replace(/(['"])(?:\.\.\/)+(chunk-[^'"]+\.js)\1/g, (_match, quote, chunkFile) => {
|
|
438
|
+
return `${quote}${hmrChunkBaseUrl}/${chunkFile}${quote}`;
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
isMissingTempOutputError(error) {
|
|
442
|
+
if (error instanceof FileNotFoundError) {
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
if (!(error instanceof Error)) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
if (error.message.includes("not found") || error.message.includes("ENOENT")) {
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
const errorCause = error.cause;
|
|
452
|
+
if (errorCause instanceof FileNotFoundError) {
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
return typeof errorCause === "object" && errorCause !== null && "code" in errorCause && errorCause.code === "ENOENT";
|
|
456
|
+
}
|
|
283
457
|
/**
|
|
284
|
-
* Processes bundled output
|
|
458
|
+
* Processes bundled output and injects the React HMR handler.
|
|
285
459
|
* Writes to temp file first, then renames atomically to avoid conflicts.
|
|
286
460
|
*
|
|
287
461
|
* @param tempPath - Path to the temporary bundled file
|
|
@@ -296,7 +470,7 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
296
470
|
}
|
|
297
471
|
try {
|
|
298
472
|
let code = await fileSystem.readFile(tempPath);
|
|
299
|
-
code = this.
|
|
473
|
+
code = this.rewriteChunkImportUrls(code);
|
|
300
474
|
code = injectHmrHandler(code);
|
|
301
475
|
await fileSystem.writeAsync(finalPath, code);
|
|
302
476
|
await fileSystem.removeAsync(tempPath).catch(() => {
|
|
@@ -304,7 +478,7 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
304
478
|
appLogger.debug(`Processed ${url} with HMR handler`);
|
|
305
479
|
return true;
|
|
306
480
|
} catch (error) {
|
|
307
|
-
if (
|
|
481
|
+
if (this.isMissingTempOutputError(error)) {
|
|
308
482
|
appLogger.debug(`Skipping stale temp output for ${url}: ${tempPath}`);
|
|
309
483
|
await fileSystem.removeAsync(tempPath).catch(() => {
|
|
310
484
|
});
|
|
@@ -316,27 +490,6 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
316
490
|
return false;
|
|
317
491
|
}
|
|
318
492
|
}
|
|
319
|
-
/**
|
|
320
|
-
* Replaces bare specifiers with runtime URLs.
|
|
321
|
-
*
|
|
322
|
-
* Handles both static imports and dynamic imports.
|
|
323
|
-
*
|
|
324
|
-
* @param code - The bundled code to transform
|
|
325
|
-
* @returns The transformed code with runtime URLs
|
|
326
|
-
*/
|
|
327
|
-
replaceBareSpecifiers(code) {
|
|
328
|
-
const specifierMap = this.context.getSpecifierMap();
|
|
329
|
-
if (specifierMap.size === 0) {
|
|
330
|
-
return code;
|
|
331
|
-
}
|
|
332
|
-
let result = code;
|
|
333
|
-
for (const [bareSpec, runtimeUrl] of specifierMap.entries()) {
|
|
334
|
-
const escaped = bareSpec.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
335
|
-
result = result.replace(new RegExp(`from\\s*["']${escaped}["']`, "g"), `from "${runtimeUrl}"`);
|
|
336
|
-
result = result.replace(new RegExp(`import\\(["']${escaped}["']\\)`, "g"), `import("${runtimeUrl}")`);
|
|
337
|
-
}
|
|
338
|
-
return result;
|
|
339
|
-
}
|
|
340
493
|
}
|
|
341
494
|
export {
|
|
342
495
|
ReactHmrStrategy
|