@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
@@ -0,0 +1,29 @@
1
+ import type { PageParams, PageQuery } from '../../../types/public-types.js';
2
+ export type PageDependencyInstance = {
3
+ params?: PageParams;
4
+ query?: PageQuery;
5
+ };
6
+ export type GroupedGraphScope = {
7
+ integrationName: string;
8
+ planKey: string;
9
+ };
10
+ /**
11
+ * Builds a stable cache key for one concrete dependency resolver input.
12
+ *
13
+ * @remarks
14
+ * The representation deliberately retains scalar versus array values. Delimiter-based
15
+ * encodings would make otherwise distinct route inputs collide.
16
+ */
17
+ export declare function createPageDependencyInstanceKey(input: PageDependencyInstance): string;
18
+ /**
19
+ * Combines a route file with an optional dependency-instance key for grouped graph maps.
20
+ */
21
+ export declare function createRouteGraphLookupKey(routeFile: string, dependencyInstanceKey?: string): string;
22
+ /**
23
+ * Serializes the grouped graph cache scope for one integration and dependency instance.
24
+ */
25
+ export declare function serializeGroupedGraphCacheKey(scope: GroupedGraphScope): string;
26
+ /**
27
+ * Reconstructs dependency resolver inputs from a serialized dependency-instance key.
28
+ */
29
+ export declare function parsePageDependencyInstanceKey(dependencyInstanceKey: string): PageDependencyInstance;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Builds a stable cache key for one concrete dependency resolver input.
3
+ *
4
+ * @remarks
5
+ * The representation deliberately retains scalar versus array values. Delimiter-based
6
+ * encodings would make otherwise distinct route inputs collide.
7
+ */
8
+ export function createPageDependencyInstanceKey(input) {
9
+ const params = canonicalizeInput(input.params);
10
+ const query = canonicalizeInput(input.query);
11
+ if (params.length === 0 && query.length === 0) {
12
+ return '';
13
+ }
14
+ return JSON.stringify({ params, query });
15
+ }
16
+ /**
17
+ * Combines a route file with an optional dependency-instance key for grouped graph maps.
18
+ */
19
+ export function createRouteGraphLookupKey(routeFile, dependencyInstanceKey = '') {
20
+ return JSON.stringify([routeFile, dependencyInstanceKey]);
21
+ }
22
+ /**
23
+ * Serializes the grouped graph cache scope for one integration and dependency instance.
24
+ */
25
+ export function serializeGroupedGraphCacheKey(scope) {
26
+ return JSON.stringify(['grouped', scope.integrationName, scope.planKey]);
27
+ }
28
+ /**
29
+ * Reconstructs dependency resolver inputs from a serialized dependency-instance key.
30
+ */
31
+ export function parsePageDependencyInstanceKey(dependencyInstanceKey) {
32
+ if (!dependencyInstanceKey) {
33
+ return {};
34
+ }
35
+ const parsed = JSON.parse(dependencyInstanceKey);
36
+ if (!isSerializedDependencyInstance(parsed)) {
37
+ throw new TypeError('Invalid page dependency instance key.');
38
+ }
39
+ return {
40
+ ...(parsed.params.length > 0 ? { params: Object.fromEntries(parsed.params) } : {}),
41
+ ...(parsed.query.length > 0 ? { query: Object.fromEntries(parsed.query) } : {}),
42
+ };
43
+ }
44
+ function canonicalizeInput(input) {
45
+ return Object.entries(input ?? {})
46
+ .sort(([left], [right]) => left.localeCompare(right))
47
+ .map(([key, value]) => [key, Array.isArray(value) ? [...value] : value]);
48
+ }
49
+ function isSerializedDependencyInstance(value) {
50
+ if (!value || typeof value !== 'object' || !('params' in value) || !('query' in value)) {
51
+ return false;
52
+ }
53
+ return Array.isArray(value.params) && Array.isArray(value.query);
54
+ }
@@ -2,6 +2,7 @@ import type { ProcessedAsset } from '../../../services/assets/asset-processing-s
2
2
  import type { HtmlDocumentContribution } from '../../../services/html/html-transformer.service.js';
