@ecopages/core 0.2.0-beta.27 → 0.2.0-beta.28

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 (70) hide show
  1. package/README.md +1 -1
  2. package/package.json +42 -26
  3. package/src/eco/eco.js +2 -2
  4. package/src/eco/eco.utils.d.ts +1 -1
  5. package/src/eco/eco.utils.js +1 -1
  6. package/src/hmr/client/hmr-runtime.js +1 -1
  7. package/src/hmr/hmr-file-change-prep.d.ts +1 -1
  8. package/src/hmr/hmr-file-change-prep.js +1 -1
  9. package/src/plugins/processor.d.ts +8 -0
  10. package/src/route-renderer/GRAPH.md +11 -11
  11. package/src/route-renderer/README.md +11 -6
  12. package/src/route-renderer/orchestration/{document-shell-render.service.d.ts → document-shell/document-shell-render.service.d.ts} +19 -2
  13. package/src/route-renderer/orchestration/{document-shell-render.service.js → document-shell/document-shell-render.service.js} +28 -0
  14. package/src/route-renderer/orchestration/{layout-shell-props.service.d.ts → document-shell/layout-shell-props.service.d.ts} +1 -1
  15. package/src/route-renderer/orchestration/{component-render-context.d.ts → foreign-child/component-render-context.d.ts} +1 -1
  16. package/src/route-renderer/orchestration/{component-render-context.js → foreign-child/component-render-context.js} +1 -1
  17. package/src/route-renderer/orchestration/{render-output.utils.d.ts → foreign-child/foreign-child-output.utils.d.ts} +1 -7
  18. package/src/route-renderer/orchestration/{render-output.utils.js → foreign-child/foreign-child-output.utils.js} +1 -26
  19. package/src/route-renderer/orchestration/{foreign-subtree-execution.service.d.ts → foreign-child/foreign-subtree-execution.service.d.ts} +5 -5
  20. package/src/route-renderer/orchestration/{foreign-subtree-execution.service.js → foreign-child/foreign-subtree-execution.service.js} +2 -2
  21. package/src/route-renderer/orchestration/foreign-child/owning-renderer-resolution.d.ts +19 -0
  22. package/src/route-renderer/orchestration/foreign-child/owning-renderer-resolution.js +35 -0
  23. package/src/route-renderer/orchestration/integration-renderer.d.ts +4 -46
  24. package/src/route-renderer/orchestration/integration-renderer.js +53 -157
  25. package/src/route-renderer/orchestration/integration-renderer.test-fixtures.d.ts +75 -0
  26. package/src/route-renderer/orchestration/integration-renderer.test-fixtures.js +177 -0
  27. package/src/route-renderer/orchestration/{component-graph-collectors.d.ts → ownership-graph/component-graph-collectors.d.ts} +4 -4
  28. package/src/route-renderer/orchestration/{component-graph-collectors.js → ownership-graph/component-graph-collectors.js} +1 -1
  29. package/src/route-renderer/orchestration/{component-graph.d.ts → ownership-graph/component-graph.d.ts} +1 -3
  30. package/src/route-renderer/orchestration/{component-graph.js → ownership-graph/component-graph.js} +0 -2
  31. package/src/route-renderer/orchestration/{ownership-validation.service.d.ts → ownership-graph/ownership-validation.service.d.ts} +2 -2
  32. package/src/route-renderer/orchestration/{ownership-validation.service.js → ownership-graph/ownership-validation.service.js} +3 -4
  33. package/src/route-renderer/orchestration/{global-injector-assets.service.d.ts → page-browser-graph/global-injector-assets.service.d.ts} +3 -3
  34. package/src/route-renderer/orchestration/{global-injector-assets.service.js → page-browser-graph/global-injector-assets.service.js} +2 -2
  35. package/src/route-renderer/orchestration/{page-browser-graph-contribution.loader.d.ts → page-browser-graph/page-browser-graph-contribution.loader.d.ts} +1 -1
  36. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph-merge.utils.d.ts +12 -0
  37. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph-merge.utils.js +29 -0
  38. package/src/route-renderer/orchestration/{page-browser-graph-session.d.ts → page-browser-graph/page-browser-graph-session.d.ts} +3 -3
  39. package/src/route-renderer/orchestration/{page-browser-graph-session.js → page-browser-graph/page-browser-graph-session.js} +1 -1
  40. package/src/route-renderer/orchestration/{page-browser-graph.service.d.ts → page-browser-graph/page-browser-graph.service.d.ts} +3 -3
  41. package/src/route-renderer/orchestration/{page-browser-graph.service.js → page-browser-graph/page-browser-graph.service.js} +3 -3
  42. package/src/route-renderer/orchestration/{processed-asset-dedupe.d.ts → page-browser-graph/processed-asset-dedupe.d.ts} +1 -1
  43. package/src/route-renderer/orchestration/{integration-route-render-adapter.d.ts → route-pipeline/integration-route-render-adapter.d.ts} +3 -3
  44. package/src/route-renderer/orchestration/{integration-route-render-adapter.js → route-pipeline/integration-route-render-adapter.js} +1 -1
  45. package/src/route-renderer/orchestration/route-pipeline/marker-artifact.utils.d.ts +6 -0
  46. package/src/route-renderer/orchestration/route-pipeline/marker-artifact.utils.js +27 -0
  47. package/src/route-renderer/orchestration/{route-html-finalization.service.d.ts → route-pipeline/route-html-finalization.service.d.ts} +2 -2
  48. package/src/route-renderer/orchestration/{route-prepared-options.builder.d.ts → route-pipeline/route-prepared-options.builder.d.ts} +3 -3
  49. package/src/route-renderer/orchestration/{route-prepared-options.builder.js → route-pipeline/route-prepared-options.builder.js} +2 -2
  50. package/src/route-renderer/orchestration/{route-prepared-options.utils.js → route-pipeline/route-prepared-options.utils.js} +1 -1
  51. package/src/route-renderer/orchestration/{route-render-orchestrator.d.ts → route-pipeline/route-render-orchestrator.d.ts} +6 -6
  52. package/src/route-renderer/orchestration/{route-render-orchestrator.js → route-pipeline/route-render-orchestrator.js} +8 -5
  53. package/src/route-renderer/orchestration/string-markup-renderer.js +1 -1
  54. package/src/services/html/html-transformer.service.js +1 -1
  55. package/src/services/invalidation/development-invalidation.service.d.ts +5 -0
  56. package/src/services/invalidation/development-invalidation.service.js +8 -9
  57. package/src/static-site-generator/production-page-browser-graph-prebuild.js +1 -1
  58. package/src/types/internal-types.d.ts +1 -1
  59. package/src/types/public-types.d.ts +1 -1
  60. package/src/watchers/project-watcher.d.ts +3 -3
  61. package/src/watchers/project-watcher.js +4 -4
  62. package/src/route-renderer/orchestration/declared-ownership-graph.d.ts +0 -1
  63. package/src/route-renderer/orchestration/declared-ownership-graph.js +0 -8
  64. package/src/route-renderer/orchestration/template-serialization.d.ts +0 -38
  65. package/src/route-renderer/orchestration/template-serialization.js +0 -45
  66. /package/src/route-renderer/orchestration/{layout-shell-props.service.js → document-shell/layout-shell-props.service.js} +0 -0
  67. /package/src/route-renderer/orchestration/{page-browser-graph-contribution.loader.js → page-browser-graph/page-browser-graph-contribution.loader.js} +0 -0
  68. /package/src/route-renderer/orchestration/{processed-asset-dedupe.js → page-browser-graph/processed-asset-dedupe.js} +0 -0
  69. /package/src/route-renderer/orchestration/{route-html-finalization.service.js → route-pipeline/route-html-finalization.service.js} +0 -0
  70. /package/src/route-renderer/orchestration/{route-prepared-options.utils.d.ts → route-pipeline/route-prepared-options.utils.d.ts} +0 -0
