@ecopages/core 0.2.0-beta.37 → 0.2.0-beta.38

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 (35) hide show
  1. package/package.json +11 -3
  2. package/src/eco/eco.browser.js +9 -2
  3. package/src/eco/eco.js +8 -2
  4. package/src/eco/eco.types.d.ts +29 -2
  5. package/src/route-renderer/README.md +2 -1
  6. package/src/route-renderer/orchestration/integration-renderer.d.ts +19 -3
  7. package/src/route-renderer/orchestration/integration-renderer.js +89 -8
  8. package/src/route-renderer/orchestration/page-browser-graph/grouped-graph-build-plan.d.ts +15 -0
  9. package/src/route-renderer/orchestration/page-browser-graph/grouped-graph-build-plan.js +9 -0
  10. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph-contribution.merge.d.ts +5 -0
  11. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph-contribution.merge.js +30 -0
  12. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph-session.d.ts +22 -6
  13. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph-session.js +145 -68
  14. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph.service.d.ts +9 -2
  15. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph.service.js +56 -39
  16. package/src/route-renderer/orchestration/page-browser-graph/route-instance-key.d.ts +29 -0
  17. package/src/route-renderer/orchestration/page-browser-graph/route-instance-key.js +54 -0
  18. package/src/route-renderer/orchestration/route-pipeline/integration-route-render-adapter.d.ts +3 -0
  19. package/src/route-renderer/orchestration/route-pipeline/integration-route-render-adapter.js +3 -2
  20. package/src/route-renderer/orchestration/route-pipeline/route-prepared-options.builder.d.ts +1 -0
  21. package/src/route-renderer/orchestration/route-pipeline/route-prepared-options.builder.js +3 -5
  22. package/src/route-renderer/orchestration/route-pipeline/route-render-orchestrator.d.ts +14 -4
  23. package/src/route-renderer/orchestration/route-pipeline/route-render-orchestrator.js +31 -4
  24. package/src/route-renderer/page-loading/file-scoped-dependency-components.d.ts +42 -0
  25. package/src/route-renderer/page-loading/file-scoped-dependency-components.js +93 -0
  26. package/src/route-renderer/page-loading/page-module-loader.d.ts +1 -0
  27. package/src/route-renderer/page-loading/page-module-loader.js +1 -0
  28. package/src/route-renderer/page-loading/resolved-page-dependencies.d.ts +12 -0
  29. package/src/route-renderer/page-loading/resolved-page-dependencies.js +38 -0
  30. package/src/static-site-generator/production-page-browser-graph-prebuild.d.ts +10 -6
  31. package/src/static-site-generator/production-page-browser-graph-prebuild.js +29 -10
  32. package/src/static-site-generator/static-site-generator.js +4 -1
  33. package/src/types/public-types.d.ts +9 -1
  34. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph-contribution.loader.d.ts +0 -9
  35. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph-contribution.loader.js +0 -11
@@ -1,23 +1,42 @@
1
1
  import path from 'node:path';
2
+ import { serializeGroupedGraphCacheKey } from './route-instance-key.js';
2
3
  import { createHash } from 'node:crypto';
3
4
  import { appLogger } from '../../../global/app-logger.js';
4
5
  function isSkipCacheGroupedOutcome(outcome) {
5
6
  return 'skipCache' in outcome;
6
7
  }
7
8
  function serializeGraphKey(key) {
8
- return `${key.policy}::${key.integrationName}::${key.routeFile}::${key.entryFingerprint}`;
9
+ return JSON.stringify([
10
+ 'graph',
11
+ key.policy,
12
+ key.integrationName,
13
+ key.routeFile,
14
+ key.dependencyInstanceKey,
15
+ key.entryFingerprint,
16
+ ]);
9
17
  }
10
18
  function normalizeDependencyPath(filePath) {
11
19
  return path.resolve(filePath);
12
20
  }
