@ecopages/react 0.2.0-alpha.5 → 0.2.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -41
- package/README.md +135 -29
- package/package.json +3 -3
- package/src/react-hmr-strategy.d.ts +22 -30
- package/src/react-hmr-strategy.js +57 -120
- package/src/react-hmr-strategy.ts +76 -145
- package/src/react-renderer.d.ts +130 -11
- package/src/react-renderer.js +368 -64
- package/src/react-renderer.ts +490 -90
- package/src/react.plugin.d.ts +17 -5
- package/src/react.plugin.js +44 -13
- package/src/react.plugin.ts +49 -14
- package/src/router-adapter.d.ts +2 -2
- package/src/router-adapter.ts +2 -2
- package/src/services/react-bundle.service.d.ts +2 -30
- package/src/services/react-bundle.service.js +19 -94
- package/src/services/react-bundle.service.ts +20 -129
- package/src/services/react-hydration-asset.service.js +3 -3
- package/src/services/react-hydration-asset.service.ts +7 -4
- package/src/services/react-page-module.service.d.ts +3 -0
- package/src/services/react-page-module.service.js +20 -16
- package/src/services/react-page-module.service.ts +27 -17
- package/src/services/react-runtime-bundle.service.d.ts +12 -12
- package/src/services/react-runtime-bundle.service.js +98 -180
- package/src/services/react-runtime-bundle.service.ts +112 -211
- package/src/utils/client-graph-boundary-plugin.js +78 -1
- package/src/utils/client-graph-boundary-plugin.ts +122 -1
- package/src/utils/hydration-scripts.d.ts +18 -1
- package/src/utils/hydration-scripts.js +83 -32
- package/src/utils/hydration-scripts.ts +159 -38
- 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-dom-runtime-interop-plugin.ts +33 -0
- package/src/utils/react-mdx-loader-plugin.js +13 -5
- package/src/utils/react-mdx-loader-plugin.ts +28 -5
- package/src/utils/react-runtime-specifier-map.d.ts +6 -0
- package/src/utils/react-runtime-specifier-map.js +37 -0
- package/src/utils/react-runtime-specifier-map.ts +45 -0
- package/src/utils/use-sync-external-store-shim-plugin.d.ts +5 -0
- package/src/utils/use-sync-external-store-shim-plugin.js +41 -0
- package/src/utils/use-sync-external-store-shim-plugin.ts +45 -0
|
@@ -1,14 +1,22 @@
|
|
|
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 { createRuntimeSpecifierAliasPlugin } from "@ecopages/core/build/runtime-specifier-alias-plugin";
|
|
5
4
|
import { FileNotFoundError, fileSystem } from "@ecopages/file-system";
|
|
6
5
|
import { Logger } from "@ecopages/logger";
|
|
7
6
|
import { injectHmrHandler } from "./utils/hmr-scripts.js";
|
|
8
7
|
import { createClientGraphBoundaryPlugin } from "./utils/client-graph-boundary-plugin.js";
|
|
9
8
|
import { collectPageDeclaredModules, collectPageDeclaredModulesFromModule } from "./utils/declared-modules.js";
|
|
9
|
+
import { getReactClientGraphAllowSpecifiers } from "./utils/react-runtime-specifier-map.js";
|
|
10
|
+
import { createUseSyncExternalStoreShimPlugin } from "./utils/use-sync-external-store-shim-plugin.js";
|
|
10
11
|
const appLogger = new Logger("[ReactHmrStrategy]");
|
|
11
12
|
class ReactHmrStrategy extends HmrStrategy {
|
|
13
|
+
type = HmrStrategyType.INTEGRATION;
|
|
14
|
+
mdxCompilerOptions;
|
|
15
|
+
ownedTemplateExtensions;
|
|
16
|
+
allTemplateExtensions;
|
|
17
|
+
async importNodePageModule(entrypointPath) {
|
|
18
|
+
return await this.context.importServerModule(entrypointPath);
|
|
19
|
+
}
|
|
12
20
|
/**
|
|
13
21
|
* Creates a new React HMR strategy instance.
|
|
14
22
|
*
|
|
@@ -22,86 +30,17 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
22
30
|
* @param explicitGraphEnabled - Enables explicit graph mode for React HMR bundling.
|
|
23
31
|
* In explicit mode, HMR builds omit AST server-only stripping plugins in React paths.
|
|
24
32
|
*/
|
|
25
|
-
|
|
33
|
+
context;
|
|
34
|
+
pageMetadataCache;
|
|
35
|
+
explicitGraphEnabled;
|
|
36
|
+
constructor(context, pageMetadataCache, mdxCompilerOptions, ownedTemplateExtensions = [".tsx"], allTemplateExtensions = [".tsx"], explicitGraphEnabled = false) {
|
|
26
37
|
super();
|
|
27
38
|
this.context = context;
|
|
28
39
|
this.pageMetadataCache = pageMetadataCache;
|
|
29
40
|
this.explicitGraphEnabled = explicitGraphEnabled;
|
|
30
41
|
this.mdxCompilerOptions = mdxCompilerOptions;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
mdxCompilerOptions;
|
|
34
|
-
knownEntrypoints = /* @__PURE__ */ new Set();
|
|
35
|
-
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);
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Redirects `use-sync-external-store/shim` imports to React's built-in
|
|
66
|
-
* `useSyncExternalStore`.
|
|
67
|
-
*
|
|
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.
|
|
74
|
-
*/
|
|
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
|
-
};
|
|
42
|
+
this.ownedTemplateExtensions = new Set(ownedTemplateExtensions);
|
|
43
|
+
this.allTemplateExtensions = [...allTemplateExtensions].sort((a, b) => b.length - a.length);
|
|
105
44
|
}
|
|
106
45
|
/**
|
|
107
46
|
* Returns build plugins for React HMR bundling.
|
|
@@ -110,30 +49,53 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
110
49
|
* (including `node:*`) from breaking the browser bundle.
|
|
111
50
|
*/
|
|
112
51
|
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
|
-
];
|
|
52
|
+
const allowSpecifiers = getReactClientGraphAllowSpecifiers(this.context.getSpecifierMap().keys());
|
|
53
|
+
const runtimeAliasPlugin = createRuntimeSpecifierAliasPlugin(this.context.getSpecifierMap(), {
|
|
54
|
+
name: "react-hmr-runtime-specifier-alias"
|
|
55
|
+
});
|
|
122
56
|
return [
|
|
123
57
|
createClientGraphBoundaryPlugin({
|
|
124
58
|
absWorkingDir: path.dirname(this.context.getSrcDir()),
|
|
125
59
|
alwaysAllowSpecifiers: allowSpecifiers,
|
|
126
60
|
declaredModules
|
|
127
61
|
}),
|
|
62
|
+
...runtimeAliasPlugin ? [runtimeAliasPlugin] : [],
|
|
128
63
|
...this.context.getPlugins(),
|
|
129
|
-
|
|
64
|
+
createUseSyncExternalStoreShimPlugin({
|
|
65
|
+
name: "react-hmr-use-sync-external-store-shim",
|
|
66
|
+
namespace: "ecopages-react-hmr-shim"
|
|
67
|
+
})
|
|
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
|
+
if (!this.isRouteTemplate(filePath)) {
|
|
134
78
|
return true;
|
|
135
79
|
}
|
|
136
|
-
|
|
80
|
+
const templateExtension = this.resolveTemplateExtension(filePath);
|
|
81
|
+
if (!templateExtension) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return this.ownedTemplateExtensions.has(templateExtension);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Returns true when a route file uses a compound extension like `page.foo.tsx`.
|
|
88
|
+
*
|
|
89
|
+
* @remarks
|
|
90
|
+
* React integration owns plain `.tsx` route templates. Compound extensions in
|
|
91
|
+
* pages/layouts are integration-specific route templates and should not be
|
|
92
|
+
* claimed by React HMR strategy.
|
|
93
|
+
*/
|
|
94
|
+
isRouteTemplate(filePath) {
|
|
95
|
+
return filePath.startsWith(this.context.getPagesDir()) || filePath.startsWith(this.context.getLayoutsDir());
|
|
96
|
+
}
|
|
97
|
+
resolveTemplateExtension(filePath) {
|
|
98
|
+
return this.allTemplateExtensions.find((extension) => filePath.endsWith(extension));
|
|
137
99
|
}
|
|
138
100
|
/**
|
|
139
101
|
* Determines if the file is a React/MDX entrypoint that's registered for HMR.
|
|
@@ -185,8 +147,8 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
185
147
|
if (isLayout) {
|
|
186
148
|
appLogger.debug(`Detected layout file change: ${_filePath}`);
|
|
187
149
|
}
|
|
188
|
-
const
|
|
189
|
-
|
|
150
|
+
const changedEntrypointOutput = watchedFiles.get(_filePath);
|
|
151
|
+
const entrypointsToBuild = changedEntrypointOutput ? [[_filePath, changedEntrypointOutput]] : watchedFiles.entries();
|
|
190
152
|
const updates = [];
|
|
191
153
|
for (const [entrypoint, outputUrl] of entrypointsToBuild) {
|
|
192
154
|
if (!this.isReactEntrypoint(entrypoint)) {
|
|
@@ -246,16 +208,13 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
246
208
|
const mdxPlugin = createReactMdxLoaderPlugin(this.mdxCompilerOptions);
|
|
247
209
|
plugins.unshift(mdxPlugin);
|
|
248
210
|
}
|
|
249
|
-
const result = await
|
|
211
|
+
const result = await this.context.getBrowserBundleService().bundle({
|
|
212
|
+
profile: "hmr-entrypoint",
|
|
250
213
|
entrypoints: [entrypointPath],
|
|
251
214
|
outdir: tempDir,
|
|
252
215
|
naming: `[name].[hash].tmp`,
|
|
253
|
-
target: "browser",
|
|
254
|
-
format: "esm",
|
|
255
|
-
sourcemap: "none",
|
|
256
216
|
plugins,
|
|
257
|
-
minify: false
|
|
258
|
-
external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime", "react-dom/client"]
|
|
217
|
+
minify: false
|
|
259
218
|
});
|
|
260
219
|
if (!result.success) {
|
|
261
220
|
appLogger.error(`Failed to build ${entrypointPath}:`, result.logs);
|
|
@@ -281,7 +240,7 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
281
240
|
return filepath.replace(/\[([^\]]+)\]/g, "_$1_");
|
|
282
241
|
}
|
|
283
242
|
/**
|
|
284
|
-
* Processes bundled output
|
|
243
|
+
* Processes bundled output and injects the React HMR handler.
|
|
285
244
|
* Writes to temp file first, then renames atomically to avoid conflicts.
|
|
286
245
|
*
|
|
287
246
|
* @param tempPath - Path to the temporary bundled file
|
|
@@ -296,7 +255,6 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
296
255
|
}
|
|
297
256
|
try {
|
|
298
257
|
let code = await fileSystem.readFile(tempPath);
|
|
299
|
-
code = this.replaceBareSpecifiers(code);
|
|
300
258
|
code = injectHmrHandler(code);
|
|
301
259
|
await fileSystem.writeAsync(finalPath, code);
|
|
302
260
|
await fileSystem.removeAsync(tempPath).catch(() => {
|
|
@@ -316,27 +274,6 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
316
274
|
return false;
|
|
317
275
|
}
|
|
318
276
|
}
|
|
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
277
|
}
|
|
341
278
|
export {
|
|
342
279
|
ReactHmrStrategy
|
|
@@ -8,18 +8,19 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import path from 'node:path';
|
|
11
|
-
import { pathToFileURL } from 'node:url';
|
|
12
11
|
|
|
13
12
|
import { HmrStrategy, HmrStrategyType, type HmrAction } from '@ecopages/core/hmr/hmr-strategy';
|
|
14
|
-
import { defaultBuildAdapter } from '@ecopages/core/build/build-adapter';
|
|
15
13
|
import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
|
|
14
|
+
import { createRuntimeSpecifierAliasPlugin } from '@ecopages/core/build/runtime-specifier-alias-plugin';
|
|
16
15
|
import { FileNotFoundError, fileSystem } from '@ecopages/file-system';
|
|
17
16
|
import { Logger } from '@ecopages/logger';
|
|
18
|
-
import type { DefaultHmrContext
|
|
17
|
+
import type { DefaultHmrContext } from '@ecopages/core';
|
|
19
18
|
import type { CompileOptions } from '@mdx-js/mdx';
|
|
20
19
|
import { injectHmrHandler } from './utils/hmr-scripts.ts';
|
|
21
20
|
import { createClientGraphBoundaryPlugin } from './utils/client-graph-boundary-plugin.ts';
|
|
22
21
|
import { collectPageDeclaredModules, collectPageDeclaredModulesFromModule } from './utils/declared-modules.ts';
|
|
22
|
+
import { getReactClientGraphAllowSpecifiers } from './utils/react-runtime-specifier-map.ts';
|
|
23
|
+
import { createUseSyncExternalStoreShimPlugin } from './utils/use-sync-external-store-shim-plugin.ts';
|
|
23
24
|
import type { ReactHmrPageMetadataCache } from './services/react-hmr-page-metadata-cache.ts';
|
|
24
25
|
|
|
25
26
|
const appLogger = new Logger('[ReactHmrStrategy]');
|
|
@@ -33,9 +34,11 @@ const appLogger = new Logger('[ReactHmrStrategy]');
|
|
|
33
34
|
* The processing steps are:
|
|
34
35
|
* 1. Check if any React entrypoints are registered
|
|
35
36
|
* 2. Rebuild all React entrypoints (the changed file could be a dependency)
|
|
36
|
-
* 3.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
37
|
+
* 3. Rebuild browser output through the shared browser bundle service while
|
|
38
|
+
* preserving React-specific runtime aliases and graph policy
|
|
39
|
+
* 4. Read page config metadata through the shared server-module loading path
|
|
40
|
+
* 5. Inject HMR acceptance handler
|
|
41
|
+
* 6. Broadcast update events for each rebuilt entrypoint
|
|
39
42
|
*
|
|
40
43
|
* @remarks
|
|
41
44
|
* This strategy has higher priority than generic JsHmrStrategy, allowing it
|
|
@@ -63,95 +66,13 @@ const appLogger = new Logger('[ReactHmrStrategy]');
|
|
|
63
66
|
export class ReactHmrStrategy extends HmrStrategy {
|
|
64
67
|
readonly type = HmrStrategyType.INTEGRATION;
|
|
65
68
|
private mdxCompilerOptions?: CompileOptions;
|
|
66
|
-
private readonly
|
|
67
|
-
|
|
69
|
+
private readonly ownedTemplateExtensions: Set<string>;
|
|
70
|
+
private readonly allTemplateExtensions: string[];
|
|
68
71
|
private async importNodePageModule(entrypointPath: string): Promise<{
|
|
69
|
-
default?: { config?:
|
|
70
|
-
config?:
|
|
72
|
+
default?: { config?: Record<string, unknown> };
|
|
73
|
+
config?: Record<string, unknown>;
|
|
71
74
|
}> {
|
|
72
|
-
|
|
73
|
-
const rootDir = path.dirname(srcDir);
|
|
74
|
-
const outdir = path.join(path.resolve(this.context.getDistDir(), '..', '..'), '.server-modules');
|
|
75
|
-
const fileBaseName = path.basename(entrypointPath, path.extname(entrypointPath));
|
|
76
|
-
const fileHash = fileSystem.hash(entrypointPath);
|
|
77
|
-
const outputFileName = `${fileBaseName}-${fileHash}.js`;
|
|
78
|
-
|
|
79
|
-
const buildResult = await defaultBuildAdapter.build({
|
|
80
|
-
entrypoints: [entrypointPath],
|
|
81
|
-
root: rootDir,
|
|
82
|
-
outdir,
|
|
83
|
-
target: 'node',
|
|
84
|
-
format: 'esm',
|
|
85
|
-
sourcemap: 'none',
|
|
86
|
-
splitting: false,
|
|
87
|
-
minify: false,
|
|
88
|
-
naming: outputFileName,
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
if (!buildResult.success) {
|
|
92
|
-
const details = buildResult.logs.map((log) => log.message).join(' | ');
|
|
93
|
-
throw new Error(`Error transpiling React HMR page module: ${details}`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const preferredOutputPath = path.join(outdir, outputFileName);
|
|
97
|
-
const compiledOutput =
|
|
98
|
-
buildResult.outputs.find((output) => output.path === preferredOutputPath)?.path ??
|
|
99
|
-
buildResult.outputs.find((output) => output.path.endsWith('.js'))?.path;
|
|
100
|
-
|
|
101
|
-
if (!compiledOutput) {
|
|
102
|
-
throw new Error(`No transpiled output generated for React HMR page module: ${entrypointPath}`);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return (await import(pathToFileURL(compiledOutput).href)) as {
|
|
106
|
-
default?: { config?: EcoComponentConfig };
|
|
107
|
-
config?: EcoComponentConfig;
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Redirects `use-sync-external-store/shim` imports to React's built-in
|
|
113
|
-
* `useSyncExternalStore`.
|
|
114
|
-
*
|
|
115
|
-
* Libraries like React Aria still list `use-sync-external-store` as a
|
|
116
|
-
* dependency to support React 16/17. On React 18+ the `/shim` export is
|
|
117
|
-
* already a pass-through, but without this plugin esbuild would bundle
|
|
118
|
-
* the full CJS shim (including `process.env` branching) into the browser
|
|
119
|
-
* bundle. The plugin short-circuits the resolution so only a single clean
|
|
120
|
-
* ESM re-export is emitted.
|
|
121
|
-
*/
|
|
122
|
-
private createUseSyncExternalStoreShimPlugin(): EcoBuildPlugin {
|
|
123
|
-
return {
|
|
124
|
-
name: 'react-hmr-use-sync-external-store-shim',
|
|
125
|
-
setup(build) {
|
|
126
|
-
build.onResolve({ filter: /^use-sync-external-store\/shim(?:\/index\.js)?$/ }, () => ({
|
|
127
|
-
path: 'use-sync-external-store/shim',
|
|
128
|
-
namespace: 'ecopages-react-hmr-shim',
|
|
129
|
-
}));
|
|
130
|
-
|
|
131
|
-
build.onLoad(
|
|
132
|
-
{ filter: /^use-sync-external-store\/shim$/, namespace: 'ecopages-react-hmr-shim' },
|
|
133
|
-
() => ({
|
|
134
|
-
contents: "export { useSyncExternalStore } from 'react';",
|
|
135
|
-
loader: 'js',
|
|
136
|
-
}),
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
build.onLoad({ filter: /[\\/]use-sync-external-store[\\/]shim[\\/]index\.js$/ }, () => ({
|
|
140
|
-
contents: "export { useSyncExternalStore } from 'react';",
|
|
141
|
-
loader: 'js',
|
|
142
|
-
}));
|
|
143
|
-
|
|
144
|
-
build.onLoad(
|
|
145
|
-
{
|
|
146
|
-
filter: /[\\/]use-sync-external-store[\\/]cjs[\\/]use-sync-external-store-shim\.development\.js$/,
|
|
147
|
-
},
|
|
148
|
-
() => ({
|
|
149
|
-
contents: "export { useSyncExternalStore } from 'react';",
|
|
150
|
-
loader: 'js',
|
|
151
|
-
}),
|
|
152
|
-
);
|
|
153
|
-
},
|
|
154
|
-
};
|
|
75
|
+
return await this.context.importServerModule(entrypointPath);
|
|
155
76
|
}
|
|
156
77
|
|
|
157
78
|
/**
|
|
@@ -167,14 +88,25 @@ export class ReactHmrStrategy extends HmrStrategy {
|
|
|
167
88
|
* @param explicitGraphEnabled - Enables explicit graph mode for React HMR bundling.
|
|
168
89
|
* In explicit mode, HMR builds omit AST server-only stripping plugins in React paths.
|
|
169
90
|
*/
|
|
91
|
+
private context: DefaultHmrContext;
|
|
92
|
+
private pageMetadataCache: ReactHmrPageMetadataCache;
|
|
93
|
+
private explicitGraphEnabled: boolean;
|
|
94
|
+
|
|
170
95
|
constructor(
|
|
171
|
-
|
|
172
|
-
|
|
96
|
+
context: DefaultHmrContext,
|
|
97
|
+
pageMetadataCache: ReactHmrPageMetadataCache,
|
|
173
98
|
mdxCompilerOptions?: CompileOptions,
|
|
174
|
-
|
|
99
|
+
ownedTemplateExtensions: string[] = ['.tsx'],
|
|
100
|
+
allTemplateExtensions: string[] = ['.tsx'],
|
|
101
|
+
explicitGraphEnabled = false,
|
|
175
102
|
) {
|
|
176
103
|
super();
|
|
104
|
+
this.context = context;
|
|
105
|
+
this.pageMetadataCache = pageMetadataCache;
|
|
106
|
+
this.explicitGraphEnabled = explicitGraphEnabled;
|
|
177
107
|
this.mdxCompilerOptions = mdxCompilerOptions;
|
|
108
|
+
this.ownedTemplateExtensions = new Set(ownedTemplateExtensions);
|
|
109
|
+
this.allTemplateExtensions = [...allTemplateExtensions].sort((a, b) => b.length - a.length);
|
|
178
110
|
}
|
|
179
111
|
|
|
180
112
|
/**
|
|
@@ -184,15 +116,11 @@ export class ReactHmrStrategy extends HmrStrategy {
|
|
|
184
116
|
* (including `node:*`) from breaking the browser bundle.
|
|
185
117
|
*/
|
|
186
118
|
private getBuildPlugins(declaredModules?: string[]): EcoBuildPlugin[] {
|
|
187
|
-
const allowSpecifiers =
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
'react-
|
|
191
|
-
|
|
192
|
-
'react/jsx-dev-runtime',
|
|
193
|
-
'react-dom/client',
|
|
194
|
-
...Array.from(this.context.getSpecifierMap().keys()),
|
|
195
|
-
];
|
|
119
|
+
const allowSpecifiers = getReactClientGraphAllowSpecifiers(this.context.getSpecifierMap().keys());
|
|
120
|
+
|
|
121
|
+
const runtimeAliasPlugin = createRuntimeSpecifierAliasPlugin(this.context.getSpecifierMap(), {
|
|
122
|
+
name: 'react-hmr-runtime-specifier-alias',
|
|
123
|
+
});
|
|
196
124
|
|
|
197
125
|
return [
|
|
198
126
|
createClientGraphBoundaryPlugin({
|
|
@@ -200,17 +128,50 @@ export class ReactHmrStrategy extends HmrStrategy {
|
|
|
200
128
|
alwaysAllowSpecifiers: allowSpecifiers,
|
|
201
129
|
declaredModules,
|
|
202
130
|
}),
|
|
131
|
+
...(runtimeAliasPlugin ? [runtimeAliasPlugin] : []),
|
|
203
132
|
...this.context.getPlugins(),
|
|
204
|
-
|
|
133
|
+
createUseSyncExternalStoreShimPlugin({
|
|
134
|
+
name: 'react-hmr-use-sync-external-store-shim',
|
|
135
|
+
namespace: 'ecopages-react-hmr-shim',
|
|
136
|
+
}),
|
|
205
137
|
];
|
|
206
138
|
}
|
|
207
139
|
|
|
208
140
|
private isReactEntrypoint(filePath: string): boolean {
|
|
209
|
-
if (filePath.endsWith('.
|
|
141
|
+
if (filePath.endsWith('.mdx')) {
|
|
142
|
+
return this.mdxCompilerOptions !== undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!filePath.endsWith('.tsx')) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!this.isRouteTemplate(filePath)) {
|
|
210
150
|
return true;
|
|
211
151
|
}
|
|
212
152
|
|
|
213
|
-
|
|
153
|
+
const templateExtension = this.resolveTemplateExtension(filePath);
|
|
154
|
+
if (!templateExtension) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return this.ownedTemplateExtensions.has(templateExtension);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Returns true when a route file uses a compound extension like `page.foo.tsx`.
|
|
163
|
+
*
|
|
164
|
+
* @remarks
|
|
165
|
+
* React integration owns plain `.tsx` route templates. Compound extensions in
|
|
166
|
+
* pages/layouts are integration-specific route templates and should not be
|
|
167
|
+
* claimed by React HMR strategy.
|
|
168
|
+
*/
|
|
169
|
+
private isRouteTemplate(filePath: string): boolean {
|
|
170
|
+
return filePath.startsWith(this.context.getPagesDir()) || filePath.startsWith(this.context.getLayoutsDir());
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private resolveTemplateExtension(filePath: string): string | undefined {
|
|
174
|
+
return this.allTemplateExtensions.find((extension) => filePath.endsWith(extension));
|
|
214
175
|
}
|
|
215
176
|
|
|
216
177
|
/**
|
|
@@ -269,11 +230,10 @@ export class ReactHmrStrategy extends HmrStrategy {
|
|
|
269
230
|
appLogger.debug(`Detected layout file change: ${_filePath}`);
|
|
270
231
|
}
|
|
271
232
|
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
this.knownEntrypoints.add(_filePath);
|
|
233
|
+
const changedEntrypointOutput = watchedFiles.get(_filePath);
|
|
234
|
+
const entrypointsToBuild = changedEntrypointOutput
|
|
235
|
+
? [[_filePath, changedEntrypointOutput]]
|
|
236
|
+
: watchedFiles.entries();
|
|
277
237
|
|
|
278
238
|
const updates: string[] = [];
|
|
279
239
|
for (const [entrypoint, outputUrl] of entrypointsToBuild) {
|
|
@@ -346,16 +306,13 @@ export class ReactHmrStrategy extends HmrStrategy {
|
|
|
346
306
|
plugins.unshift(mdxPlugin);
|
|
347
307
|
}
|
|
348
308
|
|
|
349
|
-
const result = await
|
|
309
|
+
const result = await this.context.getBrowserBundleService().bundle({
|
|
310
|
+
profile: 'hmr-entrypoint',
|
|
350
311
|
entrypoints: [entrypointPath],
|
|
351
312
|
outdir: tempDir,
|
|
352
313
|
naming: `[name].[hash].tmp`,
|
|
353
|
-
target: 'browser',
|
|
354
|
-
format: 'esm',
|
|
355
|
-
sourcemap: 'none',
|
|
356
314
|
plugins,
|
|
357
315
|
minify: false,
|
|
358
|
-
external: ['react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime', 'react-dom/client'],
|
|
359
316
|
});
|
|
360
317
|
|
|
361
318
|
if (!result.success) {
|
|
@@ -386,7 +343,7 @@ export class ReactHmrStrategy extends HmrStrategy {
|
|
|
386
343
|
}
|
|
387
344
|
|
|
388
345
|
/**
|
|
389
|
-
* Processes bundled output
|
|
346
|
+
* Processes bundled output and injects the React HMR handler.
|
|
390
347
|
* Writes to temp file first, then renames atomically to avoid conflicts.
|
|
391
348
|
*
|
|
392
349
|
* @param tempPath - Path to the temporary bundled file
|
|
@@ -403,7 +360,6 @@ export class ReactHmrStrategy extends HmrStrategy {
|
|
|
403
360
|
try {
|
|
404
361
|
let code = await fileSystem.readFile(tempPath);
|
|
405
362
|
|
|
406
|
-
code = this.replaceBareSpecifiers(code);
|
|
407
363
|
code = injectHmrHandler(code);
|
|
408
364
|
|
|
409
365
|
await fileSystem.writeAsync(finalPath, code);
|
|
@@ -427,29 +383,4 @@ export class ReactHmrStrategy extends HmrStrategy {
|
|
|
427
383
|
return false;
|
|
428
384
|
}
|
|
429
385
|
}
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Replaces bare specifiers with runtime URLs.
|
|
433
|
-
*
|
|
434
|
-
* Handles both static imports and dynamic imports.
|
|
435
|
-
*
|
|
436
|
-
* @param code - The bundled code to transform
|
|
437
|
-
* @returns The transformed code with runtime URLs
|
|
438
|
-
*/
|
|
439
|
-
private replaceBareSpecifiers(code: string): string {
|
|
440
|
-
const specifierMap = this.context.getSpecifierMap();
|
|
441
|
-
|
|
442
|
-
if (specifierMap.size === 0) {
|
|
443
|
-
return code;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
let result = code;
|
|
447
|
-
for (const [bareSpec, runtimeUrl] of specifierMap.entries()) {
|
|
448
|
-
const escaped = bareSpec.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
449
|
-
result = result.replace(new RegExp(`from\\s*["']${escaped}["']`, 'g'), `from "${runtimeUrl}"`);
|
|
450
|
-
result = result.replace(new RegExp(`import\\(["']${escaped}["']\\)`, 'g'), `import("${runtimeUrl}")`);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
return result;
|
|
454
|
-
}
|
|
455
386
|
}
|