@ecopages/core 0.2.0-alpha.12 → 0.2.0-alpha.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +7 -28
  2. package/README.md +5 -4
  3. package/package.json +2 -2
  4. package/src/adapters/bun/hmr-manager.js +2 -2
  5. package/src/adapters/node/node-hmr-manager.js +2 -2
  6. package/src/adapters/node/server-adapter.d.ts +2 -2
  7. package/src/adapters/node/server-adapter.js +5 -5
  8. package/src/build/build-adapter.d.ts +7 -6
  9. package/src/build/build-adapter.js +6 -7
  10. package/src/eco/eco.js +15 -6
  11. package/src/eco/eco.utils.d.ts +1 -1
  12. package/src/eco/eco.utils.js +5 -1
  13. package/src/hmr/hmr-strategy.d.ts +2 -2
  14. package/src/integrations/ghtml/ghtml-renderer.d.ts +6 -1
  15. package/src/integrations/ghtml/ghtml-renderer.js +29 -28
  16. package/src/plugins/integration-plugin.d.ts +1 -24
  17. package/src/plugins/integration-plugin.js +0 -14
  18. package/src/route-renderer/GRAPH.md +54 -84
  19. package/src/route-renderer/README.md +11 -22
  20. package/src/route-renderer/orchestration/component-render-context.d.ts +33 -84
  21. package/src/route-renderer/orchestration/component-render-context.js +30 -108
  22. package/src/route-renderer/orchestration/integration-renderer.d.ts +219 -96
  23. package/src/route-renderer/orchestration/integration-renderer.js +416 -236
  24. package/src/route-renderer/orchestration/queued-boundary-runtime.service.d.ts +93 -0
  25. package/src/route-renderer/orchestration/queued-boundary-runtime.service.js +155 -0
  26. package/src/route-renderer/orchestration/render-execution.service.d.ts +8 -71
  27. package/src/route-renderer/orchestration/render-execution.service.js +28 -115
  28. package/src/route-renderer/orchestration/render-output.utils.d.ts +6 -0
  29. package/src/route-renderer/orchestration/render-output.utils.js +25 -0
  30. package/src/route-renderer/orchestration/render-preparation.service.d.ts +0 -9
  31. package/src/route-renderer/orchestration/render-preparation.service.js +3 -34
  32. package/src/route-renderer/page-loading/dependency-resolver.js +6 -1
  33. package/src/route-renderer/page-loading/page-module-loader.d.ts +1 -2
  34. package/src/route-renderer/page-loading/page-module-loader.js +0 -2
  35. package/src/router/client/navigation-coordinator.js +2 -2
  36. package/src/router/server/fs-router-scanner.js +6 -1
  37. package/src/services/runtime-state/dev-graph.service.d.ts +5 -5
  38. package/src/services/runtime-state/dev-graph.service.js +10 -10
  39. package/src/types/public-types.d.ts +2 -5
  40. package/src/eco/component-render-context.d.ts +0 -2
  41. package/src/eco/component-render-context.js +0 -12
  42. package/src/route-renderer/component-graph/component-graph-executor.d.ts +0 -33
  43. package/src/route-renderer/component-graph/component-graph-executor.js +0 -30
  44. package/src/route-renderer/component-graph/component-graph.d.ts +0 -53
  45. package/src/route-renderer/component-graph/component-graph.js +0 -94
  46. package/src/route-renderer/component-graph/component-marker.d.ts +0 -52
  47. package/src/route-renderer/component-graph/component-marker.js +0 -44
  48. package/src/route-renderer/component-graph/component-reference.d.ts +0 -10
  49. package/src/route-renderer/component-graph/component-reference.js +0 -34
  50. package/src/route-renderer/component-graph/marker-graph-resolver.d.ts +0 -79
  51. package/src/route-renderer/component-graph/marker-graph-resolver.js +0 -117
