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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +63 -7
  2. package/package.json +4 -47
  3. package/src/adapters/bun/create-app.ts +54 -2
  4. package/src/adapters/bun/hmr-manager.test.ts +0 -2
  5. package/src/adapters/bun/hmr-manager.ts +1 -24
  6. package/src/adapters/bun/server-adapter.ts +30 -4
  7. package/src/adapters/node/node-hmr-manager.test.ts +0 -2
  8. package/src/adapters/node/node-hmr-manager.ts +2 -25
  9. package/src/adapters/shared/explicit-static-render-preparation.ts +58 -0
  10. package/src/adapters/shared/explicit-static-route-matcher.test.ts +6 -6
  11. package/src/adapters/shared/explicit-static-route-matcher.ts +22 -31
  12. package/src/adapters/shared/file-route-middleware-pipeline.test.ts +5 -10
  13. package/src/adapters/shared/file-route-middleware-pipeline.ts +8 -17
  14. package/src/adapters/shared/fs-server-response-factory.test.ts +32 -43
  15. package/src/adapters/shared/fs-server-response-factory.ts +15 -37
  16. package/src/adapters/shared/fs-server-response-matcher.test.ts +65 -39
  17. package/src/adapters/shared/fs-server-response-matcher.ts +94 -43
  18. package/src/adapters/shared/hmr-manager.contract.test.ts +0 -4
  19. package/src/adapters/shared/render-context.ts +3 -3
  20. package/src/adapters/shared/server-adapter.test.ts +53 -0
  21. package/src/adapters/shared/server-adapter.ts +228 -159
  22. package/src/adapters/shared/server-route-handler.test.ts +6 -5
  23. package/src/adapters/shared/server-route-handler.ts +4 -4
  24. package/src/adapters/shared/server-static-builder.test.ts +4 -4
  25. package/src/adapters/shared/server-static-builder.ts +4 -4
  26. package/src/config/README.md +1 -1
  27. package/src/config/config-builder.test.ts +0 -1
  28. package/src/config/config-builder.ts +2 -7
  29. package/src/dev/host-runtime.ts +34 -0
  30. package/src/eco/eco.browser.test.ts +2 -2
  31. package/src/eco/eco.browser.ts +2 -2
  32. package/src/eco/eco.test.ts +6 -6
  33. package/src/eco/eco.ts +12 -12
  34. package/src/eco/eco.types.ts +3 -3
  35. package/src/errors/index.ts +1 -0
  36. package/src/hmr/client/hmr-runtime.ts +4 -2
  37. package/src/hmr/strategies/js-hmr-strategy.test.ts +0 -1
  38. package/src/hmr/strategies/js-hmr-strategy.ts +0 -6
  39. package/src/integrations/ghtml/ghtml-renderer.test.ts +7 -7
  40. package/src/integrations/ghtml/ghtml-renderer.ts +1 -11
  41. package/src/plugins/eco-component-meta-plugin.ts +0 -1
  42. package/src/plugins/integration-plugin.test.ts +9 -14
  43. package/src/plugins/integration-plugin.ts +34 -22
  44. package/src/plugins/processor.ts +17 -0
  45. package/src/route-renderer/GRAPH.md +81 -289
  46. package/src/route-renderer/README.md +67 -105
  47. package/src/route-renderer/orchestration/component-render-context.ts +45 -38
  48. package/src/route-renderer/orchestration/declared-ownership-graph.ts +62 -0
  49. package/src/route-renderer/orchestration/foreign-subtree-execution.service.ts +383 -0
  50. package/src/route-renderer/orchestration/integration-renderer.test.ts +118 -121
  51. package/src/route-renderer/orchestration/integration-renderer.ts +362 -403
  52. package/src/route-renderer/orchestration/ownership-planning.service.ts +97 -0
  53. package/src/route-renderer/orchestration/ownership-validation.service.ts +76 -0
  54. package/src/route-renderer/orchestration/processed-asset-dedupe.ts +1 -1
  55. package/src/route-renderer/orchestration/{queued-boundary-runtime.service.test.ts → queued-foreign-subtree-resolution.service.test.ts} +76 -71
  56. package/src/route-renderer/orchestration/{queued-boundary-runtime.service.ts → queued-foreign-subtree-resolution.service.ts} +68 -63
  57. package/src/route-renderer/orchestration/render-output.utils.ts +21 -13
  58. package/src/route-renderer/orchestration/{render-preparation.service.test.ts → route-render-orchestrator.prepare-render-options.test.ts} +160 -85
  59. package/src/route-renderer/orchestration/route-render-orchestrator.test.ts +265 -0
  60. package/src/route-renderer/orchestration/{render-preparation.service.ts → route-render-orchestrator.ts} +244 -160
  61. package/src/route-renderer/page-loading/component-dependency-collection.ts +9 -3
  62. package/src/route-renderer/page-loading/declared-asset-collection.ts +2 -5
  63. package/src/route-renderer/page-loading/dependency-resolver.test.ts +107 -11
  64. package/src/route-renderer/page-loading/dependency-resolver.ts +6 -12
  65. package/src/route-renderer/page-loading/ecopages-virtual-imports.ts +1 -1
  66. package/src/route-renderer/page-loading/lazy-entry-collection.ts +1 -1
  67. package/src/route-renderer/page-loading/lazy-trigger-planning.ts +1 -1
  68. package/src/route-renderer/page-loading/module-declaration-aggregation.ts +1 -1
  69. package/src/route-renderer/page-loading/module-declaration-scripts.ts +1 -1
  70. package/src/route-renderer/page-loading/page-dependency-bundling.ts +105 -66
  71. package/src/route-renderer/route-renderer.ts +28 -31
  72. package/src/router/README.md +16 -19
  73. package/src/router/server/route-registry.test.ts +176 -0
  74. package/src/router/server/route-registry.ts +382 -0
  75. package/src/services/README.md +1 -2
  76. package/src/services/assets/asset-processing-service/asset-dependency-keys.ts +1 -1
  77. package/src/services/assets/asset-processing-service/asset-processing.service.test.ts +1 -4
  78. package/src/services/assets/asset-processing-service/asset-processing.service.ts +1 -2
  79. package/src/services/assets/asset-processing-service/assets.types.ts +3 -0
  80. package/src/services/assets/asset-processing-service/grouped-content-bundles.ts +1 -1
  81. package/src/services/assets/asset-processing-service/index.ts +1 -0
  82. package/src/{route-renderer/orchestration/page-packaging.service.test.ts → services/assets/asset-processing-service/page-package.test.ts} +38 -14
  83. package/src/services/assets/asset-processing-service/page-package.ts +93 -0
  84. package/src/services/assets/asset-processing-service/processors/base/base-script-processor.ts +4 -5
  85. package/src/services/assets/asset-processing-service/processors/script/content-script.processor.test.ts +13 -10
  86. package/src/services/assets/asset-processing-service/processors/script/content-script.processor.ts +3 -0
  87. package/src/services/assets/asset-processing-service/processors/script/file-script.processor.ts +6 -0
  88. package/src/services/assets/asset-processing-service/processors/script/node-module-script.processor.ts +2 -0
  89. package/src/services/assets/asset-processing-service/processors/stylesheet/content-stylesheet.processor.ts +1 -0
  90. package/src/services/assets/asset-processing-service/processors/stylesheet/file-stylesheet.processor.ts +2 -0
  91. package/src/services/assets/asset-processing-service/ungrouped-dependency-processing.ts +1 -1
  92. package/src/services/html/html-transformer.service.test.ts +1 -4
  93. package/src/services/module-loading/app-server-module-transpiler.service.ts +1 -3
  94. package/src/services/module-loading/node-bootstrap-plugin.ts +17 -3
  95. package/src/services/module-loading/page-module-import.service.ts +0 -1
  96. package/src/services/module-loading/source-module-support.ts +1 -1
  97. package/src/static-site-generator/static-site-generator.test.ts +124 -32
  98. package/src/static-site-generator/static-site-generator.ts +168 -185
  99. package/src/types/internal-types.ts +13 -12
  100. package/src/types/public-types.ts +55 -39
  101. package/src/watchers/project-watcher.test-helpers.ts +4 -3
  102. package/src/route-renderer/orchestration/boundary-planning.service.ts +0 -146
  103. package/src/route-renderer/orchestration/page-packaging.service.ts +0 -85
  104. package/src/route-renderer/orchestration/render-execution.service.test.ts +0 -196
  105. package/src/route-renderer/orchestration/render-execution.service.ts +0 -182
  106. package/src/route-renderer/orchestration/route-shell-composer.service.ts +0 -162
  107. package/src/router/server/fs-router-scanner.test.ts +0 -83
  108. package/src/router/server/fs-router-scanner.ts +0 -224
  109. package/src/router/server/fs-router.test.ts +0 -214
  110. package/src/router/server/fs-router.ts +0 -122
  111. package/src/services/runtime-state/runtime-specifier-registry.service.ts +0 -96
