@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
@@ -8,16 +8,12 @@ import type { EcoPagesAppConfig, IHmrManager } from '../../types/internal-types.
8
8
  import type {
9
9
  ComponentRenderInput,
10
10
  ComponentRenderResult,
11
- BoundaryRenderPayload,
11
+ ForeignSubtreeRenderPayload,
12
12
  EcoComponent,
13
13
  EcoComponentDependencies,
14
14
  EcoFunctionComponent,
15
- EcoPageComponent,
16
15
  EcoPageFile,
17
16
  EcoPagesElement,
18
- GetMetadata,
19
- GetMetadataContext,
20
- GetStaticProps,
21
17
  BaseIntegrationContext,
22
18
  HtmlTemplateProps,
23
19
  IntegrationRendererRenderOptions,
@@ -28,6 +24,7 @@ import type {
28
24
  } from '../../types/public-types.ts';
29
25
  import {
30
26
  type AssetProcessingService,
27
+ createPagePackage,
31
28
  type ProcessedAsset,
32
29
  } from '../../services/assets/asset-processing-service/index.ts';
33
30
  import { HtmlTransformerService } from '../../services/html/html-transformer.service.ts';
@@ -35,23 +32,20 @@ import { invariant } from '../../utils/invariant.ts';
35
32
  import { HttpError } from '../../errors/http-error.ts';
36
33
  import { DependencyResolverService } from '../page-loading/dependency-resolver.ts';
37
34
  import { PageModuleLoaderService } from '../page-loading/page-module-loader.ts';
38
- import { PagePackagingService } from './page-packaging.service.ts';
39
- import { RenderExecutionService } from './render-execution.service.ts';
40
- import { RenderPreparationService } from './render-preparation.service.ts';
41
- import { RouteShellComposer } from './route-shell-composer.service.ts';
42
- import type { ComponentBoundaryRuntime } from './component-render-context.ts';
43
- import { normalizeBoundaryArtifactHtml } from './render-output.utils.ts';
44
- import { getComponentRenderContext, runWithComponentRenderContext } from './component-render-context.ts';
35
+ import { OwnershipValidationService } from './ownership-validation.service.ts';
45
36
  import {
46
- QueuedBoundaryRuntimeService,
47
- type QueuedBoundaryResolution,
48
- type QueuedBoundaryRuntimeContext,
49
- } from './queued-boundary-runtime.service.ts';
50
-
51
- type BoundaryRenderDecisionInput = {
52
- currentIntegration: string;
53
- targetIntegration?: string;
54
- };
37
+ type RouteHtmlFinalization,
38
+ RouteRenderOrchestrator,
39
+ type RouteRenderOrchestratorAdapter,
40
+ type RouteRenderOrchestratorResolvedInputs,
41
+ } from './route-render-orchestrator.ts';
42
+ import type { ForeignChildRuntime } from './component-render-context.ts';
43
+ import { normalizeUnresolvedMarkerArtifactHtml } from './render-output.utils.ts';
44
+ import {
45
+ ForeignSubtreeExecutionService,
46
+ type ForeignSubtreeExecutionOwningRenderer,
47
+ } from './foreign-subtree-execution.service.ts';
48
+ import { type QueuedForeignSubtreeResolutionContext } from './queued-foreign-subtree-resolution.service.ts';
55
49
 
56
50
  /**
57
51
  * Controls how one route module is loaded outside the normal render path.
@@ -91,11 +85,8 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
91
85
  protected runtimeOrigin: string;
92
86
  protected dependencyResolverService: DependencyResolverService;
93
87
  protected pageModuleLoaderService: PageModuleLoaderService;
94
- protected renderPreparationService: RenderPreparationService;
95
- protected renderExecutionService: RenderExecutionService;
96
- protected pagePackagingService: PagePackagingService;
97
- protected readonly routeShellComposer = new RouteShellComposer();
98
- protected readonly queuedBoundaryRuntimeService = new QueuedBoundaryRuntimeService();
88
+ protected routeRenderOrchestrator: RouteRenderOrchestrator;
89
+ protected readonly foreignSubtreeExecutionService = new ForeignSubtreeExecutionService();
99
90
 
100
91
  protected DOC_TYPE = '<!DOCTYPE html>';
101
92
 
@@ -112,7 +103,7 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
112
103
  }
113
104
 
114
105
  /**
115
- * Reads the execution-scoped foreign renderer cache from one boundary input.
106
+ * Reads the execution-scoped owning-renderer cache from one render input.
116
107
  *
117
108
  * Shared page/layout/document shell helpers pass one cache through
118
109
  * `integrationContext` so repeated delegation to the same foreign integration
@@ -121,10 +112,10 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
121
112
  * stored on the renderer, which avoids leaking mutable integration state across
122
113
  * requests while still preventing redundant renderer initialization.
123
114
  *
124
- * @param integrationContext - Optional boundary context carried with one render input.
115
+ * @param integrationContext - Optional render context carried with one render input.
125
116
  * @returns The current execution cache when present.
126
117
  */
