@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.
Files changed (68) hide show
  1. package/README.md +152 -29
  2. package/package.json +24 -12
  3. package/src/eco-embed.d.ts +11 -0
  4. package/src/eco-embed.js +11 -0
  5. package/src/react-hmr-strategy.d.ts +65 -43
  6. package/src/react-hmr-strategy.js +298 -145
  7. package/src/react-renderer.d.ts +169 -42
  8. package/src/react-renderer.js +484 -164
  9. package/src/react.constants.d.ts +1 -0
  10. package/src/react.constants.js +4 -0
  11. package/src/react.plugin.d.ts +40 -111
  12. package/src/react.plugin.js +136 -61
  13. package/src/react.types.d.ts +88 -0
  14. package/src/react.types.js +0 -0
  15. package/src/router-adapter.d.ts +7 -14
  16. package/src/runtime/use-sync-external-store-with-selector.d.ts +3 -0
  17. package/src/runtime/use-sync-external-store-with-selector.js +56 -0
  18. package/src/services/react-bundle.service.d.ts +22 -35
  19. package/src/services/react-bundle.service.js +41 -105
  20. package/src/services/react-hmr-page-metadata-cache.d.ts +9 -0
  21. package/src/services/react-hmr-page-metadata-cache.js +18 -2
  22. package/src/services/react-hydration-asset.service.d.ts +28 -19
  23. package/src/services/react-hydration-asset.service.js +85 -66
  24. package/src/services/react-mdx-config-dependency.service.d.ts +36 -0
  25. package/src/services/react-mdx-config-dependency.service.js +122 -0
  26. package/src/services/react-page-module.service.d.ts +10 -2
  27. package/src/services/react-page-module.service.js +47 -39
  28. package/src/services/react-page-payload.service.d.ts +46 -0
  29. package/src/services/react-page-payload.service.js +67 -0
  30. package/src/services/react-runtime-bundle.service.d.ts +20 -13
  31. package/src/services/react-runtime-bundle.service.js +146 -179
  32. package/src/utils/client-graph-boundary-plugin.d.ts +1 -1
  33. package/src/utils/client-graph-boundary-plugin.js +80 -3
  34. package/src/utils/component-config-traversal.d.ts +36 -0
  35. package/src/utils/component-config-traversal.js +54 -0
  36. package/src/utils/declared-modules.d.ts +1 -1
  37. package/src/utils/declared-modules.js +7 -16
  38. package/src/utils/dynamic.test.browser.d.ts +1 -0
  39. package/src/utils/dynamic.test.browser.js +33 -0
  40. package/src/utils/hydration-scripts.d.ts +27 -6
  41. package/src/utils/hydration-scripts.js +177 -44
  42. package/src/utils/hydration-scripts.test.browser.d.ts +1 -0
  43. package/src/utils/hydration-scripts.test.browser.js +198 -0
  44. package/src/utils/react-dom-runtime-interop-plugin.d.ts +5 -0
  45. package/src/utils/react-dom-runtime-interop-plugin.js +38 -0
  46. package/src/utils/react-mdx-loader-plugin.d.ts +1 -1
  47. package/src/utils/react-mdx-loader-plugin.js +13 -5
  48. package/src/utils/react-runtime-alias-map.d.ts +8 -0
  49. package/src/utils/react-runtime-alias-map.js +90 -0
  50. package/CHANGELOG.md +0 -67
  51. package/src/react-hmr-strategy.ts +0 -455
  52. package/src/react-renderer.ts +0 -403
  53. package/src/react.plugin.ts +0 -241
  54. package/src/router-adapter.ts +0 -95
  55. package/src/services/react-bundle.service.ts +0 -217
  56. package/src/services/react-hmr-page-metadata-cache.ts +0 -24
  57. package/src/services/react-hydration-asset.service.ts +0 -260
  58. package/src/services/react-page-module.service.ts +0 -214
  59. package/src/services/react-runtime-bundle.service.ts +0 -271
  60. package/src/utils/client-graph-boundary-plugin.ts +0 -710
  61. package/src/utils/client-only.ts +0 -27
  62. package/src/utils/declared-modules.ts +0 -99
  63. package/src/utils/dynamic.ts +0 -27
  64. package/src/utils/hmr-scripts.ts +0 -47
  65. package/src/utils/html-boundary.ts +0 -66
  66. package/src/utils/hydration-scripts.ts +0 -338
  67. package/src/utils/reachability-analyzer.ts +0 -593
  68. 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 { defaultBuildAdapter } from "@ecopages/core/build/build-adapter";
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
- knownEntrypoints = /* @__PURE__ */ new Set();
16
+ ownedTemplateExtensions;
17
+ allTemplateExtensions;
35
18
  async importNodePageModule(entrypointPath) {
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);
19
+ return await this.context.importServerModule(entrypointPath);
63
20
  }
64
21
  /**
65
- * Redirects `use-sync-external-store/shim` imports to React's built-in
66
- * `useSyncExternalStore`.
22
+ * Creates a new React HMR strategy instance.
67
23
  *
68
- * Libraries like React Aria still list `use-sync-external-store` as a
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
- createUseSyncExternalStoreShimPlugin() {
76
- return {
77
- name: "react-hmr-use-sync-external-store-shim",
78
- setup(build) {
79
- build.onResolve({ filter: /^use-sync-external-store\/shim(?:\/index\.js)?$/ }, () => ({
80
- path: "use-sync-external-store/shim",
81
- namespace: "ecopages-react-hmr-shim"
82
- }));
83
- build.onLoad(
84
- { filter: /^use-sync-external-store\/shim$/, namespace: "ecopages-react-hmr-shim" },
85
- () => ({
86
- contents: "export { useSyncExternalStore } from 'react';",
87
- loader: "js"
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
- "@ecopages/core",
115
- "react",
116
- "react-dom",
117
- "react/jsx-runtime",
118
- "react/jsx-dev-runtime",
119
- "react-dom/client",
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
- ...this.context.getPlugins(),
129
- this.createUseSyncExternalStoreShimPlugin()
66
+ ...runtimeRewritePlugin ? [runtimeRewritePlugin] : [],
67
+ ...this.context.getPlugins()
130
68
  ];
131
69
  }
132
70
  isReactEntrypoint(filePath) {
133
- if (filePath.endsWith(".tsx")) {
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
- return filePath.endsWith(".mdx") && this.mdxCompilerOptions !== void 0;
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 entrypointsToBuild = !this.knownEntrypoints.has(_filePath) && watchedFiles.has(_filePath) ? [[_filePath, watchedFiles.get(_filePath)]] : watchedFiles.entries();
189
- this.knownEntrypoints.add(_filePath);
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
- for (const [entrypoint, outputUrl] of entrypointsToBuild) {
192
- if (!this.isReactEntrypoint(entrypoint)) {
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 ${entrypoint}`);
196
- const success = await this.bundleReactEntrypoint(entrypoint, outputUrl);
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 srcDir = this.context.getSrcDir();
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 defaultBuildAdapter.build({
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 processed = await this.processOutput(tempFile, outputPath, outputUrl);
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 by replacing specifiers and injecting HMR handler.
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.replaceBareSpecifiers(code);
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 (error instanceof FileNotFoundError || error instanceof Error && error.message.includes("not found") || error instanceof Error && "code" in error && error.code === "ENOENT") {
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