@ecopages/core 0.2.0-alpha.16 → 0.2.0-alpha.17

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.
package/CHANGELOG.md CHANGED
@@ -9,12 +9,15 @@ All notable changes to `@ecopages/core` are documented here.
9
9
  ### Features
10
10
 
11
11
  - Added app-owned runtime and build ownership around `createApp()`, host module loading, the browser-safe `eco` export, `eco.html()`, `eco.layout()`, and the published `EcoPagesAppConfig` surface.
12
+ - Added boundary-plan metadata and a compatibility `renderBoundary()` payload contract for mixed-renderer orchestration.
12
13
 
13
14
  ### Refactoring
14
15
 
15
16
  - Consolidated runtime state around shared module-loading services, app-owned build execution, and the universal `createApp()` boundary.
16
17
  - Simplified route-renderer orchestration around renderer-owned boundary runtimes, shared string-boundary queue helpers, and a smaller component render context.
17
18
  - Centralized shared integration renderer bootstrapping so package integrations only append renderer-specific config instead of duplicating core lifecycle wiring.
19
+ - Moved shared queued boundary resolution to attachment-policy payloads and constructor-injectable planning services.
20
+ - Extracted shared page, layout, and document-shell composition into a narrow `RouteShellComposer` while keeping renderer-owned boundary handoff in `IntegrationRenderer`.
18
21
  - Removed marker-era compatibility capture, the shared route-level fallback resolver, deprecated `@ecopages/core/node*` escape hatches, and other dead route-renderer internals.
19
22
 
20
23
  ### Bug Fixes
@@ -34,6 +37,7 @@ All notable changes to `@ecopages/core` are documented here.
34
37
 
35
38
  - Added regression coverage for app-owned runtime services, Node fallback paths, and cross-runtime invalidation behavior.
36
39
  - Strengthened the core ghtml integration tests so route and explicit render paths await real outcomes and cover `renderToResponse` behavior.
40
+ - Added core regression coverage for boundary plans, payload contracts, and typed mixed-boundary context flow.
37
41
 
38
42
  ---
39
43
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/core",
3
- "version": "0.2.0-alpha.16",
3
+ "version": "0.2.0-alpha.17",
4
4
  "description": "Core package for Ecopages",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -17,7 +17,7 @@
17
17
  "directory": "packages/core"
18
18
  },
