@ecopages/react 0.2.0-alpha.51 → 0.2.0-alpha.52

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/react",
3
- "version": "0.2.0-alpha.51",
3
+ "version": "0.2.0-alpha.52",
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.51",
72
+ "@ecopages/core": "0.2.0-alpha.52",
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.51",
79
+ "@ecopages/file-system": "0.2.0-alpha.52",
80
80
  "@ecopages/logger": "^0.2.3",
81
81
  "@mdx-js/esbuild": "^3.1.1",
82
82
  "@mdx-js/mdx": "^3.1.1",
@@ -103,8 +103,15 @@ export declare class ReactHmrStrategy extends HmrStrategy {
103
103
  /**
104
104
  * Determines if the file is a React/MDX entrypoint that's registered for HMR.
105
105
  *
106
+ * Uses a three-way decision strategy for selective invalidation:
107
+ * 1. If the file is a watched entrypoint, check if React owns it
108
+ * 2. If the file is a dependency of watched entrypoints (via dependency graph),
109
+ * check if any affected entrypoints are React-owned. Returns false if hits
110
+ * exist but none are owned (prevents unnecessary rebuilds).
111
+ * 3. Otherwise, check if the file itself is a React entrypoint template
112
+ *
106
113
  * @param filePath - Absolute path to the changed file
107
- * @returns True if this is a registered React or MDX entrypoint
114
+ * @returns True if this file should trigger React HMR rebuilds
108
115
  */
109
116
  matches(filePath: string): boolean;
110
117
  /**
@@ -123,7 +130,6 @@ export declare class ReactHmrStrategy extends HmrStrategy {
123
130
  private getEntrypointOutput;
124
131
  private getGroupedTempOutputPattern;
125
132
  private collectReactPageBuildTargets;
126
- private getRequestedTargets;
127
133
  /**
128
134
  * Expands one HMR request into the full React page build cohort when needed.
129
135
  *
@@ -135,12 +141,17 @@ export declare class ReactHmrStrategy extends HmrStrategy {
135
141
  private resolveBuildTargets;
136
142
  private partitionBuildTargets;
137
143
  /**
138
- * Processes a React file change by rebuilding all React entrypoints.
144
+ * Processes a React file change by rebuilding affected React entrypoints.
145
+ *
146
+ * Uses a three-way decision strategy for selective invalidation:
147
+ * 1. Changed file is a watched entrypoint: rebuild only that entrypoint
148
+ * 2. Dependency graph has hits: rebuild only affected React-owned entrypoints.
149
+ * If hits exist but none map to React-owned entrypoints, return 'none' to
150
+ * prevent unnecessary rebuilds.
151
+ * 3. Dependency graph miss: fall back to rebuilding all watched entrypoints
139
152
  *
140
153
  * For layout files, broadcasts a 'layout-update' event to trigger full page reload.
141
154
  * 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
155
  *
145
156
  * @param _filePath - Absolute path to the changed file
146
157
  * @returns Action to broadcast update events (layout-update for layouts, update for components)
@@ -149,11 +160,26 @@ export declare class ReactHmrStrategy extends HmrStrategy {
149
160
  /**
150
161
  * Bundles a single React/MDX entrypoint with HMR support.
151
162
  *
163
+ * After successful bundling, populates the entrypoint dependency graph with
164
+ * the build's dependency metadata. This enables selective invalidation on
165
+ * subsequent file changes, so only entrypoints affected by a changed
166
+ * dependency are rebuilt.
167
+ *
152
168
  * @param entrypointPath - Absolute path to the source file
153
169
  * @param outputUrl - URL path for the bundled file
154
170
  * @returns True if bundling was successful
155
171
  */
156
172
  private bundleReactEntrypoint;
173
+ /**
174
+ * Bundles multiple React/MDX entrypoints in a single build pass.
175
+ *
176
+ * Uses code splitting to share common dependencies across entrypoints.
177
+ * After successful bundling, populates the entrypoint dependency graph with
178
+ * the build's dependency metadata for selective invalidation.
179
+ *
180
+ * @param entrypoints - Array of entrypoint paths and their output URLs
181
+ * @returns Array of output URLs that were successfully built
182
+ */
157
183
  private bundleReactEntrypoints;
158
184
  private resolveTempOutputPath;
159
185
  /**
@@ -106,8 +106,15 @@ class ReactHmrStrategy extends HmrStrategy {
106
106
  /**
107
107
  * Determines if the file is a React/MDX entrypoint that's registered for HMR.
108
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
+ *
109
116
  * @param filePath - Absolute path to the changed file
110
- * @returns True if this is a registered React or MDX entrypoint
117
+ * @returns True if this file should trigger React HMR rebuilds
111
118
  */
112
119
  matches(filePath) {
113
120
  const watchedFiles = this.context.getWatchedFiles();
@@ -118,6 +125,15 @@ class ReactHmrStrategy extends HmrStrategy {
118
125
  if (watchedFiles.has(filePath)) {
119
126
  return this.ownsWatchedEntrypoint(filePath);
120
127
  }
128
+ const dependencyHits = this.context.getEntrypointDependencyGraph().getDependencyEntrypoints(filePath);
129
+ if (dependencyHits.size > 0) {
130
+ for (const entrypoint of dependencyHits) {
131
+ if (this.ownsWatchedEntrypoint(entrypoint)) {
132
+ return true;
133
+ }
134
+ }
135
+ return false;
136
+ }
121
137
  return this.isReactEntrypoint(filePath);
122
138
  }
123
139
  /**
@@ -180,13 +196,6 @@ class ReactHmrStrategy extends HmrStrategy {
180
196
  (left, right) => left.entrypointPath.localeCompare(right.entrypointPath)
181
197
  );
182
198
  }
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
199
  /**
191
200
  * Expands one HMR request into the full React page build cohort when needed.
192
201
  *
@@ -225,12 +234,17 @@ class ReactHmrStrategy extends HmrStrategy {
225
234
  };
226
235
  }
227
236
  /**
228
- * Processes a React file change by rebuilding all React entrypoints.
237
+ * Processes a React file change by rebuilding affected React entrypoints.
238
+ *
239
+ * Uses a three-way decision strategy for selective invalidation:
240
+ * 1. Changed file is a watched entrypoint: rebuild only that entrypoint
241
+ * 2. Dependency graph has hits: rebuild only affected React-owned entrypoints.
242
+ * If hits exist but none map to React-owned entrypoints, return 'none' to
243
+ * prevent unnecessary rebuilds.
244
+ * 3. Dependency graph miss: fall back to rebuilding all watched entrypoints
229
245
  *
230
246
  * For layout files, broadcasts a 'layout-update' event to trigger full page reload.
231
247
  * 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
248
  *
235
249
  * @param _filePath - Absolute path to the changed file
236
250
  * @returns Action to broadcast update events (layout-update for layouts, update for components)
@@ -251,7 +265,22 @@ class ReactHmrStrategy extends HmrStrategy {
251
265
  appLogger.debug(`Skipping non-React watched entrypoint: ${_filePath}`);
252
266
  return { type: "none" };
253
267
  }
254
- const requestedTargets = this.getRequestedTargets(_filePath, changedEntrypointOutput, watchedFiles);
268
+ const dependencyHits = this.context.getEntrypointDependencyGraph().getDependencyEntrypoints(_filePath);
269
+ const hasDependencyHits = dependencyHits.size > 0;
270
+ const affectedEntrypoints = /* @__PURE__ */ new Map();
271
+ if (hasDependencyHits && !changedEntrypointOutput) {
272
+ for (const entrypoint of dependencyHits) {
273
+ const outputUrl = watchedFiles.get(entrypoint);
274
+ if (outputUrl && this.ownsWatchedEntrypoint(entrypoint)) {
275
+ affectedEntrypoints.set(entrypoint, outputUrl);
276
+ }
277
+ }
278
+ if (affectedEntrypoints.size === 0) {
279
+ appLogger.debug(`Dependency hits found but none map to React-owned watched entrypoints`);
280
+ return { type: "none" };
281
+ }
282
+ }
283
+ const requestedTargets = changedEntrypointOutput ? [{ entrypointPath: _filePath, outputUrl: changedEntrypointOutput }] : hasDependencyHits ? Array.from(affectedEntrypoints, ([entrypointPath, outputUrl]) => ({ entrypointPath, outputUrl })) : Array.from(watchedFiles, ([entrypointPath, outputUrl]) => ({ entrypointPath, outputUrl }));
255
284
  const groupedPageTargets = await this.resolveBuildTargets(requestedTargets, _filePath);
256
285
  const { pageTargets, nonPageTargets } = this.partitionBuildTargets(requestedTargets, groupedPageTargets);
257
286
  const updates = [];
@@ -311,6 +340,11 @@ class ReactHmrStrategy extends HmrStrategy {
311
340
  /**
312
341
  * Bundles a single React/MDX entrypoint with HMR support.
313
342
  *
343
+ * After successful bundling, populates the entrypoint dependency graph with
344
+ * the build's dependency metadata. This enables selective invalidation on
345
+ * subsequent file changes, so only entrypoints affected by a changed
346
+ * dependency are rebuilt.
347
+ *
314
348
  * @param entrypointPath - Absolute path to the source file
315
349
  * @param outputUrl - URL path for the bundled file
316
350
  * @returns True if bundling was successful
@@ -338,6 +372,12 @@ class ReactHmrStrategy extends HmrStrategy {
338
372
  appLogger.error(`Failed to build ${entrypointPath}:`, result.logs);
339
373
  return false;
340
374
  }
375
+ if (result.dependencyGraph?.entrypoints) {
376
+ const dependencyGraph = this.context.getEntrypointDependencyGraph();
377
+ for (const [entrypoint, deps] of Object.entries(result.dependencyGraph.entrypoints)) {
378
+ dependencyGraph.setEntrypointDependencies(entrypoint, deps);
379
+ }
380
+ }
341
381
  const tempFile = result.outputs[0]?.path;
342
382
  if (!tempFile) {
343
383
  appLogger.error(`No output file generated for ${entrypointPath}`);
@@ -355,6 +395,16 @@ class ReactHmrStrategy extends HmrStrategy {
355
395
  return false;
356
396
  }
357
397
  }
398
+ /**
399
+ * Bundles multiple React/MDX entrypoints in a single build pass.
400
+ *
401
+ * Uses code splitting to share common dependencies across entrypoints.
402
+ * After successful bundling, populates the entrypoint dependency graph with
403
+ * the build's dependency metadata for selective invalidation.
404
+ *
405
+ * @param entrypoints - Array of entrypoint paths and their output URLs
406
+ * @returns Array of output URLs that were successfully built
407
+ */
358
408
  async bundleReactEntrypoints(entrypoints) {
359
409
  try {
360
410
  const declaredModules = /* @__PURE__ */ new Set();
@@ -387,6 +437,12 @@ class ReactHmrStrategy extends HmrStrategy {
387
437
  appLogger.error(`Failed to build grouped React entrypoints:`, result.logs);
388
438
  return [];
389
439
  }
440
+ if (result.dependencyGraph?.entrypoints) {
441
+ const dependencyGraph = this.context.getEntrypointDependencyGraph();
442
+ for (const [entrypoint, deps] of Object.entries(result.dependencyGraph.entrypoints)) {
443
+ dependencyGraph.setEntrypointDependencies(entrypoint, deps);
444
+ }
445
+ }
390
446
  const updatedOutputs = [];
391
447
  for (const { entrypointPath, outputUrl } of entrypoints) {
392
448
  const { outputPath } = this.getEntrypointOutput(entrypointPath);