3
3
  import type { EcoPagesAppConfig } from '../../../types/internal-types.js';
4
4
  import type { EcoComponent, EcoPageFile, IntegrationRendererRenderOptions, PageBrowserGraphContribution, PageBrowserGraphContributionContext, PagePackageResult, RouteRendererBody, RouteRendererOptions } from '../../../types/public-types.js';
5
+ import type { ResolvedPageDependencies } from '../../page-loading/resolved-page-dependencies.js';
5
6
  import type { RouteHtmlFinalization, RouteRenderOrchestratorAdapter, RouteRenderOrchestratorResolvedInputs } from './route-render-orchestrator.js';
6
7
  /**
7
8
  * Host surface required to build the orchestrator adapter for one integration renderer.
@@ -19,6 +20,8 @@ export type IntegrationRouteRenderAdapterHost<C> = {
19
20
  }>;
20
21
  importPageFile(file: string): Promise<EcoPageFile>;
21
22
  collectPageBrowserGraphContribution(context: PageBrowserGraphContributionContext): Promise<PageBrowserGraphContribution | undefined>;
23
+ resolvePageDependencies(context: PageBrowserGraphContributionContext): Promise<ResolvedPageDependencies | undefined>;
24
+ buildPageBrowserGraphContributionContext(routeFile: string, routeOptions?: Pick<RouteRendererOptions, 'params' | 'query'>): Promise<PageBrowserGraphContributionContext>;
22
25
  renderRouteBody(renderOptions: IntegrationRendererRenderOptions<C>): Promise<RouteRendererBody>;
23
26
  getDocumentAttributes(renderOptions: IntegrationRendererRenderOptions<C>): Record<string, string> | undefined;
24
27
  getHtmlDocumentContributions(options: {
@@ -1,4 +1,3 @@
1
- import { loadPageBrowserGraphContribution } from '../page-browser-graph/page-browser-graph-contribution.loader.js';
2
1
  import { buildRouteHtmlFinalization } from './route-html-finalization.service.js';
3
2
  /**
4
3
  * Builds the internal route-render adapter consumed by `RouteRenderOrchestrator`.
@@ -8,7 +7,9 @@ export function createIntegrationRouteRenderAdapter(host) {
8
7
  name: host.name,
9
8
  resolveRouteRenderInputs: (routeOptions) => host.resolveRouteRenderInputs(routeOptions),
10
9
  resolveRouteDependencies: (input) => host.resolveRouteDependencies(input),
11
- collectPageBrowserGraphContribution: (routeFile) => loadPageBrowserGraphContribution(routeFile, (file) => host.importPageFile(file), (context) => host.collectPageBrowserGraphContribution(context)),
10
+ collectPageBrowserGraphContribution: (context) => host.collectPageBrowserGraphContribution(context),
11
+ resolvePageDependencies: (context) => host.resolvePageDependencies(context),
12
+ buildPageBrowserGraphContributionContext: (routeFile, routeOptions) => host.buildPageBrowserGraphContributionContext(routeFile, routeOptions),
12
13
  renderRouteBody: (renderOptions) => host.renderRouteBody(renderOptions),
13
14
  getRouteHtmlFinalization: (renderOptions) => buildRouteHtmlFinalization({
14
15
  appConfig: host.appConfig,
@@ -17,6 +17,7 @@ type PreparedRenderInputs = {
17
17
  export declare function buildPreparedRenderOptions<C = unknown>(input: {
18
18
  routeOptions: RouteRendererOptions;
19
19
  resolvedInputs: PreparedRenderInputs;
20
+ resolvedPageDependencyComponents?: ReadonlyArray<EcoComponent | Partial<EcoComponent>>;
20
21
  resolvedDependencies: ProcessedAsset[];
21
22
  allDependencies: ProcessedAsset[];
22
23
  pageBrowserGraph?: PageBrowserGraphResult;
@@ -5,14 +5,11 @@ import { createPageLocalsProxy } from './route-prepared-options.utils.js';
5
5
  * Assembles the final integration render options after route prep dependencies resolve.
6
6
  */
