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