127
- private getBoundaryRendererCache(
118
+ private getOwningRendererCache(
128
119
  integrationContext?: BaseIntegrationContext,
129
120
  ): Map<string, IntegrationRenderer<any>> | undefined {
130
121
  if (integrationContext?.rendererCache instanceof Map) {
@@ -134,7 +125,7 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
134
125
  return undefined;
135
126
  }
136
127
 
137
- private getRegisteredBoundaryOwner(component: EcoComponent): string | undefined {
128
+ private getForeignOwnerIntegrationName(component: EcoComponent): string | undefined {
138
129
  const integrationName = component.config?.integration ?? component.config?.__eco?.integration;
139
130
  if (!integrationName || integrationName === this.name) {
140
131
  return undefined;
@@ -146,18 +137,18 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
146
137
  }
147
138
 
148
139
  /**
149
- * Attaches an execution-scoped foreign renderer cache to one boundary input.
140
+ * Attaches an execution-scoped owning-renderer cache to one render input.
150
141
  *
151
142
  * Foreign-owned page, layout, or document shells may delegate several times in
152
143
  * the same render flow. Threading the cache through `integrationContext`
153
- * preserves renderer reuse without changing the public boundary input contract.
144
+ * preserves renderer reuse without changing the public render input contract.
154
145
  * Existing integration-specific context is preserved and augmented.
155
146
  *
156
- * @param input - Original boundary render input.
147
+ * @param input - Original render input.
157
148
  * @param rendererCache - Execution-scoped renderer cache to propagate.
158
- * @returns Boundary input augmented with the shared renderer cache.
149
+ * @returns Render input augmented with the shared renderer cache.
159
150
  */
160
- private withBoundaryRendererCache(
151
+ private withOwningRendererCache(
161
152
  input: ComponentRenderInput,
162
153
  rendererCache: Map<string, IntegrationRenderer<any>>,
163
154
  ): ComponentRenderInput {
@@ -270,7 +261,7 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
270
261
  const resolvedDependencies = this.htmlTransformer.dedupeProcessedAssets(
271
262
  await this.resolveDependencies(componentsToResolve),
272
263
  );
273
- this.htmlTransformer.setPagePackage(this.pagePackagingService.createPagePackage(resolvedDependencies));
264
+ this.htmlTransformer.setPagePackage(createPagePackage(resolvedDependencies));
274
265
  return resolvedDependencies;
275
266
  }
276
267
 
@@ -278,7 +269,7 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
278
269
  * Merges component-scoped assets into the active HTML transformer state.
279
270
  *
280
271
  * Explicit page, layout, and document shell composition can produce assets at
281
- * each boundary. This helper deduplicates those groups and folds them back into
272
+ * each foreign subtree. This helper deduplicates those groups and folds them back into
282
273
  * the transformer so downstream HTML finalization sees one canonical asset set.
283
274
  *
284
275
  * @param assetGroups - Optional groups of processed assets to merge.
@@ -300,7 +291,7 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
300
291
  ...nextDependencies,
301
292
  ]);
302
293
 
303
- this.htmlTransformer.setPagePackage(this.pagePackagingService.createPagePackage(mergedDependencies));
294
+ this.htmlTransformer.setPagePackage(createPagePackage(mergedDependencies));
304
295
 
305
296
  return nextDependencies;
306
297
  }
@@ -332,7 +323,7 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
332
323
  *
333
324
  * Same-integration views can optionally stream or render inline via the caller's
334
325
  * `renderInline()` hook. Once a view may cross integration boundaries, this
335
- * helper routes the render through `renderComponentBoundary()` instead so mixed
326
+ * helper routes the render through `renderComponentWithForeignChildren()` instead so mixed
336
327
  * shells can reuse the execution-scoped renderer cache and resolve nested
337
328
  * foreign ownership before the partial response is returned.
338
329
  *
@@ -346,17 +337,19 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
346
337
  renderInline?: () => Promise<BodyInit>;
347
338
  transformHtml?: (html: string) => string;
348
339
  }): Promise<Response> {
349
- return this.routeShellComposer.renderPartialViewResponse(input, {
350
- hasForeignBoundaryDescendants: (component) => this.hasForeignBoundaryDescendants(component),
351
- createHtmlResponse: (body, ctx) => this.createHtmlResponse(body, ctx),
352
- renderComponentBoundary: (boundaryInput) => this.renderComponentBoundary(boundaryInput),
353
- prepareViewDependencies: (view, layout) => this.prepareViewDependencies(view, layout),
354
- getHtmlTemplate: () => this.getHtmlTemplate(),
355
- resolveViewMetadata: (view, props) => this.resolveViewMetadata(view, props),
356
- appendProcessedDependencies: (...assetGroups) => this.appendProcessedDependencies(...assetGroups),
357
- finalizeResolvedHtml: (options) => this.finalizeResolvedHtml(options),
358
- docType: this.DOC_TYPE,
340
+ if (input.renderInline && !this.hasForeignChildDescendants(input.view as EcoComponent)) {
341
+ return this.createHtmlResponse(await input.renderInline(), input.ctx);
342
+ }
343
+
344
+ const rendererCache = new Map<string, unknown>() as BaseIntegrationContext['rendererCache'];
345
+ const viewRender = await this.renderComponentWithForeignChildren({
346
+ component: input.view as EcoComponent,
347
+ props: (input.props ?? {}) as Record<string, unknown>,
348
+ integrationContext: { rendererCache },
359
349
  });
350
+ const html = input.transformHtml ? input.transformHtml(viewRender.html) : viewRender.html;
351
+
352
+ return this.createHtmlResponse(html, input.ctx);
360
353
  }
361
354
 
362
355
  /**
@@ -377,26 +370,47 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
377
370
  ctx: RenderToResponseContext;
378
371
  layout?: EcoComponent;
379
372
  }): Promise<Response> {
380
- return this.routeShellComposer.renderViewWithDocumentShell(input, {
381
- hasForeignBoundaryDescendants: (component) => this.hasForeignBoundaryDescendants(component),
382
- createHtmlResponse: (body, ctx) => this.createHtmlResponse(body, ctx),
383
- renderComponentBoundary: (boundaryInput) => this.renderComponentBoundary(boundaryInput),
384
- prepareViewDependencies: (view, layout) => this.prepareViewDependencies(view, layout),
385
- getHtmlTemplate: () => this.getHtmlTemplate(),
386
- resolveViewMetadata: (view, props) => this.resolveViewMetadata(view, props),
387
- appendProcessedDependencies: (...assetGroups) => this.appendProcessedDependencies(...assetGroups),
388
- finalizeResolvedHtml: (options) => this.finalizeResolvedHtml(options),
389
- docType: this.DOC_TYPE,
373
+ const normalizedProps = (input.props ?? {}) as Record<string, unknown>;
374
+
375
+ if (input.ctx.partial) {
376
+ return this.renderPartialViewResponse(input);
377
+ }
378
+
379
+ await this.prepareViewDependencies(input.view, input.layout);
380
+
381
+ const HtmlTemplate = await this.getHtmlTemplate();
382
+ const metadata = await this.resolveViewMetadata(input.view, input.props);
383
+ const { documentHtml } = await this.composeDocumentShell({
384
+ primaryComponent: input.view as EcoComponent,
385
+ primaryProps: normalizedProps,
386
+ layout: input.layout
387
+ ? {
388
+ component: input.layout,
389
+ props: {},
390
+ }
391
+ : undefined,
392
+ htmlTemplate: HtmlTemplate as EcoComponent,
393
+ documentProps: {
394
+ metadata,
395
+ pageProps: normalizedProps,
396
+ },
397
+ });
398
+
399
+ const html = await this.finalizeResolvedHtml({
400
+ html: `${this.DOC_TYPE}${documentHtml}`,
401
+ partial: false,
390
402
  });
403
+
404
+ return this.createHtmlResponse(html, input.ctx);
391
405
  }
392
406
 
393
407
  /**
394
408
  * Renders a route page through optional layout and document shells.
395
409
  *
396
- * Route rendering and explicit view rendering now share the same boundary-owned
410
+ * Route rendering and explicit view rendering now share the same renderer-owned
397
411
  * shell composition model. This helper composes page, layout, and html template
398
- * boundaries while threading one execution-scoped renderer cache through every
399
- * delegated boundary so foreign shell ownership remains stable and renderer
412
+ * renders while threading one execution-scoped renderer cache through every
413
+ * delegated foreign subtree so foreign shell ownership remains stable and renderer
400
414
  * initialization is reused inside the current request.
401
415
  *
402
416
  * @param input - Page, layout, document, and metadata inputs for the route render.
@@ -417,33 +431,77 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
417
431
  documentProps?: Record<string, unknown>;
418
432
  transformDocumentHtml?: (html: string) => string;
419
433
  }): Promise<string> {
420
- return this.routeShellComposer.renderPageWithDocumentShell(input, {
421
- hasForeignBoundaryDescendants: (component) => this.hasForeignBoundaryDescendants(component),
422
- createHtmlResponse: (body, ctx) => this.createHtmlResponse(body, ctx),
423
- renderComponentBoundary: (boundaryInput) => this.renderComponentBoundary(boundaryInput),
424
- prepareViewDependencies: (view, layout) => this.prepareViewDependencies(view, layout),
425
- getHtmlTemplate: () => this.getHtmlTemplate(),
426
- resolveViewMetadata: (view, props) => this.resolveViewMetadata(view, props),
427
- appendProcessedDependencies: (...assetGroups) => this.appendProcessedDependencies(...assetGroups),
428
- finalizeResolvedHtml: (options) => this.finalizeResolvedHtml(options),
429
- docType: this.DOC_TYPE,
434
+ const { documentHtml: composedDocumentHtml } = await this.composeDocumentShell({
435
+ primaryComponent: input.page.component,
436
+ primaryProps: input.page.props,
437
+ layout: input.layout,
438
+ htmlTemplate: input.htmlTemplate,
439
+ documentProps: {
440
+ metadata: input.metadata,
441
+ pageProps: input.pageProps,
442
+ ...(input.documentProps ?? {}),
443
+ },
430
444
  });
445
+
446
+ const documentHtml = input.transformDocumentHtml
447
+ ? input.transformDocumentHtml(composedDocumentHtml)
448
+ : composedDocumentHtml;
449
+
450
+ return `${this.DOC_TYPE}${documentHtml}`;
451
+ }
452
+
453
+ private async composeDocumentShell(input: {
454
+ primaryComponent: EcoComponent;
455
+ primaryProps: Record<string, unknown>;
456
+ layout?: {
457
+ component: EcoComponent;
458
+ props?: Record<string, unknown>;
459
+ };
460
+ htmlTemplate: EcoComponent;
461
+ documentProps: Record<string, unknown>;
462
+ }): Promise<{ documentHtml: string }> {
463
+ const rendererCache = new Map<string, unknown>() as BaseIntegrationContext['rendererCache'];
464
+ const primaryRender = await this.renderComponentWithForeignChildren({
465
+ component: input.primaryComponent,
466
+ props: input.primaryProps,
467
+ integrationContext: { rendererCache },
468
+ });
469
+ const layoutRender = input.layout
470
+ ? await this.renderComponentWithForeignChildren({
471
+ component: input.layout.component,
472
+ props: input.layout.props ?? {},
473
+ children: primaryRender.html,
474
+ integrationContext: { rendererCache },
475
+ })
476
+ : undefined;
477
+ const documentRender = await this.renderComponentWithForeignChildren({
478
+ component: input.htmlTemplate,
479
+ props: input.documentProps,
480
+ children: layoutRender?.html ?? primaryRender.html,
481
+ integrationContext: { rendererCache },
482
+ });
483
+
484
+ this.appendProcessedDependencies(primaryRender.assets, layoutRender?.assets, documentRender.assets);
485
+
486
+ return {
487
+ documentHtml: documentRender.html,
488
+ };
431
489
  }
432
490
 
433
491
  /**
434
- * Renders one string-first component boundary and collects its assets.
492
+ * Renders one string-first component with serialized children and collects its assets.
435
493
  *
436
- * String-oriented integrations frequently share the same boundary contract:
494
+ * String-oriented integrations frequently share the same component contract:
437
495
  * pass serialized children through props, coerce the render result to HTML, and
438
496
  * attach any component-scoped dependencies. This helper centralizes that flow
439
497
  * so integrations can opt into shared orchestration without repeating the same
440
- * boundary boilerplate.
498
+ * string-render boilerplate.
441
499
  *
442
- * @param input - Boundary render input.
500
+ * @param input - Component render input.
443
501
  * @param component - String-oriented component implementation to execute.
444
502
  * @returns Structured component render result for orchestration paths.
445
503
  */
446
- protected async renderStringComponentBoundary(
504
+ protected async renderStringComponentWithSerializedChildren(
447
505
  input: ComponentRenderInput,
448
506
  component: (props: Record<string, unknown>) => Promise<EcoPagesElement> | EcoPagesElement,
449
507
  ): Promise<ComponentRenderResult> {
@@ -465,41 +523,18 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
465
523
  };
466
524
  }
467
525
 
468
- protected getBoundaryTokenPrefix(): string {
469
- return `__${this.name}_boundary__`;
470
- }
471
-
472
- protected getBoundaryRuntimeContextKey(): string {
473
- return `__${this.name}_boundary_runtime__`;
526
+ protected getForeignSubtreeTokenPrefix(): string {
527
+ return `__${this.name}_foreign_subtree__`;
474
528
  }
475
529
 
476
- protected getQueuedBoundaryRuntime<TContext extends QueuedBoundaryRuntimeContext>(
477
- input: ComponentRenderInput,
478
- runtimeContextKey = this.getBoundaryRuntimeContextKey(),
479
- ): TContext | undefined {
480
- return this.queuedBoundaryRuntimeService.getRuntimeContext<TContext>(input, runtimeContextKey);
530
+ protected getForeignSubtreeResolutionContextKey(): string {
531
+ return `__${this.name}_foreign_subtree_runtime__`;
481
532
  }
482
533
 
483
- protected async resolveQueuedBoundaryTokens(
484
- html: string,
485
- queuedResolutionsByToken: Map<string, QueuedBoundaryResolution>,
486
- resolveToken: (token: string) => Promise<string>,
487
- ): Promise<string> {
488
- let resolvedHtml = html;
489
-
490
- for (const token of queuedResolutionsByToken.keys()) {
491
- if (!resolvedHtml.includes(token)) {
492
- continue;
493
- }
494
-
495
- resolvedHtml = resolvedHtml.split(token).join(await resolveToken(token));
496
- }
497
-
498
- return resolvedHtml;
499
- }
500
-
501
- protected createQueuedBoundaryRuntime<TContext extends QueuedBoundaryRuntimeContext>(options: {
502
- boundaryInput: ComponentRenderInput;
534
+ protected createQueuedForeignSubtreeExecutionRuntime<
535
+ TContext extends QueuedForeignSubtreeResolutionContext,
536
+ >(options: {
537
+ renderInput: ComponentRenderInput;
503
538
  rendererCache: Map<string, IntegrationRenderer<any>>;
504
539
  runtimeContextKey?: string;
505
540
  tokenPrefix?: string;
@@ -507,80 +542,55 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
507
542
  integrationContext: BaseIntegrationContext & Record<string, unknown>,
508
543
  rendererCache: Map<string, unknown>,
509
544
  ) => TContext;
510
- }): ComponentBoundaryRuntime {
511
- return this.queuedBoundaryRuntimeService.createRuntime<TContext>({
512
- boundaryInput: options.boundaryInput,
513
- rendererCache: options.rendererCache as Map<string, unknown>,
514
- runtimeContextKey: options.runtimeContextKey ?? this.getBoundaryRuntimeContextKey(),
515
- tokenPrefix: options.tokenPrefix ?? this.getBoundaryTokenPrefix(),
516
- shouldQueueBoundary: (input) => this.shouldResolveBoundaryInOwningRenderer(input),
545
+ }): ForeignChildRuntime {
546
+ return this.foreignSubtreeExecutionService.createQueuedRuntime<TContext>({
547
+ renderInput: options.renderInput,
548
+ rendererCache: options.rendererCache,
549
+ runtimeContextKey: options.runtimeContextKey ?? this.getForeignSubtreeResolutionContextKey(),
550
+ tokenPrefix: options.tokenPrefix ?? this.getForeignSubtreeTokenPrefix(),
517
551
  createRuntimeContext: options.createRuntimeContext,
518
552
  });
519
553
  }
520
554
 
521
- protected async resolveRendererOwnedQueuedBoundaryHtml<TContext extends QueuedBoundaryRuntimeContext>(options: {
522
- html: string;
523
- runtimeContext?: TContext;
524
- queueLabel: string;
525
- renderQueuedChildren: (
526
- children: unknown,
527
- runtimeContext: TContext,
528
- queuedResolutionsByToken: Map<string, QueuedBoundaryResolution>,
529
- resolveToken: (token: string) => Promise<string>,
530
- ) => Promise<{ assets: ProcessedAsset[]; html?: string }>;
531
- }): Promise<{ assets: ProcessedAsset[]; html: string }> {
532
- return this.queuedBoundaryRuntimeService.resolveQueuedHtml({
533
- html: options.html,
534
- runtimeContext: options.runtimeContext,
535
- queueLabel: options.queueLabel,
536
- renderQueuedChildren: options.renderQueuedChildren,
537
- resolveBoundary: (input, rendererCache) =>
538
- this.resolveBoundaryPayloadInOwningRenderer(
539
- input,
540
- rendererCache as Map<string, IntegrationRenderer<any>>,
541
- ),
542
- applyAttributesToFirstElement: (html, attributes) =>
543
- this.htmlTransformer.applyAttributesToFirstElement(html, attributes),
544
- dedupeProcessedAssets: (assets) => this.htmlTransformer.dedupeProcessedAssets(assets),
545
- });
555
+ protected getQueuedForeignSubtreeResolutionContext<TContext extends QueuedForeignSubtreeResolutionContext>(
556
+ input: ComponentRenderInput,
557
+ ): TContext | undefined {
558
+ return this.foreignSubtreeExecutionService.getQueuedRuntimeContext<TContext>(
559
+ input,
560
+ this.getForeignSubtreeResolutionContextKey(),
561
+ );
546
562
  }
547
563
 
548
564
  /**
549
565
  * Renders a string-first component, then resolves any queued foreign
550
566
  * boundaries before returning final component HTML.
551
567
  */
552
- protected async renderStringComponentBoundaryWithQueuedForeignBoundaries(
568
+ protected async renderStringComponentWithQueuedForeignSubtrees(
553
569
  input: ComponentRenderInput,
554
570
  component: (props: Record<string, unknown>) => Promise<EcoPagesElement> | EcoPagesElement,
555
571
  ): Promise<ComponentRenderResult> {
556
- const componentRender = await this.renderStringComponentBoundary(input, component);
557
- const queuedBoundaryResolution = await this.resolveRendererOwnedQueuedBoundaryHtml({
572
+ const componentRender = await this.renderStringComponentWithSerializedChildren(input, component);
573
+ const queuedForeignSubtreeResolution = await this.foreignSubtreeExecutionService.resolveStringQueuedHtml({
574
+ currentIntegrationName: this.name,
575
+ renderInput: input,
558
576
  html: componentRender.html,
559
- runtimeContext: this.getQueuedBoundaryRuntime<QueuedBoundaryRuntimeContext>(input),
577
+ runtimeContextKey: this.getForeignSubtreeResolutionContextKey(),
560
578
  queueLabel: 'String',
561
- renderQueuedChildren: async (children, _runtimeContext, queuedResolutionsByToken, resolveToken) => {
562
- if (children === undefined) {
563
- return { assets: [], html: undefined };
564
- }
565
-
566
- const html = await this.resolveQueuedBoundaryTokens(
567
- typeof children === 'string' ? children : String(children ?? ''),
568
- queuedResolutionsByToken,
569
- resolveToken,
570
- );
571
-
572
- return { assets: [], html };
573
- },
579
+ getOwningRenderer: (integrationName, rendererCache) =>
580
+ this.getIntegrationRendererForName(integrationName, rendererCache),
581
+ applyAttributesToFirstElement: (html, attributes) =>
582
+ this.htmlTransformer.applyAttributesToFirstElement(html, attributes),
583
+ dedupeProcessedAssets: (assets) => this.htmlTransformer.dedupeProcessedAssets(assets),
574
584
  });
575
585
  const mergedAssets = this.htmlTransformer.dedupeProcessedAssets([
576
586
  ...(componentRender.assets ?? []),
577
- ...queuedBoundaryResolution.assets,
587
+ ...queuedForeignSubtreeResolution.assets,
578
588
  ]);
579
589
 
580
590
  return {
581
591
  ...componentRender,
582
- html: queuedBoundaryResolution.html,
583
- rootTag: this.getRootTagName(queuedBoundaryResolution.html),
592
+ html: queuedForeignSubtreeResolution.html,
593
+ rootTag: this.getRootTagName(queuedForeignSubtreeResolution.html),
584
594
  assets: mergedAssets.length > 0 ? mergedAssets : undefined,
585
595
  };
586
596
  }
@@ -601,16 +611,14 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
601
611
  this.appConfig = appConfig;
602
612
  this.assetProcessingService = assetProcessingService;
603
613
  this.htmlTransformer = new HtmlTransformerService();
604
- this.pagePackagingService = new PagePackagingService();
605
614
  this.resolvedIntegrationDependencies = resolvedIntegrationDependencies || [];
606
615
  this.rendererModules = rendererModules ?? appConfig.runtime?.rendererModuleContext;
607
616
  this.runtimeOrigin = runtimeOrigin;
608
617
  this.dependencyResolverService = new DependencyResolverService(appConfig, assetProcessingService);
609
618
  this.pageModuleLoaderService = new PageModuleLoaderService(appConfig, runtimeOrigin);
610
- this.renderPreparationService = new RenderPreparationService(appConfig, assetProcessingService, {
611
- pagePackagingService: this.pagePackagingService,
619
+ this.routeRenderOrchestrator = new RouteRenderOrchestrator(appConfig, assetProcessingService, {
620
+ ownershipValidationService: new OwnershipValidationService(appConfig),
612
621
  });
613
- this.renderExecutionService = new RenderExecutionService();
614
622
  }
615
623
 
616
624
  /**
@@ -648,45 +656,6 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
648
656
  }
649
657
  }
650
658
 
651
- /**
652
- * Returns the static props for the page.
653
- * It calls the provided getStaticProps function with the given options.
654
- *
655
- * @param getStaticProps - The function to get static props.
656
- * @param options - The options to pass to the getStaticProps function.
657
- * @returns The static props and metadata.
658
- */
659
- protected async getStaticProps(
660
- getStaticProps?: GetStaticProps<Record<string, unknown>>,
661
- options?: Pick<RouteRendererOptions, 'params'>,
662
- ): Promise<{
663
- props: Record<string, unknown>;
664
- metadata?: PageMetadataProps;
665
- }> {
666
- return this.pageModuleLoaderService.getStaticPropsForPage({
667
- getStaticProps,
668
- params: options?.params,
669
- });
670
- }
671
-
672
- /**
673
- * Returns the metadata properties for the page.
674
- * It calls the provided getMetadata function with the given context.
675
- *
676
- * @param getMetadata - The function to get metadata.
677
- * @param context - The context to pass to the getMetadata function.
678
- * @returns The metadata properties.
679
- */
680
- protected async getMetadataProps(
681
- getMetadata: GetMetadata | undefined,
682
- { props, params, query }: GetMetadataContext,
683
- ): Promise<PageMetadataProps> {
684
- return this.pageModuleLoaderService.getMetadataPropsForPage({
685
- getMetadata,
686
- context: { props, params, query } as GetMetadataContext,
687
- });
688
- }
689
-
690
659
  protected usesIntegrationPageImporter(_file: string): boolean {
691
660
  return false;
692
661
  }
@@ -815,6 +784,129 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
815
784
  return this.dependencyResolverService.processComponentDependencies(components, this.name);
816
785
  }
817
786
 
787
+ /**
788
+ * Builds the internal route-render adapter consumed by `RouteRenderOrchestrator`.
789
+ *
790
+ * The route orchestrator needs a narrow orchestration contract, but those hooks should
791
+ * not become public API on the renderer base class. Keeping the adapter object
792
+ * local to the execution path lets the orchestrator depend on one explicit seam while
793
+ * subclasses continue to override protected renderer behavior directly.
794
+ */
795
+ protected createRouteRenderOrchestratorAdapter(): RouteRenderOrchestratorAdapter<C> {
796
+ return {
797
+ name: this.name,
798
+ resolveRouteRenderInputs: (routeOptions) => this.resolveRouteRenderInputs(routeOptions),
799
+ resolveRouteAssets: (input) => this.resolveRouteAssets(input),
800
+ resolveRoutePageComponentRender: (input) => this.resolveRoutePageComponentRender(input),
801
+ renderRouteBody: (renderOptions) => this.renderRouteBody(renderOptions),
802
+ getRouteHtmlFinalization: (renderOptions) => this.getRouteHtmlFinalization(renderOptions),
803
+ transformRouteResponse: (response) => this.transformRouteResponse(response),
804
+ };
805
+ }
806
+
807
+ protected async resolveRouteRenderInputs(
808
+ routeOptions: RouteRendererOptions,
809
+ ): Promise<RouteRenderOrchestratorResolvedInputs> {
810
+ const pageModule = await this.pageModuleLoaderService.resolvePageModule({
811
+ file: routeOptions.file,
812
+ importPageFileFn: (targetFile) => this.importPageFile(targetFile),
813
+ });
814
+ const { Page, integrationSpecificProps } = pageModule;
815
+ const HtmlTemplate = await this.getHtmlTemplate();
816
+ const Layout = Page.config?.layout;
817
+ const { props, metadata } = await this.pageModuleLoaderService.resolvePageData({
818
+ pageModule,
819
+ routeOptions,
820
+ });
821
+
822
+ return {
823
+ Page,
824
+ HtmlTemplate: HtmlTemplate as EcoComponent<HtmlTemplateProps>,
825
+ Layout,
826
+ props,
827
+ metadata,
828
+ integrationSpecificProps,
829
+ };
830
+ }
831
+
832
+ protected async resolveRouteAssets(input: {
833
+ routeOptions: RouteRendererOptions;
834
+ components: (EcoComponent | Partial<EcoComponent>)[];
835
+ }): Promise<{ resolvedDependencies: ProcessedAsset[]; pageBrowserGraph?: { assets: ProcessedAsset[] } }> {
836
+ return {
837
+ resolvedDependencies: await this.resolveDependencies(input.components),
838
+ pageBrowserGraph: await this.buildPageBrowserGraph(input.routeOptions.file),
839
+ };
840
+ }
841
+
842
+ protected async resolveRoutePageComponentRender(input: {
843
+ Page: EcoComponent;
844
+ Layout?: EcoComponent;
845
+ props: Record<string, unknown>;
846
+ routeOptions: RouteRendererOptions;
847
+ }): Promise<ComponentRenderResult | undefined> {
848
+ if (!this.shouldRenderPageComponent({ Page: input.Page, Layout: input.Layout, options: input.routeOptions })) {
849
+ return undefined;
850
+ }
851
+
852
+ return this.renderComponentWithForeignChildren({
853
+ component: input.Page,
854
+ props: {
855
+ ...input.props,
856
+ params: input.routeOptions.params || {},
857
+ query: input.routeOptions.query || {},
858
+ },
859
+ integrationContext: {
860
+ componentInstanceId: 'eco-page-root',
861
+ },
862
+ });
863
+ }
864
+
865
+ protected async renderRouteBody(renderOptions: IntegrationRendererRenderOptions<C>): Promise<RouteRendererBody> {
866
+ return this.render(renderOptions);
867
+ }
868
+
869
+ protected getRouteHtmlFinalization(renderOptions: IntegrationRendererRenderOptions<C>): RouteHtmlFinalization {
870
+ const componentRootAttributes =
871
+ renderOptions.componentRender?.canAttachAttributes &&
872
+ renderOptions.componentRender.rootAttributes &&
873
+ Object.keys(renderOptions.componentRender.rootAttributes).length > 0
874
+ ? (renderOptions.componentRender.rootAttributes as Record<string, string>)
875
+ : undefined;
876
+ const documentAttributes = this.getDocumentAttributes(renderOptions);
877
+ const hasStructuralFinalization =
878
+ (componentRootAttributes && Object.keys(componentRootAttributes).length > 0) ||
879
+ (documentAttributes && Object.keys(documentAttributes).length > 0);
880
+
881
+ if (!hasStructuralFinalization) {
882
+ return {};
883
+ }
884
+
885
+ return {
886
+ finalizeHtml: (html) => {
887
+ let renderedHtml = html;
888
+
889
+ if (componentRootAttributes) {
890
+ renderedHtml = this.htmlTransformer.applyAttributesToFirstBodyElement(
891
+ renderedHtml,
892
+ componentRootAttributes,
893
+ );
894
+ }
895
+
896
+ if (documentAttributes) {
897
+ renderedHtml = this.htmlTransformer.applyAttributesToHtmlElement(renderedHtml, documentAttributes);
898
+ }
899
+
900
+ return renderedHtml;
901
+ },
902
+ };
903
+ }
904
+
905
+ protected async transformRouteResponse(response: Response): Promise<RouteRendererBody> {
906
+ const transformedResponse = await this.htmlTransformer.transform(response);
907
+ return (transformedResponse.body ?? (await transformedResponse.text())) as RouteRendererBody;
908
+ }
909
+
818
910
  /**
819
911
  * Prepares the render options for the integration renderer.
820
912
  * It imports the page file, collects dependencies, and prepares the render options.
@@ -822,27 +914,14 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
822
914
  * @param options - The route renderer options.
823
915
  * @returns The prepared render options.
824
916
  */
825
- protected async prepareRenderOptions(options: RouteRendererOptions): Promise<IntegrationRendererRenderOptions> {
826
- const preparedOptions = await this.renderPreparationService.prepare(options, this.name, {
827
- resolvePageModule: (file) => this.resolvePageModule(file),
828
- getHtmlTemplate: () => this.getHtmlTemplate(),
829
- resolvePageData: (pageModule, routeOptions) => this.resolvePageData(pageModule, routeOptions),
830
- resolveDependencies: (components) => this.resolveDependencies(components),
831
- buildRouteRenderAssets: (file) => this.buildRouteRenderAssets(file),
832
- shouldRenderPageComponent: (input) => this.shouldRenderPageComponent(input),
833
- renderPageComponent: ({ component, props }) =>
834
- this.renderComponentBoundary({
835
- component,
836
- props,
837
- integrationContext: {
838
- componentInstanceId: 'eco-page-root',
839
- },
840
- }),
841
- });
842
-
843
- invariant(preparedOptions.pagePackage !== undefined, 'Expected render preparation to produce a page package');
844
- this.htmlTransformer.setPagePackage(preparedOptions.pagePackage);
845
- return preparedOptions;
917
+ protected async prepareRenderOptions(
918
+ options: RouteRendererOptions,
919
+ adapter = this.createRouteRenderOrchestratorAdapter(),
920
+ ): Promise<IntegrationRendererRenderOptions<C>> {
921
+ const renderOptions = await this.routeRenderOrchestrator.prepareRenderOptions(options, adapter);
922
+ invariant(renderOptions.pagePackage !== undefined, 'Expected render preparation to produce a page package');
923
+ this.htmlTransformer.setPagePackage(renderOptions.pagePackage);
924
+ return renderOptions;
846
925
  }
847
926
 
848
927
  /**
@@ -861,47 +940,13 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
861
940
  return true;
862
941
  }
863
942
 
864
- /**
865
- * Resolves the page module and normalizes exports.
866
- */
867
- protected async resolvePageModule(file: string): Promise<{
868
- Page: EcoPageFile['default'] | EcoPageComponent<any>;
869
- getStaticProps?: GetStaticProps<Record<string, unknown>>;
870
- getMetadata?: GetMetadata;
871
- integrationSpecificProps: Record<string, unknown>;
872
- }> {
873
- return this.pageModuleLoaderService.resolvePageModule({
874
- file,
875
- importPageFileFn: (targetFile) => this.importPageFile(targetFile),
876
- });
877
- }
878
-
879
- /**
880
- * Resolves static props and metadata for the page.
881
- */
882
- protected async resolvePageData(
883
- pageModule: {
884
- getStaticProps?: GetStaticProps<Record<string, unknown>>;
885
- getMetadata?: GetMetadata;
886
- },
887
- options: RouteRendererOptions,
888
- ): Promise<{
889
- props: Record<string, unknown>;
890
- metadata: PageMetadataProps;
891
- }> {
892
- return this.pageModuleLoaderService.resolvePageData({
893
- pageModule,
894
- routeOptions: options,
895
- });
896
- }
897
-
898
943
  /**
899
944
  * Executes the integration renderer with the provided options.
900
945
  *
901
946
  * Execution flow:
902
947
  * 1. Build normalized render options (`prepareRenderOptions`).
903
948
  * 2. Render the route body once.
904
- * 3. Reject unresolved route-level boundary artifacts.
949
+ * 3. Reject unresolved route-level eco-marker artifacts.
905
950
  * 4. Optionally apply root attributes for page/component root boundaries.
906
951
  * 5. Run HTML transformer with final dependency set.
907
952
  *
@@ -913,27 +958,16 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
913
958
  * @returns Rendered route body plus effective cache strategy.
914
959
  */
915
960
  public async execute(options: RouteRendererOptions): Promise<RouteRenderResult> {
916
- return this.renderExecutionService.execute(options, {
917
- prepareRenderOptions: (routeOptions) =>
918
- this.prepareRenderOptions(routeOptions) as Promise<IntegrationRendererRenderOptions<C>>,
919
- render: (renderOptions) => this.render(renderOptions),
920
- getDocumentAttributes: (renderOptions) => this.getDocumentAttributes(renderOptions),
921
- applyAttributesToHtmlElement: (html, attributes) =>
922
- this.htmlTransformer.applyAttributesToHtmlElement(html, attributes),
923
- applyAttributesToFirstBodyElement: (html, attributes) =>
924
- this.htmlTransformer.applyAttributesToFirstBodyElement(html, attributes),
925
- transformResponse: async (response) => {
926
- const transformedResponse = await this.htmlTransformer.transform(response);
927
- return (transformedResponse.body ?? (await transformedResponse.text())) as RouteRendererBody;
928
- },
929
- });
961
+ const adapter = this.createRouteRenderOrchestratorAdapter();
962
+ const renderOptions = await this.prepareRenderOptions(options, adapter);
963
+ return this.routeRenderOrchestrator.executePrepared(renderOptions, adapter);
930
964
  }
931
965
 
932
966
  /**
933
967
  * Finalizes already-resolved HTML for explicit renderer-owned paths.
934
968
  *
935
969
  * This keeps document and root-attribute stamping plus HTML transformation
936
- * available after a renderer has completed nested boundary resolution without
970
+ * available after a renderer has completed nested foreign-subtree resolution without
937
971
  * routing back through shared route execution.
938
972
  */
939
973
  protected async finalizeResolvedHtml(options: {
@@ -992,12 +1026,12 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
992
1026
  * @returns Renderer for the requested integration.
993
1027
  * @throws Error when no integration plugin matches `integrationName`.
994
1028
  */
995
- private getIntegrationRendererForName(
1029
+ protected getIntegrationRendererForName(
996
1030
  integrationName: string,
997
- cache: Map<string, IntegrationRenderer<any>>,
998
- ): IntegrationRenderer<any> {
1031
+ cache: Map<string, ForeignSubtreeExecutionOwningRenderer>,
1032
+ ): ForeignSubtreeExecutionOwningRenderer {
999
1033
  if (cache.has(integrationName)) {
1000
- return cache.get(integrationName) as IntegrationRenderer<any>;
1034
+ return cache.get(integrationName) as ForeignSubtreeExecutionOwningRenderer;
1001
1035
  }
1002
1036
 
1003
1037
  if (integrationName === this.name) {
@@ -1008,7 +1042,7 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
1008
1042
  const integrationPlugin = this.appConfig.integrations.find(
1009
1043
  (integration) => integration.name === integrationName,
1010
1044
  );
1011
- invariant(!!integrationPlugin, `[ecopages] Integration not found for boundary owner: ${integrationName}`);
1045
+ invariant(!!integrationPlugin, `[ecopages] Integration not found for foreign owner: ${integrationName}`);
1012
1046
  const renderer = integrationPlugin.initializeRenderer({
1013
1047
  rendererModules: this.appConfig.runtime?.rendererModuleContext,
1014
1048
  });
@@ -1025,95 +1059,38 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
1025
1059
  */
1026
1060
  abstract render(options: IntegrationRendererRenderOptions<C>): Promise<RouteRendererBody>;
1027
1061
 
1028
- protected async resolveBoundaryInOwningRenderer(
1029
- input: ComponentRenderInput,
1030
- rendererCache: Map<string, IntegrationRenderer<any>>,
1031
- ): Promise<ComponentRenderResult | undefined> {
1032
- const boundaryOwner = this.getRegisteredBoundaryOwner(input.component);
1033
- if (!boundaryOwner) {
1034
- return undefined;
1035
- }
1036
-
1037
- const owningRenderer = this.getIntegrationRendererForName(boundaryOwner, rendererCache);
1038
- if (owningRenderer === this || owningRenderer.name === this.name) {
1039
- return undefined;
1040
- }
1041
- return await owningRenderer.renderComponentBoundary(this.withBoundaryRendererCache(input, rendererCache));
1042
- }
1043
-
1044
- protected async resolveBoundaryPayloadInOwningRenderer(
1045
- input: ComponentRenderInput,
1046
- rendererCache: Map<string, IntegrationRenderer<any>>,
1047
- ): Promise<BoundaryRenderPayload | undefined> {
1048
- const boundaryOwner = this.getRegisteredBoundaryOwner(input.component);
1049
- if (!boundaryOwner) {
1050
- return undefined;
1051
- }
1052
-
1053
- const owningRenderer = this.getIntegrationRendererForName(boundaryOwner, rendererCache);
1054
- if (owningRenderer === this || owningRenderer.name === this.name) {
1055
- return undefined;
1056
- }
1057
-
1058
- return await owningRenderer.renderBoundary(this.withBoundaryRendererCache(input, rendererCache));
1059
- }
1060
-
1061
1062
  /**
1062
- * Renders one component under this integration's boundary runtime and resolves
1063
- * any nested foreign boundaries captured during that render.
1063
+ * Renders one component under this integration's foreign-child runtime and resolves
1064
+ * any nested foreign children captured during that render.
1064
1065
  *
1065
1066
  * Without this wrapper, a component tree with foreign-owned descendants would
1066
- * render them with no active boundary runtime, which bypasses the owning
1067
- * renderer's nested-boundary handoff.
1067
+ * render them with no active foreign-child runtime, which bypasses the owning
1068
+ * renderer's nested foreign-child handoff.
1068
1069
  */
1069
- async renderComponentBoundary(input: ComponentRenderInput): Promise<ComponentRenderResult> {
1070
- const rendererCache =
1071
- this.getBoundaryRendererCache(input.integrationContext) ?? new Map<string, IntegrationRenderer<any>>();
1072
- const delegatedBoundaryRender = await this.resolveBoundaryInOwningRenderer(input, rendererCache);
1073
-
1074
- if (delegatedBoundaryRender) {
1075
- return delegatedBoundaryRender;
1076
- }
1077
-
1078
- const hasForeignBoundaries = this.hasForeignBoundaryDescendants(input.component);
1079
- const activeRenderContext = getComponentRenderContext();
1080
-
1081
- if (!hasForeignBoundaries) {
1082
- if (!activeRenderContext || activeRenderContext.currentIntegration === this.name) {
1083
- return this.normalizeComponentBoundaryRender(await this.renderComponent(input));
1084
- }
1085
-
1086
- const sameIntegrationExecution = await runWithComponentRenderContext(
1087
- {
1088
- currentIntegration: this.name,
1089
- },
1090
- async () => this.renderComponent(input),
1091
- );
1092
-
1093
- return this.normalizeComponentBoundaryRender(sameIntegrationExecution.value);
1094
- }
1095
-
1096
- const execution = await runWithComponentRenderContext(
1097
- {
1098
- currentIntegration: this.name,
1099
- boundaryRuntime: this.createComponentBoundaryRuntime({
1100
- boundaryInput: input,
1101
- rendererCache,
1070
+ async renderComponentWithForeignChildren(input: ComponentRenderInput): Promise<ComponentRenderResult> {
1071
+ return await this.foreignSubtreeExecutionService.executeComponentRender({
1072
+ currentIntegrationName: this.name,
1073
+ input,
1074
+ renderComponent: (renderInput) => this.renderComponent(renderInput),
1075
+ normalizeComponentRenderOutput: (result) => this.normalizeComponentRenderOutput(result),
1076
+ hasForeignChildDescendants: (component) => this.hasForeignChildDescendants(component),
1077
+ createForeignChildRuntime: ({ renderInput, rendererCache }) =>
1078
+ this.createForeignChildRuntime({
1079
+ renderInput,
1080
+ rendererCache: rendererCache as Map<string, IntegrationRenderer<any>>,
1102
1081
  }),
1103
- },
1104
- async () => this.renderComponent(input),
1105
- );
1106
-
1107
- return this.normalizeComponentBoundaryRender(execution.value);
1082
+ getOwningRenderer: (integrationName, rendererCache) =>
1083
+ this.getIntegrationRendererForName(integrationName, rendererCache),
1084
+ });
1108
1085
  }
1109
1086
 
1110
1087
  /**
1111
- * Compatibility boundary contract that exposes a narrower payload shape for
1088
+ * Compatibility foreign-subtree contract that exposes a narrower payload shape for
1112
1089
  * future route-composition work while preserving the current
1113
- * `renderComponentBoundary()` runtime semantics.
1090
+ * `renderComponentWithForeignChildren()` runtime semantics.
1114
1091
  */
1115
- async renderBoundary(input: ComponentRenderInput): Promise<BoundaryRenderPayload> {
1116
- const result = await this.renderComponentBoundary(input);
1092
+ async renderForeignSubtree(input: ComponentRenderInput): Promise<ForeignSubtreeRenderPayload> {
1093
+ const result = await this.renderComponentWithForeignChildren(input);
1117
1094
 
1118
1095
  return {
1119
1096
  html: result.html,
@@ -1125,8 +1102,8 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
1125
1102
  };
1126
1103
  }
1127
1104
 
1128
- private normalizeComponentBoundaryRender(result: ComponentRenderResult): ComponentRenderResult {
1129
- const normalizedHtml = this.normalizeBoundaryArtifactHtml(result.html);
1105
+ private normalizeComponentRenderOutput(result: ComponentRenderResult): ComponentRenderResult {
1106
+ const normalizedHtml = this.normalizeUnresolvedMarkerArtifactHtml(result.html);
1130
1107
 
1131
1108
  return normalizedHtml === result.html
1132
1109
  ? result
@@ -1136,18 +1113,18 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
1136
1113
  };
1137
1114
  }
1138
1115
 
1139
- protected normalizeBoundaryArtifactHtml(html: string): string {
1140
- return normalizeBoundaryArtifactHtml(html);
1116
+ protected normalizeUnresolvedMarkerArtifactHtml(html: string): string {
1117
+ return normalizeUnresolvedMarkerArtifactHtml(html);
1141
1118
  }
1142
1119
 
1143
1120
  /**
1144
1121
  * Returns whether the component dependency tree crosses into another
1145
1122
  * integration.
1146
1123
  *
1147
- * This keeps boundary-runtime setup narrow: same-integration trees can render
1124
+ * This keeps foreign-child runtime setup narrow: same-integration trees can render
1148
1125
  * directly without paying the queue orchestration cost.
1149
1126
  */
1150
- protected hasForeignBoundaryDescendants(component: EcoComponent): boolean {
1127
+ protected hasForeignChildDescendants(component: EcoComponent): boolean {
1151
1128
  const stack = [component];
1152
1129
  const seen = new Set<EcoComponent>();
1153
1130
 
@@ -1190,8 +1167,8 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
1190
1167
  * Default behavior delegates to `renderToResponse` in partial mode and wraps
1191
1168
  * the resulting HTML into the `ComponentRenderResult` contract.
1192
1169
  *
1193
- * In boundary resolution, this method is the integration-owned step that turns an
1194
- * already-resolved deferred boundary into concrete HTML, assets, and optional
1170
+ * In foreign-subtree resolution, this method is the integration-owned step that turns an
1171
+ * already-resolved deferred foreign subtree into concrete HTML, assets, and optional
1195
1172
  * root attributes.
1196
1173
  *
1197
1174
  * Integrations can override this for richer behavior (asset emission,
@@ -1228,58 +1205,40 @@ export abstract class IntegrationRenderer<C = EcoPagesElement> {
1228
1205
  }
1229
1206
 
1230
1207
  /**
1231
- * Method to build route render assets.
1208
+ * Builds the Page Browser Graph owned by this integration for one Page.
1232
1209
  * This method can be optionally overridden by the specific integration renderer.
1233
1210
  *
1234
1211
  * @param file - The file path to build assets for.
1235
- * @returns The processed assets or undefined.
1212
+ * @returns The structured Page Browser Graph or undefined.
1236
1213
  */
1237
- protected buildRouteRenderAssets(_file: string): Promise<ProcessedAsset[]> | undefined {
1214
+ protected async buildPageBrowserGraph(_file: string): Promise<{ assets: ProcessedAsset[] } | undefined> {
1238
1215
  return undefined;
1239
1216
  }
1240
1217
 
1241
1218
  /**
1242
- * Creates the per-render boundary runtime adopted by the shared component
1219
+ * Creates the per-render foreign-child runtime adopted by the shared component
1243
1220
  * render context.
1244
1221
  *
1245
- * Real mixed-integration renderers should override this and keep foreign
1246
- * boundary resolution inside their own renderer-owned queue. The base runtime
1247
- * fails fast when a renderer crosses into a foreign owner without providing its
1248
- * own handoff mechanism.
1222
+ * The default runtime queues delegated foreign subtrees inside the owning
1223
+ * renderer so string and markup renderers do not need to re-declare the same
1224
+ * handoff boilerplate. Override only when a renderer needs custom runtime
1225
+ * context or a different foreign-child execution strategy.
1249
1226
  */
1250
- protected createComponentBoundaryRuntime(_options: {
1251
- boundaryInput: ComponentRenderInput;
1227
+ protected createForeignChildRuntime(options: {
1228
+ renderInput: ComponentRenderInput;
1252
1229
  rendererCache: Map<string, IntegrationRenderer<any>>;
1253
- }): ComponentBoundaryRuntime {
1254
- const decideBoundaryInterception = (input: BoundaryRenderDecisionInput) => {
1255
- if (!this.shouldResolveBoundaryInOwningRenderer(input)) {
1256
- return { kind: 'inline' as const };
1257
- }
1258
-
1259
- throw new Error(
1260
- `[ecopages] ${this.name} renderer crossed into ${input.targetIntegration} without a renderer-owned boundary runtime. Override createComponentBoundaryRuntime() to resolve foreign boundaries inside the owning renderer.`,
1261
- );
1262
- };
1263
-
1264
- const runtime: ComponentBoundaryRuntime = {
1265
- interceptBoundary: decideBoundaryInterception,
1266
- interceptBoundarySync: decideBoundaryInterception,
1267
- };
1268
-
1269
- return runtime;
1230
+ }): ForeignChildRuntime {
1231
+ return this.createQueuedForeignSubtreeExecutionRuntime({
1232
+ renderInput: options.renderInput,
1233
+ rendererCache: options.rendererCache,
1234
+ });
1270
1235
  }
1271
1236
 
1272
1237
  /**
1273
- * Resolves whether a boundary should leave the current render pass and be
1274
- * resolved by its owning renderer.
1275
- *
1276
- * Boundaries owned by the current integration always render inline. Foreign-
1277
- * owned boundaries must be handed off by a renderer-owned runtime.
1278
- *
1279
- * @param input Boundary metadata for the active render pass.
1280
- * @returns `true` when the boundary should leave the current pass; otherwise `false`.
1238
+ * Creates an explicit fail-fast runtime for tests or renderers that do not
1239
+ * support cross-integration foreign-child execution.
1281
1240
  */
1282
- protected shouldResolveBoundaryInOwningRenderer(input: BoundaryRenderDecisionInput): boolean {
1283
- return !!input.targetIntegration && input.targetIntegration !== input.currentIntegration;
1241
+ protected createFailFastForeignChildRuntime(): ForeignChildRuntime {
1242
+ return this.foreignSubtreeExecutionService.createFailFastRuntime(this.name);
1284
1243
  }
1285
1244
  }