@ecopages/core 0.2.0-alpha.26 → 0.2.0-alpha.28

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 (106) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +63 -7
  3. package/package.json +8 -94
  4. package/src/adapters/bun/create-app.d.ts +1 -0
  5. package/src/adapters/bun/create-app.js +39 -2
  6. package/src/adapters/bun/hmr-manager.d.ts +1 -13
  7. package/src/adapters/bun/hmr-manager.js +1 -22
  8. package/src/adapters/bun/server-adapter.js +23 -4
  9. package/src/adapters/node/node-hmr-manager.d.ts +2 -14
  10. package/src/adapters/node/node-hmr-manager.js +2 -23
  11. package/src/adapters/shared/explicit-static-render-preparation.d.ts +25 -0
  12. package/src/adapters/shared/explicit-static-render-preparation.js +26 -0
  13. package/src/adapters/shared/explicit-static-route-matcher.d.ts +5 -2
  14. package/src/adapters/shared/explicit-static-route-matcher.js +14 -16
  15. package/src/adapters/shared/file-route-middleware-pipeline.d.ts +7 -10
  16. package/src/adapters/shared/file-route-middleware-pipeline.js +2 -11
  17. package/src/adapters/shared/fs-server-response-factory.d.ts +13 -9
  18. package/src/adapters/shared/fs-server-response-factory.js +10 -26
  19. package/src/adapters/shared/fs-server-response-matcher.d.ts +14 -6
  20. package/src/adapters/shared/fs-server-response-matcher.js +67 -28
  21. package/src/adapters/shared/render-context.d.ts +2 -2
  22. package/src/adapters/shared/server-adapter.d.ts +21 -10
  23. package/src/adapters/shared/server-adapter.js +171 -132
  24. package/src/adapters/shared/server-route-handler.d.ts +2 -2
  25. package/src/adapters/shared/server-route-handler.js +1 -1
  26. package/src/adapters/shared/server-static-builder.d.ts +4 -4
  27. package/src/config/README.md +1 -1
  28. package/src/config/config-builder.d.ts +2 -2
  29. package/src/config/config-builder.js +0 -5
  30. package/src/dev/host-runtime.d.ts +10 -0
  31. package/src/dev/host-runtime.js +24 -0
  32. package/src/eco/eco.js +7 -7
  33. package/src/eco/eco.types.d.ts +3 -3
  34. package/src/errors/index.d.ts +1 -0
  35. package/src/errors/index.js +3 -1
  36. package/src/hmr/strategies/js-hmr-strategy.d.ts +0 -5
  37. package/src/integrations/ghtml/ghtml-renderer.d.ts +0 -4
  38. package/src/integrations/ghtml/ghtml-renderer.js +1 -7
  39. package/src/plugins/eco-component-meta-plugin.js +0 -1
  40. package/src/plugins/integration-plugin.d.ts +14 -18
  41. package/src/plugins/integration-plugin.js +14 -21
  42. package/src/plugins/processor.d.ts +2 -0
  43. package/src/plugins/processor.js +6 -1
  44. package/src/route-renderer/GRAPH.md +81 -289
  45. package/src/route-renderer/README.md +67 -105
  46. package/src/route-renderer/orchestration/component-render-context.d.ts +24 -18
  47. package/src/route-renderer/orchestration/component-render-context.js +14 -14
  48. package/src/route-renderer/orchestration/declared-ownership-graph.d.ts +18 -0
  49. package/src/route-renderer/orchestration/declared-ownership-graph.js +34 -0
  50. package/src/route-renderer/orchestration/foreign-subtree-execution.service.d.ts +108 -0
  51. package/src/route-renderer/orchestration/foreign-subtree-execution.service.js +206 -0
  52. package/src/route-renderer/orchestration/integration-renderer.d.ts +96 -136
  53. package/src/route-renderer/orchestration/integration-renderer.js +280 -303
  54. package/src/route-renderer/orchestration/ownership-planning.service.d.ts +24 -0
  55. package/src/route-renderer/orchestration/ownership-planning.service.js +63 -0
  56. package/src/route-renderer/orchestration/ownership-validation.service.d.ts +29 -0
  57. package/src/route-renderer/orchestration/ownership-validation.service.js +53 -0
  58. package/src/route-renderer/orchestration/queued-foreign-subtree-resolution.service.d.ts +90 -0
  59. package/src/route-renderer/orchestration/{queued-boundary-runtime.service.js → queued-foreign-subtree-resolution.service.js} +28 -25
  60. package/src/route-renderer/orchestration/render-output.utils.d.ts +3 -3
  61. package/src/route-renderer/orchestration/render-output.utils.js +6 -6
  62. package/src/route-renderer/orchestration/route-render-orchestrator.d.ts +120 -0
  63. package/src/route-renderer/orchestration/{render-preparation.service.js → route-render-orchestrator.js} +132 -108
  64. package/src/route-renderer/page-loading/component-dependency-collection.js +8 -1
  65. package/src/route-renderer/page-loading/dependency-resolver.js +5 -7
  66. package/src/route-renderer/page-loading/page-dependency-bundling.d.ts +1 -1
  67. package/src/route-renderer/page-loading/page-dependency-bundling.js +41 -19
  68. package/src/route-renderer/route-renderer.d.ts +28 -26
  69. package/src/route-renderer/route-renderer.js +4 -27
  70. package/src/router/README.md +16 -19
  71. package/src/router/server/route-registry.d.ts +78 -0
  72. package/src/router/server/route-registry.js +262 -0
  73. package/src/services/README.md +1 -2
  74. package/src/services/assets/asset-processing-service/assets.types.d.ts +3 -0
  75. package/src/services/assets/asset-processing-service/index.d.ts +1 -0
  76. package/src/services/assets/asset-processing-service/index.js +1 -0
  77. package/src/services/assets/asset-processing-service/page-package.d.ts +3 -0
  78. package/src/services/assets/asset-processing-service/page-package.js +74 -0
  79. package/src/services/assets/asset-processing-service/processors/base/base-script-processor.js +4 -4
  80. package/src/services/assets/asset-processing-service/processors/script/content-script.processor.js +6 -3
  81. package/src/services/assets/asset-processing-service/processors/script/file-script.processor.js +9 -3
  82. package/src/services/assets/asset-processing-service/processors/script/node-module-script.processor.js +4 -2
  83. package/src/services/assets/asset-processing-service/processors/stylesheet/content-stylesheet.processor.js +2 -1
  84. package/src/services/assets/asset-processing-service/processors/stylesheet/file-stylesheet.processor.js +3 -1
  85. package/src/services/module-loading/node-bootstrap-plugin.js +15 -3
  86. package/src/static-site-generator/static-site-generator.d.ts +20 -21
  87. package/src/static-site-generator/static-site-generator.js +107 -140
  88. package/src/types/internal-types.d.ts +13 -12
  89. package/src/types/public-types.d.ts +46 -36
  90. package/src/watchers/project-watcher.test-helpers.js +5 -5
  91. package/src/route-renderer/orchestration/boundary-planning.service.d.ts +0 -25
  92. package/src/route-renderer/orchestration/boundary-planning.service.js +0 -97
  93. package/src/route-renderer/orchestration/page-packaging.service.d.ts +0 -16
  94. package/src/route-renderer/orchestration/page-packaging.service.js +0 -66
  95. package/src/route-renderer/orchestration/queued-boundary-runtime.service.d.ts +0 -89
  96. package/src/route-renderer/orchestration/render-execution.service.d.ts +0 -43
  97. package/src/route-renderer/orchestration/render-execution.service.js +0 -106
  98. package/src/route-renderer/orchestration/render-preparation.service.d.ts +0 -120
  99. package/src/route-renderer/orchestration/route-shell-composer.service.d.ts +0 -50
  100. package/src/route-renderer/orchestration/route-shell-composer.service.js +0 -81
  101. package/src/router/server/fs-router-scanner.d.ts +0 -41
  102. package/src/router/server/fs-router-scanner.js +0 -161
  103. package/src/router/server/fs-router.d.ts +0 -26
  104. package/src/router/server/fs-router.js +0 -100
  105. package/src/services/runtime-state/runtime-specifier-registry.service.d.ts +0 -69
  106. package/src/services/runtime-state/runtime-specifier-registry.service.js +0 -37