@@ -0,0 +1,93 @@
1
+ import type { ProcessedAsset } from '../../services/assets/asset-processing-service/index.js';
2
+ import type { ComponentRenderInput, ComponentRenderResult, EcoComponent } from '../../types/public-types.js';
3
+ import type { ComponentBoundaryRuntime } from './component-render-context.js';
4
+ export type QueuedBoundaryDecisionInput = {
5
+ currentIntegration: string;
6
+ targetIntegration?: string;
7
+ component: EcoComponent;
8
+ props: Record<string, unknown>;
9
+ };
10
+ export type QueuedBoundaryResolution = {
11
+ token: string;
12
+ component: EcoComponent;
13
+ props: Record<string, unknown>;
14
+ componentInstanceId: string;
15
+ };
16
+ /**
17
+ * Shared mutable state for one renderer-owned queued boundary runtime.
18
+ *
19
+ * Renderers that cannot resolve foreign boundaries inline can enqueue transport
20
+ * tokens during their initial render, then resolve those tokens against the
21
+ * owning renderer before returning final HTML.
22
+ */
23
+ export type QueuedBoundaryRuntimeContext = {
24
+ rendererCache: Map<string, unknown>;
25
+ componentInstanceScope?: string;
26
+ nextBoundaryId: number;
27
+ queuedResolutions: QueuedBoundaryResolution[];
28
+ };
29
+ type QueuedBoundaryIntegrationContext = {
30
+ rendererCache?: Map<string, unknown>;
31
+ componentInstanceId?: string;
32
+ [key: string]: unknown;
33
+ };
34
+ type QueuedBoundaryChildRenderResult = {
35
+ assets: ProcessedAsset[];
36
+ html?: string;
37
+ };
38
+ /**
39
+ * Shared queue orchestration for renderer-owned boundary runtimes that emit
40
+ * temporary transport tokens during one render pass.
41
+ *
42
+ * The service keeps three responsibilities in one place:
43
+ * - storing per-render queue state on the active integration context
44
+ * - creating a `ComponentBoundaryRuntime` that enqueues foreign boundaries
45
+ * - resolving queued tokens back through the owning renderer before final HTML
46
+ * leaves the current renderer
47
+ *
48
+ * Renderers still own framework-specific child rendering. This service only
49
+ * handles queue bookkeeping, recursion, cycle detection, and asset merging.
50
+ */
51
+ export declare class QueuedBoundaryRuntimeService {
52
+ /**
53
+ * Reads the queued boundary runtime state previously attached to one render.
54
+ */
55
+ getRuntimeContext<TContext extends QueuedBoundaryRuntimeContext>(input: ComponentRenderInput, runtimeContextKey: string): TContext | undefined;
56
+ /**
57
+ * Creates the runtime hook used by `runWithComponentRenderContext()` for one
58
+ * renderer-owned queue.
59
+ *
60
+ * When the renderer decides a boundary must be handed off, the runtime returns
61
+ * a resolved transport token instead of rendering the foreign component inline.
62
+ */
63
+ createRuntime<TContext extends QueuedBoundaryRuntimeContext>(options: {
64
+ boundaryInput: ComponentRenderInput;
65
+ rendererCache: Map<string, unknown>;
66
+ runtimeContextKey: string;
67
+ tokenPrefix: string;
68
+ shouldQueueBoundary: (input: QueuedBoundaryDecisionInput) => boolean;
69
+ createRuntimeContext?: (integrationContext: QueuedBoundaryIntegrationContext, rendererCache: Map<string, unknown>) => TContext;
70
+ }): ComponentBoundaryRuntime;
71
+ /**
72
+ * Resolves every queued transport token in one renderer-owned HTML fragment.
73
+ *
74
+ * The caller supplies framework-specific child rendering, while this service
75
+ * handles recursive token replacement, cycle detection, root-attribute
76
+ * application, and merged asset collection.
77
+ */
78
+ resolveQueuedHtml<TContext extends QueuedBoundaryRuntimeContext>(options: {
79
+ html: string;
80
+ runtimeContext?: TContext;
81
+ queueLabel: string;
82
+ renderQueuedChildren: (children: unknown, runtimeContext: TContext, queuedResolutionsByToken: Map<string, QueuedBoundaryResolution>, resolveToken: (token: string) => Promise<string>) => Promise<QueuedBoundaryChildRenderResult>;
83
+ resolveBoundary: (input: ComponentRenderInput, rendererCache: Map<string, unknown>) => Promise<ComponentRenderResult | undefined>;
84
+ applyAttributesToFirstElement: (html: string, attributes: Record<string, string>) => string;
85
+ dedupeProcessedAssets: (assets: ProcessedAsset[]) => ProcessedAsset[];
86
+ }): Promise<{
87
+ assets: ProcessedAsset[];
88
+ html: string;
89
+ }>;
90
+ private createBoundaryToken;
91
+ private ensureRuntimeContext;
92
+ }
93
+ export {};
@@ -0,0 +1,155 @@
1
+ class QueuedBoundaryRuntimeService {
2
+ /**
3
+ * Reads the queued boundary runtime state previously attached to one render.
4
+ */
5
+ getRuntimeContext(input, runtimeContextKey) {
6
+ const integrationContext = input.integrationContext;
7
+ const runtimeContext = integrationContext?.[runtimeContextKey];
8
+ if (typeof runtimeContext !== "object" || runtimeContext === null) {
9
+ return void 0;
10
+ }
11
+ return runtimeContext;
12
+ }
13
+ /**
14
+ * Creates the runtime hook used by `runWithComponentRenderContext()` for one
15
+ * renderer-owned queue.
16
+ *
17
+ * When the renderer decides a boundary must be handed off, the runtime returns
18
+ * a resolved transport token instead of rendering the foreign component inline.
19
+ */
20
+ createRuntime(options) {
21
+ const runtimeContext = this.ensureRuntimeContext(options);
22
+ const interceptBoundary = (input) => {
23
+ if (!options.shouldQueueBoundary(input)) {
24
+ return { kind: "inline" };
25
+ }
26
+ runtimeContext.nextBoundaryId += 1;
27
+ const boundaryId = runtimeContext.nextBoundaryId;
28
+ const token = this.createBoundaryToken(options.tokenPrefix, runtimeContext, boundaryId);
29
+ runtimeContext.queuedResolutions.push({
30
+ token,
31
+ component: input.component,
32
+ props: { ...input.props },
33
+ componentInstanceId: runtimeContext.componentInstanceScope ? `${runtimeContext.componentInstanceScope}_n_${boundaryId}` : `n_${boundaryId}`
34
+ });
35
+ return {
36
+ kind: "resolved",
37
+ value: token
38
+ };
39
+ };
40
+ return {
41
+ interceptBoundary,
42
+ interceptBoundarySync: interceptBoundary
43
+ };
44
+ }
45
+ /**
46
+ * Resolves every queued transport token in one renderer-owned HTML fragment.
47
+ *
48
+ * The caller supplies framework-specific child rendering, while this service
49
+ * handles recursive token replacement, cycle detection, root-attribute
50
+ * application, and merged asset collection.
51
+ */
52
+ async resolveQueuedHtml(options) {
53
+ if (!options.runtimeContext || options.runtimeContext.queuedResolutions.length === 0) {
54
+ return { assets: [], html: options.html };
55
+ }
56
+ const runtimeContext = options.runtimeContext;
57
+ const queuedResolutionsByToken = new Map(
58
+ runtimeContext.queuedResolutions.map((resolution) => [resolution.token, resolution])
59
+ );
60
+ const resolvedHtmlByToken = /* @__PURE__ */ new Map();
61
+ const resolvingTokens = /* @__PURE__ */ new Set();
62
+ const collectedAssets = [];
63
+ const resolveToken = async (token) => {
64
+ const cachedHtml = resolvedHtmlByToken.get(token);
65
+ if (cachedHtml) {
66
+ return cachedHtml;
67
+ }
68
+ const resolution = queuedResolutionsByToken.get(token);
69
+ if (!resolution) {
70
+ return token;
71
+ }
72
+ if (resolvingTokens.has(token)) {
73
+ throw new Error(
74
+ `[ecopages] ${options.queueLabel} boundary queue contains a cycle or unresolved dependency links.`
75
+ );
76
+ }
77
+ resolvingTokens.add(token);
78
+ try {
79
+ const renderedChildren = await options.renderQueuedChildren(
80
+ resolution.props.children,
81
+ runtimeContext,
82
+ queuedResolutionsByToken,
83
+ resolveToken
84
+ );
85
+ if (renderedChildren.assets.length > 0) {
86
+ collectedAssets.push(...renderedChildren.assets);
87
+ }
88
+ const boundaryRender = await options.resolveBoundary(
89
+ {
90
+ component: resolution.component,
91
+ props: { ...resolution.props },
92
+ children: renderedChildren.html,
93
+ integrationContext: {
94
+ rendererCache: runtimeContext.rendererCache,
95
+ componentInstanceId: resolution.componentInstanceId
96
+ }
97
+ },
98
+ runtimeContext.rendererCache
99
+ );
100
+ if (!boundaryRender) {
101
+ throw new Error(
102
+ `[ecopages] ${options.queueLabel} queued boundary could not resolve its owning renderer.`
103
+ );
104
+ }
105
+ if ((boundaryRender.assets?.length ?? 0) > 0) {
106
+ collectedAssets.push(...boundaryRender.assets ?? []);
107
+ }
108
+ const resolvedHtml2 = boundaryRender.canAttachAttributes && boundaryRender.rootAttributes ? options.applyAttributesToFirstElement(boundaryRender.html, boundaryRender.rootAttributes) : boundaryRender.html;
109
+ resolvedHtmlByToken.set(token, resolvedHtml2);
110
+ return resolvedHtml2;
111
+ } finally {
112
+ resolvingTokens.delete(token);
113
+ }
114
+ };
115
+ let resolvedHtml = options.html;
116
+ for (const resolution of runtimeContext.queuedResolutions) {
117
+ if (!resolvedHtml.includes(resolution.token)) {
118
+ continue;
119
+ }
120
+ resolvedHtml = resolvedHtml.split(resolution.token).join(await resolveToken(resolution.token));
121
+ }
122
+ return {
123
+ assets: options.dedupeProcessedAssets(collectedAssets),
124
+ html: resolvedHtml
125
+ };
126
+ }
127
+ createBoundaryToken(tokenPrefix, runtimeContext, boundaryId) {
128
+ return `${tokenPrefix}${runtimeContext.componentInstanceScope ?? "root"}__${boundaryId}__`;
129
+ }
130
+ ensureRuntimeContext(options) {
131
+ let integrationContext;
132
+ if (typeof options.boundaryInput.integrationContext === "object" && options.boundaryInput.integrationContext !== null) {
133
+ integrationContext = options.boundaryInput.integrationContext;
134
+ } else {
135
+ integrationContext = {};
136
+ }
137
+ const existingRuntimeContext = integrationContext[options.runtimeContextKey];
138
+ if (typeof existingRuntimeContext !== "object" || existingRuntimeContext === null) {
139
+ integrationContext[options.runtimeContextKey] = options.createRuntimeContext?.(integrationContext, options.rendererCache) ?? {
140
+ rendererCache: options.rendererCache,
141
+ componentInstanceScope: integrationContext.componentInstanceId,
142
+ nextBoundaryId: 0,
143
+ queuedResolutions: []
144
+ };
145
+ } else {
146
+ existingRuntimeContext.rendererCache = options.rendererCache;
147
+ }
148
+ integrationContext.rendererCache = options.rendererCache;
149
+ options.boundaryInput.integrationContext = integrationContext;
150
+ return integrationContext[options.runtimeContextKey];
151
+ }
152
+ }
153
+ export {
154
+ QueuedBoundaryRuntimeService
155
+ };
@@ -1,74 +1,31 @@
1
- import { type ComponentRenderBoundaryContext } from './component-render-context.js';
2
- import type { EcoComponent, IntegrationRendererRenderOptions, RouteRendererBody, RouteRendererOptions, RouteRenderResult } from '../../types/public-types.js';
3
- import type { ProcessedAsset } from '../../services/assets/asset-processing-service/index.js';
4
- import type { MarkerGraphContext } from '../component-graph/marker-graph-resolver.js';
5
- /**
6
- * Serializable graph context merged from render-time captured references and
7
- * optional explicit page-module graph metadata.
8
- */
9
- export type RenderExecutionGraphContext = {
10
- propsByRef?: Record<string, Record<string, unknown>>;
11
- slotChildrenByRef?: MarkerGraphContext['slotChildrenByRef'];
12
- };
1
+ import type { IntegrationRendererRenderOptions, RouteRendererBody, RouteRendererOptions, RouteRenderResult } from '../../types/public-types.js';
13
2
  export interface CapturedHtmlRenderResult {
14
3
  body: RouteRendererBody;
15
4
  html: string;
16
- graphContext: RenderExecutionGraphContext;
17
5
  }