7
7
  export function buildPreparedRenderOptions(input) {
8
- const { routeOptions, resolvedInputs, resolvedDependencies, allDependencies, pageBrowserGraph, appConfig } = input;
8
+ const { routeOptions, resolvedInputs, resolvedPageDependencyComponents, resolvedDependencies, allDependencies, pageBrowserGraph, appConfig, } = input;
9
9
  const { Page, HtmlTemplate, Layouts, Layout, layoutEntries, props, metadata, integrationSpecificProps } = resolvedInputs;
10
10
  const dedupedDependencies = dedupeProcessedAssets(allDependencies);
11
11
  const pagePackage = createPagePackage(dedupedDependencies, { pageBrowserGraph });
12
- const resolvedProps = {
13
- ...props,
14
- ...(routeOptions.props ?? {}),
15
- };
12
+ const resolvedProps = props;
16
13
  const pageProps = {
17
14
  ...resolvedProps,
18
15
  params: routeOptions.params || {},
@@ -28,6 +25,7 @@ export function buildPreparedRenderOptions(input) {
28
25
  const locals = localsAvailable ? routeOptions.locals : undefined;
29
26
  const preparedOptions = {
30
27
  ...routeOptions,
28
+ resolvedPageDependencyComponents,
31
29
  resolvedDependencies,
32
30
  pagePackage,
33
31
  HtmlTemplate: HtmlTemplate,
@@ -1,11 +1,14 @@
1
1
  import type { EcoPagesAppConfig } from '../../../types/internal-types.js';
2
- import type { EcoComponent, EcoPageComponent, EcoPageFile, HtmlTemplateProps, IntegrationRendererRenderOptions, PageBrowserGraphContribution, PageBrowserGraphResult, PageMetadataProps, RouteRendererBody, RouteRendererOptions, RouteRenderResult, EcoPageLayoutEntry } from '../../../types/public-types.js';
2
+ import type { EcoComponent, EcoPageComponent, EcoPageFile, HtmlTemplateProps, IntegrationRendererRenderOptions, PageBrowserGraphContribution, PageBrowserGraphContributionContext, PageBrowserGraphResult, PageMetadataProps, RouteRendererBody, RouteRendererOptions, RouteRenderResult, EcoPageLayoutEntry } from '../../../types/public-types.js';
3
3
  import type { AssetProcessingService, ProcessedAsset } from '../../../services/assets/asset-processing-service/index.js';
4
4
  import type { HtmlDocumentContribution } from '../../../services/html/html-transformer.service.js';
5
5
  import { OwnershipValidationService } from '../ownership-graph/ownership-validation.service.js';
6
6
  import { PageBrowserGraphService } from '../page-browser-graph/page-browser-graph.service.js';
7
+ import type { GroupedGraphBuildPlan } from '../page-browser-graph/grouped-graph-build-plan.js';
8
+ import type { ResolvedPageDependencies } from '../../page-loading/resolved-page-dependencies.js';
7
9
  export type RouteRenderOrchestratorResolvedInputs = {
8
10
  Page: EcoPageFile['default'] | EcoPageComponent<any>;
11
+ pageModule: EcoPageFile;
9
12
  HtmlTemplate: EcoComponent<HtmlTemplateProps>;
10
13
  Layouts: EcoComponent[];
11
14
  Layout?: EcoComponent;
@@ -43,9 +46,14 @@ export interface RouteRenderOrchestratorAdapter<C> {
43
46
  components: (EcoComponent | Partial<EcoComponent>)[];
44
47
  }): Promise<RouteRenderOrchestratorResolvedDependencies>;
45
48
  /**
46
- * Collects declarative Page Browser Graph requirements for one route.
49
+ * Collects declarative Page Browser Graph requirements for one route instance.
47
50
  */
48
- collectPageBrowserGraphContribution(routeFile: string): Promise<PageBrowserGraphContribution | undefined>;
51
+ collectPageBrowserGraphContribution(context: PageBrowserGraphContributionContext): Promise<PageBrowserGraphContribution | undefined>;
52
+ resolvePageDependencies(context: PageBrowserGraphContributionContext): Promise<ResolvedPageDependencies | undefined>;
53
+ /**
54
+ * Builds the graph contribution context for one route file and optional params.
55
+ */
56
+ buildPageBrowserGraphContributionContext(routeFile: string, routeOptions?: Pick<RouteRendererOptions, 'params' | 'query'>): Promise<PageBrowserGraphContributionContext>;
49
57
  /**
50
58
  * Executes the Integration-specific route render.
51
59
  */
@@ -97,8 +105,10 @@ export declare class RouteRenderOrchestrator {
97
105
  prepareRenderOptions<C = unknown>(routeOptions: RouteRendererOptions, adapter: RouteRenderOrchestratorAdapter<C>): Promise<IntegrationRendererRenderOptions<C>>;
98
106
  resolveDeclaredPageBrowserGraph(input: {
99
107
  routeFile: string;
108
+ dependencyInstanceKey?: string;
100
109
  integrationName: string;
101
- collectContribution: (routeFile: string) => Promise<PageBrowserGraphContribution | undefined>;
110
+ collectContribution: () => Promise<PageBrowserGraphContribution | undefined>;
111
+ groupedBuildPlan?: GroupedGraphBuildPlan;
102
112
  }): Promise<PageBrowserGraphResult | undefined>;
103
113
  /**
104
114
  * Captures one route render body as HTML while preserving a replayable body value.
@@ -1,9 +1,14 @@
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';
4
6
  import { buildGlobalInjectorAssets } from '../page-browser-graph/global-injector-assets.service.js';
7
+ import { mergePageBrowserGraphContributions } from '../page-browser-graph/page-browser-graph-contribution.merge.js';
5
8
  import { PageBrowserGraphService } from '../page-browser-graph/page-browser-graph.service.js';
9
+ import { createPageDependencyInstanceKey } from '../page-browser-graph/route-instance-key.js';
6
10
  import { buildPreparedRenderOptions } from './route-prepared-options.builder.js';
11
+ import { measureRouteRenderPhase } from '../../../diagnostics/request-pipeline-metrics.js';
7
12
  /**
8
13
  * Owns one route render from normalized module loading through final HTML output.
9
14
  *
@@ -33,28 +38,56 @@ export class RouteRenderOrchestrator {
33
38
  * HTML transformation.
34
39
  */
35
40
  async prepareRenderOptions(routeOptions, adapter) {
36
- const resolvedInputs = await adapter.resolveRouteRenderInputs(routeOptions);
37
- const { Page, HtmlTemplate, Layouts } = resolvedInputs;
38
- const validationErrors = this.ownershipValidationService.validate({
39
- currentIntegrationName: adapter.name,
40
- roots: [
41
- { component: HtmlTemplate, source: 'html-template' },
42
- ...Layouts.map((layout) => ({ component: layout, source: 'layout' })),
43
- { component: Page, source: 'page' },
44
- ],
41
+ const resolvedInputs = await measureRouteRenderPhase('resolve-route-inputs', () => adapter.resolveRouteRenderInputs(routeOptions));
42
+ const finalProps = {
43
+ ...resolvedInputs.props,
44
+ ...(routeOptions.props ?? {}),
45
+ };
46
+ const finalResolvedInputs = {
47
+ ...resolvedInputs,
48
+ props: finalProps,
49
+ };
50
+ const { Page, HtmlTemplate, Layouts } = finalResolvedInputs;
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);
45
61
  });
46
- throwIfOwnershipInvalid(validationErrors);
47
62
  const componentsToResolve = [HtmlTemplate, ...Layouts, Page];
48
- const [{ resolvedDependencies }, pageBrowserGraph] = await Promise.all([
49
- adapter.resolveRouteDependencies({
50
- components: componentsToResolve,
51
- }),
52
- this.pageBrowserGraphService.resolvePageBrowserGraph({
53
- routeFile: routeOptions.file,
54
- integrationName: adapter.name,
55
- collectContribution: async (routeFile) => await adapter.collectPageBrowserGraphContribution(routeFile),
56
- }),
57
- ]);
63
+ const dependencyInstanceKey = createPageDependencyInstanceKey({
64
+ params: routeOptions.params,
65
+ query: routeOptions.query,
66
+ });
67
+ const graphContext = {
68
+ file: routeOptions.file,
69
+ pageModule: finalResolvedInputs.pageModule,
70
+ props: finalProps,
71
+ params: routeOptions.params,
72
+ query: routeOptions.query,
73
+ dependencyInstanceKey,
74
+ };
75
+ const resolvedPageDependencies = await adapter.resolvePageDependencies(graphContext);
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
+ });
90
+ const resolvedPageDependencyComponents = resolvedPageDependencies?.components ?? [];
58
91
  const allDependencies = [
59
92
  ...resolvedDependencies,
60
93
  ...collectUsedIntegrationDependenciesFromGraph(this.appConfig, componentsToResolve, adapter.name),
@@ -67,14 +100,24 @@ export class RouteRenderOrchestrator {
67
100
  buildEagerSsrLazyAssetsFromGraph(this.assetProcessingService, componentsToResolve, adapter.name),
68
101
  ]);
69
102
  allDependencies.push(...globalAssets, ...eagerSsrLazyAssets);
70
- return buildPreparedRenderOptions({
71
- routeOptions,
72
- resolvedInputs,
73
- resolvedDependencies,
74
- allDependencies,
75
- pageBrowserGraph,
76
- appConfig: this.appConfig,
103
+ const sourceDependencyPaths = collectHtmlCacheSourceDependencyPaths({
104
+ routeFile: routeOptions.file,
105
+ processedAssets: allDependencies,
106
+ graphDependencyPaths,
107
+ additionalSourcePaths: collectRenderShellSourcePaths(this.appConfig, componentsToResolve),
77
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
+ };
78
121
  }
79
122
  async resolveDeclaredPageBrowserGraph(input) {
80
123
  return await this.pageBrowserGraphService.resolvePageBrowserGraph(input);
@@ -101,7 +144,7 @@ export class RouteRenderOrchestrator {
101
144
  * Executes the route-render finalization path for already prepared render options.
102
145
  */
103
146
  async executePrepared(renderOptions, adapter) {
104
- const renderExecution = await this.captureHtmlRender(async () => adapter.renderRouteBody(renderOptions));
147
+ const renderExecution = await measureRouteRenderPhase('render-body', () => this.captureHtmlRender(async () => adapter.renderRouteBody(renderOptions)));
105
148
  const unresolvedArtifactInspection = inspectUnresolvedMarkerArtifactHtml(renderExecution.html);
106
149
  const htmlFinalization = adapter.getRouteHtmlFinalization(renderOptions);
107
150
  const hasUnresolvedMarkerHtml = unresolvedArtifactInspection.hasUnresolvedMarkerArtifacts;
@@ -110,27 +153,30 @@ export class RouteRenderOrchestrator {
110
153
  }
111
154
  const canReuseCapturedBody = !hasUnresolvedMarkerHtml && htmlFinalization.finalizeHtml === undefined;
112
155
  if (canReuseCapturedBody) {
113
- const body = await adapter.transformRouteResponse(new Response(renderExecution.body, {
156
+ const responseBody = typeof renderExecution.body === 'string' ? renderExecution.body : renderExecution.html;
157
+ const body = await measureRouteRenderPhase('transform-response', () => adapter.transformRouteResponse(new Response(responseBody, {
114
158
  headers: {
115
159
  'Content-Type': 'text/html',
116
160
  },
117
- }), htmlFinalization.htmlContributions, renderOptions.pagePackage);
161
+ }), htmlFinalization.htmlContributions, renderOptions.pagePackage));
118
162
  return {
119
163
  body,
120
164
  cacheStrategy: renderOptions.cacheStrategy,
165
+ sourceDependencyPaths: renderOptions.sourceDependencyPaths,
121
166
  };
122
167
  }
123
168
  const finalization = htmlFinalization.finalizeHtml
124
169
  ? htmlFinalization.finalizeHtml(unresolvedArtifactInspection.normalizedHtml)
125
170
  : unresolvedArtifactInspection.normalizedHtml;
126
- const body = await adapter.transformRouteResponse(new Response(finalization, {
171
+ const body = await measureRouteRenderPhase('transform-response', () => adapter.transformRouteResponse(new Response(finalization, {
127
172
  headers: {
128
173
  'Content-Type': 'text/html',
129
174
  },
130
- }), htmlFinalization.htmlContributions, renderOptions.pagePackage);
175
+ }), htmlFinalization.htmlContributions, renderOptions.pagePackage));
131
176
  return {
132
177
  body,
133
178
  cacheStrategy: renderOptions.cacheStrategy,
179
+ sourceDependencyPaths: renderOptions.sourceDependencyPaths,
134
180
  };
135
181
  }
136
182
  async captureRenderedBody(body) {
@@ -154,3 +200,16 @@ export class RouteRenderOrchestrator {
154
200
  };
155
201
  }
156
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 [];
@@ -0,0 +1,42 @@
1
+ import type { EcoComponent, EcoComponentConfig, EcoComponentDependencies } from '../../types/public-types.js';
2
+ import type { PageDependenciesResult } from '../../eco/eco.types.js';
3
+ /**
4
+ * Attaches canonical file-backed identity to one component config.
5
+ */
6
+ export declare function attachEcoFileMetadataToConfig(config: EcoComponentConfig, ownerFile: string, integrationName: string): EcoComponentConfig;
7
+ /**
8
+ * Builds a synthetic component config rooted at one file so direct dependency
9
+ * declarations can flow through the shared component dependency collector.
10
+ */
11
+ export declare function createFileScopedDependencyComponent(options: {
12
+ ownerFile: string;
13
+ integrationName: string;
14
+ dependencies: Pick<EcoComponentDependencies, 'scripts' | 'stylesheets' | 'modules'>;
15
+ }): Partial<EcoComponent> | undefined;
16
+ /**
17
+ * Expands one dependency bag into component roots understood by the shared collector.
18
+ */
19
+ export declare function collectFileScopedDependencyComponents(options: {
20
+ ownerFile: string;
21
+ integrationName: string;
22
+ dependencies: EcoComponentDependencies;
23
+ }): Array<EcoComponent | Partial<EcoComponent>>;
24
+ export type CollectComponentConfigFilePathsOptions = {
25
+ additionalPaths?: ReadonlyArray<string>;
26
+ includeLayouts?: boolean;
27
+ };
28
+ /**
29
+ * Walks component configs and collects every resolved identity file path.
30
+ */
31
+ export declare function collectComponentConfigFilePaths(components: ReadonlyArray<EcoComponent | Partial<EcoComponent> | undefined>, options?: CollectComponentConfigFilePathsOptions): Set<string>;
32
+ /**
33
+ * Collects source files that should invalidate a per-instance browser graph entry.
34
+ */
35
+ export declare function collectDependencyWatchPaths(ownerFile: string, components: ReadonlyArray<EcoComponent | Partial<EcoComponent>>): string[];
36
+ /**
37
+ * Separates optional owner metadata from a resolved page dependency bag.
38
+ */
39
+ export declare function splitPageDependenciesResult(result: PageDependenciesResult): {
40
+ dependencies: EcoComponentDependencies;
41
+ ownerFile?: string;
42
+ };
@@ -0,0 +1,94 @@
1
+ import path from 'node:path';
2
+ import { rapidhash } from '../../utils/hash.js';
3
+ import { bindComponentIdentity, getComponentIdentity } from '../../eco/component-identity.js';
4
+ /**
5
+ * Attaches canonical file-backed identity to one component config.
6
+ */
7
+ export function attachEcoFileMetadataToConfig(config, ownerFile, integrationName) {
8
+ return bindComponentIdentity({
9
+ id: getComponentIdentity(config)?.id ?? rapidhash(ownerFile).toString(36),
10
+ file: ownerFile,
11
+ integration: getComponentIdentity(config)?.integration ?? integrationName,
12
+ }, config);
13
+ }
14
+ /**
15
+ * Builds a synthetic component config rooted at one file so direct dependency
16
+ * declarations can flow through the shared component dependency collector.
17
+ */
18
+ export function createFileScopedDependencyComponent(options) {
19
+ const { scripts, stylesheets, modules } = options.dependencies;
20
+ if (!scripts?.length && !stylesheets?.length && !modules?.length) {
21
+ return undefined;
22
+ }
23
+ return {
24
+ config: attachEcoFileMetadataToConfig({ dependencies: { scripts, stylesheets, modules } }, options.ownerFile, options.integrationName),
25
+ };
26
+ }
27
+ /**
28
+ * Expands one dependency bag into component roots understood by the shared collector.
29
+ */
30
+ export function collectFileScopedDependencyComponents(options) {
31
+ const components = [];
32
+ if (options.dependencies.components?.length) {
33
+ components.push(...options.dependencies.components);
34
+ }
35
+ const ownerComponent = createFileScopedDependencyComponent({
36
+ ownerFile: options.ownerFile,
37
+ integrationName: options.integrationName,
38
+ dependencies: options.dependencies,
39
+ });
40
+ if (ownerComponent) {
41
+ components.push(ownerComponent);
42
+ }
43
+ return components;
44
+ }
45
+ /**
46
+ * Walks component configs and collects every resolved identity file path.
47
+ */
48
+ export function collectComponentConfigFilePaths(components, options) {
49
+ const files = new Set();
50
+ const visited = new Set();
51
+ for (const additionalPath of options?.additionalPaths ?? []) {
52
+ files.add(path.resolve(additionalPath));
53
+ }
54
+ const visit = (config) => {
55
+ const file = getComponentIdentity(config)?.file;
56
+ if (!file) {
57
+ return;
58
+ }
59
+ if (!config) {
60
+ return;
61
+ }
62
+ const resolved = path.resolve(file);
63
+ if (visited.has(resolved)) {
64
+ return;
65
+ }
66
+ visited.add(resolved);
67
+ files.add(resolved);
68
+ for (const dependency of config.dependencies?.components ?? []) {
69
+ visit(dependency?.config);
70
+ }
71
+ if (options?.includeLayouts) {
72
+ for (const layout of config.layouts ?? []) {
73
+ visit(layout?.config);
74
+ }
75
+ }
76
+ };
77
+ for (const component of components) {
78
+ visit(component?.config);
79
+ }
80
+ return files;
81
+ }
82
+ /**
83
+ * Collects source files that should invalidate a per-instance browser graph entry.
84
+ */
85
+ export function collectDependencyWatchPaths(ownerFile, components) {
86
+ return Array.from(collectComponentConfigFilePaths(components, { additionalPaths: [ownerFile] }));
87
+ }
88
+ /**
89
+ * Separates optional owner metadata from a resolved page dependency bag.
90
+ */
91
+ export function splitPageDependenciesResult(result) {
92
+ const { ownerFile, ...dependencies } = result;
93
+ return { dependencies, ownerFile };
94
+ }
@@ -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,8 +61,10 @@ 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
+ module: EcoPageFile;
67
68
  Page: EcoPageFile['default'] | EcoPageComponent<any>;
68
69
  getStaticProps?: GetStaticProps<Record<string, unknown>>;
69
70
  getMetadata?: GetMetadata;