@ecopages/core 0.2.0-alpha.39 → 0.2.0-alpha.40

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 (56) hide show
  1. package/package.json +2 -2
  2. package/src/adapters/bun/create-app.d.ts +8 -1
  3. package/src/adapters/bun/create-app.js +52 -65
  4. package/src/adapters/bun/hmr-manager.d.ts +19 -103
  5. package/src/adapters/bun/hmr-manager.js +26 -280
  6. package/src/adapters/bun/runtime-host.d.ts +52 -0
  7. package/src/adapters/bun/runtime-host.js +56 -0
  8. package/src/adapters/bun/server-adapter.d.ts +89 -28
  9. package/src/adapters/bun/server-adapter.js +113 -61
  10. package/src/adapters/bun/static-preview-host.d.ts +28 -0
  11. package/src/adapters/bun/static-preview-host.js +45 -0
  12. package/src/adapters/node/create-app.d.ts +9 -3
  13. package/src/adapters/node/create-app.js +24 -81
  14. package/src/adapters/node/http-request-bridge.d.ts +57 -0
  15. package/src/adapters/node/http-request-bridge.js +118 -0
  16. package/src/adapters/node/node-hmr-manager.d.ts +22 -91
  17. package/src/adapters/node/node-hmr-manager.js +26 -272
  18. package/src/adapters/node/runtime-host.d.ts +57 -0
  19. package/src/adapters/node/runtime-host.js +92 -0
  20. package/src/adapters/node/server-adapter-dependencies.d.ts +19 -0
  21. package/src/adapters/node/server-adapter-dependencies.js +18 -0
  22. package/src/adapters/node/server-adapter.d.ts +10 -37
  23. package/src/adapters/node/server-adapter.js +55 -125
  24. package/src/adapters/node/static-preview-host.d.ts +55 -0
  25. package/src/adapters/node/static-preview-host.js +68 -0
  26. package/src/adapters/shared/runtime-app-bootstrap.d.ts +26 -0
  27. package/src/adapters/shared/runtime-app-bootstrap.js +46 -0
  28. package/src/adapters/shared/runtime-host.d.ts +12 -0
  29. package/src/adapters/shared/runtime-host.js +0 -0
  30. package/src/adapters/shared/shared-hmr-manager.d.ts +59 -0
  31. package/src/adapters/shared/shared-hmr-manager.js +239 -0
  32. package/src/adapters/shared/static-preview-host.d.ts +10 -0
  33. package/src/adapters/shared/static-preview-host.js +0 -0
  34. package/src/build/build-adapter.js +12 -1
  35. package/src/build/esbuild-build-adapter.d.ts +1 -0
  36. package/src/build/esbuild-build-adapter.js +13 -0
  37. package/src/hmr/strategies/js-hmr-strategy.js +0 -4
  38. package/src/plugins/integration-plugin.d.ts +6 -1
  39. package/src/route-renderer/orchestration/integration-renderer.d.ts +32 -14
  40. package/src/route-renderer/orchestration/integration-renderer.js +80 -14
  41. package/src/route-renderer/orchestration/processed-asset-dedupe.d.ts +1 -0
  42. package/src/route-renderer/orchestration/processed-asset-dedupe.js +15 -11
  43. package/src/route-renderer/orchestration/route-render-orchestrator.d.ts +22 -8
  44. package/src/route-renderer/orchestration/route-render-orchestrator.js +59 -10
  45. package/src/services/assets/asset-processing-service/page-package.d.ts +4 -1
  46. package/src/services/assets/asset-processing-service/page-package.js +11 -5
  47. package/src/services/assets/asset-processing-service/processors/script/node-module-script.processor.js +3 -1
  48. package/src/services/html/html-rewriter-provider.service.d.ts +3 -0
  49. package/src/services/html/html-transformer.service.d.ts +10 -1
  50. package/src/services/html/html-transformer.service.js +80 -9
  51. package/src/services/module-loading/page-module-import.service.js +2 -2
  52. package/src/types/public-types.d.ts +24 -7
  53. package/src/adapters/bun/server-lifecycle.d.ts +0 -63
  54. package/src/adapters/bun/server-lifecycle.js +0 -92
  55. package/src/adapters/shared/runtime-bootstrap.d.ts +0 -38
  56. package/src/adapters/shared/runtime-bootstrap.js +0 -43
@@ -15,6 +15,7 @@ import {
15
15
  ForeignSubtreeExecutionService
16
16
  } from "./foreign-subtree-execution.service.js";
17
17
  import {} from "./queued-foreign-subtree-resolution.service.js";
18
+ import { buildProcessedAssetDedupeKey } from "./processed-asset-dedupe.js";
18
19
  function isMarkupNodeLike(value) {
19
20
  return typeof value === "object" && value !== null && "nodeType" in value && typeof value.nodeType === "number" && "outerHTML" in value && typeof value.outerHTML === "string";
20
21
  }