18
6
  export interface FinalizeHtmlRenderOptions {
19
7
  html: string;
20
- graphContext: RenderExecutionGraphContext;
21
- componentsToResolve: EcoComponent[];
22
8
  componentRootAttributes?: Record<string, string>;
23
9
  documentAttributes?: Record<string, string>;
24
- mergeAssets?: boolean;
25
10
  }
26
11
  export interface RenderExecutionCallbacks<C> {
27
12
  prepareRenderOptions(options: RouteRendererOptions): Promise<IntegrationRendererRenderOptions<C>>;
28
13
  render(renderOptions: IntegrationRendererRenderOptions<C>): Promise<RouteRendererBody>;
29
- getComponentRenderBoundaryContext(): ComponentRenderBoundaryContext;
30
- serializeDeferredValue?: (value: unknown, serializeValue: (value: unknown) => string | undefined) => string | undefined;
31
14
  getDocumentAttributes(renderOptions: IntegrationRendererRenderOptions<C>): Record<string, string> | undefined;
32
- resolveMarkerGraphHtml(input: {
33
- html: string;
34
- componentsToResolve: EcoComponent[];
35
- graphContext: RenderExecutionGraphContext;
36
- }): Promise<{
37
- html: string;
38
- assets: ProcessedAsset[];
39
- }>;
40
- dedupeProcessedAssets(assets: ProcessedAsset[]): ProcessedAsset[];
41
- getProcessedDependencies(): ProcessedAsset[];
42
- setProcessedDependencies(dependencies: ProcessedAsset[]): void;
43
15
  applyAttributesToHtmlElement(html: string, attributes: Record<string, string>): string;
44
16
  applyAttributesToFirstBodyElement(html: string, attributes: Record<string, string>): string;
45
17
  transformResponse(response: Response): Promise<RouteRendererBody>;
46
18
  }
