@ecopages/core 0.2.0-beta.27 → 0.2.0-beta.29

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 (103) hide show
  1. package/README.md +1 -1
  2. package/package.json +42 -26
  3. package/src/adapters/abstract/application-adapter.d.ts +22 -4
  4. package/src/adapters/abstract/application-adapter.js +27 -4
  5. package/src/adapters/abstract/server-adapter.d.ts +10 -0
  6. package/src/adapters/bun/create-app.d.ts +1 -0
  7. package/src/adapters/bun/create-app.js +11 -4
  8. package/src/adapters/bun/server-adapter.js +1 -0
  9. package/src/adapters/node/create-app.d.ts +1 -0
  10. package/src/adapters/node/create-app.js +10 -3
  11. package/src/adapters/node/server-adapter.js +1 -0
  12. package/src/adapters/shared/fs-server-response-factory.d.ts +5 -0
  13. package/src/adapters/shared/fs-server-response-factory.js +20 -0
  14. package/src/adapters/shared/fs-server-response-matcher.d.ts +18 -0
  15. package/src/adapters/shared/fs-server-response-matcher.js +75 -15
  16. package/src/config/config-builder.d.ts +12 -2
  17. package/src/config/config-builder.js +41 -2
  18. package/src/config/constants.d.ts +1 -0
  19. package/src/config/constants.js +2 -1
  20. package/src/eco/eco.js +2 -2
  21. package/src/eco/eco.utils.d.ts +1 -1
  22. package/src/eco/eco.utils.js +1 -1
  23. package/src/hmr/client/hmr-runtime.js +1 -1
  24. package/src/hmr/hmr-file-change-prep.d.ts +1 -1
  25. package/src/hmr/hmr-file-change-prep.js +1 -1
  26. package/src/plugins/processor.d.ts +8 -0
  27. package/src/route-renderer/GRAPH.md +11 -11
  28. package/src/route-renderer/README.md +11 -6
  29. package/src/route-renderer/orchestration/{document-shell-render.service.d.ts → document-shell/document-shell-render.service.d.ts} +19 -2
  30. package/src/route-renderer/orchestration/{document-shell-render.service.js → document-shell/document-shell-render.service.js} +28 -0
  31. package/src/route-renderer/orchestration/{layout-shell-props.service.d.ts → document-shell/layout-shell-props.service.d.ts} +1 -1
  32. package/src/route-renderer/orchestration/{component-render-context.d.ts → foreign-child/component-render-context.d.ts} +1 -1
  33. package/src/route-renderer/orchestration/{component-render-context.js → foreign-child/component-render-context.js} +1 -1
  34. package/src/route-renderer/orchestration/{render-output.utils.d.ts → foreign-child/foreign-child-output.utils.d.ts} +1 -7
  35. package/src/route-renderer/orchestration/{render-output.utils.js → foreign-child/foreign-child-output.utils.js} +1 -26
  36. package/src/route-renderer/orchestration/{foreign-subtree-execution.service.d.ts → foreign-child/foreign-subtree-execution.service.d.ts} +5 -5
  37. package/src/route-renderer/orchestration/{foreign-subtree-execution.service.js → foreign-child/foreign-subtree-execution.service.js} +2 -2
  38. package/src/route-renderer/orchestration/foreign-child/owning-renderer-resolution.d.ts +19 -0
  39. package/src/route-renderer/orchestration/foreign-child/owning-renderer-resolution.js +35 -0
  40. package/src/route-renderer/orchestration/integration-renderer.d.ts +4 -46
  41. package/src/route-renderer/orchestration/integration-renderer.js +53 -157
  42. package/src/route-renderer/orchestration/integration-renderer.test-fixtures.d.ts +75 -0
  43. package/src/route-renderer/orchestration/integration-renderer.test-fixtures.js +177 -0
  44. package/src/route-renderer/orchestration/{component-graph-collectors.d.ts → ownership-graph/component-graph-collectors.d.ts} +4 -4
  45. package/src/route-renderer/orchestration/{component-graph-collectors.js → ownership-graph/component-graph-collectors.js} +1 -1
  46. package/src/route-renderer/orchestration/{component-graph.d.ts → ownership-graph/component-graph.d.ts} +1 -3
  47. package/src/route-renderer/orchestration/{component-graph.js → ownership-graph/component-graph.js} +0 -2
  48. package/src/route-renderer/orchestration/{ownership-validation.service.d.ts → ownership-graph/ownership-validation.service.d.ts} +2 -2
  49. package/src/route-renderer/orchestration/{ownership-validation.service.js → ownership-graph/ownership-validation.service.js} +3 -4
  50. package/src/route-renderer/orchestration/{global-injector-assets.service.d.ts → page-browser-graph/global-injector-assets.service.d.ts} +3 -3
  51. package/src/route-renderer/orchestration/{global-injector-assets.service.js → page-browser-graph/global-injector-assets.service.js} +2 -2
  52. package/src/route-renderer/orchestration/{page-browser-graph-contribution.loader.d.ts → page-browser-graph/page-browser-graph-contribution.loader.d.ts} +1 -1
  53. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph-merge.utils.d.ts +12 -0
  54. package/src/route-renderer/orchestration/page-browser-graph/page-browser-graph-merge.utils.js +29 -0
  55. package/src/route-renderer/orchestration/{page-browser-graph-session.d.ts → page-browser-graph/page-browser-graph-session.d.ts} +3 -3
  56. package/src/route-renderer/orchestration/{page-browser-graph-session.js → page-browser-graph/page-browser-graph-session.js} +1 -1
  57. package/src/route-renderer/orchestration/{page-browser-graph.service.d.ts → page-browser-graph/page-browser-graph.service.d.ts} +3 -3
  58. package/src/route-renderer/orchestration/{page-browser-graph.service.js → page-browser-graph/page-browser-graph.service.js} +3 -3
  59. package/src/route-renderer/orchestration/{processed-asset-dedupe.d.ts → page-browser-graph/processed-asset-dedupe.d.ts} +1 -1
  60. package/src/route-renderer/orchestration/{integration-route-render-adapter.d.ts → route-pipeline/integration-route-render-adapter.d.ts} +3 -3
  61. package/src/route-renderer/orchestration/{integration-route-render-adapter.js → route-pipeline/integration-route-render-adapter.js} +1 -1
  62. package/src/route-renderer/orchestration/route-pipeline/marker-artifact.utils.d.ts +6 -0
  63. package/src/route-renderer/orchestration/route-pipeline/marker-artifact.utils.js +27 -0
  64. package/src/route-renderer/orchestration/route-pipeline/robots-meta.contribution.d.ts +15 -0
  65. package/src/route-renderer/orchestration/route-pipeline/robots-meta.contribution.js +36 -0
  66. package/src/route-renderer/orchestration/{route-html-finalization.service.d.ts → route-pipeline/route-html-finalization.service.d.ts} +6 -2
  67. package/src/route-renderer/orchestration/{route-html-finalization.service.js → route-pipeline/route-html-finalization.service.js} +5 -2
  68. package/src/route-renderer/orchestration/{route-prepared-options.builder.d.ts → route-pipeline/route-prepared-options.builder.d.ts} +3 -3
  69. package/src/route-renderer/orchestration/{route-prepared-options.builder.js → route-pipeline/route-prepared-options.builder.js} +8 -4
  70. package/src/route-renderer/orchestration/{route-prepared-options.utils.js → route-pipeline/route-prepared-options.utils.js} +1 -1
  71. package/src/route-renderer/orchestration/{route-render-orchestrator.d.ts → route-pipeline/route-render-orchestrator.d.ts} +6 -6
  72. package/src/route-renderer/orchestration/{route-render-orchestrator.js → route-pipeline/route-render-orchestrator.js} +8 -5
  73. package/src/route-renderer/orchestration/string-markup-renderer.js +1 -1
  74. package/src/services/html/html-transformer.service.js +1 -1
  75. package/src/services/invalidation/development-invalidation.service.d.ts +5 -0
  76. package/src/services/invalidation/development-invalidation.service.js +8 -9
  77. package/src/static-site-generator/README.md +9 -1
  78. package/src/static-site-generator/production-page-browser-graph-prebuild.js +1 -1
  79. package/src/static-site-generator/sitemap-routes.d.ts +14 -0
  80. package/src/static-site-generator/sitemap-routes.js +31 -0
  81. package/src/static-site-generator/sitemap.d.ts +15 -0
  82. package/src/static-site-generator/sitemap.js +33 -0
  83. package/src/static-site-generator/static-export-context.d.ts +9 -1
  84. package/src/static-site-generator/static-site-generator.d.ts +27 -6
  85. package/src/static-site-generator/static-site-generator.js +200 -50
  86. package/src/types/internal-types.d.ts +9 -2
  87. package/src/types/public-types.d.ts +78 -2
  88. package/src/utils/ecopages-route-info.d.ts +26 -0
  89. package/src/utils/ecopages-route-info.js +26 -0
  90. package/src/utils/html-escaping.d.ts +7 -0
  91. package/src/utils/html-escaping.js +5 -1
  92. package/src/utils/path-pattern.d.ts +23 -0
  93. package/src/utils/path-pattern.js +27 -0
  94. package/src/watchers/project-watcher.d.ts +3 -3
  95. package/src/watchers/project-watcher.js +4 -4
  96. package/src/route-renderer/orchestration/declared-ownership-graph.d.ts +0 -1
  97. package/src/route-renderer/orchestration/declared-ownership-graph.js +0 -8
  98. package/src/route-renderer/orchestration/template-serialization.d.ts +0 -38
  99. package/src/route-renderer/orchestration/template-serialization.js +0 -45
  100. /package/src/route-renderer/orchestration/{layout-shell-props.service.js → document-shell/layout-shell-props.service.js} +0 -0
  101. /package/src/route-renderer/orchestration/{page-browser-graph-contribution.loader.js → page-browser-graph/page-browser-graph-contribution.loader.js} +0 -0
  102. /package/src/route-renderer/orchestration/{processed-asset-dedupe.js → page-browser-graph/processed-asset-dedupe.js} +0 -0
  103. /package/src/route-renderer/orchestration/{route-prepared-options.utils.d.ts → route-pipeline/route-prepared-options.utils.d.ts} +0 -0
