@ecopages/core 0.2.0-beta.38 → 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 (108) hide show
  1. package/README.md +1 -0
  2. package/package.json +27 -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 +2 -2
  45. package/src/eco/eco.js +7 -6
  46. package/src/eco/eco.types.d.ts +6 -5
  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 +10 -8
  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 +0 -8
  64. package/src/route-renderer/orchestration/integration-renderer.js +6 -19
  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/page-browser-graph-session.d.ts +1 -0
  70. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph-session.js +12 -0
  71. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph.service.d.ts +9 -0
  72. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph.service.js +7 -0
  73. package/src/route-renderer/orchestration/route-pipeline/route-render-orchestrator.js +65 -33
  74. package/src/route-renderer/page-loading/component-dependency-collection.js +3 -2
  75. package/src/route-renderer/page-loading/ecopages-virtual-imports.d.ts +0 -5
  76. package/src/route-renderer/page-loading/ecopages-virtual-imports.js +4 -10
  77. package/src/route-renderer/page-loading/file-scoped-dependency-components.d.ts +2 -2
  78. package/src/route-renderer/page-loading/file-scoped-dependency-components.js +12 -11
  79. package/src/route-renderer/page-loading/lazy-trigger-planning.js +2 -1
  80. package/src/route-renderer/page-loading/page-module-loader.d.ts +1 -1
  81. package/src/route-renderer/page-loading/page-module-loader.js +8 -3
  82. package/src/router/README.md +8 -0
  83. package/src/services/README.md +5 -0
  84. package/src/services/assets/asset-processing-service/asset-processing.service.d.ts +1 -0
  85. package/src/services/assets/asset-processing-service/asset-processing.service.js +22 -5
  86. package/src/services/cache/cache.types.d.ts +4 -2
  87. package/src/services/cache/html-page-cache-dependency-index.d.ts +34 -0
  88. package/src/services/cache/html-page-cache-dependency-index.js +88 -0
  89. package/src/services/cache/index.d.ts +2 -0
  90. package/src/services/cache/index.js +2 -0
  91. package/src/services/cache/page-cache-service.d.ts +31 -1
  92. package/src/services/cache/page-cache-service.js +79 -3
  93. package/src/services/cache/page-request-cache-coordinator.service.js +4 -0
  94. package/src/services/module-loading/README.md +9 -6
  95. package/src/services/module-loading/app-server-module-transpiler.service.js +0 -3
  96. package/src/services/module-loading/collection-server-module-build.service.d.ts +19 -0
  97. package/src/services/module-loading/collection-server-module-build.service.js +83 -0
  98. package/src/services/module-loading/page-module-import.service.d.ts +5 -11
  99. package/src/services/module-loading/page-module-import.service.js +56 -73
  100. package/src/services/module-loading/route-module-build-cache.store.js +6 -6
  101. package/src/services/module-loading/route-module-build-manifest.d.ts +8 -5
  102. package/src/services/module-loading/route-module-build-manifest.js +20 -32
  103. package/src/services/module-loading/server-module-transpiler.service.d.ts +0 -2
  104. package/src/services/module-loading/server-module-transpiler.service.js +0 -3
  105. package/src/static-site-generator/static-site-generator.js +2 -2
  106. package/src/types/internal-types.d.ts +4 -0
  107. package/src/types/public-types.d.ts +18 -18
  108. package/src/watchers/project-watcher.js +14 -0
@@ -366,14 +366,14 @@ export class IntegrationRenderer {
366
366
  ? input.children.outerHTML
367
367
  : undefined;
368
368
  if (input.children !== undefined && serializedChildren === undefined) {
369
- const componentFile = input.component.config?.__eco?.file ?? 'unknown component';
369
+ const componentFile = input.component.config?.identity?.file ?? 'unknown component';
370
370
  const childTag = Object.prototype.toString.call(input.children);
371
371
  throw new TypeError(`[ecopages] ${this.name} renderer expected serialized children for ${componentFile}, received ${childTag}.`);
372
372
  }
373
373
  const props = serializedChildren === undefined ? input.props : { ...input.props, children: serializedChildren };
374
374
  const content = await component(props);
375
375
  if (typeof content !== 'string') {
376
- const componentFile = input.component.config?.__eco?.file ?? 'unknown component';
376
+ const componentFile = input.component.config?.identity?.file ?? 'unknown component';
377
377
  const contentTag = Object.prototype.toString.call(content);
378
378
  throw new TypeError(`[ecopages] ${this.name} renderer expected a string render result for ${componentFile}, received ${contentTag}.`);
379
379
  }
@@ -453,33 +453,19 @@ export class IntegrationRenderer {
453
453
  invariant(false, `Error importing HtmlTemplate: ${error}`);
454
454
  }
455
455
  }
