@ecopages/core 0.2.0-alpha.26 → 0.2.0-alpha.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 (106) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +63 -7
  3. package/package.json +8 -94
  4. package/src/adapters/bun/create-app.d.ts +1 -0
  5. package/src/adapters/bun/create-app.js +39 -2
  6. package/src/adapters/bun/hmr-manager.d.ts +1 -13
  7. package/src/adapters/bun/hmr-manager.js +1 -22
  8. package/src/adapters/bun/server-adapter.js +23 -4
  9. package/src/adapters/node/node-hmr-manager.d.ts +2 -14
  10. package/src/adapters/node/node-hmr-manager.js +2 -23
  11. package/src/adapters/shared/explicit-static-render-preparation.d.ts +25 -0
  12. package/src/adapters/shared/explicit-static-render-preparation.js +26 -0
  13. package/src/adapters/shared/explicit-static-route-matcher.d.ts +5 -2
  14. package/src/adapters/shared/explicit-static-route-matcher.js +14 -16
  15. package/src/adapters/shared/file-route-middleware-pipeline.d.ts +7 -10
  16. package/src/adapters/shared/file-route-middleware-pipeline.js +2 -11
  17. package/src/adapters/shared/fs-server-response-factory.d.ts +13 -9
  18. package/src/adapters/shared/fs-server-response-factory.js +10 -26
  19. package/src/adapters/shared/fs-server-response-matcher.d.ts +14 -6
  20. package/src/adapters/shared/fs-server-response-matcher.js +67 -28
  21. package/src/adapters/shared/render-context.d.ts +2 -2
  22. package/src/adapters/shared/server-adapter.d.ts +21 -10
  23. package/src/adapters/shared/server-adapter.js +171 -132
  24. package/src/adapters/shared/server-route-handler.d.ts +2 -2
  25. package/src/adapters/shared/server-route-handler.js +1 -1
  26. package/src/adapters/shared/server-static-builder.d.ts +4 -4
  27. package/src/config/README.md +1 -1
  28. package/src/config/config-builder.d.ts +2 -2
  29. package/src/config/config-builder.js +0 -5
  30. package/src/dev/host-runtime.d.ts +10 -0
  31. package/src/dev/host-runtime.js +24 -0
  32. package/src/eco/eco.js +7 -7
  33. package/src/eco/eco.types.d.ts +3 -3
  34. package/src/errors/index.d.ts +1 -0
  35. package/src/errors/index.js +3 -1
  36. package/src/hmr/strategies/js-hmr-strategy.d.ts +0 -5
  37. package/src/integrations/ghtml/ghtml-renderer.d.ts +0 -4
  38. package/src/integrations/ghtml/ghtml-renderer.js +1 -7
  39. package/src/plugins/eco-component-meta-plugin.js +0 -1
  40. package/src/plugins/integration-plugin.d.ts +14 -18
  41. package/src/plugins/integration-plugin.js +14 -21
  42. package/src/plugins/processor.d.ts +2 -0
  43. package/src/plugins/processor.js +6 -1
  44. package/src/route-renderer/GRAPH.md +81 -289
  45. package/src/route-renderer/README.md +67 -105
  46. package/src/route-renderer/orchestration/component-render-context.d.ts +24 -18
  47. package/src/route-renderer/orchestration/component-render-context.js +14 -14
  48. package/src/route-renderer/orchestration/declared-ownership-graph.d.ts +18 -0
  49. package/src/route-renderer/orchestration/declared-ownership-graph.js +34 -0
  50. package/src/route-renderer/orchestration/foreign-subtree-execution.service.d.ts +108 -0
  51. package/src/route-renderer/orchestration/foreign-subtree-execution.service.js +206 -0
  52. package/src/route-renderer/orchestration/integration-renderer.d.ts +96 -136
  53. package/src/route-renderer/orchestration/integration-renderer.js +280 -303
  54. package/src/route-renderer/orchestration/ownership-planning.service.d.ts +24 -0
  55. package/src/route-renderer/orchestration/ownership-planning.service.js +63 -0
  56. package/src/route-renderer/orchestration/ownership-validation.service.d.ts +29 -0
  57. package/src/route-renderer/orchestration/ownership-validation.service.js +53 -0
  58. package/src/route-renderer/orchestration/queued-foreign-subtree-resolution.service.d.ts +90 -0
  59. package/src/route-renderer/orchestration/{queued-boundary-runtime.service.js → queued-foreign-subtree-resolution.service.js} +28 -25
  60. package/src/route-renderer/orchestration/render-output.utils.d.ts +3 -3
  61. package/src/route-renderer/orchestration/render-output.utils.js +6 -6
  62. package/src/route-renderer/orchestration/route-render-orchestrator.d.ts +120 -0
  63. package/src/route-renderer/orchestration/{render-preparation.service.js → route-render-orchestrator.js} +132 -108
  64. package/src/route-renderer/page-loading/component-dependency-collection.js +8 -1
  65. package/src/route-renderer/page-loading/dependency-resolver.js +5 -7
  66. package/src/route-renderer/page-loading/page-dependency-bundling.d.ts +1 -1
  67. package/src/route-renderer/page-loading/page-dependency-bundling.js +41 -19
  68. package/src/route-renderer/route-renderer.d.ts +28 -26
  69. package/src/route-renderer/route-renderer.js +4 -27
  70. package/src/router/README.md +16 -19
  71. package/src/router/server/route-registry.d.ts +78 -0
  72. package/src/router/server/route-registry.js +262 -0
  73. package/src/services/README.md +1 -2
  74. package/src/services/assets/asset-processing-service/assets.types.d.ts +3 -0
  75. package/src/services/assets/asset-processing-service/index.d.ts +1 -0
  76. package/src/services/assets/asset-processing-service/index.js +1 -0
  77. package/src/services/assets/asset-processing-service/page-package.d.ts +3 -0
  78. package/src/services/assets/asset-processing-service/page-package.js +74 -0
  79. package/src/services/assets/asset-processing-service/processors/base/base-script-processor.js +4 -4
  80. package/src/services/assets/asset-processing-service/processors/script/content-script.processor.js +6 -3
  81. package/src/services/assets/asset-processing-service/processors/script/file-script.processor.js +9 -3
  82. package/src/services/assets/asset-processing-service/processors/script/node-module-script.processor.js +4 -2
  83. package/src/services/assets/asset-processing-service/processors/stylesheet/content-stylesheet.processor.js +2 -1
  84. package/src/services/assets/asset-processing-service/processors/stylesheet/file-stylesheet.processor.js +3 -1
  85. package/src/services/module-loading/node-bootstrap-plugin.js +15 -3
  86. package/src/static-site-generator/static-site-generator.d.ts +20 -21
  87. package/src/static-site-generator/static-site-generator.js +107 -140
  88. package/src/types/internal-types.d.ts +13 -12
  89. package/src/types/public-types.d.ts +46 -36
  90. package/src/watchers/project-watcher.test-helpers.js +5 -5
  91. package/src/route-renderer/orchestration/boundary-planning.service.d.ts +0 -25
  92. package/src/route-renderer/orchestration/boundary-planning.service.js +0 -97
  93. package/src/route-renderer/orchestration/page-packaging.service.d.ts +0 -16
  94. package/src/route-renderer/orchestration/page-packaging.service.js +0 -66
  95. package/src/route-renderer/orchestration/queued-boundary-runtime.service.d.ts +0 -89
  96. package/src/route-renderer/orchestration/render-execution.service.d.ts +0 -43
  97. package/src/route-renderer/orchestration/render-execution.service.js +0 -106
  98. package/src/route-renderer/orchestration/render-preparation.service.d.ts +0 -120
  99. package/src/route-renderer/orchestration/route-shell-composer.service.d.ts +0 -50
  100. package/src/route-renderer/orchestration/route-shell-composer.service.js +0 -81
  101. package/src/router/server/fs-router-scanner.d.ts +0 -41
  102. package/src/router/server/fs-router-scanner.js +0 -161
  103. package/src/router/server/fs-router.d.ts +0 -26
  104. package/src/router/server/fs-router.js +0 -100
  105. package/src/services/runtime-state/runtime-specifier-registry.service.d.ts +0 -69
  106. package/src/services/runtime-state/runtime-specifier-registry.service.js +0 -37
