@ecopages/react 0.2.0-alpha.9 → 0.2.0-beta.1
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 +30 -13
- package/package.json +23 -12
- package/src/eco-embed.d.ts +11 -0
- package/src/eco-embed.js +11 -0
- package/src/react-hmr-strategy.d.ts +102 -18
- package/src/react-hmr-strategy.js +427 -50
- package/src/react-renderer.d.ts +100 -92
- package/src/react-renderer.js +356 -340
- package/src/react.constants.d.ts +1 -0
- package/src/react.constants.js +4 -0
- package/src/react.plugin.d.ts +25 -107
- package/src/react.plugin.js +109 -61
- package/src/react.types.d.ts +88 -0
- package/src/react.types.js +0 -0
- package/src/router-adapter.d.ts +7 -14
- package/src/runtime/use-sync-external-store-with-selector.d.ts +3 -0
- package/src/runtime/use-sync-external-store-with-selector.js +56 -0
- package/src/services/pages-index.d.ts +64 -0
- package/src/services/pages-index.js +73 -0
- package/src/services/react-bundle.service.d.ts +24 -9
- package/src/services/react-bundle.service.js +35 -24
- package/src/services/react-hmr-page-metadata-cache.d.ts +10 -1
- 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 +83 -64
- 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 +8 -3
- package/src/services/react-page-module.service.js +33 -26
- 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 +9 -2
- package/src/services/react-runtime-bundle.service.js +77 -16
- package/src/utils/client-graph-boundary-cache.d.ts +108 -0
- package/src/utils/client-graph-boundary-cache.js +116 -0
- package/src/utils/client-graph-boundary-plugin.d.ts +13 -5
- package/src/utils/client-graph-boundary-plugin.js +63 -5
- 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 +9 -5
- package/src/utils/hydration-scripts.js +119 -34
- 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 +1 -1
- package/src/utils/react-dom-runtime-interop-plugin.js +9 -0
- package/src/utils/react-mdx-loader-plugin.d.ts +1 -1
- package/src/utils/{react-runtime-specifier-map.d.ts → react-runtime-alias-map.d.ts} +3 -1
- package/src/utils/react-runtime-alias-map.js +90 -0
- package/CHANGELOG.md +0 -27
- package/src/react-hmr-strategy.ts +0 -386
- package/src/react-renderer.ts +0 -803
- package/src/react.plugin.ts +0 -276
- package/src/router-adapter.ts +0 -95
- package/src/services/react-bundle.service.ts +0 -108
- package/src/services/react-hmr-page-metadata-cache.ts +0 -24
- package/src/services/react-hydration-asset.service.ts +0 -263
- package/src/services/react-page-module.service.ts +0 -224
- package/src/services/react-runtime-bundle.service.ts +0 -172
- package/src/utils/client-graph-boundary-plugin.ts +0 -831
- 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 -459
- package/src/utils/reachability-analyzer.ts +0 -593
- package/src/utils/react-dom-runtime-interop-plugin.ts +0 -33
- package/src/utils/react-mdx-loader-plugin.ts +0 -63
- package/src/utils/react-runtime-specifier-map.js +0 -37
- package/src/utils/react-runtime-specifier-map.ts +0 -45
- package/src/utils/use-sync-external-store-shim-plugin.d.ts +0 -5
- package/src/utils/use-sync-external-store-shim-plugin.js +0 -41
- package/src/utils/use-sync-external-store-shim-plugin.ts +0 -45
package/CHANGELOG.md
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to `@ecopages/react` are documented here.
|
|
4
|
-
|
|
5
|
-
> **Note:** Changelog tracking begins at version `0.2.0`. Changes prior to this release are not recorded here but are available in the git history.
|
|
6
|
-
|
|
7
|
-
## [UNRELEASED] — TBD
|
|
8
|
-
|
|
9
|
-
### Features & Performance
|
|
10
|
-
|
|
11
|
-
- **Performance Hydration**: Introduced static reachability analysis to enforce explicit hydration boundaries and optimized HMR via metadata caching.
|
|
12
|
-
- **Service-Oriented Internals**: Refactored the integration into focused core-backed services for bundling, hydration, and page-module loading.
|
|
13
|
-
- **React MDX**: Inlined MDX support directly into the React integration for a zero-config setup, including Node-native compatibility for experimental startup.
|
|
14
|
-
|
|
15
|
-
### Bug Fixes & Refactoring
|
|
16
|
-
|
|
17
|
-
- **Handoff Stability**: Standardized router-backed page payloads and document owner markers for mixed-router stability during navigation.
|
|
18
|
-
- **Hydration Hardening**: Fixed island remount races, prop collisions, and layout metadata resolution during development and route handoffs.
|
|
19
|
-
- **Architecture**: Centralized runtime specifiers and consolidated browser-side integration state under `window.__ECO_PAGES__`.
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## Migration Notes
|
|
24
|
-
|
|
25
|
-
- The React integration now requires explicit client boundary declarations for client-rendered components.
|
|
26
|
-
- React MDX support is built in and no longer requires installing `@ecopages/mdx` just to enable React MDX routes.
|
|
27
|
-
- The internal service layer is not part of the public API and may change between releases.
|
|
@@ -1,386 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* React HMR Strategy
|
|
3
|
-
*
|
|
4
|
-
* Handles hot module replacement for React components.
|
|
5
|
-
* Triggers module invalidation on changes to ensure fresh component re-renders.
|
|
6
|
-
*
|
|
7
|
-
* @module
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import path from 'node:path';
|
|
11
|
-
|
|
12
|
-
import { HmrStrategy, HmrStrategyType, type HmrAction } from '@ecopages/core/hmr/hmr-strategy';
|
|
13
|
-
import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
|
|
14
|
-
import { createRuntimeSpecifierAliasPlugin } from '@ecopages/core/build/runtime-specifier-alias-plugin';
|
|
15
|
-
import { FileNotFoundError, fileSystem } from '@ecopages/file-system';
|
|
16
|
-
import { Logger } from '@ecopages/logger';
|
|
17
|
-
import type { DefaultHmrContext } from '@ecopages/core';
|
|
18
|
-
import type { CompileOptions } from '@mdx-js/mdx';
|
|
19
|
-
import { injectHmrHandler } from './utils/hmr-scripts.ts';
|
|
20
|
-
import { createClientGraphBoundaryPlugin } from './utils/client-graph-boundary-plugin.ts';
|
|
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';
|
|
24
|
-
import type { ReactHmrPageMetadataCache } from './services/react-hmr-page-metadata-cache.ts';
|
|
25
|
-
|
|
26
|
-
const appLogger = new Logger('[ReactHmrStrategy]');
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Strategy for handling React component HMR updates.
|
|
30
|
-
*
|
|
31
|
-
* This strategy provides React-specific HMR handling by rebuilding entrypoints
|
|
32
|
-
* and injecting HMR acceptance handlers that trigger module invalidation.
|
|
33
|
-
*
|
|
34
|
-
* The processing steps are:
|
|
35
|
-
* 1. Check if any React entrypoints are registered
|
|
36
|
-
* 2. Rebuild all React entrypoints (the changed file could be a dependency)
|
|
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
|
|
42
|
-
*
|
|
43
|
-
* @remarks
|
|
44
|
-
* This strategy has higher priority than generic JsHmrStrategy, allowing it
|
|
45
|
-
* to handle React files specially while falling back to generic handling for
|
|
46
|
-
* non-React files.
|
|
47
|
-
*
|
|
48
|
-
* Future enhancement: Track dependencies using Bun's transpiler API to only
|
|
49
|
-
* rebuild affected entrypoints instead of all of them.
|
|
50
|
-
*
|
|
51
|
-
* @see https://bun.sh/docs/runtime/transpiler
|
|
52
|
-
*
|
|
53
|
-
* @example
|
|
54
|
-
* ```typescript
|
|
55
|
-
* const context = {
|
|
56
|
-
* getWatchedFiles: () => watchedFilesMap,
|
|
57
|
-
* getSpecifierMap: () => specifierMap,
|
|
58
|
-
* getDistDir: () => '/path/to/dist/_hmr',
|
|
59
|
-
* getPlugins: () => [],
|
|
60
|
-
* getSrcDir: () => '/path/to/src',
|
|
61
|
-
* getLayoutsDir: () => '/path/to/src/layouts'
|
|
62
|
-
* };
|
|
63
|
-
* const strategy = new ReactHmrStrategy(context);
|
|
64
|
-
* ```
|
|
65
|
-
*/
|
|
66
|
-
export class ReactHmrStrategy extends HmrStrategy {
|
|
67
|
-
readonly type = HmrStrategyType.INTEGRATION;
|
|
68
|
-
private mdxCompilerOptions?: CompileOptions;
|
|
69
|
-
private readonly ownedTemplateExtensions: Set<string>;
|
|
70
|
-
private readonly allTemplateExtensions: string[];
|
|
71
|
-
private async importNodePageModule(entrypointPath: string): Promise<{
|
|
72
|
-
default?: { config?: Record<string, unknown> };
|
|
73
|
-
config?: Record<string, unknown>;
|
|
74
|
-
}> {
|
|
75
|
-
return await this.context.importServerModule(entrypointPath);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Creates a new React HMR strategy instance.
|
|
80
|
-
*
|
|
81
|
-
* @param context - The HMR context providing access to watched files, plugins, build directories,
|
|
82
|
-
* and the layouts directory for detecting layout file changes that require full
|
|
83
|
-
* page reloads instead of module-level HMR updates.
|
|
84
|
-
* @param pageMetadataCache - React-only cache of declared browser modules discovered during
|
|
85
|
-
* server rendering. This avoids re-importing unchanged page modules
|
|
86
|
-
* during save-time Fast Refresh rebuilds.
|
|
87
|
-
* @param mdxCompilerOptions - Optional MDX compiler options for processing .mdx files
|
|
88
|
-
* @param explicitGraphEnabled - Enables explicit graph mode for React HMR bundling.
|
|
89
|
-
* In explicit mode, HMR builds omit AST server-only stripping plugins in React paths.
|
|
90
|
-
*/
|
|
91
|
-
private context: DefaultHmrContext;
|
|
92
|
-
private pageMetadataCache: ReactHmrPageMetadataCache;
|
|
93
|
-
private explicitGraphEnabled: boolean;
|
|
94
|
-
|
|
95
|
-
constructor(
|
|
96
|
-
context: DefaultHmrContext,
|
|
97
|
-
pageMetadataCache: ReactHmrPageMetadataCache,
|
|
98
|
-
mdxCompilerOptions?: CompileOptions,
|
|
99
|
-
ownedTemplateExtensions: string[] = ['.tsx'],
|
|
100
|
-
allTemplateExtensions: string[] = ['.tsx'],
|
|
101
|
-
explicitGraphEnabled = false,
|
|
102
|
-
) {
|
|
103
|
-
super();
|
|
104
|
-
this.context = context;
|
|
105
|
-
this.pageMetadataCache = pageMetadataCache;
|
|
106
|
-
this.explicitGraphEnabled = explicitGraphEnabled;
|
|
107
|
-
this.mdxCompilerOptions = mdxCompilerOptions;
|
|
108
|
-
this.ownedTemplateExtensions = new Set(ownedTemplateExtensions);
|
|
109
|
-
this.allTemplateExtensions = [...allTemplateExtensions].sort((a, b) => b.length - a.length);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Returns build plugins for React HMR bundling.
|
|
114
|
-
*
|
|
115
|
-
* Includes the client graph boundary plugin to prevent undeclared imports
|
|
116
|
-
* (including `node:*`) from breaking the browser bundle.
|
|
117
|
-
*/
|
|
118
|
-
private getBuildPlugins(declaredModules?: string[]): EcoBuildPlugin[] {
|
|
119
|
-
const allowSpecifiers = getReactClientGraphAllowSpecifiers(this.context.getSpecifierMap().keys());
|
|
120
|
-
|
|
121
|
-
const runtimeAliasPlugin = createRuntimeSpecifierAliasPlugin(this.context.getSpecifierMap(), {
|
|
122
|
-
name: 'react-hmr-runtime-specifier-alias',
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
return [
|
|
126
|
-
createClientGraphBoundaryPlugin({
|
|
127
|
-
absWorkingDir: path.dirname(this.context.getSrcDir()),
|
|
128
|
-
alwaysAllowSpecifiers: allowSpecifiers,
|
|
129
|
-
declaredModules,
|
|
130
|
-
}),
|
|
131
|
-
...(runtimeAliasPlugin ? [runtimeAliasPlugin] : []),
|
|
132
|
-
...this.context.getPlugins(),
|
|
133
|
-
createUseSyncExternalStoreShimPlugin({
|
|
134
|
-
name: 'react-hmr-use-sync-external-store-shim',
|
|
135
|
-
namespace: 'ecopages-react-hmr-shim',
|
|
136
|
-
}),
|
|
137
|
-
];
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
private isReactEntrypoint(filePath: string): boolean {
|
|
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)) {
|
|
150
|
-
return true;
|
|
151
|
-
}
|
|
152
|
-
|
|
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));
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Determines if the file is a React/MDX entrypoint that's registered for HMR.
|
|
179
|
-
*
|
|
180
|
-
* @param filePath - Absolute path to the changed file
|
|
181
|
-
* @returns True if this is a registered React or MDX entrypoint
|
|
182
|
-
*/
|
|
183
|
-
matches(filePath: string): boolean {
|
|
184
|
-
const watchedFiles = this.context.getWatchedFiles();
|
|
185
|
-
appLogger.debug(`Checking ${filePath}. Watched: ${watchedFiles.size}`);
|
|
186
|
-
if (watchedFiles.size === 0) {
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return this.isReactEntrypoint(filePath);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Checks if a file is a layout file.
|
|
195
|
-
*
|
|
196
|
-
* Layout files require special HMR handling because they wrap multiple pages and affect
|
|
197
|
-
* the entire page structure. When a layout changes, we trigger a 'layout-update' event
|
|
198
|
-
* instead of a regular 'update' event, which instructs the browser to perform a full
|
|
199
|
-
* page reload (or clear cache and re-render) rather than attempting module-level HMR.
|
|
200
|
-
*
|
|
201
|
-
* @param filePath - Absolute path to the file
|
|
202
|
-
* @returns True if the file is in the layouts directory
|
|
203
|
-
*/
|
|
204
|
-
private isLayoutFile(filePath: string): boolean {
|
|
205
|
-
return filePath.startsWith(this.context.getLayoutsDir());
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Processes a React file change by rebuilding all React entrypoints.
|
|
210
|
-
*
|
|
211
|
-
* For layout files, broadcasts a 'layout-update' event to trigger full page reload.
|
|
212
|
-
* For regular components/pages, broadcasts 'update' events for module-level HMR.
|
|
213
|
-
* When a page entrypoint is first registered, only that entrypoint is built.
|
|
214
|
-
* Subsequent file updates rebuild all watched React entrypoints as usual.
|
|
215
|
-
*
|
|
216
|
-
* @param _filePath - Absolute path to the changed file
|
|
217
|
-
* @returns Action to broadcast update events (layout-update for layouts, update for components)
|
|
218
|
-
*/
|
|
219
|
-
async process(_filePath: string): Promise<HmrAction> {
|
|
220
|
-
appLogger.debug(`Processing ${_filePath}`);
|
|
221
|
-
const watchedFiles = this.context.getWatchedFiles();
|
|
222
|
-
|
|
223
|
-
if (watchedFiles.size === 0) {
|
|
224
|
-
appLogger.debug(`No watched files`);
|
|
225
|
-
return { type: 'none' };
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const isLayout = this.isLayoutFile(_filePath);
|
|
229
|
-
if (isLayout) {
|
|
230
|
-
appLogger.debug(`Detected layout file change: ${_filePath}`);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const changedEntrypointOutput = watchedFiles.get(_filePath);
|
|
234
|
-
const entrypointsToBuild = changedEntrypointOutput
|
|
235
|
-
? [[_filePath, changedEntrypointOutput]]
|
|
236
|
-
: watchedFiles.entries();
|
|
237
|
-
|
|
238
|
-
const updates: string[] = [];
|
|
239
|
-
for (const [entrypoint, outputUrl] of entrypointsToBuild) {
|
|
240
|
-
if (!this.isReactEntrypoint(entrypoint)) {
|
|
241
|
-
continue;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
appLogger.debug(`Bundling ${entrypoint}`);
|
|
245
|
-
const success = await this.bundleReactEntrypoint(entrypoint, outputUrl);
|
|
246
|
-
if (success) {
|
|
247
|
-
updates.push(outputUrl);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (updates.length > 0) {
|
|
252
|
-
if (isLayout) {
|
|
253
|
-
appLogger.debug(`Layout update detected, sending layout-update event`);
|
|
254
|
-
return {
|
|
255
|
-
type: 'broadcast',
|
|
256
|
-
events: [
|
|
257
|
-
{
|
|
258
|
-
type: 'layout-update',
|
|
259
|
-
},
|
|
260
|
-
],
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
appLogger.debug(`Broadcasting ${updates.length} updates`);
|
|
265
|
-
return {
|
|
266
|
-
type: 'broadcast',
|
|
267
|
-
events: updates.map((path) => ({
|
|
268
|
-
type: 'update',
|
|
269
|
-
path,
|
|
270
|
-
timestamp: Date.now(),
|
|
271
|
-
})),
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
appLogger.debug(`No updates generated`);
|
|
276
|
-
return { type: 'none' };
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Bundles a single React/MDX entrypoint with HMR support.
|
|
281
|
-
*
|
|
282
|
-
* @param entrypointPath - Absolute path to the source file
|
|
283
|
-
* @param outputUrl - URL path for the bundled file
|
|
284
|
-
* @returns True if bundling was successful
|
|
285
|
-
*/
|
|
286
|
-
private async bundleReactEntrypoint(entrypointPath: string, outputUrl: string): Promise<boolean> {
|
|
287
|
-
try {
|
|
288
|
-
const isMdx = entrypointPath.endsWith('.mdx');
|
|
289
|
-
const srcDir = this.context.getSrcDir();
|
|
290
|
-
const relativePath = path.relative(srcDir, entrypointPath);
|
|
291
|
-
const relativePathJs = relativePath.replace(/\.(tsx?|jsx?|mdx)$/, '.js');
|
|
292
|
-
const encodedPathJs = this.encodeDynamicSegments(relativePathJs);
|
|
293
|
-
const outputPath = path.join(this.context.getDistDir(), encodedPathJs);
|
|
294
|
-
const tempDir = path.dirname(outputPath);
|
|
295
|
-
|
|
296
|
-
const declaredModules = this.pageMetadataCache.getDeclaredModules(entrypointPath)
|
|
297
|
-
? this.pageMetadataCache.getDeclaredModules(entrypointPath)!
|
|
298
|
-
: isMdx
|
|
299
|
-
? await collectPageDeclaredModules(entrypointPath)
|
|
300
|
-
: collectPageDeclaredModulesFromModule(await this.importNodePageModule(entrypointPath));
|
|
301
|
-
const plugins = this.getBuildPlugins(declaredModules);
|
|
302
|
-
|
|
303
|
-
if (isMdx && this.mdxCompilerOptions) {
|
|
304
|
-
const { createReactMdxLoaderPlugin } = await import('./utils/react-mdx-loader-plugin.ts');
|
|
305
|
-
const mdxPlugin = createReactMdxLoaderPlugin(this.mdxCompilerOptions);
|
|
306
|
-
plugins.unshift(mdxPlugin);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const result = await this.context.getBrowserBundleService().bundle({
|
|
310
|
-
profile: 'hmr-entrypoint',
|
|
311
|
-
entrypoints: [entrypointPath],
|
|
312
|
-
outdir: tempDir,
|
|
313
|
-
naming: `[name].[hash].tmp`,
|
|
314
|
-
plugins,
|
|
315
|
-
minify: false,
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
if (!result.success) {
|
|
319
|
-
appLogger.error(`Failed to build ${entrypointPath}:`, result.logs);
|
|
320
|
-
return false;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const tempFile = result.outputs[0]?.path;
|
|
324
|
-
if (!tempFile) {
|
|
325
|
-
appLogger.error(`No output file generated for ${entrypointPath}`);
|
|
326
|
-
return false;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const processed = await this.processOutput(tempFile, outputPath, outputUrl);
|
|
330
|
-
return processed;
|
|
331
|
-
} catch (error) {
|
|
332
|
-
appLogger.error(`Error bundling ${entrypointPath}:`, error as Error);
|
|
333
|
-
return false;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Encodes dynamic route segments (brackets) in file paths.
|
|
339
|
-
* Converts `[slug]` to `_slug_` to avoid filesystem issues.
|
|
340
|
-
*/
|
|
341
|
-
private encodeDynamicSegments(filepath: string): string {
|
|
342
|
-
return filepath.replace(/\[([^\]]+)\]/g, '_$1_');
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Processes bundled output and injects the React HMR handler.
|
|
347
|
-
* Writes to temp file first, then renames atomically to avoid conflicts.
|
|
348
|
-
*
|
|
349
|
-
* @param tempPath - Path to the temporary bundled file
|
|
350
|
-
* @param finalPath - Final destination path
|
|
351
|
-
* @param url - URL path for logging
|
|
352
|
-
* @returns True if processing was successful
|
|
353
|
-
*/
|
|
354
|
-
private async processOutput(tempPath: string, finalPath: string, url: string): Promise<boolean> {
|
|
355
|
-
if (!fileSystem.exists(tempPath)) {
|
|
356
|
-
appLogger.debug(`Skipping stale temp output for ${url}: ${tempPath}`);
|
|
357
|
-
return false;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
try {
|
|
361
|
-
let code = await fileSystem.readFile(tempPath);
|
|
362
|
-
|
|
363
|
-
code = injectHmrHandler(code);
|
|
364
|
-
|
|
365
|
-
await fileSystem.writeAsync(finalPath, code);
|
|
366
|
-
await fileSystem.removeAsync(tempPath).catch(() => {});
|
|
367
|
-
|
|
368
|
-
appLogger.debug(`Processed ${url} with HMR handler`);
|
|
369
|
-
return true;
|
|
370
|
-
} catch (error) {
|
|
371
|
-
if (
|
|
372
|
-
error instanceof FileNotFoundError ||
|
|
373
|
-
(error instanceof Error && error.message.includes('not found')) ||
|
|
374
|
-
(error instanceof Error && 'code' in error && error.code === 'ENOENT')
|
|
375
|
-
) {
|
|
376
|
-
appLogger.debug(`Skipping stale temp output for ${url}: ${tempPath}`);
|
|
377
|
-
await fileSystem.removeAsync(tempPath).catch(() => {});
|
|
378
|
-
return false;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
appLogger.error(`Error processing output for ${url}:`, error as Error);
|
|
382
|
-
await fileSystem.removeAsync(tempPath).catch(() => {});
|
|
383
|
-
return false;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|