456
- usesIntegrationPageImporter(_file) {
457
- return false;
458
- }
459
- async importIntegrationPageFile(_file, _options) {
460
- invariant(false, 'Integration page importer must be implemented when enabled');
461
- }
462
456
  normalizeImportedPageFile(_file, pageModule) {
463
457
  return pageModule;
464
458
  }
465
459
  /**
466
460
  * Imports the page file from the specified path.
467
- * It uses dynamic import to load the file and returns the imported module.
468
461
  *
469
462
  * @param file - The file path to import.
470
463
  * @returns The imported module.
471
464
  */
472
465
  async importPageFile(file, options) {
473
- const bypassCache = options?.bypassCache ?? false;
474
- const pageModule = this.usesIntegrationPageImporter(file)
475
- ? await this.importIntegrationPageFile(file, {
476
- bypassCache,
477
- cacheScope: options?.cacheScope,
478
- })
479
- : await this.pageModuleLoaderService.importPageFile(file, {
480
- bypassCache,
481
- cacheScope: options?.cacheScope,
482
- });
466
+ const pageModule = await this.pageModuleLoaderService.importPageFile(file, {
467
+ bypassCache: options?.bypassCache,
468
+ });
483
469
  return this.normalizeImportedPageFile(file, pageModule);
484
470
  }
485
471
  /**
@@ -546,6 +532,7 @@ export class IntegrationRenderer {
546
532
  async resolveRouteRenderInputs(routeOptions) {
547
533
  const resolvedPageModule = await this.pageModuleLoaderService.resolvePageModule({
548
534
  file: routeOptions.file,
535
+ pageModule: routeOptions.pageModule,
549
536
  importPageFileFn: (targetFile) => this.importPageFile(targetFile),
550
537
  });
551
538
  const { Page, integrationSpecificProps, module: pageModule } = resolvedPageModule;
@@ -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;
@@ -78,6 +78,7 @@ export declare class SessionPageBrowserGraphCache {
78
78
  */
79
79
  getBuildCount(): number;
80
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;
81
82
  getAffectedGraphIdentities(filePath: string): AffectedGraphIdentity[];
82
83
  resolveGraph(key: GraphKey, provisionalDependencyPaths: ReadonlySet<string>, build: () => Promise<GraphBuildResult | undefined>): Promise<PageBrowserGraphResult | undefined>;
83
84
  resolveGroupedGraph(scope: GroupedGraphScope, build: () => Promise<GroupedGraphBuildOutcome | undefined>): Promise<Map<string, ProcessedAsset[]>>;
@@ -139,6 +139,18 @@ export class SessionPageBrowserGraphCache {
139
139
  }
140
140
  return undefined;