47
- export interface FinalizeHtmlRenderCallbacks {
48
- resolveMarkerGraphHtml(input: {
49
- html: string;
50
- componentsToResolve: EcoComponent[];
51
- graphContext: RenderExecutionGraphContext;
52
- }): Promise<{
53
- html: string;
54
- assets: ProcessedAsset[];
55
- }>;
56
- dedupeProcessedAssets(assets: ProcessedAsset[]): ProcessedAsset[];
57
- getProcessedDependencies(): ProcessedAsset[];
58
- setProcessedDependencies(dependencies: ProcessedAsset[]): void;
59
- applyAttributesToHtmlElement(html: string, attributes: Record<string, string>): string;
60
- applyAttributesToFirstBodyElement(html: string, attributes: Record<string, string>): string;
61
- }
62
19
  /**
63
20
  * Executes the main post-preparation rendering flow for integration renderers.
64
21
  *
65
22
  * This service owns the orchestration that happens after normalized render
66
- * options have been prepared: the first render pass, graph-context capture,
67
- * deferred marker resolution, root-attribute application, and final HTML
68
- * transformation into a response body stream.
23
+ * options have been prepared: one render pass, unresolved boundary-marker
24
+ * enforcement, root-attribute application, and final HTML transformation into
25
+ * a response body stream.
69
26
  */
