@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.
- package/README.md +63 -7
- package/package.json +4 -47
- package/src/adapters/bun/create-app.ts +54 -2
- package/src/adapters/bun/hmr-manager.test.ts +0 -2
- package/src/adapters/bun/hmr-manager.ts +1 -24
- package/src/adapters/bun/server-adapter.ts +30 -4
- package/src/adapters/node/node-hmr-manager.test.ts +0 -2
- package/src/adapters/node/node-hmr-manager.ts +2 -25
- package/src/adapters/shared/explicit-static-render-preparation.ts +58 -0
- package/src/adapters/shared/explicit-static-route-matcher.test.ts +6 -6
- package/src/adapters/shared/explicit-static-route-matcher.ts +22 -31
- package/src/adapters/shared/file-route-middleware-pipeline.test.ts +5 -10
- package/src/adapters/shared/file-route-middleware-pipeline.ts +8 -17
- package/src/adapters/shared/fs-server-response-factory.test.ts +32 -43
- package/src/adapters/shared/fs-server-response-factory.ts +15 -37
- package/src/adapters/shared/fs-server-response-matcher.test.ts +65 -39
- package/src/adapters/shared/fs-server-response-matcher.ts +94 -43
- package/src/adapters/shared/hmr-manager.contract.test.ts +0 -4
- package/src/adapters/shared/render-context.ts +3 -3
- package/src/adapters/shared/server-adapter.test.ts +53 -0
- package/src/adapters/shared/server-adapter.ts +228 -159
- package/src/adapters/shared/server-route-handler.test.ts +6 -5
- package/src/adapters/shared/server-route-handler.ts +4 -4
- package/src/adapters/shared/server-static-builder.test.ts +4 -4
- package/src/adapters/shared/server-static-builder.ts +4 -4
- package/src/config/README.md +1 -1
- package/src/config/config-builder.test.ts +0 -1
- package/src/config/config-builder.ts +2 -7
- package/src/dev/host-runtime.ts +34 -0
- package/src/eco/eco.browser.test.ts +2 -2
- package/src/eco/eco.browser.ts +2 -2
- package/src/eco/eco.test.ts +6 -6
- package/src/eco/eco.ts +12 -12
- package/src/eco/eco.types.ts +3 -3
- package/src/errors/index.ts +1 -0
- package/src/hmr/client/hmr-runtime.ts +4 -2
- package/src/hmr/strategies/js-hmr-strategy.test.ts +0 -1
- package/src/hmr/strategies/js-hmr-strategy.ts +0 -6
- package/src/integrations/ghtml/ghtml-renderer.test.ts +7 -7
- package/src/integrations/ghtml/ghtml-renderer.ts +1 -11
- package/src/plugins/eco-component-meta-plugin.ts +0 -1
- package/src/plugins/integration-plugin.test.ts +9 -14
- package/src/plugins/integration-plugin.ts +34 -22
- package/src/plugins/processor.ts +17 -0
- package/src/route-renderer/GRAPH.md +81 -289
- package/src/route-renderer/README.md +67 -105
- package/src/route-renderer/orchestration/component-render-context.ts +45 -38
- package/src/route-renderer/orchestration/declared-ownership-graph.ts +62 -0
- package/src/route-renderer/orchestration/foreign-subtree-execution.service.ts +383 -0
- package/src/route-renderer/orchestration/integration-renderer.test.ts +118 -121
- package/src/route-renderer/orchestration/integration-renderer.ts +362 -403
- package/src/route-renderer/orchestration/ownership-planning.service.ts +97 -0
- package/src/route-renderer/orchestration/ownership-validation.service.ts +76 -0
- package/src/route-renderer/orchestration/processed-asset-dedupe.ts +1 -1
- package/src/route-renderer/orchestration/{queued-boundary-runtime.service.test.ts → queued-foreign-subtree-resolution.service.test.ts} +76 -71
- package/src/route-renderer/orchestration/{queued-boundary-runtime.service.ts → queued-foreign-subtree-resolution.service.ts} +68 -63
- package/src/route-renderer/orchestration/render-output.utils.ts +21 -13
- package/src/route-renderer/orchestration/{render-preparation.service.test.ts → route-render-orchestrator.prepare-render-options.test.ts} +160 -85
- package/src/route-renderer/orchestration/route-render-orchestrator.test.ts +265 -0
- package/src/route-renderer/orchestration/{render-preparation.service.ts → route-render-orchestrator.ts} +244 -160
- package/src/route-renderer/page-loading/component-dependency-collection.ts +9 -3
- package/src/route-renderer/page-loading/declared-asset-collection.ts +2 -5
- package/src/route-renderer/page-loading/dependency-resolver.test.ts +107 -11
- package/src/route-renderer/page-loading/dependency-resolver.ts +6 -12
- package/src/route-renderer/page-loading/ecopages-virtual-imports.ts +1 -1
- package/src/route-renderer/page-loading/lazy-entry-collection.ts +1 -1
- package/src/route-renderer/page-loading/lazy-trigger-planning.ts +1 -1
- package/src/route-renderer/page-loading/module-declaration-aggregation.ts +1 -1
- package/src/route-renderer/page-loading/module-declaration-scripts.ts +1 -1
- package/src/route-renderer/page-loading/page-dependency-bundling.ts +105 -66
- package/src/route-renderer/route-renderer.ts +28 -31
- package/src/router/README.md +16 -19
- package/src/router/server/route-registry.test.ts +176 -0
- package/src/router/server/route-registry.ts +382 -0
- package/src/services/README.md +1 -2
- package/src/services/assets/asset-processing-service/asset-dependency-keys.ts +1 -1
- package/src/services/assets/asset-processing-service/asset-processing.service.test.ts +1 -4
- package/src/services/assets/asset-processing-service/asset-processing.service.ts +1 -2
- package/src/services/assets/asset-processing-service/assets.types.ts +3 -0
- package/src/services/assets/asset-processing-service/grouped-content-bundles.ts +1 -1
- package/src/services/assets/asset-processing-service/index.ts +1 -0
- package/src/{route-renderer/orchestration/page-packaging.service.test.ts → services/assets/asset-processing-service/page-package.test.ts} +38 -14
- package/src/services/assets/asset-processing-service/page-package.ts +93 -0
- package/src/services/assets/asset-processing-service/processors/base/base-script-processor.ts +4 -5
- package/src/services/assets/asset-processing-service/processors/script/content-script.processor.test.ts +13 -10
- package/src/services/assets/asset-processing-service/processors/script/content-script.processor.ts +3 -0
- package/src/services/assets/asset-processing-service/processors/script/file-script.processor.ts +6 -0
- package/src/services/assets/asset-processing-service/processors/script/node-module-script.processor.ts +2 -0
- package/src/services/assets/asset-processing-service/processors/stylesheet/content-stylesheet.processor.ts +1 -0
- package/src/services/assets/asset-processing-service/processors/stylesheet/file-stylesheet.processor.ts +2 -0
- package/src/services/assets/asset-processing-service/ungrouped-dependency-processing.ts +1 -1
- package/src/services/html/html-transformer.service.test.ts +1 -4
- package/src/services/module-loading/app-server-module-transpiler.service.ts +1 -3
- package/src/services/module-loading/node-bootstrap-plugin.ts +17 -3
- package/src/services/module-loading/page-module-import.service.ts +0 -1
- package/src/services/module-loading/source-module-support.ts +1 -1
- package/src/static-site-generator/static-site-generator.test.ts +124 -32
- package/src/static-site-generator/static-site-generator.ts +168 -185
- package/src/types/internal-types.ts +13 -12
- package/src/types/public-types.ts +55 -39
- package/src/watchers/project-watcher.test-helpers.ts +4 -3
- package/src/route-renderer/orchestration/boundary-planning.service.ts +0 -146
- package/src/route-renderer/orchestration/page-packaging.service.ts +0 -85
- package/src/route-renderer/orchestration/render-execution.service.test.ts +0 -196
- package/src/route-renderer/orchestration/render-execution.service.ts +0 -182
- package/src/route-renderer/orchestration/route-shell-composer.service.ts +0 -162
- package/src/router/server/fs-router-scanner.test.ts +0 -83
- package/src/router/server/fs-router-scanner.ts +0 -224
- package/src/router/server/fs-router.test.ts +0 -214
- package/src/router/server/fs-router.ts +0 -122
- 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
|
+
}
|
|
@@ -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
|
-
|
|
6
|
+
ForeignSubtreeRenderPayload,
|
|
7
7
|
ComponentRenderInput,
|
|
8
8
|
EcoComponent,
|
|
9
9
|
} from '../../types/public-types.ts';
|
|
10
|
-
import {
|
|
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('
|
|
54
|
-
it('creates scoped queue tokens and stores runtime state on the
|
|
55
|
-
const service = new
|
|
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
|
|
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
|
-
|
|
72
|
+
renderInput,
|
|
70
73
|
rendererCache,
|
|
71
|
-
runtimeContextKey: '
|
|
74
|
+
runtimeContextKey: '__testQueuedForeignSubtreeRuntime',
|
|
72
75
|
tokenPrefix: '__TEST_QUEUE__',
|
|
73
|
-
|
|
76
|
+
shouldQueueForeignChild: () => true,
|
|
74
77
|
});
|
|
75
78
|
|
|
76
|
-
const interception = runtime.
|
|
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<
|
|
90
|
-
|
|
91
|
-
'
|
|
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
|
-
|
|
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(
|
|
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
|
|
118
|
+
const service = new QueuedForeignSubtreeResolutionService();
|
|
116
119
|
const shell = createComponent('shell', 'shell');
|
|
117
|
-
const
|
|
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
|
-
|
|
131
|
+
renderInput,
|
|
129
132
|
rendererCache,
|
|
130
|
-
runtimeContextKey: '
|
|
133
|
+
runtimeContextKey: '__testQueuedForeignSubtreeRuntime',
|
|
131
134
|
tokenPrefix: '__TEST_QUEUE__',
|
|
132
|
-
|
|
135
|
+
shouldQueueForeignChild: () => true,
|
|
133
136
|
});
|
|
134
137
|
|
|
135
|
-
expect(
|
|
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
|
|
145
|
-
const service = new
|
|
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
|
|
148
|
-
const
|
|
149
|
-
const
|
|
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
|
-
|
|
162
|
+
renderInput,
|
|
160
163
|
rendererCache,
|
|
161
|
-
runtimeContextKey: '
|
|
164
|
+
runtimeContextKey: '__testQueuedForeignSubtreeRuntime',
|
|
162
165
|
tokenPrefix: '__TEST_QUEUE__',
|
|
163
|
-
|
|
166
|
+
shouldQueueForeignChild: () => true,
|
|
164
167
|
});
|
|
165
168
|
|
|
166
|
-
const parentToken = runtime.
|
|
169
|
+
const parentToken = runtime.interceptForeignChildSync?.({
|
|
167
170
|
currentIntegration: 'shell',
|
|
168
171
|
targetIntegration: 'deferred',
|
|
169
|
-
component:
|
|
172
|
+
component: parentForeignSubtree,
|
|
170
173
|
props: { label: 'parent' },
|
|
171
174
|
});
|
|
172
175
|
|
|
173
|
-
const childToken = runtime.
|
|
176
|
+
const childToken = runtime.interceptForeignChildSync?.({
|
|
174
177
|
currentIntegration: 'shell',
|
|
175
178
|
targetIntegration: 'deferred',
|
|
176
|
-
component:
|
|
179
|
+
component: childForeignSubtree,
|
|
177
180
|
props: { label: 'child' },
|
|
178
181
|
});
|
|
179
182
|
|
|
180
|
-
const runtimeContext = service.getRuntimeContext<
|
|
181
|
-
|
|
182
|
-
'
|
|
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
|
|
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
|
|
191
|
-
|
|
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: `<
|
|
213
|
+
html: `<section>${input.children ?? ''}</section>`,
|
|
194
214
|
attachmentPolicy: { kind: 'first-element' },
|
|
195
|
-
rootTag: '
|
|
215
|
+
rootTag: 'section',
|
|
196
216
|
integrationName: 'deferred',
|
|
197
217
|
rootAttributes: {
|
|
198
|
-
'data-owner': '
|
|
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('
|
|
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
|
-
|
|
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(
|
|
263
|
+
expect(resolveForeignSubtree).toHaveBeenCalledTimes(2);
|
|
259
264
|
});
|
|
260
265
|
|
|
261
|
-
it('throws when queued
|
|
262
|
-
const service = new
|
|
263
|
-
const
|
|
264
|
-
const
|
|
265
|
-
const runtimeContext:
|
|
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
|
-
|
|
273
|
+
nextForeignSubtreeId: 2,
|
|
269
274
|
queuedResolutions: [
|
|
270
275
|
{
|
|
271
276
|
token: '__TEST_QUEUE__host__1__',
|
|
272
|
-
component:
|
|
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:
|
|
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
|
-
|
|
312
|
+
resolveForeignSubtree: async (input) => ({
|
|
308
313
|
html: `<section>${input.children ?? ''}</section>`,
|
|
309
314
|
assets: [],
|
|
310
315
|
attachmentPolicy: { kind: 'first-element' },
|