141
141
  }
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;
153
+ }
142
154
  getAffectedGraphIdentities(filePath) {
143
155
  const identities = [];
144
156
  for (const serializedKey of this.dependencyIndex.getAffectedKeys(filePath)) {
@@ -24,6 +24,15 @@ export declare class PageBrowserGraphService {
24
24
  private readonly assetProcessingService;
25
25
  constructor(appConfig: EcoPagesAppConfig, assetProcessingService: AssetProcessingService);
26
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;
27
36
  /**
28
37
  * Resolves or builds one Page Browser Graph under the given policy.
29
38
  */
@@ -23,6 +23,13 @@ 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
  */
@@ -1,3 +1,5 @@
1
+ import { collectHtmlCacheSourceDependencyPaths } from '../../../services/cache/html-page-cache-dependency-index.js';
2
+ import { getComponentIdentity } from '../../../eco/component-identity.js';
1
3
  import { inspectUnresolvedMarkerArtifactHtml } from './marker-artifact.utils.js';
2
4
  import { OwnershipValidationService, throwIfOwnershipInvalid, } from '../ownership-graph/ownership-validation.service.js';
3
5
  import { buildEagerSsrLazyAssetsFromGraph, collectResolvedLazyTriggersFromGraph, collectUsedIntegrationDependenciesFromGraph, } from '../ownership-graph/component-graph-collectors.js';
@@ -6,6 +8,7 @@ import { mergePageBrowserGraphContributions } from '../page-browser-graph/page-b
6
8
  import { PageBrowserGraphService } from '../page-browser-graph/page-browser-graph.service.js';
7
9
  import { createPageDependencyInstanceKey } from '../page-browser-graph/route-instance-key.js';
8
10
  import { buildPreparedRenderOptions } from './route-prepared-options.builder.js';
11
+ import { measureRouteRenderPhase } from '../../../diagnostics/request-pipeline-metrics.js';
9
12
  /**
10
13
  * Owns one route render from normalized module loading through final HTML output.
11
14
  *
@@ -35,7 +38,7 @@ export class RouteRenderOrchestrator {
35
38
  * HTML transformation.
36
39
  */
37
40
  async prepareRenderOptions(routeOptions, adapter) {
38
- const resolvedInputs = await adapter.resolveRouteRenderInputs(routeOptions);
41
+ const resolvedInputs = await measureRouteRenderPhase('resolve-route-inputs', () => adapter.resolveRouteRenderInputs(routeOptions));
39
42
  const finalProps = {
40
43
  ...resolvedInputs.props,
41
44
  ...(routeOptions.props ?? {}),
@@ -45,15 +48,17 @@ export class RouteRenderOrchestrator {
45
48
  props: finalProps,
46
49
  };
47
50
  const { Page, HtmlTemplate, Layouts } = finalResolvedInputs;
48
- const validationErrors = this.ownershipValidationService.validate({
49
- currentIntegrationName: adapter.name,
50
- roots: [
51
- { component: HtmlTemplate, source: 'html-template' },
52
- ...Layouts.map((layout) => ({ component: layout, source: 'layout' })),
53
- { component: Page, source: 'page' },
54
- ],
51
+ await measureRouteRenderPhase('ownership-validation', async () => {
52
+ const validationErrors = this.ownershipValidationService.validate({
53
+ currentIntegrationName: adapter.name,
54
+ roots: [
55
+ { component: HtmlTemplate, source: 'html-template' },
56
+ ...Layouts.map((layout) => ({ component: layout, source: 'layout' })),
57
+ { component: Page, source: 'page' },
58
+ ],
59
+ });
60
+ throwIfOwnershipInvalid(validationErrors);
55
61
  });
56
- throwIfOwnershipInvalid(validationErrors);
57
62
  const componentsToResolve = [HtmlTemplate, ...Layouts, Page];
58
63
  const dependencyInstanceKey = createPageDependencyInstanceKey({
59
64
  params: routeOptions.params,
@@ -68,17 +73,20 @@ export class RouteRenderOrchestrator {
68
73
  dependencyInstanceKey,
69
74
  };
70
75
  const resolvedPageDependencies = await adapter.resolvePageDependencies(graphContext);
71
- const [{ resolvedDependencies }, pageBrowserGraph] = await Promise.all([
72
- adapter.resolveRouteDependencies({
73
- components: componentsToResolve,
74
- }),
75
- this.pageBrowserGraphService.resolvePageBrowserGraph({
76
- routeFile: routeOptions.file,
77
- dependencyInstanceKey,
78
- integrationName: adapter.name,
79
- collectContribution: async () => mergePageBrowserGraphContributions(await adapter.collectPageBrowserGraphContribution(graphContext), resolvedPageDependencies?.contribution),
80
- }),
81
- ]);
76
+ const { resolvedDependencies } = await measureRouteRenderPhase('resolve-dependencies', () => adapter.resolveRouteDependencies({
77
+ components: componentsToResolve,
78
+ }));
79
+ const pageBrowserGraph = await measureRouteRenderPhase('page-browser-graph', () => this.pageBrowserGraphService.resolvePageBrowserGraph({
80
+ routeFile: routeOptions.file,
81
+ dependencyInstanceKey,
82
+ integrationName: adapter.name,
83
+ collectContribution: async () => mergePageBrowserGraphContributions(await adapter.collectPageBrowserGraphContribution(graphContext), resolvedPageDependencies?.contribution),
84
+ }));
85
+ const graphDependencyPaths = this.pageBrowserGraphService.getDependencyPathsForRoute({
86
+ integrationName: adapter.name,
87
+ routeFile: routeOptions.file,
88
+ dependencyInstanceKey,
89
+ });
82
90
  const resolvedPageDependencyComponents = resolvedPageDependencies?.components ?? [];
83
91
  const allDependencies = [
84
92
  ...resolvedDependencies,
@@ -92,15 +100,24 @@ export class RouteRenderOrchestrator {
92
100
  buildEagerSsrLazyAssetsFromGraph(this.assetProcessingService, componentsToResolve, adapter.name),
93
101
  ]);
94
102
  allDependencies.push(...globalAssets, ...eagerSsrLazyAssets);
95
- return buildPreparedRenderOptions({
96
- routeOptions,
97
- resolvedInputs: finalResolvedInputs,
98
- resolvedPageDependencyComponents,
99
- resolvedDependencies,
100
- allDependencies,
101
- pageBrowserGraph,
102
- appConfig: this.appConfig,
103
+ const sourceDependencyPaths = collectHtmlCacheSourceDependencyPaths({
104
+ routeFile: routeOptions.file,
105
+ processedAssets: allDependencies,
106
+ graphDependencyPaths,
107
+ additionalSourcePaths: collectRenderShellSourcePaths(this.appConfig, componentsToResolve),
103
108
  });
109
+ return {
110
+ ...buildPreparedRenderOptions({
111
+ routeOptions,
112
+ resolvedInputs: finalResolvedInputs,
113
+ resolvedPageDependencyComponents,
114
+ resolvedDependencies,
115
+ allDependencies,
116
+ pageBrowserGraph,
117
+ appConfig: this.appConfig,
118
+ }),
119
+ sourceDependencyPaths,
120
+ };
104
121
  }
105
122
  async resolveDeclaredPageBrowserGraph(input) {
106
123
  return await this.pageBrowserGraphService.resolvePageBrowserGraph(input);
@@ -127,7 +144,7 @@ export class RouteRenderOrchestrator {
127
144
  * Executes the route-render finalization path for already prepared render options.
128
145
  */
129
146
  async executePrepared(renderOptions, adapter) {
130
- const renderExecution = await this.captureHtmlRender(async () => adapter.renderRouteBody(renderOptions));
147
+ const renderExecution = await measureRouteRenderPhase('render-body', () => this.captureHtmlRender(async () => adapter.renderRouteBody(renderOptions)));
131
148
  const unresolvedArtifactInspection = inspectUnresolvedMarkerArtifactHtml(renderExecution.html);
132
149
  const htmlFinalization = adapter.getRouteHtmlFinalization(renderOptions);
133
150
  const hasUnresolvedMarkerHtml = unresolvedArtifactInspection.hasUnresolvedMarkerArtifacts;
@@ -137,27 +154,29 @@ export class RouteRenderOrchestrator {
137
154
  const canReuseCapturedBody = !hasUnresolvedMarkerHtml && htmlFinalization.finalizeHtml === undefined;
138
155
  if (canReuseCapturedBody) {
139
156
  const responseBody = typeof renderExecution.body === 'string' ? renderExecution.body : renderExecution.html;
140
- const body = await adapter.transformRouteResponse(new Response(responseBody, {
157
+ const body = await measureRouteRenderPhase('transform-response', () => adapter.transformRouteResponse(new Response(responseBody, {
141
158
  headers: {
142
159
  'Content-Type': 'text/html',
143
160
  },
144
- }), htmlFinalization.htmlContributions, renderOptions.pagePackage);
161
+ }), htmlFinalization.htmlContributions, renderOptions.pagePackage));
145
162
  return {
146
163
  body,
147
164
  cacheStrategy: renderOptions.cacheStrategy,
165
+ sourceDependencyPaths: renderOptions.sourceDependencyPaths,
148
166
  };
149
167
  }
150
168
  const finalization = htmlFinalization.finalizeHtml
151
169
  ? htmlFinalization.finalizeHtml(unresolvedArtifactInspection.normalizedHtml)
152
170
  : unresolvedArtifactInspection.normalizedHtml;
153
- const body = await adapter.transformRouteResponse(new Response(finalization, {
171
+ const body = await measureRouteRenderPhase('transform-response', () => adapter.transformRouteResponse(new Response(finalization, {
154
172
  headers: {
155
173
  'Content-Type': 'text/html',
156
174
  },
157
- }), htmlFinalization.htmlContributions, renderOptions.pagePackage);
175
+ }), htmlFinalization.htmlContributions, renderOptions.pagePackage));
158
176
  return {
159
177
  body,
160
178
  cacheStrategy: renderOptions.cacheStrategy,
179
+ sourceDependencyPaths: renderOptions.sourceDependencyPaths,
161
180
  };
162
181
  }
163
182
  async captureRenderedBody(body) {
@@ -181,3 +200,16 @@ export class RouteRenderOrchestrator {
181
200
  };
182
201
  }
183
202
  }
203
+ function collectRenderShellSourcePaths(appConfig, components) {
204
+ const sourcePaths = new Set();
205
+ if (appConfig.absolutePaths?.htmlTemplatePath) {
206
+ sourcePaths.add(appConfig.absolutePaths.htmlTemplatePath);
207
+ }
208
+ for (const component of components) {
209
+ const componentFile = getComponentIdentity(component)?.file;
210
+ if (componentFile) {
211
+ sourcePaths.add(componentFile);
212
+ }
213
+ }
214
+ return [...sourcePaths];
215
+ }
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { assertEcoDeclaredComponent } from '../../eco/eco-declared-component.js';
3
+ import { getComponentIdentity } from '../../eco/component-identity.js';
3
4
  import { AssetFactory } from '../../services/assets/asset-processing-service/index.js';
4
5
  import { extractEcopagesVirtualImports } from './ecopages-virtual-imports.js';
5
6
  import { collectDeclaredAssetEntries } from './declared-asset-collection.js';
@@ -43,7 +44,7 @@ export function collectComponentDependencies(options) {
43
44
  for (const component of components) {
44
45
  if (!component)
45
46
  continue;
46
- const componentFile = component.config?.__eco?.file;
47
+ const componentFile = getComponentIdentity(component)?.file;
47
48
  if (!componentFile)
48
49
  continue;
49
50
  const stylesheetDependencyKeys = new Set();
@@ -55,7 +56,7 @@ export function collectComponentDependencies(options) {
55
56
  const collect = (config) => {
56
57
  if (!config)
57
58
  return;
58
- const file = config.__eco?.file;
59
+ const file = getComponentIdentity(config)?.file;
59
60
  if (!file)
60
61
  return;
61
62
  const dir = path.dirname(file);
@@ -2,11 +2,6 @@ export type EcopagesVirtualImport = {
2
2
  from: string;
3
3
  imports: string[] | undefined;
4
4
  };
5
- /**
6
- * @remarks
7
- * Content server modules static-import every MDX entry. They must never become
8
- * browser module-script dependencies discovered from page or component sources.
9
- */
10
5
  export declare function isBrowserEcopagesVirtualImport(specifier: string): boolean;
11
6
  /**
12
7
  * Extracts runtime `ecopages:` virtual-module imports from a component source file.
@@ -1,14 +1,8 @@
1
1
  import { readFileSync } from 'node:fs';
2
- import { parseSync } from 'oxc-parser';
3
- /** Matches `ecopages:content/<collection>/server` SSR-only MDX resolver modules. */
4
- const CONTENT_SERVER_VIRTUAL_MODULE_PATTERN = /^ecopages:content\/[a-z][a-z0-9-]+\/server$/;
5
- /**
6
- * @remarks
7
- * Content server modules static-import every MDX entry. They must never become
8
- * browser module-script dependencies discovered from page or component sources.
9
- */
2
+ import { isContentServerVirtualModule } from '../../build/contracts/content-virtual-modules.js';
3
+ import { parseModuleSource } from '../../cache/module-parse-cache.js';
10
4
  export function isBrowserEcopagesVirtualImport(specifier) {
11
- return specifier.startsWith('ecopages:') && !CONTENT_SERVER_VIRTUAL_MODULE_PATTERN.test(specifier);
5
+ return specifier.startsWith('ecopages:') && !isContentServerVirtualModule(specifier);
12
6
  }
13
7
  /**
14
8
  * Extracts runtime `ecopages:` virtual-module imports from a component source file.
@@ -26,7 +20,7 @@ export function extractEcopagesVirtualImports(file) {
26
20
  }
27
21
  let result;
28
22
  try {
29
- result = parseSync(file, source, { sourceType: 'module' });
23
+ result = parseModuleSource(file, source);
30
24
  }
31
25
  catch {
32
26
  return [];
@@ -1,7 +1,7 @@
1
1
  import type { EcoComponent, EcoComponentConfig, EcoComponentDependencies } from '../../types/public-types.js';
2
2
  import type { PageDependenciesResult } from '../../eco/eco.types.js';
3
3
  /**
4
- * Attaches file-backed `__eco` metadata to one component config.
4
+ * Attaches canonical file-backed identity to one component config.
5
5
  */
6
6
  export declare function attachEcoFileMetadataToConfig(config: EcoComponentConfig, ownerFile: string, integrationName: string): EcoComponentConfig;
7
7
  /**
@@ -26,7 +26,7 @@ export type CollectComponentConfigFilePathsOptions = {
26
26
  includeLayouts?: boolean;
27
27
  };
28
28
  /**
29
- * Walks component configs and collects every resolved `config.__eco.file` path.
29
+ * Walks component configs and collects every resolved identity file path.
30
30
  */
31
31
  export declare function collectComponentConfigFilePaths(components: ReadonlyArray<EcoComponent | Partial<EcoComponent> | undefined>, options?: CollectComponentConfigFilePathsOptions): Set<string>;
32
32
  /**
@@ -1,17 +1,15 @@
1
1
  import path from 'node:path';
2
2
  import { rapidhash } from '../../utils/hash.js';
3
+ import { bindComponentIdentity, getComponentIdentity } from '../../eco/component-identity.js';
3
4
  /**
4
- * Attaches file-backed `__eco` metadata to one component config.
5
+ * Attaches canonical file-backed identity to one component config.
5
6
  */
6
7
  export function attachEcoFileMetadataToConfig(config, ownerFile, integrationName) {
7
- return {
8
- ...config,
9
- __eco: {
10
- id: config.__eco?.id ?? rapidhash(ownerFile).toString(36),
11
- file: ownerFile,
12
- integration: config.__eco?.integration ?? integrationName,
13
- },
14
- };
8
+ return bindComponentIdentity({
9
+ id: getComponentIdentity(config)?.id ?? rapidhash(ownerFile).toString(36),
10
+ file: ownerFile,
11
+ integration: getComponentIdentity(config)?.integration ?? integrationName,
12
+ }, config);
15
13
  }
16
14
  /**
17
15
  * Builds a synthetic component config rooted at one file so direct dependency
@@ -45,7 +43,7 @@ export function collectFileScopedDependencyComponents(options) {
45
43
  return components;
46
44
  }
47
45
  /**
48
- * Walks component configs and collects every resolved `config.__eco.file` path.
46
+ * Walks component configs and collects every resolved identity file path.
49
47
  */
50
48
  export function collectComponentConfigFilePaths(components, options) {
51
49
  const files = new Set();
@@ -54,10 +52,13 @@ export function collectComponentConfigFilePaths(components, options) {
54
52
  files.add(path.resolve(additionalPath));
55
53
  }
56
54
  const visit = (config) => {
57
- const file = config?.__eco?.file;
55
+ const file = getComponentIdentity(config)?.file;
58
56
  if (!file) {
59
57
  return;
60
58
  }
59
+ if (!config) {
60
+ return;
61
+ }
61
62
  const resolved = path.resolve(file);
62
63
  if (visited.has(resolved)) {
63
64
  return;
@@ -1,3 +1,4 @@
1
+ import { getComponentIdentity } from '../../eco/component-identity.js';
1
2
  import { rapidhash } from '../../utils/hash.js';
2
3
  /**
3
4
  * Creates a stable grouping key for a lazy trigger declaration.
@@ -24,7 +25,7 @@ export function getLazyTriggerKey(lazy) {
24
25
  export function buildResolvedLazyTriggers(config, groups) {
25
26
  if (groups.length === 0)
26
27
  return [];
27
- const componentFile = config.__eco?.file ?? '';
28
+ const componentFile = getComponentIdentity(config)?.file ?? '';
28
29
  const sortedUrls = groups
29
30
  .flatMap((group) => group.scripts)
30
31
  .sort()
@@ -29,7 +29,6 @@ export declare class PageModuleLoaderService {
29
29
  */
30
30
  importPageFile(file: string, options?: {
31
31
  bypassCache?: boolean;
32
- cacheScope?: string;
33
32
  }): Promise<EcoPageFile>;
34
33
  /**
35
34
  * Executes the page's static-props hook with Ecopages runtime context.
@@ -62,6 +61,7 @@ export declare class PageModuleLoaderService {
62
61
  */
63
62
  resolvePageModule(options: {
64
63
  file: string;
64
+ pageModule?: EcoPageFile;
65
65
  importPageFileFn?: (file: string) => Promise<EcoPageFile>;
66
66
  }): Promise<{
67
67
  module: EcoPageFile;
@@ -1,6 +1,7 @@
1
1
  import { invariant } from '../../utils/invariant.js';
2
2
  import { getAppModuleLoader } from '../../services/module-loading/app-server-module-transpiler.service.js';
3
3
  import { resolveInternalExecutionDir } from '../../utils/resolve-work-dir.js';
4
+ import { HttpError } from '../../errors/http-error.js';
4
5
  /**
5
6
  * Loads route page modules and normalizes their data hooks for rendering.
6
7
  *
@@ -39,7 +40,6 @@ export class PageModuleLoaderService {
39
40
  rootDir: this.appConfig.rootDir,
40
41
  outdir: `${resolveInternalExecutionDir(this.appConfig)}/.server-modules`,
41
42
  bypassCache: options?.bypassCache,
42
- cacheScope: options?.cacheScope,
43
43
  transpileErrorMessage: (details) => `Error transpiling page file: ${details}`,
44
44
  noOutputMessage: (targetFilePath) => `No transpiled output generated for page: ${targetFilePath}`,
45
45
  });
@@ -65,7 +65,11 @@ export class PageModuleLoaderService {
65
65
  })
66
66
  .then((data) => data)
67
67
  .catch((err) => {
68
- throw new Error(`Error fetching static props: ${err.message}`);
68
+ if (HttpError.isHttpError(err) || err instanceof Response) {
69
+ throw err;
70
+ }
71
+ const message = err instanceof Error ? err.message : String(err);
72
+ throw new Error(`Error fetching static props: ${message}`, { cause: err });
69
73
  })
70
74
  : {
71
75
  props: {},
@@ -98,7 +102,8 @@ export class PageModuleLoaderService {
98
102
  * When both component static methods and module exports exist, component statics win.
99
103
  */
100
104
  async resolvePageModule(options) {
101
- const module = await (options.importPageFileFn ?? ((file) => this.importPageFile(file)))(options.file);
105
+ const module = options.pageModule ??
106
+ (await (options.importPageFileFn ?? ((file) => this.importPageFile(file)))(options.file));
102
107
  const { default: Page, getStaticProps: moduleGetStaticProps, getMetadata: moduleGetMetadata, ...integrationSpecificProps } = module;
103
108
  return {
104
109
  module,