@@ -179,6 +180,46 @@ class IntegrationRenderer {
179
180
  this.htmlTransformer.setPagePackage(createPagePackage(resolvedDependencies));
180
181
  return resolvedDependencies;
181
182
  }
183
+ async resolvePageBrowserGraphForFile(filePath) {
184
+ return await this.routeRenderOrchestrator.resolveDeclaredPageBrowserGraph({
185
+ routeFile: filePath,
186
+ integrationName: this.name,
187
+ collectContribution: async () => {
188
+ const pageModule = await this.importPageFile(filePath);
189
+ return await this.collectPageBrowserGraphContribution({ file: filePath, pageModule });
190
+ }
191
+ });
192
+ }
193
+ mergePageBrowserGraphIntoPagePackage(pageBrowserGraph) {
194
+ if (!pageBrowserGraph) {
195
+ return void 0;
196
+ }
197
+ const currentPagePackage = this.htmlTransformer.getPagePackage();
198
+ const mergedPageBrowserGraph = currentPagePackage?.pageBrowserGraph ? {
199
+ entryAssets: this.htmlTransformer.dedupeProcessedAssets([
200
+ ...currentPagePackage.pageBrowserGraph.entryAssets,
201
+ ...pageBrowserGraph.entryAssets
202
+ ]),
203
+ chunkAssets: this.htmlTransformer.dedupeProcessedAssets([
204
+ ...currentPagePackage.pageBrowserGraph.chunkAssets,
205
+ ...pageBrowserGraph.chunkAssets
206
+ ])
207
+ } : pageBrowserGraph;
208
+ const pageBrowserGraphAssetKeys = new Set(
209
+ [...mergedPageBrowserGraph.entryAssets, ...mergedPageBrowserGraph.chunkAssets].map(
210
+ (asset) => buildProcessedAssetDedupeKey(asset)
211
+ )
212
+ );
213
+ const baseAssets = currentPagePackage ? currentPagePackage.assets.filter(
214
+ (asset) => !pageBrowserGraphAssetKeys.has(buildProcessedAssetDedupeKey(asset))
215
+ ) : this.htmlTransformer.getProcessedDependencies();
216
+ this.htmlTransformer.setPagePackage(
217
+ createPagePackage(this.htmlTransformer.dedupeProcessedAssets(baseAssets), {
218
+ pageBrowserGraph: mergedPageBrowserGraph
219
+ })
220
+ );
221
+ return mergedPageBrowserGraph;
222
+ }
182
223
  /**
183
224
  * Merges component-scoped assets into the active HTML transformer state.
184
225
  *
@@ -579,11 +620,15 @@ class IntegrationRenderer {
579
620
  return {
580
621
  name: this.name,
581
622
  resolveRouteRenderInputs: (routeOptions) => this.resolveRouteRenderInputs(routeOptions),
582
- resolveRouteAssets: (input) => this.resolveRouteAssets(input),
623
+ resolveRouteDependencies: (input) => this.resolveRouteDependencies(input),
624
+ collectPageBrowserGraphContribution: async (routeFile) => {
625
+ const pageModule = await this.importPageFile(routeFile);
626
+ return await this.collectPageBrowserGraphContribution({ file: routeFile, pageModule });
627
+ },
583
628
  resolveRoutePageComponentRender: (input) => this.resolveRoutePageComponentRender(input),
584
629
  renderRouteBody: (renderOptions) => this.renderRouteBody(renderOptions),
585
630
  getRouteHtmlFinalization: (renderOptions) => this.getRouteHtmlFinalization(renderOptions),
586
- transformRouteResponse: (response) => this.transformRouteResponse(response)
631
+ transformRouteResponse: (response, htmlContributions) => this.transformRouteResponse(response, htmlContributions)
587
632
  };
588
633
  }
589
634
  async resolveRouteRenderInputs(routeOptions) {
@@ -607,10 +652,9 @@ class IntegrationRenderer {
607
652
  integrationSpecificProps
608
653
  };
609
654
  }
610
- async resolveRouteAssets(input) {
655
+ async resolveRouteDependencies(input) {
611
656
  return {
612
- resolvedDependencies: await this.resolveDependencies(input.components),
613
- pageBrowserGraph: await this.buildPageBrowserGraph(input.routeOptions.file)
657
+ resolvedDependencies: await this.resolveDependencies(input.components)
614
658
  };
615
659
  }
616
660
  async resolveRoutePageComponentRender(input) {
@@ -635,11 +679,13 @@ class IntegrationRenderer {
635
679
  getRouteHtmlFinalization(renderOptions) {
636
680
  const componentRootAttributes = renderOptions.componentRender?.canAttachAttributes && renderOptions.componentRender.rootAttributes && Object.keys(renderOptions.componentRender.rootAttributes).length > 0 ? renderOptions.componentRender.rootAttributes : void 0;
637
681
  const documentAttributes = this.getDocumentAttributes(renderOptions);
682
+ const htmlContributions = this.getHtmlDocumentContributions({ renderOptions, partial: false });
638
683
  const hasStructuralFinalization = componentRootAttributes && Object.keys(componentRootAttributes).length > 0 || documentAttributes && Object.keys(documentAttributes).length > 0;
639
- if (!hasStructuralFinalization) {
684
+ if (!hasStructuralFinalization && (!htmlContributions || htmlContributions.length === 0)) {
640
685
  return {};
641
686
  }
642
687
  return {
688
+ htmlContributions,
643
689
  finalizeHtml: (html) => {
644
690
  let renderedHtml = html;
645
691
  if (componentRootAttributes) {
@@ -655,8 +701,8 @@ class IntegrationRenderer {
655
701
  }
656
702
  };
657
703
  }
658
- async transformRouteResponse(response) {
659
- const transformedResponse = await this.htmlTransformer.transform(response);
704
+ async transformRouteResponse(response, htmlContributions) {
705
+ const transformedResponse = await this.htmlTransformer.transform(response, htmlContributions);
660
706
  return transformedResponse.body ?? await transformedResponse.text();
661
707
  }
662
708
  /**
@@ -726,10 +772,14 @@ class IntegrationRenderer {
726
772
  if (!shouldTransform) {
727
773
  return html;
728
774
  }
775
+ const htmlContributions = options.htmlContributions ?? this.getHtmlDocumentContributions({
776
+ partial: options.partial
777
+ });
729
778
  const transformedResponse = await this.htmlTransformer.transform(
730
779
  new Response(html, {
731
780
  headers: { "Content-Type": "text/html" }
732
- })
781
+ }),
782
+ htmlContributions
733
783
  );
734
784
  return await transformedResponse.text();
735
785
  }
@@ -742,6 +792,18 @@ class IntegrationRenderer {
742
792
  getDocumentAttributes(_renderOptions) {
743
793
  return void 0;
744
794
  }
795
+ /**
796
+ * Returns declarative HTML fragments that core should inject into the final document.
797
+ *
798
+ * @remarks
799
+ * Integrations may contribute document markup here, but core retains ownership
800
+ * of the final HTML rewrite pipeline and placement semantics. This is the
801
+ * supported document-markup extension point for integrations instead of custom
802
+ * response finalization logic.
803
+ */
804
+ getHtmlDocumentContributions(_options) {
805
+ return void 0;
806
+ }
745
807
  /**
746
808
  * Returns a renderer instance for a given integration name.
747
809
  *
@@ -883,13 +945,17 @@ class IntegrationRenderer {
883
945
  return rootTag?.[1];
884
946
  }
885
947
  /**
886
- * Builds the Page Browser Graph owned by this integration for one Page.
887
- * This method can be optionally overridden by the specific integration renderer.
948
+ * Collects declarative Page Browser Graph contributions for one Page.
949
+ *
950
+ * @remarks
951
+ * Integrations may describe page-scoped browser requirements here, while core
952
+ * retains ownership of dependency processing and final graph assembly. This is
953
+ * the supported page-browser extension point for integrations.
888
954
  *
889
- * @param file - The file path to build assets for.
890
- * @returns The structured Page Browser Graph or undefined.
955
+ * @param context - The route file path and already imported page module.
956
+ * @returns Declarative dependencies or pre-resolved assets for the Page.
891
957
  */