@@ -17,6 +17,11 @@ import {
17
17
  prebuildProductionPageBrowserGraphs,
18
18
  shouldPrebuildProductionPageBrowserGraphs
19
19
  } from "./production-page-browser-graph-prebuild.js";
20
+ import { PageModuleLoaderService } from "../route-renderer/page-loading/page-module-loader.js";
21
+ import { renderSitemap } from "./sitemap.js";
22
+ import { resolveSitemapLocations } from "./sitemap-routes.js";
23
+ import { toEcopagesRouteInfo } from "../utils/ecopages-route-info.js";
24
+ import { normalizePathname } from "../utils/path-pattern.js";
20
25
  const STATIC_SITE_GENERATOR_ERRORS = {
21
26
  ROUTE_RENDERER_FACTORY_REQUIRED: "RouteRendererFactory is required for render strategy",
22
27
  unsupportedBodyType: (bodyType) => `Unsupported body type for static generation: ${bodyType}`,
@@ -46,6 +51,7 @@ class StaticSiteGenerator {
46
51
  routeModuleBuildCacheOverride;
47
52
  staticRenderCacheContext;
48
53
  forceFullStaticGeneration = false;
54
+ pageModuleLoader;
49
55
  /**
50
56
  * Creates the static-site generator for one app config.
51
57
  */
@@ -64,8 +70,9 @@ class StaticSiteGenerator {
64
70
  return this.appConfig.absolutePaths?.distDir ?? path.join(this.appConfig.rootDir, this.appConfig.distDir);
65
71
  }
66
72
  async shouldSkipStaticPageFile(filePath, routeRendererFactory) {
67
- const module = await routeRendererFactory.getPageRenderer(filePath).loadPageModule(filePath);
68
- return module.default?.cache === "dynamic";
73
+ const pageModule = await routeRendererFactory.getPageRenderer(filePath).loadPageModule(filePath);
74
+ const Page = pageModule.default;
75
+ return Page.cache === "dynamic";
69
76
  }
70
77
  shouldSkipStaticView(_routePath, view) {
71
78
  return view.cache === "dynamic";
@@ -88,6 +95,14 @@ class StaticSiteGenerator {
88
95
  fileSystem.ensureDir(this.getExportDir());
89
96
  fileSystem.write(path.join(this.getExportDir(), "robots.txt"), data);
90
97
  }
98
+ /**
99
+ * Writes the configured sitemap file to the export directory.
100
+ */
101
+ generateSitemap(locations, sitemap) {
102
+ const fileName = sitemap.fileName ?? "sitemap.xml";
103
+ fileSystem.ensureDir(this.getExportDir());
104
+ fileSystem.write(path.join(this.getExportDir(), fileName), renderSitemap(locations));
105
+ }
91
106
  /**
92
107
  * Returns whether the input path points at the root directory.
93
108
  */
@@ -116,7 +131,6 @@ class StaticSiteGenerator {
116
131
  return outputPath;
117
132
  }
118
133
  async writeStaticPageArtifact(options) {
119
- options.activeStaticPathnames?.add(options.pathname);
120
134
  const directories = options.directories ?? this.getDirectories([options.pathname]);
121
135
  const renderedOutputPath = this.getOutputPath(options.pathname, directories);
122
136
  const routeModuleBuildCache = this.getRouteModuleBuildCache();
@@ -127,6 +141,8 @@ class StaticSiteGenerator {
127
141
  context: this.staticRenderCacheContext,
128
142
  force: this.forceFullStaticGeneration
129
143
  })) {
144
+ options.activeStaticPathnames?.add(options.pathname);
145
+ await options.onSuccessfulExport?.();
130
146
  appLogger.debug(`Skipped unchanged static page: ${options.debugLabel ?? options.pathname}`);
131
147
  return;
132
148
  }
@@ -135,15 +151,80 @@ class StaticSiteGenerator {
135
151
  return;
136
152
  }
137
153
  const outputPath = this.writeStaticOutput(options.pathname, contents, directories);
154
+ options.activeStaticPathnames?.add(options.pathname);
138
155
  if (options.sourceFile) {
139
- routeModuleBuildCache.recordStaticRender({
140
- filePath: options.sourceFile,
141
- pathname: options.pathname,
142
- sourceHash: fileSystem.hash(options.sourceFile),
143
- renderedOutputPath: outputPath,
144
- context: this.staticRenderCacheContext
145
- });
156
+ try {
157
+ routeModuleBuildCache.recordStaticRender({
158
+ filePath: options.sourceFile,
159
+ pathname: options.pathname,
160
+ sourceHash: fileSystem.hash(options.sourceFile),
161
+ renderedOutputPath: outputPath,
162
+ context: this.staticRenderCacheContext
163
+ });
164
+ } catch (error) {
165
+ appLogger.debug(
166
+ `Failed to record static render cache for ${options.pathname}`,
167
+ error instanceof Error ? error.message : String(error)
168
+ );
169
+ }
146
170
  }
171
+ await options.onSuccessfulExport?.();
172
+ }
173
+ /**
174
+ * Marks a successfully exported pathname as sitemap-eligible when robots allow indexing.
175
+ *
176
+ * @remarks
177
+ * Fail-closed: metadata resolution errors omit the URL. `robots.index === false`
178
+ * also omits it. Eligibility is decided during generation, not by reloading modules
179
+ * after `afterStaticExport`.
180
+ */
181
+ async considerSitemapEligibility(input) {
182
+ if (!input.sitemapEligiblePathnames || !this.appConfig.sitemap?.enabled) {
183
+ return;
184
+ }
185
+ try {
186
+ const metadata = await input.resolveMetadata();
187
+ if (metadata.robots?.index === false) {
188
+ return;
189
+ }
190
+ input.sitemapEligiblePathnames.add(normalizePathname(input.pathname));
191
+ } catch (error) {
192
+ appLogger.debug(
193
+ `Sitemap eligibility skipped for ${input.pathname}; metadata resolution failed`,
194
+ error instanceof Error ? error.message : String(error)
195
+ );
196
+ }
197
+ }
198
+ getPageModuleLoader(baseUrl) {
199
+ this.pageModuleLoader ??= new PageModuleLoaderService(this.appConfig, baseUrl);
200
+ return this.pageModuleLoader;
201
+ }
202
+ async resolveFilesystemPageMetadata(input) {
203
+ const loader = this.getPageModuleLoader(input.baseUrl);
204
+ const pageModule = await loader.resolvePageModule({
205
+ file: input.filePath,
206
+ importPageFileFn: (file) => input.routeRendererFactory.getPageRenderer(file).loadPageModule(file)
207
+ });
208
+ const { metadata } = await loader.resolvePageData({
209
+ pageModule,
210
+ routeOptions: {
211
+ file: input.filePath,
212
+ params: input.params
213
+ }
214
+ });
215
+ return metadata;
216
+ }
217
+ async resolveExplicitViewMetadata(input) {
218
+ if (!input.view.metadata) {
219
+ return this.appConfig.defaultMetadata;
220
+ }
221
+ const dynamicMetadata = await input.view.metadata({
222
+ params: input.params,
223
+ query: {},
224
+ props: input.props,
225
+ appConfig: this.appConfig
226
+ });
227
+ return { ...this.appConfig.defaultMetadata, ...dynamicMetadata };
147
228
  }
148
229
  pruneStaleStaticOutputs(activeStaticPathnames) {
149
230
  const removedOutputPaths = this.getRouteModuleBuildCache().pruneStaleRenderedOutputs(activeStaticPathnames);
@@ -155,11 +236,15 @@ class StaticSiteGenerator {
155
236
  }
156
237
  }
157
238
  }