@@ -1,12 +1,14 @@
1
1
  import { createRequire } from "node:module";
2
2
  import path from "node:path";
3
3
  import {
4
- AssetFactory
4
+ AssetFactory,
5
+ createPagePackage
5
6
  } from "../../services/assets/asset-processing-service/index.js";
6
7
  import { buildGlobalInjectorBootstrapContent, buildGlobalInjectorMapScript } from "../../eco/global-injector-map.js";
7
8
  import { LocalsAccessError } from "../../errors/locals-access-error.js";
8
- import { BoundaryPlanningService } from "./boundary-planning.service.js";
9
- import { PagePackagingService } from "./page-packaging.service.js";
9
+ import { inspectUnresolvedMarkerArtifactHtml } from "./render-output.utils.js";
10
+ import { OwnershipValidationService } from "./ownership-validation.service.js";
11
+ import { OwnershipPlanningService } from "./ownership-planning.service.js";
10
12
  import { dedupeProcessedAssets } from "./processed-asset-dedupe.js";
11
13
  function createPageLocalsProxy(filePath) {
12
14
  const errorMessage = `[ecopages] Request locals are only available during request-time rendering with cache: 'dynamic'. Page: ${filePath}. If you meant to use locals here, set cache: 'dynamic' and provide locals from route middleware/handlers.`;
@@ -37,83 +39,74 @@ function createPageLocalsProxy(filePath) {
37
39
  }
38
40
  );
39
41
  }
