@ecopages/core 0.2.0-alpha.25 → 0.2.0-alpha.27

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 (111) hide show
  1. package/README.md +63 -7
  2. package/package.json +4 -47
  3. package/src/adapters/bun/create-app.ts +54 -2
  4. package/src/adapters/bun/hmr-manager.test.ts +0 -2
  5. package/src/adapters/bun/hmr-manager.ts +1 -24
  6. package/src/adapters/bun/server-adapter.ts +30 -4
  7. package/src/adapters/node/node-hmr-manager.test.ts +0 -2
  8. package/src/adapters/node/node-hmr-manager.ts +2 -25
  9. package/src/adapters/shared/explicit-static-render-preparation.ts +58 -0
  10. package/src/adapters/shared/explicit-static-route-matcher.test.ts +6 -6
  11. package/src/adapters/shared/explicit-static-route-matcher.ts +22 -31
  12. package/src/adapters/shared/file-route-middleware-pipeline.test.ts +5 -10
  13. package/src/adapters/shared/file-route-middleware-pipeline.ts +8 -17
  14. package/src/adapters/shared/fs-server-response-factory.test.ts +32 -43
  15. package/src/adapters/shared/fs-server-response-factory.ts +15 -37
  16. package/src/adapters/shared/fs-server-response-matcher.test.ts +65 -39
  17. package/src/adapters/shared/fs-server-response-matcher.ts +94 -43
  18. package/src/adapters/shared/hmr-manager.contract.test.ts +0 -4
  19. package/src/adapters/shared/render-context.ts +3 -3
  20. package/src/adapters/shared/server-adapter.test.ts +53 -0
  21. package/src/adapters/shared/server-adapter.ts +228 -159
  22. package/src/adapters/shared/server-route-handler.test.ts +6 -5
  23. package/src/adapters/shared/server-route-handler.ts +4 -4
  24. package/src/adapters/shared/server-static-builder.test.ts +4 -4
  25. package/src/adapters/shared/server-static-builder.ts +4 -4
  26. package/src/config/README.md +1 -1
  27. package/src/config/config-builder.test.ts +0 -1
  28. package/src/config/config-builder.ts +2 -7
  29. package/src/dev/host-runtime.ts +34 -0
  30. package/src/eco/eco.browser.test.ts +2 -2
  31. package/src/eco/eco.browser.ts +2 -2
  32. package/src/eco/eco.test.ts +6 -6
  33. package/src/eco/eco.ts +12 -12
  34. package/src/eco/eco.types.ts +3 -3
  35. package/src/errors/index.ts +1 -0
  36. package/src/hmr/client/hmr-runtime.ts +4 -2
  37. package/src/hmr/strategies/js-hmr-strategy.test.ts +0 -1
  38. package/src/hmr/strategies/js-hmr-strategy.ts +0 -6
  39. package/src/integrations/ghtml/ghtml-renderer.test.ts +7 -7
  40. package/src/integrations/ghtml/ghtml-renderer.ts +1 -11
  41. package/src/plugins/eco-component-meta-plugin.ts +0 -1
  42. package/src/plugins/integration-plugin.test.ts +9 -14
  43. package/src/plugins/integration-plugin.ts +34 -22
  44. package/src/plugins/processor.ts +17 -0
  45. package/src/route-renderer/GRAPH.md +81 -289
  46. package/src/route-renderer/README.md +67 -105
  47. package/src/route-renderer/orchestration/component-render-context.ts +45 -38
  48. package/src/route-renderer/orchestration/declared-ownership-graph.ts +62 -0
  49. package/src/route-renderer/orchestration/foreign-subtree-execution.service.ts +383 -0
  50. package/src/route-renderer/orchestration/integration-renderer.test.ts +118 -121
  51. package/src/route-renderer/orchestration/integration-renderer.ts +362 -403
  52. package/src/route-renderer/orchestration/ownership-planning.service.ts +97 -0
  53. package/src/route-renderer/orchestration/ownership-validation.service.ts +76 -0
  54. package/src/route-renderer/orchestration/processed-asset-dedupe.ts +1 -1
  55. package/src/route-renderer/orchestration/{queued-boundary-runtime.service.test.ts → queued-foreign-subtree-resolution.service.test.ts} +76 -71
  56. package/src/route-renderer/orchestration/{queued-boundary-runtime.service.ts → queued-foreign-subtree-resolution.service.ts} +68 -63
  57. package/src/route-renderer/orchestration/render-output.utils.ts +21 -13
  58. package/src/route-renderer/orchestration/{render-preparation.service.test.ts → route-render-orchestrator.prepare-render-options.test.ts} +160 -85
  59. package/src/route-renderer/orchestration/route-render-orchestrator.test.ts +265 -0
  60. package/src/route-renderer/orchestration/{render-preparation.service.ts → route-render-orchestrator.ts} +244 -160
  61. package/src/route-renderer/page-loading/component-dependency-collection.ts +9 -3
  62. package/src/route-renderer/page-loading/declared-asset-collection.ts +2 -5
  63. package/src/route-renderer/page-loading/dependency-resolver.test.ts +107 -11
  64. package/src/route-renderer/page-loading/dependency-resolver.ts +6 -12
  65. package/src/route-renderer/page-loading/ecopages-virtual-imports.ts +1 -1
  66. package/src/route-renderer/page-loading/lazy-entry-collection.ts +1 -1
  67. package/src/route-renderer/page-loading/lazy-trigger-planning.ts +1 -1
  68. package/src/route-renderer/page-loading/module-declaration-aggregation.ts +1 -1
  69. package/src/route-renderer/page-loading/module-declaration-scripts.ts +1 -1
  70. package/src/route-renderer/page-loading/page-dependency-bundling.ts +105 -66
  71. package/src/route-renderer/route-renderer.ts +28 -31
  72. package/src/router/README.md +16 -19
  73. package/src/router/server/route-registry.test.ts +176 -0
  74. package/src/router/server/route-registry.ts +382 -0
  75. package/src/services/README.md +1 -2
  76. package/src/services/assets/asset-processing-service/asset-dependency-keys.ts +1 -1
  77. package/src/services/assets/asset-processing-service/asset-processing.service.test.ts +1 -4
  78. package/src/services/assets/asset-processing-service/asset-processing.service.ts +1 -2
  79. package/src/services/assets/asset-processing-service/assets.types.ts +3 -0
  80. package/src/services/assets/asset-processing-service/grouped-content-bundles.ts +1 -1
  81. package/src/services/assets/asset-processing-service/index.ts +1 -0
  82. package/src/{route-renderer/orchestration/page-packaging.service.test.ts → services/assets/asset-processing-service/page-package.test.ts} +38 -14
  83. package/src/services/assets/asset-processing-service/page-package.ts +93 -0
  84. package/src/services/assets/asset-processing-service/processors/base/base-script-processor.ts +4 -5
  85. package/src/services/assets/asset-processing-service/processors/script/content-script.processor.test.ts +13 -10
  86. package/src/services/assets/asset-processing-service/processors/script/content-script.processor.ts +3 -0
  87. package/src/services/assets/asset-processing-service/processors/script/file-script.processor.ts +6 -0
  88. package/src/services/assets/asset-processing-service/processors/script/node-module-script.processor.ts +2 -0
  89. package/src/services/assets/asset-processing-service/processors/stylesheet/content-stylesheet.processor.ts +1 -0
  90. package/src/services/assets/asset-processing-service/processors/stylesheet/file-stylesheet.processor.ts +2 -0
  91. package/src/services/assets/asset-processing-service/ungrouped-dependency-processing.ts +1 -1
  92. package/src/services/html/html-transformer.service.test.ts +1 -4
  93. package/src/services/module-loading/app-server-module-transpiler.service.ts +1 -3
  94. package/src/services/module-loading/node-bootstrap-plugin.ts +17 -3
  95. package/src/services/module-loading/page-module-import.service.ts +0 -1
  96. package/src/services/module-loading/source-module-support.ts +1 -1
  97. package/src/static-site-generator/static-site-generator.test.ts +124 -32
  98. package/src/static-site-generator/static-site-generator.ts +168 -185
  99. package/src/types/internal-types.ts +13 -12
  100. package/src/types/public-types.ts +55 -39
  101. package/src/watchers/project-watcher.test-helpers.ts +4 -3
  102. package/src/route-renderer/orchestration/boundary-planning.service.ts +0 -146
  103. package/src/route-renderer/orchestration/page-packaging.service.ts +0 -85
  104. package/src/route-renderer/orchestration/render-execution.service.test.ts +0 -196
  105. package/src/route-renderer/orchestration/render-execution.service.ts +0 -182
  106. package/src/route-renderer/orchestration/route-shell-composer.service.ts +0 -162
  107. package/src/router/server/fs-router-scanner.test.ts +0 -83
  108. package/src/router/server/fs-router-scanner.ts +0 -224
  109. package/src/router/server/fs-router.test.ts +0 -214
  110. package/src/router/server/fs-router.ts +0 -122
  111. package/src/services/runtime-state/runtime-specifier-registry.service.ts +0 -96