70
27
  export declare class RenderExecutionService {
71
- captureHtmlRender(currentIntegrationName: string, boundaryContext: ComponentRenderBoundaryContext, serializeDeferredValue: ((value: unknown, serializeValue: (value: unknown) => string | undefined) => string | undefined) | undefined, render: () => Promise<RouteRendererBody>): Promise<CapturedHtmlRenderResult>;
28
+ captureHtmlRender(render: () => Promise<RouteRendererBody>): Promise<CapturedHtmlRenderResult>;
72
29
  /**
73
30
  * Executes one integration render pass and returns the final route render
74
31
  * result.
@@ -79,28 +36,8 @@ export declare class RenderExecutionService {
79
36
  * @param callbacks Renderer-specific hooks required during execution.
80
37
  * @returns Final route render output with body and cache strategy.
81
38
  */
82
- execute<C = unknown>(options: RouteRendererOptions, currentIntegrationName: string, callbacks: RenderExecutionCallbacks<C>): Promise<RouteRenderResult>;
39
+ execute<C = unknown>(options: RouteRendererOptions, callbacks: RenderExecutionCallbacks<C>): Promise<RouteRenderResult>;
83
40
  private captureRenderedBody;
84
- /**
85
- * Merges captured render-time graph references with any explicit graph context
86
- * provided by the page module.
87
- *
88
- * @param capturedGraphContext Graph context captured from the first render pass.
89
- * @param explicitGraphContext Optional page-module graph metadata.
90
- * @returns Merged graph context used during marker resolution.
91
- */
92
- mergeGraphContext(capturedGraphContext: RenderExecutionGraphContext, explicitGraphContext?: RenderExecutionGraphContext): RenderExecutionGraphContext;
93
- finalizeHtmlRender(options: FinalizeHtmlRenderOptions, callbacks: FinalizeHtmlRenderCallbacks): Promise<{
94
- html: string;
95
- assets: ProcessedAsset[];
96
- }>;
97
- /**
98
- * Returns the component set that participates in marker graph resolution for a
99
- * render pass.
100
- *
101
- * @typeParam C Integration render output element type.
102
- * @param renderOptions Normalized render options for the pass.
103
- * @returns Ordered component list for graph registry construction.
104
- */
105
- private getComponentsToResolve;
41
+ finalizeHtmlRender(options: FinalizeHtmlRenderOptions, callbacks: Pick<RenderExecutionCallbacks<unknown>, 'applyAttributesToHtmlElement' | 'applyAttributesToFirstBodyElement'>): Promise<string>;
42
+ private applyFinalHtmlAttributes;
106
43
  }