40
- class RenderPreparationService {
42
+ class RouteRenderOrchestrator {
41
43
  appConfig;
42
44
  assetProcessingService;
43
- boundaryPlanningService;
44
- pagePackagingService;
45
- /**
46
- * Creates the render-preparation orchestrator for one app instance.
47
- *
48
- * @remarks
49
- * The service is app-scoped because it depends on finalized config defaults and
50
- * the app-owned asset-processing pipeline while remaining renderer-agnostic.
51
- */
45
+ ownershipPlanningService;
46
+ ownershipValidationService;
52
47
  constructor(appConfig, assetProcessingService, dependencies = {}) {
53
48
  this.appConfig = appConfig;
54
49
  this.assetProcessingService = assetProcessingService;
55
- this.boundaryPlanningService = dependencies.boundaryPlanningService ?? new BoundaryPlanningService(appConfig);
56
- this.pagePackagingService = dependencies.pagePackagingService ?? new PagePackagingService();
50
+ this.ownershipPlanningService = dependencies.ownershipPlanningService ?? new OwnershipPlanningService();
51
+ this.ownershipValidationService = dependencies.ownershipValidationService ?? new OwnershipValidationService(appConfig);
57
52
  }
58
53
  /**
59
- * Builds the final render options object used by the integration-specific
60
- * renderer.
61
- *
62
- * The returned object contains normalized page data, processed dependency
63
- * state, component render artifacts, and the locals contract expected by the
64
- * rest of the pipeline.
54
+ * Builds normalized route render options before the integration render runs.
65
55
  *
66
- * @typeParam C Integration render output element type.
67
- * @param routeOptions Route-level render inputs.
68
- * @param currentIntegrationName Active integration name for this preparation pass.
69
- * @param callbacks Renderer-specific hooks used during preparation.
70
- * @returns Normalized render options.
56
+ * This preparation step validates route-root ownership, resolves page data,
57
+ * collects processed assets, captures optional page-root render metadata, and
58
+ * produces the page package consumed by downstream HTML transformation.
71
59
  */
72
- async prepare(routeOptions, currentIntegrationName, callbacks) {
73
- const pageModule = await callbacks.resolvePageModule(routeOptions.file);
74
- const { Page, integrationSpecificProps } = pageModule;
75
- const HtmlTemplate = await callbacks.getHtmlTemplate();
76
- const { props, metadata } = await callbacks.resolvePageData(pageModule, routeOptions);
77
- const Layout = Page.config?.layout;
78
- const boundaryPlan = this.boundaryPlanningService.buildPlan({
60
+ async prepareRenderOptions(routeOptions, adapter) {
61
+ const resolvedInputs = await adapter.resolveRouteRenderInputs(routeOptions);
62
+ const { Page, HtmlTemplate, Layout, props, metadata, integrationSpecificProps } = resolvedInputs;
63
+ const validationErrors = this.ownershipValidationService.validate({
64
+ currentIntegrationName: adapter.name,
65
+ roots: [
66
+ { component: HtmlTemplate, source: "html-template" },
67
+ ...Layout ? [{ component: Layout, source: "layout" }] : [],
68
+ { component: Page, source: "page" }
69
+ ]
70
+ });
71
+ const ownershipPlan = this.ownershipPlanningService.buildPlan({
79
72
  routeFile: routeOptions.file,
80
- currentIntegrationName,
73
+ currentIntegrationName: adapter.name,
81
74
  HtmlTemplate,
82
75
  Layout,
83
- Page
76
+ Page,
77
+ validationErrors
84
78
  });
85
79
  const componentsToResolve = Layout ? [HtmlTemplate, Layout, Page] : [HtmlTemplate, Page];
86
- const resolvedDependencies = await callbacks.resolveDependencies(componentsToResolve);
87
- const usedIntegrationDependencies = this.collectUsedIntegrationDependencies(
88
- componentsToResolve,
89
- currentIntegrationName
90
- );
91
- const pageDeps = await callbacks.buildRouteRenderAssets(routeOptions.file) || [];
92
- const allDependencies = [...resolvedDependencies, ...usedIntegrationDependencies, ...pageDeps];
93
- let componentRender;
94
- if (callbacks.shouldRenderPageComponent({ Page, Layout, options: routeOptions })) {
95
- const pageRootRender = await this.renderPageRoot({
96
- Page,
97
- props,
98
- routeOptions,
99
- callbacks
100
- });
101
- componentRender = pageRootRender.componentRender;
102
- if (componentRender.assets?.length) {
103
- allDependencies.push(...componentRender.assets);
104
- }
80
+ const { resolvedDependencies, pageBrowserGraph } = await adapter.resolveRouteAssets({
81
+ routeOptions,
82
+ components: componentsToResolve
83
+ });
84
+ const usedIntegrationDependencies = this.collectUsedIntegrationDependencies(componentsToResolve, adapter.name);
85
+ const allDependencies = [
86
+ ...resolvedDependencies,
87
+ ...usedIntegrationDependencies,
88
+ ...pageBrowserGraph?.assets ?? []
89
+ ];
90
+ const componentRender = await adapter.resolveRoutePageComponentRender({
91
+ Page,
92
+ Layout,
93
+ props,
94
+ routeOptions
95
+ });
96
+ if (componentRender?.assets?.length) {
97
+ allDependencies.push(...componentRender.assets);
105
98
  }
106
99
  const triggers = this.collectResolvedTriggers(componentsToResolve);
107
100
  if (triggers.length > 0) {
108
- const globalAssets = await this.buildGlobalInjectorAssets(triggers, currentIntegrationName);
101
+ const globalAssets = await this.buildGlobalInjectorAssets(triggers, adapter.name);
109
102
  allDependencies.push(...globalAssets);
110
103
  }
111
- const eagerSsrLazyAssets = await this.buildEagerSsrLazyAssets(componentsToResolve, currentIntegrationName);
104
+ const eagerSsrLazyAssets = await this.buildEagerSsrLazyAssets(componentsToResolve, adapter.name);
112
105
  if (eagerSsrLazyAssets.length > 0) {
113
106
  allDependencies.push(...eagerSsrLazyAssets);
114
107
  }
115
108
  const dedupedDependencies = dedupeProcessedAssets(allDependencies);
116
- const pagePackage = this.pagePackagingService.createPagePackage(dedupedDependencies);
109
+ const pagePackage = createPagePackage(dedupedDependencies);
117
110
  const pageProps = {
118
111
  ...props,
119
112
  params: routeOptions.params || {},
@@ -141,7 +134,7 @@ class RenderPreparationService {
141
134
  locals,
142
135
  pageLocals,
143
136
  cacheStrategy,
144
- boundaryPlan
137
+ ownershipPlan
145
138
  };
146
139
  return {
147
140
  ...integrationSpecificProps,
@@ -149,15 +142,83 @@ class RenderPreparationService {
149
142
  };
150
143
  }
151
144
  /**
152
- * Collects resolved lazy trigger metadata from the component tree.
153
- *
154
- * Traversal is depth-first and deduplicated by component identity so shared
155
- * component dependencies do not emit duplicate trigger sets.
156
- *
157
- * @param components Root component set.
158
- * @param seen Internal visited set for shared graphs.
159
- * @returns All resolved lazy triggers reachable from the root set.
145
+ * Captures one route render body as HTML while preserving a replayable body value.
146
+ */
147
+ async captureHtmlRender(render) {
148
+ const renderedBody = await render();
149
+ const capturedRender = await this.captureRenderedBody(renderedBody);
150
+ return {
151
+ body: capturedRender.body,
152
+ html: capturedRender.html
153
+ };
154
+ }
155
+ /**
156
+ * Executes the full route-render flow and returns the final body plus cache strategy.
160
157
  */
158
+ async execute(options, adapter) {
159
+ const renderOptions = await this.prepareRenderOptions(options, adapter);
160
+ return this.executePrepared(renderOptions, adapter);
161
+ }
162
+ /**
163
+ * Executes the route-render finalization path for already prepared render options.
164
+ */
165
+ async executePrepared(renderOptions, adapter) {
166
+ const renderExecution = await this.captureHtmlRender(async () => adapter.renderRouteBody(renderOptions));
167
+ const unresolvedArtifactInspection = inspectUnresolvedMarkerArtifactHtml(renderExecution.html);
168
+ const htmlFinalization = adapter.getRouteHtmlFinalization(renderOptions);
169
+ const hasUnresolvedMarkerHtml = unresolvedArtifactInspection.hasUnresolvedMarkerArtifacts;
170
+ if (hasUnresolvedMarkerHtml) {
171
+ throw new Error(
172
+ "[ecopages] Route render returned unresolved eco-marker artifact HTML. Full-route unresolved-marker fallback has been removed; resolve mixed foreign children inside renderComponentWithForeignChildren()."
173
+ );
174
+ }
175
+ const canReuseCapturedBody = !hasUnresolvedMarkerHtml && htmlFinalization.finalizeHtml === void 0;
176
+ if (canReuseCapturedBody) {
177
+ const body2 = await adapter.transformRouteResponse(
178
+ new Response(renderExecution.body, {
179
+ headers: {
180
+ "Content-Type": "text/html"
181
+ }
182
+ })
183
+ );
184
+ return {
185
+ body: body2,
186
+ cacheStrategy: renderOptions.cacheStrategy
187
+ };
188
+ }
189
+ const finalization = htmlFinalization.finalizeHtml ? htmlFinalization.finalizeHtml(unresolvedArtifactInspection.normalizedHtml) : unresolvedArtifactInspection.normalizedHtml;
190
+ const body = await adapter.transformRouteResponse(
191
+ new Response(finalization, {
192
+ headers: {
193
+ "Content-Type": "text/html"
194
+ }
195
+ })
196
+ );
197
+ return {
198
+ body,
199
+ cacheStrategy: renderOptions.cacheStrategy
200
+ };
201
+ }
202
+ async captureRenderedBody(body) {
203
+ const response = new Response(body);
204
+ if (typeof body === "string") {
205
+ return {
206
+ body,
207
+ html: await response.text()
208
+ };
209
+ }
210
+ if (!response.body) {
211
+ return {
212
+ body,
213
+ html: await response.text()
214
+ };
215
+ }
216
+ const [capturedBody, replayBody] = response.body.tee();
217
+ return {
218
+ body: replayBody,
219
+ html: await new Response(capturedBody).text()
220
+ };
221
+ }
161
222
  collectResolvedTriggers(components, seen = /* @__PURE__ */ new Set()) {
162
223
  const triggers = [];
163
224
  for (const comp of components) {
@@ -180,13 +241,6 @@ class RenderPreparationService {
180
241
  }
181
242
  return triggers;
182
243
  }
183
- /**
184
- * Collects global integration dependencies used by nested components belonging
185
- * to integrations other than the current renderer.
186
- *
187
- * @param components Root component set.
188
- * @returns Processed integration dependencies contributed by nested integrations.
189
- */
190
244
  collectUsedIntegrationDependencies(components, currentIntegrationName) {
191
245
  const integrationNames = this.collectIntegrationNames(components);
192
246
  const dependencies = [];
@@ -204,13 +258,6 @@ class RenderPreparationService {
204
258
  }
205
259
  return dependencies;
206
260
  }
207
- /**
208
- * Discovers integration names referenced by the component dependency graph.
209
- *
210
- * @param components Root component set.
211
- * @param seen Internal visited set for shared graphs.
212
- * @returns Set of integration names found in the graph.
213
- */
214
261
  collectIntegrationNames(components, seen = /* @__PURE__ */ new Set()) {
215
262
  const integrationNames = /* @__PURE__ */ new Set();
216
263
  for (const comp of components) {
@@ -236,32 +283,6 @@ class RenderPreparationService {
236
283
  }
237
284
  return integrationNames;
238
285
  }
239
- /**
240
- * Renders the page root through the component-level render contract so any
241
- * integration-specific assets and root attributes are available before the main
242
- * document render.
243
- *
244
- * @param input Page root render inputs.
245
- * @returns Structured component render result.
246
- */
247
- async renderPageRoot(input) {
248
- return {
249
- componentRender: await input.callbacks.renderPageComponent({
250
- component: input.Page,
251
- props: {
252
- ...input.props,
253
- params: input.routeOptions.params || {},
254
- query: input.routeOptions.query || {}
255
- }
256
- })
257
- };
258
- }
259
- /**
260
- * Builds the runtime assets needed to bootstrap global lazy trigger execution.
261
- *
262
- * @param triggers Fully resolved lazy trigger definitions.
263
- * @returns Processed assets that should be merged into the final dependency set.
264
- */
265
286
  async buildGlobalInjectorAssets(triggers, currentIntegrationName) {
266
287
  const globalInjectorImportPath = createRequire(import.meta.url).resolve("@ecopages/scripts-injector/global");
267
288
  const mapScript = AssetFactory.createInlineContentScript({
@@ -280,7 +301,10 @@ class RenderPreparationService {
280
301
  packageRole: "keep-separate",
281
302
  bundle: true
282
303
  });
283
- return this.assetProcessingService.processDependencies([mapScript, bootstrapInlineScript], currentIntegrationName);
304
+ return this.assetProcessingService.processDependencies(
305
+ [mapScript, bootstrapInlineScript],
306
+ currentIntegrationName
307
+ );
284
308
  }
285
309
  async buildEagerSsrLazyAssets(components, currentIntegrationName) {
286
310
  const dependencies = this.collectEagerSsrLazyDependencies(components);
@@ -360,5 +384,5 @@ class RenderPreparationService {
360
384
  }
361
385
  }
362
386
  export {
363
- RenderPreparationService
387
+ RouteRenderOrchestrator
364
388
  };
@@ -32,7 +32,14 @@ function resolveDependencyPath(componentDir, pathUrl) {
32
32
  return path.join(componentDir, pathUrl);
33
33
  }
34
34
  function collectComponentDependencies(options) {
35
- const { components, integrationName, resolveLazyScripts, createEcopagesJsxLazyEntryName, isEcopagesJsxIntegration, errors } = options;
35
+ const {
36
+ components,
37
+ integrationName,
38
+ resolveLazyScripts,
39
+ createEcopagesJsxLazyEntryName,
40
+ isEcopagesJsxIntegration,
41
+ errors
42
+ } = options;
36
43
  const dependencies = [];
37
44
  const lazyScriptsByConfig = /* @__PURE__ */ new Map();
38
45
  const lazyDependencyKeys = /* @__PURE__ */ new Set();
@@ -1,11 +1,9 @@
1
1
  import path from "node:path";
2
2
  import { rapidhash } from "../../utils/hash.js";
3
3
  import { AssetFactory } from "../../services/assets/asset-processing-service/index.js";
4
- import {
5
- buildResolvedLazyTriggers
6
- } from "./lazy-trigger-planning.js";
4
+ import { buildResolvedLazyTriggers } from "./lazy-trigger-planning.js";
7
5
  import { collectComponentDependencies } from "./component-dependency-collection.js";
8
- import { createUnifiedPageDependencies } from "./page-dependency-bundling.js";
6
+ import { packagePageDependencies } from "./page-dependency-bundling.js";
9
7
  const DEPENDENCY_ERRORS = {
10
8
  INVALID_STYLESHEET_ENTRY: "Invalid stylesheet dependency entry: expected src or content",
11
9
  INVALID_SCRIPT_ENTRY: "Invalid script dependency entry: expected src or content",
@@ -77,12 +75,12 @@ class DependencyResolverService {
77
75
  lazyScriptMissingSrc: DEPENDENCY_ERRORS.LAZY_SCRIPT_MISSING_SRC
78
76
  }
79
77
  });
80
- const unifiedDependencies = createUnifiedPageDependencies(dependencies, integrationName);
81
- const hasLazyDependencies = unifiedDependencies.some(
78
+ const packagedDependencies = packagePageDependencies(dependencies, integrationName);
79
+ const hasLazyDependencies = packagedDependencies.some(
82
80
  (dep) => dep.kind === "script" && dep.excludeFromHtml === true
83
81
  );
84
82
  const processedDependencies = await this.assetProcessingService.processDependencies(
85
- unifiedDependencies,
83
+ packagedDependencies,
86
84
  integrationName
87
85
  );
88
86
  const lazyKeyToOutputUrl = /* @__PURE__ */ new Map();
@@ -10,4 +10,4 @@ export declare function shouldBundlePageDependencies(integrationName: string): b
10
10
  * Only assets with the default attribute shape and without explicit packaging roles are
11
11
  * collapsed so integration-specific or lazy behavior keeps its existing ownership model.
12
12
  */
13
- export declare function createUnifiedPageDependencies(dependencies: AssetDefinition[], integrationName: string): AssetDefinition[];
13
+ export declare function packagePageDependencies(dependencies: AssetDefinition[], integrationName: string): AssetDefinition[];
@@ -16,7 +16,7 @@ function normalizeCssReferenceToken(token) {
16
16
  return token.trim().replace(/^['"]|['"]$/g, "");
17
17
  }
18
18
  function isSafeBundledStylesheetContent(content) {
19
- for (const match of content.matchAll(/@import\s+(?:url\()?['"]?([^'"\)\s]+)['"]?\)?/g)) {
19
+ for (const match of content.matchAll(/@import\s+(?:url\()?['"]?([^'")\s]+)['"]?\)?/g)) {
20
20
  const reference = normalizeCssReferenceToken(match[1] ?? "");
21
21
  if (reference && !isSafeBundledStylesheetReference(reference)) {
22
22
  return false;
@@ -39,11 +39,12 @@ function isBundleableFileScriptAsset(dependency) {
39
39
  function shouldBundlePageDependencies(integrationName) {
40
40
  return integrationName === "react" || integrationName === "ecopages-jsx";
41
41
  }
42
- function createUnifiedPageDependencies(dependencies, integrationName) {
43
- if (!shouldBundlePageDependencies(integrationName)) {
44
- return dependencies;
45
- }
42
+ function createPageDependencyPackagingPlan(dependencies, integrationName) {
43
+ const shouldBundleDependencies = process.env.NODE_ENV === "production";
46
44
  const bundleableStyles = dependencies.filter((dependency) => {
45
+ if (!shouldBundleDependencies) {
46
+ return false;
47
+ }
47
48
  if (dependency.kind !== "stylesheet" || dependency.inline || dependency.position === "body") {
48
49
  return false;
49
50
  }
@@ -59,57 +60,78 @@ function createUnifiedPageDependencies(dependencies, integrationName) {
59
60
  return isSafeBundledStylesheetContent(readFileSync(dependency.filepath, "utf8"));
60
61
  }).filter(isBundleableFileStylesheetAsset);
61
62
  const bundleableScripts = dependencies.filter((dependency) => {
63
+ if (!shouldBundleDependencies) {
64
+ return false;
65
+ }
62
66
  return dependency.kind === "script" && dependency.source === "file" && !dependency.inline && !dependency.excludeFromHtml && dependency.position !== "body" && !dependency.packageRole && dependency.bundle !== false && hasOnlyExpectedAttributes(dependency.attributes, { type: "module", defer: "" }) && existsSync(dependency.filepath);
63
67
  }).filter(isBundleableFileScriptAsset);
64
68
  const bundledStylesheet = bundleableStyles.length > 1 ? AssetFactory.createContentStylesheet({
65
69
  content: bundleableStyles.map((dependency) => readFileSync(dependency.filepath, "utf8")).join("\n"),
66
70
  position: "head",
67
71
  attributes: { rel: "stylesheet" },
68
- packageRole: "page-style"
72
+ packageRole: "page-style",
73
+ bundledSourceFilepaths: bundleableStyles.map((dependency) => dependency.filepath)
69
74
  }) : void 0;
70
- const bundleableStyleFilepaths = new Set(bundleableStyles.map((dependency) => dependency.filepath));
71
75
  const pageScriptImports = [...new Set(bundleableScripts.map((dependency) => dependency.filepath))];
72
- const bundleableScriptFilepaths = new Set(pageScriptImports);
73
76
  const shouldBundlePageScript = pageScriptImports.length > 0 && bundleableScripts.length > 1;
74
77
  const bundledScript = shouldBundlePageScript ? AssetFactory.createContentScript({
75
78
  name: `${integrationName}-page-${rapidhash(pageScriptImports.join("|")).toString(16)}`,
76
79
  content: pageScriptImports.map((filepath) => `import ${JSON.stringify(filepath)};`).join("\n"),
77
80
  position: "head",
78
81
  attributes: { type: "module", defer: "" },
79
- packageRole: "page-script"
82
+ packageRole: "page-script",
83
+ bundledSourceFilepaths: pageScriptImports
80
84
  }) : void 0;
81
85
  if (!bundledStylesheet && !bundledScript) {
82
- return dependencies;
86
+ return void 0;
83
87
  }
88
+ return {
89
+ bundledStylesheet,
90
+ bundledScript,
91
+ bundleableStyleFilepaths: new Set(bundleableStyles.map((dependency) => dependency.filepath)),
92
+ bundleableScriptFilepaths: new Set(pageScriptImports)
93
+ };
94
+ }
95
+ function applyPageDependencyPackagingPlan(dependencies, plan) {
84
96
  const unifiedDependencies = [];
85
97
  let insertedStylesheet = false;
86
98
  let insertedScript = false;
87
99
  for (const dependency of dependencies) {
88
- if (bundledStylesheet && dependency.kind === "stylesheet" && dependency.source === "file" && bundleableStyleFilepaths.has(dependency.filepath)) {
100
+ if (plan.bundledStylesheet && dependency.kind === "stylesheet" && dependency.source === "file" && plan.bundleableStyleFilepaths.has(dependency.filepath)) {
89
101
  if (!insertedStylesheet) {
90
- unifiedDependencies.push(bundledStylesheet);
102
+ unifiedDependencies.push(plan.bundledStylesheet);
91
103
  insertedStylesheet = true;
92
104
  }
93
105
  continue;
94
106
  }
95
- if (bundledScript && dependency.kind === "script" && dependency.source === "file" && bundleableScriptFilepaths.has(dependency.filepath)) {
107
+ if (plan.bundledScript && dependency.kind === "script" && dependency.source === "file" && plan.bundleableScriptFilepaths.has(dependency.filepath)) {
96
108
  if (!insertedScript) {
97
- unifiedDependencies.push(bundledScript);
109
+ unifiedDependencies.push(plan.bundledScript);
98
110
  insertedScript = true;
99
111
  }
100
112
  continue;
101
113
  }
102
114
  unifiedDependencies.push(dependency);
103
115
  }
104
- if (bundledScript && !insertedScript) {
105
- unifiedDependencies.push(bundledScript);
116
+ if (plan.bundledScript && !insertedScript) {
117
+ unifiedDependencies.push(plan.bundledScript);
106
118
  }
107
- if (bundledStylesheet && !insertedStylesheet) {
108
- unifiedDependencies.push(bundledStylesheet);
119
+ if (plan.bundledStylesheet && !insertedStylesheet) {
120
+ unifiedDependencies.push(plan.bundledStylesheet);
109
121
  }
110
122
  return unifiedDependencies;
111
123
  }
124
+ function packagePageDependencies(dependencies, integrationName) {
125
+ if (!shouldBundlePageDependencies(integrationName)) {
126
+ return dependencies;
127
+ }
128
+ const plan = createPageDependencyPackagingPlan(dependencies, integrationName);
129
+ if (!plan) {
130
+ return dependencies;
131
+ }
132
+ return applyPageDependencyPackagingPlan(dependencies, plan);
133
+ }
112
134
  export {
113
- createUnifiedPageDependencies,
135
+ packagePageDependencies,
114
136
  shouldBundlePageDependencies
115
137
  };
@@ -1,30 +1,33 @@
1
1
  import type { EcoPagesAppConfig } from '../types/internal-types.js';
2
- import type { IntegrationPlugin } from '../plugins/integration-plugin.js';
3
- import type { EcoPageFile, RouteRenderResult, RouteRendererOptions } from '../types/public-types.js';
4
- import type { IntegrationRenderer, RouteModuleLoadOptions } from './orchestration/integration-renderer.js';
2
+ import type { AnyIntegrationPlugin } from '../plugins/integration-plugin.js';
3
+ import type { IntegrationRenderer } from './orchestration/integration-renderer.js';
5
4
  /**
6
- * Thin wrapper around one initialized integration renderer.
5
+ * Narrow route-render contract exposed to higher-level routing code.
7
6
  *
8
7
  * @remarks
9
- * This type exists so higher-level routing code can ask for a route renderer
10
- * without depending on the full integration plugin lifecycle. It delegates all
11
- * real work to the integration-specific renderer selected by the factory.
8
+ * Higher-level routing code only needs request execution and page-module
9
+ * loading. Returning this narrowed shape avoids a dedicated wrapper class while
10
+ * still keeping callers off the full integration renderer surface.
12
11
  */
13
- export declare class RouteRenderer {
14
- private renderer;
15
- /**
16
- * Creates a route renderer bound to one integration renderer instance.
17
- */
18
- constructor(renderer: IntegrationRenderer);
19
- /**
20
- * Executes the render pipeline for one matched route.
21
- */
22
- createRoute(options: RouteRendererOptions): Promise<RouteRenderResult>;
23
- /**
24
- * Loads the route module through the owning integration renderer.
25
- */
26
- loadPageModule(filePath: string, options?: RouteModuleLoadOptions): Promise<EcoPageFile>;
12
+ export type PageRouteRenderer = Pick<IntegrationRenderer<unknown>, 'execute' | 'loadPageModule'>;
13
+ /**
14
+ * Narrow explicit-view render contract exposed to static route handling.
15
+ *
16
+ * @remarks
17
+ * Explicit static routes only need `renderToResponse()`, so the factory can
18
+ * hide the broader integration renderer surface there as well.
19
+ */
20
+ export type ExplicitViewRenderer = Pick<IntegrationRenderer<unknown>, 'renderToResponse'>;
21
+ export interface PageRendererResolver {
22
+ getPageRenderer(filePath: string): PageRouteRenderer;
27
23
  }
24
+ export interface ExplicitViewRendererResolver {
25
+ getExplicitViewRenderer(integrationName: string): ExplicitViewRenderer | null;
26
+ }
27
+ /**
28
+ * Combined renderer-factory contract used by static generation.
29
+ */
30
+ export type StaticGenerationRendererResolver = PageRendererResolver & ExplicitViewRendererResolver;
28
31
  /**
29
32
  * Selects and caches integration renderers for route files and explicit views.
30
33
  *
@@ -49,16 +52,15 @@ export declare class RouteRendererFactory {
49
52
  /**
50
53
  * Returns a route renderer for the supplied route file.
51
54
  */
52
- createRenderer(filePath: string): RouteRenderer;
55
+ getPageRenderer(filePath: string): PageRouteRenderer;
53
56
  /**
54
- * Get an integration renderer by its name.
55
- * Used for explicit routing where views specify their integration via __eco.integration.
57
+ * Returns a renderer for an explicit view integration.
56
58
  */
57
- getRendererByIntegration(integrationName: string): IntegrationRenderer | null;
59
+ getExplicitViewRenderer(integrationName: string): ExplicitViewRenderer | null;
58
60
  /**
59
61
  * Resolves the integration plugin that owns a given route file.
60
62
  */
61
- getIntegrationPlugin(filePath: string): IntegrationPlugin;
63
+ getIntegrationPlugin(filePath: string): AnyIntegrationPlugin;
62
64
  /**
63
65
  * Returns the cached renderer engine for the file's owning integration,
64
66
  * creating it on first use.
@@ -1,26 +1,5 @@
1
1
  import { invariant } from "../utils/invariant.js";
2
2
  import { PathUtils } from "../utils/path-utils.module.js";
3
- class RouteRenderer {
4
- renderer;
5
- /**
6
- * Creates a route renderer bound to one integration renderer instance.
7
- */
8
- constructor(renderer) {
9
- this.renderer = renderer;
10
- }
11
- /**
12
- * Executes the render pipeline for one matched route.
13
- */
14
- async createRoute(options) {
15
- return this.renderer.execute(options);
16
- }
17
- /**
18
- * Loads the route module through the owning integration renderer.
19
- */
20
- async loadPageModule(filePath, options) {
21
- return this.renderer.loadPageModule(filePath, options);
22
- }
23
- }
24
3
  class RouteRendererFactory {
25
4
  appConfig;
26
5
  runtimeOrigin;
@@ -41,16 +20,15 @@ class RouteRendererFactory {
41
20
  /**
42
21
  * Returns a route renderer for the supplied route file.
43
22
  */
44
- createRenderer(filePath) {
23
+ getPageRenderer(filePath) {
45
24
  const integrationRenderer = this.getRouteRendererEngine(filePath);
46
25
  invariant(!!integrationRenderer, `No integration renderer found for file: ${filePath}`);
47
- return new RouteRenderer(integrationRenderer);
26
+ return integrationRenderer;
48
27
  }
49
28
  /**
50
- * Get an integration renderer by its name.
51
- * Used for explicit routing where views specify their integration via __eco.integration.
29
+ * Returns a renderer for an explicit view integration.
52
30
  */
53
- getRendererByIntegration(integrationName) {
31
+ getExplicitViewRenderer(integrationName) {
54
32
  const integrationPlugin = this.appConfig.integrations.find((plugin) => plugin.name === integrationName);
55
33
  if (!integrationPlugin) {
56
34
  return null;
@@ -98,6 +76,5 @@ class RouteRendererFactory {
98
76
  }
99
77
  }
100
78
  export {
101
- RouteRenderer,
102
79
  RouteRendererFactory
103
80
  };