@@ -4,16 +4,16 @@
4
4
  * @module
5
5
  */
6
6
  import type { EcoPagesAppConfig, IHmrManager } from '../../types/internal-types.js';
7
- import type { ComponentRenderInput, ComponentRenderResult, EcoComponent, EcoPageFile, EcoPagesElement, BaseIntegrationContext, HtmlTemplateProps, IntegrationRendererRenderOptions, PageBrowserGraphContribution, PageBrowserGraphContributionContext, PageBrowserGraphResult, PageMetadataProps, PagePackageResult, RouteRendererBody, RouteRendererOptions, RouteRenderResult } from '../../types/public-types.js';
7
+ import type { ComponentRenderInput, ComponentRenderResult, EcoComponent, EcoPageFile, EcoPagesElement, HtmlTemplateProps, IntegrationRendererRenderOptions, PageBrowserGraphContribution, PageBrowserGraphContributionContext, PageBrowserGraphResult, 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 type { HtmlDocumentContribution } from '../../services/html/html-transformer.service.js';
11
11
  import { HttpError } from '../../errors/http-error.js';
12
12
  import { DependencyResolverService } from '../page-loading/dependency-resolver.js';
13
13
  import { PageModuleLoaderService } from '../page-loading/page-module-loader.js';
14
- import { RouteRenderOrchestrator, type RouteRenderOrchestratorAdapter, type RouteRenderOrchestratorResolvedInputs } from './route-render-orchestrator.js';
15
- import type { ForeignChildRuntime } from './component-render-context.js';
16
- import { ForeignSubtreeExecutionService, type ForeignSubtreeExecutionOwningRenderer, type ForeignSubtreeQueuedHtmlOptions, type QueuedForeignSubtreeResolutionContext } from './foreign-subtree-execution.service.js';
14
+ import { RouteRenderOrchestrator, type RouteRenderOrchestratorAdapter, type RouteRenderOrchestratorResolvedInputs } from './route-pipeline/route-render-orchestrator.js';
15
+ import type { ForeignChildRuntime } from './foreign-child/component-render-context.js';
16
+ import { ForeignSubtreeExecutionService } from './foreign-child/foreign-subtree-execution.service.js';
17
17
  /**
18
18
  * Controls how one route module is loaded outside the normal render path.
19
19
  *
@@ -118,7 +118,6 @@ export declare abstract class IntegrationRenderer<C = EcoPagesElement> {
118
118
  */
119
119
  protected prepareViewDependencies(view: EcoComponent, layout?: EcoComponent): Promise<ProcessedAsset[]>;
120
120
  protected resolvePageBrowserGraphForFile(filePath: string): Promise<PageBrowserGraphResult | undefined>;
121
- protected mergePageBrowserGraphIntoPagePackage(pageBrowserGraph?: PageBrowserGraphResult): PageBrowserGraphResult | undefined;
122
121
  /**
123
122
  * Merges component-scoped assets into the active HTML transformer state.
124
123
  *
@@ -208,20 +207,6 @@ export declare abstract class IntegrationRenderer<C = EcoPagesElement> {
208
207
  transformDocumentHtml?: (html: string) => string;
209
208
  }): Promise<string>;
210
209
  protected renderStringComponentWithSerializedChildren(input: ComponentRenderInput, component: (props: Record<string, unknown>) => Promise<EcoPagesElement> | EcoPagesElement): Promise<ComponentRenderResult>;
211
- protected getForeignSubtreeTokenPrefix(): string;
212
- protected getForeignSubtreeResolutionContextKey(): string;
213
- protected createQueuedForeignSubtreeExecutionRuntime<TContext extends QueuedForeignSubtreeResolutionContext>(options: {
214
- renderInput: ComponentRenderInput;
215
- rendererCache: Map<string, IntegrationRenderer<any>>;
216
- runtimeContextKey?: string;
217
- tokenPrefix?: string;
218
- createRuntimeContext?: (integrationContext: BaseIntegrationContext & Record<string, unknown>, rendererCache: Map<string, unknown>) => TContext;
219
- }): ForeignChildRuntime;
220
- protected getQueuedForeignSubtreeResolutionContext<TContext extends QueuedForeignSubtreeResolutionContext>(input: ComponentRenderInput): TContext | undefined;
221
- protected resolveQueuedForeignSubtreeHtml<TContext extends QueuedForeignSubtreeResolutionContext>(html: string, runtimeContext: TContext | undefined, renderQueuedChildren: ForeignSubtreeQueuedHtmlOptions<TContext>['renderQueuedChildren'], queueLabel?: string): Promise<{
222
- assets: ProcessedAsset[];
223
- html: string;
224
- }>;
225
210
  /**
226
211
  * Renders a string-first component, then resolves any queued foreign
227
212
  * boundaries before returning final component HTML.
@@ -291,7 +276,6 @@ export declare abstract class IntegrationRenderer<C = EcoPagesElement> {
291
276
  resolvedDependencies: ProcessedAsset[];
292
277
  }>;
293
278
  protected renderRouteBody(renderOptions: IntegrationRendererRenderOptions<C>): Promise<RouteRendererBody>;
294
- protected transformRouteResponse(response: Response, htmlContributions?: HtmlDocumentContribution[], pagePackage?: PagePackageResult): Promise<RouteRendererBody>;
295
279
  /**
296
280
  * Prepares the render options for the integration renderer.
297
281
  * It imports the page file, collects dependencies, and prepares the render options.
@@ -318,21 +302,6 @@ export declare abstract class IntegrationRenderer<C = EcoPagesElement> {
318
302
  * @returns Rendered route body plus effective cache strategy.
319
303
  */
320
304
  execute(options: RouteRendererOptions): Promise<RouteRenderResult>;
321
- /**
322
- * Finalizes already-resolved HTML for explicit renderer-owned paths.
323
- *
324
- * This keeps document and root-attribute stamping plus HTML transformation
325
- * available after a renderer has completed nested foreign-subtree resolution without
326
- * routing back through shared route execution.
327
- */
328
- protected finalizeResolvedHtml(options: {
329
- html: string;
330
- partial?: boolean;
331
- componentRootAttributes?: Record<string, string>;
332
- documentAttributes?: Record<string, string>;
333
- transformHtml?: boolean;
334
- htmlContributions?: HtmlDocumentContribution[];
335
- }): Promise<string>;
336
305
  /**
337
306
  * Returns document-level attributes to stamp onto the rendered `<html>` tag.
338
307
  *
@@ -352,17 +321,6 @@ export declare abstract class IntegrationRenderer<C = EcoPagesElement> {
352
321
  * response finalization logic.
353
322
  */
354
323
  protected getHtmlDocumentContributions(_options: HtmlDocumentContributionContext<C>): HtmlDocumentContribution[] | undefined;
355
- /**
356
- * Returns a renderer instance for a given integration name.
357
- *
358
- * Uses a per-execution cache to avoid repeated renderer initialization.
359
- *
360
- * @param integrationName Target integration name.
361
- * @param cache Render-pass renderer cache.
362
- * @returns Renderer for the requested integration.
363
- * @throws Error when no integration plugin matches `integrationName`.
364
- */
365
- protected getIntegrationRendererForName(integrationName: string, cache: Map<string, ForeignSubtreeExecutionOwningRenderer>): ForeignSubtreeExecutionOwningRenderer;
366
324
  /**
367
325
  * Abstract method to render the integration-specific component.
368
326
  * This method should be implemented by the specific integration renderer.
@@ -6,25 +6,31 @@ import { invariant } from "../../utils/invariant.js";
6
6
  import { HttpError } from "../../errors/http-error.js";
7
7
  import { DependencyResolverService } from "../page-loading/dependency-resolver.js";
8
8
  import { PageModuleLoaderService } from "../page-loading/page-module-loader.js";
9
- import { OwnershipValidationService } from "./ownership-validation.service.js";
10
- import { hasForeignChildDescendantsInGraph } from "./component-graph-collectors.js";
9
+ import { OwnershipValidationService } from "./ownership-graph/ownership-validation.service.js";
10
+ import { hasForeignChildDescendantsInGraph } from "./ownership-graph/component-graph-collectors.js";
11
11
  import {
12
12
  RouteRenderOrchestrator
13
- } from "./route-render-orchestrator.js";
14
- import { createIntegrationRouteRenderAdapter } from "./integration-route-render-adapter.js";
15
- import { loadPageBrowserGraphContribution } from "./page-browser-graph-contribution.loader.js";
16
- import { normalizeUnresolvedMarkerArtifactHtml, isMarkupNodeLike } from "./render-output.utils.js";
17
- import { ensureIntegrationRuntimeReady } from "../../build/app-build-manifest-runtime.js";
13
+ } from "./route-pipeline/route-render-orchestrator.js";
14
+ import { createIntegrationRouteRenderAdapter } from "./route-pipeline/integration-route-render-adapter.js";
15
+ import { loadPageBrowserGraphContribution } from "./page-browser-graph/page-browser-graph-contribution.loader.js";
16
+ import { normalizeUnresolvedMarkerArtifactHtml } from "./route-pipeline/marker-artifact.utils.js";
17
+ import { isMarkupNodeLike } from "./foreign-child/foreign-child-output.utils.js";
18
18
  import {
19
- ForeignSubtreeExecutionService
20
- } from "./foreign-subtree-execution.service.js";
21
- import { buildProcessedAssetDedupeKey } from "./processed-asset-dedupe.js";
19
+ getForeignSubtreeResolutionContextKey,
20
+ getForeignSubtreeTokenPrefix,
21
+ resolveOwningIntegrationRenderer
22
+ } from "./foreign-child/owning-renderer-resolution.js";
23
+ import { ensureIntegrationRuntimeReady } from "../../build/app-build-manifest-runtime.js";
24
+ import { ForeignSubtreeExecutionService } from "./foreign-child/foreign-subtree-execution.service.js";
22
25
  import {
23
- applyDocumentShellAttributeStamping,
24
26
  composeDocumentShell,
27
+ finalizeDocumentShellHtml,
25
28
  renderPageDocumentShell
26
- } from "./document-shell-render.service.js";
27
- import { resolveInnermostPageLayout, resolvePageLayoutComponents } from "./layout-shell-props.service.js";
29
+ } from "./document-shell/document-shell-render.service.js";
30
+ import {
31
+ resolveInnermostPageLayout,
32
+ resolvePageLayoutComponents
33
+ } from "./document-shell/layout-shell-props.service.js";
28
34
  class IntegrationRenderer {
29
35
  appConfig;
30
36
  assetProcessingService;
@@ -186,36 +192,6 @@ class IntegrationRenderer {
186
192
  )
187
193
  });
188
194
  }
189
- mergePageBrowserGraphIntoPagePackage(pageBrowserGraph) {
190
- if (!pageBrowserGraph) {
191
- return void 0;
192
- }
193
- const currentPagePackage = this.htmlTransformer.getPagePackage();
194
- const mergedPageBrowserGraph = currentPagePackage?.pageBrowserGraph ? {
195
- entryAssets: this.htmlTransformer.dedupeProcessedAssets([
196
- ...currentPagePackage.pageBrowserGraph.entryAssets,
197
- ...pageBrowserGraph.entryAssets
198
- ]),
199
- chunkAssets: this.htmlTransformer.dedupeProcessedAssets([
200
- ...currentPagePackage.pageBrowserGraph.chunkAssets,
201
- ...pageBrowserGraph.chunkAssets
202
- ])
203
- } : pageBrowserGraph;
204
- const pageBrowserGraphAssetKeys = new Set(
205
- [...mergedPageBrowserGraph.entryAssets, ...mergedPageBrowserGraph.chunkAssets].map(
206
- (asset) => buildProcessedAssetDedupeKey(asset)
207
- )
208
- );
209
- const baseAssets = currentPagePackage ? currentPagePackage.assets.filter(
210
- (asset) => !pageBrowserGraphAssetKeys.has(buildProcessedAssetDedupeKey(asset))
211
- ) : this.htmlTransformer.getProcessedDependencies();
212
- this.htmlTransformer.setPagePackage(
213
- createPagePackage(this.htmlTransformer.dedupeProcessedAssets(baseAssets), {
214
- pageBrowserGraph: mergedPageBrowserGraph
215
- })
216
- );
217
- return mergedPageBrowserGraph;
218
- }
219
195
  /**
220
196
  * Merges component-scoped assets into the active HTML transformer state.
221
197
  *
@@ -333,9 +309,11 @@ class IntegrationRenderer {
333
309
  }
334
310
  );
335
311
  const transformedDocumentHtml = input.transformDocumentHtml ? input.transformDocumentHtml(documentHtml) : documentHtml;
336
- const html = await this.finalizeResolvedHtml({
312
+ this.appendProcessedDependencies(this.getRendererBootstrapDependencies(false));
313
+ const html = await finalizeDocumentShellHtml(this.htmlTransformer, {
337
314
  html: `${this.DOC_TYPE}${transformedDocumentHtml}`,
338
- partial: false
315
+ partial: false,
316
+ htmlContributions: this.getHtmlDocumentContributions({ partial: false })
339
317
  });
340
318
  return this.createHtmlResponse(html, input.ctx);
341
319
  }
@@ -389,39 +367,6 @@ class IntegrationRenderer {
389
367
  assets
390
368
  };
391
369
  }
392
- getForeignSubtreeTokenPrefix() {
393
- return `__${this.name}_foreign_subtree__`;
394
- }
395
- getForeignSubtreeResolutionContextKey() {
396
- return `__${this.name}_foreign_subtree_runtime__`;
397
- }
398
- createQueuedForeignSubtreeExecutionRuntime(options) {
399
- return this.foreignSubtreeExecutionService.createQueuedRuntime({
400
- renderInput: options.renderInput,
401
- rendererCache: options.rendererCache,
402
- runtimeContextKey: options.runtimeContextKey ?? this.getForeignSubtreeResolutionContextKey(),
403
- tokenPrefix: options.tokenPrefix ?? this.getForeignSubtreeTokenPrefix(),
404
- createRuntimeContext: options.createRuntimeContext
405
- });
406
- }
407
- getQueuedForeignSubtreeResolutionContext(input) {
408
- return this.foreignSubtreeExecutionService.getQueuedRuntimeContext(
409
- input,
410
- this.getForeignSubtreeResolutionContextKey()
411
- );
412
- }
413
- async resolveQueuedForeignSubtreeHtml(html, runtimeContext, renderQueuedChildren, queueLabel = this.name) {
414
- return this.foreignSubtreeExecutionService.resolveQueuedHtml({
415
- currentIntegrationName: this.name,
416
- html,
417
- runtimeContext,
418
- queueLabel,
419
- getOwningRenderer: (integrationName, rendererCache) => this.getIntegrationRendererForName(integrationName, rendererCache),
420
- applyAttributesToFirstElement: (resolvedHtml, attributes) => this.htmlTransformer.applyAttributesToFirstElement(resolvedHtml, attributes),
421
- dedupeProcessedAssets: (assets) => this.htmlTransformer.dedupeProcessedAssets(assets),
422
- renderQueuedChildren
423
- });
424
- }
425
370
  /**
426
371
  * Renders a string-first component, then resolves any queued foreign
427
372
  * boundaries before returning final component HTML.
@@ -432,9 +377,16 @@ class IntegrationRenderer {
432
377
  currentIntegrationName: this.name,
433
378
  renderInput: input,
434
379
  html: componentRender.html,
435
- runtimeContextKey: this.getForeignSubtreeResolutionContextKey(),
380
+ runtimeContextKey: getForeignSubtreeResolutionContextKey(this.name),
436
381
  queueLabel: "String",
437
- getOwningRenderer: (integrationName, rendererCache) => this.getIntegrationRendererForName(integrationName, rendererCache),
382
+ getOwningRenderer: (integrationName, rendererCache) => resolveOwningIntegrationRenderer({
383
+ appConfig: this.appConfig,
384
+ runtimeOrigin: this.runtimeOrigin,
385
+ currentIntegrationName: this.name,
386
+ currentRenderer: this,
387
+ integrationName,
388
+ cache: rendererCache
389
+ }),
438
390
  applyAttributesToFirstElement: (html, attributes) => this.htmlTransformer.applyAttributesToFirstElement(html, attributes),
439
391
  dedupeProcessedAssets: (assets) => this.htmlTransformer.dedupeProcessedAssets(assets)
440
392
  });
@@ -559,7 +511,15 @@ class IntegrationRenderer {
559
511
  getDocumentAttributes: (renderOptions) => this.getDocumentAttributes(renderOptions),
560
512
  getHtmlDocumentContributions: (options) => this.getHtmlDocumentContributions(options),
561
513
  applyAttributesToHtmlElement: (html, attributes) => this.applyAttributesToHtmlElement(html, attributes),
562
- transformRouteResponse: (response, htmlContributions, pagePackage) => this.transformRouteResponse(response, htmlContributions, pagePackage)
514
+ transformRouteResponse: async (response, htmlContributions, pagePackage) => {
515
+ const resolvedPagePackage = this.htmlTransformer.getPagePackage() ?? pagePackage;
516
+ const transformedResponse = await this.htmlTransformer.transform(
517
+ response,
518
+ htmlContributions,
519
+ resolvedPagePackage
520
+ );
521
+ return transformedResponse.body ?? await transformedResponse.text();
522
+ }
563
523
  });
564
524
  }
565
525
  async resolveRouteRenderInputs(routeOptions) {
@@ -594,15 +554,6 @@ class IntegrationRenderer {
594
554
  async renderRouteBody(renderOptions) {
595
555
  return this.render(renderOptions);
596
556
  }
597
- async transformRouteResponse(response, htmlContributions, pagePackage) {
598
- const resolvedPagePackage = this.htmlTransformer.getPagePackage() ?? pagePackage;
599
- const transformedResponse = await this.htmlTransformer.transform(
600
- response,
601
- htmlContributions,
602
- resolvedPagePackage
603
- );
604
- return transformedResponse.body ?? await transformedResponse.text();
605
- }
606
557
  /**
607
558
  * Prepares the render options for the integration renderer.
608
559
  * It imports the page file, collects dependencies, and prepares the render options.
@@ -641,42 +592,6 @@ class IntegrationRenderer {
641
592
  return this.routeRenderOrchestrator.executePrepared(renderOptions, adapter);
642
593
  });
643
594
  }
644
- /**
645
- * Finalizes already-resolved HTML for explicit renderer-owned paths.
646
- *
647
- * This keeps document and root-attribute stamping plus HTML transformation
648
- * available after a renderer has completed nested foreign-subtree resolution without
649
- * routing back through shared route execution.
650
- */
651
- async finalizeResolvedHtml(options) {
652
- const rendererBootstrapDependencies = this.getRendererBootstrapDependencies(options.partial);
653
- this.appendProcessedDependencies(rendererBootstrapDependencies);
654
- let html = applyDocumentShellAttributeStamping(
655
- options.html,
656
- {
657
- applyAttributesToFirstBodyElement: (nextHtml, attributes) => this.htmlTransformer.applyAttributesToFirstBodyElement(nextHtml, attributes),
658
- applyAttributesToHtmlElement: (nextHtml, attributes) => this.htmlTransformer.applyAttributesToHtmlElement(nextHtml, attributes)
659
- },
660
- {
661
- componentRootAttributes: options.componentRootAttributes,
662
- documentAttributes: options.documentAttributes
663
- }
664
- );
665
- const shouldTransform = options.transformHtml ?? !options.partial;
666
- if (!shouldTransform) {
667
- return html;
668
- }
669
- const htmlContributions = options.htmlContributions ?? this.getHtmlDocumentContributions({
670
- partial: options.partial
671
- });
672
- const transformedResponse = await this.htmlTransformer.transform(
673
- new Response(html, {
674
- headers: { "Content-Type": "text/html" }
675
- }),
676
- htmlContributions
677
- );
678
- return await transformedResponse.text();
679
- }
680
595
  /**
681
596
  * Returns document-level attributes to stamp onto the rendered `<html>` tag.
682
597
  *
@@ -704,34 +619,6 @@ class IntegrationRenderer {
704
619
  getHtmlDocumentContributions(_options) {
705
620
  return void 0;
706
621
  }
707
- /**
708
- * Returns a renderer instance for a given integration name.
709
- *
710
- * Uses a per-execution cache to avoid repeated renderer initialization.
711
- *
712
- * @param integrationName Target integration name.
713
- * @param cache Render-pass renderer cache.
714
- * @returns Renderer for the requested integration.
715
- * @throws Error when no integration plugin matches `integrationName`.
716
- */
717
- getIntegrationRendererForName(integrationName, cache) {
718
- if (cache.has(integrationName)) {
719
- return cache.get(integrationName);
720
- }
721
- if (integrationName === this.name) {
722
- cache.set(integrationName, this);
723
- return this;
724
- }
725
- const integrationPlugin = this.appConfig.integrations.find(
726
- (integration) => integration.name === integrationName
727
- );
728
- invariant(!!integrationPlugin, `[ecopages] Integration not found for foreign owner: ${integrationName}`);
729
- const renderer = integrationPlugin.initializeRenderer({
730
- rendererModules: this.appConfig.runtime?.rendererModuleContext
731
- });
732
- cache.set(integrationName, renderer);
733
- return renderer;
734
- }
735
622
  /**
736
623
  * Renders one component under this integration's foreign-child runtime and resolves
737
624
  * any nested foreign children captured during that render.
@@ -751,7 +638,14 @@ class IntegrationRenderer {
751
638
  renderInput,
752
639
  rendererCache
753
640
  }),
754
- getOwningRenderer: (integrationName, rendererCache) => this.getIntegrationRendererForName(integrationName, rendererCache)
641
+ getOwningRenderer: (integrationName, rendererCache) => resolveOwningIntegrationRenderer({
642
+ appConfig: this.appConfig,
643
+ runtimeOrigin: this.runtimeOrigin,
644
+ currentIntegrationName: this.name,
645
+ currentRenderer: this,
646
+ integrationName,
647
+ cache: rendererCache
648
+ })
755
649
  });
756
650
  }
757
651
  normalizeComponentRenderOutput(result) {
@@ -838,9 +732,11 @@ class IntegrationRenderer {
838
732
  * context or a different foreign-child execution strategy.
839
733
  */
840
734
  createForeignChildRuntime(options) {
841
- return this.createQueuedForeignSubtreeExecutionRuntime({
735
+ return this.foreignSubtreeExecutionService.createQueuedRuntime({
842
736
  renderInput: options.renderInput,
843
- rendererCache: options.rendererCache
737
+ rendererCache: options.rendererCache,
738
+ runtimeContextKey: getForeignSubtreeResolutionContextKey(this.name),
739
+ tokenPrefix: getForeignSubtreeTokenPrefix(this.name)
844
740
  });
845
741
  }
846
742
  /**
@@ -0,0 +1,75 @@
1
+ import { IntegrationRenderer, type HtmlDocumentContribution, type RenderToResponseContext } from './integration-renderer.js';
2
+ import { getComponentRenderContext, type ForeignChildRuntime } from './foreign-child/component-render-context.js';
3
+ import type { EcoPagesAppConfig } from '../../types/internal-types.js';
4
+ import type { AssetProcessingService, ProcessedAsset } from '../../services/assets/asset-processing-service/index.js';
5
+ import type { ComponentRenderInput, ComponentRenderResult, EcoPagesElement, IntegrationRendererRenderOptions, EcoPageFile, RouteRendererBody, RouteRendererOptions, EcoComponent, HtmlTemplateProps } from '../../types/public-types.js';
6
+ import type { PagePackageResult } from '../../types/public-types.js';
7
+ export declare function createUnresolvedMarkerArtifact(nodeId: string, componentRef: string, propsRef: string): string;
8
+ export declare function createMockIntegrationPlugin(plugin: {
9
+ name: string;
10
+ } & Record<string, unknown>): EcoPagesAppConfig['integrations'][number];
11
+ export declare const testAppConfig: EcoPagesAppConfig;
12
+ export declare const testAssetService: AssetProcessingService;
13
+ /**
14
+ * Concrete implementation with ed file loading for testing purposes.
15
+ */
16
+ export declare class TestIntegrationRenderer extends IntegrationRenderer<EcoPagesElement> {
17
+ name: string;
18
+ ForeignChildRuntimeCreationCount: number;
19
+ ForeignChildRenderCount: number;
20
+ UseFailFastForeignChildRuntime: boolean;
21
+ PageModule: EcoPageFile | null;
22
+ HtmlTemplate: EcoComponent<HtmlTemplateProps> | null;
23
+ RenderedBody: RouteRendererBody;
24
+ RenderBodyFactory: ((context: ReturnType<typeof getComponentRenderContext>) => RouteRendererBody | Promise<RouteRendererBody>) | null;
25
+ MockComponentRenderResult: ComponentRenderResult | null;
26
+ ImportedFiles: string[];
27
+ HtmlContributions: HtmlDocumentContribution[];
28
+ render(_options: IntegrationRendererRenderOptions<EcoPagesElement>): Promise<RouteRendererBody>;
29
+ renderComponent(_input: ComponentRenderInput): Promise<ComponentRenderResult>;
30
+ renderComponentWithForeignChildren(input: ComponentRenderInput): Promise<ComponentRenderResult>;
31
+ renderToResponse<P>(view: EcoComponent<P>, props: P, ctx: RenderToResponseContext): Promise<Response>;
32
+ protected importPageFile(_file: string): Promise<EcoPageFile>;
33
+ protected getHtmlTemplate(): Promise<EcoComponent<HtmlTemplateProps>>;
34
+ protected resolveDependencies(_components: (EcoComponent | Partial<EcoComponent>)[]): Promise<ProcessedAsset[]>;
35
+ testPrepareRenderOptions(options: RouteRendererOptions): Promise<IntegrationRendererRenderOptions<EcoPagesElement>>;
36
+ testShouldDelegateForeignChild(input: {
37
+ currentIntegration: string;
38
+ targetIntegration?: string;
39
+ }): boolean;
40
+ testHasForeignChildDescendants(component: EcoComponent): boolean;
41
+ testGetHtmlTemplate(): Promise<import("../../index.browser.js").EcoFunctionComponent<HtmlTemplateProps<EcoPagesElement>, any>>;
42
+ testBaseGetHtmlTemplate(): Promise<import("../../index.browser.js").EcoFunctionComponent<HtmlTemplateProps<EcoPagesElement>, any>>;
43
+ testGetRendererBootstrapDependencies(partial?: boolean): ProcessedAsset[];
44
+ testFinalizeDocumentShellHtml(options: {
45
+ html: string;
46
+ partial?: boolean;
47
+ htmlContributions?: HtmlDocumentContribution[];
48
+ }): Promise<string>;
49
+ testTransformRouteResponse(response: Response, htmlContributions?: HtmlDocumentContribution[], pagePackage?: PagePackageResult): Promise<RouteRendererBody>;
50
+ appendTestProcessedDependencies(assets: ProcessedAsset[]): ProcessedAsset[];
51
+ setTestPagePackage(pagePackage: PagePackageResult): void;
52
+ getTestPagePackage(): PagePackageResult | undefined;
53
+ getTestProcessedDependencies(): ProcessedAsset[];
54
+ testRenderPartialViewResponse<P>(input: {
55
+ view: EcoComponent<P>;
56
+ props: P;
57
+ ctx?: RenderToResponseContext;
58
+ renderInline?: () => Promise<BodyInit>;
59
+ transformHtml?: (html: string) => string;
60
+ }): Promise<Response>;
61
+ testRenderViewWithDocumentShell<P>(input: {
62
+ view: EcoComponent<P>;
63
+ props: P;
64
+ ctx?: RenderToResponseContext;
65
+ layout?: EcoComponent;
66
+ }): Promise<Response>;
67
+ testRenderForeignSubtree(input: ComponentRenderInput): Promise<import("../../index.browser.js").ForeignSubtreeRenderPayload>;
68
+ protected createForeignChildRuntime(options: {
69
+ renderInput: ComponentRenderInput;
70
+ rendererCache: Map<string, IntegrationRenderer<any>>;
71
+ }): ForeignChildRuntime;
72
+ protected getHtmlDocumentContributions(_options?: {
73
+ partial?: boolean;
74
+ }): HtmlDocumentContribution[] | undefined;
75
+ }
@@ -0,0 +1,177 @@
1
+ import { vi } from "vitest";
2
+ import { resolveInnermostPageLayout } from "./document-shell/layout-shell-props.service.js";
3
+ import { finalizeDocumentShellHtml } from "./document-shell/document-shell-render.service.js";
4
+ import {
5
+ IntegrationRenderer
6
+ } from "./integration-renderer.js";
7
+ import { getComponentRenderContext } from "./foreign-child/component-render-context.js";
8
+ import { toForeignSubtreeRenderPayload } from "./foreign-child/foreign-subtree-execution.service.js";
9
+ function createUnresolvedMarkerArtifact(nodeId, componentRef, propsRef) {
10
+ return `<eco-marker data-eco-node-id="${nodeId}" data-eco-component-ref="${componentRef}" data-eco-props-ref="${propsRef}"></eco-marker>`;
11
+ }
12
+ function createMockIntegrationPlugin(plugin) {
13
+ return {
14
+ setConfig: vi.fn(),
15
+ setRuntimeOrigin: vi.fn(),
16
+ setup: vi.fn(async () => {
17
+ }),
18
+ ...plugin
19
+ };
20
+ }
21
+ const testAppConfig = {
22
+ absolutePaths: {
23
+ pagesDir: "/app/pages",
24
+ htmlTemplatePath: "/app/index.ghtml.ts"
25
+ },
26
+ integrations: [],
27
+ defaultMetadata: {
28
+ title: "Default Title",
29
+ description: "Default Description"
30
+ },
31
+ srcDir: "/app"
32
+ };
33
+ const testAssetService = {
34
+ processDependencies: vi.fn(() => Promise.resolve([]))
35
+ };
36
+ class TestIntegrationRenderer extends IntegrationRenderer {
37
+ name = "test-renderer";
38
+ ForeignChildRuntimeCreationCount = 0;
39
+ ForeignChildRenderCount = 0;
40
+ UseFailFastForeignChildRuntime = false;
41
+ PageModule = null;
42
+ HtmlTemplate = null;
43
+ RenderedBody = "<html><body>Test Page</body></html>";
44
+ RenderBodyFactory = null;
45
+ MockComponentRenderResult = null;
46
+ ImportedFiles = [];
47
+ HtmlContributions = [];
48
+ async render(_options) {
49
+ if (this.RenderBodyFactory) {
50
+ return await this.RenderBodyFactory(getComponentRenderContext());
51
+ }
52
+ return this.RenderedBody;
53
+ }
54
+ async renderComponent(_input) {
55
+ if (this.MockComponentRenderResult) {
56
+ return this.MockComponentRenderResult;
57
+ }
58
+ return super.renderComponent(_input);
59
+ }
60
+ async renderComponentWithForeignChildren(input) {
61
+ this.ForeignChildRenderCount += 1;
62
+ return await super.renderComponentWithForeignChildren(input);
63
+ }
64
+ async renderToResponse(view, props, ctx) {
65
+ const viewFn = view;
66
+ const content = viewFn(props);
67
+ let body;
68
+ if (ctx.partial) {
69
+ body = content;
70
+ } else {
71
+ const Layout = resolveInnermostPageLayout(view.config?.layouts);
72
+ const children = Layout ? Layout({ children: content }) : content;
73
+ body = `<!DOCTYPE html><html><body>${children}</body></html>`;
74
+ }
75
+ const headers = new Headers({ "Content-Type": "text/html; charset=utf-8" });
76
+ if (ctx.headers) {
77
+ for (const [key, value] of Object.entries(ctx.headers)) {
78
+ headers.set(key, value);
79
+ }
80
+ }
81
+ return new Response(body, {
82
+ status: ctx.status ?? 200,
83
+ headers
84
+ });
85
+ }
86
+ async importPageFile(_file) {
87
+ this.ImportedFiles.push(_file);
88
+ if (!this.PageModule) throw new Error("Mock page module not set");
89
+ return this.PageModule;
90
+ }
91
+ async getHtmlTemplate() {
92
+ if (!this.HtmlTemplate) throw new Error("Mock HTML template not set");
93
+ return this.HtmlTemplate;
94
+ }
95
+ async resolveDependencies(_components) {
96
+ return [];
97
+ }
98
+ async testPrepareRenderOptions(options) {
99
+ return this.prepareRenderOptions(options);
100
+ }
101
+ testShouldDelegateForeignChild(input) {
102
+ return this.foreignSubtreeExecutionService.shouldDelegateForeignChild(input);
103
+ }
104
+ testHasForeignChildDescendants(component) {
105
+ return this.hasForeignChildDescendants(component);
106
+ }
107
+ async testGetHtmlTemplate() {
108
+ return this.getHtmlTemplate();
109
+ }
110
+ async testBaseGetHtmlTemplate() {
111
+ return super.getHtmlTemplate();
112
+ }
113
+ testGetRendererBootstrapDependencies(partial = false) {
114
+ return this.getRendererBootstrapDependencies(partial);
115
+ }
116
+ async testFinalizeDocumentShellHtml(options) {
117
+ this.appendProcessedDependencies(this.getRendererBootstrapDependencies(options.partial));
118
+ return finalizeDocumentShellHtml(this.htmlTransformer, {
119
+ html: options.html,
120
+ partial: options.partial,
121
+ htmlContributions: options.htmlContributions ?? this.getHtmlDocumentContributions({ partial: options.partial ?? false })
122
+ });
123
+ }
124
+ async testTransformRouteResponse(response, htmlContributions, pagePackage) {
125
+ const adapter = this.createRouteRenderOrchestratorAdapter();
126
+ return adapter.transformRouteResponse(response, htmlContributions, pagePackage);
127
+ }
128
+ appendTestProcessedDependencies(assets) {
129
+ return this.appendProcessedDependencies(assets);
130
+ }
131
+ setTestPagePackage(pagePackage) {
132
+ this.htmlTransformer.setPagePackage(pagePackage);
133
+ }
134
+ getTestPagePackage() {
135
+ return this.htmlTransformer.getPagePackage();
136
+ }
137
+ getTestProcessedDependencies() {
138
+ return this.htmlTransformer.getProcessedDependencies();
139
+ }
140
+ async testRenderPartialViewResponse(input) {
141
+ return this.renderPartialViewResponse({
142
+ view: input.view,
143
+ props: input.props,
144
+ ctx: input.ctx ?? { partial: true },
145
+ renderInline: input.renderInline,
146
+ transformHtml: input.transformHtml
147
+ });
148
+ }
149
+ async testRenderViewWithDocumentShell(input) {
150
+ return this.renderViewWithDocumentShell({
151
+ view: input.view,
152
+ props: input.props,
153
+ ctx: input.ctx ?? {},
154
+ layout: input.layout
155
+ });
156
+ }
157
+ async testRenderForeignSubtree(input) {
158
+ return toForeignSubtreeRenderPayload(await this.renderComponentWithForeignChildren(input));
159
+ }
160
+ createForeignChildRuntime(options) {
161
+ this.ForeignChildRuntimeCreationCount += 1;
162
+ if (this.UseFailFastForeignChildRuntime) {
163
+ return this.createFailFastForeignChildRuntime();
164
+ }
165
+ return super.createForeignChildRuntime(options);
166
+ }
167
+ getHtmlDocumentContributions(_options) {
168
+ return this.HtmlContributions;
169
+ }
170
+ }
171
+ export {
172
+ TestIntegrationRenderer,
173
+ createMockIntegrationPlugin,
174
+ createUnresolvedMarkerArtifact,
175
+ testAppConfig,
176
+ testAssetService
177
+ };
@@ -1,7 +1,7 @@
1
- import type { EcoPagesAppConfig } from '../../types/internal-types.js';
2
- import type { EcoComponent, ResolvedLazyTrigger } from '../../types/public-types.js';
3
- import type { AssetDefinition } from '../../services/assets/asset-processing-service/index.js';
4
- import type { AssetProcessingService, ProcessedAsset } from '../../services/assets/asset-processing-service/index.js';
1
+ import type { EcoPagesAppConfig } from '../../../types/internal-types.js';
2
+ import type { EcoComponent, ResolvedLazyTrigger } from '../../../types/public-types.js';
3
+ import type { AssetDefinition } from '../../../services/assets/asset-processing-service/index.js';
4
+ import type { AssetProcessingService, ProcessedAsset } from '../../../services/assets/asset-processing-service/index.js';
5
5
  export declare function collectIntegrationNamesFromGraph(components: (EcoComponent | Partial<EcoComponent>)[], currentIntegrationName: string): Set<string>;
6
6
  export declare function collectResolvedLazyTriggersFromGraph(components: (EcoComponent | Partial<EcoComponent>)[], currentIntegrationName: string): ResolvedLazyTrigger[];
7
7
  export declare function hasForeignChildDescendantsInGraph(component: EcoComponent, currentIntegrationName: string): boolean;
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import { AssetFactory } from "../../services/assets/asset-processing-service/index.js";
2
+ import { AssetFactory } from "../../../services/assets/asset-processing-service/index.js";
3
3
  import { walkComponentGraph } from "./component-graph.js";
4
4
  function toGraphRoots(components) {
5
5
  return components.filter(Boolean).map((component) => ({ component }));