19
19
  "dependencies": {
20
- "@ecopages/file-system": "0.2.0-alpha.16",
20
+ "@ecopages/file-system": "0.2.0-alpha.17",
21
21
  "@ecopages/logger": "^0.2.3",
22
22
  "@ecopages/scripts-injector": "^0.1.3",
23
23
  "@worker-tools/html-rewriter": "0.1.0-pre.19",
@@ -27,15 +27,6 @@ type RuntimeKind = 'node' | 'bun';
27
27
  * Provides a fluent interface for setting various configuration options and managing
28
28
  * application settings.
29
29
  *
30
- * @example
31
- * ```typescript
32
- * const config = new ConfigBuilder()
33
- * .setBaseUrl('https://example.com')
34
- * .setRootDir('./myproject')
35
- * .setSrcDir('source')
36
- * .build();
37
- * ```
38
- *
39
30
  * @remarks
40
31
  * The ConfigBuilder follows the builder pattern and allows for:
41
32
  * - Setting directory paths for various components (pages, includes, layouts, etc.)
@@ -14,7 +14,8 @@ import {
14
14
  updateAppBuildManifest
15
15
  } from "../build/build-adapter.js";
16
16
  import { createAppBuildExecutor } from "../build/dev-build-coordinator.js";
17
- import { GHTML_PLUGIN_NAME, ghtmlPlugin } from "../integrations/ghtml/ghtml.plugin.js";
17
+ import { GHTML_PLUGIN_NAME } from "../integrations/ghtml/ghtml.constants.js";
18
+ import { ghtmlPlugin } from "../integrations/ghtml/ghtml.plugin.js";
18
19
  import { createEcoComponentMetaPlugin } from "../plugins/eco-component-meta-plugin.js";
19
20
  import { createEcoComponentMetaTransform } from "../plugins/eco-component-meta-plugin.js";
20
21
  import {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  IntegrationRenderer
3
3
  } from "../../route-renderer/orchestration/integration-renderer.js";
4
- import { GHTML_PLUGIN_NAME } from "./ghtml.plugin.js";
4
+ import { GHTML_PLUGIN_NAME } from "./ghtml.constants.js";
5
5
  class GhtmlRenderer extends IntegrationRenderer {
6
6
  name = GHTML_PLUGIN_NAME;
7
7
  async renderComponent(input) {
@@ -0,0 +1 @@
1
+ export declare const GHTML_PLUGIN_NAME = "ghtml";
@@ -0,0 +1,4 @@
1
+ const GHTML_PLUGIN_NAME = "ghtml";
2
+ export {
3
+ GHTML_PLUGIN_NAME
4
+ };
@@ -1,9 +1,5 @@
1
1
  import { IntegrationPlugin, type IntegrationPluginConfig } from '../../plugins/integration-plugin.js';
2
2
  import { GhtmlRenderer } from './ghtml-renderer.js';
3
- /**
4
- * The name of the ghtml plugin
5
- */
6
- export declare const GHTML_PLUGIN_NAME = "ghtml";
7
3
  /**
8
4
  * The Ghtml plugin class
9
5
  * This plugin provides support for ghtml components in Ecopages
@@ -1,6 +1,6 @@
1
1
  import { IntegrationPlugin } from "../../plugins/integration-plugin.js";
2
2
  import { GhtmlRenderer } from "./ghtml-renderer.js";
3
- const GHTML_PLUGIN_NAME = "ghtml";
3
+ import { GHTML_PLUGIN_NAME } from "./ghtml.constants.js";
4
4
  class GhtmlPlugin extends IntegrationPlugin {
5
5
  renderer = GhtmlRenderer;
6
6
  constructor(options) {
@@ -15,7 +15,6 @@ function ghtmlPlugin(options) {
15
15
  return new GhtmlPlugin(options);
16
16
  }
17
17
  export {
18
- GHTML_PLUGIN_NAME,
19
18
  GhtmlPlugin,
20
19
  ghtmlPlugin
21
20
  };
@@ -26,6 +26,7 @@ Framework-owned orchestration services and renderer base class:
26
26
  - `integration-renderer.ts`: abstract base class that coordinates end-to-end route rendering.
27
27
  - `render-preparation.service.ts`: page module/data/dependency preparation before render.
28
28
  - `render-execution.service.ts`: render capture, unresolved boundary artifact enforcement, and finalization.
29
+ - `route-shell-composer.service.ts`: shared page/view/layout/html-template shell composition used by multiple integrations.
29
30
  - `queued-boundary-runtime.service.ts`: shared queued foreign-boundary runtime used directly by renderer-owned helpers, including string-first renderers.
30
31
 
31
32
  It also provides:
@@ -59,28 +60,65 @@ Default behavior:
59
60
  - renderer-owned component-boundary orchestration + component render artifacts.
60
61
  - global lazy trigger map + global injector bootstrap.
61
62
 
63
+ ## Mixed Renderer Mental Model
64
+
65
+ The current mixed-renderer contract has four phases:
66
+
67
+ 1. `render-preparation.service.ts` builds the route inputs and a conservative `boundaryPlan` from declared component dependencies.
68
+ 2. The selected integration renderer owns page, layout, document-shell, and explicit-view composition for that route.
69
+ 3. `route-shell-composer.service.ts` applies the shared page/view/layout/html-template composition flow while calling back into the owning renderer for each boundary render.
70
+ 4. Renderer-owned boundary runtimes resolve foreign nested components through the owning renderer and exchange a compatibility `renderBoundary()` payload with explicit attachment-policy semantics.
71
+ 5. `render-execution.service.ts` finalizes the response and fails if unresolved boundary artifact HTML survives the renderer-owned resolution pass.
72
+
73
+ Important:
74
+
75
+ - Renderer-owned deferral is intentional. Ecopages does not run a route-level fallback resolver after render completion.
76
+ - Boundary ownership is planned from declared component dependency metadata, not inferred purely from rendered HTML.
77
+ - Same-integration children do not have to pass through one universal string-only transport. Each renderer keeps its own child transport rules for same-integration trees.
78
+
79
+ ## Declared Foreign Child Contract
80
+
81
+ Mixed-integration component configs must declare every possible foreign child in `config.dependencies.components`. The planning pass uses those declarations to describe ownership transitions and surface invalid or unknown foreign owners before render execution.
82
+
83
+ Current behavior:
84
+
85
+ - Missing or unknown ownership is recorded on the route `boundaryPlan` as validation errors.
86
+ - Renderer-owned runtime discovery still resolves actual foreign descendants during render.
87
+ - If unresolved boundary artifact HTML reaches route finalization, Ecopages throws instead of attempting a route-level recovery pass.
88
+
62
89
  Global injector lifecycle notes:
63
90
 
64
91
  - The bootstrap remains active across client-side navigations.
65
92
  - On `eco:after-swap`, it prunes stale `ecopages/global-injector-map` scripts and calls `refresh()` so newly swapped `data-eco-trigger` elements can bind their lazy rules.
66
93
  - It must not call injector `cleanup()` on every swap, because that permanently disables future refresh work for the current runtime instance.
67
94
 
68
- ## Current Component Artifact Contract
95
+ ## Boundary Payload Contract
69
96
 
70
- Integration `renderComponent()` returns `ComponentRenderResult` with:
97
+ The compatibility boundary API is `renderBoundary()`. Today it wraps the existing `renderComponentBoundary()` behavior and returns a narrower payload:
71
98
 
72
99
  - `html`
73
- - `canAttachAttributes`
100
+ - `assets`
74
101
  - `rootTag`
75
102
  - `integrationName`
76
103
  - optional `rootAttributes`
77
- - optional `assets`
104
+ - `attachmentPolicy`
105
+
106
+ `renderComponent()` still returns `ComponentRenderResult` internally, including `canAttachAttributes`, because renderer-local implementations have not been collapsed into one universal boundary primitive.
107
+
108
+ Base orchestration uses the compatibility payload to:
78
109
 
79
- Current base orchestration behavior:
110
+ - keep queued foreign-boundary resolution renderer-owned
111
+ - apply root attributes only when `attachmentPolicy.kind === 'first-element'`
112
+ - preserve asset bubbling through the normal dependency pipeline
80
113
 
81
- - Calls `renderComponent()` for the page root component.
82
- - Merges returned `assets` into processed dependencies.
83
- - Applies returned `rootAttributes` to the first element under `<body>`.
114
+ The lower-level `ComponentRenderResult` currently includes:
115
+
116
+ - `html`
117
+ - `canAttachAttributes`
118
+ - `rootTag`
119
+ - `integrationName`
120
+ - optional `rootAttributes`
121
+ - optional `assets`
84
122
 
85
123
  When rendered output still contains unresolved boundary artifact HTML:
86
124
 
@@ -114,3 +152,5 @@ If you are reading this file to understand today's contract, you can stop at the
114
152
 
115
153
  - Deep multi-level mixed-integration trees now rely on renderer-owned boundary runtimes rather than a shared post-render graph resolver.
116
154
  - Each renderer still decides how to hand off foreign boundaries, so specialized runtimes remain appropriate where child serialization or hydration contracts differ.
155
+ - `boundaryPlan` is currently preparation-time metadata and diagnostics. It does not yet drive a full route-composer execution model.
156
+ - A narrow `RouteShellComposer` now owns shared shell composition, but a broader route composer that absorbs boundary ownership or execution flow is still deferred.
@@ -0,0 +1,25 @@
1
+ import type { EcoPagesAppConfig } from '../../types/internal-types.js';
2
+ import type { BoundaryPlan, EcoComponent } from '../../types/public-types.js';
3
+ type BoundaryPlanBuildInput = {
4
+ routeFile: string;
5
+ currentIntegrationName: string;
6
+ HtmlTemplate: EcoComponent;
7
+ Layout?: EcoComponent;
8
+ Page: EcoComponent;
9
+ };
10
+ /**
11
+ * Builds a declared ownership plan from the component dependency graph.
12
+ *
13
+ * The plan is intentionally conservative: it reflects declared component
14
+ * dependencies available during render preparation and records diagnostics for
15
+ * foreign ownership edges that cannot be validated against registered
16
+ * integrations or stable component metadata.
17
+ */
18
+ export declare class BoundaryPlanningService {
19
+ private readonly appConfig;
20
+ private nextSyntheticId;
21
+ constructor(appConfig: EcoPagesAppConfig);
22
+ buildPlan(input: BoundaryPlanBuildInput): BoundaryPlan;
23
+ private isRegisteredIntegration;
24
+ }
25
+ export {};
@@ -0,0 +1,97 @@
1
+ class BoundaryPlanningService {
2
+ appConfig;
3
+ nextSyntheticId = 0;
4
+ constructor(appConfig) {
5
+ this.appConfig = appConfig;
6
+ }
7
+ buildPlan(input) {
8
+ this.nextSyntheticId = 0;
9
+ const validationErrors = [];
10
+ const rendererNames = /* @__PURE__ */ new Set([input.currentIntegrationName]);
11
+ let foreignEdgeCount = 0;
12
+ const buildNode = (component, source, parentIntegrationName, lineage) => {
13
+ const integrationName = component.config?.integration ?? component.config?.__eco?.integration ?? parentIntegrationName;
14
+ const componentMeta = component.config?.__eco;
15
+ const isForeignToParent = integrationName !== parentIntegrationName;
16
+ const componentId = componentMeta?.id ?? componentMeta?.file ?? `${source}:${this.nextSyntheticId += 1}`;
17
+ rendererNames.add(integrationName);
18
+ if (isForeignToParent) {
19
+ foreignEdgeCount += 1;
20
+ if (!componentMeta) {
21
+ validationErrors.push({
22
+ code: "MISSING_COMPONENT_METADATA",
23
+ message: `[ecopages] Foreign boundary "${componentId}" must provide stable __eco metadata so ownership diagnostics stay actionable. Declared dependencies must include all possible foreign children.`,
24
+ componentId,
25
+ integrationName
26
+ });
27
+ }
28
+ if (!this.isRegisteredIntegration(integrationName, input.currentIntegrationName)) {
29
+ validationErrors.push({
30
+ code: "UNKNOWN_INTEGRATION_OWNER",
31
+ message: `[ecopages] Foreign boundary "${componentId}" references unknown integration owner "${integrationName}". Declared dependencies must include all possible foreign children and those integrations must be registered.`,
32
+ componentId,
33
+ componentFile: componentMeta?.file,
34
+ integrationName
35
+ });
36
+ }
37
+ }
38
+ const nextLineage = new Set(lineage);
39
+ nextLineage.add(component);
40
+ const children = (component.config?.dependencies?.components ?? []).flatMap((child) => {
41
+ if (!child || nextLineage.has(child)) {
42
+ return [];
43
+ }
44
+ return [buildNode(child, "dependency", integrationName, nextLineage)];
45
+ });
46
+ return {
47
+ id: componentId,
48
+ source,
49
+ ownership: {
50
+ integrationName,
51
+ componentId,
52
+ componentFile: componentMeta?.file,
53
+ isPageEntry: source === "page",
54
+ isForeignToParent
55
+ },
56
+ children,
57
+ declaredDependenciesValid: true
58
+ };
59
+ };
60
+ const roots = [
61
+ { component: input.HtmlTemplate, source: "html-template" },
62
+ ...input.Layout ? [{ component: input.Layout, source: "layout" }] : [],
63
+ { component: input.Page, source: "page" }
64
+ ];
65
+ const root = {
66
+ id: `route:${input.routeFile}`,
67
+ source: "route",
68
+ ownership: {
69
+ integrationName: input.currentIntegrationName,
70
+ componentId: `route:${input.routeFile}`,
71
+ componentFile: input.routeFile,
72
+ isPageEntry: false,
73
+ isForeignToParent: false
74
+ },
75
+ children: roots.map(
76
+ ({ component, source }) => buildNode(component, source, input.currentIntegrationName, /* @__PURE__ */ new Set())
77
+ ),
78
+ declaredDependenciesValid: validationErrors.length === 0
79
+ };
80
+ return {
81
+ root,
82
+ rendererNames: Array.from(rendererNames),
83
+ foreignEdgeCount,
84
+ hasValidationErrors: validationErrors.length > 0,
85
+ validationErrors
86
+ };
87
+ }
88
+ isRegisteredIntegration(integrationName, currentIntegrationName) {
89
+ if (integrationName === currentIntegrationName) {
90
+ return true;
91
+ }
92
+ return this.appConfig.integrations.some((integration) => integration.name === integrationName);
93
+ }
94
+ }
95
+ export {
96
+ BoundaryPlanningService
97
+ };
@@ -4,7 +4,7 @@
4
4
  * @module
5
5
  */
6
6
  import type { EcoPagesAppConfig, IHmrManager } from '../../types/internal-types.js';
7
- import type { ComponentRenderInput, ComponentRenderResult, EcoComponent, EcoComponentDependencies, EcoPageComponent, EcoPageFile, EcoPagesElement, GetMetadata, GetMetadataContext, GetStaticProps, HtmlTemplateProps, IntegrationRendererRenderOptions, PageMetadataProps, RouteRendererBody, RouteRendererOptions, RouteRenderResult } from '../../types/public-types.js';
7
+ import type { ComponentRenderInput, ComponentRenderResult, BoundaryRenderPayload, EcoComponent, EcoComponentDependencies, EcoPageComponent, EcoPageFile, EcoPagesElement, GetMetadata, GetMetadataContext, GetStaticProps, BaseIntegrationContext, HtmlTemplateProps, IntegrationRendererRenderOptions, PageMetadataProps, RouteRendererBody, RouteRendererOptions, RouteRenderResult } from '../../types/public-types.js';
8
8
  import { type AssetProcessingService, type ProcessedAsset } from '../../services/assets/asset-processing-service/index.js';
9
9
  import { HtmlTransformerService } from '../../services/html/html-transformer.service.js';
10
10
  import { HttpError } from '../../errors/http-error.js';
@@ -12,6 +12,7 @@ import { DependencyResolverService } from '../page-loading/dependency-resolver.j
12
12
  import { PageModuleLoaderService } from '../page-loading/page-module-loader.js';
13
13
  import { RenderExecutionService } from './render-execution.service.js';
14
14
  import { RenderPreparationService } from './render-preparation.service.js';
15
+ import { RouteShellComposer } from './route-shell-composer.service.js';
15
16
  import type { ComponentBoundaryRuntime } from './component-render-context.js';
16
17
  import { QueuedBoundaryRuntimeService, type QueuedBoundaryResolution, type QueuedBoundaryRuntimeContext } from './queued-boundary-runtime.service.js';
17
18
  type BoundaryRenderDecisionInput = {
@@ -56,6 +57,7 @@ export declare abstract class IntegrationRenderer<C = EcoPagesElement> {
56
57
  protected pageModuleLoaderService: PageModuleLoaderService;
57
58
  protected renderPreparationService: RenderPreparationService;
58
59
  protected renderExecutionService: RenderExecutionService;
60
+ protected readonly routeShellComposer: RouteShellComposer;
59
61
  protected readonly queuedBoundaryRuntimeService: QueuedBoundaryRuntimeService;
60
62
  protected DOC_TYPE: string;
61
63
  /**
@@ -238,11 +240,7 @@ export declare abstract class IntegrationRenderer<C = EcoPagesElement> {
238
240
  rendererCache: Map<string, IntegrationRenderer<any>>;
239
241
  runtimeContextKey?: string;
240
242
  tokenPrefix?: string;
241
- createRuntimeContext?: (integrationContext: {
242
- rendererCache?: Map<string, unknown>;
243
- componentInstanceId?: string;
244
- [key: string]: unknown;
245
- }, rendererCache: Map<string, unknown>) => TContext;
243
+ createRuntimeContext?: (integrationContext: BaseIntegrationContext & Record<string, unknown>, rendererCache: Map<string, unknown>) => TContext;
246
244
  }): ComponentBoundaryRuntime;
247
245
  protected resolveRendererOwnedQueuedBoundaryHtml<TContext extends QueuedBoundaryRuntimeContext>(options: {
248
246
  html: string;
@@ -460,6 +458,7 @@ export declare abstract class IntegrationRenderer<C = EcoPagesElement> {
460
458
  */
461
459
  abstract render(options: IntegrationRendererRenderOptions<C>): Promise<RouteRendererBody>;
462
460
  protected resolveBoundaryInOwningRenderer(input: ComponentRenderInput, rendererCache: Map<string, IntegrationRenderer<any>>): Promise<ComponentRenderResult | undefined>;
461
+ protected resolveBoundaryPayloadInOwningRenderer(input: ComponentRenderInput, rendererCache: Map<string, IntegrationRenderer<any>>): Promise<BoundaryRenderPayload | undefined>;
463
462
  /**
464
463
  * Renders one component under this integration's boundary runtime and resolves
465
464
  * any nested foreign boundaries captured during that render.
@@ -469,6 +468,12 @@ export declare abstract class IntegrationRenderer<C = EcoPagesElement> {
469
468
  * renderer's nested-boundary handoff.
470
469
  */
471
470
  renderComponentBoundary(input: ComponentRenderInput): Promise<ComponentRenderResult>;
471
+ /**
472
+ * Compatibility boundary contract that exposes a narrower payload shape for
473
+ * future route-composition work while preserving the current
474
+ * `renderComponentBoundary()` runtime semantics.
475
+ */
476
+ renderBoundary(input: ComponentRenderInput): Promise<BoundaryRenderPayload>;
472
477
  private normalizeComponentBoundaryRender;
473
478
  protected normalizeBoundaryArtifactHtml(html: string): string;
474
479
  /**
@@ -8,6 +8,7 @@ import { DependencyResolverService } from "../page-loading/dependency-resolver.j
8
8
  import { PageModuleLoaderService } from "../page-loading/page-module-loader.js";
9
9
  import { RenderExecutionService } from "./render-execution.service.js";
10
10
  import { RenderPreparationService } from "./render-preparation.service.js";
11
+ import { RouteShellComposer } from "./route-shell-composer.service.js";
11
12
  import { normalizeBoundaryArtifactHtml } from "./render-output.utils.js";
12
13
  import { getComponentRenderContext, runWithComponentRenderContext } from "./component-render-context.js";
13
14
  import {
@@ -54,6 +55,7 @@ class IntegrationRenderer {
54
55
  pageModuleLoaderService;
55
56
  renderPreparationService;
56
57
  renderExecutionService;
58
+ routeShellComposer = new RouteShellComposer();
57
59
  queuedBoundaryRuntimeService = new QueuedBoundaryRuntimeService();
58
60
  DOC_TYPE = "<!DOCTYPE html>";
59
61
  /**
@@ -81,7 +83,7 @@ class IntegrationRenderer {
81
83
  * @returns The current execution cache when present.
82
84
  */
83
85
  getBoundaryRendererCache(integrationContext) {
84
- if (typeof integrationContext === "object" && integrationContext !== null && "rendererCache" in integrationContext && integrationContext.rendererCache instanceof Map) {
86
+ if (integrationContext?.rendererCache instanceof Map) {
85
87
  return integrationContext.rendererCache;
86
88
  }
87
89
  return void 0;
@@ -107,9 +109,10 @@ class IntegrationRenderer {
107
109
  */
108
110
  withBoundaryRendererCache(input, rendererCache) {
109
111
  const integrationContext = input.integrationContext;
112
+ const sharedRendererCache = rendererCache;
110
113
  return {
111
114
  ...input,
112
- integrationContext: typeof integrationContext === "object" && integrationContext !== null ? { ...integrationContext, rendererCache } : { rendererCache }
115
+ integrationContext: integrationContext ? { ...integrationContext, rendererCache: sharedRendererCache } : { rendererCache: sharedRendererCache }
113
116
  };
114
117
  }
115
118
  getRendererModuleValue(key) {
@@ -259,17 +262,17 @@ class IntegrationRenderer {
259
262
  * @returns HTML response for the partial render.
260
263
  */
261
264
  async renderPartialViewResponse(input) {
262
- if (input.renderInline && !this.hasForeignBoundaryDescendants(input.view)) {
263
- return this.createHtmlResponse(await input.renderInline(), input.ctx);
264
- }
265
- const rendererCache = /* @__PURE__ */ new Map();
266
- const viewRender = await this.renderComponentBoundary({
267
- component: input.view,
268
- props: input.props ?? {},
269
- integrationContext: { rendererCache }
265
+ return this.routeShellComposer.renderPartialViewResponse(input, {
266
+ hasForeignBoundaryDescendants: (component) => this.hasForeignBoundaryDescendants(component),
267
+ createHtmlResponse: (body, ctx) => this.createHtmlResponse(body, ctx),
268
+ renderComponentBoundary: (boundaryInput) => this.renderComponentBoundary(boundaryInput),
269
+ prepareViewDependencies: (view, layout) => this.prepareViewDependencies(view, layout),
270
+ getHtmlTemplate: () => this.getHtmlTemplate(),
271
+ resolveViewMetadata: (view, props) => this.resolveViewMetadata(view, props),
272
+ appendProcessedDependencies: (...assetGroups) => this.appendProcessedDependencies(...assetGroups),
273
+ finalizeResolvedHtml: (options) => this.finalizeResolvedHtml(options),
274
+ docType: this.DOC_TYPE
270
275
  });
271
- const html = input.transformHtml ? input.transformHtml(viewRender.html) : viewRender.html;
272
- return this.createHtmlResponse(html, input.ctx);
273
276
  }
274
277
  /**
275
278
  * Renders an explicit view through optional layout and document shells.
@@ -284,44 +287,17 @@ class IntegrationRenderer {
284
287
  * @returns HTML response for the explicit view render.
285
288
  */
286
289
  async renderViewWithDocumentShell(input) {
287
- const normalizedProps = input.props ?? {};
288
- if (input.ctx.partial) {
289
- return this.renderPartialViewResponse({
290
- view: input.view,
291
- props: input.props,
292
- ctx: input.ctx
293
- });
294
- }
295
- await this.prepareViewDependencies(input.view, input.layout);
296
- const HtmlTemplate = await this.getHtmlTemplate();
297
- const metadata = await this.resolveViewMetadata(input.view, input.props);
298
- const rendererCache = /* @__PURE__ */ new Map();
299
- const viewRender = await this.renderComponentBoundary({
300
- component: input.view,
301
- props: normalizedProps,
302
- integrationContext: { rendererCache }
303
- });
304
- const layoutRender = input.layout ? await this.renderComponentBoundary({
305
- component: input.layout,
306
- props: {},
307
- children: viewRender.html,
308
- integrationContext: { rendererCache }
309
- }) : void 0;
310
- const documentRender = await this.renderComponentBoundary({
311
- component: HtmlTemplate,
312
- props: {
313
- metadata,
314
- pageProps: normalizedProps
315
- },
316
- children: layoutRender?.html ?? viewRender.html,
317
- integrationContext: { rendererCache }
318
- });
319
- this.appendProcessedDependencies(viewRender.assets, layoutRender?.assets, documentRender.assets);
320
- const html = await this.finalizeResolvedHtml({
321
- html: `${this.DOC_TYPE}${documentRender.html}`,
322
- partial: false
290
+ return this.routeShellComposer.renderViewWithDocumentShell(input, {
291
+ hasForeignBoundaryDescendants: (component) => this.hasForeignBoundaryDescendants(component),
292
+ createHtmlResponse: (body, ctx) => this.createHtmlResponse(body, ctx),
293
+ renderComponentBoundary: (boundaryInput) => this.renderComponentBoundary(boundaryInput),
294
+ prepareViewDependencies: (view, layout) => this.prepareViewDependencies(view, layout),
295
+ getHtmlTemplate: () => this.getHtmlTemplate(),
296
+ resolveViewMetadata: (view, props) => this.resolveViewMetadata(view, props),
297
+ appendProcessedDependencies: (...assetGroups) => this.appendProcessedDependencies(...assetGroups),
298
+ finalizeResolvedHtml: (options) => this.finalizeResolvedHtml(options),
299
+ docType: this.DOC_TYPE
323
300
  });
324
- return this.createHtmlResponse(html, input.ctx);
325
301
  }
326
302
  /**
327
303
  * Renders a route page through optional layout and document shells.
@@ -336,31 +312,17 @@ class IntegrationRenderer {
336
312
  * @returns Final serialized document HTML including the doctype prefix.
337
313
  */
338
314
  async renderPageWithDocumentShell(input) {
339
- const rendererCache = /* @__PURE__ */ new Map();
340
- const pageRender = await this.renderComponentBoundary({
341
- component: input.page.component,
342
- props: input.page.props,
343
- integrationContext: { rendererCache }
344
- });
345
- const layoutRender = input.layout ? await this.renderComponentBoundary({
346
- component: input.layout.component,
347
- props: input.layout.props ?? {},
348
- children: pageRender.html,
349
- integrationContext: { rendererCache }
350
- }) : void 0;
351
- const documentRender = await this.renderComponentBoundary({
352
- component: input.htmlTemplate,
353
- props: {
354
- metadata: input.metadata,
355
- pageProps: input.pageProps,
356
- ...input.documentProps ?? {}
357
- },
358
- children: layoutRender?.html ?? pageRender.html,
359
- integrationContext: { rendererCache }
315
+ return this.routeShellComposer.renderPageWithDocumentShell(input, {
316
+ hasForeignBoundaryDescendants: (component) => this.hasForeignBoundaryDescendants(component),
317
+ createHtmlResponse: (body, ctx) => this.createHtmlResponse(body, ctx),
318
+ renderComponentBoundary: (boundaryInput) => this.renderComponentBoundary(boundaryInput),
319
+ prepareViewDependencies: (view, layout) => this.prepareViewDependencies(view, layout),
320
+ getHtmlTemplate: () => this.getHtmlTemplate(),
321
+ resolveViewMetadata: (view, props) => this.resolveViewMetadata(view, props),
322
+ appendProcessedDependencies: (...assetGroups) => this.appendProcessedDependencies(...assetGroups),
323
+ finalizeResolvedHtml: (options) => this.finalizeResolvedHtml(options),
324
+ docType: this.DOC_TYPE
360
325
  });
361
- this.appendProcessedDependencies(pageRender.assets, layoutRender?.assets, documentRender.assets);
362
- const documentHtml = input.transformDocumentHtml ? input.transformDocumentHtml(documentRender.html) : documentRender.html;
363
- return `${this.DOC_TYPE}${documentHtml}`;
364
326
  }
365
327
  /**
366
328
  * Renders one string-first component boundary and collects its assets.
@@ -423,7 +385,10 @@ class IntegrationRenderer {
423
385
  runtimeContext: options.runtimeContext,
424
386
  queueLabel: options.queueLabel,
425
387
  renderQueuedChildren: options.renderQueuedChildren,
426
- resolveBoundary: (input, rendererCache) => this.resolveBoundaryInOwningRenderer(input, rendererCache),
388
+ resolveBoundary: (input, rendererCache) => this.resolveBoundaryPayloadInOwningRenderer(
389
+ input,
390
+ rendererCache
391
+ ),
427
392
  applyAttributesToFirstElement: (html, attributes) => this.htmlTransformer.applyAttributesToFirstElement(html, attributes),
428
393
  dedupeProcessedAssets: (assets) => this.htmlTransformer.dedupeProcessedAssets(assets)
429
394
  });
@@ -798,6 +763,17 @@ class IntegrationRenderer {
798
763
  }
799
764
  return await owningRenderer.renderComponentBoundary(this.withBoundaryRendererCache(input, rendererCache));
800
765
  }
766
+ async resolveBoundaryPayloadInOwningRenderer(input, rendererCache) {
767
+ const boundaryOwner = this.getRegisteredBoundaryOwner(input.component);
768
+ if (!boundaryOwner) {
769
+ return void 0;
770
+ }
771
+ const owningRenderer = this.getIntegrationRendererForName(boundaryOwner, rendererCache);
772
+ if (owningRenderer === this || owningRenderer.name === this.name) {
773
+ return void 0;
774
+ }
775
+ return await owningRenderer.renderBoundary(this.withBoundaryRendererCache(input, rendererCache));
776
+ }
801
777
  /**
802
778
  * Renders one component under this integration's boundary runtime and resolves
803
779
  * any nested foreign boundaries captured during that render.
@@ -838,6 +814,22 @@ class IntegrationRenderer {
838
814
  );
839
815
  return this.normalizeComponentBoundaryRender(execution.value);
840
816
  }
817
+ /**
818
+ * Compatibility boundary contract that exposes a narrower payload shape for
819
+ * future route-composition work while preserving the current
820
+ * `renderComponentBoundary()` runtime semantics.
821
+ */
822
+ async renderBoundary(input) {
823
+ const result = await this.renderComponentBoundary(input);
824
+ return {
825
+ html: result.html,
826
+ assets: result.assets ?? [],
827
+ rootTag: result.rootTag,
828
+ rootAttributes: result.rootAttributes,
829
+ attachmentPolicy: result.canAttachAttributes ? { kind: "first-element" } : { kind: "none" },
830
+ integrationName: result.integrationName
831
+ };
832
+ }
841
833
  normalizeComponentBoundaryRender(result) {
842
834
  const normalizedHtml = this.normalizeBoundaryArtifactHtml(result.html);
843
835
  return normalizedHtml === result.html ? result : {
@@ -1,5 +1,5 @@
1
1
  import type { ProcessedAsset } from '../../services/assets/asset-processing-service/index.js';
2
- import type { ComponentRenderInput, ComponentRenderResult, EcoComponent } from '../../types/public-types.js';
2
+ import type { BaseIntegrationContext, BoundaryRenderPayload, ComponentRenderInput, EcoComponent } from '../../types/public-types.js';
3
3
  import type { ComponentBoundaryRuntime } from './component-render-context.js';
4
4
  export type QueuedBoundaryDecisionInput = {
5
5
  currentIntegration: string;
@@ -26,11 +26,7 @@ export type QueuedBoundaryRuntimeContext = {
26
26
  nextBoundaryId: number;
27
27
  queuedResolutions: QueuedBoundaryResolution[];
28
28
  };
29
- type QueuedBoundaryIntegrationContext = {
30
- rendererCache?: Map<string, unknown>;
31
- componentInstanceId?: string;
32
- [key: string]: unknown;
33
- };
29
+ type QueuedBoundaryIntegrationContext = BaseIntegrationContext & Record<string, unknown>;
34
30
  type QueuedBoundaryChildRenderResult = {
35
31
  assets: ProcessedAsset[];
36
32
  html?: string;
@@ -80,7 +76,7 @@ export declare class QueuedBoundaryRuntimeService {
80
76
  runtimeContext?: TContext;
81
77
  queueLabel: string;
82
78
  renderQueuedChildren: (children: unknown, runtimeContext: TContext, queuedResolutionsByToken: Map<string, QueuedBoundaryResolution>, resolveToken: (token: string) => Promise<string>) => Promise<QueuedBoundaryChildRenderResult>;
83
- resolveBoundary: (input: ComponentRenderInput, rendererCache: Map<string, unknown>) => Promise<ComponentRenderResult | undefined>;
79
+ resolveBoundary: (input: ComponentRenderInput, rendererCache: Map<string, unknown>) => Promise<BoundaryRenderPayload | undefined>;
84
80
  applyAttributesToFirstElement: (html: string, attributes: Record<string, string>) => string;
85
81
  dedupeProcessedAssets: (assets: ProcessedAsset[]) => ProcessedAsset[];
86
82
  }): Promise<{
@@ -105,7 +105,7 @@ class QueuedBoundaryRuntimeService {
105
105
  if ((boundaryRender.assets?.length ?? 0) > 0) {
106
106
  collectedAssets.push(...boundaryRender.assets ?? []);
107
107
  }
108
- const resolvedHtml2 = boundaryRender.canAttachAttributes && boundaryRender.rootAttributes ? options.applyAttributesToFirstElement(boundaryRender.html, boundaryRender.rootAttributes) : boundaryRender.html;
108
+ const resolvedHtml2 = boundaryRender.attachmentPolicy.kind === "first-element" && boundaryRender.rootAttributes ? options.applyAttributesToFirstElement(boundaryRender.html, boundaryRender.rootAttributes) : boundaryRender.html;
109
109
  resolvedHtmlByToken.set(token, resolvedHtml2);
110
110
  return resolvedHtml2;
111
111
  } finally {
@@ -1,6 +1,7 @@
1
1
  import type { EcoPagesAppConfig } from '../../types/internal-types.js';
2
2
  import type { ComponentRenderResult, EcoComponent, EcoPageComponent, EcoPageFile, EcoPagesElement, GetMetadata, GetStaticProps, HtmlTemplateProps, IntegrationRendererRenderOptions, PageMetadataProps, RouteRendererOptions } from '../../types/public-types.js';
3
3
  import { type AssetProcessingService, type ProcessedAsset } from '../../services/assets/asset-processing-service/index.js';
4
+ import { BoundaryPlanningService } from './boundary-planning.service.js';
4
5
  type ResolvedPageModule = {
5
6
  Page: EcoPageFile['default'] | EcoPageComponent<any>;
6
7
  getStaticProps?: GetStaticProps<Record<string, unknown>>;
@@ -32,6 +33,9 @@ export interface RenderPreparationCallbacks {
32
33
  dedupeProcessedAssets(assets: ProcessedAsset[]): ProcessedAsset[];
33
34
  createPageLocalsProxy(filePath: string): RouteRendererOptions['locals'];
34
35
  }
36
+ export interface RenderPreparationServiceDependencies {
37
+ boundaryPlanningService?: BoundaryPlanningService;
38
+ }
35
39
  /**
36
40
  * Prepares the normalized render inputs consumed by `IntegrationRenderer.execute()`.
37
41
  *
@@ -43,6 +47,7 @@ export interface RenderPreparationCallbacks {
43
47
  export declare class RenderPreparationService {
44
48
  private appConfig;
45
49
  private assetProcessingService;
50
+ private readonly boundaryPlanningService;
46
51
  /**
47
52
  * Creates the render-preparation orchestrator for one app instance.
48
53
  *
@@ -50,7 +55,7 @@ export declare class RenderPreparationService {
50
55
  * The service is app-scoped because it depends on finalized config defaults and
51
56
  * the app-owned asset-processing pipeline while remaining renderer-agnostic.
52
57
  */
53
- constructor(appConfig: EcoPagesAppConfig, assetProcessingService: AssetProcessingService);
58
+ constructor(appConfig: EcoPagesAppConfig, assetProcessingService: AssetProcessingService, dependencies?: RenderPreparationServiceDependencies);
54
59
  /**
55
60
  * Builds the final render options object used by the integration-specific
56
61
  * renderer.
@@ -1,13 +1,14 @@
1
1
  import { createRequire } from "node:module";
2
2
  import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
3
  import {
5
4
  AssetFactory
6
5
  } from "../../services/assets/asset-processing-service/index.js";
7
6
  import { buildGlobalInjectorBootstrapContent, buildGlobalInjectorMapScript } from "../../eco/global-injector-map.js";
7
+ import { BoundaryPlanningService } from "./boundary-planning.service.js";
8
8
  class RenderPreparationService {
9
9
  appConfig;
10
10
  assetProcessingService;
11
+ boundaryPlanningService;
11
12
  /**
12
13
  * Creates the render-preparation orchestrator for one app instance.
13
14
  *
@@ -15,9 +16,10 @@ class RenderPreparationService {
15
16
  * The service is app-scoped because it depends on finalized config defaults and
16
17
  * the app-owned asset-processing pipeline while remaining renderer-agnostic.
17
18
  */
18
- constructor(appConfig, assetProcessingService) {
19
+ constructor(appConfig, assetProcessingService, dependencies = {}) {
19
20
  this.appConfig = appConfig;
20
21
  this.assetProcessingService = assetProcessingService;
22
+ this.boundaryPlanningService = dependencies.boundaryPlanningService ?? new BoundaryPlanningService(appConfig);
21
23
  }
22
24
  /**
23
25
  * Builds the final render options object used by the integration-specific
@@ -39,6 +41,13 @@ class RenderPreparationService {
39
41
  const HtmlTemplate = await callbacks.getHtmlTemplate();
40
42
  const { props, metadata } = await callbacks.resolvePageData(pageModule, routeOptions);
41
43
  const Layout = Page.config?.layout;
44
+ const boundaryPlan = this.boundaryPlanningService.buildPlan({
45
+ routeFile: routeOptions.file,
46
+ currentIntegrationName,
47
+ HtmlTemplate,
48
+ Layout,
49
+ Page
50
+ });
42
51
  const componentsToResolve = Layout ? [HtmlTemplate, Layout, Page] : [HtmlTemplate, Page];
43
52
  const resolvedDependencies = await callbacks.resolveDependencies(componentsToResolve);
44
53
  const usedIntegrationDependencies = this.collectUsedIntegrationDependencies(
@@ -95,7 +104,8 @@ class RenderPreparationService {
95
104
  pageProps,
96
105
  locals,
97
106
  pageLocals,
98
- cacheStrategy
107
+ cacheStrategy,
108
+ boundaryPlan
99
109
  };
100
110
  return {
101
111
  ...integrationSpecificProps,
@@ -0,0 +1,50 @@
1
+ import type { ComponentRenderInput, ComponentRenderResult, EcoComponent, HtmlTemplateProps, PageMetadataProps } from '../../types/public-types.js';
2
+ import type { ProcessedAsset } from '../../services/assets/asset-processing-service/index.js';
3
+ import type { RenderToResponseContext } from './integration-renderer.js';
4
+ export interface RouteShellComposerCallbacks {
5
+ hasForeignBoundaryDescendants(component: EcoComponent): boolean;
6
+ createHtmlResponse(body: BodyInit, ctx: RenderToResponseContext): Response;
7
+ renderComponentBoundary(input: ComponentRenderInput): Promise<ComponentRenderResult>;
8
+ prepareViewDependencies(view: EcoComponent, layout?: EcoComponent): Promise<ProcessedAsset[]>;
9
+ getHtmlTemplate(): Promise<EcoComponent<HtmlTemplateProps>>;
10
+ resolveViewMetadata<P>(view: EcoComponent<P>, props: P): Promise<PageMetadataProps>;
11
+ appendProcessedDependencies(...assetGroups: Array<readonly ProcessedAsset[] | undefined>): ProcessedAsset[];
12
+ finalizeResolvedHtml(options: {
13
+ html: string;
14
+ partial?: boolean;
15
+ componentRootAttributes?: Record<string, string>;
16
+ documentAttributes?: Record<string, string>;
17
+ transformHtml?: boolean;
18
+ }): Promise<string>;
19
+ docType: string;
20
+ }
21
+ export declare class RouteShellComposer {
22
+ renderPartialViewResponse<P>(input: {
23
+ view: EcoComponent<P>;
24
+ props: P;
25
+ ctx: RenderToResponseContext;
26
+ renderInline?: () => Promise<BodyInit>;
27
+ transformHtml?: (html: string) => string;
28
+ }, callbacks: RouteShellComposerCallbacks): Promise<Response>;
29
+ renderViewWithDocumentShell<P>(input: {
30
+ view: EcoComponent<P>;
31
+ props: P;
32
+ ctx: RenderToResponseContext;
33
+ layout?: EcoComponent;
34
+ }, callbacks: RouteShellComposerCallbacks): Promise<Response>;
35
+ renderPageWithDocumentShell(input: {
36
+ page: {
37
+ component: EcoComponent;
38
+ props: Record<string, unknown>;
39
+ };
40
+ layout?: {
41
+ component: EcoComponent;
42
+ props?: Record<string, unknown>;
43
+ };
44
+ htmlTemplate: EcoComponent;
45
+ metadata: PageMetadataProps;
46
+ pageProps: Record<string, unknown>;
47
+ documentProps?: Record<string, unknown>;
48
+ transformDocumentHtml?: (html: string) => string;
49
+ }, callbacks: RouteShellComposerCallbacks): Promise<string>;
50
+ }
@@ -0,0 +1,81 @@
1
+ class RouteShellComposer {
2
+ async renderPartialViewResponse(input, callbacks) {
3
+ if (input.renderInline && !callbacks.hasForeignBoundaryDescendants(input.view)) {
4
+ return callbacks.createHtmlResponse(await input.renderInline(), input.ctx);
5
+ }
6
+ const rendererCache = /* @__PURE__ */ new Map();
7
+ const viewRender = await callbacks.renderComponentBoundary({
8
+ component: input.view,
9
+ props: input.props ?? {},
10
+ integrationContext: { rendererCache }
11
+ });
12
+ const html = input.transformHtml ? input.transformHtml(viewRender.html) : viewRender.html;
13
+ return callbacks.createHtmlResponse(html, input.ctx);
14
+ }
15
+ async renderViewWithDocumentShell(input, callbacks) {
16
+ const normalizedProps = input.props ?? {};
17
+ if (input.ctx.partial) {
18
+ return this.renderPartialViewResponse(input, callbacks);
19
+ }
20
+ await callbacks.prepareViewDependencies(input.view, input.layout);
21
+ const HtmlTemplate = await callbacks.getHtmlTemplate();
22
+ const metadata = await callbacks.resolveViewMetadata(input.view, input.props);
23
+ const rendererCache = /* @__PURE__ */ new Map();
24
+ const viewRender = await callbacks.renderComponentBoundary({
25
+ component: input.view,
26
+ props: normalizedProps,
27
+ integrationContext: { rendererCache }
28
+ });
29
+ const layoutRender = input.layout ? await callbacks.renderComponentBoundary({
30
+ component: input.layout,
31
+ props: {},
32
+ children: viewRender.html,
33
+ integrationContext: { rendererCache }
34
+ }) : void 0;
35
+ const documentRender = await callbacks.renderComponentBoundary({
36
+ component: HtmlTemplate,
37
+ props: {
38
+ metadata,
39
+ pageProps: normalizedProps
40
+ },
41
+ children: layoutRender?.html ?? viewRender.html,
42
+ integrationContext: { rendererCache }
43
+ });
44
+ callbacks.appendProcessedDependencies(viewRender.assets, layoutRender?.assets, documentRender.assets);
45
+ const html = await callbacks.finalizeResolvedHtml({
46
+ html: `${callbacks.docType}${documentRender.html}`,
47
+ partial: false
48
+ });
49
+ return callbacks.createHtmlResponse(html, input.ctx);
50
+ }
51
+ async renderPageWithDocumentShell(input, callbacks) {
52
+ const rendererCache = /* @__PURE__ */ new Map();
53
+ const pageRender = await callbacks.renderComponentBoundary({
54
+ component: input.page.component,
55
+ props: input.page.props,
56
+ integrationContext: { rendererCache }
57
+ });
58
+ const layoutRender = input.layout ? await callbacks.renderComponentBoundary({
59
+ component: input.layout.component,
60
+ props: input.layout.props ?? {},
61
+ children: pageRender.html,
62
+ integrationContext: { rendererCache }
63
+ }) : void 0;
64
+ const documentRender = await callbacks.renderComponentBoundary({
65
+ component: input.htmlTemplate,
66
+ props: {
67
+ metadata: input.metadata,
68
+ pageProps: input.pageProps,
69
+ ...input.documentProps ?? {}
70
+ },
71
+ children: layoutRender?.html ?? pageRender.html,
72
+ integrationContext: { rendererCache }
73
+ });
74
+ callbacks.appendProcessedDependencies(pageRender.assets, layoutRender?.assets, documentRender.assets);
75
+ const documentHtml = input.transformDocumentHtml ? input.transformDocumentHtml(documentRender.html) : documentRender.html;
76
+ return `${callbacks.docType}${documentHtml}`;
77
+ }
78
+ }
79
+ export {
80
+ RouteShellComposer
81
+ };
@@ -165,8 +165,9 @@ function createNodeBootstrapPlugin(options) {
165
165
  });
166
166
  build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, async (args) => {
167
167
  const absolutePath = path.resolve(args.path);
168
- const shouldPreserveImportMeta = importMetaRewritePaths.has(absolutePath);
169
- const shouldRewriteReexports = shouldRewriteBootstrapSource(absolutePath, projectDir);
168
+ const isProjectSource = shouldRewriteBootstrapSource(absolutePath, projectDir);
169
+ const shouldPreserveImportMeta = isProjectSource || importMetaRewritePaths.has(absolutePath);
170
+ const shouldRewriteReexports = isProjectSource;
170
171
  if (!shouldPreserveImportMeta && !shouldRewriteReexports) {
171
172
  return void 0;
172
173
  }
@@ -678,12 +678,66 @@ export type IntegrationRendererRenderOptions<C = EcoPagesElement> = RouteRendere
678
678
  pageProps?: Record<string, unknown>;
679
679
  cacheStrategy?: CacheStrategy;
680
680
  pageLocals?: RequestLocals;
681
+ boundaryPlan?: BoundaryPlan;
681
682
  };
682
- export interface ComponentRenderInput {
683
+ export type BoundaryValidationErrorCode = 'UNKNOWN_INTEGRATION_OWNER' | 'MISSING_COMPONENT_METADATA';
684
+ export interface BoundaryValidationError {
685
+ code: BoundaryValidationErrorCode;
686
+ message: string;
687
+ componentId?: string;
688
+ componentFile?: string;
689
+ integrationName?: string;
690
+ }
691
+ export type BoundaryPlanNodeSource = 'route' | 'page' | 'layout' | 'html-template' | 'dependency';
692
+ export interface BoundaryOwnership {
693
+ integrationName: string;
694
+ componentId: string;
695
+ componentFile?: string;
696
+ isPageEntry: boolean;
697
+ isForeignToParent: boolean;
698
+ }
699
+ export interface BoundaryPlanNode {
700
+ id: string;
701
+ source: BoundaryPlanNodeSource;
702
+ ownership: BoundaryOwnership;
703
+ children: BoundaryPlanNode[];
704
+ declaredDependenciesValid: boolean;
705
+ }
706
+ export interface BoundaryPlan {
707
+ root: BoundaryPlanNode;
708
+ rendererNames: string[];
709
+ foreignEdgeCount: number;
710
+ hasValidationErrors: boolean;
711
+ validationErrors: BoundaryValidationError[];
712
+ }
713
+ export type BoundaryAttachmentPolicy = {
714
+ kind: 'none';
715
+ } | {
716
+ kind: 'first-element';
717
+ };
718
+ export interface BoundaryRenderPayload {
719
+ html: string;
720
+ assets: ProcessedAsset[];
721
+ rootTag?: string;
722
+ rootAttributes?: Record<string, string>;
723
+ attachmentPolicy: BoundaryAttachmentPolicy;
724
+ integrationName: string;
725
+ }
726
+ /**
727
+ * Shared execution-scoped context threaded through component boundary renders.
728
+ *
729
+ * Integrations can extend this with renderer-local runtime keys, but the cache
730
+ * and optional component instance identity are shared across all renderers.
731
+ */
732
+ export interface BaseIntegrationContext {
733
+ rendererCache?: Map<string, unknown>;
734
+ componentInstanceId?: string;
735
+ }
736
+ export interface ComponentRenderInput<TIntegrationContext extends BaseIntegrationContext = BaseIntegrationContext> {
683
737
  component: EcoComponent;
684
738
  props: Record<string, unknown>;
685
739
  children?: unknown;
686
- integrationContext?: unknown;
740
+ integrationContext?: TIntegrationContext;
687
741
  }
688
742
  export interface ComponentRenderResult {
689
743
  html: string;