@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
@@ -15,7 +15,6 @@ import {
15
15
  InMemoryEntrypointDependencyGraph,
16
16
  setAppEntrypointDependencyGraph
17
17
  } from "../../services/runtime-state/entrypoint-dependency-graph.service.js";
18
- import { getAppRuntimeSpecifierRegistry } from "../../services/runtime-state/runtime-specifier-registry.service.js";
19
18
  import { resolveInternalExecutionDir, resolveInternalWorkDir } from "../../utils/resolve-work-dir.js";
20
19
  class NodeHmrManager {
21
20
  static entrypointRegistrationTimeoutMs = 4e3;
@@ -31,7 +30,6 @@ class NodeHmrManager {
31
30
  entrypointRegistrar;
32
31
  browserBundleService;
33
32
  entrypointDependencyGraph;
34
- runtimeSpecifierRegistry;
35
33
  serverModuleTranspiler;
36
34
  constructor({ appConfig, bridge }) {
37
35
  this.appConfig = appConfig;
@@ -49,7 +47,6 @@ class NodeHmrManager {
49
47
  const existingEntrypointDependencyGraph = getAppEntrypointDependencyGraph(appConfig);
50
48
  this.entrypointDependencyGraph = existingEntrypointDependencyGraph instanceof InMemoryEntrypointDependencyGraph ? existingEntrypointDependencyGraph : new InMemoryEntrypointDependencyGraph();
51
49
  setAppEntrypointDependencyGraph(this.appConfig, this.entrypointDependencyGraph);
52
- this.runtimeSpecifierRegistry = getAppRuntimeSpecifierRegistry(this.appConfig);
53
50
  this.serverModuleTranspiler = getAppServerModuleTranspiler(this.appConfig);
54
51
  this.cleanDistDir();
55
52
  this.initializeStrategies();
@@ -87,7 +84,6 @@ class NodeHmrManager {
87
84
  initializeStrategies() {
88
85
  const jsContext = {
89
86
  getWatchedFiles: () => this.watchedFiles,
90
- getSpecifierMap: () => this.runtimeSpecifierRegistry.getAll(),
91
87
  getDistDir: () => this.distDir,
92
88
  getPlugins: () => this.plugins,
93
89
  getSrcDir: () => this.appConfig.absolutePaths.srcDir,
@@ -112,18 +108,6 @@ class NodeHmrManager {
112
108
  isEnabled() {
113
109
  return this.enabled;
114
110
  }
115
- /**
116
- * Registers runtime bare-specifier mappings exposed by integrations.
117
- *
118
- * @remarks
119
- * These mappings are consumed by framework-owned HMR strategies such as the
120
- * React integration strategy when they rewrite browser bundles. The registry
121
- * stays generic so the same mappings can support broader import-map-style
122
- * runtime features later without moving integration semantics into core.
123
- */
124
- registerSpecifierMap(map) {
125
- this.runtimeSpecifierRegistry.register(map);
126
- }
127
111
  async buildRuntime() {
128
112
  const runtimeSource = path.resolve(import.meta.dirname, "../../hmr/client/hmr-runtime.js");
129
113
  try {
@@ -184,9 +168,6 @@ class NodeHmrManager {
184
168
  getWatchedFiles() {
185
169
  return this.watchedFiles;
186
170
  }
187
- getSpecifierMap() {
188
- return this.runtimeSpecifierRegistry.getAll();
189
- }
190
171
  getDistDir() {
191
172
  return this.distDir;
192
173
  }
@@ -196,7 +177,6 @@ class NodeHmrManager {
196
177
  getDefaultContext() {
197
178
  return {
198
179
  getWatchedFiles: () => this.watchedFiles,
199
- getSpecifierMap: () => this.runtimeSpecifierRegistry.getAll(),
200
180
  getDistDir: () => this.distDir,
201
181
  getPlugins: () => this.plugins,
202
182
  getSrcDir: () => this.appConfig.absolutePaths.srcDir,
@@ -291,8 +271,8 @@ class NodeHmrManager {
291
271
  * @remarks
292
272
  * The manager intentionally does not remove emitted `_hmr` files from disk
293
273
  * because multiple app processes may share the same dist directory during test
294
- * runs. It does clear in-memory indexes so old entrypoints, dependencies, and
295
- * specifier maps cannot leak across a reused manager instance.
274
+ * runs. It does clear in-memory indexes so old entrypoints and dependencies
275
+ * cannot leak across a reused manager instance.
296
276
  */
297
277
  stop() {
298
278
  this.entrypointRegistrations.clear();
@@ -301,7 +281,6 @@ class NodeHmrManager {
301
281
  }
302
282
  this.watchers.clear();
303
283
  this.watchedFiles.clear();
304
- this.runtimeSpecifierRegistry.clear();
305
284
  this.entrypointDependencyGraph.reset();
306
285
  this.plugins = [];
307
286
  }
@@ -0,0 +1,25 @@
1
+ import type { EcoPagesAppConfig } from '../../types/internal-types.js';
2
+ import type { EcoFunctionComponent, EcoPageComponent } from '../../types/public-types.js';
3
+ import type { ExplicitViewRenderer, ExplicitViewRendererResolver } from '../../route-renderer/route-renderer.js';
4
+ type ExplicitStaticRenderPreparationResult = {
5
+ renderer: ExplicitViewRenderer;
6
+ props: Record<string, unknown>;
7
+ view: EcoFunctionComponent<Record<string, unknown>, any>;
8
+ };
9
+ /**
10
+ * Resolves the renderer and static props needed to render one explicit static
11
+ * view at runtime or during static generation.
12
+ */
13
+ export declare function prepareExplicitStaticRender(input: {
14
+ routePath: string;
15
+ view: EcoPageComponent<any>;
16
+ params: Record<string, string | string[]>;
17
+ appConfig: EcoPagesAppConfig;
18
+ runtimeOrigin: string;
19
+ routeRendererFactory: ExplicitViewRendererResolver;
20
+ errors: {
21
+ missingIntegration(routePath: string): string;
22
+ noRendererForIntegration(integrationName: string): string;
23
+ };
24
+ }): Promise<ExplicitStaticRenderPreparationResult>;
25
+ export {};
@@ -0,0 +1,26 @@
1
+ function getViewIntegrationName(view) {
2
+ return view.config?.integration ?? view.config?.__eco?.integration;
3
+ }
4
+ async function prepareExplicitStaticRender(input) {
5
+ const integrationName = getViewIntegrationName(input.view);
6
+ if (!integrationName) {
7
+ throw new Error(input.errors.missingIntegration(input.routePath));
8
+ }
9
+ const renderer = input.routeRendererFactory.getExplicitViewRenderer(integrationName);
10
+ if (!renderer) {
11
+ throw new Error(input.errors.noRendererForIntegration(integrationName));
12
+ }
13
+ const props = input.view.staticProps ? (await input.view.staticProps({
14
+ pathname: { params: input.params },
15
+ appConfig: input.appConfig,
16
+ runtimeOrigin: input.runtimeOrigin
17
+ })).props : {};
18
+ return {
19
+ renderer,
20
+ props,
21
+ view: input.view
22
+ };
23
+ }
24
+ export {
25
+ prepareExplicitStaticRender
26
+ };
@@ -1,19 +1,22 @@
1
1
  import type { EcoPagesAppConfig } from '../../types/internal-types.js';
2
2
  import type { StaticRoute } from '../../types/public-types.js';
3
- import type { RouteRendererFactory } from '../../route-renderer/route-renderer.js';
3
+ import type { ExplicitViewRendererResolver } from '../../route-renderer/route-renderer.js';
4
4
  export declare const EXPLICIT_STATIC_ROUTE_MATCHER_ERRORS: {
5
5
  readonly missingIntegration: (routePath: string) => string;
6
6
  readonly noRendererForIntegration: (integrationName: string) => string;
7
7
  };
8
8
  export interface ExplicitStaticRouteMatcherOptions {
9
9
  appConfig: EcoPagesAppConfig;
10
- routeRendererFactory: RouteRendererFactory;
10
+ routeRendererFactory: ExplicitViewRendererResolver;
11
11
  staticRoutes: StaticRoute[];
12
12
  }
13
13
  export interface ExplicitRouteMatch {
14
14
  route: StaticRoute;
15
15
  params: Record<string, string>;
16
16
  }
17
+ /**
18
+ * Matches and renders explicit static routes declared through `app.static()`.
19
+ */
17
20
  export declare class ExplicitStaticRouteMatcher {
18
21
  private readonly appConfig;
19
22
  private readonly routeRendererFactory;
@@ -1,11 +1,9 @@
1
1
  import { appLogger } from "../../global/app-logger.js";
2
+ import { prepareExplicitStaticRender } from "./explicit-static-render-preparation.js";
2
3
  const EXPLICIT_STATIC_ROUTE_MATCHER_ERRORS = {
3
4
  missingIntegration: (routePath) => `View at ${routePath} is missing __eco.integration. Ensure it's defined with eco.page() and exported as default.`,
4
5
  noRendererForIntegration: (integrationName) => `No renderer found for integration: ${integrationName}`
5
6
  };
6
- function getViewIntegrationName(view) {
7
- return view.config?.integration ?? view.config?.__eco?.integration;
8
- }
9
7
  class ExplicitStaticRouteMatcher {
10
8
  appConfig;
11
9
  routeRendererFactory;
@@ -74,20 +72,20 @@ class ExplicitStaticRouteMatcher {
74
72
  try {
75
73
  const mod = await route.loader();
76
74
  const view = mod.default;
77
- const integrationName = getViewIntegrationName(view);
78
- if (!integrationName) {
79
- throw new Error(EXPLICIT_STATIC_ROUTE_MATCHER_ERRORS.missingIntegration(route.path));
80
- }
81
- const renderer = this.routeRendererFactory.getRendererByIntegration(integrationName);
82
- if (!renderer) {
83
- throw new Error(EXPLICIT_STATIC_ROUTE_MATCHER_ERRORS.noRendererForIntegration(integrationName));
84
- }
85
- const props = view.staticProps ? (await view.staticProps({
86
- pathname: { params },
75
+ const {
76
+ renderer,
77
+ props,
78
+ view: renderableView
79
+ } = await prepareExplicitStaticRender({
80
+ routePath: route.path,
81
+ view,
82
+ params,
87
83
  appConfig: this.appConfig,
88
- runtimeOrigin: this.appConfig.baseUrl
89
- })).props : {};
90
- return renderer.renderToResponse(view, props, {});
84
+ runtimeOrigin: this.appConfig.baseUrl,
85
+ routeRendererFactory: this.routeRendererFactory,
86
+ errors: EXPLICIT_STATIC_ROUTE_MATCHER_ERRORS
87
+ });
88
+ return renderer.renderToResponse(renderableView, props, {});
91
89
  } catch (error) {
92
90
  appLogger.error(
93
91
  `Error rendering explicit static route ${route.path}:`,
@@ -1,8 +1,6 @@
1
- import type { Middleware, ApiHandlerContext, RequestLocals } from '../../types/public-types.js';
1
+ import type { FileRouteMiddleware, FileRouteMiddlewareContext, RequestLocals } from '../../types/public-types.js';
2
2
  import type { PageCacheService } from '../../services/cache/page-cache-service.js';
3
3
  export declare const FILE_ROUTE_MIDDLEWARE_PIPELINE_ERRORS: {
4
- readonly CTX_RENDER_UNAVAILABLE: "[ecopages] ctx.render is not available in file-route middleware";
5
- readonly CTX_RENDER_PARTIAL_UNAVAILABLE: "[ecopages] ctx.renderPartial is not available in file-route middleware";
6
4
  readonly middlewareRequiresDynamic: (filePath: string) => string;
7
5
  };
8
6
  /**
@@ -26,7 +24,7 @@ export declare class FileRouteMiddlewarePipeline {
26
24
  * @throws LocalsAccessError When middleware is configured for a non-dynamic page.
27
25
  */
28
26
  assertValidConfiguration(input: {
29
- middleware: Middleware[];
27
+ middleware: FileRouteMiddleware[];
30
28
  pageCacheStrategy: 'static' | 'dynamic' | {
31
29
  revalidate: number;
32
30
  tags?: string[];
@@ -36,9 +34,8 @@ export declare class FileRouteMiddlewarePipeline {
36
34
  /**
37
35
  * Creates the request-scoped middleware context used by page middleware.
38
36
  *
39
- * The context intentionally disables `render()` and `renderPartial()` inside
40
- * file-route middleware because rendering is owned by the page route pipeline,
41
- * not by middleware stages.
37
+ * The context intentionally omits `render()` and `renderPartial()` because
38
+ * rendering is owned by the page route pipeline, not by middleware stages.
42
39
  *
43
40
  * @param input Request details and the mutable locals store.
44
41
  * @returns Middleware execution context.
@@ -47,7 +44,7 @@ export declare class FileRouteMiddlewarePipeline {
47
44
  request: Request;
48
45
  params: Record<string, string>;
49
46
  locals: RequestLocals;
50
- }): ApiHandlerContext;
47
+ }): FileRouteMiddlewareContext;
51
48
  /**
52
49
  * Runs the middleware chain and eventually delegates to the render callback.
53
50
  *
@@ -58,8 +55,8 @@ export declare class FileRouteMiddlewarePipeline {
58
55
  * @returns Response from middleware or final render stage.
59
56
  */
60
57
  run(input: {
61
- middleware: Middleware[];
62
- context: ApiHandlerContext;
58
+ middleware: FileRouteMiddleware[];
59
+ context: FileRouteMiddlewareContext;
63
60
  renderResponse: () => Promise<Response>;
64
61
  }): Promise<Response>;
65
62
  }
@@ -2,8 +2,6 @@ import { createRequire } from "../../utils/locals-utils.js";
2
2
  import { ApiResponseBuilder } from "./api-response.js";
3
3
  import { LocalsAccessError } from "../../errors/locals-access-error.js";
4
4
  const FILE_ROUTE_MIDDLEWARE_PIPELINE_ERRORS = {
5
- CTX_RENDER_UNAVAILABLE: "[ecopages] ctx.render is not available in file-route middleware",
6
- CTX_RENDER_PARTIAL_UNAVAILABLE: "[ecopages] ctx.renderPartial is not available in file-route middleware",
7
5
  middlewareRequiresDynamic: (filePath) => `[ecopages] Page middleware requires cache: 'dynamic'. Page: ${filePath}`
8
6
  };
9
7
  class FileRouteMiddlewarePipeline {
@@ -30,9 +28,8 @@ class FileRouteMiddlewarePipeline {
30
28
  /**
31
29
  * Creates the request-scoped middleware context used by page middleware.
32
30
  *
33
- * The context intentionally disables `render()` and `renderPartial()` inside
34
- * file-route middleware because rendering is owned by the page route pipeline,
35
- * not by middleware stages.
31
+ * The context intentionally omits `render()` and `renderPartial()` because
32
+ * rendering is owned by the page route pipeline, not by middleware stages.
36
33
  *
37
34
  * @param input Request details and the mutable locals store.
38
35
  * @returns Middleware execution context.
@@ -48,12 +45,6 @@ class FileRouteMiddlewarePipeline {
48
45
  },
49
46
  locals: input.locals,
50
47
  require: createRequire(() => context.locals),
51
- render: async () => {
52
- throw new Error(FILE_ROUTE_MIDDLEWARE_PIPELINE_ERRORS.CTX_RENDER_UNAVAILABLE);
53
- },
54
- renderPartial: async () => {
55
- throw new Error(FILE_ROUTE_MIDDLEWARE_PIPELINE_ERRORS.CTX_RENDER_PARTIAL_UNAVAILABLE);
56
- },
57
48
  json: (data, options) => {
58
49
  const builder = new ApiResponseBuilder();
59
50
  if (options?.status) builder.status(options.status);
@@ -1,19 +1,23 @@
1
- import type { EcoPagesAppConfig, FileSystemServerOptions } from '../../types/internal-types.js';
1
+ import type { FileSystemServerOptions } from '../../types/internal-types.js';
2
2
  import type { RouteRendererBody } from '../../types/public-types.js';
3
- import type { RouteRendererFactory } from '../../route-renderer/route-renderer.js';
3
+ /**
4
+ * Builds HTTP responses for static files and shared file-system fallbacks.
5
+ */
4
6
  export declare class FileSystemServerResponseFactory {
5
- private appConfig;
6
- private routeRendererFactory;
7
7
  private options;
8
- constructor({ appConfig, routeRendererFactory, options, }: {
9
- appConfig: EcoPagesAppConfig;
10
- routeRendererFactory: RouteRendererFactory;
8
+ constructor({ options }: {
11
9
  options: FileSystemServerOptions;
12
10
  });
13
11
  isHtml(contentType: string): contentType is "text/html";
14
12
  shouldEnableGzip(contentType: string): boolean;
15
13
  createResponseWithBody(body: RouteRendererBody, init?: ResponseInit): Promise<Response>;
16
14
  createDefaultNotFoundResponse(): Promise<Response>;
17
- createCustomNotFoundResponse(): Promise<Response>;
18
- createFileResponse(filePath: string, contentType: string): Promise<Response>;
15
+ /**
16
+ * Wraps already-rendered HTML in a 404 response envelope.
17
+ */
18
+ createHtmlNotFoundResponse(body: RouteRendererBody): Promise<Response>;
19
+ /**
20
+ * Reads a static file response, returning `null` when the file is missing.
21
+ */
22
+ createFileResponse(filePath: string, contentType: string): Promise<Response | null>;
19
23
  }
@@ -2,16 +2,8 @@ import { STATUS_MESSAGE } from "../../config/constants.js";
2
2
  import { appLogger } from "../../global/app-logger.js";
3
3
  import { fileSystem } from "@ecopages/file-system";
4
4
  class FileSystemServerResponseFactory {
5
- appConfig;
6
- routeRendererFactory;
7
5
  options;
8
- constructor({
9
- appConfig,
10
- routeRendererFactory,
11
- options
12
- }) {
13
- this.appConfig = appConfig;
14
- this.routeRendererFactory = routeRendererFactory;
6
+ constructor({ options }) {
15
7
  this.options = options;
16
8
  }
17
9
  isHtml(contentType) {
@@ -34,22 +26,11 @@ class FileSystemServerResponseFactory {
34
26
  status: 404
35
27
  });
36
28
  }
37
- async createCustomNotFoundResponse() {
38
- const error404TemplatePath = this.appConfig.absolutePaths.error404TemplatePath;
39
- try {
40
- fileSystem.verifyFileExists(error404TemplatePath);
41
- } catch {
42
- appLogger.debug(
43
- "Custom 404 template not found, falling back to default 404 response",
44
- error404TemplatePath
45
- );
46
- return this.createDefaultNotFoundResponse();
47
- }
48
- const routeRenderer = this.routeRendererFactory.createRenderer(error404TemplatePath);
49
- const result = await routeRenderer.createRoute({
50
- file: error404TemplatePath
51
- });
52
- return await this.createResponseWithBody(result.body, {
29
+ /**
30
+ * Wraps already-rendered HTML in a 404 response envelope.
31
+ */
32
+ async createHtmlNotFoundResponse(body) {
33
+ return await this.createResponseWithBody(body, {
53
34
  status: 404,
54
35
  statusText: STATUS_MESSAGE[404],
55
36
  headers: {
@@ -57,6 +38,9 @@ class FileSystemServerResponseFactory {
57
38
  }
58
39
  });
59
40
  }
41
+ /**
42
+ * Reads a static file response, returning `null` when the file is missing.
43
+ */
60
44
  async createFileResponse(filePath, contentType) {
61
45
  try {
62
46
  let file;
@@ -88,7 +72,7 @@ class FileSystemServerResponseFactory {
88
72
  } else {
89
73
  appLogger.error("Error reading file", filePath, err);
90
74
  }
91
- return this.createCustomNotFoundResponse();
75
+ return null;
92
76
  }
93
77
  }
94
78
  }
@@ -1,13 +1,14 @@
1
1
  import type { EcoPagesAppConfig, MatchResult } from '../../types/internal-types.js';
2
- import type { RouteRendererFactory } from '../../route-renderer/route-renderer.js';
3
- import type { FSRouter } from '../../router/server/fs-router.js';
2
+ import type { PageRendererResolver } from '../../route-renderer/route-renderer.js';
3
+ import type { RouteRegistry } from '../../router/server/route-registry.js';
4
4
  import type { PageCacheService } from '../../services/cache/page-cache-service.js';
5
5
  import type { CacheStrategy } from '../../services/cache/cache.types.js';
6
6
  import type { FileSystemServerResponseFactory } from './fs-server-response-factory.js';
7
7
  export interface FileSystemResponseMatcherOptions {
8
8
  appConfig: EcoPagesAppConfig;
9
- router: FSRouter;
10
- routeRendererFactory: RouteRendererFactory;
9
+ assetPrefix: string;
10
+ router: RouteRegistry;
11
+ routeRendererFactory: PageRendererResolver;
11
12
  fileSystemResponseFactory: FileSystemServerResponseFactory;
12
13
  /** Optional cache service. When null, caching is disabled. */
13
14
  cacheService?: PageCacheService | null;
@@ -17,18 +18,19 @@ export interface FileSystemResponseMatcherOptions {
17
18
  /**
18
19
  * Matches file-system routes to rendered HTML responses.
19
20
  *
20
- * render pipeline. It coordinates page module inspection, request-local policy,
21
+ * This render pipeline coordinates page module inspection, request-local policy,
21
22
  * renderer invocation, middleware execution, cache integration, and fallback
22
23
  * error translation.
23
24
  */
24
25
  export declare class FileSystemResponseMatcher {
25
26
  private appConfig;
27
+ private assetPrefix;
26
28
  private router;
27
29
  private routeRendererFactory;
28
30
  private fileSystemResponseFactory;
29
31
  private pageRequestCacheCoordinator;
30
32
  private fileRouteMiddlewarePipeline;
31
- constructor({ appConfig, router, routeRendererFactory, fileSystemResponseFactory, cacheService, defaultCacheStrategy, }: FileSystemResponseMatcherOptions);
33
+ constructor({ appConfig, assetPrefix, router, routeRendererFactory, fileSystemResponseFactory, cacheService, defaultCacheStrategy, }: FileSystemResponseMatcherOptions);
32
34
  /**
33
35
  * Resolves unmatched paths either as static asset requests or as the custom
34
36
  * not-found page.
@@ -48,6 +50,12 @@ export declare class FileSystemResponseMatcher {
48
50
  * @returns Final response for the matched route.
49
51
  */
50
52
  handleMatch(match: MatchResult, request?: Request): Promise<Response>;
53
+ /**
54
+ * Renders the app-owned custom 404 page, falling back to the default text 404
55
+ * when the page template cannot be resolved.
56
+ */
57
+ private renderCustomNotFoundResponse;
58
+ private createExecutionPlan;
51
59
  /**
52
60
  * Loads the matched page module for request-time inspection.
53
61
  *
@@ -7,6 +7,7 @@ import { LocalsAccessError } from "../../errors/locals-access-error.js";
7
7
  import { isDevelopmentRuntime } from "../../utils/runtime.js";
8
8
  class FileSystemResponseMatcher {
9
9
  appConfig;
10
+ assetPrefix;
10
11
  router;
11
12
  routeRendererFactory;
12
13
  fileSystemResponseFactory;
@@ -14,6 +15,7 @@ class FileSystemResponseMatcher {
14
15
  fileRouteMiddlewarePipeline;
15
16
  constructor({
16
17
  appConfig,
18
+ assetPrefix,
17
19
  router,
18
20
  routeRendererFactory,
19
21
  fileSystemResponseFactory,
@@ -21,6 +23,7 @@ class FileSystemResponseMatcher {
21
23
  defaultCacheStrategy = "static"
22
24
  }) {
23
25
  this.appConfig = appConfig;
26
+ this.assetPrefix = assetPrefix;
24
27
  this.router = router;
25
28
  this.routeRendererFactory = routeRendererFactory;
26
29
  this.fileSystemResponseFactory = fileSystemResponseFactory;
@@ -36,12 +39,13 @@ class FileSystemResponseMatcher {
36
39
  async handleNoMatch(requestUrl) {
37
40
  const isStaticFileRequest = ServerUtils.hasKnownExtension(requestUrl);
38
41
  if (!isStaticFileRequest) {
39
- return this.fileSystemResponseFactory.createCustomNotFoundResponse();
42
+ return this.renderCustomNotFoundResponse();
40
43
  }
41
44
  const relativeUrl = requestUrl.startsWith("/") ? requestUrl.slice(1) : requestUrl;
42
- const filePath = path.join(this.router.assetPrefix, relativeUrl);
45
+ const filePath = path.join(this.assetPrefix, relativeUrl);
43
46
  const contentType = ServerUtils.getContentType(filePath);
44
- return this.fileSystemResponseFactory.createFileResponse(filePath, contentType);
47
+ const response = await this.fileSystemResponseFactory.createFileResponse(filePath, contentType);
48
+ return response ?? this.renderCustomNotFoundResponse();
45
49
  }
46
50
  /**
47
51
  * Handles a matched file-system page route.
@@ -55,34 +59,25 @@ class FileSystemResponseMatcher {
55
59
  * @returns Final response for the matched route.
56
60
  */
57
61
  async handleMatch(match, request) {
58
- const cacheKey = this.pageRequestCacheCoordinator.buildCacheKey(match);
59
62
  try {
60
- const resolvedRequest = request ?? new Request(new URL(cacheKey, this.router.origin).toString(), {
61
- method: "GET"
62
- });
63
- const localsStore = {};
64
- const pageModule = await this.importPageModule(match.filePath);
65
- const Page = pageModule?.default;
66
- const pageMiddleware = Page?.middleware ?? [];
67
- const pageCacheStrategy = Page?.cache ?? this.pageRequestCacheCoordinator.getDefaultCacheStrategy();
68
- const localsForRender = pageCacheStrategy === "dynamic" ? localsStore : void 0;
63
+ const executionPlan = await this.createExecutionPlan(match, request);
69
64
  this.fileRouteMiddlewarePipeline.assertValidConfiguration({
70
- middleware: pageMiddleware,
71
- pageCacheStrategy,
72
- filePath: match.filePath
65
+ middleware: executionPlan.pageMiddleware,
66
+ pageCacheStrategy: executionPlan.pageCacheStrategy,
67
+ filePath: executionPlan.pageFilePath
73
68
  });
74
- const routeRenderer = this.routeRendererFactory.createRenderer(match.filePath);
69
+ const routeRenderer = this.routeRendererFactory.getPageRenderer(executionPlan.pageFilePath);
75
70
  const middlewareContext = this.fileRouteMiddlewarePipeline.createContext({
76
- request: resolvedRequest,
71
+ request: executionPlan.request,
77
72
  params: match.params,
78
- locals: localsStore
73
+ locals: executionPlan.localsStore
79
74
  });
80
75
  const renderFn = async () => {
81
- const result = await routeRenderer.createRoute({
82
- file: match.filePath,
76
+ const result = await routeRenderer.execute({
77
+ file: executionPlan.pageFilePath,
83
78
  params: match.params,
84
79
  query: match.query,
85
- locals: localsForRender
80
+ locals: executionPlan.localsForRender
86
81
  });
87
82
  const html = await this.pageRequestCacheCoordinator.bodyToString(result.body);
88
83
  const strategy = result.cacheStrategy ?? this.pageRequestCacheCoordinator.getDefaultCacheStrategy();
@@ -90,13 +85,13 @@ class FileSystemResponseMatcher {
90
85
  };
91
86
  const renderResponse = async () => {
92
87
  return this.pageRequestCacheCoordinator.render({
93
- cacheKey,
94
- pageCacheStrategy,
88
+ cacheKey: executionPlan.cacheKey,
89
+ pageCacheStrategy: executionPlan.pageCacheStrategy,
95
90
  renderFn
96
91
  });
97
92
  };
98
93
  return await this.fileRouteMiddlewarePipeline.run({
99
- middleware: pageMiddleware,
94
+ middleware: executionPlan.pageMiddleware,
100
95
  context: middlewareContext,
101
96
  renderResponse
102
97
  });
@@ -112,12 +107,56 @@ class FileSystemResponseMatcher {
112
107
  }
113
108
  if (error instanceof Error) {
114
109
  if (isDevelopmentRuntime() || appLogger.isDebugEnabled()) {
115
- appLogger.error(`[FileSystemResponseMatcher] ${error.message} at ${match.pathname}`);
110
+ appLogger.error(`[FileSystemResponseMatcher] ${error.message} at ${match.requestedPathname}`);
116
111
  }
117
112
  }
118
- return this.fileSystemResponseFactory.createCustomNotFoundResponse();
113
+ return this.renderCustomNotFoundResponse();
114
+ }
115
+ }
116
+ /**
117
+ * Renders the app-owned custom 404 page, falling back to the default text 404
118
+ * when the page template cannot be resolved.
119
+ */
120
+ async renderCustomNotFoundResponse() {
121
+ const error404TemplatePath = this.appConfig.absolutePaths.error404TemplatePath;
122
+ try {
123
+ const routeRenderer = this.routeRendererFactory.getPageRenderer(error404TemplatePath);
124
+ const result = await routeRenderer.execute({
125
+ file: error404TemplatePath
126
+ });
127
+ return this.fileSystemResponseFactory.createHtmlNotFoundResponse(result.body);
128
+ } catch {
129
+ appLogger.debug(
130
+ "Custom 404 template not found, falling back to default 404 response",
131
+ error404TemplatePath
132
+ );
133
+ return this.fileSystemResponseFactory.createDefaultNotFoundResponse();
119
134
  }
120
135
  }
136
+ async createExecutionPlan(match, request) {
137
+ const cacheKey = this.pageRequestCacheCoordinator.buildCacheKey({
138
+ pathname: match.requestedPathname,
139
+ query: match.query
140
+ });
141
+ const resolvedRequest = request ?? new Request(new URL(cacheKey, this.router.origin).toString(), {
142
+ method: "GET"
143
+ });
144
+ const localsStore = {};
145
+ const pageFilePath = match.templateRoute.filePath;
146
+ const pageModule = await this.importPageModule(pageFilePath);
147
+ const Page = pageModule?.default;
148
+ const pageMiddleware = Page?.middleware ?? [];
149
+ const pageCacheStrategy = Page?.cache ?? this.pageRequestCacheCoordinator.getDefaultCacheStrategy();
150
+ return {
151
+ cacheKey,
152
+ request: resolvedRequest,
153
+ pageFilePath,
154
+ pageMiddleware,
155
+ pageCacheStrategy,
156
+ localsStore,
157
+ localsForRender: pageCacheStrategy === "dynamic" ? localsStore : void 0
158
+ };
159
+ }
121
160
  /**
122
161
  * Loads the matched page module for request-time inspection.
123
162
  *
@@ -130,7 +169,7 @@ class FileSystemResponseMatcher {
130
169
  * @returns Imported page module.
131
170
  */
132
171
  async importPageModule(filePath) {
133
- const routeRenderer = this.routeRendererFactory.createRenderer(filePath);
172
+ const routeRenderer = this.routeRendererFactory.getPageRenderer(filePath);
134
173
  return routeRenderer.loadPageModule(filePath, {
135
174
  cacheScope: "request-metadata"
136
175
  });
@@ -1,7 +1,7 @@
1
1
  import type { RenderContext } from '../../types/public-types.js';
2
- import type { IntegrationPlugin } from '../../plugins/integration-plugin.js';
2
+ import type { AnyIntegrationPlugin } from '../../plugins/integration-plugin.js';
3
3
  export interface CreateRenderContextOptions {
4
- integrations: IntegrationPlugin[];
4
+ integrations: AnyIntegrationPlugin[];
5
5
  rendererModules?: unknown;
6
6
  }
7
7
  /**