@ecopages/react 0.2.0-alpha.51 → 0.2.0-alpha.53
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/package.json +3 -3
- package/src/react-hmr-strategy.d.ts +34 -5
- package/src/react-hmr-strategy.js +115 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecopages/react",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.53",
|
|
4
4
|
"description": "React integration for Ecopages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ecopages",
|
|
@@ -69,14 +69,14 @@
|
|
|
69
69
|
"directory": "packages/integrations/react"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
|
-
"@ecopages/core": "0.2.0-alpha.
|
|
72
|
+
"@ecopages/core": "0.2.0-alpha.53",
|
|
73
73
|
"@types/react": "^19",
|
|
74
74
|
"@types/react-dom": "^19",
|
|
75
75
|
"react": "^19",
|
|
76
76
|
"react-dom": "^19"
|
|
77
77
|
},
|
|
78
78
|
"dependencies": {
|
|
79
|
-
"@ecopages/file-system": "0.2.0-alpha.
|
|
79
|
+
"@ecopages/file-system": "0.2.0-alpha.53",
|
|
80
80
|
"@ecopages/logger": "^0.2.3",
|
|
81
81
|
"@mdx-js/esbuild": "^3.1.1",
|
|
82
82
|
"@mdx-js/mdx": "^3.1.1",
|
|
@@ -100,11 +100,21 @@ export declare class ReactHmrStrategy extends HmrStrategy {
|
|
|
100
100
|
private isRouteTemplate;
|
|
101
101
|
private resolveTemplateExtension;
|
|
102
102
|
private ownsWatchedEntrypoint;
|
|
103
|
+
private configContainsFile;
|
|
104
|
+
private pageModuleRequiresLayoutRefresh;
|
|
105
|
+
private hasLayoutOwnedDependencyTarget;
|
|
103
106
|
/**
|
|
104
107
|
* Determines if the file is a React/MDX entrypoint that's registered for HMR.
|
|
105
108
|
*
|
|
109
|
+
* Uses a three-way decision strategy for selective invalidation:
|
|
110
|
+
* 1. If the file is a watched entrypoint, check if React owns it
|
|
111
|
+
* 2. If the file is a dependency of watched entrypoints (via dependency graph),
|
|
112
|
+
* check if any affected entrypoints are React-owned. Returns false if hits
|
|
113
|
+
* exist but none are owned (prevents unnecessary rebuilds).
|
|
114
|
+
* 3. Otherwise, check if the file itself is a React entrypoint template
|
|
115
|
+
*
|
|
106
116
|
* @param filePath - Absolute path to the changed file
|
|
107
|
-
* @returns True if this
|
|
117
|
+
* @returns True if this file should trigger React HMR rebuilds
|
|
108
118
|
*/
|
|
109
119
|
matches(filePath: string): boolean;
|
|
110
120
|
/**
|
|
@@ -123,7 +133,6 @@ export declare class ReactHmrStrategy extends HmrStrategy {
|
|
|
123
133
|
private getEntrypointOutput;
|
|
124
134
|
private getGroupedTempOutputPattern;
|
|
125
135
|
private collectReactPageBuildTargets;
|
|
126
|
-
private getRequestedTargets;
|
|
127
136
|
/**
|
|
128
137
|
* Expands one HMR request into the full React page build cohort when needed.
|
|
129
138
|
*
|
|
@@ -135,12 +144,17 @@ export declare class ReactHmrStrategy extends HmrStrategy {
|
|
|
135
144
|
private resolveBuildTargets;
|
|
136
145
|
private partitionBuildTargets;
|
|
137
146
|
/**
|
|
138
|
-
* Processes a React file change by rebuilding
|
|
147
|
+
* Processes a React file change by rebuilding affected React entrypoints.
|
|
148
|
+
*
|
|
149
|
+
* Uses a three-way decision strategy for selective invalidation:
|
|
150
|
+
* 1. Changed file is a watched entrypoint: rebuild only that entrypoint
|
|
151
|
+
* 2. Dependency graph has hits: rebuild only affected React-owned entrypoints.
|
|
152
|
+
* If hits exist but none map to React-owned entrypoints, return 'none' to
|
|
153
|
+
* prevent unnecessary rebuilds.
|
|
154
|
+
* 3. Dependency graph miss: fall back to rebuilding all watched entrypoints
|
|
139
155
|
*
|
|
140
156
|
* For layout files, broadcasts a 'layout-update' event to trigger full page reload.
|
|
141
157
|
* For regular components/pages, broadcasts 'update' events for module-level HMR.
|
|
142
|
-
* When a page entrypoint is first registered, only that entrypoint is built.
|
|
143
|
-
* Subsequent file updates rebuild all watched React entrypoints as usual.
|
|
144
158
|
*
|
|
145
159
|
* @param _filePath - Absolute path to the changed file
|
|
146
160
|
* @returns Action to broadcast update events (layout-update for layouts, update for components)
|
|
@@ -149,11 +163,26 @@ export declare class ReactHmrStrategy extends HmrStrategy {
|
|
|
149
163
|
/**
|
|
150
164
|
* Bundles a single React/MDX entrypoint with HMR support.
|
|
151
165
|
*
|
|
166
|
+
* After successful bundling, populates the entrypoint dependency graph with
|
|
167
|
+
* the build's dependency metadata. This enables selective invalidation on
|
|
168
|
+
* subsequent file changes, so only entrypoints affected by a changed
|
|
169
|
+
* dependency are rebuilt.
|
|
170
|
+
*
|
|
152
171
|
* @param entrypointPath - Absolute path to the source file
|
|
153
172
|
* @param outputUrl - URL path for the bundled file
|
|
154
173
|
* @returns True if bundling was successful
|
|
155
174
|
*/
|
|
156
175
|
private bundleReactEntrypoint;
|
|
176
|
+
/**
|
|
177
|
+
* Bundles multiple React/MDX entrypoints in a single build pass.
|
|
178
|
+
*
|
|
179
|
+
* Uses code splitting to share common dependencies across entrypoints.
|
|
180
|
+
* After successful bundling, populates the entrypoint dependency graph with
|
|
181
|
+
* the build's dependency metadata for selective invalidation.
|
|
182
|
+
*
|
|
183
|
+
* @param entrypoints - Array of entrypoint paths and their output URLs
|
|
184
|
+
* @returns Array of output URLs that were successfully built
|
|
185
|
+
*/
|
|
157
186
|
private bundleReactEntrypoints;
|
|
158
187
|
private resolveTempOutputPath;
|
|
159
188
|
/**
|
|
@@ -7,6 +7,7 @@ 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 { someInConfigTree } from "./utils/component-config-traversal.js";
|
|
10
11
|
import { createReactMdxLoaderPlugin } from "./utils/react-mdx-loader-plugin.js";
|
|
11
12
|
import { getReactClientGraphAllowSpecifiers } from "./utils/react-runtime-alias-map.js";
|
|
12
13
|
const appLogger = new Logger("[ReactHmrStrategy]");
|
|
@@ -103,11 +104,44 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
103
104
|
ownsWatchedEntrypoint(filePath) {
|
|
104
105
|
return this.pageMetadataCache.ownsEntrypoint(filePath);
|
|
105
106
|
}
|
|
107
|
+
configContainsFile(config, filePath) {
|
|
108
|
+
const resolvedFilePath = path.resolve(filePath);
|
|
109
|
+
return someInConfigTree(config, (node) => {
|
|
110
|
+
if (!node.__eco?.file) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
return path.resolve(node.__eco.file) === resolvedFilePath;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
pageModuleRequiresLayoutRefresh(pageModule, filePath) {
|
|
117
|
+
return [pageModule.default?.config, pageModule.config].some((config) => {
|
|
118
|
+
return this.configContainsFile(config?.layout?.config, filePath);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
async hasLayoutOwnedDependencyTarget(changedFilePath, requestedTargets) {
|
|
122
|
+
for (const target of requestedTargets) {
|
|
123
|
+
if (!this.isPageEntrypoint(target.entrypointPath)) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const pageModule = await this.importNodePageModule(target.entrypointPath);
|
|
127
|
+
if (this.pageModuleRequiresLayoutRefresh(pageModule, changedFilePath)) {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
106
133
|
/**
|
|
107
134
|
* Determines if the file is a React/MDX entrypoint that's registered for HMR.
|
|
108
135
|
*
|
|
136
|
+
* Uses a three-way decision strategy for selective invalidation:
|
|
137
|
+
* 1. If the file is a watched entrypoint, check if React owns it
|
|
138
|
+
* 2. If the file is a dependency of watched entrypoints (via dependency graph),
|
|
139
|
+
* check if any affected entrypoints are React-owned. Returns false if hits
|
|
140
|
+
* exist but none are owned (prevents unnecessary rebuilds).
|
|
141
|
+
* 3. Otherwise, check if the file itself is a React entrypoint template
|
|
142
|
+
*
|
|
109
143
|
* @param filePath - Absolute path to the changed file
|
|
110
|
-
* @returns True if this
|
|
144
|
+
* @returns True if this file should trigger React HMR rebuilds
|
|
111
145
|
*/
|
|
112
146
|
matches(filePath) {
|
|
113
147
|
const watchedFiles = this.context.getWatchedFiles();
|
|
@@ -118,6 +152,15 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
118
152
|
if (watchedFiles.has(filePath)) {
|
|
119
153
|
return this.ownsWatchedEntrypoint(filePath);
|
|
120
154
|
}
|
|
155
|
+
const dependencyHits = this.context.getEntrypointDependencyGraph().getDependencyEntrypoints(filePath);
|
|
156
|
+
if (dependencyHits.size > 0) {
|
|
157
|
+
for (const entrypoint of dependencyHits) {
|
|
158
|
+
if (this.ownsWatchedEntrypoint(entrypoint)) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
121
164
|
return this.isReactEntrypoint(filePath);
|
|
122
165
|
}
|
|
123
166
|
/**
|
|
@@ -180,13 +223,6 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
180
223
|
(left, right) => left.entrypointPath.localeCompare(right.entrypointPath)
|
|
181
224
|
);
|
|
182
225
|
}
|
|
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
226
|
/**
|
|
191
227
|
* Expands one HMR request into the full React page build cohort when needed.
|
|
192
228
|
*
|
|
@@ -225,12 +261,17 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
225
261
|
};
|
|
226
262
|
}
|
|
227
263
|
/**
|
|
228
|
-
* Processes a React file change by rebuilding
|
|
264
|
+
* Processes a React file change by rebuilding affected React entrypoints.
|
|
265
|
+
*
|
|
266
|
+
* Uses a three-way decision strategy for selective invalidation:
|
|
267
|
+
* 1. Changed file is a watched entrypoint: rebuild only that entrypoint
|
|
268
|
+
* 2. Dependency graph has hits: rebuild only affected React-owned entrypoints.
|
|
269
|
+
* If hits exist but none map to React-owned entrypoints, return 'none' to
|
|
270
|
+
* prevent unnecessary rebuilds.
|
|
271
|
+
* 3. Dependency graph miss: fall back to rebuilding all watched entrypoints
|
|
229
272
|
*
|
|
230
273
|
* For layout files, broadcasts a 'layout-update' event to trigger full page reload.
|
|
231
274
|
* For regular components/pages, broadcasts 'update' events for module-level HMR.
|
|
232
|
-
* When a page entrypoint is first registered, only that entrypoint is built.
|
|
233
|
-
* Subsequent file updates rebuild all watched React entrypoints as usual.
|
|
234
275
|
*
|
|
235
276
|
* @param _filePath - Absolute path to the changed file
|
|
236
277
|
* @returns Action to broadcast update events (layout-update for layouts, update for components)
|
|
@@ -243,6 +284,7 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
243
284
|
return { type: "none" };
|
|
244
285
|
}
|
|
245
286
|
const isLayout = this.isLayoutFile(_filePath);
|
|
287
|
+
const isChangedPageEntrypoint = this.isPageEntrypoint(_filePath);
|
|
246
288
|
if (isLayout) {
|
|
247
289
|
appLogger.debug(`Detected layout file change: ${_filePath}`);
|
|
248
290
|
}
|
|
@@ -251,9 +293,42 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
251
293
|
appLogger.debug(`Skipping non-React watched entrypoint: ${_filePath}`);
|
|
252
294
|
return { type: "none" };
|
|
253
295
|
}
|
|
254
|
-
const
|
|
296
|
+
const dependencyHits = this.context.getEntrypointDependencyGraph().getDependencyEntrypoints(_filePath);
|
|
297
|
+
const hasDependencyHits = dependencyHits.size > 0;
|
|
298
|
+
const affectedEntrypoints = /* @__PURE__ */ new Map();
|
|
299
|
+
let hasOwnedLayoutDependencyHit = false;
|
|
300
|
+
let layoutOwnedPageTargets = [];
|
|
301
|
+
let hasLayoutOwnedRequestedTarget = false;
|
|
302
|
+
if (hasDependencyHits && !changedEntrypointOutput) {
|
|
303
|
+
for (const entrypoint of dependencyHits) {
|
|
304
|
+
const outputUrl = watchedFiles.get(entrypoint);
|
|
305
|
+
if (outputUrl && this.ownsWatchedEntrypoint(entrypoint)) {
|
|
306
|
+
affectedEntrypoints.set(entrypoint, outputUrl);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (this.isLayoutFile(entrypoint) && this.ownsWatchedEntrypoint(entrypoint)) {
|
|
310
|
+
hasOwnedLayoutDependencyHit = true;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (affectedEntrypoints.size === 0 && !hasOwnedLayoutDependencyHit) {
|
|
314
|
+
appLogger.debug(`Dependency hits found but none map to React-owned watched entrypoints`);
|
|
315
|
+
return { type: "none" };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (changedEntrypointOutput && !isLayout && !isChangedPageEntrypoint) {
|
|
319
|
+
layoutOwnedPageTargets = await this.collectReactPageBuildTargets();
|
|
320
|
+
hasLayoutOwnedRequestedTarget = await this.hasLayoutOwnedDependencyTarget(
|
|
321
|
+
_filePath,
|
|
322
|
+
layoutOwnedPageTargets
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
const requestedTargets = changedEntrypointOutput ? hasLayoutOwnedRequestedTarget ? [{ entrypointPath: _filePath, outputUrl: changedEntrypointOutput }, ...layoutOwnedPageTargets] : [{ entrypointPath: _filePath, outputUrl: changedEntrypointOutput }] : hasOwnedLayoutDependencyHit ? await this.collectReactPageBuildTargets() : hasDependencyHits ? Array.from(affectedEntrypoints, ([entrypointPath, outputUrl]) => ({ entrypointPath, outputUrl })) : Array.from(watchedFiles, ([entrypointPath, outputUrl]) => ({ entrypointPath, outputUrl }));
|
|
255
326
|
const groupedPageTargets = await this.resolveBuildTargets(requestedTargets, _filePath);
|
|
256
327
|
const { pageTargets, nonPageTargets } = this.partitionBuildTargets(requestedTargets, groupedPageTargets);
|
|
328
|
+
if (!changedEntrypointOutput) {
|
|
329
|
+
hasLayoutOwnedRequestedTarget = await this.hasLayoutOwnedDependencyTarget(_filePath, requestedTargets);
|
|
330
|
+
}
|
|
331
|
+
const requiresLayoutRefresh = isLayout || hasOwnedLayoutDependencyHit || hasLayoutOwnedRequestedTarget;
|
|
257
332
|
const updates = [];
|
|
258
333
|
const requestedOutputUrls = new Set(requestedTargets.map((target) => target.outputUrl));
|
|
259
334
|
if (pageTargets.length > 1) {
|
|
@@ -284,7 +359,7 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
284
359
|
}
|
|
285
360
|
}
|
|
286
361
|
if (updates.length > 0) {
|
|
287
|
-
if (
|
|
362
|
+
if (requiresLayoutRefresh) {
|
|
288
363
|
appLogger.debug(`Layout update detected, sending layout-update event`);
|
|
289
364
|
return {
|
|
290
365
|
type: "broadcast",
|
|
@@ -311,6 +386,11 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
311
386
|
/**
|
|
312
387
|
* Bundles a single React/MDX entrypoint with HMR support.
|
|
313
388
|
*
|
|
389
|
+
* After successful bundling, populates the entrypoint dependency graph with
|
|
390
|
+
* the build's dependency metadata. This enables selective invalidation on
|
|
391
|
+
* subsequent file changes, so only entrypoints affected by a changed
|
|
392
|
+
* dependency are rebuilt.
|
|
393
|
+
*
|
|
314
394
|
* @param entrypointPath - Absolute path to the source file
|
|
315
395
|
* @param outputUrl - URL path for the bundled file
|
|
316
396
|
* @returns True if bundling was successful
|
|
@@ -338,6 +418,12 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
338
418
|
appLogger.error(`Failed to build ${entrypointPath}:`, result.logs);
|
|
339
419
|
return false;
|
|
340
420
|
}
|
|
421
|
+
if (result.dependencyGraph?.entrypoints) {
|
|
422
|
+
const dependencyGraph = this.context.getEntrypointDependencyGraph();
|
|
423
|
+
for (const [entrypoint, deps] of Object.entries(result.dependencyGraph.entrypoints)) {
|
|
424
|
+
dependencyGraph.setEntrypointDependencies(entrypoint, deps);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
341
427
|
const tempFile = result.outputs[0]?.path;
|
|
342
428
|
if (!tempFile) {
|
|
343
429
|
appLogger.error(`No output file generated for ${entrypointPath}`);
|
|
@@ -355,6 +441,16 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
355
441
|
return false;
|
|
356
442
|
}
|
|
357
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* Bundles multiple React/MDX entrypoints in a single build pass.
|
|
446
|
+
*
|
|
447
|
+
* Uses code splitting to share common dependencies across entrypoints.
|
|
448
|
+
* After successful bundling, populates the entrypoint dependency graph with
|
|
449
|
+
* the build's dependency metadata for selective invalidation.
|
|
450
|
+
*
|
|
451
|
+
* @param entrypoints - Array of entrypoint paths and their output URLs
|
|
452
|
+
* @returns Array of output URLs that were successfully built
|
|
453
|
+
*/
|
|
358
454
|
async bundleReactEntrypoints(entrypoints) {
|
|
359
455
|
try {
|
|
360
456
|
const declaredModules = /* @__PURE__ */ new Set();
|
|
@@ -387,6 +483,12 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
387
483
|
appLogger.error(`Failed to build grouped React entrypoints:`, result.logs);
|
|
388
484
|
return [];
|
|
389
485
|
}
|
|
486
|
+
if (result.dependencyGraph?.entrypoints) {
|
|
487
|
+
const dependencyGraph = this.context.getEntrypointDependencyGraph();
|
|
488
|
+
for (const [entrypoint, deps] of Object.entries(result.dependencyGraph.entrypoints)) {
|
|
489
|
+
dependencyGraph.setEntrypointDependencies(entrypoint, deps);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
390
492
|
const updatedOutputs = [];
|
|
391
493
|
for (const { entrypointPath, outputUrl } of entrypoints) {
|
|
392
494
|
const { outputPath } = this.getEntrypointOutput(entrypointPath);
|