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