@@ -1,19 +1,20 @@
1
1
  import {
2
+ createPagePackage
2
3
  } from "../../services/assets/asset-processing-service/index.js";
3
4
  import { HtmlTransformerService } from "../../services/html/html-transformer.service.js";
4
5
  import { invariant } from "../../utils/invariant.js";
5
6
  import { HttpError } from "../../errors/http-error.js";
6
7
  import { DependencyResolverService } from "../page-loading/dependency-resolver.js";
7
8
  import { PageModuleLoaderService } from "../page-loading/page-module-loader.js";
8
- import { PagePackagingService } from "./page-packaging.service.js";
9
- import { RenderExecutionService } from "./render-execution.service.js";
10
- import { RenderPreparationService } from "./render-preparation.service.js";
11
- import { RouteShellComposer } from "./route-shell-composer.service.js";
12
- import { normalizeBoundaryArtifactHtml } from "./render-output.utils.js";
13
- import { getComponentRenderContext, runWithComponentRenderContext } from "./component-render-context.js";
9
+ import { OwnershipValidationService } from "./ownership-validation.service.js";
14
10
  import {
15
- QueuedBoundaryRuntimeService
16
- } from "./queued-boundary-runtime.service.js";
11
+ RouteRenderOrchestrator
12
+ } from "./route-render-orchestrator.js";
13
+ import { normalizeUnresolvedMarkerArtifactHtml } from "./render-output.utils.js";
14
+ import {
15
+ ForeignSubtreeExecutionService
16
+ } from "./foreign-subtree-execution.service.js";
17
+ import {} from "./queued-foreign-subtree-resolution.service.js";
17
18
  class IntegrationRenderer {
18
19
  appConfig;
19
20
  assetProcessingService;
@@ -24,11 +25,8 @@ class IntegrationRenderer {
24
25
  runtimeOrigin;
25
26
  dependencyResolverService;
26
27
  pageModuleLoaderService;
27
- renderPreparationService;
28
- renderExecutionService;
29
- pagePackagingService;
30
- routeShellComposer = new RouteShellComposer();
31
- queuedBoundaryRuntimeService = new QueuedBoundaryRuntimeService();
28
+ routeRenderOrchestrator;
29
+ foreignSubtreeExecutionService = new ForeignSubtreeExecutionService();
32
30
  DOC_TYPE = "<!DOCTYPE html>";
33
31
  /**
34
32
  * Loads one route module through the owning renderer's import path.
@@ -42,7 +40,7 @@ class IntegrationRenderer {
42
40
  return this.importPageFile(file, options);
43
41
  }
44
42
  /**
45
- * Reads the execution-scoped foreign renderer cache from one boundary input.
43
+ * Reads the execution-scoped owning-renderer cache from one render input.
46
44
  *
47
45
  * Shared page/layout/document shell helpers pass one cache through
48
46
  * `integrationContext` so repeated delegation to the same foreign integration
@@ -51,16 +49,16 @@ class IntegrationRenderer {
51
49
  * stored on the renderer, which avoids leaking mutable integration state across
52
50
  * requests while still preventing redundant renderer initialization.
53
51
  *
54
- * @param integrationContext - Optional boundary context carried with one render input.
52
+ * @param integrationContext - Optional render context carried with one render input.
55
53
  * @returns The current execution cache when present.
56
54
  */
57
- getBoundaryRendererCache(integrationContext) {
55
+ getOwningRendererCache(integrationContext) {
58
56
  if (integrationContext?.rendererCache instanceof Map) {
59
57
  return integrationContext.rendererCache;
60
58
  }
61
59
  return void 0;
62
60
  }
63
- getRegisteredBoundaryOwner(component) {
61
+ getForeignOwnerIntegrationName(component) {
64
62
  const integrationName = component.config?.integration ?? component.config?.__eco?.integration;
65
63
  if (!integrationName || integrationName === this.name) {
66
64
  return void 0;
@@ -68,18 +66,18 @@ class IntegrationRenderer {
68
66
  return this.appConfig.integrations.some((integration) => integration.name === integrationName) ? integrationName : void 0;
69
67
  }
70
68
  /**
71
- * Attaches an execution-scoped foreign renderer cache to one boundary input.
69
+ * Attaches an execution-scoped owning-renderer cache to one render input.
72
70
  *
73
71
  * Foreign-owned page, layout, or document shells may delegate several times in
74
72
  * the same render flow. Threading the cache through `integrationContext`
75
- * preserves renderer reuse without changing the public boundary input contract.
73
+ * preserves renderer reuse without changing the public render input contract.
76
74
  * Existing integration-specific context is preserved and augmented.
77
75
  *
78
- * @param input - Original boundary render input.
76
+ * @param input - Original render input.
79
77
  * @param rendererCache - Execution-scoped renderer cache to propagate.
80
- * @returns Boundary input augmented with the shared renderer cache.
78
+ * @returns Render input augmented with the shared renderer cache.
81
79
  */
82
- withBoundaryRendererCache(input, rendererCache) {
80
+ withOwningRendererCache(input, rendererCache) {
83
81
  const integrationContext = input.integrationContext;
84
82
  const sharedRendererCache = rendererCache;
85
83
  return {
@@ -175,14 +173,14 @@ class IntegrationRenderer {
175
173
  const resolvedDependencies = this.htmlTransformer.dedupeProcessedAssets(
176
174
  await this.resolveDependencies(componentsToResolve)
177
175
  );
178
- this.htmlTransformer.setPagePackage(this.pagePackagingService.createPagePackage(resolvedDependencies));
176
+ this.htmlTransformer.setPagePackage(createPagePackage(resolvedDependencies));
179
177
  return resolvedDependencies;
180
178
  }
181
179
  /**
182
180
  * Merges component-scoped assets into the active HTML transformer state.
183
181
  *
184
182
  * Explicit page, layout, and document shell composition can produce assets at
185
- * each boundary. This helper deduplicates those groups and folds them back into
183
+ * each foreign subtree. This helper deduplicates those groups and folds them back into
186
184
  * the transformer so downstream HTML finalization sees one canonical asset set.
187
185
  *
188
186
  * @param assetGroups - Optional groups of processed assets to merge.
@@ -199,7 +197,7 @@ class IntegrationRenderer {
199
197
  ...this.htmlTransformer.getProcessedDependencies(),
200
198
  ...nextDependencies
201
199
  ]);
202
- this.htmlTransformer.setPagePackage(this.pagePackagingService.createPagePackage(mergedDependencies));
200
+ this.htmlTransformer.setPagePackage(createPagePackage(mergedDependencies));
203
201
  return nextDependencies;
204
202
  }
205
203
  /**
@@ -226,7 +224,7 @@ class IntegrationRenderer {
226
224
  *
227
225
  * Same-integration views can optionally stream or render inline via the caller's
228
226
  * `renderInline()` hook. Once a view may cross integration boundaries, this
229
- * helper routes the render through `renderComponentBoundary()` instead so mixed
227
+ * helper routes the render through `renderComponentWithForeignChildren()` instead so mixed
230
228
  * shells can reuse the execution-scoped renderer cache and resolve nested
231
229
  * foreign ownership before the partial response is returned.
232
230
  *
@@ -234,17 +232,17 @@ class IntegrationRenderer {
234
232
  * @returns HTML response for the partial render.
235
233
  */
236
234
  async renderPartialViewResponse(input) {
237
- return this.routeShellComposer.renderPartialViewResponse(input, {
238
- hasForeignBoundaryDescendants: (component) => this.hasForeignBoundaryDescendants(component),
239
- createHtmlResponse: (body, ctx) => this.createHtmlResponse(body, ctx),
240
- renderComponentBoundary: (boundaryInput) => this.renderComponentBoundary(boundaryInput),
241
- prepareViewDependencies: (view, layout) => this.prepareViewDependencies(view, layout),
242
- getHtmlTemplate: () => this.getHtmlTemplate(),
243
- resolveViewMetadata: (view, props) => this.resolveViewMetadata(view, props),
244
- appendProcessedDependencies: (...assetGroups) => this.appendProcessedDependencies(...assetGroups),
245
- finalizeResolvedHtml: (options) => this.finalizeResolvedHtml(options),
246
- docType: this.DOC_TYPE
235
+ if (input.renderInline && !this.hasForeignChildDescendants(input.view)) {
236
+ return this.createHtmlResponse(await input.renderInline(), input.ctx);
237
+ }
238
+ const rendererCache = /* @__PURE__ */ new Map();
239
+ const viewRender = await this.renderComponentWithForeignChildren({
240
+ component: input.view,
241
+ props: input.props ?? {},
242
+ integrationContext: { rendererCache }
247
243
  });
244
+ const html = input.transformHtml ? input.transformHtml(viewRender.html) : viewRender.html;
245
+ return this.createHtmlResponse(html, input.ctx);
248
246
  }
249
247
  /**
250
248
  * Renders an explicit view through optional layout and document shells.
@@ -259,57 +257,97 @@ class IntegrationRenderer {
259
257
  * @returns HTML response for the explicit view render.
260
258
  */
261
259
  async renderViewWithDocumentShell(input) {
262
- return this.routeShellComposer.renderViewWithDocumentShell(input, {
263
- hasForeignBoundaryDescendants: (component) => this.hasForeignBoundaryDescendants(component),
264
- createHtmlResponse: (body, ctx) => this.createHtmlResponse(body, ctx),
265
- renderComponentBoundary: (boundaryInput) => this.renderComponentBoundary(boundaryInput),
266
- prepareViewDependencies: (view, layout) => this.prepareViewDependencies(view, layout),
267
- getHtmlTemplate: () => this.getHtmlTemplate(),
268
- resolveViewMetadata: (view, props) => this.resolveViewMetadata(view, props),
269
- appendProcessedDependencies: (...assetGroups) => this.appendProcessedDependencies(...assetGroups),
270
- finalizeResolvedHtml: (options) => this.finalizeResolvedHtml(options),
271
- docType: this.DOC_TYPE
260
+ const normalizedProps = input.props ?? {};
261
+ if (input.ctx.partial) {
262
+ return this.renderPartialViewResponse(input);
263
+ }
264
+ await this.prepareViewDependencies(input.view, input.layout);
265
+ const HtmlTemplate = await this.getHtmlTemplate();
266
+ const metadata = await this.resolveViewMetadata(input.view, input.props);
267
+ const { documentHtml } = await this.composeDocumentShell({
268
+ primaryComponent: input.view,
269
+ primaryProps: normalizedProps,
270
+ layout: input.layout ? {
271
+ component: input.layout,
272
+ props: {}
273
+ } : void 0,
274
+ htmlTemplate: HtmlTemplate,
275
+ documentProps: {
276
+ metadata,
277
+ pageProps: normalizedProps
278
+ }
279
+ });
280
+ const html = await this.finalizeResolvedHtml({
281
+ html: `${this.DOC_TYPE}${documentHtml}`,
282
+ partial: false
272
283
  });
284
+ return this.createHtmlResponse(html, input.ctx);
273
285
  }
274
286
  /**
275
287
  * Renders a route page through optional layout and document shells.
276
288
  *
277
- * Route rendering and explicit view rendering now share the same boundary-owned
289
+ * Route rendering and explicit view rendering now share the same renderer-owned
278
290
  * shell composition model. This helper composes page, layout, and html template
279
- * boundaries while threading one execution-scoped renderer cache through every
280
- * delegated boundary so foreign shell ownership remains stable and renderer
291
+ * renders while threading one execution-scoped renderer cache through every
292
+ * delegated foreign subtree so foreign shell ownership remains stable and renderer
281
293
  * initialization is reused inside the current request.
282
294
  *
283
295
  * @param input - Page, layout, document, and metadata inputs for the route render.
284
296
  * @returns Final serialized document HTML including the doctype prefix.
285
297
  */
286
298
  async renderPageWithDocumentShell(input) {
287
- return this.routeShellComposer.renderPageWithDocumentShell(input, {
288
- hasForeignBoundaryDescendants: (component) => this.hasForeignBoundaryDescendants(component),
289
- createHtmlResponse: (body, ctx) => this.createHtmlResponse(body, ctx),
290
- renderComponentBoundary: (boundaryInput) => this.renderComponentBoundary(boundaryInput),
291
- prepareViewDependencies: (view, layout) => this.prepareViewDependencies(view, layout),
292
- getHtmlTemplate: () => this.getHtmlTemplate(),
293
- resolveViewMetadata: (view, props) => this.resolveViewMetadata(view, props),
294
- appendProcessedDependencies: (...assetGroups) => this.appendProcessedDependencies(...assetGroups),
295
- finalizeResolvedHtml: (options) => this.finalizeResolvedHtml(options),
296
- docType: this.DOC_TYPE
299
+ const { documentHtml: composedDocumentHtml } = await this.composeDocumentShell({
300
+ primaryComponent: input.page.component,
301
+ primaryProps: input.page.props,
302
+ layout: input.layout,
303
+ htmlTemplate: input.htmlTemplate,
304
+ documentProps: {
305
+ metadata: input.metadata,
306
+ pageProps: input.pageProps,
307
+ ...input.documentProps ?? {}
308
+ }
297
309
  });
310
+ const documentHtml = input.transformDocumentHtml ? input.transformDocumentHtml(composedDocumentHtml) : composedDocumentHtml;
311
+ return `${this.DOC_TYPE}${documentHtml}`;
312
+ }
313
+ async composeDocumentShell(input) {
314
+ const rendererCache = /* @__PURE__ */ new Map();
315
+ const primaryRender = await this.renderComponentWithForeignChildren({
316
+ component: input.primaryComponent,
317
+ props: input.primaryProps,
318
+ integrationContext: { rendererCache }
319
+ });
320
+ const layoutRender = input.layout ? await this.renderComponentWithForeignChildren({
321
+ component: input.layout.component,
322
+ props: input.layout.props ?? {},
323
+ children: primaryRender.html,
324
+ integrationContext: { rendererCache }
325
+ }) : void 0;
326
+ const documentRender = await this.renderComponentWithForeignChildren({
327
+ component: input.htmlTemplate,
328
+ props: input.documentProps,
329
+ children: layoutRender?.html ?? primaryRender.html,
330
+ integrationContext: { rendererCache }
331
+ });
332
+ this.appendProcessedDependencies(primaryRender.assets, layoutRender?.assets, documentRender.assets);
333
+ return {
334
+ documentHtml: documentRender.html
335
+ };
298
336
  }
299
337
  /**
300
- * Renders one string-first component boundary and collects its assets.
338
+ * Renders one string-first component with serialized children and collects its assets.
301
339
  *
302
- * String-oriented integrations frequently share the same boundary contract:
340
+ * String-oriented integrations frequently share the same component contract:
303
341
  * pass serialized children through props, coerce the render result to HTML, and
304
342
  * attach any component-scoped dependencies. This helper centralizes that flow
305
343
  * so integrations can opt into shared orchestration without repeating the same
306
- * boundary boilerplate.
344
+ * string-render boilerplate.
307
345
  *
308
- * @param input - Boundary render input.
346
+ * @param input - Component render input.
309
347
  * @param component - String-oriented component implementation to execute.
310
348
  * @returns Structured component render result for orchestration paths.
311
349
  */
312
- async renderStringComponentBoundary(input, component) {
350
+ async renderStringComponentWithSerializedChildren(input, component) {
313
351
  const props = input.children === void 0 ? input.props : { ...input.props, children: input.children };
314
352
  const content = await component(props);
315
353
  const html = String(content);
@@ -322,79 +360,51 @@ class IntegrationRenderer {
322
360
  assets
323
361
  };
324
362
  }
325
- getBoundaryTokenPrefix() {
326
- return `__${this.name}_boundary__`;
363
+ getForeignSubtreeTokenPrefix() {
364
+ return `__${this.name}_foreign_subtree__`;
327
365
  }
328
- getBoundaryRuntimeContextKey() {
329
- return `__${this.name}_boundary_runtime__`;
366
+ getForeignSubtreeResolutionContextKey() {
367
+ return `__${this.name}_foreign_subtree_runtime__`;
330
368
  }
331
- getQueuedBoundaryRuntime(input, runtimeContextKey = this.getBoundaryRuntimeContextKey()) {
332
- return this.queuedBoundaryRuntimeService.getRuntimeContext(input, runtimeContextKey);
333
- }
334
- async resolveQueuedBoundaryTokens(html, queuedResolutionsByToken, resolveToken) {
335
- let resolvedHtml = html;
336
- for (const token of queuedResolutionsByToken.keys()) {
337
- if (!resolvedHtml.includes(token)) {
338
- continue;
339
- }
340
- resolvedHtml = resolvedHtml.split(token).join(await resolveToken(token));
341
- }
342
- return resolvedHtml;
343
- }
344
- createQueuedBoundaryRuntime(options) {
345
- return this.queuedBoundaryRuntimeService.createRuntime({
346
- boundaryInput: options.boundaryInput,
369
+ createQueuedForeignSubtreeExecutionRuntime(options) {
370
+ return this.foreignSubtreeExecutionService.createQueuedRuntime({
371
+ renderInput: options.renderInput,
347
372
  rendererCache: options.rendererCache,
348
- runtimeContextKey: options.runtimeContextKey ?? this.getBoundaryRuntimeContextKey(),
349
- tokenPrefix: options.tokenPrefix ?? this.getBoundaryTokenPrefix(),
350
- shouldQueueBoundary: (input) => this.shouldResolveBoundaryInOwningRenderer(input),
373
+ runtimeContextKey: options.runtimeContextKey ?? this.getForeignSubtreeResolutionContextKey(),
374
+ tokenPrefix: options.tokenPrefix ?? this.getForeignSubtreeTokenPrefix(),
351
375
  createRuntimeContext: options.createRuntimeContext
352
376
  });
353
377
  }
354
- async resolveRendererOwnedQueuedBoundaryHtml(options) {
355
- return this.queuedBoundaryRuntimeService.resolveQueuedHtml({
356
- html: options.html,
357
- runtimeContext: options.runtimeContext,
358
- queueLabel: options.queueLabel,
359
- renderQueuedChildren: options.renderQueuedChildren,
360
- resolveBoundary: (input, rendererCache) => this.resolveBoundaryPayloadInOwningRenderer(
361
- input,
362
- rendererCache
363
- ),
364
- applyAttributesToFirstElement: (html, attributes) => this.htmlTransformer.applyAttributesToFirstElement(html, attributes),
365
- dedupeProcessedAssets: (assets) => this.htmlTransformer.dedupeProcessedAssets(assets)
366
- });
378
+ getQueuedForeignSubtreeResolutionContext(input) {
379
+ return this.foreignSubtreeExecutionService.getQueuedRuntimeContext(
380
+ input,
381
+ this.getForeignSubtreeResolutionContextKey()
382
+ );
367
383
  }
368
384
  /**
369
385
  * Renders a string-first component, then resolves any queued foreign
370
386
  * boundaries before returning final component HTML.
371
387
  */
372
- async renderStringComponentBoundaryWithQueuedForeignBoundaries(input, component) {
373
- const componentRender = await this.renderStringComponentBoundary(input, component);
374
- const queuedBoundaryResolution = await this.resolveRendererOwnedQueuedBoundaryHtml({
388
+ async renderStringComponentWithQueuedForeignSubtrees(input, component) {
389
+ const componentRender = await this.renderStringComponentWithSerializedChildren(input, component);
390
+ const queuedForeignSubtreeResolution = await this.foreignSubtreeExecutionService.resolveStringQueuedHtml({
391
+ currentIntegrationName: this.name,
392
+ renderInput: input,
375
393
  html: componentRender.html,
376
- runtimeContext: this.getQueuedBoundaryRuntime(input),
394
+ runtimeContextKey: this.getForeignSubtreeResolutionContextKey(),
377
395
  queueLabel: "String",
378
- renderQueuedChildren: async (children, _runtimeContext, queuedResolutionsByToken, resolveToken) => {
379
- if (children === void 0) {
380
- return { assets: [], html: void 0 };
381
- }
382
- const html = await this.resolveQueuedBoundaryTokens(
383
- typeof children === "string" ? children : String(children ?? ""),
384
- queuedResolutionsByToken,
385
- resolveToken
386
- );
387
- return { assets: [], html };
388
- }
396
+ getOwningRenderer: (integrationName, rendererCache) => this.getIntegrationRendererForName(integrationName, rendererCache),
397
+ applyAttributesToFirstElement: (html, attributes) => this.htmlTransformer.applyAttributesToFirstElement(html, attributes),
398
+ dedupeProcessedAssets: (assets) => this.htmlTransformer.dedupeProcessedAssets(assets)
389
399
  });
390
400
  const mergedAssets = this.htmlTransformer.dedupeProcessedAssets([
391
401
  ...componentRender.assets ?? [],
392
- ...queuedBoundaryResolution.assets
402
+ ...queuedForeignSubtreeResolution.assets
393
403
  ]);
394
404
  return {
395
405
  ...componentRender,
396
- html: queuedBoundaryResolution.html,
397
- rootTag: this.getRootTagName(queuedBoundaryResolution.html),
406
+ html: queuedForeignSubtreeResolution.html,
407
+ rootTag: this.getRootTagName(queuedForeignSubtreeResolution.html),
398
408
  assets: mergedAssets.length > 0 ? mergedAssets : void 0
399
409
  };
400
410
  }
@@ -408,16 +418,14 @@ class IntegrationRenderer {
408
418
  this.appConfig = appConfig;
409
419
  this.assetProcessingService = assetProcessingService;
410
420
  this.htmlTransformer = new HtmlTransformerService();
411
- this.pagePackagingService = new PagePackagingService();
412
421
  this.resolvedIntegrationDependencies = resolvedIntegrationDependencies || [];
413
422
  this.rendererModules = rendererModules ?? appConfig.runtime?.rendererModuleContext;
414
423
  this.runtimeOrigin = runtimeOrigin;
415
424
  this.dependencyResolverService = new DependencyResolverService(appConfig, assetProcessingService);
416
425
  this.pageModuleLoaderService = new PageModuleLoaderService(appConfig, runtimeOrigin);
417
- this.renderPreparationService = new RenderPreparationService(appConfig, assetProcessingService, {
418
- pagePackagingService: this.pagePackagingService
426
+ this.routeRenderOrchestrator = new RouteRenderOrchestrator(appConfig, assetProcessingService, {
427
+ ownershipValidationService: new OwnershipValidationService(appConfig)
419
428
  });
420
- this.renderExecutionService = new RenderExecutionService();
421
429
  }
422
430
  /**
423
431
  * Returns the HTML path from the provided file path.
@@ -451,34 +459,6 @@ class IntegrationRenderer {
451
459
  invariant(false, `Error importing HtmlTemplate: ${error}`);
452
460
  }
453
461
  }
454
- /**
455
- * Returns the static props for the page.
456
- * It calls the provided getStaticProps function with the given options.
457
- *
458
- * @param getStaticProps - The function to get static props.
459
- * @param options - The options to pass to the getStaticProps function.
460
- * @returns The static props and metadata.
461
- */
462
- async getStaticProps(getStaticProps, options) {
463
- return this.pageModuleLoaderService.getStaticPropsForPage({
464
- getStaticProps,
465
- params: options?.params
466
- });
467
- }
468
- /**
469
- * Returns the metadata properties for the page.
470
- * It calls the provided getMetadata function with the given context.
471
- *
472
- * @param getMetadata - The function to get metadata.
473
- * @param context - The context to pass to the getMetadata function.
474
- * @returns The metadata properties.
475
- */
476
- async getMetadataProps(getMetadata, { props, params, query }) {
477
- return this.pageModuleLoaderService.getMetadataPropsForPage({
478
- getMetadata,
479
- context: { props, params, query }
480
- });
481
- }
482
462
  usesIntegrationPageImporter(_file) {
483
463
  return false;
484
464
  }
@@ -576,6 +556,98 @@ class IntegrationRenderer {
576
556
  async processComponentDependencies(components) {
577
557
  return this.dependencyResolverService.processComponentDependencies(components, this.name);
578
558
  }
559
+ /**
560
+ * Builds the internal route-render adapter consumed by `RouteRenderOrchestrator`.
561
+ *
562
+ * The route orchestrator needs a narrow orchestration contract, but those hooks should
563
+ * not become public API on the renderer base class. Keeping the adapter object
564
+ * local to the execution path lets the orchestrator depend on one explicit seam while
565
+ * subclasses continue to override protected renderer behavior directly.
566
+ */
567
+ createRouteRenderOrchestratorAdapter() {
568
+ return {
569
+ name: this.name,
570
+ resolveRouteRenderInputs: (routeOptions) => this.resolveRouteRenderInputs(routeOptions),
571
+ resolveRouteAssets: (input) => this.resolveRouteAssets(input),
572
+ resolveRoutePageComponentRender: (input) => this.resolveRoutePageComponentRender(input),
573
+ renderRouteBody: (renderOptions) => this.renderRouteBody(renderOptions),
574
+ getRouteHtmlFinalization: (renderOptions) => this.getRouteHtmlFinalization(renderOptions),
575
+ transformRouteResponse: (response) => this.transformRouteResponse(response)
576
+ };
577
+ }
578
+ async resolveRouteRenderInputs(routeOptions) {
579
+ const pageModule = await this.pageModuleLoaderService.resolvePageModule({
580
+ file: routeOptions.file,
581
+ importPageFileFn: (targetFile) => this.importPageFile(targetFile)
582
+ });
583
+ const { Page, integrationSpecificProps } = pageModule;
584
+ const HtmlTemplate = await this.getHtmlTemplate();
585
+ const Layout = Page.config?.layout;
586
+ const { props, metadata } = await this.pageModuleLoaderService.resolvePageData({
587
+ pageModule,
588
+ routeOptions
589
+ });
590
+ return {
591
+ Page,
592
+ HtmlTemplate,
593
+ Layout,
594
+ props,
595
+ metadata,
596
+ integrationSpecificProps
597
+ };
598
+ }
599
+ async resolveRouteAssets(input) {
600
+ return {
601
+ resolvedDependencies: await this.resolveDependencies(input.components),
602
+ pageBrowserGraph: await this.buildPageBrowserGraph(input.routeOptions.file)
603
+ };
604
+ }
605
+ async resolveRoutePageComponentRender(input) {
606
+ if (!this.shouldRenderPageComponent({ Page: input.Page, Layout: input.Layout, options: input.routeOptions })) {
607
+ return void 0;
608
+ }
609
+ return this.renderComponentWithForeignChildren({
610
+ component: input.Page,
611
+ props: {
612
+ ...input.props,
613
+ params: input.routeOptions.params || {},
614
+ query: input.routeOptions.query || {}
615
+ },
616
+ integrationContext: {
617
+ componentInstanceId: "eco-page-root"
618
+ }
619
+ });
620
+ }
621
+ async renderRouteBody(renderOptions) {
622
+ return this.render(renderOptions);
623
+ }
624
+ getRouteHtmlFinalization(renderOptions) {
625
+ const componentRootAttributes = renderOptions.componentRender?.canAttachAttributes && renderOptions.componentRender.rootAttributes && Object.keys(renderOptions.componentRender.rootAttributes).length > 0 ? renderOptions.componentRender.rootAttributes : void 0;
626
+ const documentAttributes = this.getDocumentAttributes(renderOptions);
627
+ const hasStructuralFinalization = componentRootAttributes && Object.keys(componentRootAttributes).length > 0 || documentAttributes && Object.keys(documentAttributes).length > 0;
628
+ if (!hasStructuralFinalization) {
629
+ return {};
630
+ }
631
+ return {
632
+ finalizeHtml: (html) => {
633
+ let renderedHtml = html;
634
+ if (componentRootAttributes) {
635
+ renderedHtml = this.htmlTransformer.applyAttributesToFirstBodyElement(
636
+ renderedHtml,
637
+ componentRootAttributes
638
+ );
639
+ }
640
+ if (documentAttributes) {
641
+ renderedHtml = this.htmlTransformer.applyAttributesToHtmlElement(renderedHtml, documentAttributes);
642
+ }
643
+ return renderedHtml;
644
+ }
645
+ };
646
+ }
647
+ async transformRouteResponse(response) {
648
+ const transformedResponse = await this.htmlTransformer.transform(response);
649
+ return transformedResponse.body ?? await transformedResponse.text();
650
+ }
579
651
  /**
580
652
  * Prepares the render options for the integration renderer.
581
653
  * It imports the page file, collects dependencies, and prepares the render options.
@@ -583,25 +655,11 @@ class IntegrationRenderer {
583
655
  * @param options - The route renderer options.
584
656
  * @returns The prepared render options.
585
657
  */
586
- async prepareRenderOptions(options) {
587
- const preparedOptions = await this.renderPreparationService.prepare(options, this.name, {
588
- resolvePageModule: (file) => this.resolvePageModule(file),
589
- getHtmlTemplate: () => this.getHtmlTemplate(),
590
- resolvePageData: (pageModule, routeOptions) => this.resolvePageData(pageModule, routeOptions),
591
- resolveDependencies: (components) => this.resolveDependencies(components),
592
- buildRouteRenderAssets: (file) => this.buildRouteRenderAssets(file),
593
- shouldRenderPageComponent: (input) => this.shouldRenderPageComponent(input),
594
- renderPageComponent: ({ component, props }) => this.renderComponentBoundary({
595
- component,
596
- props,
597
- integrationContext: {
598
- componentInstanceId: "eco-page-root"
599
- }
600
- })
601
- });
602
- invariant(preparedOptions.pagePackage !== void 0, "Expected render preparation to produce a page package");
603
- this.htmlTransformer.setPagePackage(preparedOptions.pagePackage);
604
- return preparedOptions;
658
+ async prepareRenderOptions(options, adapter = this.createRouteRenderOrchestratorAdapter()) {
659
+ const renderOptions = await this.routeRenderOrchestrator.prepareRenderOptions(options, adapter);
660
+ invariant(renderOptions.pagePackage !== void 0, "Expected render preparation to produce a page package");
661
+ this.htmlTransformer.setPagePackage(renderOptions.pagePackage);
662
+ return renderOptions;
605
663
  }
606
664
  /**
607
665
  * Controls whether the page root should be rendered through `renderComponent()`
@@ -614,31 +672,13 @@ class IntegrationRenderer {
614
672
  shouldRenderPageComponent(_input) {
615
673
  return true;
616
674
  }
617
- /**
618
- * Resolves the page module and normalizes exports.
619
- */
620
- async resolvePageModule(file) {
621
- return this.pageModuleLoaderService.resolvePageModule({
622
- file,
623
- importPageFileFn: (targetFile) => this.importPageFile(targetFile)
624
- });
625
- }
626
- /**
627
- * Resolves static props and metadata for the page.
628
- */
629
- async resolvePageData(pageModule, options) {
630
- return this.pageModuleLoaderService.resolvePageData({
631
- pageModule,
632
- routeOptions: options
633
- });
634
- }
635
675
  /**
636
676
  * Executes the integration renderer with the provided options.
637
677
  *
638
678
  * Execution flow:
639
679
  * 1. Build normalized render options (`prepareRenderOptions`).
640
680
  * 2. Render the route body once.
641
- * 3. Reject unresolved route-level boundary artifacts.
681
+ * 3. Reject unresolved route-level eco-marker artifacts.
642
682
  * 4. Optionally apply root attributes for page/component root boundaries.
643
683
  * 5. Run HTML transformer with final dependency set.
644
684
  *
@@ -650,23 +690,15 @@ class IntegrationRenderer {
650
690
  * @returns Rendered route body plus effective cache strategy.
651
691
  */
652
692
  async execute(options) {
653
- return this.renderExecutionService.execute(options, {
654
- prepareRenderOptions: (routeOptions) => this.prepareRenderOptions(routeOptions),
655
- render: (renderOptions) => this.render(renderOptions),
656
- getDocumentAttributes: (renderOptions) => this.getDocumentAttributes(renderOptions),
657
- applyAttributesToHtmlElement: (html, attributes) => this.htmlTransformer.applyAttributesToHtmlElement(html, attributes),
658
- applyAttributesToFirstBodyElement: (html, attributes) => this.htmlTransformer.applyAttributesToFirstBodyElement(html, attributes),
659
- transformResponse: async (response) => {
660
- const transformedResponse = await this.htmlTransformer.transform(response);
661
- return transformedResponse.body ?? await transformedResponse.text();
662
- }
663
- });
693
+ const adapter = this.createRouteRenderOrchestratorAdapter();
694
+ const renderOptions = await this.prepareRenderOptions(options, adapter);
695
+ return this.routeRenderOrchestrator.executePrepared(renderOptions, adapter);
664
696
  }
665
697
  /**
666
698
  * Finalizes already-resolved HTML for explicit renderer-owned paths.
667
699
  *
668
700
  * This keeps document and root-attribute stamping plus HTML transformation
669
- * available after a renderer has completed nested boundary resolution without
701
+ * available after a renderer has completed nested foreign-subtree resolution without
670
702
  * routing back through shared route execution.
671
703
  */
672
704
  async finalizeResolvedHtml(options) {
@@ -720,82 +752,42 @@ class IntegrationRenderer {
720
752
  const integrationPlugin = this.appConfig.integrations.find(
721
753
  (integration) => integration.name === integrationName
722
754
  );
723
- invariant(!!integrationPlugin, `[ecopages] Integration not found for boundary owner: ${integrationName}`);
755
+ invariant(!!integrationPlugin, `[ecopages] Integration not found for foreign owner: ${integrationName}`);
724
756
  const renderer = integrationPlugin.initializeRenderer({
725
757
  rendererModules: this.appConfig.runtime?.rendererModuleContext
726
758
  });
727
759
  cache.set(integrationName, renderer);
728
760
  return renderer;
729
761
  }
730
- async resolveBoundaryInOwningRenderer(input, rendererCache) {
731
- const boundaryOwner = this.getRegisteredBoundaryOwner(input.component);
732
- if (!boundaryOwner) {
733
- return void 0;
734
- }
735
- const owningRenderer = this.getIntegrationRendererForName(boundaryOwner, rendererCache);
736
- if (owningRenderer === this || owningRenderer.name === this.name) {
737
- return void 0;
738
- }
739
- return await owningRenderer.renderComponentBoundary(this.withBoundaryRendererCache(input, rendererCache));
740
- }
741
- async resolveBoundaryPayloadInOwningRenderer(input, rendererCache) {
742
- const boundaryOwner = this.getRegisteredBoundaryOwner(input.component);
743
- if (!boundaryOwner) {
744
- return void 0;
745
- }
746
- const owningRenderer = this.getIntegrationRendererForName(boundaryOwner, rendererCache);
747
- if (owningRenderer === this || owningRenderer.name === this.name) {
748
- return void 0;
749
- }
750
- return await owningRenderer.renderBoundary(this.withBoundaryRendererCache(input, rendererCache));
751
- }
752
762
  /**
753
- * Renders one component under this integration's boundary runtime and resolves
754
- * any nested foreign boundaries captured during that render.
763
+ * Renders one component under this integration's foreign-child runtime and resolves
764
+ * any nested foreign children captured during that render.
755
765
  *
756
766
  * Without this wrapper, a component tree with foreign-owned descendants would
757
- * render them with no active boundary runtime, which bypasses the owning
758
- * renderer's nested-boundary handoff.
759
- */
760
- async renderComponentBoundary(input) {
761
- const rendererCache = this.getBoundaryRendererCache(input.integrationContext) ?? /* @__PURE__ */ new Map();
762
- const delegatedBoundaryRender = await this.resolveBoundaryInOwningRenderer(input, rendererCache);
763
- if (delegatedBoundaryRender) {
764
- return delegatedBoundaryRender;
765
- }
766
- const hasForeignBoundaries = this.hasForeignBoundaryDescendants(input.component);
767
- const activeRenderContext = getComponentRenderContext();
768
- if (!hasForeignBoundaries) {
769
- if (!activeRenderContext || activeRenderContext.currentIntegration === this.name) {
770
- return this.normalizeComponentBoundaryRender(await this.renderComponent(input));
771
- }
772
- const sameIntegrationExecution = await runWithComponentRenderContext(
773
- {
774
- currentIntegration: this.name
775
- },
776
- async () => this.renderComponent(input)
777
- );
778
- return this.normalizeComponentBoundaryRender(sameIntegrationExecution.value);
779
- }
780
- const execution = await runWithComponentRenderContext(
781
- {
782
- currentIntegration: this.name,
783
- boundaryRuntime: this.createComponentBoundaryRuntime({
784
- boundaryInput: input,
785
- rendererCache
786
- })
787
- },
788
- async () => this.renderComponent(input)
789
- );
790
- return this.normalizeComponentBoundaryRender(execution.value);
767
+ * render them with no active foreign-child runtime, which bypasses the owning
768
+ * renderer's nested foreign-child handoff.
769
+ */
770
+ async renderComponentWithForeignChildren(input) {
771
+ return await this.foreignSubtreeExecutionService.executeComponentRender({
772
+ currentIntegrationName: this.name,
773
+ input,
774
+ renderComponent: (renderInput) => this.renderComponent(renderInput),
775
+ normalizeComponentRenderOutput: (result) => this.normalizeComponentRenderOutput(result),
776
+ hasForeignChildDescendants: (component) => this.hasForeignChildDescendants(component),
777
+ createForeignChildRuntime: ({ renderInput, rendererCache }) => this.createForeignChildRuntime({
778
+ renderInput,
779
+ rendererCache
780
+ }),
781
+ getOwningRenderer: (integrationName, rendererCache) => this.getIntegrationRendererForName(integrationName, rendererCache)
782
+ });
791
783
  }
792
784
  /**
793
- * Compatibility boundary contract that exposes a narrower payload shape for
785
+ * Compatibility foreign-subtree contract that exposes a narrower payload shape for
794
786
  * future route-composition work while preserving the current
795
- * `renderComponentBoundary()` runtime semantics.
787
+ * `renderComponentWithForeignChildren()` runtime semantics.
796
788
  */
797
- async renderBoundary(input) {
798
- const result = await this.renderComponentBoundary(input);
789
+ async renderForeignSubtree(input) {
790
+ const result = await this.renderComponentWithForeignChildren(input);
799
791
  return {
800
792
  html: result.html,
801
793
  assets: result.assets ?? [],
@@ -805,24 +797,24 @@ class IntegrationRenderer {
805
797
  integrationName: result.integrationName
806
798
  };
807
799
  }
808
- normalizeComponentBoundaryRender(result) {
809
- const normalizedHtml = this.normalizeBoundaryArtifactHtml(result.html);
800
+ normalizeComponentRenderOutput(result) {
801
+ const normalizedHtml = this.normalizeUnresolvedMarkerArtifactHtml(result.html);
810
802
  return normalizedHtml === result.html ? result : {
811
803
  ...result,
812
804
  html: normalizedHtml
813
805
  };
814
806
  }
815
- normalizeBoundaryArtifactHtml(html) {
816
- return normalizeBoundaryArtifactHtml(html);
807
+ normalizeUnresolvedMarkerArtifactHtml(html) {
808
+ return normalizeUnresolvedMarkerArtifactHtml(html);
817
809
  }
818
810
  /**
819
811
  * Returns whether the component dependency tree crosses into another
820
812
  * integration.
821
813
  *
822
- * This keeps boundary-runtime setup narrow: same-integration trees can render
814
+ * This keeps foreign-child runtime setup narrow: same-integration trees can render
823
815
  * directly without paying the queue orchestration cost.
824
816
  */
825
- hasForeignBoundaryDescendants(component) {
817
+ hasForeignChildDescendants(component) {
826
818
  const stack = [component];
827
819
  const seen = /* @__PURE__ */ new Set();
828
820
  while (stack.length > 0) {
@@ -845,8 +837,8 @@ class IntegrationRenderer {
845
837
  * Default behavior delegates to `renderToResponse` in partial mode and wraps
846
838
  * the resulting HTML into the `ComponentRenderResult` contract.
847
839
  *
848
- * In boundary resolution, this method is the integration-owned step that turns an
849
- * already-resolved deferred boundary into concrete HTML, assets, and optional
840
+ * In foreign-subtree resolution, this method is the integration-owned step that turns an
841
+ * already-resolved deferred foreign subtree into concrete HTML, assets, and optional
850
842
  * root attributes.
851
843
  *
852
844
  * Integrations can override this for richer behavior (asset emission,
@@ -880,51 +872,36 @@ class IntegrationRenderer {
880
872
  return rootTag?.[1];
881
873
  }
882
874
  /**
883
- * Method to build route render assets.
875
+ * Builds the Page Browser Graph owned by this integration for one Page.
884
876
  * This method can be optionally overridden by the specific integration renderer.
885
877
  *
886
878
  * @param file - The file path to build assets for.
887
- * @returns The processed assets or undefined.
879
+ * @returns The structured Page Browser Graph or undefined.
888
880
  */
889
- buildRouteRenderAssets(_file) {
881
+ async buildPageBrowserGraph(_file) {
890
882
  return void 0;
891
883
  }
892
884
  /**
893
- * Creates the per-render boundary runtime adopted by the shared component
885
+ * Creates the per-render foreign-child runtime adopted by the shared component
894
886
  * render context.
895
887
  *
896
- * Real mixed-integration renderers should override this and keep foreign
897
- * boundary resolution inside their own renderer-owned queue. The base runtime
898
- * fails fast when a renderer crosses into a foreign owner without providing its
899
- * own handoff mechanism.
888
+ * The default runtime queues delegated foreign subtrees inside the owning
889
+ * renderer so string and markup renderers do not need to re-declare the same
890
+ * handoff boilerplate. Override only when a renderer needs custom runtime
891
+ * context or a different foreign-child execution strategy.
900
892
  */
901
- createComponentBoundaryRuntime(_options) {
902
- const decideBoundaryInterception = (input) => {
903
- if (!this.shouldResolveBoundaryInOwningRenderer(input)) {
904
- return { kind: "inline" };
905
- }
906
- throw new Error(
907
- `[ecopages] ${this.name} renderer crossed into ${input.targetIntegration} without a renderer-owned boundary runtime. Override createComponentBoundaryRuntime() to resolve foreign boundaries inside the owning renderer.`
908
- );
909
- };
910
- const runtime = {
911
- interceptBoundary: decideBoundaryInterception,
912
- interceptBoundarySync: decideBoundaryInterception
913
- };
914
- return runtime;
893
+ createForeignChildRuntime(options) {
894
+ return this.createQueuedForeignSubtreeExecutionRuntime({
895
+ renderInput: options.renderInput,
896
+ rendererCache: options.rendererCache
897
+ });
915
898
  }
916
899
  /**
917
- * Resolves whether a boundary should leave the current render pass and be
918
- * resolved by its owning renderer.
919
- *
920
- * Boundaries owned by the current integration always render inline. Foreign-
921
- * owned boundaries must be handed off by a renderer-owned runtime.
922
- *
923
- * @param input Boundary metadata for the active render pass.
924
- * @returns `true` when the boundary should leave the current pass; otherwise `false`.
900
+ * Creates an explicit fail-fast runtime for tests or renderers that do not
901
+ * support cross-integration foreign-child execution.
925
902
  */
926
- shouldResolveBoundaryInOwningRenderer(input) {
927
- return !!input.targetIntegration && input.targetIntegration !== input.currentIntegration;
903
+ createFailFastForeignChildRuntime() {
904
+ return this.foreignSubtreeExecutionService.createFailFastRuntime(this.name);
928
905
  }
929
906
  }
930
907
  export {