@@ -3,66 +3,57 @@ import path from 'node:path';
3
3
  import type { EcoPagesAppConfig } from '../../types/internal-types.ts';
4
4
  import type {
5
5
  ComponentRenderResult,
6
+ DependencyAttributes,
6
7
  EcoComponent,
7
8
  EcoComponentConfig,
8
- DependencyAttributes,
9
9
  EcoPageComponent,
10
10
  EcoPageFile,
11
- EcoPagesElement,
12
- GetMetadata,
13
- GetStaticProps,
14
11
  HtmlTemplateProps,
15
12
  IntegrationRendererRenderOptions,
16
- PageProps,
13
+ PageBrowserGraphResult,
17
14
  PageMetadataProps,
15
+ PageProps,
18
16
  ResolvedLazyTrigger,
17
+ RouteRendererBody,
19
18
  RouteRendererOptions,
19
+ RouteRenderResult,
20
20
  } from '../../types/public-types.ts';
21
21
  import {
22
22
  type AssetProcessingService,
23
23
  AssetFactory,
24
+ createPagePackage,
24
25
  type ProcessedAsset,
25
26
  } from '../../services/assets/asset-processing-service/index.ts';
26
27
  import { buildGlobalInjectorBootstrapContent, buildGlobalInjectorMapScript } from '../../eco/global-injector-map.ts';
