@ecopages/core 0.2.0-alpha.25 → 0.2.0-alpha.27

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 (111) hide show
  1. package/README.md +63 -7
  2. package/package.json +4 -47
  3. package/src/adapters/bun/create-app.ts +54 -2
  4. package/src/adapters/bun/hmr-manager.test.ts +0 -2
  5. package/src/adapters/bun/hmr-manager.ts +1 -24
  6. package/src/adapters/bun/server-adapter.ts +30 -4
  7. package/src/adapters/node/node-hmr-manager.test.ts +0 -2
  8. package/src/adapters/node/node-hmr-manager.ts +2 -25
  9. package/src/adapters/shared/explicit-static-render-preparation.ts +58 -0
  10. package/src/adapters/shared/explicit-static-route-matcher.test.ts +6 -6
  11. package/src/adapters/shared/explicit-static-route-matcher.ts +22 -31
  12. package/src/adapters/shared/file-route-middleware-pipeline.test.ts +5 -10
  13. package/src/adapters/shared/file-route-middleware-pipeline.ts +8 -17
  14. package/src/adapters/shared/fs-server-response-factory.test.ts +32 -43
  15. package/src/adapters/shared/fs-server-response-factory.ts +15 -37
  16. package/src/adapters/shared/fs-server-response-matcher.test.ts +65 -39
  17. package/src/adapters/shared/fs-server-response-matcher.ts +94 -43
  18. package/src/adapters/shared/hmr-manager.contract.test.ts +0 -4
  19. package/src/adapters/shared/render-context.ts +3 -3
  20. package/src/adapters/shared/server-adapter.test.ts +53 -0
  21. package/src/adapters/shared/server-adapter.ts +228 -159
  22. package/src/adapters/shared/server-route-handler.test.ts +6 -5
  23. package/src/adapters/shared/server-route-handler.ts +4 -4
  24. package/src/adapters/shared/server-static-builder.test.ts +4 -4
  25. package/src/adapters/shared/server-static-builder.ts +4 -4
  26. package/src/config/README.md +1 -1
  27. package/src/config/config-builder.test.ts +0 -1
  28. package/src/config/config-builder.ts +2 -7
  29. package/src/dev/host-runtime.ts +34 -0
  30. package/src/eco/eco.browser.test.ts +2 -2
  31. package/src/eco/eco.browser.ts +2 -2
  32. package/src/eco/eco.test.ts +6 -6
  33. package/src/eco/eco.ts +12 -12
  34. package/src/eco/eco.types.ts +3 -3
  35. package/src/errors/index.ts +1 -0
  36. package/src/hmr/client/hmr-runtime.ts +4 -2
  37. package/src/hmr/strategies/js-hmr-strategy.test.ts +0 -1
  38. package/src/hmr/strategies/js-hmr-strategy.ts +0 -6
  39. package/src/integrations/ghtml/ghtml-renderer.test.ts +7 -7
  40. package/src/integrations/ghtml/ghtml-renderer.ts +1 -11
  41. package/src/plugins/eco-component-meta-plugin.ts +0 -1
  42. package/src/plugins/integration-plugin.test.ts +9 -14
  43. package/src/plugins/integration-plugin.ts +34 -22
  44. package/src/plugins/processor.ts +17 -0
  45. package/src/route-renderer/GRAPH.md +81 -289
  46. package/src/route-renderer/README.md +67 -105
  47. package/src/route-renderer/orchestration/component-render-context.ts +45 -38
  48. package/src/route-renderer/orchestration/declared-ownership-graph.ts +62 -0
  49. package/src/route-renderer/orchestration/foreign-subtree-execution.service.ts +383 -0
  50. package/src/route-renderer/orchestration/integration-renderer.test.ts +118 -121
  51. package/src/route-renderer/orchestration/integration-renderer.ts +362 -403
  52. package/src/route-renderer/orchestration/ownership-planning.service.ts +97 -0
  53. package/src/route-renderer/orchestration/ownership-validation.service.ts +76 -0
  54. package/src/route-renderer/orchestration/processed-asset-dedupe.ts +1 -1
  55. package/src/route-renderer/orchestration/{queued-boundary-runtime.service.test.ts → queued-foreign-subtree-resolution.service.test.ts} +76 -71
  56. package/src/route-renderer/orchestration/{queued-boundary-runtime.service.ts → queued-foreign-subtree-resolution.service.ts} +68 -63
  57. package/src/route-renderer/orchestration/render-output.utils.ts +21 -13
  58. package/src/route-renderer/orchestration/{render-preparation.service.test.ts → route-render-orchestrator.prepare-render-options.test.ts} +160 -85
  59. package/src/route-renderer/orchestration/route-render-orchestrator.test.ts +265 -0
  60. package/src/route-renderer/orchestration/{render-preparation.service.ts → route-render-orchestrator.ts} +244 -160
  61. package/src/route-renderer/page-loading/component-dependency-collection.ts +9 -3
  62. package/src/route-renderer/page-loading/declared-asset-collection.ts +2 -5
  63. package/src/route-renderer/page-loading/dependency-resolver.test.ts +107 -11
  64. package/src/route-renderer/page-loading/dependency-resolver.ts +6 -12
  65. package/src/route-renderer/page-loading/ecopages-virtual-imports.ts +1 -1
  66. package/src/route-renderer/page-loading/lazy-entry-collection.ts +1 -1
  67. package/src/route-renderer/page-loading/lazy-trigger-planning.ts +1 -1
  68. package/src/route-renderer/page-loading/module-declaration-aggregation.ts +1 -1
  69. package/src/route-renderer/page-loading/module-declaration-scripts.ts +1 -1
  70. package/src/route-renderer/page-loading/page-dependency-bundling.ts +105 -66
  71. package/src/route-renderer/route-renderer.ts +28 -31
  72. package/src/router/README.md +16 -19
  73. package/src/router/server/route-registry.test.ts +176 -0
  74. package/src/router/server/route-registry.ts +382 -0
  75. package/src/services/README.md +1 -2
  76. package/src/services/assets/asset-processing-service/asset-dependency-keys.ts +1 -1
  77. package/src/services/assets/asset-processing-service/asset-processing.service.test.ts +1 -4
  78. package/src/services/assets/asset-processing-service/asset-processing.service.ts +1 -2
  79. package/src/services/assets/asset-processing-service/assets.types.ts +3 -0
  80. package/src/services/assets/asset-processing-service/grouped-content-bundles.ts +1 -1
  81. package/src/services/assets/asset-processing-service/index.ts +1 -0
  82. package/src/{route-renderer/orchestration/page-packaging.service.test.ts → services/assets/asset-processing-service/page-package.test.ts} +38 -14
  83. package/src/services/assets/asset-processing-service/page-package.ts +93 -0
  84. package/src/services/assets/asset-processing-service/processors/base/base-script-processor.ts +4 -5
  85. package/src/services/assets/asset-processing-service/processors/script/content-script.processor.test.ts +13 -10
  86. package/src/services/assets/asset-processing-service/processors/script/content-script.processor.ts +3 -0
  87. package/src/services/assets/asset-processing-service/processors/script/file-script.processor.ts +6 -0
  88. package/src/services/assets/asset-processing-service/processors/script/node-module-script.processor.ts +2 -0
  89. package/src/services/assets/asset-processing-service/processors/stylesheet/content-stylesheet.processor.ts +1 -0
  90. package/src/services/assets/asset-processing-service/processors/stylesheet/file-stylesheet.processor.ts +2 -0
  91. package/src/services/assets/asset-processing-service/ungrouped-dependency-processing.ts +1 -1
  92. package/src/services/html/html-transformer.service.test.ts +1 -4
  93. package/src/services/module-loading/app-server-module-transpiler.service.ts +1 -3
  94. package/src/services/module-loading/node-bootstrap-plugin.ts +17 -3
  95. package/src/services/module-loading/page-module-import.service.ts +0 -1
  96. package/src/services/module-loading/source-module-support.ts +1 -1
  97. package/src/static-site-generator/static-site-generator.test.ts +124 -32
  98. package/src/static-site-generator/static-site-generator.ts +168 -185
  99. package/src/types/internal-types.ts +13 -12
  100. package/src/types/public-types.ts +55 -39
  101. package/src/watchers/project-watcher.test-helpers.ts +4 -3
  102. package/src/route-renderer/orchestration/boundary-planning.service.ts +0 -146
  103. package/src/route-renderer/orchestration/page-packaging.service.ts +0 -85
  104. package/src/route-renderer/orchestration/render-execution.service.test.ts +0 -196
  105. package/src/route-renderer/orchestration/render-execution.service.ts +0 -182
  106. package/src/route-renderer/orchestration/route-shell-composer.service.ts +0 -162
  107. package/src/router/server/fs-router-scanner.test.ts +0 -83
  108. package/src/router/server/fs-router-scanner.ts +0 -224
  109. package/src/router/server/fs-router.test.ts +0 -214
  110. package/src/router/server/fs-router.ts +0 -122
  111. package/src/services/runtime-state/runtime-specifier-registry.service.ts +0 -96
