@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
@@ -4,7 +4,7 @@
4
4
  * @module
5
5
  */
6
6
  import type { EcoPagesAppConfig, IHmrManager } from '../../types/internal-types.js';
7
- import type { ComponentRenderInput, ComponentRenderResult, EcoComponent, EcoPageFile, EcoPagesElement, HtmlTemplateProps, IntegrationRendererRenderOptions, PageBrowserGraphContribution, PageBrowserGraphContributionContext, PageBrowserGraphResult, PageMetadataProps, RouteRendererBody, RouteRendererOptions, RouteRenderResult } from '../../types/public-types.js';
7
+ import type { ComponentRenderInput, ComponentRenderResult, EcoComponent, EcoComponentDependencies, EcoPageFile, EcoPagesElement, HtmlTemplateProps, IntegrationRendererRenderOptions, PageBrowserGraphContribution, PageBrowserGraphContributionContext, PageBrowserGraphResult, PageMetadataProps, RouteRendererBody, RouteRendererOptions, RouteRenderResult } from '../../types/public-types.js';
8
8
  import { type AssetProcessingService, type ProcessedAsset } from '../../services/assets/asset-processing-service/index.js';
9
9
  import { HtmlTransformerService } from '../../services/html/html-transformer.service.js';
10
10
  import type { HtmlDocumentContribution } from '../../services/html/html-transformer.service.js';
@@ -12,18 +12,15 @@ import { HttpError } from '../../errors/http-error.js';
12
12
  import { DependencyResolverService } from '../page-loading/dependency-resolver.js';
13
13
  import { PageModuleLoaderService } from '../page-loading/page-module-loader.js';
14
14
  import { RouteRenderOrchestrator, type RouteRenderOrchestratorAdapter, type RouteRenderOrchestratorResolvedInputs } from './route-pipeline/route-render-orchestrator.js';
15
+ import { type ResolvedPageDependencies } from '../page-loading/resolved-page-dependencies.js';
16
+ import { type GroupedGraphBuildPlan } from './page-browser-graph/grouped-graph-build-plan.js';
15
17
  import type { ForeignChildRuntime } from './foreign-child/component-render-context.js';
16
18
  import { ForeignSubtreeExecutionService } from './foreign-child/foreign-subtree-execution.service.js';
17
19
  /**
18
20
  * Controls how one route module is loaded outside the normal render path.
19
- *
20
- * Request-time metadata inspection and static-generation probes use these
21
- * options to isolate their module identity from the main render cache while
22
- * still going through the owning integration's import setup.
23
21
  */
24
22
  export type RouteModuleLoadOptions = {
25
23
  bypassCache?: boolean;
26
- cacheScope?: string;
27
24
  };
28
25
  /**
29
26
  * Context for renderToResponse method.
@@ -72,9 +69,19 @@ export declare abstract class IntegrationRenderer<C = EcoPagesElement> {
72
69
  */
73
70
  protected ensureIntegrationRuntimeActivated(): Promise<void>;
74
71
  /**
75
- * Prebuilds the production Page Browser Graph for one route file.
72
+ * Prebuilds the production Page Browser Graph for one route file and optional params.
76
73
  */
77
- prebuildProductionPageBrowserGraph(routeFile: string): Promise<void>;
74
+ prebuildProductionPageBrowserGraph(routeFile: string, options?: Pick<RouteRendererOptions, 'params' | 'query'> & {
75
+ groupedBuildPlan?: GroupedGraphBuildPlan;
76
+ }): Promise<void>;
77
+ /**
78
+ * Builds one production grouped graph plan for the supplied static route instances.
79
+ */
80
+ buildGroupedGraphBuildPlan(instances: ReadonlyArray<{
81
+ routeFile: string;
82
+ params?: RouteRendererOptions['params'];
83
+ query?: RouteRendererOptions['query'];
84
+ }>): Promise<GroupedGraphBuildPlan>;
78
85
  /**
79
86
  * Loads one route module through the owning renderer's import path.
80
87
  *
@@ -118,6 +125,7 @@ export declare abstract class IntegrationRenderer<C = EcoPagesElement> {
118
125
  */
119
126
  protected prepareViewDependencies(view: EcoComponent, layout?: EcoComponent): Promise<ProcessedAsset[]>;
