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

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 (125) hide show
  1. package/README.md +1 -0
  2. package/package.json +35 -3
  3. package/src/adapters/bun/server-adapter.js +1 -0
  4. package/src/adapters/node/server-adapter.js +1 -0
  5. package/src/adapters/shared/http/explicit-static-render-preparation.js +1 -1
  6. package/src/adapters/shared/http/explicit-static-route-matcher.js +1 -1
  7. package/src/adapters/shared/http/fs-server-response-matcher.js +15 -7
  8. package/src/adapters/shared/runtime/collect-dev-prewarm-plan.d.ts +11 -0
  9. package/src/adapters/shared/runtime/collect-dev-prewarm-plan.js +29 -0
  10. package/src/adapters/shared/runtime/dev-static-route-prewarm.d.ts +16 -0
  11. package/src/adapters/shared/runtime/dev-static-route-prewarm.js +66 -0
  12. package/src/adapters/shared/runtime/render-context.js +1 -1
  13. package/src/adapters/shared/runtime/server-adapter.d.ts +7 -1
  14. package/src/adapters/shared/runtime/server-adapter.js +64 -5
  15. package/src/build/README.md +1 -1
  16. package/src/build/browser/browser-runtime-plugin.js +2 -5
  17. package/src/build/cache/production-build-cache.d.ts +3 -2
  18. package/src/build/cache/production-build-cache.js +2 -2
  19. package/src/build/contracts/content-virtual-modules.d.ts +12 -0
  20. package/src/build/contracts/content-virtual-modules.js +29 -0
  21. package/src/build/rolldown/rolldown-adapter-helpers.js +1 -1
  22. package/src/build/runtime/build-request-policy.d.ts +3 -4
  23. package/src/build/runtime/build-request-policy.js +4 -4
  24. package/src/cache/index.d.ts +2 -1
  25. package/src/cache/index.js +2 -1
  26. package/src/cache/module-parse-cache.d.ts +5 -0
  27. package/src/cache/module-parse-cache.js +27 -3
  28. package/src/cache/module-transform-profiler.d.ts +8 -0
  29. package/src/cache/module-transform-profiler.js +8 -0
  30. package/src/client/view-transitions.d.ts +17 -1
  31. package/src/client/view-transitions.js +45 -1
  32. package/src/client/view-transitions.test.browser.d.ts +1 -0
  33. package/src/client/view-transitions.test.browser.js +56 -0
  34. package/src/config/config-builder.d.ts +16 -9
  35. package/src/config/config-builder.js +24 -14
  36. package/src/dev/transform-server/dev-transform-vendor-registry.d.ts +1 -0
  37. package/src/dev/transform-server/dev-transform-vendor-registry.js +15 -2
  38. package/src/diagnostics/request-pipeline-metrics.d.ts +38 -0
  39. package/src/diagnostics/request-pipeline-metrics.js +121 -0
  40. package/src/eco/component-identity.d.ts +13 -0
  41. package/src/eco/component-identity.js +18 -0
  42. package/src/eco/eco-declared-component.d.ts +3 -2
  43. package/src/eco/eco-declared-component.js +6 -4
  44. package/src/eco/eco.browser.js +11 -4
  45. package/src/eco/eco.js +15 -8
  46. package/src/eco/eco.types.d.ts +34 -6
  47. package/src/errors/http-error.d.ts +8 -0
  48. package/src/errors/http-error.js +16 -0
  49. package/src/errors/undeclared-component-dependency-error.d.ts +1 -1
  50. package/src/errors/undeclared-component-dependency-error.js +2 -2
  51. package/src/index.browser.d.ts +1 -0
  52. package/src/index.browser.js +1 -0
  53. package/src/index.d.ts +1 -0
  54. package/src/index.js +1 -0
  55. package/src/plugins/README.md +2 -1
  56. package/src/plugins/eco-component-meta-plugin.d.ts +6 -102
  57. package/src/plugins/eco-component-meta-plugin.js +110 -391
  58. package/src/plugins/processor.d.ts +10 -0
  59. package/src/plugins/processor.js +9 -0
  60. package/src/route-renderer/README.md +12 -9
  61. package/src/route-renderer/orchestration/document-shell/document-shell-render.service.js +1 -1
  62. package/src/route-renderer/orchestration/foreign-child/foreign-subtree-execution.service.js +1 -1
  63. package/src/route-renderer/orchestration/integration-renderer.d.ts +19 -11
  64. package/src/route-renderer/orchestration/integration-renderer.js +95 -27
  65. package/src/route-renderer/orchestration/ownership-graph/component-graph-collectors.js +5 -4
  66. package/src/route-renderer/orchestration/ownership-graph/component-graph.js +6 -4
  67. package/src/route-renderer/orchestration/ownership-graph/ownership-validation.service.d.ts +0 -1
  68. package/src/route-renderer/orchestration/ownership-graph/ownership-validation.service.js +10 -19
  69. package/src/route-renderer/orchestration/page-browser-graph/grouped-graph-build-plan.d.ts +15 -0
  70. package/src/route-renderer/orchestration/page-browser-graph/grouped-graph-build-plan.js +9 -0
  71. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph-contribution.merge.d.ts +5 -0
  72. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph-contribution.merge.js +30 -0
  73. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph-session.d.ts +23 -6
  74. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph-session.js +156 -67
  75. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph.service.d.ts +18 -2
  76. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph.service.js +63 -39
  77. package/src/route-renderer/orchestration/page-browser-graph/route-instance-key.d.ts +29 -0
  78. package/src/route-renderer/orchestration/page-browser-graph/route-instance-key.js +54 -0
  79. package/src/route-renderer/orchestration/route-pipeline/integration-route-render-adapter.d.ts +3 -0
  80. package/src/route-renderer/orchestration/route-pipeline/integration-route-render-adapter.js +3 -2
  81. package/src/route-renderer/orchestration/route-pipeline/route-prepared-options.builder.d.ts +1 -0
  82. package/src/route-renderer/orchestration/route-pipeline/route-prepared-options.builder.js +3 -5
  83. package/src/route-renderer/orchestration/route-pipeline/route-render-orchestrator.d.ts +14 -4
  84. package/src/route-renderer/orchestration/route-pipeline/route-render-orchestrator.js +91 -32
  85. package/src/route-renderer/page-loading/component-dependency-collection.js +3 -2
  86. package/src/route-renderer/page-loading/ecopages-virtual-imports.d.ts +0 -5
  87. package/src/route-renderer/page-loading/ecopages-virtual-imports.js +4 -10
  88. package/src/route-renderer/page-loading/file-scoped-dependency-components.d.ts +42 -0
  89. package/src/route-renderer/page-loading/file-scoped-dependency-components.js +94 -0
  90. package/src/route-renderer/page-loading/lazy-trigger-planning.js +2 -1
  91. package/src/route-renderer/page-loading/page-module-loader.d.ts +2 -1
  92. package/src/route-renderer/page-loading/page-module-loader.js +9 -3
  93. package/src/route-renderer/page-loading/resolved-page-dependencies.d.ts +12 -0
  94. package/src/route-renderer/page-loading/resolved-page-dependencies.js +38 -0
  95. package/src/router/README.md +8 -0
  96. package/src/services/README.md +5 -0
  97. package/src/services/assets/asset-processing-service/asset-processing.service.d.ts +1 -0
  98. package/src/services/assets/asset-processing-service/asset-processing.service.js +22 -5
  99. package/src/services/cache/cache.types.d.ts +4 -2
  100. package/src/services/cache/html-page-cache-dependency-index.d.ts +34 -0
  101. package/src/services/cache/html-page-cache-dependency-index.js +88 -0
  102. package/src/services/cache/index.d.ts +2 -0
  103. package/src/services/cache/index.js +2 -0
  104. package/src/services/cache/page-cache-service.d.ts +31 -1
  105. package/src/services/cache/page-cache-service.js +79 -3
  106. package/src/services/cache/page-request-cache-coordinator.service.js +4 -0
  107. package/src/services/module-loading/README.md +9 -6
  108. package/src/services/module-loading/app-server-module-transpiler.service.js +0 -3
  109. package/src/services/module-loading/collection-server-module-build.service.d.ts +19 -0
  110. package/src/services/module-loading/collection-server-module-build.service.js +83 -0
  111. package/src/services/module-loading/page-module-import.service.d.ts +5 -11
  112. package/src/services/module-loading/page-module-import.service.js +56 -73
  113. package/src/services/module-loading/route-module-build-cache.store.js +6 -6
  114. package/src/services/module-loading/route-module-build-manifest.d.ts +8 -5
  115. package/src/services/module-loading/route-module-build-manifest.js +20 -32
  116. package/src/services/module-loading/server-module-transpiler.service.d.ts +0 -2
  117. package/src/services/module-loading/server-module-transpiler.service.js +0 -3
  118. package/src/static-site-generator/production-page-browser-graph-prebuild.d.ts +10 -6
  119. package/src/static-site-generator/production-page-browser-graph-prebuild.js +29 -10
  120. package/src/static-site-generator/static-site-generator.js +6 -3
  121. package/src/types/internal-types.d.ts +4 -0
  122. package/src/types/public-types.d.ts +27 -19
  123. package/src/watchers/project-watcher.js +14 -0
  124. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph-contribution.loader.d.ts +0 -9
  125. 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,33 +113,43 @@ 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) {
124
+ /**
125
+ * Page Browser Graph compilations in this dev-server session.
126
+ */
127
+ getBuildCount() {
128
+ return this.buildCount;
129
+ }
130
+ getGraphByRoute(integrationName, routeFile, policy, dependencyInstanceKey = '') {
95
131
  const normalizedRoute = normalizeDependencyPath(routeFile);
96
132
  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;
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;
105
138
  }
106
- return record.result;
107
139
  }
