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