27
28
  import { LocalsAccessError } from '../../errors/locals-access-error.ts';
28
- import { BoundaryPlanningService } from './boundary-planning.service.ts';
29
- import { PagePackagingService } from './page-packaging.service.ts';
29
+ import { inspectUnresolvedMarkerArtifactHtml } from './render-output.utils.ts';
30
+ import { OwnershipValidationService } from './ownership-validation.service.ts';
31
+ import { OwnershipPlanningService } from './ownership-planning.service.ts';
30
32
  import { dedupeProcessedAssets } from './processed-asset-dedupe.ts';
31
33
 
32
- type ResolvedPageModule = {
34
+ export type RouteRenderOrchestratorResolvedInputs = {
33
35
  Page: EcoPageFile['default'] | EcoPageComponent<any>;
34
- getStaticProps?: GetStaticProps<Record<string, unknown>>;
35
- getMetadata?: GetMetadata;
36
+ HtmlTemplate: EcoComponent<HtmlTemplateProps>;
37
+ Layout?: EcoComponent;
38
+ props: Record<string, unknown>;
39
+ metadata: PageMetadataProps;
36
40
  integrationSpecificProps: Record<string, unknown>;
37
41
  };
38
42
 
39
- export interface RenderPreparationCallbacks {
40
- resolvePageModule(file: string): Promise<ResolvedPageModule>;
41
- getHtmlTemplate(): Promise<EcoComponent<HtmlTemplateProps>>;
42
- resolvePageData(
43
- pageModule: {
44
- getStaticProps?: GetStaticProps<Record<string, unknown>>;
45
- getMetadata?: GetMetadata;
46
- },
47
- routeOptions: RouteRendererOptions,
48
- ): Promise<{ props: Record<string, unknown>; metadata: PageMetadataProps }>;
49
- resolveDependencies(components: (EcoComponent | Partial<EcoComponent>)[]): Promise<ProcessedAsset[]>;
50
- buildRouteRenderAssets(file: string): Promise<ProcessedAsset[]> | undefined;
51
- shouldRenderPageComponent(input: {
52
- Page: EcoComponent;
53
- Layout?: EcoComponent;
54
- options: RouteRendererOptions;
55
- }): boolean;
56
- renderPageComponent(input: {
57
- component: EcoComponent;
58
- props: Record<string, unknown>;
59
- }): Promise<ComponentRenderResult>;
60
- }
43
+ export type RouteRenderOrchestratorResolvedAssets = {
44
+ resolvedDependencies: ProcessedAsset[];
45
+ pageBrowserGraph?: PageBrowserGraphResult;
46
+ };
61
47
 
62
- export interface RenderPreparationServiceDependencies {
63
- boundaryPlanningService?: BoundaryPlanningService;
64
- pagePackagingService?: PagePackagingService;
65
- }
48
+ /**
49
+ * Structural HTML work applied after the route body has been fully resolved.
50
+ *
51
+ * The shared route flow only needs to know whether a post-render HTML step
52
+ * exists. When `finalizeHtml` is absent, the captured body can be reused as-is.
53
+ */
54
+ export type RouteHtmlFinalization = {
55
+ finalizeHtml?(html: string): string;
56
+ };
66
57
 
67
58
  function createPageLocalsProxy(filePath: string): Record<string, never> {
68
59
  const errorMessage = `[ecopages] Request locals are only available during request-time rendering with cache: 'dynamic'. Page: ${filePath}. If you meant to use locals here, set cache: 'dynamic' and provide locals from route middleware/handlers.`;
@@ -95,114 +86,158 @@ function createPageLocalsProxy(filePath: string): Record<string, never> {
95
86
  );
96
87
  }
97
88
 
89
+ export interface RouteRenderOrchestratorAdapter<C> {
90
+ /**
91
+ * Name of the owning Integration for the current route render.
92
+ */
93
+ readonly name: string;
94
+ /**
95
+ * Loads the Integration-owned route inputs needed for one Page render.
96
+ */
97
+ resolveRouteRenderInputs(routeOptions: RouteRendererOptions): Promise<RouteRenderOrchestratorResolvedInputs>;
98
+ /**
99
+ * Resolves route-owned assets needed before Integration rendering starts.
100
+ */
101
+ resolveRouteAssets(input: {
102
+ routeOptions: RouteRendererOptions;
103
+ components: (EcoComponent | Partial<EcoComponent>)[];
104
+ }): Promise<RouteRenderOrchestratorResolvedAssets>;
105
+ /**
106
+ * Resolves the optional page-root render through the foreign-child-aware component contract.
107
+ */
108
+ resolveRoutePageComponentRender(input: {
109
+ Page: EcoComponent;
110
+ Layout?: EcoComponent;
111
+ props: Record<string, unknown>;
112
+ routeOptions: RouteRendererOptions;
113
+ }): Promise<ComponentRenderResult | undefined>;
114
+ /**
115
+ * Executes the Integration-specific route render.
116
+ */
117
+ renderRouteBody(renderOptions: IntegrationRendererRenderOptions<C>): Promise<RouteRendererBody>;
118
+ /**
119
+ * Returns the structural Html finalization plan for one prepared route render.
120
+ */
121
+ getRouteHtmlFinalization(renderOptions: IntegrationRendererRenderOptions<C>): RouteHtmlFinalization;
122
+ /**
123
+ * Runs SSR-policy response transformation and returns the body value exposed to callers.
124
+ */
125
+ transformRouteResponse(response: Response): Promise<RouteRendererBody>;
126
+ }
127
+
128
+ /**
129
+ * Captured route-render output in both replayable body and string HTML forms.
130
+ */
131
+ export interface CapturedHtmlRenderResult {
132
+ body: RouteRendererBody;
133
+ html: string;
134
+ }
135
+
136
+ /**
137
+ * Optional app-scoped collaborators used by the route render orchestrator.
138
+ */
139
+ export interface RouteRenderOrchestratorDependencies {
140
+ ownershipPlanningService?: OwnershipPlanningService;
141
+ ownershipValidationService?: OwnershipValidationService;
142
+ }
143
+
98
144
  /**
99
- * Prepares the normalized render inputs consumed by `IntegrationRenderer.execute()`.
145
+ * Owns one route render from normalized module loading through final HTML output.
100
146
  *
101
- * This service owns the orchestration that happens before the main HTML render:
102
- * page module resolution, data loading, dependency aggregation, page-root
103
- * component artifact capture, lazy trigger bootstrap generation, and request
104
- * locals policy.
147
+ * This orchestrator keeps route rendering as one app-scoped unit while still
148
+ * delegating integration-specific behavior through the adapter seam. It owns
149
+ * route-root validation, dependency aggregation, page package creation, and the
150
+ * final HTML/body handling that happens after the integration render returns.
105
151
  */
106
- export class RenderPreparationService {
107
- private appConfig: EcoPagesAppConfig;
108
- private assetProcessingService: AssetProcessingService;
109
- private readonly boundaryPlanningService: BoundaryPlanningService;
110
- private readonly pagePackagingService: PagePackagingService;
152
+ export class RouteRenderOrchestrator {
153
+ private readonly appConfig: EcoPagesAppConfig;
154
+ private readonly assetProcessingService: AssetProcessingService;
155
+ private readonly ownershipPlanningService: OwnershipPlanningService;
156
+ private readonly ownershipValidationService: OwnershipValidationService;
111
157
 
112
- /**
113
- * Creates the render-preparation orchestrator for one app instance.
114
- *
115
- * @remarks
116
- * The service is app-scoped because it depends on finalized config defaults and
117
- * the app-owned asset-processing pipeline while remaining renderer-agnostic.
118
- */
119
158
  constructor(
120
159
  appConfig: EcoPagesAppConfig,
121
160
  assetProcessingService: AssetProcessingService,
122
- dependencies: RenderPreparationServiceDependencies = {},
161
+ dependencies: RouteRenderOrchestratorDependencies = {},
123
162
  ) {
124
163
  this.appConfig = appConfig;
125
164
  this.assetProcessingService = assetProcessingService;
126
- this.boundaryPlanningService = dependencies.boundaryPlanningService ?? new BoundaryPlanningService(appConfig);
127
- this.pagePackagingService = dependencies.pagePackagingService ?? new PagePackagingService();
165
+ this.ownershipPlanningService = dependencies.ownershipPlanningService ?? new OwnershipPlanningService();
166
+ this.ownershipValidationService =
167
+ dependencies.ownershipValidationService ?? new OwnershipValidationService(appConfig);
128
168
  }
129
169
 
130
170
  /**
131
- * Builds the final render options object used by the integration-specific
132
- * renderer.
171
+ * Builds normalized route render options before the integration render runs.
133
172
  *
134
- * The returned object contains normalized page data, processed dependency
135
- * state, component render artifacts, and the locals contract expected by the
136
- * rest of the pipeline.
137
- *
138
- * @typeParam C Integration render output element type.
139
- * @param routeOptions Route-level render inputs.
140
- * @param currentIntegrationName Active integration name for this preparation pass.
141
- * @param callbacks Renderer-specific hooks used during preparation.
142
- * @returns Normalized render options.
173
+ * This preparation step validates route-root ownership, resolves page data,
174
+ * collects processed assets, captures optional page-root render metadata, and
175
+ * produces the page package consumed by downstream HTML transformation.
143
176
  */
144
- async prepare<C = EcoPagesElement>(
177
+ async prepareRenderOptions<C = unknown>(
145
178
  routeOptions: RouteRendererOptions,
146
- currentIntegrationName: string,
147
- callbacks: RenderPreparationCallbacks,
179
+ adapter: RouteRenderOrchestratorAdapter<C>,
148
180
  ): Promise<IntegrationRendererRenderOptions<C>> {
149
- const pageModule = await callbacks.resolvePageModule(routeOptions.file);
150
- const { Page, integrationSpecificProps } = pageModule;
151
- const HtmlTemplate = await callbacks.getHtmlTemplate();
152
- const { props, metadata } = await callbacks.resolvePageData(pageModule, routeOptions);
153
- const Layout = Page.config?.layout;
154
- const boundaryPlan = this.boundaryPlanningService.buildPlan({
181
+ const resolvedInputs = await adapter.resolveRouteRenderInputs(routeOptions);
182
+ const { Page, HtmlTemplate, Layout, props, metadata, integrationSpecificProps } = resolvedInputs;
183
+ const validationErrors = this.ownershipValidationService.validate({
184
+ currentIntegrationName: adapter.name,
185
+ roots: [
186
+ { component: HtmlTemplate as EcoComponent, source: 'html-template' },
187
+ ...(Layout ? [{ component: Layout as EcoComponent, source: 'layout' as const }] : []),
188
+ { component: Page as EcoComponent, source: 'page' },
189
+ ],
190
+ });
191
+ const ownershipPlan = this.ownershipPlanningService.buildPlan({
155
192
  routeFile: routeOptions.file,
156
- currentIntegrationName,
157
- HtmlTemplate,
193
+ currentIntegrationName: adapter.name,
194
+ HtmlTemplate: HtmlTemplate as EcoComponent,
158
195
  Layout,
159
196
  Page: Page as EcoComponent,
197
+ validationErrors,
160
198
  });
161
199
 
162
200
  const componentsToResolve = Layout ? [HtmlTemplate, Layout, Page] : [HtmlTemplate, Page];
163
- const resolvedDependencies = await callbacks.resolveDependencies(componentsToResolve);
164
- const usedIntegrationDependencies = this.collectUsedIntegrationDependencies(
165
- componentsToResolve,
166
- currentIntegrationName,
167
- );
168
- const pageDeps = (await callbacks.buildRouteRenderAssets(routeOptions.file)) || [];
169
- const allDependencies = [...resolvedDependencies, ...usedIntegrationDependencies, ...pageDeps];
170
-
171
- let componentRender: ComponentRenderResult | undefined;
172
- if (callbacks.shouldRenderPageComponent({ Page, Layout, options: routeOptions })) {
173
- const pageRootRender = await this.renderPageRoot({
174
- Page: Page as EcoComponent,
175
- props,
176
- routeOptions,
177
- callbacks,
178
- });
179
- componentRender = pageRootRender.componentRender;
180
-
181
- if (componentRender.assets?.length) {
182
- allDependencies.push(...componentRender.assets);
183
- }
201
+ const { resolvedDependencies, pageBrowserGraph } = await adapter.resolveRouteAssets({
202
+ routeOptions,
203
+ components: componentsToResolve,
204
+ });
205
+ const usedIntegrationDependencies = this.collectUsedIntegrationDependencies(componentsToResolve, adapter.name);
206
+ const allDependencies = [
207
+ ...resolvedDependencies,
208
+ ...usedIntegrationDependencies,
209
+ ...(pageBrowserGraph?.assets ?? []),
210
+ ];
211
+
212
+ const componentRender = await adapter.resolveRoutePageComponentRender({
213
+ Page: Page as EcoComponent,
214
+ Layout,
215
+ props,
216
+ routeOptions,
217
+ });
218
+
219
+ if (componentRender?.assets?.length) {
220
+ allDependencies.push(...componentRender.assets);
184
221
  }
185
222
 
186
223
  const triggers = this.collectResolvedTriggers(componentsToResolve);
187
224
  if (triggers.length > 0) {
188
- const globalAssets = await this.buildGlobalInjectorAssets(triggers, currentIntegrationName);
225
+ const globalAssets = await this.buildGlobalInjectorAssets(triggers, adapter.name);
189
226
  allDependencies.push(...globalAssets);
190
227
  }
191
228
 
192
- const eagerSsrLazyAssets = await this.buildEagerSsrLazyAssets(componentsToResolve, currentIntegrationName);
229
+ const eagerSsrLazyAssets = await this.buildEagerSsrLazyAssets(componentsToResolve, adapter.name);
193
230
  if (eagerSsrLazyAssets.length > 0) {
194
231
  allDependencies.push(...eagerSsrLazyAssets);
195
232
  }
196
233
 
197
234
  const dedupedDependencies = dedupeProcessedAssets(allDependencies);
198
- const pagePackage = this.pagePackagingService.createPagePackage(dedupedDependencies);
199
-
235
+ const pagePackage = createPagePackage(dedupedDependencies);
200
236
  const pageProps = {
201
237
  ...props,
202
238
  params: routeOptions.params || {},
203
239
  query: routeOptions.query || {},
204
240
  };
205
-
206
241
  const cacheStrategy = (Page as EcoPageComponent<any>).cache;
207
242
  const defaultCacheStrategy = this.appConfig.cache?.defaultStrategy ?? 'static';
208
243
  const effectiveCacheStrategy = cacheStrategy ?? defaultCacheStrategy;
@@ -229,7 +264,7 @@ export class RenderPreparationService {
229
264
  locals,
230
265
  pageLocals,
231
266
  cacheStrategy,
232
- boundaryPlan,
267
+ ownershipPlan,
233
268
  };
234
269
 
235
270
  return {
@@ -239,15 +274,107 @@ export class RenderPreparationService {
239
274
  }
240
275
 
241
276
  /**
242
- * Collects resolved lazy trigger metadata from the component tree.
243
- *
244
- * Traversal is depth-first and deduplicated by component identity so shared
245
- * component dependencies do not emit duplicate trigger sets.
246
- *
247
- * @param components Root component set.
248
- * @param seen Internal visited set for shared graphs.
249
- * @returns All resolved lazy triggers reachable from the root set.
277
+ * Captures one route render body as HTML while preserving a replayable body value.
278
+ */
279
+ async captureHtmlRender(render: () => Promise<RouteRendererBody>): Promise<CapturedHtmlRenderResult> {
280
+ const renderedBody = await render();
281
+ const capturedRender = await this.captureRenderedBody(renderedBody);
282
+
283
+ return {
284
+ body: capturedRender.body,
285
+ html: capturedRender.html,
286
+ };
287
+ }
288
+
289
+ /**
290
+ * Executes the full route-render flow and returns the final body plus cache strategy.
291
+ */
292
+ async execute<C = unknown>(
293
+ options: RouteRendererOptions,
294
+ adapter: RouteRenderOrchestratorAdapter<C>,
295
+ ): Promise<RouteRenderResult> {
296
+ const renderOptions = await this.prepareRenderOptions(options, adapter);
297
+ return this.executePrepared(renderOptions, adapter);
298
+ }
299
+
300
+ /**
301
+ * Executes the route-render finalization path for already prepared render options.
250
302
  */
303
+ async executePrepared<C = unknown>(
304
+ renderOptions: IntegrationRendererRenderOptions<C>,
305
+ adapter: RouteRenderOrchestratorAdapter<C>,
306
+ ): Promise<RouteRenderResult> {
307
+ const renderExecution = await this.captureHtmlRender(async () => adapter.renderRouteBody(renderOptions));
308
+ const unresolvedArtifactInspection = inspectUnresolvedMarkerArtifactHtml(renderExecution.html);
309
+ const htmlFinalization = adapter.getRouteHtmlFinalization(renderOptions);
310
+ const hasUnresolvedMarkerHtml = unresolvedArtifactInspection.hasUnresolvedMarkerArtifacts;
311
+
312
+ if (hasUnresolvedMarkerHtml) {
313
+ throw new Error(
314
+ '[ecopages] Route render returned unresolved eco-marker artifact HTML. Full-route unresolved-marker fallback has been removed; resolve mixed foreign children inside renderComponentWithForeignChildren().',
315
+ );
316
+ }
317
+
318
+ const canReuseCapturedBody = !hasUnresolvedMarkerHtml && htmlFinalization.finalizeHtml === undefined;
319
+
320
+ if (canReuseCapturedBody) {
321
+ const body = await adapter.transformRouteResponse(
322
+ new Response(renderExecution.body as BodyInit, {
323
+ headers: {
324
+ 'Content-Type': 'text/html',
325
+ },
326
+ }),
327
+ );
328
+
329
+ return {
330
+ body,
331
+ cacheStrategy: renderOptions.cacheStrategy,
332
+ };
333
+ }
334
+
335
+ const finalization = htmlFinalization.finalizeHtml
336
+ ? htmlFinalization.finalizeHtml(unresolvedArtifactInspection.normalizedHtml)
337
+ : unresolvedArtifactInspection.normalizedHtml;
338
+
339
+ const body = await adapter.transformRouteResponse(
340
+ new Response(finalization, {
341
+ headers: {
342
+ 'Content-Type': 'text/html',
343
+ },
344
+ }),
345
+ );
346
+
347
+ return {
348
+ body,
349
+ cacheStrategy: renderOptions.cacheStrategy,
350
+ };
351
+ }
352
+
353
+ private async captureRenderedBody(body: RouteRendererBody): Promise<{ body: RouteRendererBody; html: string }> {
354
+ const response = new Response(body as BodyInit);
355
+
356
+ if (typeof body === 'string') {
357
+ return {
358
+ body,
359
+ html: await response.text(),
360
+ };
361
+ }
362
+
363
+ if (!response.body) {
364
+ return {
365
+ body,
366
+ html: await response.text(),
367
+ };
368
+ }
369
+
370
+ const [capturedBody, replayBody] = response.body.tee();
371
+
372
+ return {
373
+ body: replayBody,
374
+ html: await new Response(capturedBody).text(),
375
+ };
376
+ }
377
+
251
378
  private collectResolvedTriggers(
252
379
  components: (EcoComponent | Partial<EcoComponent>)[],
253
380
  seen = new Set<object>(),
@@ -275,13 +402,6 @@ export class RenderPreparationService {
275
402
  return triggers;
276
403
  }
277
404
 
278
- /**
279
- * Collects global integration dependencies used by nested components belonging
280
- * to integrations other than the current renderer.
281
- *
282
- * @param components Root component set.
283
- * @returns Processed integration dependencies contributed by nested integrations.
284
- */
285
405
  private collectUsedIntegrationDependencies(
286
406
  components: (EcoComponent | Partial<EcoComponent>)[],
287
407
  currentIntegrationName: string,
@@ -307,13 +427,6 @@ export class RenderPreparationService {
307
427
  return dependencies;
308
428
  }
309
429
 
310
- /**
311
- * Discovers integration names referenced by the component dependency graph.
312
- *
313
- * @param components Root component set.
314
- * @param seen Internal visited set for shared graphs.
315
- * @returns Set of integration names found in the graph.
316
- */
317
430
  private collectIntegrationNames(
318
431
  components: (EcoComponent | Partial<EcoComponent>)[],
319
432
  seen = new Set<object>(),
@@ -348,38 +461,6 @@ export class RenderPreparationService {
348
461
  return integrationNames;
349
462
  }
350
463
 
351
- /**
352
- * Renders the page root through the component-level render contract so any
353
- * integration-specific assets and root attributes are available before the main
354
- * document render.
355
- *
356
- * @param input Page root render inputs.
357
- * @returns Structured component render result.
358
- */
359
- private async renderPageRoot(input: {
360
- Page: EcoComponent;
361
- props: Record<string, unknown>;
362
- routeOptions: RouteRendererOptions;
363
- callbacks: RenderPreparationCallbacks;
364
- }): Promise<{ componentRender: ComponentRenderResult }> {
365
- return {
366
- componentRender: await input.callbacks.renderPageComponent({
367
- component: input.Page,
368
- props: {
369
- ...input.props,
370
- params: input.routeOptions.params || {},
371
- query: input.routeOptions.query || {},
372
- },
373
- }),
374
- };
375
- }
376
-
377
- /**
378
- * Builds the runtime assets needed to bootstrap global lazy trigger execution.
379
- *
380
- * @param triggers Fully resolved lazy trigger definitions.
381
- * @returns Processed assets that should be merged into the final dependency set.
382
- */
383
464
  private async buildGlobalInjectorAssets(
384
465
  triggers: ResolvedLazyTrigger[],
385
466
  currentIntegrationName: string,
@@ -403,7 +484,10 @@ export class RenderPreparationService {
403
484
  bundle: true,
404
485
  });
405
486
 
406
- return this.assetProcessingService.processDependencies([mapScript, bootstrapInlineScript], currentIntegrationName);
487
+ return this.assetProcessingService.processDependencies(
488
+ [mapScript, bootstrapInlineScript],
489
+ currentIntegrationName,
490
+ );
407
491
  }
408
492
 
409
493
  private async buildEagerSsrLazyAssets(
@@ -81,8 +81,14 @@ type CollectComponentDependenciesOptions = {
81
81
  export function collectComponentDependencies(
82
82
  options: CollectComponentDependenciesOptions,
83
83
  ): CollectedComponentDependencies {
84
- const { components, integrationName, resolveLazyScripts, createEcopagesJsxLazyEntryName, isEcopagesJsxIntegration, errors } =
85
- options;
84
+ const {
85
+ components,
86
+ integrationName,
87
+ resolveLazyScripts,
88
+ createEcopagesJsxLazyEntryName,
89
+ isEcopagesJsxIntegration,
90
+ errors,
91
+ } = options;
86
92
  const dependencies: AssetDefinition[] = [];
87
93
  const lazyScriptsByConfig = new Map<NonNullable<EcoComponent['config']>, Map<string, LazyGroup>>();
88
94
  const lazyDependencyKeys = new Set<string>();
@@ -193,4 +199,4 @@ export function collectComponentDependencies(
193
199
  dependencies,
194
200
  lazyScriptsByConfig,
195
201
  };
196
- }
202
+ }
@@ -1,7 +1,4 @@
1
- import type {
2
- EcoComponentScriptEntry,
3
- EcoComponentStylesheetEntry,
4
- } from '../../types/public-types.ts';
1
+ import type { EcoComponentScriptEntry, EcoComponentStylesheetEntry } from '../../types/public-types.ts';
5
2
  import type { AssetDefinition } from '../../services/assets/asset-processing-service/index.ts';
6
3
  import { AssetFactory } from '../../services/assets/asset-processing-service/index.ts';
7
4
 
@@ -153,4 +150,4 @@ export function collectDeclaredAssetEntries(options: CollectDeclaredAssetEntries
153
150
  }),
154
151
  );
155
152
  }
156
- }
153
+ }