158
- async createFilesystemStaticContents(route, _baseUrl, routeRendererFactory, skipped) {
239
+ async createFilesystemStaticContents(input) {
159
240
  const {
160
- templateRoute: { filePath },
161
- params
162
- } = route;
241
+ route: {
242
+ templateRoute: { filePath },
243
+ params
244
+ },
245
+ routeRendererFactory,
246
+ skipped
247
+ } = input;
163
248
  if (!routeRendererFactory) {
164
249
  throw new Error(STATIC_SITE_GENERATOR_ERRORS.ROUTE_RENDERER_FACTORY_REQUIRED);
165
250
  }
@@ -187,8 +272,9 @@ class StaticSiteGenerator {
187
272
  * @remarks
188
273
  * Routes are rendered through the normal route renderer directly.
189
274
  */
190
- async generateStaticPages(router, baseUrl, routeRendererFactory, skipped, activeStaticPathnames, preloadedRoutes) {
191
- const routes = preloadedRoutes ?? await router.listStaticGenerationRoutes({ runtimeOrigin: baseUrl });
275
+ async generateStaticPages(input) {
276
+ const { router, baseUrl, routeRendererFactory, skipped, activeStaticPathnames, sitemapEligiblePathnames } = input;
277
+ const routes = input.preloadedRoutes ?? await router.listStaticGenerationRoutes({ runtimeOrigin: baseUrl });
192
278
  appLogger.debug(
193
279
  "Static Pages",
194
280
  routes.map((route) => route.requestUrl)
@@ -202,7 +288,27 @@ class StaticSiteGenerator {
202
288
  directories,
203
289
  debugLabel: route.requestUrl,
204
290
  activeStaticPathnames,
205
- createContents: () => this.createFilesystemStaticContents(route, baseUrl, routeRendererFactory, skipped)
291
+ onSuccessfulExport: () => this.considerSitemapEligibility({
292
+ pathname: route.pathname,
293
+ sitemapEligiblePathnames,
294
+ resolveMetadata: () => {
295
+ if (!routeRendererFactory) {
296
+ throw new Error(STATIC_SITE_GENERATOR_ERRORS.ROUTE_RENDERER_FACTORY_REQUIRED);
297
+ }
298
+ return this.resolveFilesystemPageMetadata({
299
+ filePath: route.templateRoute.filePath,
300
+ params: route.params,
301
+ baseUrl,
302
+ routeRendererFactory
303
+ });
304
+ }
305
+ }),
306
+ createContents: () => this.createFilesystemStaticContents({
307
+ route,
308
+ baseUrl,
309
+ routeRendererFactory,
310
+ skipped
311
+ })
206
312
  });
207
313
  } catch (error) {
208
314
  appLogger.error(
@@ -220,7 +326,8 @@ class StaticSiteGenerator {
220
326
  routeRendererFactory: input.routeRendererFactory,
221
327
  staticRoutes: input.staticRoutes,
222
328
  force: input.force,
223
- preserveExportDirectory: input.preserveExportDirectory
329
+ preserveExportDirectory: input.preserveExportDirectory,
330
+ routes: input.routes
224
331
  };
225
332
  }
226
333
  async invokeStaticExportHook(hook, context) {
@@ -244,8 +351,10 @@ class StaticSiteGenerator {
244
351
  }) {
245
352
  const skippedDynamicPages = [];
246
353
  const activeStaticPathnames = /* @__PURE__ */ new Set();
354
+ const sitemapEligiblePathnames = this.appConfig.sitemap?.enabled ? /* @__PURE__ */ new Set() : void 0;
247
355
  this.forceFullStaticGeneration = force;
248
356
  this.staticRenderCacheContext = createRouteModuleStaticRenderCacheContext(this.appConfig);
357
+ this.pageModuleLoader = void 0;
249
358
  if (!force) {
250
359
  this.getRouteModuleBuildCache().ensureIncrementalStaticGenerationContext(this.staticRenderCacheContext);
251
360
  }
@@ -253,6 +362,7 @@ class StaticSiteGenerator {
253
362
  clearProductionPageBrowserGraphSession(this.appConfig);
254
363
  }
255
364
  const routes = await router.listStaticGenerationRoutes({ runtimeOrigin: baseUrl });
365
+ const exportRoutes = routes.map((route) => toEcopagesRouteInfo(route));
256
366
  if (shouldBuildPagesUnifiedGraph()) {
257
367
  await ensurePagesUnifiedGraphBuilt({
258
368
  appConfig: this.appConfig,
@@ -267,7 +377,8 @@ class StaticSiteGenerator {
267
377
  routeRendererFactory,
268
378
  staticRoutes,
269
379
  force,
270
- preserveExportDirectory
380
+ preserveExportDirectory,
381
+ routes: exportRoutes
271
382
  });
272
383
  await this.invokeStaticExportHook("beforeStaticExport", staticExportContext);
273
384
  try {
@@ -278,21 +389,23 @@ class StaticSiteGenerator {
278
389
  );
279
390
  }
280
391
  this.generateRobotsTxt();
281
- await this.generateStaticPages(
392
+ await this.generateStaticPages({
282
393
  router,
283
394
  baseUrl,
284
395
  routeRendererFactory,
285
- skippedDynamicPages,
396
+ skipped: skippedDynamicPages,
286
397
  activeStaticPathnames,
287
- routes
288
- );
398
+ preloadedRoutes: routes,
399
+ sitemapEligiblePathnames
400
+ });
289
401
  if (staticRoutes && staticRoutes.length > 0 && routeRendererFactory) {
290
- await this.generateExplicitStaticPages(
402
+ await this.generateExplicitStaticPages({
291
403
  staticRoutes,
292
404
  routeRendererFactory,
293
- skippedDynamicPages,
294
- activeStaticPathnames
295
- );
405
+ skipped: skippedDynamicPages,
406
+ activeStaticPathnames,
407
+ sitemapEligiblePathnames
408
+ });
296
409
  }
297
410
  if (preserveExportDirectory) {
298
411
  this.pruneStaleStaticOutputs(activeStaticPathnames);
@@ -305,6 +418,14 @@ class StaticSiteGenerator {
305
418
  } finally {
306
419
  await this.invokeStaticExportHook("afterStaticExport", staticExportContext);
307
420
  }
421
+ if (this.appConfig.sitemap?.enabled && sitemapEligiblePathnames) {
422
+ const locations = resolveSitemapLocations({
423
+ eligiblePathnames: [...sitemapEligiblePathnames],
424
+ sitemap: this.appConfig.sitemap,
425
+ baseUrl
426
+ });
427
+ this.generateSitemap(locations, this.appConfig.sitemap);
428
+ }
308
429
  if (skippedDynamicPages.length > 0) {
309
430
  appLogger.debug(
310
431
  `Skipped ${skippedDynamicPages.length} page(s) with cache: 'dynamic' (not supported in static generation)`,
@@ -312,24 +433,26 @@ class StaticSiteGenerator {
312
433
  );
313
434
  }
314
435
  }
315
- /**
316
- * Generates static pages from explicit static routes registered via app.static().
317
- * These routes use eco.page views via loader functions for HMR support.
318
- */
319
- async generateExplicitStaticPages(staticRoutes, routeRendererFactory, skipped, activeStaticPathnames) {
436
+ async generateExplicitStaticPages(input) {
320
437
  appLogger.debug(
321
438
  "Generating explicit static routes",
322
- staticRoutes.map((r) => r.path)
439
+ input.staticRoutes.map((r) => r.path)
323
440
  );
324
- for (const route of staticRoutes) {
441
+ for (const route of input.staticRoutes) {
325
442
  try {
326
443
  const mod = await route.loader();
327
444
  const view = mod.default;
328
445
  if (this.shouldSkipStaticView(route.path, view)) {
329
- skipped?.push(route.path);
446
+ input.skipped?.push(route.path);
330
447
  continue;
331
448
  }
332
- await this.generateExplicitStaticRoute(route.path, view, routeRendererFactory, activeStaticPathnames);
449
+ await this.generateExplicitStaticRoute({
450
+ routePath: route.path,
451
+ view,
452
+ routeRendererFactory: input.routeRendererFactory,
453
+ activeStaticPathnames: input.activeStaticPathnames,
454
+ sitemapEligiblePathnames: input.sitemapEligiblePathnames
455
+ });
333
456
  } catch (error) {
334
457
  appLogger.error(
335
458
  `Error generating explicit static page for ${route.path}:`,
@@ -345,8 +468,13 @@ class StaticSiteGenerator {
345
468
  }
346
469
  return path.isAbsolute(sourceFile) ? sourceFile : path.join(this.appConfig.rootDir, sourceFile);
347
470
  }
348
- async generateExplicitStaticRoute(routePath, view, routeRendererFactory, activeStaticPathnames) {
349
- const { renderer, routeEntries } = await this.planExplicitStaticRoute(routePath, view, routeRendererFactory);
471
+ async generateExplicitStaticRoute(input) {
472
+ const { routePath, view, routeRendererFactory, activeStaticPathnames, sitemapEligiblePathnames } = input;
473
+ const { renderer, routeEntries } = await this.planExplicitStaticRoute({
474
+ routePath,
475
+ view,
476
+ routeRendererFactory
477
+ });
350
478
  const sourceFile = this.resolveExplicitViewSourceFile(view);
351
479
  for (const { pathname, params } of routeEntries) {
352
480
  await this.writeStaticPageArtifact({
@@ -354,37 +482,59 @@ class StaticSiteGenerator {
354
482
  sourceFile,
355
483
  debugLabel: pathname,
356
484
  activeStaticPathnames,
357
- createContents: () => this.createExplicitStaticContents(routePath, view, params, routeRendererFactory, renderer)
485
+ onSuccessfulExport: () => this.considerSitemapEligibility({
486
+ pathname,
487
+ sitemapEligiblePathnames,
488
+ resolveMetadata: async () => {
489
+ const { props } = await prepareExplicitStaticRender({
490
+ routePath,
491
+ view,
492
+ params,
493
+ appConfig: this.appConfig,
494
+ runtimeOrigin: this.appConfig.baseUrl,
495
+ routeRendererFactory,
496
+ errors: STATIC_SITE_GENERATOR_ERRORS
497
+ });
498
+ return this.resolveExplicitViewMetadata({ view, params, props });
499
+ }
500
+ }),
501
+ createContents: () => this.createExplicitStaticContents({
502
+ routePath,
503
+ view,
504
+ params,
505
+ routeRendererFactory,
506
+ renderer
507
+ })
358
508
  });
359
509
  appLogger.debug(`Generated static page: ${pathname}`);
360
510
  }
361
511
  }
362
- async planExplicitStaticRoute(routePath, view, routeRendererFactory) {
512
+ async planExplicitStaticRoute(input) {
363
513
  const { renderer } = await prepareExplicitStaticRender({
364
- routePath,
365
- view,
514
+ routePath: input.routePath,
515
+ view: input.view,
366
516
  params: {},
367
517
  appConfig: this.appConfig,
368
518
  runtimeOrigin: this.appConfig.baseUrl,
369
- routeRendererFactory,
519
+ routeRendererFactory: input.routeRendererFactory,
370
520
  errors: STATIC_SITE_GENERATOR_ERRORS
371
521
  });
372
522
  return {
373
523
  renderer,
374
- routeEntries: await this.listExplicitStaticRouteEntries(routePath, view)
524
+ routeEntries: await this.listExplicitStaticRouteEntries(input.routePath, input.view)
375
525
  };
376
526
  }
377
- async createExplicitStaticContents(routePath, view, params, routeRendererFactory, renderer) {
527
+ async createExplicitStaticContents(input) {
378
528
  const { props, view: renderableView } = await prepareExplicitStaticRender({
379
- routePath,
380
- view,
381
- params,
529
+ routePath: input.routePath,
530
+ view: input.view,
531
+ params: input.params,
382
532
  appConfig: this.appConfig,
383
533
  runtimeOrigin: this.appConfig.baseUrl,
384
- routeRendererFactory,
534
+ routeRendererFactory: input.routeRendererFactory,
385
535
  errors: STATIC_SITE_GENERATOR_ERRORS
386
536
  });
387
- const response = await renderer.renderToResponse(renderableView, props, {});
537
+ const response = await input.renderer.renderToResponse(renderableView, props, {});
388
538
  return response.text();
389
539
  }
390
540
  async listExplicitStaticRouteEntries(routePath, view) {
@@ -5,14 +5,14 @@ import type { BuildRuntime } from '../build/runtime/build-runtime.js';
5
5
  import type { AnyIntegrationPlugin } from '../plugins/integration-plugin.js';
6
6
  import type { Processor } from '../plugins/processor.js';
7
7
  import type { EcoSourceTransform } from '../plugins/source-transform.js';
8
- import type { PageMetadataProps } from './public-types.js';
8
+ import type { PageMetadataProps, SitemapConfig } from './public-types.js';
9
9
  import type { RouteRegistry } from '../router/server/route-registry.js';
10
10
  import type { CacheConfig } from '../services/cache/cache.types.js';
11
11
  import type { DevGraphService } from '../services/runtime-state/dev-graph.service.js';
12
12
  import type { AppModuleLoader } from '../services/module-loading/app-module-loader.service.js';
13
13
  import type { SourceModuleLoader } from '../services/module-loading/module-loading-types.js';
14
14
  import type { EntrypointDependencyGraph } from '../services/runtime-state/entrypoint-dependency-graph.service.js';
15
- import type { SessionPageBrowserGraphCache } from '../route-renderer/orchestration/page-browser-graph-session.js';
15
+ import type { SessionPageBrowserGraphCache } from '../route-renderer/orchestration/page-browser-graph/page-browser-graph-session.js';
16
16
  import type { ServerInvalidationState } from '../services/runtime-state/server-invalidation-state.service.js';
17
17
  import type { ServerModuleTranspiler } from '../services/module-loading/server-module-transpiler.service.js';
18
18
  import type { RouteModuleBuildCache } from '../services/module-loading/route-module-build-cache.store.js';
@@ -99,6 +99,12 @@ export type EcoPagesAppConfig = {
99
99
  */
100
100
  preferences: RobotsPreference;
101
101
  };
102
+ /**
103
+ * Automatic sitemap.xml generation during static export.
104
+ *
105
+ * @default { enabled: false, fileName: 'sitemap.xml', extraUrls: [], exclude: [] }
106
+ */
107
+ sitemap: SitemapConfig;
102
108
  /** Additional paths to watch. Use this to monitor extra files. It is relative to the rootDir */
103
109
  additionalWatchPaths: string[];
104
110
  /**
@@ -123,6 +129,7 @@ export type EcoPagesAppConfig = {
123
129
  srcDir: string;
124
130
  htmlTemplatePath: string;
125
131
  error404TemplatePath: string;
132
+ error500TemplatePath: string;
126
133
  };
127
134
  /**
128
135
  * The processors to be used in the app
@@ -1,7 +1,7 @@
1
1
  import type { Readable } from 'node:stream';
2
2
  import type { ApiResponseBuilder } from '../adapters/shared/api-response.js';
3
3
  import type { BuildExecutor } from '../build/build-adapter.js';
4
- import type { ForeignChildRuntime } from '../route-renderer/orchestration/component-render-context.js';
4
+ import type { ForeignChildRuntime } from '../route-renderer/orchestration/foreign-child/component-render-context.js';
5
5
  import type { EcoPageComponent } from '../eco/eco.types.js';
6
6
  import type { EcoPagesAppConfig } from './internal-types.js';
7
7
  import type { HmrStrategy } from '../hmr/hmr-strategy.js';
@@ -534,6 +534,21 @@ export type EcoComponent<P = any, R = any> = IsAny<P> extends true ? EcoFunction
534
534
  export type PageProps<T = unknown> = T & StaticPageContext & {
535
535
  locals?: RequestLocals;
536
536
  };
537
+ /**
538
+ * Page-level robots directives for the document head and sitemap filtering.
539
+ *
540
+ * @remarks
541
+ * When `index` is `false`, the page is omitted from the auto-generated sitemap
542
+ * for routes whose metadata can be resolved during static export (filesystem
543
+ * pages and `app.static()` views with `metadata`). Defaults when omitted:
544
+ * `index: true`, `follow: true`. A `<meta name="robots">` tag is emitted by the
545
+ * route HTML finalization path when directives differ from those defaults.
546
+ */
547
+ export interface PageRobotsMetadata {
548
+ index?: boolean;
549
+ follow?: boolean;
550
+ nocache?: boolean;
551
+ }
537
552
  /**
538
553
  * Represents the metadata for a page.
539
554
  */
@@ -543,6 +558,48 @@ export interface PageMetadataProps {
543
558
  image?: string;
544
559
  url?: string;
545
560
  keywords?: string[];
561
+ robots?: PageRobotsMetadata;
562
+ }
563
+ /**
564
+ * Configuration for automatic `sitemap.xml` generation during static export.
565
+ *
566
+ * @remarks
567
+ * Disabled by default. When enabled, the sitemap is written after
568
+ * `afterStaticExport` so integration-generated URLs can be listed via
569
+ * `extraUrls`. Use `exclude` for bulk pathname filters and `metadata.robots.index: false`
570
+ * for page-level noindex that also removes the URL from the sitemap when metadata
571
+ * is available during export.
572
+ */
573
+ export interface SitemapConfig {
574
+ /** Emit sitemap.xml during static generation. @default false */
575
+ enabled?: boolean;
576
+ /** Output file name. @default "sitemap.xml" */
577
+ fileName?: string;
578
+ /**
579
+ * Extra, non-page URLs to include (e.g. "/rss.xml").
580
+ *
581
+ * @remarks
582
+ * Relative paths are resolved against `baseUrl`. Absolute `http(s)` URLs pass
583
+ * through. Always appended after page URLs; not filtered by `exclude` or page robots.
584
+ */
585
+ extraUrls?: string[];
586
+ /**
587
+ * Pathname patterns to exclude even if a page exists.
588
+ *
589
+ * @remarks
590
+ * Supported patterns: exact paths (`/admin`) and prefix wildcards (`/admin/**`).
591
+ * Unsupported patterns are treated as exact matches. This is not a full glob engine.
592
+ */
593
+ exclude?: string[];
594
+ }
595
+ /**
596
+ * Slim route descriptor for static-export hooks and runtime consumers.
597
+ */
598
+ export interface EcopagesRouteInfo {
599
+ /** Resolved URL pathname, e.g. `/blog/my-post`. */
600
+ pathname: string;
601
+ /** Route params for dynamic routes (empty for static routes). */
602
+ params: Record<string, string>;
546
603
  }
547
604
  /**
548
605
  * Represents the props for the head of a page.
@@ -622,12 +679,25 @@ export type EcoLayoutComponent<T = EcoPagesElement> = EcoComponent<LayoutProps<T
622
679
  */
623
680
  export type EcoHtmlComponent<T = EcoPagesElement> = EcoComponent<HtmlTemplateProps, T>;
624
681
  /**
625
- * Represents the props for the error 404 template.
682
+ * Props type for the semantic `404.*` page template.
683
+ * @remarks `message` and `stack` are declared for future error context. The
684
+ * runtime does not currently pass these props when rendering the custom 404 page.
626
685
  */
627
686
  export interface Error404TemplateProps extends Omit<HtmlTemplateProps, 'children'> {
628
687
  message: string;
629
688
  stack?: string;
630
689
  }
690
+ /**
691
+ * Props type for the semantic `500.*` page template.
692
+ * @remarks In development, the page-pipeline passes `message` and `stack` from the
693
+ * thrown error when rendering this page after a failure. In production those
694
+ * fields are omitted so stacks are not serialized into HTML. Direct visits to
695
+ * `/500` also omit them.
696
+ */
697
+ export interface Error500TemplateProps extends Omit<HtmlTemplateProps, 'children'> {
698
+ message?: string;
699
+ stack?: string;
700
+ }
631
701
  /**
632
702
  * Represents the parameters for a page.
633
703
  * The keys are strings, and the values can be either a string or an array of strings.
@@ -751,6 +821,12 @@ export type RouteRendererOptions = {
751
821
  params?: PageParams;
752
822
  query?: PageQuery;
753
823
  locals?: RequestLocals;
824
+ /**
825
+ * Extra props merged into the page props for this render.
826
+ * @remarks Used by semantic error pages (for example development `message` /
827
+ * `stack` on the custom 500 page).
828
+ */
829
+ props?: Record<string, unknown>;
754
830
  };
755
831
  /**
756
832
  * The body of the route renderer.
@@ -0,0 +1,26 @@
1
+ import type { EcopagesRouteInfo } from '../types/public-types.js';
2
+ /**
3
+ * Maps a static-generation or registry route into the public slim route shape.
4
+ */
5
+ export declare function toEcopagesRouteInfo(route: {
6
+ pathname: string;
7
+ params: Record<string, string | string[]>;
8
+ }): EcopagesRouteInfo;
9
+ /**
10
+ * Flattens string | string[] route params into string values for public APIs.
11
+ */
12
+ export declare function normalizeRouteParams(params: Record<string, string | string[]>): Record<string, string>;
13
+ type StaticGenerationRouteLister = (input: {
14
+ runtimeOrigin: string;
15
+ }) => Promise<readonly {
16
+ pathname: string;
17
+ params: Record<string, string | string[]>;
18
+ }[]>;
19
+ /**
20
+ * Resolves {@link EcopagesRouteInfo} entries for app-start and similar consumers.
21
+ */
22
+ export declare function resolveAppStartRoutes(input: {
23
+ listStaticGenerationRoutes?: StaticGenerationRouteLister;
24
+ runtimeOrigin: string;
25
+ }): Promise<EcopagesRouteInfo[]>;
26
+ export {};
@@ -0,0 +1,26 @@
1
+ import { normalizePathname } from "./path-pattern.js";
2
+ function toEcopagesRouteInfo(route) {
3
+ return {
4
+ pathname: normalizePathname(route.pathname),
5
+ params: normalizeRouteParams(route.params)
6
+ };
7
+ }
8
+ function normalizeRouteParams(params) {
9
+ const normalized = {};
10
+ for (const [key, value] of Object.entries(params)) {
11
+ normalized[key] = Array.isArray(value) ? value.join("/") : value;
12
+ }
13
+ return normalized;
14
+ }
15
+ async function resolveAppStartRoutes(input) {
16
+ if (!input.listStaticGenerationRoutes) {
17
+ return [];
18
+ }
19
+ const routes = await input.listStaticGenerationRoutes({ runtimeOrigin: input.runtimeOrigin });
20
+ return routes.map(toEcopagesRouteInfo);
21
+ }
22
+ export {
23
+ normalizeRouteParams,
24
+ resolveAppStartRoutes,
25
+ toEcopagesRouteInfo
26
+ };
@@ -5,3 +5,10 @@
5
5
  * @returns Escaped attribute-safe string.
6
6
  */
7
7
  export declare function escapeHtmlAttribute(value: string): string;
8
+ /**
9
+ * Escapes a string for safe use as XML text content (e.g. `<loc>` values).
10
+ *
11
+ * @param value Raw text node value.
12
+ * @returns Escaped XML-safe string.
13
+ */
14
+ export declare function escapeXmlText(value: string): string;
@@ -1,6 +1,10 @@
1
1
  function escapeHtmlAttribute(value) {
2
2
  return value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
3
3
  }
4
+ function escapeXmlText(value) {
5
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
6
+ }
4
7
  export {
5
- escapeHtmlAttribute
8
+ escapeHtmlAttribute,
9
+ escapeXmlText
6
10
  };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Normalizes a URL pathname for matching and sitemap location building.
3
+ *
4
+ * @remarks
5
+ * Ensures a leading slash and strips trailing slashes except for `/`.
6
+ */
7
+ export declare function normalizePathname(pathname: string): string;
8
+ /**
9
+ * Matches a pathname against a small set of supported patterns.
10
+ *
11
+ * @remarks
12
+ * Supported forms:
13
+ * - exact: `/admin`
14
+ * - prefix wildcard: `/admin/**` (matches `/admin` and descendants)
15
+ * - trailing `**` only as a full segment after `/`
16
+ *
17
+ * This is not a full glob engine. Unsupported patterns are treated as exact matches.
18
+ */
19
+ export declare function matchPathPattern(pathname: string, pattern: string): boolean;
20
+ /**
21
+ * Returns true when the pathname matches any of the patterns.
22
+ */
23
+ export declare function matchesAnyPathPattern(pathname: string, patterns: readonly string[]): boolean;