@@ -2,8 +2,7 @@ import path from 'node:path';
2
2
  import { AbstractServerAdapter } from '../abstract/server-adapter.ts';
3
3
  import type { ServerAdapterOptions, ServerAdapterResult } from '../abstract/server-adapter.ts';
4
4
  import { RouteRendererFactory } from '../../route-renderer/route-renderer.ts';
5
- import { FSRouter } from '../../router/server/fs-router.ts';
6
- import { FSRouterScanner } from '../../router/server/fs-router-scanner.ts';
5
+ import { RouteRegistry } from '../../router/server/route-registry.ts';
7
6
  import { MemoryCacheStore } from '../../services/cache/memory-cache-store.ts';
8
7
  import { PageCacheService } from '../../services/cache/page-cache-service.ts';
9
8
  import { SchemaValidationService } from '../../services/validation/schema-validation-service.ts';
@@ -19,6 +18,10 @@ import { HttpError } from '../../errors/http-error.ts';
19
18
  import { ApiResponseBuilder } from './api-response.ts';
20
19
  import { appLogger } from '../../global/app-logger.ts';
21
20
  import { fileSystem } from '@ecopages/file-system';
21
+ import type { EcoPageFile } from '../../types/public-types.ts';
22
+ import { getAppServerModuleTranspiler } from '../../services/module-loading/app-server-module-transpiler.service.ts';
23
+ import { resolveInternalExecutionDir } from '../../utils/resolve-work-dir.ts';
24
+ import type { RouteRegistryPageModuleAdapter } from '../../router/server/route-registry.ts';
22
25
  import type {
23
26
  ApiHandler,
24
27
  ApiHandlerContext,
@@ -28,11 +31,24 @@ import type {
28
31
  StaticRoute,
29
32
  } from '../../types/public-types.ts';
30
33
 
34
+ type SharedResponseHandlerDependencies = {
35
+ cacheService: PageCacheService | null;
36
+ fileSystemResponseMatcher: FileSystemResponseMatcher;
37
+ explicitStaticRouteMatcher?: ExplicitStaticRouteMatcher;
38
+ };
39
+
40
+ type SharedRequestContext = {
41
+ apiHandlers: ApiHandler[];
42
+ errorHandler?: ErrorHandler;
43
+ serverInstance?: any;
44
+ hmrManager?: any;
45
+ };
46
+
31
47
  export abstract class SharedServerAdapter<
32
48
  TOptions extends ServerAdapterOptions,
33
49
  TResult extends ServerAdapterResult,
34
50
  > extends AbstractServerAdapter<TOptions, TResult> {
35
- protected router!: FSRouter;
51
+ protected router!: RouteRegistry;
36
52
  protected fileSystemResponseMatcher!: FileSystemResponseMatcher;
37
53
  protected routeRendererFactory!: RouteRendererFactory;
38
54
  protected routeHandler!: ServerRouteHandler;
@@ -76,33 +92,50 @@ export abstract class SharedServerAdapter<
76
92
  }
77
93
 
78
94
  /**
79
- * Scans the filesystem and dynamically constructs the universal router map.
95
+ * Scans the filesystem and dynamically constructs the Route Registry.
80
96
  *
81
97
  * This process runs identically across both Bun and Node wrappers. It analyzes the configured pages
82
98
  * directory, building a map of all available UI routes and API endpoints.
83
- * The resulting `FSRouter` instance becomes the central nervous system for mapping WinterCG incoming
99
+ * The resulting `RouteRegistry` instance becomes the central nervous system for mapping WinterCG incoming
84
100
  * Web Requests (`Request`) to their corresponding internal execution paths.
85
101
  */
86
102
  protected async initSharedRouter(): Promise<void> {
87
- const scanner = new FSRouterScanner({
88
- dir: path.join(this.appConfig.rootDir, this.appConfig.srcDir, this.appConfig.pagesDir),
103
+ this.router = new RouteRegistry({
104
+ pagesDir: path.join(this.appConfig.rootDir, this.appConfig.srcDir, this.appConfig.pagesDir),
89
105
  appConfig: this.appConfig,
90
106
  origin: this.runtimeOrigin,
91
107
  templatesExt: this.appConfig.templatesExt,
92
- options: {
93
- buildMode: !this.options?.watch,
94
- },
95
- });
96
-
97
- this.router = new FSRouter({
98
- origin: this.runtimeOrigin,
99
- assetPrefix: path.join(this.appConfig.rootDir, this.appConfig.distDir),
100
- scanner,
108
+ buildMode: !this.options?.watch,
109
+ pageModuleAdapter: this.createRouteRegistryPageModuleAdapter(),
101
110
  });
102
111
 
103
112
  await this.router.init();
104
113
  }
105
114
 
115
+ private createRouteRegistryPageModuleAdapter(): RouteRegistryPageModuleAdapter {
116
+ const serverModuleTranspiler = getAppServerModuleTranspiler(this.appConfig);
117
+
118
+ return {
119
+ loadPageModule: async (filePath) => {
120
+ const module = (await serverModuleTranspiler.importModule({
121
+ filePath,
122
+ outdir: path.join(resolveInternalExecutionDir(this.appConfig), '.server-route-modules'),
123
+ externalPackages: false,
124
+ transpileErrorMessage: (details) => `Error transpiling route module: ${details}`,
125
+ noOutputMessage: (targetFilePath) =>
126
+ `No transpiled output generated for route module: ${targetFilePath}`,
127
+ })) as EcoPageFile;
128
+
129
+ const page = module.default;
130
+
131
+ return {
132
+ staticPaths: page?.staticPaths ?? module.getStaticPaths,
133
+ staticProps: page?.staticProps ?? module.getStaticProps,
134
+ };
135
+ },
136
+ };
137
+ }
138
+
106
139
  /**
107
140
  * Sets up the unified rendering pipeline and response matching chain.
108
141
  *
@@ -124,51 +157,63 @@ export abstract class SharedServerAdapter<
124
157
  runtimeOrigin: this.runtimeOrigin,
125
158
  });
126
159
 
160
+ const { fileSystemResponseMatcher, explicitStaticRouteMatcher } =
161
+ this.createSharedResponseHandlerDependencies(staticRoutes);
162
+
163
+ this.fileSystemResponseMatcher = fileSystemResponseMatcher;
164
+ this.routeHandler = new ServerRouteHandler({
165
+ router: this.router,
166
+ fileSystemResponseMatcher: this.fileSystemResponseMatcher,
167
+ explicitStaticRouteMatcher,
168
+ watch: !!this.options?.watch,
169
+ hmrManager,
170
+ });
171
+ }
172
+
173
+ private createSharedResponseHandlerDependencies(staticRoutes: StaticRoute[]): SharedResponseHandlerDependencies {
127
174
  const fileSystemResponseFactory = new FileSystemServerResponseFactory({
128
- appConfig: this.appConfig,
129
- routeRendererFactory: this.routeRendererFactory,
130
175
  options: {
131
176
  watchMode: !!this.options?.watch,
132
177
  },
133
178
  });
134
179
 
135
- const cacheConfig = this.appConfig.cache;
136
- const isCacheEnabled = cacheConfig?.enabled ?? !this.options?.watch;
137
- let cacheService: PageCacheService | null = null;
138
-
139
- if (isCacheEnabled) {
140
- const store =
141
- cacheConfig?.store === 'memory' || !cacheConfig?.store
142
- ? new MemoryCacheStore({ maxEntries: cacheConfig?.maxEntries })
143
- : cacheConfig.store;
144
- cacheService = new PageCacheService({ store, enabled: true });
145
- }
146
-
147
- this.fileSystemResponseMatcher = new FileSystemResponseMatcher({
180
+ const cacheService = this.createSharedPageCacheService();
181
+ const fileSystemResponseMatcher = new FileSystemResponseMatcher({
148
182
  appConfig: this.appConfig,
183
+ assetPrefix: path.join(this.appConfig.rootDir, this.appConfig.distDir),
149
184
  router: this.router,
150
185
  routeRendererFactory: this.routeRendererFactory,
151
186
  fileSystemResponseFactory,
152
187
  cacheService,
153
- defaultCacheStrategy: cacheConfig?.defaultStrategy ?? 'static',
188
+ defaultCacheStrategy: this.appConfig.cache?.defaultStrategy ?? 'static',
154
189
  });
155
190
 
156
- const explicitStaticRouteMatcher =
157
- staticRoutes.length > 0
158
- ? new ExplicitStaticRouteMatcher({
159
- appConfig: this.appConfig,
160
- routeRendererFactory: this.routeRendererFactory,
161
- staticRoutes,
162
- })
163
- : undefined;
191
+ return {
192
+ cacheService,
193
+ fileSystemResponseMatcher,
194
+ explicitStaticRouteMatcher:
195
+ staticRoutes.length > 0
196
+ ? new ExplicitStaticRouteMatcher({
197
+ appConfig: this.appConfig,
198
+ routeRendererFactory: this.routeRendererFactory,
199
+ staticRoutes,
200
+ })
201
+ : undefined,
202
+ };
203
+ }
164
204
 
165
- this.routeHandler = new ServerRouteHandler({
166
- router: this.router,
167
- fileSystemResponseMatcher: this.fileSystemResponseMatcher,
168
- explicitStaticRouteMatcher,
169
- watch: !!this.options?.watch,
170
- hmrManager,
171
- });
205
+ private createSharedPageCacheService(): PageCacheService | null {
206
+ const cacheConfig = this.appConfig.cache;
207
+ const isCacheEnabled = cacheConfig?.enabled ?? !this.options?.watch;
208
+ if (!isCacheEnabled) {
209
+ return null;
210
+ }
211
+
212
+ const store =
213
+ cacheConfig?.store === 'memory' || !cacheConfig?.store
214
+ ? new MemoryCacheStore({ maxEntries: cacheConfig?.maxEntries })
215
+ : cacheConfig.store;
216
+ return new PageCacheService({ store, enabled: true });
172
217
  }
173
218
 
174
219
  protected getCacheService(): CacheInvalidator | null {
@@ -209,98 +254,20 @@ export abstract class SharedServerAdapter<
209
254
  let context: ApiHandlerContext<Request, any> | undefined;
210
255
 
211
256
  try {
212
- const middleware = routeConfig.middleware || [];
213
- const schema = routeConfig.schema;
214
- const locals: Record<string, unknown> = {};
215
-
216
- const normalizedParams = Object.fromEntries(
217
- Object.entries(params).map(([key, value]) => [key, Array.isArray(value) ? value.join('/') : value]),
218
- );
219
-
220
- context = {
221
- request,
222
- params: normalizedParams,
223
- response: new ApiResponseBuilder(),
224
- server: serverInstance,
225
- locals,
226
- require: createRequire((): Record<string, unknown> => locals),
227
- services: {
228
- cache: this.getCacheService(),
229
- },
230
- ...this.getRenderContext(),
231
- };
232
-
233
- if (schema) {
234
- const url = new URL(request.url);
235
- const queryParams = Object.fromEntries(url.searchParams);
236
- const headers = Object.fromEntries(request.headers);
237
-
238
- let body: unknown;
239
- if (schema.body) {
240
- try {
241
- const contentType = request.headers.get('Content-Type') || '';
242
- if (contentType.includes('application/json')) body = await request.clone().json();
243
- else if (contentType.includes('text/plain')) body = await request.clone().text();
244
- } catch {
245
- return context.response.status(400).json({ error: 'Invalid request body' });
246
- }
247
- }
248
-
249
- const validationResult = await this.schemaValidator.validateRequest(
250
- { body, query: queryParams, headers, params: normalizedParams },
251
- schema,
252
- );
253
-
254
- if (!validationResult.success) {
255
- return context.response.status(400).json({
256
- error: 'Validation failed',
257
- issues: validationResult.errors,
258
- });
259
- }
260
-
261
- const validated = validationResult.data!;
262
- if (validated.body !== undefined) context.body = validated.body;
263
- if (validated.query !== undefined) context.query = validated.query;
264
- if (validated.headers !== undefined) context.headers = validated.headers;
265
- if (validated.params !== undefined) context.params = validated.params as Record<string, string>;
266
- }
267
-
268
- if (middleware.length === 0) {
269
- return await routeConfig.handler(context);
257
+ context = this.createApiHandlerContext(request, params, serverInstance);
258
+ const schemaResponse = await this.applyApiRequestSchema(context, routeConfig.schema);
259
+ if (schemaResponse) {
260
+ return schemaResponse;
270
261
  }
271
262
 
272
- let index = 0;
273
- const executeNext = async (): Promise<Response> => {
274
- if (index < middleware.length) {
275
- const currentMiddleware = middleware[index++];
276
- return await currentMiddleware(context!, executeNext);
277
- }
278
- return await routeConfig.handler(context!);
279
- };
280
-
281
- return await executeNext();
263
+ return await this.runApiMiddlewareChain(context, routeConfig);
282
264
  } catch (error) {
283
265
  if (error instanceof Response) return error;
284
266
 
285
267
  if (errorHandler) {
286
268
  try {
287
269
  if (!context) {
288
- const locals: Record<string, unknown> = {};
289
- context = {
290
- request,
291
- params: Object.fromEntries(
292
- Object.entries(params).map(([key, value]) => [
293
- key,
294
- Array.isArray(value) ? value.join('/') : value,
295
- ]),
296
- ),
297
- response: new ApiResponseBuilder(),
298
- server: serverInstance,
299
- locals,
300
- require: createRequire((): Record<string, unknown> => locals),
301
- services: { cache: this.getCacheService() },
302
- ...this.getRenderContext(),
303
- };
270
+ context = this.createApiHandlerContext(request, params, serverInstance);
304
271
  }
305
272
  return await errorHandler(error, context);
306
273
  } catch (handlerError) {
@@ -314,6 +281,98 @@ export abstract class SharedServerAdapter<
314
281
  }
315
282
  }
316
283
 
284
+ private createApiHandlerContext(
285
+ request: Request,
286
+ params: Record<string, string | string[]>,
287
+ serverInstance: any,
288
+ ): ApiHandlerContext<Request, any> {
289
+ const locals: Record<string, unknown> = {};
290
+ const normalizedParams = this.normalizeApiParams(params);
291
+
292
+ return {
293
+ request,
294
+ params: normalizedParams,
295
+ response: new ApiResponseBuilder(),
296
+ server: serverInstance,
297
+ locals,
298
+ require: createRequire((): Record<string, unknown> => locals),
299
+ services: {
300
+ cache: this.getCacheService(),
301
+ },
302
+ ...this.getRenderContext(),
303
+ };
304
+ }
305
+
306
+ private normalizeApiParams(params: Record<string, string | string[]>): Record<string, string> {
307
+ return Object.fromEntries(
308
+ Object.entries(params).map(([key, value]) => [key, Array.isArray(value) ? value.join('/') : value]),
309
+ );
310
+ }
311
+
312
+ private async applyApiRequestSchema(
313
+ context: ApiHandlerContext<Request, any>,
314
+ schema: ApiHandler['schema'],
315
+ ): Promise<Response | undefined> {
316
+ if (!schema) {
317
+ return undefined;
318
+ }
319
+
320
+ const url = new URL(context.request.url);
321
+ const queryParams = Object.fromEntries(url.searchParams);
322
+ const headers = Object.fromEntries(context.request.headers);
323
+
324
+ let body: unknown;
325
+ if (schema.body) {
326
+ try {
327
+ const contentType = context.request.headers.get('Content-Type') || '';
328
+ if (contentType.includes('application/json')) body = await context.request.clone().json();
329
+ else if (contentType.includes('text/plain')) body = await context.request.clone().text();
330
+ } catch {
331
+ return context.response.status(400).json({ error: 'Invalid request body' });
332
+ }
333
+ }
334
+
335
+ const validationResult = await this.schemaValidator.validateRequest(
336
+ { body, query: queryParams, headers, params: context.params },
337
+ schema,
338
+ );
339
+
340
+ if (!validationResult.success) {
341
+ return context.response.status(400).json({
342
+ error: 'Validation failed',
343
+ issues: validationResult.errors,
344
+ });
345
+ }
346
+
347
+ const validated = validationResult.data!;
348
+ if (validated.body !== undefined) context.body = validated.body;
349
+ if (validated.query !== undefined) context.query = validated.query;
350
+ if (validated.headers !== undefined) context.headers = validated.headers;
351
+ if (validated.params !== undefined) context.params = validated.params as Record<string, string>;
352
+ return undefined;
353
+ }
354
+
355
+ private async runApiMiddlewareChain(
356
+ context: ApiHandlerContext<Request, any>,
357
+ routeConfig: ApiHandler,
358
+ ): Promise<Response> {
359
+ const middleware = routeConfig.middleware || [];
360
+ if (middleware.length === 0) {
361
+ return await routeConfig.handler(context);
362
+ }
363
+
364
+ let index = 0;
365
+ const executeNext = async (): Promise<Response> => {
366
+ if (index < middleware.length) {
367
+ const currentMiddleware = middleware[index++];
368
+ return await currentMiddleware(context, executeNext);
369
+ }
370
+ return await routeConfig.handler(context);
371
+ };
372
+
373
+ return await executeNext();
374
+ }
375
+
317
376
  private normalizePath(pathname: string): string {
318
377
  if (pathname.length > 1 && pathname.endsWith('/')) {
319
378
  return pathname.slice(0, -1);
@@ -435,26 +494,7 @@ export abstract class SharedServerAdapter<
435
494
  return null;
436
495
  }
437
496
 
438
- /**
439
- * Universally processes an incoming WinterCG Web standard Request.
440
- *
441
- * 1. Resolves static Hot Module Replacement runtime blobs if development.
442
- * 2. Checks if the incoming request matches any parsed API route schemas.
443
- * - Routes through `executeApiHandler` which performs strict validation.
444
- * 3. Falls through to standard `ServerRouteHandler` for React/Lit filesystem pages.
445
- *
446
- * Both Bun and Node bindings fall back to this exact function once they have mapped their
447
- * native HTTP objects into Web Standard Requests.
448
- */
449
- public async handleSharedRequest(
450
- request: Request,
451
- context: {
452
- apiHandlers: ApiHandler[];
453
- errorHandler?: ErrorHandler;
454
- serverInstance?: any;
455
- hmrManager?: any;
456
- },
457
- ): Promise<Response> {
497
+ private tryHandleSharedHmrRequest(request: Request, context: SharedRequestContext): Response | null {
458
498
  const url = new URL(request.url);
459
499
 
460
500
  if (url.pathname === '/_hmr_runtime.js' && context.hmrManager) {
@@ -477,15 +517,44 @@ export abstract class SharedServerAdapter<
477
517
  }
478
518
  }
479
519
 
520
+ return null;
521
+ }
522
+
523
+ private async tryHandleSharedApiRequest(request: Request, context: SharedRequestContext): Promise<Response | null> {
480
524
  const apiMatch = this.matchApiHandler(request, context.apiHandlers);
481
- if (apiMatch) {
482
- return this.executeApiHandler(
483
- request,
484
- apiMatch.params,
485
- apiMatch.routeConfig,
486
- context.serverInstance,
487
- context.errorHandler,
488
- );
525
+ if (!apiMatch) {
526
+ return null;
527
+ }
528
+
529
+ return await this.executeApiHandler(
530
+ request,
531
+ apiMatch.params,
532
+ apiMatch.routeConfig,
533
+ context.serverInstance,
534
+ context.errorHandler,
535
+ );
536
+ }
537
+
538
+ /**
539
+ * Universally processes an incoming WinterCG Web standard Request.
540
+ *
541
+ * 1. Resolves static Hot Module Replacement runtime blobs if development.
542
+ * 2. Checks if the incoming request matches any parsed API route schemas.
543
+ * - Routes through `executeApiHandler` which performs strict validation.
544
+ * 3. Falls through to standard `ServerRouteHandler` for React/Lit filesystem pages.
545
+ *
546
+ * Both Bun and Node bindings fall back to this exact function once they have mapped their
547
+ * native HTTP objects into Web Standard Requests.
548
+ */
549
+ public async handleSharedRequest(request: Request, context: SharedRequestContext): Promise<Response> {
550
+ const hmrResponse = this.tryHandleSharedHmrRequest(request, context);
551
+ if (hmrResponse) {
552
+ return hmrResponse;
553
+ }
554
+
555
+ const apiResponse = await this.tryHandleSharedApiRequest(request, context);
556
+ if (apiResponse) {
557
+ return apiResponse;
489
558
  }
490
559
 
491
560
  return this.routeHandler.handleResponse(request);
@@ -1,13 +1,14 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
  import { ServerRouteHandler } from './server-route-handler';
3
- import type { FSRouter } from '../../router/server/fs-router.ts';
3
+ import type { RouteRegistry } from '../../router/server/route-registry.ts';
4
4
  import type { FileSystemResponseMatcher } from './fs-server-response-matcher.ts';
5
5
  import type { IHmrManager } from '../../types/public-types.ts';
6
6
 
7
7
  function createMockDependencies() {
8
8
  const Router = {
9
- match: vi.fn(() => null),
10
- } as unknown as FSRouter;
9
+ matchRequest: vi.fn(() => null),
10
+ origin: 'http://localhost:3000',
11
+ } as unknown as RouteRegistry;
11
12
 
12
13
  const FileSystemResponseMatcher = {
13
14
  handleMatch: vi.fn(() => Promise.resolve(new Response('Matched Content'))),
@@ -35,7 +36,7 @@ describe('ServerRouteHandler', () => {
35
36
  fileSystemResponseMatcher: FileSystemResponseMatcher,
36
37
  });
37
38
 
38
- Router.match = vi.fn(
39
+ Router.matchRequest = vi.fn(
39
40
  () =>
40
41
  ({
41
42
  /* match */
@@ -71,7 +72,7 @@ describe('ServerRouteHandler', () => {
71
72
  hmrManager: HmrManager,
72
73
  });
73
74
 
74
- Router.match = vi.fn(() => ({}) as any);
75
+ Router.matchRequest = vi.fn(() => ({}) as any);
75
76
  FileSystemResponseMatcher.handleMatch = vi.fn(() =>
76
77
  Promise.resolve(
77
78
  new Response('<html><body></body></html>', { headers: { 'Content-Type': 'text/html' } }),
@@ -1,5 +1,5 @@
1
1
  import type { IHmrManager } from '../../types/public-types.ts';
2
- import type { FSRouter } from '../../router/server/fs-router.ts';
2
+ import type { RouteRegistry } from '../../router/server/route-registry.ts';
3
3
  import type { ExplicitStaticRouteMatcher } from './explicit-static-route-matcher.ts';
4
4
  import type { FileSystemResponseMatcher } from './fs-server-response-matcher.ts';
5
5
  import { appLogger } from '../../global/app-logger.ts';
@@ -11,7 +11,7 @@ import { injectHmrRuntimeIntoHtmlResponse, isHtmlResponse, shouldInjectHmrHtmlRe
11
11
  */
12
12
  export interface ServerRouteHandlerParams {
13
13
  /** File system router for matching request URLs to route handlers. */
14
- router: FSRouter;
14
+ router: RouteRegistry;
15
15
  /** Matcher for handling file system route responses. */
16
16
  fileSystemResponseMatcher: FileSystemResponseMatcher;
17
17
  /** Optional matcher for explicit static routes like processed images or sitemaps. */
@@ -33,7 +33,7 @@ export interface ServerRouteHandlerParams {
33
33
  * In development mode, it also injects HMR scripts into HTML responses.
34
34
  */
35
35
  export class ServerRouteHandler {
36
- private readonly router: FSRouter;
36
+ private readonly router: RouteRegistry;
37
37
  private readonly fileSystemResponseMatcher: FileSystemResponseMatcher;
38
38
  private readonly explicitStaticRouteMatcher?: ExplicitStaticRouteMatcher;
39
39
  private readonly watch: boolean;
@@ -98,7 +98,7 @@ export class ServerRouteHandler {
98
98
  return this.maybeInjectHmrScript(response);
99
99
  }
100
100
 
101
- const fsMatch = !pathname.includes('.') && this.router.match(request.url);
101
+ const fsMatch = !pathname.includes('.') && this.router.matchRequest(request.url);
102
102
 
103
103
  const response = await (fsMatch
104
104
  ? this.fileSystemResponseMatcher.handleMatch(fsMatch, request)
@@ -8,8 +8,8 @@ import {
8
8
  } from './server-static-builder';
9
9
  import type { EcoPagesAppConfig } from '../../types/internal-types';
10
10
  import type { StaticSiteGenerator } from '../../static-site-generator/static-site-generator';
11
- import type { FSRouter } from '../../router/server/fs-router';
12
- import type { RouteRendererFactory } from '../../route-renderer/route-renderer';
11
+ import type { RouteRegistry } from '../../router/server/route-registry';
12
+ import type { StaticGenerationRendererResolver } from '../../route-renderer/route-renderer';
13
13
  import fs from 'node:fs';
14
14
  import path from 'node:path';
15
15
  import os from 'node:os';
@@ -68,8 +68,8 @@ function createMockDependencies() {
68
68
  port: 3000,
69
69
  };
70
70
 
71
- const Router = {} as FSRouter;
72
- const RouteRendererFactory = {} as RouteRendererFactory;
71
+ const Router = {} as RouteRegistry;
72
+ const RouteRendererFactory = {} as StaticGenerationRendererResolver;
73
73
  const logger: ServerStaticBuilderLogger = {
74
74
  warn: (message: string, detail?: string) => {
75
75
  calls.warn.push([message, detail]);
@@ -5,9 +5,9 @@ import { StaticContentServer } from '../../dev/sc-server.ts';
5
5
  import { appLogger } from '../../global/app-logger.ts';
6
6
  import type { EcoPagesAppConfig } from '../../types/internal-types.ts';
7
7
  import type { ApiHandler, StaticRoute } from '../../types/public-types.ts';
8
- import type { RouteRendererFactory } from '../../route-renderer/route-renderer.ts';
9
- import type { FSRouter } from '../../router/server/fs-router.ts';
8
+ import type { RouteRegistry } from '../../router/server/route-registry.ts';
10
9
  import type { StaticSiteGenerator } from '../../static-site-generator/static-site-generator.ts';
10
+ import type { StaticGenerationRendererResolver } from '../../route-renderer/route-renderer.ts';
11
11
 
12
12
  export interface StaticBuildOptions {
13
13
  preview?: boolean;
@@ -128,8 +128,8 @@ export class ServerStaticBuilder {
128
128
  async build(
129
129
  options: StaticBuildOptions | undefined,
130
130
  dependencies: {
131
- router: FSRouter;
132
- routeRendererFactory: RouteRendererFactory;
131
+ router: RouteRegistry;
132
+ routeRendererFactory: StaticGenerationRendererResolver;
133
133
  staticRoutes?: StaticRoute[];
134
134
  },
135
135
  ): Promise<void> {
@@ -12,7 +12,7 @@ It is responsible for:
12
12
 
13
13
  - validating integration, processor, and loader registration
14
14
  - resolving semantic paths such as `html` and `404` templates
15
- - selecting explicit build ownership and creating app-owned runtime state such as the build adapter, build executor, build manifest, dev graph service, runtime specifier registry, and remaining compatibility-only runtime state
15
+ - selecting explicit build ownership and creating app-owned runtime state such as the build adapter, build executor, build manifest, dev graph service, and remaining compatibility-only runtime state
16
16
  - enforcing runtime capability requirements before startup
17
17
  - carrying host-injected runtime dependencies only through abstract slots such as host module loaders, never through bundler-specific core defaults
18
18
 
@@ -67,7 +67,6 @@ describe('EcoConfigBuilder', () => {
67
67
  expect(createVitePluginsFromAppSourceTransforms(config).length).toBeGreaterThan(0);
68
68
  expect(config.runtime?.serverInvalidationState).toBeDefined();
69
69
  expect(config.runtime?.entrypointDependencyGraph).toBeDefined();
70
- expect(config.runtime?.runtimeSpecifierRegistry).toBeDefined();
71
70
  });
72
71
 
73
72
  test('should allow explicit Vite-host build ownership during config build', async () => {
@@ -26,7 +26,7 @@ import { ghtmlPlugin } from '../integrations/ghtml/ghtml.plugin.ts';
26
26
  import type { EcoPagesAppConfig, RobotsPreference } from '../types/internal-types.ts';
27
27
  import { createEcoComponentMetaPlugin } from '../plugins/eco-component-meta-plugin.ts';
28
28
  import { createEcoComponentMetaTransform } from '../plugins/eco-component-meta-plugin.ts';
29
- import type { IntegrationPlugin } from '../plugins/integration-plugin.ts';
29
+ import type { AnyIntegrationPlugin } from '../plugins/integration-plugin.ts';
30
30
  import type { Processor } from '../plugins/processor.ts';
31
31
  import type { EcoSourceTransform } from '../plugins/source-transform.ts';
32
32
  import type { RuntimeCapabilityDeclaration, RuntimeCapabilityTag } from '../plugins/runtime-capability.ts';
@@ -36,10 +36,6 @@ import {
36
36
  NoopEntrypointDependencyGraph,
37
37
  setAppEntrypointDependencyGraph,
38
38
  } from '../services/runtime-state/entrypoint-dependency-graph.service.ts';
39
- import {
40
- InMemoryRuntimeSpecifierRegistry,
41
- setAppRuntimeSpecifierRegistry,
42
- } from '../services/runtime-state/runtime-specifier-registry.service.ts';
43
39
  import {
44
40
  CounterServerInvalidationState,
45
41
  setAppServerInvalidationState,
@@ -283,7 +279,7 @@ export class ConfigBuilder {
283
279
  * @param integrations - An array of integration plugins
284
280
  * @returns The ConfigBuilder instance for method chaining
285
281
  */
286
- setIntegrations(integrations: IntegrationPlugin<unknown>[]): this {
282
+ setIntegrations(integrations: AnyIntegrationPlugin[]): this {
287
283
  this.config.integrations = integrations;
288
284
  return this;
289
285
  }
@@ -727,7 +723,6 @@ export class ConfigBuilder {
727
723
  updateAppBuildManifest(this.config, await collectConfiguredAppBuildManifestContributions(this.config));
728
724
  setAppServerInvalidationState(this.config, new CounterServerInvalidationState());
729
725
  setAppEntrypointDependencyGraph(this.config, new NoopEntrypointDependencyGraph());
730
- setAppRuntimeSpecifierRegistry(this.config, new InMemoryRuntimeSpecifierRegistry());
731
726
  setAppBuildExecutor(
732
727
  this.config,
733
728
  createAppBuildExecutor({