@@ -0,0 +1,97 @@
1
+ import type {
2
+ OwnershipPlan,
3
+ OwnershipPlanNode,
4
+ OwnershipPlanNodeSource,
5
+ OwnershipValidationError,
6
+ EcoComponent,
7
+ } from '../../types/public-types.ts';
8
+ import { mapDeclaredOwnershipGraph } from './declared-ownership-graph.ts';
9
+
10
+ type OwnershipPlanBuildInput = {
11
+ routeFile: string;
12
+ currentIntegrationName: string;
13
+ HtmlTemplate: EcoComponent;
14
+ Layout?: EcoComponent;
15
+ Page: EcoComponent;
16
+ validationErrors?: OwnershipValidationError[];
17
+ };
18
+
19
+ type OwnershipPlanBuildEntry = {
20
+ component: EcoComponent;
21
+ source: Extract<OwnershipPlanNodeSource, 'page' | 'layout' | 'html-template'>;
22
+ };
23
+
24
+ /**
25
+ * Builds a declared ownership plan from the component dependency graph.
26
+ *
27
+ * The plan reflects the declared component dependency graph for one route after
28
+ * route-root ownership validation has already run. It records ownership shape,
29
+ * foreign-edge counts, and any validation errors supplied by an earlier
30
+ * validation pass.
31
+ */
32
+ export class OwnershipPlanningService {
33
+ /**
34
+ * Builds the structural ownership plan for one route render.
35
+ */
36
+ buildPlan(input: OwnershipPlanBuildInput): OwnershipPlan {
37
+ const validationErrors = input.validationErrors ?? [];
38
+ const rendererNames = new Set<string>([input.currentIntegrationName]);
39
+ let foreignEdgeCount = 0;
40
+
41
+ const roots: OwnershipPlanBuildEntry[] = [
42
+ { component: input.HtmlTemplate, source: 'html-template' },
43
+ ...(input.Layout ? [{ component: input.Layout, source: 'layout' as const }] : []),
44
+ { component: input.Page, source: 'page' },
45
+ ];
46
+
47
+ const children = mapDeclaredOwnershipGraph<OwnershipPlanNode>({
48
+ roots,
49
+ currentIntegrationName: input.currentIntegrationName,
50
+ mapNode: ({ component, source, integrationName, componentId, isForeignToParent }, children) => {
51
+ const componentMeta = component.config?.__eco;
52
+
53
+ rendererNames.add(integrationName);
54
+
55
+ if (isForeignToParent) {
56
+ foreignEdgeCount += 1;
57
+ }
58
+
59
+ return {
60
+ id: componentId,
61
+ source,
62
+ ownership: {
63
+ integrationName,
64
+ componentId,
65
+ componentFile: componentMeta?.file,
66
+ isPageEntry: source === 'page',
67
+ isForeignToParent,
68
+ },
69
+ children,
70
+ declaredDependenciesValid: true,
71
+ };
72
+ },
73
+ });
74
+
75
+ const root: OwnershipPlanNode = {
76
+ id: `route:${input.routeFile}`,
77
+ source: 'route',
78
+ ownership: {
79
+ integrationName: input.currentIntegrationName,
80
+ componentId: `route:${input.routeFile}`,
81
+ componentFile: input.routeFile,
82
+ isPageEntry: false,
83
+ isForeignToParent: false,
84
+ },
85
+ children,
86
+ declaredDependenciesValid: validationErrors.length === 0,
87
+ };
88
+
89
+ return {
90
+ root,
91
+ rendererNames: Array.from(rendererNames),
92
+ foreignEdgeCount,
93
+ hasValidationErrors: validationErrors.length > 0,
94
+ validationErrors,
95
+ };
96
+ }
97
+ }
@@ -0,0 +1,76 @@
1
+ import type { EcoPagesAppConfig } from '../../types/internal-types.ts';
2
+ import type { OwnershipPlanNodeSource, OwnershipValidationError, EcoComponent } from '../../types/public-types.ts';
3
+ import { mapDeclaredOwnershipGraph } from './declared-ownership-graph.ts';
4
+
5
+ type OwnershipValidationInput = {
6
+ currentIntegrationName: string;
7
+ roots: Array<{
8
+ component: EcoComponent;
9
+ source: Extract<OwnershipPlanNodeSource, 'page' | 'layout' | 'html-template'>;
10
+ }>;
11
+ };
12
+
13
+ /**
14
+ * Validates foreign ownership as soon as the route root graph is loaded.
15
+ *
16
+ * This check runs before route data and dependency preparation so ownership and
17
+ * metadata problems are surfaced from the loaded component graph itself rather
18
+ * than being entangled with ownership-plan construction.
19
+ */
20
+ export class OwnershipValidationService {
21
+ private readonly appConfig: EcoPagesAppConfig;
22
+
23
+ /**
24
+ * Creates the ownership validator for one finalized app config.
25
+ */
26
+ constructor(appConfig: EcoPagesAppConfig) {
27
+ this.appConfig = appConfig;
28
+ }
29
+
30
+ /**
31
+ * Validates foreign ownership edges reachable from the supplied route roots.
32
+ */
33
+ validate(input: OwnershipValidationInput): OwnershipValidationError[] {
34
+ return mapDeclaredOwnershipGraph<OwnershipValidationError[]>({
35
+ roots: input.roots,
36
+ currentIntegrationName: input.currentIntegrationName,
37
+ mapNode: ({ component, integrationName, componentId, isForeignToParent }, children) => {
38
+ const componentMeta = component.config?.__eco;
39
+ const errors = children.flat();
40
+
41
+ if (!isForeignToParent) {
42
+ return errors;
43
+ }
44
+
45
+ if (!componentMeta) {
46
+ errors.push({
47
+ code: 'MISSING_COMPONENT_METADATA',
48
+ message: `[ecopages] Foreign child "${componentId}" must provide stable __eco metadata so ownership diagnostics stay actionable. Declared dependencies must include all possible foreign children.`,
49
+ componentId,
50
+ integrationName,
51
+ });
52
+ }
53
+
54
+ if (!this.isRegisteredIntegration(integrationName, input.currentIntegrationName)) {
55
+ errors.push({
56
+ code: 'UNKNOWN_INTEGRATION_OWNER',
57
+ message: `[ecopages] Foreign child "${componentId}" references unknown integration owner "${integrationName}". Declared dependencies must include all possible foreign children and those integrations must be registered.`,
58
+ componentId,
59
+ componentFile: componentMeta?.file,
60
+ integrationName,
61
+ });
62
+ }
63
+
64
+ return errors;
65
+ },
66
+ }).flat();
67
+ }
68
+
69
+ private isRegisteredIntegration(integrationName: string, currentIntegrationName: string): boolean {
70
+ if (integrationName === currentIntegrationName) {
71
+ return true;
72
+ }
73
+
74
+ return this.appConfig.integrations.some((integration) => integration.name === integrationName);
75
+ }
76
+ }
@@ -22,4 +22,4 @@ export function dedupeProcessedAssets(assets: ProcessedAsset[]): ProcessedAsset[
22
22
  }