892
- async buildPageBrowserGraph(_file) {
958
+ async collectPageBrowserGraphContribution(_context) {
893
959
  return void 0;
894
960
  }
895
961
  /**
@@ -1,2 +1,3 @@
1
1
  import type { ProcessedAsset } from '../../services/assets/asset-processing-service/index.js';
2
+ export declare function buildProcessedAssetDedupeKey(asset: ProcessedAsset): string;
2
3
  export declare function dedupeProcessedAssets(assets: ProcessedAsset[]): ProcessedAsset[];
@@ -1,17 +1,20 @@
1
+ function buildProcessedAssetDedupeKey(asset) {
2
+ return [
3
+ asset.kind,
4
+ asset.position ?? "",
5
+ asset.srcUrl ?? "",
6
+ asset.filepath ?? "",
7
+ asset.content ?? "",
8
+ asset.inline ? "inline" : "external",
9
+ asset.excludeFromHtml ? "excluded" : "included",
10
+ asset.packageRole ?? "",
11
+ JSON.stringify(asset.attributes ?? {})
12
+ ].join("|");
13
+ }
1
14
  function dedupeProcessedAssets(assets) {
2
15
  const unique = /* @__PURE__ */ new Map();
3
16
  for (const asset of assets) {
4
- const key = [
5
- asset.kind,
6
- asset.position ?? "",
7
- asset.srcUrl ?? "",
8
- asset.filepath ?? "",
9
- asset.content ?? "",
10
- asset.inline ? "inline" : "external",
11
- asset.excludeFromHtml ? "excluded" : "included",
12
- asset.packageRole ?? "",
13
- JSON.stringify(asset.attributes ?? {})
14
- ].join("|");
17
+ const key = buildProcessedAssetDedupeKey(asset);
15
18
  if (!unique.has(key)) {
16
19
  unique.set(key, asset);
17
20
  }
@@ -19,5 +22,6 @@ function dedupeProcessedAssets(assets) {
19
22
  return [...unique.values()];
20
23
  }
21
24
  export {
25
+ buildProcessedAssetDedupeKey,
22
26
  dedupeProcessedAssets
23
27
  };
@@ -1,6 +1,7 @@
1
1
  import type { EcoPagesAppConfig } from '../../types/internal-types.js';
2
- import type { ComponentRenderResult, EcoComponent, EcoPageComponent, EcoPageFile, HtmlTemplateProps, IntegrationRendererRenderOptions, PageBrowserGraphResult, PageMetadataProps, RouteRendererBody, RouteRendererOptions, RouteRenderResult } from '../../types/public-types.js';
2
+ import type { ComponentRenderResult, EcoComponent, EcoPageComponent, EcoPageFile, HtmlTemplateProps, IntegrationRendererRenderOptions, PageBrowserGraphContribution, PageBrowserGraphResult, PageMetadataProps, RouteRendererBody, RouteRendererOptions, RouteRenderResult } from '../../types/public-types.js';
3
3
  import { type AssetProcessingService, type ProcessedAsset } from '../../services/assets/asset-processing-service/index.js';
4
+ import type { HtmlDocumentContribution } from '../../services/html/html-transformer.service.js';
4
5
  import { OwnershipValidationService } from './ownership-validation.service.js';
5
6
  import { OwnershipPlanningService } from './ownership-planning.service.js';
6
7
  export type RouteRenderOrchestratorResolvedInputs = {
@@ -11,9 +12,8 @@ export type RouteRenderOrchestratorResolvedInputs = {
11
12
  metadata: PageMetadataProps;
12
13
  integrationSpecificProps: Record<string, unknown>;
13
14
  };
14
- export type RouteRenderOrchestratorResolvedAssets = {
15
+ export type RouteRenderOrchestratorResolvedDependencies = {
15
16
  resolvedDependencies: ProcessedAsset[];
16
- pageBrowserGraph?: PageBrowserGraphResult;
17
17
  };
18
18
  /**
19
19
  * Structural HTML work applied after the route body has been fully resolved.
@@ -23,6 +23,7 @@ export type RouteRenderOrchestratorResolvedAssets = {
23
23
  */
24
24
  export type RouteHtmlFinalization = {
25
25
  finalizeHtml?(html: string): string;
26
+ htmlContributions?: HtmlDocumentContribution[];
26
27
  };
27
28
  export interface RouteRenderOrchestratorAdapter<C> {
28
29
  /**
@@ -34,12 +35,15 @@ export interface RouteRenderOrchestratorAdapter<C> {
34
35
  */
35
36
  resolveRouteRenderInputs(routeOptions: RouteRendererOptions): Promise<RouteRenderOrchestratorResolvedInputs>;
36
37
  /**
37
- * Resolves route-owned assets needed before Integration rendering starts.
38
+ * Resolves route-owned dependencies needed before Integration rendering starts.
38
39
  */
39
- resolveRouteAssets(input: {
40
- routeOptions: RouteRendererOptions;
40
+ resolveRouteDependencies(input: {
41
41
  components: (EcoComponent | Partial<EcoComponent>)[];
42
- }): Promise<RouteRenderOrchestratorResolvedAssets>;
42
+ }): Promise<RouteRenderOrchestratorResolvedDependencies>;
43
+ /**
44
+ * Collects declarative Page Browser Graph requirements for one route.
45
+ */
46
+ collectPageBrowserGraphContribution(routeFile: string): Promise<PageBrowserGraphContribution | undefined>;
43
47
  /**
44
48
  * Resolves the optional page-root render through the foreign-child-aware component contract.
45
49
  */
@@ -60,7 +64,7 @@ export interface RouteRenderOrchestratorAdapter<C> {
60
64
  /**
61
65
  * Runs SSR-policy response transformation and returns the body value exposed to callers.
62
66
  */
63
- transformRouteResponse(response: Response): Promise<RouteRendererBody>;
67
+ transformRouteResponse(response: Response, htmlContributions?: HtmlDocumentContribution[]): Promise<RouteRendererBody>;
64
68
  }
65
69
  /**
66
70
  * Captured route-render output in both replayable body and string HTML forms.
@@ -89,6 +93,7 @@ export declare class RouteRenderOrchestrator {
89
93
  private readonly assetProcessingService;
90
94
  private readonly ownershipPlanningService;
91
95
  private readonly ownershipValidationService;
96
+ private readonly pageBrowserGraphCache;
92
97
  constructor(appConfig: EcoPagesAppConfig, assetProcessingService: AssetProcessingService, dependencies?: RouteRenderOrchestratorDependencies);
93
98
  /**
94
99
  * Builds normalized route render options before the integration render runs.
@@ -98,6 +103,15 @@ export declare class RouteRenderOrchestrator {
98
103
  * produces the page package consumed by downstream HTML transformation.
99
104
  */
100
105
  prepareRenderOptions<C = unknown>(routeOptions: RouteRendererOptions, adapter: RouteRenderOrchestratorAdapter<C>): Promise<IntegrationRendererRenderOptions<C>>;
106
+ resolveDeclaredPageBrowserGraph(input: {
107
+ routeFile: string;
108
+ integrationName: string;
109
+ collectContribution: () => Promise<PageBrowserGraphContribution | undefined>;
110
+ }): Promise<PageBrowserGraphResult | undefined>;
111
+ private resolvePageBrowserGraph;
112
+ private isHmrEnabled;
113
+ private buildPageBrowserGraph;
114
+ private partitionPageBrowserGraphAssets;
101
115
  /**
102
116
  * Captures one route render body as HTML while preserving a replayable body value.
103
117
  */
@@ -44,6 +44,7 @@ class RouteRenderOrchestrator {
44
44
  assetProcessingService;
45
45
  ownershipPlanningService;
46
46
  ownershipValidationService;
47
+ pageBrowserGraphCache = /* @__PURE__ */ new Map();
47
48
  constructor(appConfig, assetProcessingService, dependencies = {}) {
48
49
  this.appConfig = appConfig;
49
50
  this.assetProcessingService = assetProcessingService;
@@ -77,16 +78,16 @@ class RouteRenderOrchestrator {
77
78
  validationErrors
78
79
  });
79
80
  const componentsToResolve = Layout ? [HtmlTemplate, Layout, Page] : [HtmlTemplate, Page];
80
- const { resolvedDependencies, pageBrowserGraph } = await adapter.resolveRouteAssets({
81
- routeOptions,
81
+ const { resolvedDependencies } = await adapter.resolveRouteDependencies({
82
82
  components: componentsToResolve
83
83
  });
84
+ const pageBrowserGraph = await this.resolvePageBrowserGraph({
85
+ routeFile: routeOptions.file,
86
+ integrationName: adapter.name,
87
+ collectContribution: async () => await adapter.collectPageBrowserGraphContribution(routeOptions.file)
88
+ });
84
89
  const usedIntegrationDependencies = this.collectUsedIntegrationDependencies(componentsToResolve, adapter.name);
85
- const allDependencies = [
86
- ...resolvedDependencies,
87
- ...usedIntegrationDependencies,
88
- ...pageBrowserGraph?.assets ?? []
89
- ];
90
+ const allDependencies = [...resolvedDependencies, ...usedIntegrationDependencies];
90
91
  const componentRender = await adapter.resolveRoutePageComponentRender({
91
92
  Page,
92
93
  Layout,
@@ -106,7 +107,7 @@ class RouteRenderOrchestrator {
106
107
  allDependencies.push(...eagerSsrLazyAssets);
107
108
  }
108
109
  const dedupedDependencies = dedupeProcessedAssets(allDependencies);
109
- const pagePackage = createPagePackage(dedupedDependencies);
110
+ const pagePackage = createPagePackage(dedupedDependencies, { pageBrowserGraph });
110
111
  const pageProps = {
111
112
  ...props,
112
113
  params: routeOptions.params || {},
@@ -141,6 +142,52 @@ class RouteRenderOrchestrator {
141
142
  ...preparedOptions
142
143
  };
143
144
  }
145
+ async resolveDeclaredPageBrowserGraph(input) {
146
+ return await this.resolvePageBrowserGraph(input);
147
+ }
148
+ async resolvePageBrowserGraph(input) {
149
+ if (this.isHmrEnabled()) {
150
+ return await this.buildPageBrowserGraph(input);
151
+ }
152
+ const cacheKey = `${input.integrationName}:${input.routeFile}`;
153
+ const cachedGraph = this.pageBrowserGraphCache.get(cacheKey);
154
+ if (cachedGraph) {
155
+ return await cachedGraph;
156
+ }
157
+ const pendingGraph = this.buildPageBrowserGraph(input).catch((error) => {
158
+ this.pageBrowserGraphCache.delete(cacheKey);
159
+ throw error;
160
+ });
161
+ this.pageBrowserGraphCache.set(cacheKey, pendingGraph);
162
+ return await pendingGraph;
163
+ }
164
+ isHmrEnabled() {
165
+ return typeof this.assetProcessingService.getHmrManager === "function" && this.assetProcessingService.getHmrManager()?.isEnabled() === true;
166
+ }
167
+ async buildPageBrowserGraph(input) {
168
+ const contribution = await input.collectContribution();
169
+ if (!contribution) {
170
+ return void 0;
171
+ }
172
+ const processedDependencies = contribution.dependencies?.length ? await this.assetProcessingService.processDependencies(
173
+ contribution.dependencies,
174
+ `${input.integrationName}:${input.routeFile}`
175
+ ) : [];
176
+ const resolvedAssets = [...processedDependencies, ...contribution.assets ?? []];
177
+ return this.partitionPageBrowserGraphAssets(resolvedAssets);
178
+ }
179
+ partitionPageBrowserGraphAssets(assets) {
180
+ const entryAssets = [];
181
+ const chunkAssets = [];
182
+ for (const asset of assets) {
183
+ if (asset.packageRole === "dynamic-chunk") {
184
+ chunkAssets.push(asset);
185
+ continue;
186
+ }
187
+ entryAssets.push(asset);
188
+ }
189
+ return { entryAssets, chunkAssets };
190
+ }
144
191
  /**
145
192
  * Captures one route render body as HTML while preserving a replayable body value.
146
193
  */
@@ -179,7 +226,8 @@ class RouteRenderOrchestrator {
179
226
  headers: {
180
227
  "Content-Type": "text/html"
181
228
  }
182
- })
229
+ }),
230
+ htmlFinalization.htmlContributions
183
231
  );
184
232
  return {
185
233
  body: body2,
@@ -192,7 +240,8 @@ class RouteRenderOrchestrator {
192
240
  headers: {
193
241
  "Content-Type": "text/html"
194
242
  }
195
- })
243
+ }),
244
+ htmlFinalization.htmlContributions
196
245
  );
197
246
  return {
198
247
  body,
@@ -1,3 +1,6 @@
1
1
  import type { PagePackageResult } from '../../../types/public-types.js';
2
2
  import type { ProcessedAsset } from './assets.types.js';
3
- export declare function createPagePackage(assets: ProcessedAsset[]): PagePackageResult;
3
+ import type { PageBrowserGraphResult } from '../../../types/public-types.js';
4
+ export declare function createPagePackage(assets: ProcessedAsset[], options?: {
5
+ pageBrowserGraph?: PageBrowserGraphResult;
6
+ }): PagePackageResult;
@@ -9,14 +9,19 @@ function getSuppressedSourceFilepaths(assets) {
9
9
  }
10
10
  return suppressed;
11
11
  }
12
- function createPagePackage(assets) {
12
+ function createPagePackage(assets, options = {}) {
13
+ const allAssets = [
14
+ ...assets,
15
+ ...options.pageBrowserGraph?.entryAssets ?? [],
16
+ ...options.pageBrowserGraph?.chunkAssets ?? []
17
+ ];
13
18
  const inlineAssets = [];
14
19
  const separateAssets = [];
15
20
  const dynamicChunks = [];
16
21
  let pageScript;
17
22
  let pageStylesheet;
18
- const suppressedSourceFilepaths = getSuppressedSourceFilepaths(assets);
19
- for (const asset of assets) {
23
+ const suppressedSourceFilepaths = getSuppressedSourceFilepaths(allAssets);
24
+ for (const asset of allAssets) {
20
25
  if (asset.inline) {
21
26
  inlineAssets.push(asset);
22
27
  continue;
@@ -48,8 +53,9 @@ function createPagePackage(assets) {
48
53
  separateAssets.push(asset);
49
54
  }
50
55
  return {
51
- assets,
52
- htmlAssets: assets.filter((asset) => shouldIncludeInHtml(asset, suppressedSourceFilepaths)),
56
+ assets: allAssets,
57
+ pageBrowserGraph: options.pageBrowserGraph,
58
+ htmlAssets: allAssets.filter((asset) => shouldIncludeInHtml(asset, suppressedSourceFilepaths)),
53
59
  pageScript,
54
60
  pageStylesheet,
55
61
  inlineAssets,
@@ -78,7 +78,9 @@ class NodeModuleScriptProcessor extends BaseScriptProcessor {
78
78
  */
79
79
  resolveModulePathFallback(importPath, rootDir, maxDepth = 5) {
80
80
  try {
81
- return fileURLToPath(import.meta.resolve(importPath, pathToFileURL(path.join(rootDir, "package.json")).href));
81
+ return fileURLToPath(
82
+ import.meta.resolve(importPath, pathToFileURL(path.join(rootDir, "package.json")).href)
83
+ );
82
84
  } catch {
83
85
  }
84
86
  let currentDir = rootDir;
@@ -1,4 +1,7 @@
1
1
  export type HtmlRewriterElement = {
2
+ prepend(content: string, options?: {
3
+ html?: boolean;
4
+ }): void;
2
5
  append(content: string, options?: {
3
6
  html?: boolean;
4
7
  }): void;
@@ -1,6 +1,11 @@
1
1
  import type { ProcessedAsset } from '../assets/asset-processing-service/assets.types.js';
2
2
  import type { PagePackageResult } from '../../types/public-types.js';
3
3
  import { type HtmlRewriterMode, type HtmlRewriterProvider } from './html-rewriter-provider.service.js';
4
+ export type HtmlDocumentContributionPlacement = 'head-prepend' | 'head-append' | 'body-prepend' | 'body-append';
5
+ export type HtmlDocumentContribution = {
6
+ placement: HtmlDocumentContributionPlacement;
7
+ html: string;
8
+ };
4
9
  export interface HtmlTransformerServiceOptions {
5
10
  htmlRewriterMode?: HtmlRewriterMode;
6
11
  htmlRewriterProvider?: HtmlRewriterProvider;
@@ -24,11 +29,14 @@ export declare class HtmlTransformerService {
24
29
  private generateStylesheetTag;
25
30
  private appendDependencies;
26
31
  private buildDependencyTags;
32
+ private applyContributions;
27
33
  /**
28
34
  * Injects generated markup immediately before the closing HTML tag when it is
29
35
  * present, or appends/prepends a fallback insertion otherwise.
30
36
  */
31
37
  private injectBeforeClosingTag;
38
+ private injectAfterOpeningTag;
39
+ private groupContributionsByPlacement;
32
40
  /**
33
41
  * Replaces the current processed dependency set used during HTML finalization.
34
42
  */
@@ -75,11 +83,12 @@ export declare class HtmlTransformerService {
75
83
  * string-based fallback remains in place for runtimes that cannot provide one
76
84
  * of those rewriter implementations.
77
85
  */
78
- transform(res: Response): Promise<Response>;
86
+ transform(res: Response, contributions?: HtmlDocumentContribution[]): Promise<Response>;
79
87
  /**
80
88
  * Splits processed assets into head and body injection groups.
81
89
  */
82
90
  private groupDependenciesByPosition;
91
+ private resolvePagePackageHtmlDependencies;
83
92
  /**
84
93
  * Builds a serialized HTML attribute string from an attribute object.
85
94
  */
@@ -1,7 +1,10 @@
1
1
  import {
2
2
  DefaultHtmlRewriterProvider
3
3
  } from "./html-rewriter-provider.service.js";
4
- import { dedupeProcessedAssets } from "../../route-renderer/orchestration/processed-asset-dedupe.js";
4
+ import {
5
+ buildProcessedAssetDedupeKey,
6
+ dedupeProcessedAssets
7
+ } from "../../route-renderer/orchestration/processed-asset-dedupe.js";
5
8
  class HtmlTransformerService {
6
9
  processedDependencies = [];
7
10
  pagePackage;
@@ -44,6 +47,11 @@ class HtmlTransformerService {
44
47
  (dep) => dep.kind === "script" ? this.generateScriptTag(dep) : this.generateStylesheetTag(dep)
45
48
  ).join("");
46
49
  }
50
+ applyContributions(element, contributions, placement) {
51
+ for (const contribution of contributions) {
52
+ element[placement](contribution.html, { html: true });
53
+ }
54
+ }
47
55
  /**
48
56
  * Injects generated markup immediately before the closing HTML tag when it is
49
57
  * present, or appends/prepends a fallback insertion otherwise.
@@ -63,6 +71,26 @@ class HtmlTransformerService {
63
71
  }
64
72
  return `${html}${content}`;
65
73
  }
74
+ injectAfterOpeningTag(html, tag, content) {
75
+ if (!content) {
76
+ return html;
77
+ }
78
+ const openingTag = new RegExp(`<${tag}\\b[^>]*>`, "i");
79
+ const match = html.match(openingTag);
80
+ if (!match || match.index === void 0) {
81
+ return tag === "head" ? `${content}${html}` : html.replace(/<body\b[^>]*>/i, (value) => `${value}${content}`);
82
+ }
83
+ const insertAt = match.index + match[0].length;
84
+ return `${html.slice(0, insertAt)}${content}${html.slice(insertAt)}`;
85
+ }
86
+ groupContributionsByPlacement(contributions) {
87
+ return {
88
+ headPrepend: contributions.filter((item) => item.placement === "head-prepend"),
89
+ headAppend: contributions.filter((item) => item.placement === "head-append"),
90
+ bodyPrepend: contributions.filter((item) => item.placement === "body-prepend"),
91
+ bodyAppend: contributions.filter((item) => item.placement === "body-append")
92
+ };
93
+ }
66
94
  /**
67
95
  * Replaces the current processed dependency set used during HTML finalization.
68
96
  */
@@ -75,7 +103,7 @@ class HtmlTransformerService {
75
103
  */
76
104
  setPagePackage(pagePackage) {
77
105
  this.pagePackage = pagePackage;
78
- this.processedDependencies = pagePackage.htmlAssets;
106
+ this.processedDependencies = this.resolvePagePackageHtmlDependencies(pagePackage);
79
107
  }
80
108
  /**
81
109
  * Returns the processed dependencies queued for the next transform pass.
@@ -160,24 +188,47 @@ class HtmlTransformerService {
160
188
  * string-based fallback remains in place for runtimes that cannot provide one
161
189
  * of those rewriter implementations.
162
190
  */
163
- async transform(res) {
191
+ async transform(res, contributions = []) {
164
192
  const { head, body } = this.groupDependenciesByPosition();
193
+ const { headPrepend, headAppend, bodyPrepend, bodyAppend } = this.groupContributionsByPlacement(contributions);
165
194
  const htmlRewriter = await this.htmlRewriterProvider.createHtmlRewriter();
166
195
  if (htmlRewriter) {
167
196
  htmlRewriter.on("head", {
168
- element: (element) => this.appendDependencies(element, head)
197
+ element: (element) => {
198
+ this.applyContributions(element, headPrepend, "prepend");
199
+ this.appendDependencies(element, head);
200
+ this.applyContributions(element, headAppend, "append");
201
+ }
169
202
  }).on("body", {
170
- element: (element) => this.appendDependencies(element, body)
203
+ element: (element) => {
204
+ this.applyContributions(element, bodyPrepend, "prepend");
205
+ this.appendDependencies(element, body);
206
+ this.applyContributions(element, bodyAppend, "append");
207
+ }
171
208
  });
172
209
  return htmlRewriter.transform(res);
173
210
  }
174
211
  const html = await res.text();
175
212
  const headers = new Headers(res.headers);
176
- const withHeadDependencies = this.injectBeforeClosingTag(html, "head", this.buildDependencyTags(head));
177
- const transformedHtml = this.injectBeforeClosingTag(
213
+ const withHeadPrependedContent = this.injectAfterOpeningTag(
214
+ html,
215
+ "head",
216
+ headPrepend.map((item) => item.html).join("")
217
+ );
218
+ const withHeadDependencies = this.injectBeforeClosingTag(
219
+ withHeadPrependedContent,
220
+ "head",
221
+ `${this.buildDependencyTags(head)}${headAppend.map((item) => item.html).join("")}`
222
+ );
223
+ const withBodyPrependedContent = this.injectAfterOpeningTag(
178
224
  withHeadDependencies,
179
225
  "body",
180
- this.buildDependencyTags(body)
226
+ bodyPrepend.map((item) => item.html).join("")
227
+ );
228
+ const transformedHtml = this.injectBeforeClosingTag(
229
+ withBodyPrependedContent,
230
+ "body",
231
+ `${this.buildDependencyTags(body)}${bodyAppend.map((item) => item.html).join("")}`
181
232
  );
182
233
  return new Response(transformedHtml, {
183
234
  headers,
@@ -189,7 +240,7 @@ class HtmlTransformerService {
189
240
  * Splits processed assets into head and body injection groups.
190
241
  */
191
242
  groupDependenciesByPosition() {
192
- const dependencies = this.pagePackage?.htmlAssets ?? this.processedDependencies;
243
+ const dependencies = this.pagePackage ? this.resolvePagePackageHtmlDependencies(this.pagePackage) : this.processedDependencies;
193
244
  return dependencies.reduce(
194
245
  (acc, dep) => {
195
246
  if (dep.kind === "script") {
@@ -204,6 +255,26 @@ class HtmlTransformerService {
204
255
  { head: [], body: [] }
205
256
  );
206
257
  }
258
+ resolvePagePackageHtmlDependencies(pagePackage) {
259
+ if (!pagePackage.pageBrowserGraph) {
260
+ return pagePackage.htmlAssets;
261
+ }
262
+ const chunkKeys = new Set(
263
+ pagePackage.pageBrowserGraph.chunkAssets.map((asset) => buildProcessedAssetDedupeKey(asset))
264
+ );
265
+ const graphKeys = new Set(
266
+ [...pagePackage.pageBrowserGraph.entryAssets, ...pagePackage.pageBrowserGraph.chunkAssets].map(
267
+ (asset) => buildProcessedAssetDedupeKey(asset)
268
+ )
269
+ );
270
+ const nonGraphHtmlAssets = pagePackage.htmlAssets.filter(
271
+ (asset) => !graphKeys.has(buildProcessedAssetDedupeKey(asset))
272
+ );
273
+ const entryHtmlAssets = pagePackage.pageBrowserGraph.entryAssets.filter(
274
+ (asset) => !chunkKeys.has(buildProcessedAssetDedupeKey(asset))
275
+ );
276
+ return dedupeProcessedAssets([...nonGraphHtmlAssets, ...entryHtmlAssets]);
277
+ }
207
278
  /**
208
279
  * Builds a serialized HTML attribute string from an attribute object.
209
280
  */