120
127
  protected resolvePageBrowserGraphForFile(filePath: string): Promise<PageBrowserGraphResult | undefined>;
128
+ protected resolvePageBrowserGraphForRoute(filePath: string, routeOptions?: Pick<RouteRendererOptions, 'params' | 'query'>, groupedBuildPlan?: GroupedGraphBuildPlan): Promise<PageBrowserGraphResult | undefined>;
121
129
  /**
122
130
  * Merges component-scoped assets into the active HTML transformer state.
123
131
  *
@@ -226,12 +234,9 @@ export declare abstract class IntegrationRenderer<C = EcoPagesElement> {
226
234
  * @returns The HTML template component.
227
235
  */
228
236
  protected getHtmlTemplate(): Promise<EcoComponent<HtmlTemplateProps>>;
229
- protected usesIntegrationPageImporter(_file: string): boolean;
230
- protected importIntegrationPageFile(_file: string, _options?: RouteModuleLoadOptions): Promise<EcoPageFile>;
231
237
  protected normalizeImportedPageFile<TPageModule extends EcoPageFile>(_file: string, pageModule: TPageModule): TPageModule;
232
238
  /**
233
239
  * Imports the page file from the specified path.
234
- * It uses dynamic import to load the file and returns the imported module.
235
240
  *
236
241
  * @param file - The file path to import.
237
242
  * @returns The imported module.
@@ -270,6 +275,9 @@ export declare abstract class IntegrationRenderer<C = EcoPagesElement> {
270
275
  */
271
276
  protected createRouteRenderOrchestratorAdapter(): RouteRenderOrchestratorAdapter<C>;
272
277
  protected resolveRouteRenderInputs(routeOptions: RouteRendererOptions): Promise<RouteRenderOrchestratorResolvedInputs>;
278
+ protected buildPageBrowserGraphContributionContext(routeFile: string, routeOptions?: Pick<RouteRendererOptions, 'params' | 'query'>): Promise<PageBrowserGraphContributionContext>;
279
+ protected resolvePageDependencies(context: PageBrowserGraphContributionContext): Promise<ResolvedPageDependencies | undefined>;
280
+ protected resolvePageBrowserGraphContributionFromDependencies(dependencies: EcoComponentDependencies, ownerFile: string): Promise<PageBrowserGraphContribution | undefined>;
273
281
  protected resolveRouteDependencies(input: {
274
282
  components: (EcoComponent | Partial<EcoComponent>)[];
275
283
  }): Promise<{
@@ -13,7 +13,11 @@ import { OwnershipValidationService } from './ownership-graph/ownership-validati
13
13
  import { hasForeignChildDescendantsInGraph } from './ownership-graph/component-graph-collectors.js';
14
14
  import { RouteRenderOrchestrator, } from './route-pipeline/route-render-orchestrator.js';
15
15
  import { createIntegrationRouteRenderAdapter } from './route-pipeline/integration-route-render-adapter.js';
16
- import { loadPageBrowserGraphContribution } from './page-browser-graph/page-browser-graph-contribution.loader.js';
16
+ import { createPageDependencyInstanceKey } from './page-browser-graph/route-instance-key.js';
17
+ import { mergePageBrowserGraphContributions } from './page-browser-graph/page-browser-graph-contribution.merge.js';
18
+ import { collectDependencyWatchPaths, collectFileScopedDependencyComponents, } from '../page-loading/file-scoped-dependency-components.js';
19
+ import { resolvePageDependenciesFromContext, } from '../page-loading/resolved-page-dependencies.js';
20
+ import { createGroupedGraphBuildPlanKey, } from './page-browser-graph/grouped-graph-build-plan.js';
17
21
  import { normalizeUnresolvedMarkerArtifactHtml } from './route-pipeline/marker-artifact.utils.js';
18
22
  import { isMarkupNodeLike } from './foreign-child/foreign-child-output.utils.js';
19
23
  import { getForeignSubtreeResolutionContextKey, getForeignSubtreeTokenPrefix, resolveOwningIntegrationRenderer, } from './foreign-child/owning-renderer-resolution.js';
@@ -66,11 +70,30 @@ export class IntegrationRenderer {
66
70
  });
67
71
  }
68
72
  /**
69
- * Prebuilds the production Page Browser Graph for one route file.
73
+ * Prebuilds the production Page Browser Graph for one route file and optional params.
70
74
  */
