@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,10 +2,31 @@ import path from 'node:path';
2
2
  import { appLogger } from '../global/app-logger.ts';
3
3
  import type { EcoPagesAppConfig } from '../types/internal-types.ts';
4
4
  import type { EcoPageComponent, StaticRoute } from '../types/public-types.ts';
5
- import type { RouteRendererFactory } from '../route-renderer/route-renderer.ts';
6
- import type { FSRouter } from '../router/server/fs-router.ts';
5
+ import type {
6
+ ExplicitViewRenderer,
7
+ ExplicitViewRendererResolver,
8
+ PageRendererResolver,
9
+ StaticGenerationRendererResolver,
10
+ } from '../route-renderer/route-renderer.ts';
11
+ import type { StaticGenerationRoute } from '../router/server/route-registry.ts';
7
12
  import { fileSystem } from '@ecopages/file-system';
8
13
  import { PathUtils } from '../utils/path-utils.module.ts';
14
+ import { prepareExplicitStaticRender } from '../adapters/shared/explicit-static-render-preparation.ts';
15
+
16
+ type StaticGenerationRouteSource = {
17
+ listStaticGenerationRoutes(input: { runtimeOrigin: string }): Promise<readonly StaticGenerationRoute[]>;
18
+ };
19
+
20
+ type StaticPageRouteRendererFactory = PageRendererResolver;
21
+
22
+ type ExplicitStaticRouteRendererFactory = ExplicitViewRendererResolver;
23
+
24
+ type StaticGenerationRendererFactory = StaticGenerationRendererResolver;
25
+
26
+ type ExplicitStaticRouteEntry = {
27
+ pathname: string;
28
+ params: Record<string, string | string[]>;
29
+ };
9
30
 