108
140
  return undefined;
109
141
  }
110
- /**
111
- * Page Browser Graph compilations in this dev-server session.
112
- */
113
- getBuildCount() {
114
- return this.buildCount;
142
+ getDependencyPathsByRoute(integrationName, routeFile, policy, dependencyInstanceKey = '') {
143
+ const normalizedRoute = normalizeDependencyPath(routeFile);
144
+ for (const record of this.records.values()) {
145
+ if (record.key.integrationName === integrationName &&
146
+ record.key.policy === policy &&
147
+ normalizeDependencyPath(record.key.routeFile) === normalizedRoute &&
148
+ record.key.dependencyInstanceKey === dependencyInstanceKey) {
149
+ return record.dependencyPaths;
150
+ }
151
+ }
152
+ return undefined;
115
153
  }
116
154
  getAffectedGraphIdentities(filePath) {
117
155
  const identities = [];
@@ -123,7 +161,7 @@ export class SessionPageBrowserGraphCache {
123
161
  }
124
162
  return identities;
125
163
  }
126
- resolveGraph(key, build) {
164
+ resolveGraph(key, provisionalDependencyPaths, build) {
127
165
  const serializedKey = serializeGraphKey(key);
128
166
  const cached = this.records.get(serializedKey);
129
167
  if (cached) {
@@ -134,35 +172,38 @@ export class SessionPageBrowserGraphCache {
134
172
  return pending;
135
173
  }
136
174
  const buildGeneration = this.bumpGraphGeneration(serializedKey);
175
+ this.inFlightDependencyPaths.set(serializedKey, provisionalDependencyPaths);
176
+ this.inFlightGenerations.set(serializedKey, buildGeneration);
177
+ this.bindDependencyPaths(serializedKey, provisionalDependencyPaths);
137
178
  const buildPromise = this.runGraphBuild(serializedKey, key, buildGeneration, build);
138
179
  this.inFlight.set(serializedKey, buildPromise);
139
180
  return buildPromise;
140
181
  }
141
- resolveGroupedGraph(integrationName, build) {
142
- const cached = this.groupedRecords.get(integrationName);
182
+ resolveGroupedGraph(scope, build) {
183
+ const cacheKey = serializeGroupedGraphCacheKey(scope);
184
+ const cached = this.groupedRecords.get(cacheKey);
143
185
  if (cached) {
144
186
  return Promise.resolve(cached.assetsByRoute);
145
187
  }
146
- const pending = this.groupedInFlight.get(integrationName);
188
+ const pending = this.groupedInFlight.get(cacheKey);
147
189
  if (pending) {
148
190
  return pending;
149
191
  }
150
- const buildGeneration = this.bumpGroupedGeneration(integrationName);
151
- const buildPromise = this.runGroupedBuild(integrationName, buildGeneration, build);
152
- this.groupedInFlight.set(integrationName, buildPromise);
192
+ const buildGeneration = this.bumpGroupedGeneration(cacheKey);
193
+ const buildPromise = this.runGroupedBuild(cacheKey, buildGeneration, build);
194
+ this.groupedInFlight.set(cacheKey, buildPromise);
153
195
  return buildPromise;
154
196
  }
155
- peekGroupedGraph(integrationName) {
156
- return this.groupedRecords.get(integrationName)?.assetsByRoute;
197
+ peekGroupedGraph(scope) {
198
+ return this.groupedRecords.get(serializeGroupedGraphCacheKey(scope))?.assetsByRoute;
157
199
  }
158
200
  invalidateByFilePath(filePath) {
159
201
  const normalizedFilePath = normalizeDependencyPath(filePath);
160
202
  const affectedKeys = this.dependencyIndex.getAffectedKeys(normalizedFilePath);
161
203
  let invalidated = 0;
162
204
  for (const serializedKey of affectedKeys) {
163
- if (serializedKey.startsWith('grouped::')) {
164
- const integrationName = serializedKey.slice('grouped::'.length);
165
- invalidated += this.invalidateGroupedRecord(integrationName);
205
+ if (this.groupedRecords.has(serializedKey) || this.groupedInFlight.has(serializedKey)) {
206
+ invalidated += this.invalidateGroupedRecord(serializedKey);
166
207
  continue;
167
208
  }
168
209
  invalidated += this.invalidateSerializedGraphKey(serializedKey);
@@ -178,9 +219,7 @@ export class SessionPageBrowserGraphCache {
178
219
  if (affectedKeys.has(serializedKey)) {
179
220
  continue;
180
221
  }
181
- this.bumpGraphGeneration(serializedKey);
182
- this.inFlight.delete(serializedKey);
183
- invalidated += 1;
222
+ invalidated += this.invalidateSerializedGraphKey(serializedKey);
184
223
  }
185
224
  if (invalidated > 0) {
186
225
  appLogger.debug(`[PageBrowserGraphSession] invalidated ${invalidated} graph record(s) for ${normalizedFilePath}`);
@@ -211,6 +250,8 @@ export class SessionPageBrowserGraphCache {
211
250
  resetForTests() {
212
251
  this.records.clear();
213
252
  this.inFlight.clear();
253
+ this.inFlightDependencyPaths.clear();
254
+ this.inFlightGenerations.clear();
214
255
  this.groupedRecords.clear();
215
256
  this.groupedInFlight.clear();
216
257
  this.graphGenerations.clear();
@@ -229,17 +270,23 @@ export class SessionPageBrowserGraphCache {
229
270
  this.dependencyIndex.unbindRecord(record);
230
271
  this.records.delete(serializedKey);
231
272
  this.inFlight.delete(serializedKey);
273
+ const inFlightDependencyPaths = this.inFlightDependencyPaths.get(serializedKey);
274
+ if (inFlightDependencyPaths) {
275
+ this.unbindDependencyPaths(serializedKey, inFlightDependencyPaths);
276
+ this.inFlightDependencyPaths.delete(serializedKey);
277
+ }
278
+ this.inFlightGenerations.delete(serializedKey);
232
279
  this.graphGenerations.delete(serializedKey);
233
280
  }
234
281
  if (policy === 'production') {
235
- for (const integrationName of [...this.groupedRecords.keys()]) {
236
- const groupedRecord = this.groupedRecords.get(integrationName);
282
+ for (const cacheKey of [...this.groupedRecords.keys()]) {
283
+ const groupedRecord = this.groupedRecords.get(cacheKey);
237
284
  if (groupedRecord) {
238
- this.dependencyIndex.unbindGroupedRecord(integrationName, groupedRecord);
285
+ this.dependencyIndex.unbindGroupedRecord(cacheKey, groupedRecord);
239
286
  }
240
- this.groupedRecords.delete(integrationName);
241
- this.groupedInFlight.delete(integrationName);
242
- this.groupedGenerations.delete(integrationName);
287
+ this.groupedRecords.delete(cacheKey);
288
+ this.groupedInFlight.delete(cacheKey);
289
+ this.groupedGenerations.delete(cacheKey);
243
290
  }
244
291
  }
245
292
  }
@@ -252,6 +299,9 @@ export class SessionPageBrowserGraphCache {
252
299
  if (normalizeDependencyPath(record.key.routeFile) !== normalizedRoute) {
253
300
  continue;
254
301
  }
302
+ if (record.key.dependencyInstanceKey !== key.dependencyInstanceKey) {
303
+ continue;
304
+ }
255
305
  if (record.key.entryFingerprint === key.entryFingerprint && record.key.policy === key.policy) {
256
306
  continue;
257
307
  }
@@ -263,9 +313,9 @@ export class SessionPageBrowserGraphCache {
263
313
  this.graphGenerations.set(serializedKey, next);
264
314
  return next;
265
315
  }
266
- bumpGroupedGeneration(integrationName) {
267
- const next = (this.groupedGenerations.get(integrationName) ?? 0) + 1;
268
- this.groupedGenerations.set(integrationName, next);
316
+ bumpGroupedGeneration(cacheKey) {
317
+ const next = (this.groupedGenerations.get(cacheKey) ?? 0) + 1;
318
+ this.groupedGenerations.set(cacheKey, next);
269
319
  return next;
270
320
  }
271
321
  invalidateSerializedGraphKey(serializedKey) {
@@ -274,18 +324,24 @@ export class SessionPageBrowserGraphCache {
274
324
  this.dependencyIndex.unbindRecord(record);
275
325
  this.records.delete(serializedKey);
276
326
  }
327
+ const inFlightDependencyPaths = this.inFlightDependencyPaths.get(serializedKey);
328
+ if (inFlightDependencyPaths) {
329
+ this.unbindDependencyPaths(serializedKey, inFlightDependencyPaths);
330
+ this.inFlightDependencyPaths.delete(serializedKey);
331
+ }
332
+ const hadInFlightBuild = this.inFlight.delete(serializedKey);
333
+ this.inFlightGenerations.delete(serializedKey);
277
334
  this.bumpGraphGeneration(serializedKey);
278
- this.inFlight.delete(serializedKey);
279
- return record ? 1 : 0;
335
+ return record || hadInFlightBuild ? 1 : 0;
280
336
  }
281
- invalidateGroupedRecord(integrationName) {
282
- const groupedRecord = this.groupedRecords.get(integrationName);
337
+ invalidateGroupedRecord(cacheKey) {
338
+ const groupedRecord = this.groupedRecords.get(cacheKey);
283
339
  if (groupedRecord) {
284
- this.dependencyIndex.unbindGroupedRecord(integrationName, groupedRecord);
285
- this.groupedRecords.delete(integrationName);
340
+ this.dependencyIndex.unbindGroupedRecord(cacheKey, groupedRecord);
341
+ this.groupedRecords.delete(cacheKey);
286
342
  }
287
- this.bumpGroupedGeneration(integrationName);
288
- this.groupedInFlight.delete(integrationName);
343
+ this.bumpGroupedGeneration(cacheKey);
344
+ this.groupedInFlight.delete(cacheKey);
289
345
  return groupedRecord ? 1 : 0;
290
346
  }
291
347
  async runGraphBuild(serializedKey, key, buildGeneration, build) {
@@ -299,6 +355,11 @@ export class SessionPageBrowserGraphCache {
299
355
  const existing = this.records.get(serializedKey);
300
356
  return existing?.result;
301
357
  }
358
+ if (buildResult.cacheable === false) {
359
+ this.releaseInFlightDependencyPaths(serializedKey, buildGeneration);
360
+ return buildResult.result;
361
+ }
362
+ this.releaseInFlightDependencyPaths(serializedKey, buildGeneration);
302
363
  this.pruneStaleRouteRecords(key);
303
364
  const previous = this.records.get(serializedKey);
304
365
  if (previous) {
@@ -319,10 +380,10 @@ export class SessionPageBrowserGraphCache {
319
380
  return Promise.reject(error);
320
381
  }
321
382
  finally {
322
- this.inFlight.delete(serializedKey);
383
+ this.clearInFlightBuild(serializedKey, buildGeneration);
323
384
  }
324
385
  }
325
- async runGroupedBuild(integrationName, buildGeneration, build) {
386
+ async runGroupedBuild(cacheKey, buildGeneration, build) {
326
387
  try {
327
388
  this.buildCount += 1;
328
389
  const buildResult = await build();
@@ -332,35 +393,60 @@ export class SessionPageBrowserGraphCache {
332
393
  if (isSkipCacheGroupedOutcome(buildResult)) {
333
394
  return buildResult.assetsByRoute;
334
395
  }
335
- if (!this.canCommitGroupedBuild(integrationName, buildGeneration)) {
336
- const existing = this.groupedRecords.get(integrationName);
396
+ if (!this.canCommitGroupedBuild(cacheKey, buildGeneration)) {
397
+ const existing = this.groupedRecords.get(cacheKey);
337
398
  return existing?.assetsByRoute ?? new Map();
338
399
  }
339
- const previous = this.groupedRecords.get(integrationName);
400
+ const previous = this.groupedRecords.get(cacheKey);
340
401
  if (previous) {
341
- this.dependencyIndex.unbindGroupedRecord(integrationName, previous);
402
+ this.dependencyIndex.unbindGroupedRecord(cacheKey, previous);
342
403
  }
343
404
  const record = {
344
405
  assetsByRoute: buildResult.assetsByRoute,
345
406
  dependencyPaths: buildResult.dependencyPaths,
346
407
  generation: buildGeneration,
347
408
  };
348
- this.groupedRecords.set(integrationName, record);
349
- this.dependencyIndex.bindGroupedRecord(integrationName, record);
409
+ this.groupedRecords.set(cacheKey, record);
410
+ this.dependencyIndex.bindGroupedRecord(cacheKey, record);
350
411
  return record.assetsByRoute;
351
412
  }
352
413
  catch (error) {
353
414
  return Promise.reject(error);
354
415
  }
355
416
  finally {
356
- this.groupedInFlight.delete(integrationName);
417
+ this.groupedInFlight.delete(cacheKey);
357
418
  }
358
419
  }
359
420
  canCommitGraphBuild(serializedKey, buildGeneration) {
360
421
  return this.graphGenerations.get(serializedKey) === buildGeneration;
361
422
  }
362
- canCommitGroupedBuild(integrationName, buildGeneration) {
363
- return this.groupedGenerations.get(integrationName) === buildGeneration;
423
+ canCommitGroupedBuild(cacheKey, buildGeneration) {
424
+ return this.groupedGenerations.get(cacheKey) === buildGeneration;
425
+ }
426
+ bindDependencyPaths(serializedKey, dependencyPaths) {
427
+ this.dependencyIndex.bindPaths(serializedKey, dependencyPaths);
428
+ }
429
+ unbindDependencyPaths(serializedKey, dependencyPaths) {
430
+ this.dependencyIndex.unbindPaths(serializedKey, dependencyPaths);
431
+ }
432
+ clearInFlightBuild(serializedKey, buildGeneration) {
433
+ if (this.inFlightGenerations.get(serializedKey) !== buildGeneration) {
434
+ return;
435
+ }
436
+ this.releaseInFlightDependencyPaths(serializedKey, buildGeneration);
437
+ this.inFlight.delete(serializedKey);
438
+ this.inFlightGenerations.delete(serializedKey);
439
+ }
440
+ releaseInFlightDependencyPaths(serializedKey, buildGeneration) {
441
+ if (this.inFlightGenerations.get(serializedKey) !== buildGeneration) {
442
+ return;
443
+ }
444
+ const dependencyPaths = this.inFlightDependencyPaths.get(serializedKey);
445
+ if (!dependencyPaths) {
446
+ return;
447
+ }
448
+ this.unbindDependencyPaths(serializedKey, dependencyPaths);
449
+ this.inFlightDependencyPaths.delete(serializedKey);
364
450
  }
365
451
  }
366
452
  const sessionByAppConfig = new WeakMap();
@@ -424,6 +510,9 @@ export function collectPageBrowserGraphDependencyPaths(routeFile, contribution,
424
510
  dependencyPaths.add(normalizeDependencyPath(bundledSourceFilepath));
425
511
  }
426
512
  }
513
+ for (const watchPath of contribution.watchPaths ?? []) {
514
+ dependencyPaths.add(normalizeDependencyPath(watchPath));
515
+ }
427
516
  return dependencyPaths;
428
517
  }
429
518
  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
  /**
@@ -16,6 +24,15 @@ export declare class PageBrowserGraphService {
16
24
  private readonly assetProcessingService;
17
25
  constructor(appConfig: EcoPagesAppConfig, assetProcessingService: AssetProcessingService);
18
26
  resolvePageBrowserGraph(input: PageBrowserGraphResolveInput): Promise<PageBrowserGraphResult | undefined>;
27
+ /**
28
+ * Returns dependency paths recorded for one resolved Page Browser Graph.
29
+ */
30
+ getDependencyPathsForRoute(input: {
31
+ integrationName: string;
32
+ routeFile: string;
33
+ dependencyInstanceKey?: string;
34
+ policy?: GraphPolicy;
35
+ }): ReadonlySet<string> | undefined;
19
36
  /**
20
37
  * Resolves or builds one Page Browser Graph under the given policy.
21
38
  */
@@ -26,6 +43,5 @@ export declare class PageBrowserGraphService {
26
43
  private materializePageBrowserGraph;
27
44
  private resolveGroupedPageBrowserAssets;
28
45
  private buildGroupedPageBrowserAssets;
29
- private listIntegrationRouteFiles;
30
46
  private partitionPageBrowserGraphAssets;
31
47
  }
@@ -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
  }
@@ -23,19 +23,27 @@ export class PageBrowserGraphService {
23
23
  async resolvePageBrowserGraph(input) {
24
24
  return await this.getOrBuild(input);
25
25
  }
26
+ /**
27
+ * Returns dependency paths recorded for one resolved Page Browser Graph.
28
+ */
29
+ getDependencyPathsForRoute(input) {
30
+ const policy = input.policy ?? this.resolveGraphPolicy();
31
+ return getAppPageBrowserGraphSession(this.appConfig).getDependencyPathsByRoute(input.integrationName, input.routeFile, policy, input.dependencyInstanceKey ?? '');
32
+ }
26
33
  /**
27
34
  * Resolves or builds one Page Browser Graph under the given policy.
28
35
  */
29
36
  async getOrBuild(input) {
30
37
  const policy = input.policy ?? this.resolveGraphPolicy();
31
38
  const session = getAppPageBrowserGraphSession(this.appConfig);
39
+ const dependencyInstanceKey = input.dependencyInstanceKey ?? '';
32
40
  if (!this.isHmrEnabled()) {
33
- const cachedByRoute = session.getGraphByRoute(input.integrationName, input.routeFile, policy);
41
+ const cachedByRoute = session.getGraphByRoute(input.integrationName, input.routeFile, policy, dependencyInstanceKey);
34
42
  if (cachedByRoute) {
35
43
  return cachedByRoute;
36
44
  }
37
45
  }
38
- const contribution = await input.collectContribution(input.routeFile);
46
+ const contribution = await input.collectContribution();
39
47
  if (!contribution) {
40
48
  return undefined;
41
49
  }
@@ -43,9 +51,10 @@ export class PageBrowserGraphService {
43
51
  return await session.resolveGraph({
44
52
  integrationName: input.integrationName,
45
53
  routeFile: input.routeFile,
54
+ dependencyInstanceKey,
46
55
  entryFingerprint,
47
56
  policy,
48
- }, async () => this.buildPageBrowserGraphRecord(input, contribution));
57
+ }, collectPageBrowserGraphDependencyPaths(input.routeFile, contribution, contribution.assets ?? []), async () => this.buildPageBrowserGraphRecord(input, contribution));
49
58
  }
50
59
  resolveGraphPolicy() {
51
60
  return this.isHmrEnabled() ? 'development' : 'production';
@@ -65,10 +74,10 @@ export class PageBrowserGraphService {
65
74
  async materializePageBrowserGraph(input, contribution) {
66
75
  const groupedDependencies = (contribution.dependencies ?? []).filter((dep) => isGroupedContentScriptAsset(dep));
67
76
  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
- : [];
77
+ const groupedResolution = groupedDependencies.length
78
+ ? await this.resolveGroupedPageBrowserAssets(input, contribution)
79
+ : undefined;
80
+ const groupedAssets = groupedResolution?.assetsByRoute.get(createRouteGraphLookupKey(input.routeFile, input.dependencyInstanceKey ?? '')) ?? [];
72
81
  const processedDependencies = ungroupedDependencies.length
73
82
  ? await this.assetProcessingService.processDependencies(ungroupedDependencies, `${input.integrationName}:${input.routeFile}`)
74
83
  : [];
@@ -78,15 +87,29 @@ export class PageBrowserGraphService {
78
87
  return {
79
88
  result,
80
89
  dependencyPaths,
90
+ cacheable: groupedResolution?.cacheable ?? true,
81
91
  };
82
92
  }
83
93
  async resolveGroupedPageBrowserAssets(input, currentContribution) {
84
94
  if (this.isHmrEnabled()) {
85
95
  return await this.buildGroupedPageBrowserAssets(input, currentContribution);
86
96
  }
97
+ if (!input.groupedBuildPlan) {
98
+ const built = await this.buildGroupedPageBrowserAssets(input, currentContribution);
99
+ return {
100
+ ...built,
101
+ cacheable: false,
102
+ };
103
+ }
87
104
  const session = getAppPageBrowserGraphSession(this.appConfig);
88
- const assetsByRoute = await session.resolveGroupedGraph(input.integrationName, async () => {
105
+ const groupedScope = {
106
+ integrationName: input.integrationName,
107
+ planKey: input.groupedBuildPlan.planKey,
108
+ };
109
+ let groupedCacheable = true;
110
+ const assetsByRoute = await session.resolveGroupedGraph(groupedScope, async () => {
89
111
  const built = await this.buildGroupedPageBrowserAssets(input, currentContribution);
112
+ groupedCacheable = !built.hasCollectionFailures;
90
113
  if (built.hasCollectionFailures) {
91
114
  return {
92
115
  skipCache: true,
@@ -103,34 +126,43 @@ export class PageBrowserGraphService {
103
126
  return {
104
127
  assetsByRoute,
105
128
  dependencyPaths: new Set(),
106
- hasCollectionFailures: false,
129
+ hasCollectionFailures: !groupedCacheable,
130
+ cacheable: groupedCacheable,
107
131
  };
108
132
  }
109
133
  async buildGroupedPageBrowserAssets(input, currentContribution) {
110
- const routeFiles = this.isHmrEnabled()
111
- ? [input.routeFile]
112
- : await this.listIntegrationRouteFiles(input.integrationName);
134
+ const dependencyInstanceKey = input.dependencyInstanceKey ?? '';
135
+ const currentRouteLookupKey = createRouteGraphLookupKey(input.routeFile, dependencyInstanceKey);
136
+ const groupedCollection = input.groupedBuildPlan
137
+ ? {
138
+ contributions: input.groupedBuildPlan.instances.map((instance) => ({
139
+ routeFile: instance.routeFile,
140
+ dependencyInstanceKey: instance.dependencyInstanceKey,
141
+ contribution: instance.contribution,
142
+ })),
143
+ hasCollectionFailures: false,
144
+ }
145
+ : {
146
+ contributions: [],
147
+ hasCollectionFailures: false,
148
+ };
149
+ const groupedContributions = groupedCollection.contributions;
113
150
  const currentRouteGroupedDependencies = (currentContribution.dependencies ?? []).filter((dep) => isGroupedContentScriptAsset(dep));
114
151
  const groupedDependencies = [...currentRouteGroupedDependencies];
115
152
  const groupedAssetKeysByRoute = new Map();
153
+ const routeDisplayPaths = new Map();
116
154
  const dependencyPaths = new Set([path.resolve(input.routeFile)]);
117
- let hasCollectionFailures = false;
155
+ let hasCollectionFailures = groupedCollection.hasCollectionFailures;
118
156
  if (currentRouteGroupedDependencies.length > 0) {
119
- groupedAssetKeysByRoute.set(input.routeFile, new Set(currentRouteGroupedDependencies.map((dep) => getGroupedBundleAssetKey(dep.groupedBundle))));
157
+ groupedAssetKeysByRoute.set(currentRouteLookupKey, new Set(currentRouteGroupedDependencies.map((dep) => getGroupedBundleAssetKey(dep.groupedBundle))));
158
+ routeDisplayPaths.set(currentRouteLookupKey, input.routeFile);
120
159
  }
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)}`);
160
+ for (const groupedContribution of groupedContributions) {
161
+ if (groupedContribution.routeFile === input.routeFile &&
162
+ groupedContribution.dependencyInstanceKey === dependencyInstanceKey) {
132
163
  continue;
133
164
  }
165
+ const { contribution, routeFile } = groupedContribution;
134
166
  if (!contribution?.dependencies?.length) {
135
167
  continue;
136
168
  }
@@ -139,7 +171,8 @@ export class PageBrowserGraphService {
139
171
  continue;
140
172
  }
141
173
  groupedDependencies.push(...routeGroupedDependencies);
142
- groupedAssetKeysByRoute.set(routeFile, new Set(routeGroupedDependencies.map((dep) => getGroupedBundleAssetKey(dep.groupedBundle))));
174
+ groupedAssetKeysByRoute.set(createRouteGraphLookupKey(routeFile, groupedContribution.dependencyInstanceKey), new Set(routeGroupedDependencies.map((dep) => getGroupedBundleAssetKey(dep.groupedBundle))));
175
+ routeDisplayPaths.set(createRouteGraphLookupKey(routeFile, groupedContribution.dependencyInstanceKey), routeFile);
143
176
  dependencyPaths.add(path.resolve(routeFile));
144
177
  }
145
178
  if (groupedDependencies.length === 0) {
@@ -147,6 +180,7 @@ export class PageBrowserGraphService {
147
180
  assetsByRoute: new Map(),
148
181
  dependencyPaths,
149
182
  hasCollectionFailures,
183
+ cacheable: Boolean(input.groupedBuildPlan) && !hasCollectionFailures,
150
184
  };
151
185
  }
152
186
  for (const dependency of groupedDependencies) {
@@ -164,7 +198,7 @@ export class PageBrowserGraphService {
164
198
  return groupedAssetKeys.has(getGroupedBundleAssetKey(asset.groupedBundle));
165
199
  });
166
200
  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.`);
201
+ 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
202
  }
169
203
  groupedAssetsByRoute.set(routeFile, matchedAssets);
170
204
  }
@@ -180,19 +214,9 @@ export class PageBrowserGraphService {
180
214
  assetsByRoute: groupedAssetsByRoute,
181
215
  dependencyPaths,
182
216
  hasCollectionFailures,
217
+ cacheable: Boolean(input.groupedBuildPlan) && !hasCollectionFailures,
183
218
  };
184
219
  }
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
220
  partitionPageBrowserGraphAssets(assets) {
197
221
  const entryAssets = [];
198
222
  const chunkAssets = [];