71
- async prebuildProductionPageBrowserGraph(routeFile) {
75
+ async prebuildProductionPageBrowserGraph(routeFile, options) {
72
76
  await this.ensureIntegrationRuntimeActivated();
73
- await this.resolvePageBrowserGraphForFile(routeFile);
77
+ await this.resolvePageBrowserGraphForRoute(routeFile, options, options?.groupedBuildPlan);
78
+ }
79
+ /**
80
+ * Builds one production grouped graph plan for the supplied static route instances.
81
+ */
82
+ async buildGroupedGraphBuildPlan(instances) {
83
+ const builtInstances = await Promise.all(instances.map(async (routeInstance) => {
84
+ const context = await this.buildPageBrowserGraphContributionContext(routeInstance.routeFile, routeInstance);
85
+ const resolvedPageDependencies = await this.resolvePageDependencies(context);
86
+ return {
87
+ routeFile: routeInstance.routeFile,
88
+ dependencyInstanceKey: context.dependencyInstanceKey ?? '',
89
+ contribution: mergePageBrowserGraphContributions(await this.collectPageBrowserGraphContribution(context), resolvedPageDependencies?.contribution),
90
+ };
91
+ }));
92
+ return {
93
+ integrationName: this.name,
94
+ planKey: createGroupedGraphBuildPlanKey(this.name, builtInstances),
95
+ instances: builtInstances,
96
+ };
74
97
  }
75
98
  /**
76
99
  * Loads one route module through the owning renderer's import path.
@@ -173,10 +196,23 @@ export class IntegrationRenderer {
173
196
  return resolvedDependencies;
174
197
  }
175
198
  async resolvePageBrowserGraphForFile(filePath) {
199
+ /**
200
+ * @remarks
201
+ * Integration renderers such as React still call this file-only entry point
202
+ * during explicit view rendering. Route-instance-aware callers should prefer
203
+ * `resolvePageBrowserGraphForRoute`.
204
+ */
205
+ return this.resolvePageBrowserGraphForRoute(filePath);
206
+ }
207
+ async resolvePageBrowserGraphForRoute(filePath, routeOptions, groupedBuildPlan) {
208
+ const graphContext = await this.buildPageBrowserGraphContributionContext(filePath, routeOptions);
209
+ const resolvedPageDependencies = await this.resolvePageDependencies(graphContext);
176
210
  return await this.routeRenderOrchestrator.resolveDeclaredPageBrowserGraph({
177
211
  routeFile: filePath,
212
+ dependencyInstanceKey: graphContext.dependencyInstanceKey,
178
213
  integrationName: this.name,
179
- collectContribution: (routeFile) => loadPageBrowserGraphContribution(routeFile, (targetFile) => this.importPageFile(targetFile), (context) => this.collectPageBrowserGraphContribution(context)),
214
+ groupedBuildPlan,
215
+ collectContribution: async () => mergePageBrowserGraphContributions(await this.collectPageBrowserGraphContribution(graphContext), resolvedPageDependencies?.contribution),
180
216
  });
181
217
  }
182
218
  /**
@@ -330,14 +366,14 @@ export class IntegrationRenderer {
330
366
  ? input.children.outerHTML
331
367
  : undefined;
332
368
  if (input.children !== undefined && serializedChildren === undefined) {
333
- const componentFile = input.component.config?.__eco?.file ?? 'unknown component';
369
+ const componentFile = input.component.config?.identity?.file ?? 'unknown component';
334
370
  const childTag = Object.prototype.toString.call(input.children);
335
371
  throw new TypeError(`[ecopages] ${this.name} renderer expected serialized children for ${componentFile}, received ${childTag}.`);
336
372
  }
337
373
  const props = serializedChildren === undefined ? input.props : { ...input.props, children: serializedChildren };
338
374
  const content = await component(props);
339
375
  if (typeof content !== 'string') {
340
- const componentFile = input.component.config?.__eco?.file ?? 'unknown component';
376
+ const componentFile = input.component.config?.identity?.file ?? 'unknown component';
341
377
  const contentTag = Object.prototype.toString.call(content);
342
378
  throw new TypeError(`[ecopages] ${this.name} renderer expected a string render result for ${componentFile}, received ${contentTag}.`);
343
379
  }
@@ -417,33 +453,19 @@ export class IntegrationRenderer {
417
453
  invariant(false, `Error importing HtmlTemplate: ${error}`);
418
454
  }
419
455
  }
420
- usesIntegrationPageImporter(_file) {
421
- return false;
422
- }
423
- async importIntegrationPageFile(_file, _options) {
424
- invariant(false, 'Integration page importer must be implemented when enabled');
425
- }
426
456
  normalizeImportedPageFile(_file, pageModule) {
427
457
  return pageModule;
428
458
  }
429
459
  /**
430
460
  * Imports the page file from the specified path.
431
- * It uses dynamic import to load the file and returns the imported module.
432
461
  *
433
462
  * @param file - The file path to import.
434
463
  * @returns The imported module.
435
464
  */