23
23
 
24
24
  return [...unique.values()];
25
- }
25
+ }
@@ -3,11 +3,14 @@ import { eco } from '../../eco/eco.ts';
3
3
  import type { ProcessedAsset } from '../../services/assets/asset-processing-service/index.ts';
4
4
  import type {
5
5
  BaseIntegrationContext,
6
- BoundaryRenderPayload,
6
+ ForeignSubtreeRenderPayload,
7
7
  ComponentRenderInput,
8
8
  EcoComponent,
9
9
  } from '../../types/public-types.ts';
10
- import { QueuedBoundaryRuntimeService, type QueuedBoundaryRuntimeContext } from './queued-boundary-runtime.service.ts';
10
+ import {
11
+ QueuedForeignSubtreeResolutionService,
12
+ type QueuedForeignSubtreeResolutionContext,
13
+ } from './queued-foreign-subtree-resolution.service.ts';
11
14
 
12
15
  function createComponent(name: string, integration = name): EcoComponent<Record<string, unknown>, string> {
13
16
  return eco.component<Record<string, unknown>, string>({
@@ -50,12 +53,12 @@ function applyAttributesToFirstElement(html: string, attributes: Record<string,
50
53
  return html.replace(/^<([a-zA-Z][a-zA-Z0-9:-]*)/, `<$1${serializedAttributes}`);
51
54
  }
52
55
 
53
- describe('QueuedBoundaryRuntimeService', () => {
54
- it('creates scoped queue tokens and stores runtime state on the boundary input', () => {
55
- const service = new QueuedBoundaryRuntimeService();
56
+ describe('QueuedForeignSubtreeResolutionService', () => {
57
+ it('creates scoped queue tokens and stores runtime state on the render input', () => {
58
+ const service = new QueuedForeignSubtreeResolutionService();
56
59
  const shell = createComponent('shell', 'shell');
57
60
  const deferredWidget = createComponent('deferred-widget', 'deferred');
58
- const boundaryInput: ComponentRenderInput = {
61
+ const renderInput: ComponentRenderInput = {
59
62
  component: shell,
60
63
  props: {},
61
64
  integrationContext: {
@@ -66,14 +69,14 @@ describe('QueuedBoundaryRuntimeService', () => {
66
69
  const originalProps = { label: 'deferred' };
67
70
 
68
71
  const runtime = service.createRuntime({
69
- boundaryInput,
72
+ renderInput,
70
73
  rendererCache,
71
- runtimeContextKey: '__testQueuedBoundaryRuntime',
74
+ runtimeContextKey: '__testQueuedForeignSubtreeRuntime',
72
75
  tokenPrefix: '__TEST_QUEUE__',
73
- shouldQueueBoundary: () => true,
76
+ shouldQueueForeignChild: () => true,
74
77
  });
75
78
 
76
- const interception = runtime.interceptBoundarySync?.({
79
+ const interception = runtime.interceptForeignChildSync?.({
77
80
  currentIntegration: 'shell',
78
81
  targetIntegration: 'deferred',
79
82
  component: deferredWidget,
@@ -86,15 +89,15 @@ describe('QueuedBoundaryRuntimeService', () => {
86
89
  value: '__TEST_QUEUE__host__1__',
87
90
  });
88
91
 
89
- const runtimeContext = service.getRuntimeContext<QueuedBoundaryRuntimeContext>(
90
- boundaryInput,
91
- '__testQueuedBoundaryRuntime',
92
+ const runtimeContext = service.getRuntimeContext<QueuedForeignSubtreeResolutionContext>(
93
+ renderInput,
94
+ '__testQueuedForeignSubtreeRuntime',
92
95
  );
93
96
 
94
97
  expect(runtimeContext).toEqual({
95
98
  rendererCache,
96
99
  componentInstanceScope: 'host',
97
- nextBoundaryId: 1,
100
+ nextForeignSubtreeId: 1,
98
101
  queuedResolutions: [
99
102
  {
100
103
  token: '__TEST_QUEUE__host__1__',
@@ -104,7 +107,7 @@ describe('QueuedBoundaryRuntimeService', () => {
104
107
  },
105
108
  ],
106
109
  });
107
- expect(boundaryInput.integrationContext).toEqual(
110
+ expect(renderInput.integrationContext).toEqual(
108
111
  expect.objectContaining({
109
112
  rendererCache,
110
113
  }),
@@ -112,9 +115,9 @@ describe('QueuedBoundaryRuntimeService', () => {
112
115
  });
113
116
 
114
117
  it('preserves existing shared integration context fields when queue runtime state is attached', () => {
115
- const service = new QueuedBoundaryRuntimeService();
118
+ const service = new QueuedForeignSubtreeResolutionService();
116
119
  const shell = createComponent('shell', 'shell');
117
- const boundaryInput: ComponentRenderInput = {
120
+ const renderInput: ComponentRenderInput = {
118
121
  component: shell,
119
122
  props: {},
120
123
  integrationContext: {
@@ -125,14 +128,14 @@ describe('QueuedBoundaryRuntimeService', () => {
125
128
  const rendererCache = new Map<string, unknown>();
126
129
 
127
130
  service.createRuntime({
128
- boundaryInput,
131
+ renderInput,
129
132
  rendererCache,
130
- runtimeContextKey: '__testQueuedBoundaryRuntime',
133
+ runtimeContextKey: '__testQueuedForeignSubtreeRuntime',
131
134
  tokenPrefix: '__TEST_QUEUE__',
132
- shouldQueueBoundary: () => true,
135
+ shouldQueueForeignChild: () => true,
133
136
  });
134
137
 
135
- expect(boundaryInput.integrationContext).toEqual(
138
+ expect(renderInput.integrationContext).toEqual(
136
139
  expect.objectContaining({
137
140
  componentInstanceId: 'host',
138
141
  customKey: 'preserved',
@@ -141,12 +144,12 @@ describe('QueuedBoundaryRuntimeService', () => {
141
144
  );
142
145
  });
143
146
 
144
- it('resolves nested queued boundaries, applies root attributes, and dedupes bubbled assets', async () => {
145
- const service = new QueuedBoundaryRuntimeService();
147
+ it('resolves nested queued foreign subtrees, applies root attributes, and dedupes bubbled assets', async () => {
148
+ const service = new QueuedForeignSubtreeResolutionService();
146
149
  const shell = createComponent('shell', 'shell');
147
- const parentBoundary = createComponent('parent-boundary', 'deferred');
148
- const childBoundary = createComponent('child-boundary', 'deferred');
149
- const boundaryInput: ComponentRenderInput = {
150
+ const parentForeignSubtree = createComponent('parent-foreign-subtree', 'deferred');
151
+ const childForeignSubtree = createComponent('child-foreign-subtree', 'deferred');
152
+ const renderInput: ComponentRenderInput = {
150
153
  component: shell,
151
154
  props: {},
152
155
  integrationContext: {
@@ -156,66 +159,68 @@ describe('QueuedBoundaryRuntimeService', () => {
156
159
  const rendererCache = new Map<string, unknown>();
157
160
 
158
161
  const runtime = service.createRuntime({
159
- boundaryInput,
162
+ renderInput,
160
163
  rendererCache,
161
- runtimeContextKey: '__testQueuedBoundaryRuntime',
164
+ runtimeContextKey: '__testQueuedForeignSubtreeRuntime',
162
165
  tokenPrefix: '__TEST_QUEUE__',
163
- shouldQueueBoundary: () => true,
166
+ shouldQueueForeignChild: () => true,
164
167
  });
165
168
 
166
- const parentToken = runtime.interceptBoundarySync?.({
169
+ const parentToken = runtime.interceptForeignChildSync?.({
167
170
  currentIntegration: 'shell',
168
171
  targetIntegration: 'deferred',
169
- component: parentBoundary,
172
+ component: parentForeignSubtree,
170
173
  props: { label: 'parent' },
171
174
  });
172
175
 
173
- const childToken = runtime.interceptBoundarySync?.({
176
+ const childToken = runtime.interceptForeignChildSync?.({
174
177
  currentIntegration: 'shell',
175
178
  targetIntegration: 'deferred',
176
- component: childBoundary,
179
+ component: childForeignSubtree,
177
180
  props: { label: 'child' },
178
181
  });
179
182
 
180
- const runtimeContext = service.getRuntimeContext<QueuedBoundaryRuntimeContext>(
181
- boundaryInput,
182
- '__testQueuedBoundaryRuntime',
183
+ const runtimeContext = service.getRuntimeContext<QueuedForeignSubtreeResolutionContext>(
184
+ renderInput,
185
+ '__testQueuedForeignSubtreeRuntime',
183
186
  );
184
187
  if (!runtimeContext || parentToken?.kind !== 'resolved' || childToken?.kind !== 'resolved') {
185
- throw new Error('Failed to initialize queued boundary test runtime.');
188
+ throw new Error('Failed to initialize queued foreign-subtree test runtime.');
186
189
  }
187
190
 
188
191
  runtimeContext.queuedResolutions[0].props.children = `<slot>${childToken.value}</slot>`;
189
192
 
190
- const resolveBoundary = vi.fn(async (input: ComponentRenderInput): Promise<BoundaryRenderPayload> => {
191
- if (input.component === childBoundary) {
193
+ const resolveForeignSubtree = vi.fn(
194
+ async (input: ComponentRenderInput): Promise<ForeignSubtreeRenderPayload> => {
195
+ if (input.component === childForeignSubtree) {
196
+ return {
197
+ html: `<span>${String(input.props.label ?? '')}</span>`,
198
+ attachmentPolicy: { kind: 'first-element' },
199
+ rootTag: 'span',
200
+ integrationName: 'deferred',
201
+ rootAttributes: {
202
+ 'data-owner': 'child',
203
+ 'data-instance': String(
204
+ (input.integrationContext as { componentInstanceId?: string } | undefined)
205
+ ?.componentInstanceId ?? 'missing',
206
+ ),
207
+ },
208
+ assets: [createAsset('shared-asset'), createAsset('child-asset')],
209
+ };
210
+ }
211
+
192
212
  return {
193
- html: `<span>${String(input.props.label ?? '')}</span>`,
213
+ html: `<section>${input.children ?? ''}</section>`,
194
214
  attachmentPolicy: { kind: 'first-element' },
195
- rootTag: 'span',
215
+ rootTag: 'section',
196
216
  integrationName: 'deferred',
197
217
  rootAttributes: {
198
- 'data-owner': 'child',
199
- 'data-instance': String(
200
- (input.integrationContext as { componentInstanceId?: string } | undefined)
201
- ?.componentInstanceId ?? 'missing',
202
- ),
218
+ 'data-owner': 'parent',
203
219
  },
204
- assets: [createAsset('shared-asset'), createAsset('child-asset')],
220
+ assets: [createAsset('shared-asset'), createAsset('parent-asset')],
205
221
  };
206
- }
207
-
208
- return {
209
- html: `<section>${input.children ?? ''}</section>`,
210
- attachmentPolicy: { kind: 'first-element' },
211
- rootTag: 'section',
212
- integrationName: 'deferred',
213
- rootAttributes: {
214
- 'data-owner': 'parent',
215
- },
216
- assets: [createAsset('shared-asset'), createAsset('parent-asset')],
217
- };
218
- });
222
+ },
223
+ );
219
224
 
220
225
  const result = await service.resolveQueuedHtml({
221
226
  html: `<article>${parentToken.value}</article>`,
@@ -241,7 +246,7 @@ describe('QueuedBoundaryRuntimeService', () => {
241
246
  html,
242
247
  };
243
248
  },
244
- resolveBoundary,
249
+ resolveForeignSubtree,
245
250
  applyAttributesToFirstElement,
246
251
  dedupeProcessedAssets: dedupeAssets,
247
252
  });
@@ -255,27 +260,27 @@ describe('QueuedBoundaryRuntimeService', () => {
255
260
  createAsset('children-asset'),
256
261
  createAsset('parent-asset'),
257
262
  ]);
258
- expect(resolveBoundary).toHaveBeenCalledTimes(2);
263
+ expect(resolveForeignSubtree).toHaveBeenCalledTimes(2);
259
264
  });
260
265
 
261
- it('throws when queued boundaries form a cycle', async () => {
262
- const service = new QueuedBoundaryRuntimeService();
263
- const boundaryA = createComponent('boundary-a', 'deferred');
264
- const boundaryB = createComponent('boundary-b', 'deferred');
265
- const runtimeContext: QueuedBoundaryRuntimeContext = {
266
+ it('throws when queued foreign subtrees form a cycle', async () => {
267
+ const service = new QueuedForeignSubtreeResolutionService();
268
+ const foreignSubtreeA = createComponent('foreign-subtree-a', 'deferred');
269
+ const foreignSubtreeB = createComponent('foreign-subtree-b', 'deferred');
270
+ const runtimeContext: QueuedForeignSubtreeResolutionContext = {
266
271
  rendererCache: new Map<string, unknown>(),
267
272
  componentInstanceScope: 'host',
268
- nextBoundaryId: 2,
273
+ nextForeignSubtreeId: 2,
269
274
  queuedResolutions: [
270
275
  {
271
276
  token: '__TEST_QUEUE__host__1__',
272
- component: boundaryA,
277
+ component: foreignSubtreeA,
273
278
  props: { children: '__TEST_QUEUE__host__2__' },
274
279
  componentInstanceId: 'host_n_1',
275
280
  },
276
281
  {
277
282
  token: '__TEST_QUEUE__host__2__',
278
- component: boundaryB,
283
+ component: foreignSubtreeB,
279
284
  props: { children: '__TEST_QUEUE__host__1__' },
280
285
  componentInstanceId: 'host_n_2',
281
286
  },
@@ -304,7 +309,7 @@ describe('QueuedBoundaryRuntimeService', () => {
304
309
 
305
310
  return { assets: [], html };
306
311
  },
307
- resolveBoundary: async (input) => ({
312
+ resolveForeignSubtree: async (input) => ({
308
313
  html: `<section>${input.children ?? ''}</section>`,
309
314
  assets: [],
310
315
  attachmentPolicy: { kind: 'first-element' },