@ecopages/core 0.2.0-beta.28 → 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.
- package/package.json +2 -2
- package/src/adapters/abstract/application-adapter.d.ts +22 -4
- package/src/adapters/abstract/application-adapter.js +27 -4
- package/src/adapters/abstract/server-adapter.d.ts +10 -0
- package/src/adapters/bun/create-app.d.ts +1 -0
- package/src/adapters/bun/create-app.js +11 -4
- package/src/adapters/bun/server-adapter.js +1 -0
- package/src/adapters/node/create-app.d.ts +1 -0
- package/src/adapters/node/create-app.js +10 -3
- package/src/adapters/node/server-adapter.js +1 -0
- package/src/adapters/shared/fs-server-response-factory.d.ts +5 -0
- package/src/adapters/shared/fs-server-response-factory.js +20 -0
- package/src/adapters/shared/fs-server-response-matcher.d.ts +18 -0
- package/src/adapters/shared/fs-server-response-matcher.js +75 -15
- package/src/config/config-builder.d.ts +12 -2
- package/src/config/config-builder.js +41 -2
- package/src/config/constants.d.ts +1 -0
- package/src/config/constants.js +2 -1
- package/src/route-renderer/orchestration/route-pipeline/robots-meta.contribution.d.ts +15 -0
- package/src/route-renderer/orchestration/route-pipeline/robots-meta.contribution.js +36 -0
- package/src/route-renderer/orchestration/route-pipeline/route-html-finalization.service.d.ts +4 -0
- package/src/route-renderer/orchestration/route-pipeline/route-html-finalization.service.js +5 -2
- package/src/route-renderer/orchestration/route-pipeline/route-prepared-options.builder.js +6 -2
- package/src/static-site-generator/README.md +9 -1
- package/src/static-site-generator/sitemap-routes.d.ts +14 -0
- package/src/static-site-generator/sitemap-routes.js +31 -0
- package/src/static-site-generator/sitemap.d.ts +15 -0
- package/src/static-site-generator/sitemap.js +33 -0
- package/src/static-site-generator/static-export-context.d.ts +9 -1
- package/src/static-site-generator/static-site-generator.d.ts +27 -6
- package/src/static-site-generator/static-site-generator.js +200 -50
- package/src/types/internal-types.d.ts +8 -1
- package/src/types/public-types.d.ts +77 -1
- package/src/utils/ecopages-route-info.d.ts +26 -0
- package/src/utils/ecopages-route-info.js +26 -0
- package/src/utils/html-escaping.d.ts +7 -0
- package/src/utils/html-escaping.js +5 -1
- package/src/utils/path-pattern.d.ts +23 -0
- package/src/utils/path-pattern.js +27 -0
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { buildRobotsMetaContribution } from "./robots-meta.contribution.js";
|
|
1
2
|
function buildRouteHtmlFinalization(context) {
|
|
2
3
|
const { renderOptions } = context;
|
|
3
4
|
const documentAttributes = context.getDocumentAttributes(renderOptions);
|
|
4
|
-
const
|
|
5
|
+
const integrationContributions = context.getHtmlDocumentContributions({ renderOptions, partial: false }) ?? [];
|
|
6
|
+
const robotsContribution = buildRobotsMetaContribution(renderOptions.metadata ?? {});
|
|
7
|
+
const htmlContributions = robotsContribution ? [robotsContribution, ...integrationContributions] : integrationContributions;
|
|
5
8
|
const hasStructuralFinalization = documentAttributes && Object.keys(documentAttributes).length > 0;
|
|
6
|
-
if (!hasStructuralFinalization &&
|
|
9
|
+
if (!hasStructuralFinalization && htmlContributions.length === 0) {
|
|
7
10
|
return {};
|
|
8
11
|
}
|
|
9
12
|
return {
|
|
@@ -6,8 +6,12 @@ function buildPreparedRenderOptions(input) {
|
|
|
6
6
|
const { Page, HtmlTemplate, Layouts, Layout, layoutEntries, props, metadata, integrationSpecificProps } = resolvedInputs;
|
|
7
7
|
const dedupedDependencies = dedupeProcessedAssets(allDependencies);
|
|
8
8
|
const pagePackage = createPagePackage(dedupedDependencies, { pageBrowserGraph });
|
|
9
|
-
const
|
|
9
|
+
const resolvedProps = {
|
|
10
10
|
...props,
|
|
11
|
+
...routeOptions.props ?? {}
|
|
12
|
+
};
|
|
13
|
+
const pageProps = {
|
|
14
|
+
...resolvedProps,
|
|
11
15
|
params: routeOptions.params || {},
|
|
12
16
|
query: routeOptions.query || {}
|
|
13
17
|
};
|
|
@@ -25,7 +29,7 @@ function buildPreparedRenderOptions(input) {
|
|
|
25
29
|
Layouts,
|
|
26
30
|
Layout,
|
|
27
31
|
layoutEntries,
|
|
28
|
-
props,
|
|
32
|
+
props: resolvedProps,
|
|
29
33
|
Page,
|
|
30
34
|
metadata,
|
|
31
35
|
params: routeOptions.params || {},
|
|
@@ -29,8 +29,10 @@ It should not invent a parallel rendering stack just for build mode.
|
|
|
29
29
|
|
|
30
30
|
| File | Role |
|
|
31
31
|
| ------------------------------ | ---------------------------------------------------------------------------- |
|
|
32
|
-
| `static-site-generator.ts` | Route enumeration, HTML artifact writes, integration export hooks
|
|
32
|
+
| `static-site-generator.ts` | Route enumeration, HTML artifact writes, integration export hooks, sitemap |
|
|
33
33
|
| `static-export-context.ts` | Hook context type for `beforeStaticExport` / `afterStaticExport` |
|
|
34
|
+
| `sitemap.ts` | Pure sitemap.xml renderer |
|
|
35
|
+
| `sitemap-routes.ts` | Sitemap location assembly (`exclude`, `extraUrls`, dedupe) |
|
|
34
36
|
| `static-build-invalidation.ts` | `dist/` reset policy, production cache clearing, static-render cache context |
|
|
35
37
|
|
|
36
38
|
Build-input fingerprinting (`hashAppConfigFile`, `createBuildInputsFingerprint`) lives in `packages/core/src/build/cache/build-input-fingerprint.ts` and is shared with server-entry and unified-graph caches.
|
|
@@ -56,4 +58,10 @@ Integrations may implement:
|
|
|
56
58
|
- `beforeStaticExport(context)` — runs after unified-graph prebuild, before page rendering
|
|
57
59
|
- `afterStaticExport(context)` — runs in a `finally` block after generation completes
|
|
58
60
|
|
|
61
|
+
When `appConfig.sitemap.enabled` is true, `sitemap.xml` is written **after** `afterStaticExport` so integration-generated URLs can be listed via `extraUrls`.
|
|
62
|
+
|
|
63
|
+
Sitemap eligibility is decided during successful page export (`activeStaticPathnames` plus `robots.index !== false`, fail-closed on metadata errors). `resolveSitemapLocations` then applies `exclude` and appends `extraUrls`.
|
|
64
|
+
|
|
65
|
+
`StaticExportContext.routes` is the unfiltered static-generation route list. Sitemap filtering is applied separately.
|
|
66
|
+
|
|
59
67
|
See `StaticExportContext` in `static-export-context.ts`.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SitemapConfig } from '../types/public-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Builds the ordered absolute locations for sitemap.xml.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* `eligiblePathnames` must already be successfully exported and robots-indexable.
|
|
7
|
+
* Filtering here is only `sitemap.exclude`, then `extraUrls` are appended.
|
|
8
|
+
* `extraUrls` always include (deduped) and are not subject to `exclude` or page robots.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveSitemapLocations(input: {
|
|
11
|
+
eligiblePathnames: readonly string[];
|
|
12
|
+
sitemap: SitemapConfig;
|
|
13
|
+
baseUrl: string;
|
|
14
|
+
}): string[];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { matchesAnyPathPattern, normalizePathname } from "../utils/path-pattern.js";
|
|
2
|
+
import { buildSitemapLocation } from "./sitemap.js";
|
|
3
|
+
function resolveSitemapLocations(input) {
|
|
4
|
+
const exclude = input.sitemap.exclude ?? [];
|
|
5
|
+
const locations = [];
|
|
6
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7
|
+
for (const pathname of input.eligiblePathnames) {
|
|
8
|
+
const normalized = normalizePathname(pathname);
|
|
9
|
+
if (matchesAnyPathPattern(normalized, exclude)) {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
const location = buildSitemapLocation(input.baseUrl, normalized);
|
|
13
|
+
if (seen.has(location)) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
seen.add(location);
|
|
17
|
+
locations.push(location);
|
|
18
|
+
}
|
|
19
|
+
for (const extraUrl of input.sitemap.extraUrls ?? []) {
|
|
20
|
+
const location = buildSitemapLocation(input.baseUrl, extraUrl);
|
|
21
|
+
if (seen.has(location)) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
seen.add(location);
|
|
25
|
+
locations.push(location);
|
|
26
|
+
}
|
|
27
|
+
return locations;
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
resolveSitemapLocations
|
|
31
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Joins a base origin with a pathname or absolute URL into a sitemap `<loc>` value.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Trailing slashes are stripped except for the site root (`/`). Absolute `http(s)`
|
|
6
|
+
* URLs in `extraUrls` are returned as-is (still slash-normalized).
|
|
7
|
+
*/
|
|
8
|
+
export declare function buildSitemapLocation(baseUrl: string, pathnameOrUrl: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Renders a sitemap.org 0.9 urlset document from absolute location URLs.
|
|
11
|
+
*
|
|
12
|
+
* @remarks
|
|
13
|
+
* Callers are responsible for deduplication; this serializer preserves input order.
|
|
14
|
+
*/
|
|
15
|
+
export declare function renderSitemap(locations: readonly string[]): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { escapeXmlText } from "../utils/html-escaping.js";
|
|
2
|
+
import { normalizePathname } from "../utils/path-pattern.js";
|
|
3
|
+
function buildSitemapLocation(baseUrl, pathnameOrUrl) {
|
|
4
|
+
const trimmed = pathnameOrUrl.trim();
|
|
5
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
6
|
+
return normalizeLocation(trimmed);
|
|
7
|
+
}
|
|
8
|
+
const origin = baseUrl.replace(/\/+$/, "");
|
|
9
|
+
const pathname = normalizePathname(trimmed);
|
|
10
|
+
return pathname === "/" ? `${origin}/` : `${origin}${pathname}`;
|
|
11
|
+
}
|
|
12
|
+
function renderSitemap(locations) {
|
|
13
|
+
const urls = locations.map((loc) => ` <url>
|
|
14
|
+
<loc>${escapeXmlText(normalizeLocation(loc))}</loc>
|
|
15
|
+
</url>`).join("\n");
|
|
16
|
+
return [
|
|
17
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
18
|
+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
19
|
+
urls,
|
|
20
|
+
"</urlset>",
|
|
21
|
+
""
|
|
22
|
+
].join("\n");
|
|
23
|
+
}
|
|
24
|
+
function normalizeLocation(location) {
|
|
25
|
+
if (/^https?:\/\/[^/]+\/?$/i.test(location)) {
|
|
26
|
+
return `${location.replace(/\/+$/, "")}/`;
|
|
27
|
+
}
|
|
28
|
+
return location.replace(/\/+$/, "");
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
buildSitemapLocation,
|
|
32
|
+
renderSitemap
|
|
33
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EcoPagesAppConfig } from '../types/internal-types.js';
|
|
2
|
-
import type { StaticRoute } from '../types/public-types.js';
|
|
2
|
+
import type { EcopagesRouteInfo, StaticRoute } from '../types/public-types.js';
|
|
3
3
|
import type { StaticGenerationRendererResolver } from '../route-renderer/route-renderer.js';
|
|
4
4
|
import type { StaticGenerationRoute } from '../router/server/route-registry.js';
|
|
5
5
|
type StaticGenerationRouteSource = {
|
|
@@ -18,5 +18,13 @@ export interface StaticExportContext {
|
|
|
18
18
|
staticRoutes?: StaticRoute[];
|
|
19
19
|
force: boolean;
|
|
20
20
|
preserveExportDirectory: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Unfiltered static-generation routes (exact + expanded dynamic).
|
|
23
|
+
*
|
|
24
|
+
* @remarks
|
|
25
|
+
* Sitemap filtering is applied separately; integrations that build custom
|
|
26
|
+
* artifacts should start from this full list.
|
|
27
|
+
*/
|
|
28
|
+
routes: EcopagesRouteInfo[];
|
|
21
29
|
}
|
|
22
30
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EcoPagesAppConfig } from '../types/internal-types.js';
|
|
2
|
-
import type { StaticRoute } from '../types/public-types.js';
|
|
2
|
+
import type { SitemapConfig, StaticRoute } from '../types/public-types.js';
|
|
3
3
|
import type { PageRendererResolver, StaticGenerationRendererResolver } from '../route-renderer/route-renderer.js';
|
|
4
4
|
import type { StaticGenerationRoute } from '../router/server/route-registry.js';
|
|
5
5
|
import type { RouteModuleBuildCache } from '../services/module-loading/route-module-build-cache.store.js';
|
|
@@ -31,6 +31,7 @@ export declare class StaticSiteGenerator {
|
|
|
31
31
|
private readonly routeModuleBuildCacheOverride?;
|
|
32
32
|
private staticRenderCacheContext;
|
|
33
33
|
private forceFullStaticGeneration;
|
|
34
|
+
private pageModuleLoader?;
|
|
34
35
|
/**
|
|
35
36
|
* Creates the static-site generator for one app config.
|
|
36
37
|
*/
|
|
@@ -46,6 +47,10 @@ export declare class StaticSiteGenerator {
|
|
|
46
47
|
* Writes the robots.txt file declared by the app config.
|
|
47
48
|
*/
|
|
48
49
|
generateRobotsTxt(): void;
|
|
50
|
+
/**
|
|
51
|
+
* Writes the configured sitemap file to the export directory.
|
|
52
|
+
*/
|
|
53
|
+
generateSitemap(locations: readonly string[], sitemap: SitemapConfig): void;
|
|
49
54
|
/**
|
|
50
55
|
* Returns whether the input path points at the root directory.
|
|
51
56
|
*/
|
|
@@ -56,6 +61,18 @@ export declare class StaticSiteGenerator {
|
|
|
56
61
|
getDirectories(routes: string[]): string[];
|
|
57
62
|
private writeStaticOutput;
|
|
58
63
|
private writeStaticPageArtifact;
|
|
64
|
+
/**
|
|
65
|
+
* Marks a successfully exported pathname as sitemap-eligible when robots allow indexing.
|
|
66
|
+
*
|
|
67
|
+
* @remarks
|
|
68
|
+
* Fail-closed: metadata resolution errors omit the URL. `robots.index === false`
|
|
69
|
+
* also omits it. Eligibility is decided during generation, not by reloading modules
|
|
70
|
+
* after `afterStaticExport`.
|
|
71
|
+
*/
|
|
72
|
+
private considerSitemapEligibility;
|
|
73
|
+
private getPageModuleLoader;
|
|
74
|
+
private resolveFilesystemPageMetadata;
|
|
75
|
+
private resolveExplicitViewMetadata;
|
|
59
76
|
private pruneStaleStaticOutputs;
|
|
60
77
|
private createFilesystemStaticContents;
|
|
61
78
|
/**
|
|
@@ -64,7 +81,15 @@ export declare class StaticSiteGenerator {
|
|
|
64
81
|
* @remarks
|
|
65
82
|
* Routes are rendered through the normal route renderer directly.
|
|
66
83
|
*/
|
|
67
|
-
generateStaticPages(
|
|
84
|
+
generateStaticPages(input: {
|
|
85
|
+
router: StaticGenerationRouteSource;
|
|
86
|
+
baseUrl: string;
|
|
87
|
+
routeRendererFactory?: StaticPageRouteRendererFactory;
|
|
88
|
+
skipped?: string[];
|
|
89
|
+
activeStaticPathnames?: Set<string>;
|
|
90
|
+
preloadedRoutes?: readonly StaticGenerationRoute[];
|
|
91
|
+
sitemapEligiblePathnames?: Set<string>;
|
|
92
|
+
}): Promise<void>;
|
|
68
93
|
private createStaticExportContext;
|
|
69
94
|
private invokeStaticExportHook;
|
|
70
95
|
/**
|
|
@@ -78,10 +103,6 @@ export declare class StaticSiteGenerator {
|
|
|
78
103
|
force?: boolean;
|
|
79
104
|
preserveExportDirectory?: boolean;
|
|
80
105
|
}): Promise<void>;
|
|
81
|
-
/**
|
|
82
|
-
* Generates static pages from explicit static routes registered via app.static().
|
|
83
|
-
* These routes use eco.page views via loader functions for HMR support.
|
|
84
|
-
*/
|
|
85
106
|
private generateExplicitStaticPages;
|
|
86
107
|
private resolveExplicitViewSourceFile;
|
|
87
108
|
private generateExplicitStaticRoute;
|
|
@@ -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
|
|
68
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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(
|
|
239
|
+
async createFilesystemStaticContents(input) {
|
|
159
240
|
const {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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(
|
|
191
|
-
const
|
|
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
|
-
|
|
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(
|
|
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(
|
|
349
|
-
const {
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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) {
|