436
465
  async importPageFile(file, options) {
437
- const bypassCache = options?.bypassCache ?? false;
438
- const pageModule = this.usesIntegrationPageImporter(file)
439
- ? await this.importIntegrationPageFile(file, {
440
- bypassCache,
441
- cacheScope: options?.cacheScope,
442
- })
443
- : await this.pageModuleLoaderService.importPageFile(file, {
444
- bypassCache,
445
- cacheScope: options?.cacheScope,
446
- });
466
+ const pageModule = await this.pageModuleLoaderService.importPageFile(file, {
467
+ bypassCache: options?.bypassCache,
468
+ });
447
469
  return this.normalizeImportedPageFile(file, pageModule);
448
470
  }
449
471
  /**
@@ -494,6 +516,8 @@ export class IntegrationRenderer {
494
516
  resolveRouteDependencies: (input) => this.resolveRouteDependencies(input),
495
517
  importPageFile: (file) => this.importPageFile(file),
496
518
  collectPageBrowserGraphContribution: (context) => this.collectPageBrowserGraphContribution(context),
519
+ resolvePageDependencies: (context) => this.resolvePageDependencies(context),
520
+ buildPageBrowserGraphContributionContext: (routeFile, routeOptions) => this.buildPageBrowserGraphContributionContext(routeFile, routeOptions),
497
521
  renderRouteBody: (renderOptions) => this.renderRouteBody(renderOptions),
498
522
  getDocumentAttributes: (renderOptions) => this.getDocumentAttributes(renderOptions),
499
523
  getHtmlDocumentContributions: (options) => this.getHtmlDocumentContributions(options),
@@ -506,20 +530,22 @@ export class IntegrationRenderer {
506
530
  });
507
531
  }
508
532
  async resolveRouteRenderInputs(routeOptions) {
509
- const pageModule = await this.pageModuleLoaderService.resolvePageModule({
533
+ const resolvedPageModule = await this.pageModuleLoaderService.resolvePageModule({
510
534
  file: routeOptions.file,
535
+ pageModule: routeOptions.pageModule,
511
536
  importPageFileFn: (targetFile) => this.importPageFile(targetFile),
512
537
  });
513
- const { Page, integrationSpecificProps } = pageModule;
538
+ const { Page, integrationSpecificProps, module: pageModule } = resolvedPageModule;
514
539
  const HtmlTemplate = await this.getHtmlTemplate();
515
540
  const Layouts = resolvePageLayoutComponents(Page.config?.layouts);
516
541
  const Layout = resolveInnermostPageLayout(Layouts);
517
542
  const { props, metadata } = await this.pageModuleLoaderService.resolvePageData({
518
- pageModule,
543
+ pageModule: resolvedPageModule,
519
544
  routeOptions,
520
545
  });
521
546
  return {
522
547
  Page,
548
+ pageModule,
523
549
  HtmlTemplate: HtmlTemplate,
524
550
  Layouts,
525
551
  Layout,
@@ -529,6 +555,48 @@ export class IntegrationRenderer {
529
555
  integrationSpecificProps,
530
556
  };
531
557
  }
558
+ async buildPageBrowserGraphContributionContext(routeFile, routeOptions) {
559
+ const { module: pageModule, ...resolvedPageModule } = await this.pageModuleLoaderService.resolvePageModule({
560
+ file: routeFile,
561
+ importPageFileFn: (targetFile) => this.importPageFile(targetFile),
562
+ });
563
+ const { props } = await this.pageModuleLoaderService.resolvePageData({
564
+ pageModule: resolvedPageModule,
565
+ routeOptions: {
566
+ file: routeFile,
567
+ params: routeOptions?.params,
568
+ query: routeOptions?.query,
569
+ },
570
+ });
571
+ return {
572
+ file: routeFile,
573
+ pageModule,
574
+ props,
575
+ params: routeOptions?.params,
576
+ query: routeOptions?.query,
577
+ dependencyInstanceKey: createPageDependencyInstanceKey({
578
+ params: routeOptions?.params,
579
+ query: routeOptions?.query,
580
+ }),
581
+ };
582
+ }
583
+ async resolvePageDependencies(context) {
584
+ return resolvePageDependenciesFromContext(context, this.name, (dependencies, ownerFile) => this.resolvePageBrowserGraphContributionFromDependencies(dependencies, ownerFile));
585
+ }
586
+ async resolvePageBrowserGraphContributionFromDependencies(dependencies, ownerFile) {
587
+ const components = collectFileScopedDependencyComponents({
588
+ ownerFile,
589
+ integrationName: this.name,
590
+ dependencies,
591
+ });
592
+ if (components.length === 0) {
593
+ return undefined;
594
+ }
595
+ return {
596
+ assets: await this.processComponentDependencies(components),
597
+ watchPaths: collectDependencyWatchPaths(ownerFile, components),
598
+ };
599
+ }
532
600
  async resolveRouteDependencies(input) {
533
601
  return {
534
602
  resolvedDependencies: await this.resolveDependencies(input.components),
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { AssetFactory } from '../../../services/assets/asset-processing-service/index.js';
3
3
  import { walkComponentGraph } from './component-graph.js';
4
+ import { getComponentIdentity } from '../../../eco/component-identity.js';
4
5
  function toGraphRoots(components) {
5
6
  return components.filter(Boolean).map((component) => ({ component }));
6
7
  }
@@ -10,7 +11,7 @@ export function collectIntegrationNamesFromGraph(components, currentIntegrationN
10
11
  roots: toGraphRoots(components),
11
12
  currentIntegrationName,
12
13
  onComponent: ({ component }) => {
13
- const integrationName = component.config?.integration ?? component.config?.__eco?.integration;
14
+ const integrationName = getComponentIdentity(component)?.integration ?? component.config?.integration;
14
15
  if (integrationName) {
15
16
  integrationNames.add(integrationName);
16
17
  }
@@ -41,7 +42,7 @@ export function hasForeignChildDescendantsInGraph(component, currentIntegrationN
41
42
  if (foundForeign) {
42
43
  return false;
43
44
  }
44
- const integrationName = currentComponent.config?.integration ?? currentComponent.config?.__eco?.integration;
45
+ const integrationName = getComponentIdentity(currentComponent)?.integration ?? currentComponent.config?.integration;
45
46
  if (integrationName && integrationName !== currentIntegrationName) {
46
47
  foundForeign = true;
47
48
  return false;
@@ -63,8 +64,8 @@ export function collectEagerSsrLazyScriptDefinitions(components) {
63
64
  currentIntegrationName: '',
64
65
  visitLayout: true,
65
66
  onConfig: (config) => {
66
- const componentFile = config?.__eco?.file;
67
- if (!componentFile) {
67
+ const componentFile = getComponentIdentity(config)?.file;
68
+ if (!componentFile || !config) {
68
69
  return;
69
70
  }
70
71
  const componentDir = path.dirname(componentFile);
@@ -1,3 +1,4 @@
1
+ import { getComponentIdentity } from '../../../eco/component-identity.js';
1
2
  export function walkComponentGraph(input) {
2
3
  const seenComponents = new Set();
3
4
  const seenConfigs = new Set();
@@ -10,7 +11,8 @@ export function walkComponentGraph(input) {
10
11
  return;
11
12
  }
12
13
  seenComponents.add(ecoComponent);
13
- const integrationName = ecoComponent.config?.integration ?? ecoComponent.config?.__eco?.integration ?? parentIntegrationName;
14
+ const identity = getComponentIdentity(ecoComponent);
15
+ const integrationName = ecoComponent.config?.integration ?? identity?.integration ?? parentIntegrationName;
14
16
  if (input.onConfig && ecoComponent.config && !seenConfigs.has(ecoComponent.config)) {
15
17
  seenConfigs.add(ecoComponent.config);
16
18
  input.onConfig(ecoComponent.config);
@@ -62,10 +64,10 @@ export function walkComponentGraph(input) {
62
64
  export function mapComponentGraph(input) {
63
65
  let nextSyntheticId = 0;
64
66
  const mapComponent = (component, source, parentIntegrationName, lineage) => {
65
- const integrationName = component.config?.integration ?? component.config?.__eco?.integration ?? parentIntegrationName;
66
- const componentMeta = component.config?.__eco;
67
+ const identity = getComponentIdentity(component);
68
+ const integrationName = component.config?.integration ?? identity?.integration ?? parentIntegrationName;
67
69
  const isForeignToParent = integrationName !== parentIntegrationName;
68
- const componentId = componentMeta?.id ?? componentMeta?.file ?? `${source}:${(nextSyntheticId += 1)}`;
70
+ const componentId = identity?.id ?? identity?.file ?? `${source}:${(nextSyntheticId += 1)}`;
69
71
  const nextLineage = new Set(lineage);
70
72
  nextLineage.add(component);
71
73
  const children = (component.config?.dependencies?.components ?? []).flatMap((child) => {
@@ -24,7 +24,6 @@ export declare class OwnershipValidationService {
24
24
  * Validates foreign ownership edges reachable from the supplied route roots.
25
25
  */
