@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.
Files changed (68) hide show
  1. package/README.md +152 -29
  2. package/package.json +16 -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 +60 -43
  6. package/src/react-hmr-strategy.js +297 -144
  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 +38 -111
  12. package/src/react.plugin.js +132 -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/services/react-bundle.service.d.ts +19 -31
  17. package/src/services/react-bundle.service.js +51 -100
  18. package/src/services/react-hmr-page-metadata-cache.d.ts +9 -0
  19. package/src/services/react-hmr-page-metadata-cache.js +18 -2
  20. package/src/services/react-hydration-asset.service.d.ts +28 -19
  21. package/src/services/react-hydration-asset.service.js +85 -66
  22. package/src/services/react-mdx-config-dependency.service.d.ts +36 -0
  23. package/src/services/react-mdx-config-dependency.service.js +122 -0
  24. package/src/services/react-page-module.service.d.ts +10 -2
  25. package/src/services/react-page-module.service.js +47 -39
  26. package/src/services/react-page-payload.service.d.ts +46 -0
  27. package/src/services/react-page-payload.service.js +67 -0
  28. package/src/services/react-runtime-bundle.service.d.ts +15 -13
  29. package/src/services/react-runtime-bundle.service.js +103 -180
  30. package/src/utils/client-graph-boundary-plugin.d.ts +1 -1
  31. package/src/utils/client-graph-boundary-plugin.js +80 -3
  32. package/src/utils/component-config-traversal.d.ts +36 -0
  33. package/src/utils/component-config-traversal.js +54 -0
  34. package/src/utils/declared-modules.d.ts +1 -1
  35. package/src/utils/declared-modules.js +7 -16
  36. package/src/utils/dynamic.test.browser.d.ts +1 -0
  37. package/src/utils/dynamic.test.browser.js +33 -0
  38. package/src/utils/hydration-scripts.d.ts +27 -6
  39. package/src/utils/hydration-scripts.js +177 -44
  40. package/src/utils/hydration-scripts.test.browser.d.ts +1 -0
  41. package/src/utils/hydration-scripts.test.browser.js +198 -0
  42. package/src/utils/react-dom-runtime-interop-plugin.d.ts +5 -0
  43. package/src/utils/react-dom-runtime-interop-plugin.js +29 -0
  44. package/src/utils/react-mdx-loader-plugin.d.ts +1 -1
  45. package/src/utils/react-mdx-loader-plugin.js +13 -5
  46. package/src/utils/react-runtime-alias-map.d.ts +6 -0
  47. package/src/utils/react-runtime-alias-map.js +33 -0
  48. package/src/utils/use-sync-external-store-shim-plugin.d.ts +5 -0
  49. package/src/utils/use-sync-external-store-shim-plugin.js +41 -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,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 { defaultBuildAdapter } from "@ecopages/core/build/build-adapter";
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
- knownEntrypoints = /* @__PURE__ */ new Set();
18
+ ownedTemplateExtensions;
19
+ allTemplateExtensions;
35
20
  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);
21
+ return await this.context.importServerModule(entrypointPath);
63
22
  }
64
23
  /**
65
- * Redirects `use-sync-external-store/shim` imports to React's built-in
66
- * `useSyncExternalStore`.
24
+ * Creates a new React HMR strategy instance.
67
25
  *
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.
26
+ * @param options - React HMR runtime services and behavior flags.
74
27
  */
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
- };
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
- "@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
- ];
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
- this.createUseSyncExternalStoreShimPlugin()
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(".tsx")) {
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
- return filePath.endsWith(".mdx") && this.mdxCompilerOptions !== void 0;
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 entrypointsToBuild = !this.knownEntrypoints.has(_filePath) && watchedFiles.has(_filePath) ? [[_filePath, watchedFiles.get(_filePath)]] : watchedFiles.entries();
189
- this.knownEntrypoints.add(_filePath);
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
- for (const [entrypoint, outputUrl] of entrypointsToBuild) {
192
- if (!this.isReactEntrypoint(entrypoint)) {
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 ${entrypoint}`);
196
- const success = await this.bundleReactEntrypoint(entrypoint, outputUrl);
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 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);
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 defaultBuildAdapter.build({
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 processed = await this.processOutput(tempFile, outputPath, outputUrl);
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 by replacing specifiers and injecting HMR handler.
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.replaceBareSpecifiers(code);
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 (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