13
21
  function parseSerializedGraphKey(serializedKey) {
14
- if (serializedKey.startsWith('grouped::')) {
22
+ let parsed;
23
+ try {
24
+ parsed = JSON.parse(serializedKey);
25
+ }
26
+ catch {
15
27
  return undefined;
16
28
  }
17
- const [policy, integrationName, routeFile, entryFingerprint] = serializedKey.split('::');
18
- if (!policy || !integrationName || !routeFile || entryFingerprint === undefined) {
29
+ if (!Array.isArray(parsed) ||
30
+ parsed.length !== 6 ||
31
+ parsed[0] !== 'graph' ||
32
+ typeof parsed[1] !== 'string' ||
33
+ typeof parsed[2] !== 'string' ||
34
+ typeof parsed[3] !== 'string' ||
35
+ typeof parsed[4] !== 'string' ||
36
+ typeof parsed[5] !== 'string') {
19
37
  return undefined;
20
38
  }
39
+ const [, policy, integrationName, routeFile, dependencyInstanceKey, entryFingerprint] = parsed;
21
40
  if (policy !== 'development' && policy !== 'production') {
22
41
  return undefined;
23
42
  }
@@ -25,6 +44,7 @@ function parseSerializedGraphKey(serializedKey) {
25
44
  policy,
26
45
  integrationName,
27
46
  routeFile,
47
+ dependencyInstanceKey,
28
48
  entryFingerprint,
29
49
  };
30
50
  }
@@ -45,15 +65,23 @@ export class GraphDependencyIndex {
45
65
  this.removeDependencyBinding(normalizeDependencyPath(dependencyPath), serializedKey);
46
66
  }
47
67
  }
48
- bindGroupedRecord(integrationName, record) {
49
- const serializedKey = `grouped::${integrationName}`;
68
+ bindGroupedRecord(cacheKey, record) {
50
69
  for (const dependencyPath of record.dependencyPaths) {
51
- this.addDependencyBinding(normalizeDependencyPath(dependencyPath), serializedKey);
70
+ this.addDependencyBinding(normalizeDependencyPath(dependencyPath), cacheKey);
52
71
  }
53
72
  }
54
- unbindGroupedRecord(integrationName, record) {
55
- const serializedKey = `grouped::${integrationName}`;
73
+ unbindGroupedRecord(cacheKey, record) {
56
74
  for (const dependencyPath of record.dependencyPaths) {
75
+ this.removeDependencyBinding(normalizeDependencyPath(dependencyPath), cacheKey);
76
+ }
77
+ }
78
+ bindPaths(serializedKey, dependencyPaths) {
79
+ for (const dependencyPath of dependencyPaths) {
80
+ this.addDependencyBinding(normalizeDependencyPath(dependencyPath), serializedKey);
81
+ }
82
+ }
83
+ unbindPaths(serializedKey, dependencyPaths) {
84
+ for (const dependencyPath of dependencyPaths) {
57
85
  this.removeDependencyBinding(normalizeDependencyPath(dependencyPath), serializedKey);
58
86
  }
59
87
  }
@@ -85,34 +113,32 @@ export class GraphDependencyIndex {
85
113
  export class SessionPageBrowserGraphCache {
86
114
  records = new Map();
87
115
  inFlight = new Map();
116
+ inFlightDependencyPaths = new Map();
117
+ inFlightGenerations = new Map();
88
118
  groupedRecords = new Map();
89
119
  groupedInFlight = new Map();
90
120
  dependencyIndex = new GraphDependencyIndex();
91
121
  graphGenerations = new Map();
92
122
  groupedGenerations = new Map();
93
123
  buildCount = 0;
94
- getGraphByRoute(integrationName, routeFile, policy) {
95
- const normalizedRoute = normalizeDependencyPath(routeFile);
96
- for (const record of this.records.values()) {
97
- if (record.key.integrationName !== integrationName) {
98
- continue;
99
- }
100
- if (record.key.policy !== policy) {
101
- continue;
102
- }
103
- if (normalizeDependencyPath(record.key.routeFile) !== normalizedRoute) {
104
- continue;
105
- }
106
- return record.result;
107
- }
108
- return undefined;
109
- }
110
124
  /**
111
125
  * Page Browser Graph compilations in this dev-server session.
112
126
  */
113
127
  getBuildCount() {
114
128
  return this.buildCount;
115
129
  }
130
+ getGraphByRoute(integrationName, routeFile, policy, dependencyInstanceKey = '') {
131
+ const normalizedRoute = normalizeDependencyPath(routeFile);
132
+ for (const record of this.records.values()) {
133
+ if (record.key.integrationName === integrationName &&
134
+ record.key.policy === policy &&
135
+ normalizeDependencyPath(record.key.routeFile) === normalizedRoute &&
136
+ record.key.dependencyInstanceKey === dependencyInstanceKey) {
137
+ return record.result;
138
+ }
139
+ }
140
+ return undefined;
141
+ }
116
142
  getAffectedGraphIdentities(filePath) {
117
143
  const identities = [];
118
144
  for (const serializedKey of this.dependencyIndex.getAffectedKeys(filePath)) {
@@ -123,7 +149,7 @@ export class SessionPageBrowserGraphCache {
123
149
  }
124
150
  return identities;
125
151
  }
126
- resolveGraph(key, build) {
152
+ resolveGraph(key, provisionalDependencyPaths, build) {
127
153
  const serializedKey = serializeGraphKey(key);
128
154
  const cached = this.records.get(serializedKey);
129
155
  if (cached) {
@@ -134,35 +160,38 @@ export class SessionPageBrowserGraphCache {
134
160
  return pending;
135
161
  }
136
162
  const buildGeneration = this.bumpGraphGeneration(serializedKey);
163
+ this.inFlightDependencyPaths.set(serializedKey, provisionalDependencyPaths);
164
+ this.inFlightGenerations.set(serializedKey, buildGeneration);
165
+ this.bindDependencyPaths(serializedKey, provisionalDependencyPaths);
137
166
  const buildPromise = this.runGraphBuild(serializedKey, key, buildGeneration, build);
138
167
  this.inFlight.set(serializedKey, buildPromise);
139
168
  return buildPromise;
140
169
  }
141
- resolveGroupedGraph(integrationName, build) {
142
- const cached = this.groupedRecords.get(integrationName);
170
+ resolveGroupedGraph(scope, build) {
171
+ const cacheKey = serializeGroupedGraphCacheKey(scope);
172
+ const cached = this.groupedRecords.get(cacheKey);
143
173
  if (cached) {
144
174
  return Promise.resolve(cached.assetsByRoute);
145
175
  }
146
- const pending = this.groupedInFlight.get(integrationName);
176
+ const pending = this.groupedInFlight.get(cacheKey);
147
177
  if (pending) {
148
178
  return pending;
149
179
  }
150
- const buildGeneration = this.bumpGroupedGeneration(integrationName);
151
- const buildPromise = this.runGroupedBuild(integrationName, buildGeneration, build);
152
- this.groupedInFlight.set(integrationName, buildPromise);
180
+ const buildGeneration = this.bumpGroupedGeneration(cacheKey);
181
+ const buildPromise = this.runGroupedBuild(cacheKey, buildGeneration, build);
182
+ this.groupedInFlight.set(cacheKey, buildPromise);
153
183
  return buildPromise;
154
184
  }
155
- peekGroupedGraph(integrationName) {
156
- return this.groupedRecords.get(integrationName)?.assetsByRoute;
185
+ peekGroupedGraph(scope) {
186
+ return this.groupedRecords.get(serializeGroupedGraphCacheKey(scope))?.assetsByRoute;
157
187
  }
158
188
  invalidateByFilePath(filePath) {
159
189
  const normalizedFilePath = normalizeDependencyPath(filePath);
160
190
  const affectedKeys = this.dependencyIndex.getAffectedKeys(normalizedFilePath);
161
191
  let invalidated = 0;
162
192
  for (const serializedKey of affectedKeys) {
163
- if (serializedKey.startsWith('grouped::')) {
164
- const integrationName = serializedKey.slice('grouped::'.length);
165
- invalidated += this.invalidateGroupedRecord(integrationName);
193
+ if (this.groupedRecords.has(serializedKey) || this.groupedInFlight.has(serializedKey)) {
194
+ invalidated += this.invalidateGroupedRecord(serializedKey);
166
195
  continue;
167
196
  }
168
197
  invalidated += this.invalidateSerializedGraphKey(serializedKey);
@@ -178,9 +207,7 @@ export class SessionPageBrowserGraphCache {
178
207
  if (affectedKeys.has(serializedKey)) {
179
208
  continue;
180
209
  }
181
- this.bumpGraphGeneration(serializedKey);
182
- this.inFlight.delete(serializedKey);
183
- invalidated += 1;
210
+ invalidated += this.invalidateSerializedGraphKey(serializedKey);
184
211
  }
185
212
  if (invalidated > 0) {
186
213
  appLogger.debug(`[PageBrowserGraphSession] invalidated ${invalidated} graph record(s) for ${normalizedFilePath}`);
@@ -211,6 +238,8 @@ export class SessionPageBrowserGraphCache {
211
238
  resetForTests() {
212
239
  this.records.clear();
213
240
  this.inFlight.clear();
241
+ this.inFlightDependencyPaths.clear();
242
+ this.inFlightGenerations.clear();
214
243
  this.groupedRecords.clear();
215
244
  this.groupedInFlight.clear();
216
245
  this.graphGenerations.clear();
@@ -229,17 +258,23 @@ export class SessionPageBrowserGraphCache {
229
258
  this.dependencyIndex.unbindRecord(record);
230
259
  this.records.delete(serializedKey);
231
260
  this.inFlight.delete(serializedKey);
261
+ const inFlightDependencyPaths = this.inFlightDependencyPaths.get(serializedKey);
262
+ if (inFlightDependencyPaths) {
263
+ this.unbindDependencyPaths(serializedKey, inFlightDependencyPaths);
264
+ this.inFlightDependencyPaths.delete(serializedKey);
265
+ }
266
+ this.inFlightGenerations.delete(serializedKey);
232
267
  this.graphGenerations.delete(serializedKey);
233
268
  }
234
269
  if (policy === 'production') {
235
- for (const integrationName of [...this.groupedRecords.keys()]) {
236
- const groupedRecord = this.groupedRecords.get(integrationName);
270
+ for (const cacheKey of [...this.groupedRecords.keys()]) {
271
+ const groupedRecord = this.groupedRecords.get(cacheKey);
237
272
  if (groupedRecord) {
238
- this.dependencyIndex.unbindGroupedRecord(integrationName, groupedRecord);
273
+ this.dependencyIndex.unbindGroupedRecord(cacheKey, groupedRecord);
239
274
  }
240
- this.groupedRecords.delete(integrationName);
241
- this.groupedInFlight.delete(integrationName);
242
- this.groupedGenerations.delete(integrationName);
275
+ this.groupedRecords.delete(cacheKey);
276
+ this.groupedInFlight.delete(cacheKey);
277
+ this.groupedGenerations.delete(cacheKey);
243
278
  }
244
279
  }
245
280
  }
@@ -252,6 +287,9 @@ export class SessionPageBrowserGraphCache {
252
287
  if (normalizeDependencyPath(record.key.routeFile) !== normalizedRoute) {
253
288
  continue;
254
289
  }
290
+ if (record.key.dependencyInstanceKey !== key.dependencyInstanceKey) {
291
+ continue;
292
+ }
255
293
  if (record.key.entryFingerprint === key.entryFingerprint && record.key.policy === key.policy) {
256
294
  continue;
257
295
  }
@@ -263,9 +301,9 @@ export class SessionPageBrowserGraphCache {
263
301
  this.graphGenerations.set(serializedKey, next);
264
302
  return next;
265
303
  }
266
- bumpGroupedGeneration(integrationName) {
267
- const next = (this.groupedGenerations.get(integrationName) ?? 0) + 1;
268
- this.groupedGenerations.set(integrationName, next);
304
+ bumpGroupedGeneration(cacheKey) {
305
+ const next = (this.groupedGenerations.get(cacheKey) ?? 0) + 1;
306
+ this.groupedGenerations.set(cacheKey, next);
269
307
  return next;
270
308
  }
271
309
  invalidateSerializedGraphKey(serializedKey) {
@@ -274,18 +312,24 @@ export class SessionPageBrowserGraphCache {
274
312
  this.dependencyIndex.unbindRecord(record);
275
313
  this.records.delete(serializedKey);
276
314
  }
315
+ const inFlightDependencyPaths = this.inFlightDependencyPaths.get(serializedKey);
316
+ if (inFlightDependencyPaths) {
317
+ this.unbindDependencyPaths(serializedKey, inFlightDependencyPaths);
318
+ this.inFlightDependencyPaths.delete(serializedKey);
319
+ }
320
+ const hadInFlightBuild = this.inFlight.delete(serializedKey);
321
+ this.inFlightGenerations.delete(serializedKey);
277
322
  this.bumpGraphGeneration(serializedKey);
278
- this.inFlight.delete(serializedKey);
279
- return record ? 1 : 0;
323
+ return record || hadInFlightBuild ? 1 : 0;
280
324
  }
281
- invalidateGroupedRecord(integrationName) {
282
- const groupedRecord = this.groupedRecords.get(integrationName);
325
+ invalidateGroupedRecord(cacheKey) {
326
+ const groupedRecord = this.groupedRecords.get(cacheKey);
283
327
  if (groupedRecord) {
284
- this.dependencyIndex.unbindGroupedRecord(integrationName, groupedRecord);
285
- this.groupedRecords.delete(integrationName);
328
+ this.dependencyIndex.unbindGroupedRecord(cacheKey, groupedRecord);
329
+ this.groupedRecords.delete(cacheKey);
286
330
  }
287
- this.bumpGroupedGeneration(integrationName);
288
- this.groupedInFlight.delete(integrationName);
331
+ this.bumpGroupedGeneration(cacheKey);
332
+ this.groupedInFlight.delete(cacheKey);
289
333
  return groupedRecord ? 1 : 0;
290
334
  }
291
335
  async runGraphBuild(serializedKey, key, buildGeneration, build) {
@@ -299,6 +343,11 @@ export class SessionPageBrowserGraphCache {
299
343
  const existing = this.records.get(serializedKey);
300
344
  return existing?.result;
301
345
  }
346
+ if (buildResult.cacheable === false) {
347
+ this.releaseInFlightDependencyPaths(serializedKey, buildGeneration);
348
+ return buildResult.result;
349
+ }
350
+ this.releaseInFlightDependencyPaths(serializedKey, buildGeneration);
302
351
  this.pruneStaleRouteRecords(key);
303
352
  const previous = this.records.get(serializedKey);
304
353
  if (previous) {
@@ -319,10 +368,10 @@ export class SessionPageBrowserGraphCache {
319
368
  return Promise.reject(error);
320
369
  }
321
370
  finally {
322
- this.inFlight.delete(serializedKey);
371
+ this.clearInFlightBuild(serializedKey, buildGeneration);
323
372
  }
324
373
  }
325
- async runGroupedBuild(integrationName, buildGeneration, build) {
374
+ async runGroupedBuild(cacheKey, buildGeneration, build) {
326
375
  try {
327
376
  this.buildCount += 1;
328
377
  const buildResult = await build();
@@ -332,35 +381,60 @@ export class SessionPageBrowserGraphCache {
332
381
  if (isSkipCacheGroupedOutcome(buildResult)) {
333
382
  return buildResult.assetsByRoute;
334
383
  }
335
- if (!this.canCommitGroupedBuild(integrationName, buildGeneration)) {
336
- const existing = this.groupedRecords.get(integrationName);
384
+ if (!this.canCommitGroupedBuild(cacheKey, buildGeneration)) {
385
+ const existing = this.groupedRecords.get(cacheKey);
337
386
  return existing?.assetsByRoute ?? new Map();
338
387
  }
339
- const previous = this.groupedRecords.get(integrationName);
388
+ const previous = this.groupedRecords.get(cacheKey);
340
389
  if (previous) {
341
- this.dependencyIndex.unbindGroupedRecord(integrationName, previous);
390
+ this.dependencyIndex.unbindGroupedRecord(cacheKey, previous);
342
391
  }
343
392
  const record = {
344
393
  assetsByRoute: buildResult.assetsByRoute,
345
394
  dependencyPaths: buildResult.dependencyPaths,
346
395
  generation: buildGeneration,
347
396
  };
348
- this.groupedRecords.set(integrationName, record);
349
- this.dependencyIndex.bindGroupedRecord(integrationName, record);
397
+ this.groupedRecords.set(cacheKey, record);
398
+ this.dependencyIndex.bindGroupedRecord(cacheKey, record);
350
399
  return record.assetsByRoute;
351
400
  }
352
401
  catch (error) {
353
402
  return Promise.reject(error);
354
403
  }
355
404
  finally {
356
- this.groupedInFlight.delete(integrationName);
405
+ this.groupedInFlight.delete(cacheKey);
357
406
  }
358
407
  }
359
408
  canCommitGraphBuild(serializedKey, buildGeneration) {
360
409
  return this.graphGenerations.get(serializedKey) === buildGeneration;
361
410
  }
362
- canCommitGroupedBuild(integrationName, buildGeneration) {
363
- return this.groupedGenerations.get(integrationName) === buildGeneration;
411
+ canCommitGroupedBuild(cacheKey, buildGeneration) {
412
+ return this.groupedGenerations.get(cacheKey) === buildGeneration;
413
+ }
414
+ bindDependencyPaths(serializedKey, dependencyPaths) {
415
+ this.dependencyIndex.bindPaths(serializedKey, dependencyPaths);
416
+ }
417
+ unbindDependencyPaths(serializedKey, dependencyPaths) {
418
+ this.dependencyIndex.unbindPaths(serializedKey, dependencyPaths);
419
+ }
420
+ clearInFlightBuild(serializedKey, buildGeneration) {
421
+ if (this.inFlightGenerations.get(serializedKey) !== buildGeneration) {
422
+ return;
423
+ }
424
+ this.releaseInFlightDependencyPaths(serializedKey, buildGeneration);
425
+ this.inFlight.delete(serializedKey);
426
+ this.inFlightGenerations.delete(serializedKey);
427
+ }
428
+ releaseInFlightDependencyPaths(serializedKey, buildGeneration) {
429
+ if (this.inFlightGenerations.get(serializedKey) !== buildGeneration) {
430
+ return;
431
+ }
432
+ const dependencyPaths = this.inFlightDependencyPaths.get(serializedKey);
433
+ if (!dependencyPaths) {
434
+ return;
435
+ }
436
+ this.unbindDependencyPaths(serializedKey, dependencyPaths);
437
+ this.inFlightDependencyPaths.delete(serializedKey);
364
438
  }
365
439
  }
366
440
  const sessionByAppConfig = new WeakMap();
@@ -424,6 +498,9 @@ export function collectPageBrowserGraphDependencyPaths(routeFile, contribution,
424
498
  dependencyPaths.add(normalizeDependencyPath(bundledSourceFilepath));
425
499
  }
426
500
  }
501
+ for (const watchPath of contribution.watchPaths ?? []) {
502
+ dependencyPaths.add(normalizeDependencyPath(watchPath));
503
+ }
427
504
  return dependencyPaths;
428
505
  }
429
506
  function describeProcessedAsset(asset) {
@@ -2,10 +2,18 @@ import type { EcoPagesAppConfig } from '../../../types/internal-types.js';
2
2
  import type { PageBrowserGraphContribution, PageBrowserGraphResult } from '../../../types/public-types.js';
3
3
  import { type AssetProcessingService } from '../../../services/assets/asset-processing-service/index.js';
4
4
  import { type GraphPolicy } from './page-browser-graph-session.js';
5
+ import type { GroupedGraphBuildPlan } from './grouped-graph-build-plan.js';
6
+ export type GroupedPageBrowserGraphContribution = {
7
+ routeFile: string;
8
+ dependencyInstanceKey: string;
9
+ contribution: PageBrowserGraphContribution | undefined;
10
+ };
5
11
  export type PageBrowserGraphResolveInput = {
6
12
  routeFile: string;
13
+ dependencyInstanceKey?: string;
7
14
  integrationName: string;
8
- collectContribution: (routeFile: string) => Promise<PageBrowserGraphContribution | undefined>;
15
+ collectContribution: () => Promise<PageBrowserGraphContribution | undefined>;
16
+ groupedBuildPlan?: GroupedGraphBuildPlan;
9
17
  policy?: GraphPolicy;
10
18
  };
11
19
  /**
@@ -26,6 +34,5 @@ export declare class PageBrowserGraphService {
26
34
  private materializePageBrowserGraph;
27
35
  private resolveGroupedPageBrowserAssets;
28
36
  private buildGroupedPageBrowserAssets;
29
- private listIntegrationRouteFiles;
30
37
  private partitionPageBrowserGraphAssets;
31
38
  }
@@ -1,9 +1,9 @@
1
1
  import path from 'node:path';
2
- import { fileSystem } from '@ecopages/file-system';
3
2
  import {} from '../../../services/assets/asset-processing-service/index.js';
4
3
  import { appLogger } from '../../../global/app-logger.js';
5
4
  import { startupTrace } from '../../../diagnostics/startup-trace.js';
6
5
  import { collectPageBrowserGraphDependencyPaths, createPageBrowserGraphEntryFingerprint, getAppPageBrowserGraphSession, } from './page-browser-graph-session.js';
6
+ import { createRouteGraphLookupKey } from './route-instance-key.js';
7
7
  function isGroupedContentScriptAsset(asset) {
8
8
  return asset.kind === 'script' && asset.source === 'content' && Boolean(asset.groupedBundle?.id);
9
9
  }
@@ -29,13 +29,14 @@ export class PageBrowserGraphService {
29
29
  async getOrBuild(input) {
30
30
  const policy = input.policy ?? this.resolveGraphPolicy();
31
31
  const session = getAppPageBrowserGraphSession(this.appConfig);
32
+ const dependencyInstanceKey = input.dependencyInstanceKey ?? '';
32
33
  if (!this.isHmrEnabled()) {
33
- const cachedByRoute = session.getGraphByRoute(input.integrationName, input.routeFile, policy);
34
+ const cachedByRoute = session.getGraphByRoute(input.integrationName, input.routeFile, policy, dependencyInstanceKey);
34
35
  if (cachedByRoute) {
35
36
  return cachedByRoute;
36
37
  }
37
38
  }
38
- const contribution = await input.collectContribution(input.routeFile);
39
+ const contribution = await input.collectContribution();
39
40
  if (!contribution) {
40
41
  return undefined;
41
42
  }
@@ -43,9 +44,10 @@ export class PageBrowserGraphService {
43
44
  return await session.resolveGraph({
44
45
  integrationName: input.integrationName,
45
46
  routeFile: input.routeFile,
47
+ dependencyInstanceKey,
46
48
  entryFingerprint,
47
49
  policy,
48
- }, async () => this.buildPageBrowserGraphRecord(input, contribution));
50
+ }, collectPageBrowserGraphDependencyPaths(input.routeFile, contribution, contribution.assets ?? []), async () => this.buildPageBrowserGraphRecord(input, contribution));
49
51
  }
50
52
  resolveGraphPolicy() {
51
53
  return this.isHmrEnabled() ? 'development' : 'production';
@@ -65,10 +67,10 @@ export class PageBrowserGraphService {
65
67
  async materializePageBrowserGraph(input, contribution) {
66
68
  const groupedDependencies = (contribution.dependencies ?? []).filter((dep) => isGroupedContentScriptAsset(dep));
67
69
  const ungroupedDependencies = (contribution.dependencies ?? []).filter((dep) => !isGroupedContentScriptAsset(dep));
68
- const groupedAssets = groupedDependencies.length
69
- ? ((await this.resolveGroupedPageBrowserAssets(input, contribution)).assetsByRoute.get(input.routeFile) ??
70
- [])
71
- : [];
70
+ const groupedResolution = groupedDependencies.length
71
+ ? await this.resolveGroupedPageBrowserAssets(input, contribution)
72
+ : undefined;
73
+ const groupedAssets = groupedResolution?.assetsByRoute.get(createRouteGraphLookupKey(input.routeFile, input.dependencyInstanceKey ?? '')) ?? [];
72
74
  const processedDependencies = ungroupedDependencies.length
73
75
  ? await this.assetProcessingService.processDependencies(ungroupedDependencies, `${input.integrationName}:${input.routeFile}`)
74
76
  : [];
@@ -78,15 +80,29 @@ export class PageBrowserGraphService {
78
80
  return {
79
81
  result,
80
82
  dependencyPaths,
83
+ cacheable: groupedResolution?.cacheable ?? true,
81
84
  };
82
85
  }
83
86
  async resolveGroupedPageBrowserAssets(input, currentContribution) {
84
87
  if (this.isHmrEnabled()) {
85
88
  return await this.buildGroupedPageBrowserAssets(input, currentContribution);
86
89
  }
90
+ if (!input.groupedBuildPlan) {
91
+ const built = await this.buildGroupedPageBrowserAssets(input, currentContribution);
92
+ return {
93
+ ...built,
94
+ cacheable: false,
95
+ };
96
+ }
87
97
  const session = getAppPageBrowserGraphSession(this.appConfig);
88
- const assetsByRoute = await session.resolveGroupedGraph(input.integrationName, async () => {
98
+ const groupedScope = {
99
+ integrationName: input.integrationName,
100
+ planKey: input.groupedBuildPlan.planKey,
101
+ };
102
+ let groupedCacheable = true;
103
+ const assetsByRoute = await session.resolveGroupedGraph(groupedScope, async () => {
89
104
  const built = await this.buildGroupedPageBrowserAssets(input, currentContribution);
105
+ groupedCacheable = !built.hasCollectionFailures;
90
106
  if (built.hasCollectionFailures) {
91
107
  return {
92
108
  skipCache: true,
@@ -103,34 +119,43 @@ export class PageBrowserGraphService {
103
119
  return {
104
120
  assetsByRoute,
105
121
  dependencyPaths: new Set(),
106
- hasCollectionFailures: false,
122
+ hasCollectionFailures: !groupedCacheable,
123
+ cacheable: groupedCacheable,
107
124
  };
108
125
  }
109
126
  async buildGroupedPageBrowserAssets(input, currentContribution) {
110
- const routeFiles = this.isHmrEnabled()
111
- ? [input.routeFile]
112
- : await this.listIntegrationRouteFiles(input.integrationName);
127
+ const dependencyInstanceKey = input.dependencyInstanceKey ?? '';
128
+ const currentRouteLookupKey = createRouteGraphLookupKey(input.routeFile, dependencyInstanceKey);
129
+ const groupedCollection = input.groupedBuildPlan
130
+ ? {
131
+ contributions: input.groupedBuildPlan.instances.map((instance) => ({
132
+ routeFile: instance.routeFile,
133
+ dependencyInstanceKey: instance.dependencyInstanceKey,
134
+ contribution: instance.contribution,
135
+ })),
136
+ hasCollectionFailures: false,
137
+ }
138
+ : {
139
+ contributions: [],
140
+ hasCollectionFailures: false,
141
+ };
142
+ const groupedContributions = groupedCollection.contributions;
113
143
  const currentRouteGroupedDependencies = (currentContribution.dependencies ?? []).filter((dep) => isGroupedContentScriptAsset(dep));
114
144
  const groupedDependencies = [...currentRouteGroupedDependencies];
115
145
  const groupedAssetKeysByRoute = new Map();
146
+ const routeDisplayPaths = new Map();
116
147
  const dependencyPaths = new Set([path.resolve(input.routeFile)]);
117
- let hasCollectionFailures = false;
148
+ let hasCollectionFailures = groupedCollection.hasCollectionFailures;
118
149
  if (currentRouteGroupedDependencies.length > 0) {
119
- groupedAssetKeysByRoute.set(input.routeFile, new Set(currentRouteGroupedDependencies.map((dep) => getGroupedBundleAssetKey(dep.groupedBundle))));
150
+ groupedAssetKeysByRoute.set(currentRouteLookupKey, new Set(currentRouteGroupedDependencies.map((dep) => getGroupedBundleAssetKey(dep.groupedBundle))));
151
+ routeDisplayPaths.set(currentRouteLookupKey, input.routeFile);
120
152
  }
121
- for (const routeFile of routeFiles) {
122
- if (routeFile === input.routeFile) {
123
- continue;
124
- }
125
- let contribution;
126
- try {
127
- contribution = await input.collectContribution(routeFile);
128
- }
129
- catch (error) {
130
- hasCollectionFailures = true;
131
- appLogger.warn(`Skipping grouped page-browser contribution for ${routeFile}: ${error instanceof Error ? error.message : String(error)}`);
153
+ for (const groupedContribution of groupedContributions) {
154
+ if (groupedContribution.routeFile === input.routeFile &&
155
+ groupedContribution.dependencyInstanceKey === dependencyInstanceKey) {
132
156
  continue;
133
157
  }
158
+ const { contribution, routeFile } = groupedContribution;
134
159
  if (!contribution?.dependencies?.length) {
135
160
  continue;
136
161
  }
@@ -139,7 +164,8 @@ export class PageBrowserGraphService {
139
164
  continue;
140
165
  }
141
166
  groupedDependencies.push(...routeGroupedDependencies);
142
- groupedAssetKeysByRoute.set(routeFile, new Set(routeGroupedDependencies.map((dep) => getGroupedBundleAssetKey(dep.groupedBundle))));
167
+ groupedAssetKeysByRoute.set(createRouteGraphLookupKey(routeFile, groupedContribution.dependencyInstanceKey), new Set(routeGroupedDependencies.map((dep) => getGroupedBundleAssetKey(dep.groupedBundle))));
168
+ routeDisplayPaths.set(createRouteGraphLookupKey(routeFile, groupedContribution.dependencyInstanceKey), routeFile);
143
169
  dependencyPaths.add(path.resolve(routeFile));
144
170
  }
145
171
  if (groupedDependencies.length === 0) {
@@ -147,6 +173,7 @@ export class PageBrowserGraphService {
147
173
  assetsByRoute: new Map(),
148
174
  dependencyPaths,
149
175
  hasCollectionFailures,
176
+ cacheable: Boolean(input.groupedBuildPlan) && !hasCollectionFailures,
150
177
  };
151
178
  }
152
179
  for (const dependency of groupedDependencies) {
@@ -164,7 +191,7 @@ export class PageBrowserGraphService {
164
191
  return groupedAssetKeys.has(getGroupedBundleAssetKey(asset.groupedBundle));
165
192
  });
166
193
  if (groupedAssetKeys.size > 0 && matchedAssets.length === 0) {
167
- appLogger.warn(`Grouped page-browser assets for ${routeFile} are missing groupedBundle metadata after processing. Hydration scripts may be omitted from HTML.`);
194
+ appLogger.warn(`Grouped page-browser assets for ${routeDisplayPaths.get(routeFile) ?? routeFile} are missing groupedBundle metadata after processing. Hydration scripts may be omitted from HTML.`);
168
195
  }
169
196
  groupedAssetsByRoute.set(routeFile, matchedAssets);
170
197
  }
@@ -180,19 +207,9 @@ export class PageBrowserGraphService {
180
207
  assetsByRoute: groupedAssetsByRoute,
181
208
  dependencyPaths,
182
209
  hasCollectionFailures,
210
+ cacheable: Boolean(input.groupedBuildPlan) && !hasCollectionFailures,
183
211
  };
184
212
  }
185
- async listIntegrationRouteFiles(integrationName) {
186
- const integration = this.appConfig.integrations.find((plugin) => plugin.name === integrationName);
187
- if (!integration) {
188
- return [];
189
- }
190
- const scannedFiles = await fileSystem.glob(integration.extensions.map((extension) => `**/*${extension}`), { cwd: this.appConfig.absolutePaths.pagesDir });
191
- return scannedFiles
192
- .filter((file) => !file.includes('.ecopages-node.'))
193
- .map((file) => path.join(this.appConfig.absolutePaths.pagesDir, file))
194
- .sort((left, right) => left.localeCompare(right));
195
- }
196
213
  partitionPageBrowserGraphAssets(assets) {
197
214
  const entryAssets = [];
198
215
  const chunkAssets = [];
@@ -0,0 +1,29 @@
1
+ import type { PageParams, PageQuery } from '../../../types/public-types.js';
2
+ export type PageDependencyInstance = {
3
+ params?: PageParams;
4
+ query?: PageQuery;
5
+ };
6
+ export type GroupedGraphScope = {
7
+ integrationName: string;
8
+ planKey: string;
9
+ };
10
+ /**
11
+ * Builds a stable cache key for one concrete dependency resolver input.
12
+ *
13
+ * @remarks
14
+ * The representation deliberately retains scalar versus array values. Delimiter-based
15
+ * encodings would make otherwise distinct route inputs collide.
16
+ */
17
+ export declare function createPageDependencyInstanceKey(input: PageDependencyInstance): string;
18
+ /**
19
+ * Combines a route file with an optional dependency-instance key for grouped graph maps.
20
+ */
21
+ export declare function createRouteGraphLookupKey(routeFile: string, dependencyInstanceKey?: string): string;
22
+ /**
23
+ * Serializes the grouped graph cache scope for one integration and dependency instance.
24
+ */
25
+ export declare function serializeGroupedGraphCacheKey(scope: GroupedGraphScope): string;
26
+ /**
27
+ * Reconstructs dependency resolver inputs from a serialized dependency-instance key.
28
+ */
29
+ export declare function parsePageDependencyInstanceKey(dependencyInstanceKey: string): PageDependencyInstance;