26
26
  validate(input: OwnershipValidationInput): OwnershipValidationError[];
27
- private assertDeclaredComponentDependencies;
28
27
  private isRegisteredIntegration;
29
28
  }
30
29
  /**
@@ -1,5 +1,6 @@
1
1
  import { assertEcoDeclaredComponent } from '../../../eco/eco-declared-component.js';
2
- import { mapComponentGraph, walkComponentGraph } from './component-graph.js';
2
+ import { getComponentIdentity } from '../../../eco/component-identity.js';
3
+ import { mapComponentGraph } from './component-graph.js';
3
4
  /**
4
5
  * Validates foreign ownership as soon as the route root graph is loaded.
5
6
  *
@@ -19,12 +20,17 @@ export class OwnershipValidationService {
19
20
  * Validates foreign ownership edges reachable from the supplied route roots.
20
21
  */
21
22
  validate(input) {
22
- this.assertDeclaredComponentDependencies(input);
23
23
  return mapComponentGraph({
24
24
  roots: input.roots,
25
25
  currentIntegrationName: input.currentIntegrationName,
26
26
  mapNode: ({ component, integrationName, componentId, isForeignToParent }, children) => {
27
- const componentMeta = component.config?.__eco;
27
+ const parentFile = getComponentIdentity(component)?.file;
28
+ for (const child of component.config?.dependencies?.components ?? []) {
29
+ if (child) {
30
+ assertEcoDeclaredComponent(child, { parentComponentFile: parentFile });
31
+ }
32
+ }
33
+ const componentMeta = getComponentIdentity(component);
28
34
  const errors = children.flat();
29
35
  if (!isForeignToParent) {
30
36
  return errors;
@@ -35,7 +41,7 @@ export class OwnershipValidationService {
35
41
  if (!componentMeta) {
36
42
  errors.push({
37
43
  code: 'MISSING_COMPONENT_METADATA',
38
- message: `[ecopages] Foreign child "${componentId}" must provide stable __eco metadata so ownership diagnostics stay actionable. Declared dependencies must include all possible foreign children.`,
44
+ message: `[ecopages] Foreign child "${componentId}" must provide stable component identity so ownership diagnostics stay actionable. Declared dependencies must include all possible foreign children.`,
39
45
  componentId,
40
46
  integrationName,
41
47
  });
@@ -53,21 +59,6 @@ export class OwnershipValidationService {
53
59
  },
54
60
  }).flat();
55
61
  }
56
- assertDeclaredComponentDependencies(input) {
57
- walkComponentGraph({
58
- roots: input.roots,
59
- currentIntegrationName: input.currentIntegrationName,
60
- onComponent: ({ component }) => {
61
- const parentFile = component.config?.__eco?.file;
62
- for (const child of component.config?.dependencies?.components ?? []) {
63
- if (!child) {
64
- continue;
65
- }
66
- assertEcoDeclaredComponent(child, { parentComponentFile: parentFile });
67
- }
68
- },
69
- });
70
- }
71
62
  isRegisteredIntegration(integrationName, currentIntegrationName) {
72
63
  if (integrationName === currentIntegrationName) {
73
64
  return true;
@@ -0,0 +1,15 @@
1
+ import type { PageBrowserGraphContribution } from '../../../types/public-types.js';
2
+ export type GroupedGraphBuildPlanInstance = {
3
+ routeFile: string;
4
+ dependencyInstanceKey: string;
5
+ contribution?: PageBrowserGraphContribution;
6
+ };
7
+ export type GroupedGraphBuildPlan = {
8
+ integrationName: string;
9
+ planKey: string;
10
+ instances: ReadonlyArray<GroupedGraphBuildPlanInstance>;
11
+ };
12
+ /**
13
+ * Builds a stable grouped-cache identity for one integration static export plan.
14
+ */
15
+ export declare function createGroupedGraphBuildPlanKey(integrationName: string, instances: ReadonlyArray<Pick<GroupedGraphBuildPlanInstance, 'routeFile' | 'dependencyInstanceKey'>>): string;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Builds a stable grouped-cache identity for one integration static export plan.
3
+ */
4
+ export function createGroupedGraphBuildPlanKey(integrationName, instances) {
5
+ const canonicalInstances = [...instances]
6
+ .map((instance) => [instance.routeFile, instance.dependencyInstanceKey])
7
+ .sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
8
+ return JSON.stringify(['grouped-plan', integrationName, canonicalInstances]);
9
+ }
@@ -0,0 +1,5 @@
1
+ import type { PageBrowserGraphContribution } from '../../../types/public-types.js';
2
+ /**
3
+ * Merges multiple Page Browser Graph contributions into one declarative payload.
4
+ */
5
+ export declare function mergePageBrowserGraphContributions(...contributions: Array<PageBrowserGraphContribution | undefined>): PageBrowserGraphContribution | undefined;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Merges multiple Page Browser Graph contributions into one declarative payload.
3
+ */
4
+ export function mergePageBrowserGraphContributions(...contributions) {
5
+ const dependencies = [];
6
+ const assets = [];
7
+ const watchPaths = new Set();
8
+ for (const contribution of contributions) {
9
+ if (!contribution) {
10
+ continue;
11
+ }
12
+ if (contribution.dependencies?.length) {
13
+ dependencies.push(...contribution.dependencies);
14
+ }
15
+ if (contribution.assets?.length) {
16
+ assets.push(...contribution.assets);
17
+ }
18
+ for (const watchPath of contribution.watchPaths ?? []) {
19
+ watchPaths.add(watchPath);
20
+ }
21
+ }
22
+ if (dependencies.length === 0 && assets.length === 0 && watchPaths.size === 0) {
23
+ return undefined;
24
+ }
25
+ return {
26
+ ...(dependencies.length > 0 ? { dependencies } : {}),
27
+ ...(assets.length > 0 ? { assets } : {}),
28
+ ...(watchPaths.size > 0 ? { watchPaths: Array.from(watchPaths) } : {}),
29
+ };
30
+ }
@@ -1,3 +1,4 @@
1
+ import type { GroupedGraphScope } from './route-instance-key.js';
1
2
  import type { EcoPagesAppConfig } from '../../../types/internal-types.js';
2
3
  import type { PageBrowserGraphContribution, PageBrowserGraphResult } from '../../../types/public-types.js';
3
4
  import type { ProcessedAsset } from '../../../services/assets/asset-processing-service/assets.types.js';
@@ -5,6 +6,7 @@ export type GraphPolicy = 'development' | 'production';
5
6
  export type GraphKey = {
6
7
  integrationName: string;
7
8
  routeFile: string;
9
+ dependencyInstanceKey: string;
8
10
  entryFingerprint: string;
9
11
  policy: GraphPolicy;
10
12
  };
@@ -18,6 +20,11 @@ export type GraphRecord = {
18
20
  export type GraphBuildResult = {
19
21
  result: PageBrowserGraphResult;
20
22
  dependencyPaths: ReadonlySet<string>;
23
+ /**
24
+ * @remarks
25
+ * When false, the build result is returned to callers but not committed to the session cache.
26
+ */
27
+ cacheable?: boolean;
21
28
  };
22
29
  type GroupedGraphRecord = {
23
30
  assetsByRoute: Map<string, ProcessedAsset[]>;
@@ -32,6 +39,7 @@ export type GroupedGraphBuildOutcome = GroupedGraphRecord | {
32
39
  export type AffectedGraphIdentity = {
33
40
  integrationName: string;
34
41
  routeFile: string;
42
+ dependencyInstanceKey: string;
35
43
  policy: GraphPolicy;
36
44
  entryFingerprint: string;
37
45
  };
@@ -42,8 +50,10 @@ export declare class GraphDependencyIndex {
42
50
  private readonly dependencyToGraphKeys;
43
51
  bindRecord(record: GraphRecord): void;
44
52
  unbindRecord(record: GraphRecord): void;
45
- bindGroupedRecord(integrationName: string, record: GroupedGraphRecord): void;
46
- unbindGroupedRecord(integrationName: string, record: GroupedGraphRecord): void;
53
+ bindGroupedRecord(cacheKey: string, record: GroupedGraphRecord): void;
54
+ unbindGroupedRecord(cacheKey: string, record: GroupedGraphRecord): void;
55
+ bindPaths(serializedKey: string, dependencyPaths: ReadonlySet<string>): void;
56
+ unbindPaths(serializedKey: string, dependencyPaths: ReadonlySet<string>): void;
47
57
  getAffectedKeys(filePath: string): Set<string>;
48
58
  resetForTests(): void;
49
59
  private addDependencyBinding;
@@ -55,21 +65,24 @@ export declare class GraphDependencyIndex {
55
65
  export declare class SessionPageBrowserGraphCache {
56
66
  private readonly records;
57
67
  private readonly inFlight;
68
+ private readonly inFlightDependencyPaths;
69
+ private readonly inFlightGenerations;
58
70
  private readonly groupedRecords;
59
71
  private readonly groupedInFlight;
60
72
  private readonly dependencyIndex;
61
73
  private readonly graphGenerations;
62
74
  private readonly groupedGenerations;
63
75
  private buildCount;
64
- getGraphByRoute(integrationName: string, routeFile: string, policy: GraphPolicy): PageBrowserGraphResult | undefined;
65
76
  /**
66
77
  * Page Browser Graph compilations in this dev-server session.
67
78
  */
68
79
  getBuildCount(): number;
80
+ getGraphByRoute(integrationName: string, routeFile: string, policy: GraphPolicy, dependencyInstanceKey?: string): PageBrowserGraphResult | undefined;
81
+ getDependencyPathsByRoute(integrationName: string, routeFile: string, policy: GraphPolicy, dependencyInstanceKey?: string): ReadonlySet<string> | undefined;
69
82
  getAffectedGraphIdentities(filePath: string): AffectedGraphIdentity[];
70
- resolveGraph(key: GraphKey, build: () => Promise<GraphBuildResult | undefined>): Promise<PageBrowserGraphResult | undefined>;
71
- resolveGroupedGraph(integrationName: string, build: () => Promise<GroupedGraphBuildOutcome | undefined>): Promise<Map<string, ProcessedAsset[]>>;
72
- peekGroupedGraph(integrationName: string): Map<string, ProcessedAsset[]> | undefined;
83
+ resolveGraph(key: GraphKey, provisionalDependencyPaths: ReadonlySet<string>, build: () => Promise<GraphBuildResult | undefined>): Promise<PageBrowserGraphResult | undefined>;
84
+ resolveGroupedGraph(scope: GroupedGraphScope, build: () => Promise<GroupedGraphBuildOutcome | undefined>): Promise<Map<string, ProcessedAsset[]>>;
85
+ peekGroupedGraph(scope: GroupedGraphScope): Map<string, ProcessedAsset[]> | undefined;
73
86
  invalidateByFilePath(filePath: string): number;
74
87
  invalidateRoute(integrationName: string, routeFile: string): void;
75
88
  invalidateByRouteFile(routeFile: string): void;
@@ -85,6 +98,10 @@ export declare class SessionPageBrowserGraphCache {
85
98
  private runGroupedBuild;
86
99
  private canCommitGraphBuild;
87
100
  private canCommitGroupedBuild;
101
+ private bindDependencyPaths;
102
+ private unbindDependencyPaths;
103
+ private clearInFlightBuild;
104
+ private releaseInFlightDependencyPaths;
88
105
  }
89
106
  /**
90
107
  * Returns the session graph cache for one app instance.