10
31
  export const STATIC_SITE_GENERATOR_ERRORS = {
11
32
  ROUTE_RENDERER_FACTORY_REQUIRED: 'RouteRendererFactory is required for render strategy',
@@ -56,9 +77,9 @@ export class StaticSiteGenerator {
56
77
  */
57
78
  private async shouldSkipStaticPageFile(
58
79
  filePath: string,
59
- routeRendererFactory: RouteRendererFactory,
80
+ routeRendererFactory: StaticPageRouteRendererFactory,
60
81
  ): Promise<boolean> {
61
- const module = (await routeRendererFactory.createRenderer(filePath).loadPageModule(filePath, {
82
+ const module = (await routeRendererFactory.getPageRenderer(filePath).loadPageModule(filePath, {
62
83
  cacheScope: 'static-page-probe',
63
84
  })) as {
64
85
  default?: EcoPageComponent<any>;
@@ -131,27 +152,65 @@ export class StaticSiteGenerator {
131
152
  return Array.from(directories);
132
153
  }
133
154
 
134
- /**
135
- * Extracts dynamic parameters from the actual path based on the template path.
136
- *
137
- * @param templatePath - The template path (e.g., "/blog/[slug]")
138
- * @param actualPath - The actual path (e.g., "/blog/my-post")
139
- * @returns A record of extracted parameters (e.g., { slug: "my-post" })
140
- */
141
- private extractParams(templatePath: string, actualPath: string): Record<string, string> {
142
- const templateSegments = templateSegmentsFromPath(templatePath);
143
- const actualSegments = templateSegmentsFromPath(actualPath);
144
- const params: Record<string, string> = {};
145
-
146
- for (let i = 0; i < templateSegments.length; i++) {
147
- const segment = templateSegments[i];
148
- if (segment.startsWith('[') && segment.endsWith(']')) {
149
- const paramName = segment.slice(1, -1).replace('...', '');
150
- params[paramName] = actualSegments[i];
155
+ private writeStaticOutput(routePath: string, contents: string | Buffer, directories: string[] = []): string {
156
+ const outputPath = this.getOutputPath(routePath, directories);
157
+ fileSystem.ensureDir(path.dirname(outputPath));
158
+ fileSystem.write(outputPath, contents);
159
+ return outputPath;
160
+ }
161
+
162
+ private getStaticBuildStrategy(filePath: string): 'fetch' | 'render' {
163
+ const ext = PathUtils.getEcoTemplateExtension(filePath);
164
+ const integration = this.appConfig.integrations.find((plugin) => plugin.extensions.includes(ext));
165
+ return integration?.staticBuildStep || 'render';
166
+ }
167
+
168
+ private async createFilesystemStaticContents(
169
+ route: StaticGenerationRoute,
170
+ baseUrl: string,
171
+ routeRendererFactory?: StaticPageRouteRendererFactory,
172
+ ): Promise<string | Buffer | null> {
173
+ const {
174
+ templateRoute: { filePath },
175
+ params,
176
+ } = route;
177
+
178
+ if (this.getStaticBuildStrategy(filePath) === 'fetch') {
179
+ const fetchUrl = this.resolveStaticFetchUrl(route.requestUrl, baseUrl);
180
+ const response = await fetch(fetchUrl);
181
+
182
+ if (!response.ok) {
183
+ appLogger.error(`Failed to fetch ${fetchUrl}. Status: ${response.status}`);
184
+ return null;
151
185
  }
186
+
187
+ return response.text();
188
+ }
189
+
190
+ if (!routeRendererFactory) {
191
+ throw new Error(STATIC_SITE_GENERATOR_ERRORS.ROUTE_RENDERER_FACTORY_REQUIRED);
192
+ }
193
+
194
+ if (await this.shouldSkipStaticPageFile(filePath, routeRendererFactory)) {
195
+ return null;
196
+ }
197
+
198
+ const renderer = routeRendererFactory.getPageRenderer(filePath);
199
+ const result = await renderer.execute({
200
+ file: filePath,
201
+ params: params as Record<string, string>,
202
+ });
203
+
204
+ const body = result.body;
205
+ if (typeof body === 'string' || Buffer.isBuffer(body)) {
206
+ return body;
152
207
  }
153
208
 
154
- return params;
209
+ if (body instanceof ReadableStream) {
210
+ return new Response(body).text();
211
+ }
212
+
213
+ throw new Error(STATIC_SITE_GENERATOR_ERRORS.unsupportedBodyType(typeof body));
155
214
  }
156
215
 
157
216
  /**
@@ -162,95 +221,31 @@ export class StaticSiteGenerator {
162
221
  * issuing a request against the running server origin. Render-strategy routes
163
222
  * go through the normal route renderer directly.
164
223
  */
165
- async generateStaticPages(router: FSRouter, baseUrl: string, routeRendererFactory?: RouteRendererFactory) {
166
- const routes = Object.keys(router.routes).filter((route) => !route.includes('['));
224
+ async generateStaticPages(
225
+ router: StaticGenerationRouteSource,
226
+ baseUrl: string,
227
+ routeRendererFactory?: StaticPageRouteRendererFactory,
228
+ ) {
229
+ const routes = await router.listStaticGenerationRoutes({ runtimeOrigin: baseUrl });
167
230
 
168
- appLogger.debug('Static Pages', routes);
169
-
170
- const directories = this.getDirectories(routes);
231
+ appLogger.debug(
232
+ 'Static Pages',
233
+ routes.map((route) => route.requestUrl),
234
+ );
171
235
 
172
- for (const directory of directories) {
173
- fileSystem.ensureDir(path.join(this.getExportDir(), directory));
174
- }
236
+ const directories = this.getDirectories(routes.map((route) => route.requestUrl));
175
237
 
176
238
  for (const route of routes) {
177
239
  try {
178
- const { filePath, pathname: routePathname } = router.routes[route];
179
-
180
- const ext = PathUtils.getEcoTemplateExtension(filePath);
181
- const integration = this.appConfig.integrations.find((plugin) => plugin.extensions.includes(ext));
182
- const strategy = integration?.staticBuildStep || 'render';
183
-
184
- let contents: string | Buffer;
185
-
186
- if (strategy === 'fetch') {
187
- const fetchUrl = this.resolveStaticFetchUrl(route, baseUrl);
188
- const response = await fetch(fetchUrl);
189
-
190
- if (!response.ok) {
191
- appLogger.error(`Failed to fetch ${fetchUrl}. Status: ${response.status}`);
192
- continue;
193
- }
194
- contents = await response.text();
195
- } else {
196
- if (!routeRendererFactory) {
197
- throw new Error(STATIC_SITE_GENERATOR_ERRORS.ROUTE_RENDERER_FACTORY_REQUIRED);
198
- }
199
-
200
- if (await this.shouldSkipStaticPageFile(filePath, routeRendererFactory)) {
201
- continue;
202
- }
203
-
204
- let pathname = routePathname;
205
- const pathnameSegments = pathname.split('/').filter(Boolean);
206
-
207
- if (pathname === '/') {
208
- pathname = '/index.html';
209
- } else if (pathnameSegments.join('/').includes('[')) {
210
- pathname = `${route.replace(router.origin, '')}.html`;
211
- } else if (pathnameSegments.length >= 1 && directories.includes(`/${pathnameSegments.join('/')}`)) {
212
- pathname = `${pathname.endsWith('/') ? pathname : `${pathname}/`}index.html`;
213
- } else {
214
- pathname += '.html';
215
- }
216
-
217
- const renderer = routeRendererFactory.createRenderer(filePath);
218
- const params = this.extractParams(routePathname, pathname.replace('.html', ''));
219
-
220
- const result = await renderer.createRoute({
221
- file: filePath,
222
- params,
223
- });
224
-
225
- const body = result.body;
226
-
227
- if (typeof body === 'string' || Buffer.isBuffer(body)) {
228
- contents = body;
229
- } else if (body instanceof ReadableStream) {
230
- contents = await new Response(body).text();
231
- } else {
232
- throw new Error(STATIC_SITE_GENERATOR_ERRORS.unsupportedBodyType(typeof body));
233
- }
234
- }
235
-
236
- let pathname = routePathname;
237
- const pathnameSegments = pathname.split('/').filter(Boolean);
238
-
239
- if (pathname === '/') {
240
- pathname = '/index.html';
241
- } else if (pathnameSegments.join('/').includes('[')) {
242
- pathname = `${route.replace(router.origin, '')}.html`;
243
- } else if (pathnameSegments.length >= 1 && directories.includes(`/${pathnameSegments.join('/')}`)) {
244
- pathname = `${pathname.endsWith('/') ? pathname : `${pathname}/`}index.html`;
245
- } else {
246
- pathname += '.html';
240
+ const contents = await this.createFilesystemStaticContents(route, baseUrl, routeRendererFactory);
241
+ if (contents === null) {
242
+ continue;
247
243
  }
248
244
 
249
- const outputPath = path.join(this.getExportDir(), pathname);
250
- fileSystem.write(outputPath, contents);
245
+ this.writeStaticOutput(route.pathname, contents, directories);
251
246
  } catch (error) {
252
247
  appLogger.error(
253
- `Error generating static page for ${route}:`,
248
+ `Error generating static page for ${route.requestUrl}:`,
254
249
  error instanceof Error ? error : String(error),
255
250
  );
256
251
  }
@@ -280,9 +275,9 @@ export class StaticSiteGenerator {
280
275
  routeRendererFactory,
281
276
  staticRoutes,
282
277
  }: {
283
- router: FSRouter;
278
+ router: StaticGenerationRouteSource;
284
279
  baseUrl: string;
285
- routeRendererFactory?: RouteRendererFactory;
280
+ routeRendererFactory?: StaticGenerationRendererFactory;
286
281
  staticRoutes?: StaticRoute[];
287
282
  }) {
288
283
  this.generateRobotsTxt();
@@ -299,7 +294,7 @@ export class StaticSiteGenerator {
299
294
  */
300
295
  private async generateExplicitStaticPages(
301
296
  staticRoutes: StaticRoute[],
302
- routeRendererFactory: RouteRendererFactory,
297
+ routeRendererFactory: ExplicitStaticRouteRendererFactory,
303
298
  ): Promise<void> {
304
299
  appLogger.debug(
305
300
  'Generating explicit static routes',
@@ -314,13 +309,7 @@ export class StaticSiteGenerator {
314
309
  continue;
315
310
  }
316
311
 
317
- const isDynamic = route.path.includes(':') || route.path.includes('[');
318
-
319
- if (isDynamic) {
320
- await this.generateDynamicStaticRoute(route.path, view, routeRendererFactory);
321
- } else {
322
- await this.generateSingleStaticRoute(route.path, view, routeRendererFactory);
323
- }
312
+ await this.generateExplicitStaticRoute(route.path, view, routeRendererFactory);
324
313
  } catch (error) {
325
314
  appLogger.error(
326
315
  `Error generating explicit static page for ${route.path}:`,
@@ -330,64 +319,81 @@ export class StaticSiteGenerator {
330
319
  }
331
320
  }
332
321
 
333
- /**
334
- * Generate a single static page for a non-dynamic route.
335
- */
336
- private async generateSingleStaticRoute(
322
+ private async generateExplicitStaticRoute(
337
323
  routePath: string,
338
324
  view: EcoPageComponent<any>,
339
- routeRendererFactory: RouteRendererFactory,
325
+ routeRendererFactory: ExplicitStaticRouteRendererFactory,
340
326
  ): Promise<void> {
341
- const integrationName = view.config?.__eco?.integration;
342
- if (!integrationName) {
343
- throw new Error(STATIC_SITE_GENERATOR_ERRORS.missingIntegration(routePath));
344
- }
327
+ const { renderer, routeEntries } = await this.planExplicitStaticRoute(routePath, view, routeRendererFactory);
345
328
 
346
- const renderer = routeRendererFactory.getRendererByIntegration(integrationName);
347
- if (!renderer) {
348
- throw new Error(STATIC_SITE_GENERATOR_ERRORS.noRendererForIntegration(integrationName));
349
- }
329
+ for (const { pathname, params } of routeEntries) {
330
+ const contents = await this.createExplicitStaticContents(
331
+ routePath,
332
+ view,
333
+ params,
334
+ routeRendererFactory,
335
+ renderer,
336
+ );
350
337
 
351
- const props = view.staticProps
352
- ? (
353
- await view.staticProps({
354
- pathname: { params: {} },
355
- appConfig: this.appConfig,
356
- runtimeOrigin: this.appConfig.baseUrl,
357
- })
358
- ).props
359
- : {};
338
+ const outputPath = this.writeStaticOutput(pathname, contents);
360
339
 
361
- const response = await renderer.renderToResponse(view, props, {});
362
- const contents = await response.text();
340
+ appLogger.debug(`Generated static page: ${pathname} -> ${outputPath}`);
341
+ }
342
+ }
363
343
 
364
- const outputPath = this.getOutputPath(routePath);
365
- fileSystem.ensureDir(path.dirname(outputPath));
366
- fileSystem.write(outputPath, contents);
344
+ private async planExplicitStaticRoute(
345
+ routePath: string,
346
+ view: EcoPageComponent<any>,
347
+ routeRendererFactory: ExplicitStaticRouteRendererFactory,
348
+ ): Promise<{ renderer: ExplicitViewRenderer; routeEntries: ExplicitStaticRouteEntry[] }> {
349
+ const { renderer } = await prepareExplicitStaticRender({
350
+ routePath,
351
+ view,
352
+ params: {},
353
+ appConfig: this.appConfig,
354
+ runtimeOrigin: this.appConfig.baseUrl,
355
+ routeRendererFactory,
356
+ errors: STATIC_SITE_GENERATOR_ERRORS,
357
+ });
367
358
 
368
- appLogger.debug(`Generated static page: ${routePath} -> ${outputPath}`);
359
+ return {
360
+ renderer,
361
+ routeEntries: await this.listExplicitStaticRouteEntries(routePath, view),
362
+ };
369
363
  }
370
364
 
371
- /**
372
- * Generate static pages for a dynamic route using staticPaths.
373
- */
374
- private async generateDynamicStaticRoute(
365
+ private async createExplicitStaticContents(
375
366
  routePath: string,
376
367
  view: EcoPageComponent<any>,
377
- routeRendererFactory: RouteRendererFactory,
378
- ): Promise<void> {
379
- if (!view.staticPaths) {
380
- throw new Error(STATIC_SITE_GENERATOR_ERRORS.dynamicRouteRequiresStaticPaths(routePath));
381
- }
368
+ params: Record<string, string | string[]>,
369
+ routeRendererFactory: ExplicitStaticRouteRendererFactory,
370
+ renderer: ExplicitViewRenderer,
371
+ ): Promise<string> {
372
+ const { props, view: renderableView } = await prepareExplicitStaticRender({
373
+ routePath,
374
+ view,
375
+ params,
376
+ appConfig: this.appConfig,
377
+ runtimeOrigin: this.appConfig.baseUrl,
378
+ routeRendererFactory,
379
+ errors: STATIC_SITE_GENERATOR_ERRORS,
380
+ });
381
+
382
+ const response = await renderer.renderToResponse(renderableView, props, {});
383
+ return response.text();
384
+ }
382
385
 
383
- const integrationName = view.config?.__eco?.integration;
384
- if (!integrationName) {
385
- throw new Error(STATIC_SITE_GENERATOR_ERRORS.missingIntegration(routePath));
386
+ private async listExplicitStaticRouteEntries(
387
+ routePath: string,
388
+ view: EcoPageComponent<any>,
389
+ ): Promise<ExplicitStaticRouteEntry[]> {
390
+ const isDynamic = routePath.includes(':') || routePath.includes('[');
391
+ if (!isDynamic) {
392
+ return [{ pathname: routePath, params: {} }];
386
393
  }
387
394
 
388
- const renderer = routeRendererFactory.getRendererByIntegration(integrationName);
389
- if (!renderer) {
390
- throw new Error(STATIC_SITE_GENERATOR_ERRORS.noRendererForIntegration(integrationName));
395
+ if (!view.staticPaths) {
396
+ throw new Error(STATIC_SITE_GENERATOR_ERRORS.dynamicRouteRequiresStaticPaths(routePath));
391
397
  }
392
398
 
393
399
  const { paths } = await view.staticPaths({
@@ -395,28 +401,10 @@ export class StaticSiteGenerator {
395
401
  runtimeOrigin: this.appConfig.baseUrl,
396
402
  });
397
403
 
398
- for (const { params } of paths) {
399
- const resolvedPath = this.resolveRoutePath(routePath, params);
400
-
401
- const props = view.staticProps
402
- ? (
403
- await view.staticProps({
404
- pathname: { params },
405
- appConfig: this.appConfig,
406
- runtimeOrigin: this.appConfig.baseUrl,
407
- })
408
- ).props
409
- : {};
410
-
411
- const response = await renderer.renderToResponse(view, props, {});
412
- const contents = await response.text();
413
-
414
- const outputPath = this.getOutputPath(resolvedPath);
415
- fileSystem.ensureDir(path.dirname(outputPath));
416
- fileSystem.write(outputPath, contents);
417
-
418
- appLogger.debug(`Generated static page: ${resolvedPath} -> ${outputPath}`);
419
- }
404
+ return paths.map(({ params }) => ({
405
+ pathname: this.resolveRoutePath(routePath, params),
406
+ params,
407
+ }));
420
408
  }
421
409
 
422
410
  /**
@@ -439,11 +427,13 @@ export class StaticSiteGenerator {
439
427
  /**
440
428
  * Get the output file path for a given route.
441
429
  */
442
- private getOutputPath(routePath: string): string {
430
+ private getOutputPath(routePath: string, directories: string[] = []): string {
443
431
  let outputName: string;
444
432
 
445
433
  if (routePath === '/') {
446
434
  outputName = 'index.html';
435
+ } else if (directories.includes(routePath)) {
436
+ outputName = `${routePath}/index.html`;
447
437
  } else if (routePath.endsWith('/')) {
448
438
  outputName = `${routePath}index.html`;
449
439
  } else {
@@ -453,10 +443,3 @@ export class StaticSiteGenerator {
453
443
  return path.join(this.getExportDir(), outputName);
454
444
  }
455
445
  }
456
-
457
- /**
458
- * Splits a path into segments, filtering out empty strings.
459
- */
460
- function templateSegmentsFromPath(path: string) {
461
- return path.split('/').filter(Boolean);
462
- }
@@ -1,17 +1,16 @@
1
1
  import type { EcoBuildPlugin } from '../build/build-types.ts';
2
2
  import type { AppBuildManifest } from '../build/build-manifest.ts';
3
3
  import type { BuildAdapter, BuildExecutor, BuildOwnership } from '../build/build-adapter.ts';
4
- import type { IntegrationPlugin } from '../plugins/integration-plugin.ts';
4
+ import type { AnyIntegrationPlugin } from '../plugins/integration-plugin.ts';
5
5
  import type { Processor } from '../plugins/processor.ts';
6
6
  import type { EcoSourceTransform } from '../plugins/source-transform.ts';
7
7
  import type { PageMetadataProps } from './public-types.ts';
8
- import type { FSRouter } from '../router/server/fs-router.ts';
8
+ import type { RouteRegistry } from '../router/server/route-registry.ts';
9
9
  import type { CacheConfig } from '../services/cache/cache.types.ts';
10
10
  import type { DevGraphService } from '../services/runtime-state/dev-graph.service.ts';
11
11
  import type { AppModuleLoader } from '../services/module-loading/app-module-loader.service.ts';
12
12
  import type { SourceModuleLoader } from '../services/module-loading/module-loading-types.ts';
13
13
  import type { EntrypointDependencyGraph } from '../services/runtime-state/entrypoint-dependency-graph.service.ts';
14
- import type { RuntimeSpecifierRegistry } from '../services/runtime-state/runtime-specifier-registry.service.ts';
15
14
  import type { ServerInvalidationState } from '../services/runtime-state/server-invalidation-state.service.ts';
16
15
  import type { ServerModuleTranspiler } from '../services/module-loading/server-module-transpiler.service.ts';
17
16
 
@@ -104,7 +103,7 @@ export type EcoPagesAppConfig = {
104
103
  */
105
104
  defaultMetadata: PageMetadataProps;
106
105
  /** Integrations plugins */
107
- integrations: IntegrationPlugin[];
106
+ integrations: AnyIntegrationPlugin[];
108
107
  /** Integrations dependencies */
109
108
  integrationsDependencies: IntegrationDependencyConfig[];
110
109
  /** Derived Paths */
@@ -157,7 +156,6 @@ export type EcoPagesAppConfig = {
157
156
  entrypointDependencyGraph?: EntrypointDependencyGraph;
158
157
  hostModuleLoader?: SourceModuleLoader;
159
158
  rendererModuleContext?: unknown;
160
- runtimeSpecifierRegistry?: RuntimeSpecifierRegistry;
161
159
  serverInvalidationState?: ServerInvalidationState;
162
160
  serverModuleTranspiler?: ServerModuleTranspiler;
163
161
  };
@@ -189,11 +187,14 @@ export type RouteKind = 'exact' | 'catch-all' | 'dynamic';
189
187
  * Represents the result of a route match.
190
188
  */
191
189
  export type MatchResult = {
192
- filePath: string;
193
- kind: RouteKind;
194
- pathname: string;
195
- query?: Record<string, string>;
196
- params?: Record<string, string | string[]>;
190
+ requestedPathname: string;
191
+ templateRoute: {
192
+ filePath: string;
193
+ kind: RouteKind;
194
+ pathname: string;
195
+ };
196
+ query: Record<string, string>;
197
+ params: Record<string, string | string[]>;
197
198
  };
198
199
 
199
200
  /**
@@ -224,10 +225,10 @@ export type FileSystemServerOptions = {
224
225
  export interface EcoPagesFileSystemServerAdapter<ServerInstanceOptions = unknown> {
225
226
  startServer(serverOptions: ServerInstanceOptions):
226
227
  | {
227
- router: FSRouter;
228
+ router: RouteRegistry;
228
229
  server: unknown;
229
230
  }
230
- | Promise<{ router: FSRouter; server: unknown }>;
231
+ | Promise<{ router: RouteRegistry; server: unknown }>;
231
232
  }
232
233
 
233
234
  export interface ProcessorPlugin {