@@ -1,39 +1,11 @@
1
- import {
2
- runWithComponentRenderContext
3
- } from "./component-render-context.js";
4
- function decodeHtmlEntities(value) {
5
- let decoded = value;
6
- let previous;
7
- do {
8
- previous = decoded;
9
- decoded = decoded.replaceAll("&quot;", '"').replaceAll("&#39;", "'").replaceAll("&#x27;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
10
- } while (decoded !== previous);
11
- return decoded;
12
- }
13
- function restoreEscapedComponentMarkers(html) {
14
- return html.replace(
15
- /&(?:amp;)?lt;eco-marker\b[\s\S]*?&(?:amp;)?gt;&(?:amp;)?lt;\/eco-marker&(?:amp;)?gt;/g,
16
- (marker) => decodeHtmlEntities(marker)
17
- );
18
- }
19
- const MAX_MARKER_RESOLUTION_PASSES = 10;
1
+ import { inspectBoundaryArtifactHtml } from "./render-output.utils.js";
20
2
  class RenderExecutionService {
21
- async captureHtmlRender(currentIntegrationName, boundaryContext, serializeDeferredValue, render) {
22
- const renderExecution = await runWithComponentRenderContext(
23
- {
24
- currentIntegration: currentIntegrationName,
25
- boundaryContext,
26
- serializeDeferredValue
27
- },
28
- async () => {
29
- const renderedBody = await render();
30
- return await this.captureRenderedBody(renderedBody);
31
- }
32
- );
3
+ async captureHtmlRender(render) {
4
+ const renderedBody = await render();
5
+ const capturedRender = await this.captureRenderedBody(renderedBody);
33
6
  return {
34
- body: renderExecution.value.body,
35
- html: renderExecution.value.html,
36
- graphContext: renderExecution.graphContext
7
+ body: capturedRender.body,
8
+ html: capturedRender.html
37
9
  };
38
10
  }
39
11
  /**
@@ -46,18 +18,19 @@ class RenderExecutionService {
46
18
  * @param callbacks Renderer-specific hooks required during execution.
47
19
  * @returns Final route render output with body and cache strategy.
48
20
  */
49
- async execute(options, currentIntegrationName, callbacks) {
21
+ async execute(options, callbacks) {
50
22
  const renderOptions = await callbacks.prepareRenderOptions(options);
51
23
  const shouldApplyComponentRootAttributes = renderOptions.componentRender?.canAttachAttributes && renderOptions.componentRender.rootAttributes && Object.keys(renderOptions.componentRender.rootAttributes).length > 0;
52
- const renderExecution = await this.captureHtmlRender(
53
- currentIntegrationName,
54
- callbacks.getComponentRenderBoundaryContext(),
55
- callbacks.serializeDeferredValue,
56
- async () => callbacks.render(renderOptions)
57
- );
58
- const normalizedCapturedHtml = restoreEscapedComponentMarkers(renderExecution.html);
24
+ const renderExecution = await this.captureHtmlRender(async () => callbacks.render(renderOptions));
25
+ const boundaryArtifacts = inspectBoundaryArtifactHtml(renderExecution.html);
59
26
  const documentAttributes = callbacks.getDocumentAttributes(renderOptions);
60
- const canReuseCapturedBody = !normalizedCapturedHtml.includes("<eco-marker") && !shouldApplyComponentRootAttributes && !(documentAttributes && Object.keys(documentAttributes).length > 0);
27
+ const hasBoundaryMarkerHtml = boundaryArtifacts.hasUnresolvedBoundaryArtifacts;
28
+ if (hasBoundaryMarkerHtml) {
29
+ throw new Error(
30
+ "[ecopages] Route render returned unresolved boundary artifact HTML. Full-route unresolved-boundary fallback has been removed; resolve mixed boundaries inside renderComponentBoundary()."
31
+ );
32
+ }
33
+ const canReuseCapturedBody = !hasBoundaryMarkerHtml && !shouldApplyComponentRootAttributes && !(documentAttributes && Object.keys(documentAttributes).length > 0);
61
34
  if (canReuseCapturedBody) {
62
35
  const body2 = await callbacks.transformResponse(
63
36
  new Response(renderExecution.body, {
@@ -71,23 +44,19 @@ class RenderExecutionService {
71
44
  cacheStrategy: renderOptions.cacheStrategy
72
45
  };
73
46
  }
74
- const componentGraphContext = this.mergeGraphContext(
75
- renderExecution.graphContext,
76
- renderOptions.componentGraphContext
77
- );
78
47
  const finalization = await this.finalizeHtmlRender(
79
48
  {
80
- html: normalizedCapturedHtml,
81
- graphContext: componentGraphContext,
82
- componentsToResolve: this.getComponentsToResolve(renderOptions),
49
+ html: boundaryArtifacts.normalizedHtml,
83
50
  componentRootAttributes: shouldApplyComponentRootAttributes ? renderOptions.componentRender?.rootAttributes : void 0,
84
- documentAttributes,
85
- mergeAssets: true
51
+ documentAttributes
86
52
  },
87
- callbacks
53
+ {
54
+ applyAttributesToHtmlElement: callbacks.applyAttributesToHtmlElement,
55
+ applyAttributesToFirstBodyElement: callbacks.applyAttributesToFirstBodyElement
56
+ }
88
57
  );
89
58
  const body = await callbacks.transformResponse(
90
- new Response(finalization.html, {
59
+ new Response(finalization, {
91
60
  headers: {
92
61
  "Content-Type": "text/html"
93
62
  }
@@ -118,74 +87,18 @@ class RenderExecutionService {
118
87
  html: await new Response(capturedBody).text()
119
88
  };
120
89
  }
121
- /**
122
- * Merges captured render-time graph references with any explicit graph context
123
- * provided by the page module.
124
- *
125
- * @param capturedGraphContext Graph context captured from the first render pass.
126
- * @param explicitGraphContext Optional page-module graph metadata.
127
- * @returns Merged graph context used during marker resolution.
128
- */
129
- mergeGraphContext(capturedGraphContext, explicitGraphContext) {
130
- return {
131
- propsByRef: {
132
- ...capturedGraphContext.propsByRef ?? {},
133
- ...explicitGraphContext?.propsByRef ?? {}
134
- },
135
- slotChildrenByRef: {
136
- ...capturedGraphContext.slotChildrenByRef ?? {},
137
- ...explicitGraphContext?.slotChildrenByRef ?? {}
138
- }
139
- };
140
- }
141
90
  async finalizeHtmlRender(options, callbacks) {
142
- let renderedHtml = restoreEscapedComponentMarkers(options.html);
143
- let markerAssets = [];
144
- for (let pass = 0; pass < MAX_MARKER_RESOLUTION_PASSES; pass += 1) {
145
- if (!renderedHtml.includes("<eco-marker")) {
146
- break;
147
- }
148
- const markerResolution = await callbacks.resolveMarkerGraphHtml({
149
- html: renderedHtml,
150
- componentsToResolve: options.componentsToResolve,
151
- graphContext: options.graphContext
152
- });
153
- const resolvedHtml = restoreEscapedComponentMarkers(markerResolution.html);
154
- markerAssets = callbacks.dedupeProcessedAssets([...markerAssets, ...markerResolution.assets]);
155
- if (options.mergeAssets !== false && markerResolution.assets.length > 0) {
156
- const mergedDependencies = callbacks.dedupeProcessedAssets([
157
- ...callbacks.getProcessedDependencies(),
158
- ...markerResolution.assets
159
- ]);
160
- callbacks.setProcessedDependencies(mergedDependencies);
161
- }
162
- if (resolvedHtml === renderedHtml) {
163
- renderedHtml = resolvedHtml;
164
- break;
165
- }
166
- renderedHtml = resolvedHtml;
167
- }
91
+ return this.applyFinalHtmlAttributes(options.html, options, callbacks);
92
+ }
93
+ applyFinalHtmlAttributes(html, options, callbacks) {
94
+ let renderedHtml = html;
168
95
  if (options.componentRootAttributes && Object.keys(options.componentRootAttributes).length > 0) {
169
96
  renderedHtml = callbacks.applyAttributesToFirstBodyElement(renderedHtml, options.componentRootAttributes);
170
97
  }
171
98
  if (options.documentAttributes && Object.keys(options.documentAttributes).length > 0) {
172
99
  renderedHtml = callbacks.applyAttributesToHtmlElement(renderedHtml, options.documentAttributes);
173
100
  }
174
- return {
175
- html: renderedHtml,
176
- assets: markerAssets
177
- };
178
- }
179
- /**
180
- * Returns the component set that participates in marker graph resolution for a
181
- * render pass.
182
- *
183
- * @typeParam C Integration render output element type.
184
- * @param renderOptions Normalized render options for the pass.
185
- * @returns Ordered component list for graph registry construction.
186
- */
187
- getComponentsToResolve(renderOptions) {
188
- return renderOptions.Layout ? [renderOptions.HtmlTemplate, renderOptions.Layout, renderOptions.Page] : [renderOptions.HtmlTemplate, renderOptions.Page];
101
+ return renderedHtml;
189
102
  }
190
103
  }
191
104
  export {
@@ -38,3 +38,9 @@ export declare function addTriggerAttribute(content: unknown, triggerId: string)
38
38
  * @param lazyGroups Resolved lazy script groups attached to the component config.
39
39
  */
40
40
  export declare function wrapWithScriptsInjector(content: unknown, lazyGroups: NonNullable<EcoComponent['config']>['_resolvedLazyScripts']): string;
41
+ export declare function decodeHtmlEntities(value: string): string;
42
+ export declare function normalizeBoundaryArtifactHtml(html: string): string;
43
+ export declare function inspectBoundaryArtifactHtml(html: string): {
44
+ hasUnresolvedBoundaryArtifacts: boolean;
45
+ normalizedHtml: string;
46
+ };
@@ -33,8 +33,33 @@ function wrapWithScriptsInjector(content, lazyGroups) {
33
33
  const injectorMapScript = buildInjectorMapScript(lazyGroups ?? []);
34
34
  return `<scripts-injector><script type="ecopages/injector-map">${injectorMapScript}<\/script>${wrappedContent}<\/scripts-injector>`;
35
35
  }
36
+ function decodeHtmlEntities(value) {
37
+ let decoded = value;
38
+ let previous;
39
+ do {
40
+ previous = decoded;
41
+ decoded = decoded.replaceAll("&quot;", '"').replaceAll("&#39;", "'").replaceAll("&#x27;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
42
+ } while (decoded !== previous);
43
+ return decoded;
44
+ }
45
+ function normalizeBoundaryArtifactHtml(html) {
46
+ return html.replace(
47
+ /&(?:amp;)?lt;eco-marker\b[\s\S]*?&(?:amp;)?gt;&(?:amp;)?lt;\/eco-marker&(?:amp;)?gt;/g,
48
+ (marker) => decodeHtmlEntities(marker)
49
+ );
50
+ }
51
+ function inspectBoundaryArtifactHtml(html) {
52
+ const normalizedHtml = normalizeBoundaryArtifactHtml(html);
53
+ return {
54
+ normalizedHtml,
55
+ hasUnresolvedBoundaryArtifacts: normalizedHtml.includes("<eco-marker")
56
+ };
57
+ }
36
58
  export {
37
59
  addTriggerAttribute,
60
+ decodeHtmlEntities,
61
+ inspectBoundaryArtifactHtml,
38
62
  isThenable,
63
+ normalizeBoundaryArtifactHtml,
39
64
  wrapWithScriptsInjector
40
65
  };
@@ -1,12 +1,10 @@
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 { type ComponentGraphContext, type ComponentRenderBoundaryContext } from './component-render-context.js';
5
4
  type ResolvedPageModule = {
6
5
  Page: EcoPageFile['default'] | EcoPageComponent<any>;
7
6
  getStaticProps?: GetStaticProps<Record<string, unknown>>;
8
7
  getMetadata?: GetMetadata;
9
- componentGraphContext?: ComponentGraphContext;
10
8
  integrationSpecificProps: Record<string, unknown>;
11
9
  };
12
10
  export interface RenderPreparationCallbacks {
@@ -30,12 +28,6 @@ export interface RenderPreparationCallbacks {
30
28
  component: EcoComponent;
31
29
  props: Record<string, unknown>;
32
30
  }): Promise<ComponentRenderResult>;
33
- /**
34
- * Returns the boundary policy context that should be active while rendering
35
- * page-root component output during preparation.
36
- */
37
- getComponentRenderBoundaryContext(): ComponentRenderBoundaryContext;
38
- serializeDeferredValue?: (value: unknown, serializeValue: (value: unknown) => string | undefined) => string | undefined;
39
31
  setProcessedDependencies(dependencies: ProcessedAsset[]): void;
40
32
  dedupeProcessedAssets(assets: ProcessedAsset[]): ProcessedAsset[];
41
33
  createPageLocalsProxy(filePath: string): RouteRendererOptions['locals'];
@@ -74,7 +66,6 @@ export declare class RenderPreparationService {
74
66
  * @returns Normalized render options.
75
67
  */
76
68
  prepare<C = EcoPagesElement>(routeOptions: RouteRendererOptions, currentIntegrationName: string, callbacks: RenderPreparationCallbacks): Promise<IntegrationRendererRenderOptions<C>>;
77
- private mergeGraphContext;
78
69
  /**
79
70
  * Collects resolved lazy trigger metadata from the component tree.
80
71
  *