@stati/core 1.27.2 → 1.29.0
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/README.md +6 -1
- package/dist/config/errors.d.ts +10 -0
- package/dist/config/errors.d.ts.map +1 -0
- package/dist/config/errors.js +10 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +15 -2
- package/dist/constants.d.ts +26 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +23 -0
- package/dist/core/build.d.ts.map +1 -1
- package/dist/core/build.js +201 -41
- package/dist/core/collections.d.ts +139 -0
- package/dist/core/collections.d.ts.map +1 -0
- package/dist/core/collections.js +342 -0
- package/dist/core/content.d.ts.map +1 -1
- package/dist/core/content.js +2 -1
- package/dist/core/dev.d.ts.map +1 -1
- package/dist/core/dev.js +11 -62
- package/dist/core/expand.d.ts +86 -0
- package/dist/core/expand.d.ts.map +1 -0
- package/dist/core/expand.js +484 -0
- package/dist/core/isg/builder.d.ts +27 -2
- package/dist/core/isg/builder.d.ts.map +1 -1
- package/dist/core/isg/builder.js +83 -11
- package/dist/core/isg/deps.d.ts +9 -0
- package/dist/core/isg/deps.d.ts.map +1 -1
- package/dist/core/isg/deps.js +30 -1
- package/dist/core/isg/hash.d.ts +14 -2
- package/dist/core/isg/hash.d.ts.map +1 -1
- package/dist/core/isg/hash.js +30 -2
- package/dist/core/isg/index.d.ts +4 -2
- package/dist/core/isg/index.d.ts.map +1 -1
- package/dist/core/isg/index.js +4 -2
- package/dist/core/isg/manifest.js +14 -0
- package/dist/core/isg/prune.d.ts +32 -0
- package/dist/core/isg/prune.d.ts.map +1 -0
- package/dist/core/isg/prune.js +113 -0
- package/dist/core/markdown.d.ts +10 -1
- package/dist/core/markdown.d.ts.map +1 -1
- package/dist/core/markdown.js +13 -3
- package/dist/core/navigation.d.ts +4 -2
- package/dist/core/navigation.d.ts.map +1 -1
- package/dist/core/navigation.js +17 -2
- package/dist/core/templates.d.ts +15 -1
- package/dist/core/templates.d.ts.map +1 -1
- package/dist/core/templates.js +121 -126
- package/dist/core/utils/fs.utils.d.ts +29 -1
- package/dist/core/utils/fs.utils.d.ts.map +1 -1
- package/dist/core/utils/fs.utils.js +74 -1
- package/dist/core/utils/index.d.ts +3 -3
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +3 -3
- package/dist/core/utils/paths.utils.d.ts +15 -0
- package/dist/core/utils/paths.utils.d.ts.map +1 -1
- package/dist/core/utils/paths.utils.js +20 -0
- package/dist/core/utils/template-discovery.utils.d.ts +25 -1
- package/dist/core/utils/template-discovery.utils.d.ts.map +1 -1
- package/dist/core/utils/template-discovery.utils.js +61 -11
- package/dist/images/config.d.ts +52 -0
- package/dist/images/config.d.ts.map +1 -0
- package/dist/images/config.js +200 -0
- package/dist/images/index.d.ts +15 -0
- package/dist/images/index.d.ts.map +1 -0
- package/dist/images/index.js +11 -0
- package/dist/images/manifest.d.ts +36 -0
- package/dist/images/manifest.d.ts.map +1 -0
- package/dist/images/manifest.js +69 -0
- package/dist/images/markdown-rule.d.ts +26 -0
- package/dist/images/markdown-rule.d.ts.map +1 -0
- package/dist/images/markdown-rule.js +60 -0
- package/dist/images/markup.d.ts +39 -0
- package/dist/images/markup.d.ts.map +1 -0
- package/dist/images/markup.js +72 -0
- package/dist/images/og.d.ts +52 -0
- package/dist/images/og.d.ts.map +1 -0
- package/dist/images/og.js +165 -0
- package/dist/images/plan.d.ts +101 -0
- package/dist/images/plan.d.ts.map +1 -0
- package/dist/images/plan.js +128 -0
- package/dist/images/provider.d.ts +35 -0
- package/dist/images/provider.d.ts.map +1 -0
- package/dist/images/provider.js +70 -0
- package/dist/images/resolve.d.ts +45 -0
- package/dist/images/resolve.d.ts.map +1 -0
- package/dist/images/resolve.js +51 -0
- package/dist/images/runtime.d.ts +258 -0
- package/dist/images/runtime.d.ts.map +1 -0
- package/dist/images/runtime.js +659 -0
- package/dist/images/sources.d.ts +47 -0
- package/dist/images/sources.d.ts.map +1 -0
- package/dist/images/sources.js +97 -0
- package/dist/images/utils/image-probe.utils.d.ts +30 -0
- package/dist/images/utils/image-probe.utils.d.ts.map +1 -0
- package/dist/images/utils/image-probe.utils.js +307 -0
- package/dist/images/utils/index.d.ts +7 -0
- package/dist/images/utils/index.d.ts.map +1 -0
- package/dist/images/utils/index.js +5 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/metrics/noop.d.ts.map +1 -1
- package/dist/metrics/noop.js +3 -0
- package/dist/metrics/recorder.d.ts.map +1 -1
- package/dist/metrics/recorder.js +3 -0
- package/dist/metrics/types.d.ts +8 -0
- package/dist/metrics/types.d.ts.map +1 -1
- package/dist/rss/generator.d.ts.map +1 -1
- package/dist/rss/generator.js +5 -2
- package/dist/search/generator.d.ts +9 -0
- package/dist/search/generator.d.ts.map +1 -1
- package/dist/search/generator.js +20 -0
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +1 -1
- package/dist/seo/auto-inject.d.ts +3 -0
- package/dist/seo/auto-inject.d.ts.map +1 -1
- package/dist/seo/auto-inject.js +1 -0
- package/dist/seo/generator.d.ts +24 -0
- package/dist/seo/generator.d.ts.map +1 -1
- package/dist/seo/generator.js +65 -1
- package/dist/seo/index.d.ts +1 -1
- package/dist/seo/index.d.ts.map +1 -1
- package/dist/seo/index.js +1 -1
- package/dist/types/config.d.ts +60 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/content.d.ts +150 -3
- package/dist/types/content.d.ts.map +1 -1
- package/dist/types/images.d.ts +185 -0
- package/dist/types/images.d.ts.map +1 -0
- package/dist/types/images.js +8 -0
- package/dist/types/index.d.ts +3 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/isg.d.ts +19 -0
- package/dist/types/isg.d.ts.map +1 -1
- package/dist/types/seo.d.ts +9 -0
- package/dist/types/seo.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -166,7 +166,8 @@ Customize layouts with [Eta templates](https://eta.js.org):
|
|
|
166
166
|
|
|
167
167
|
Only rebuild what changed:
|
|
168
168
|
|
|
169
|
-
- Smart caching based on hashes of each page's content and templates
|
|
169
|
+
- Smart caching based on hashes of each page's content and templates, and of the collection and navigation data it reads
|
|
170
|
+
- Removal of pages that no longer exist, such as deleted posts and unused tag pages
|
|
170
171
|
- TTL-based refresh for dynamic content
|
|
171
172
|
- Tag-based invalidation for related content
|
|
172
173
|
|
|
@@ -183,6 +184,10 @@ await invalidate('path:/posts');
|
|
|
183
184
|
await invalidate('age:3months');
|
|
184
185
|
```
|
|
185
186
|
|
|
187
|
+
#### Collections, Pagination and Generated Pages
|
|
188
|
+
|
|
189
|
+
A folder with an `index.md` is a collection: templates read its pages as `stati.collection`, sorted by date and grouped by tag. `paginate: 10` in front matter splits a listing into `/blog`, `/blog/page/2` and so on (`stati.pagination`), and a `[tag].md` template becomes one page per tag (`stati.generated`). See [Collections and generated pages](https://stati.build/core-concepts/collections).
|
|
190
|
+
|
|
186
191
|
#### Built-In SEO
|
|
187
192
|
|
|
188
193
|
SEO optimization works automatically with zero configuration:
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when the Stati configuration is invalid.
|
|
3
|
+
* `loadConfig()` always rethrows it, because the build cannot proceed with a broken config.
|
|
4
|
+
*/
|
|
5
|
+
export declare class ConfigValidationError extends Error {
|
|
6
|
+
constructor(message: string, options?: {
|
|
7
|
+
cause?: unknown;
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/config/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAI3D"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when the Stati configuration is invalid.
|
|
3
|
+
* `loadConfig()` always rethrows it, because the build cannot proceed with a broken config.
|
|
4
|
+
*/
|
|
5
|
+
export class ConfigValidationError extends Error {
|
|
6
|
+
constructor(message, options) {
|
|
7
|
+
super(message, options);
|
|
8
|
+
this.name = 'ConfigValidationError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AA+BrD;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAqBD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAExD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,UAAU,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,WAAW,CAAC,CA8HlF"}
|
package/dist/config/loader.js
CHANGED
|
@@ -2,6 +2,8 @@ import { existsSync, statSync } from 'node:fs';
|
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { validateISGConfig, ISGConfigurationError } from '../core/isg/validation.js';
|
|
5
|
+
import { validateImagesConfig } from '../images/config.js';
|
|
6
|
+
import { ConfigValidationError } from './errors.js';
|
|
5
7
|
import { compileStatiConfig, cleanupCompiledConfig } from '../core/utils/index.js';
|
|
6
8
|
import { DEFAULT_SRC_DIR, DEFAULT_OUT_DIR, DEFAULT_STATIC_DIR, DEFAULT_SITE_TITLE, DEFAULT_DEV_BASE_URL, DEFAULT_TTL_SECONDS, DEFAULT_MAX_AGE_CAP_DAYS, CONFIG_FILE_PATTERNS, } from '../constants.js';
|
|
7
9
|
/**
|
|
@@ -139,6 +141,16 @@ export async function loadConfig(cwd = process.cwd()) {
|
|
|
139
141
|
}
|
|
140
142
|
throw error; // Re-throw non-ISG errors
|
|
141
143
|
}
|
|
144
|
+
// Validate image configuration
|
|
145
|
+
try {
|
|
146
|
+
validateImagesConfig(mergedConfig.images);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (error instanceof ConfigValidationError) {
|
|
150
|
+
throw new ConfigValidationError(`Invalid images configuration in ${configPath}:\n${error.message}`, { cause: error });
|
|
151
|
+
}
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
142
154
|
// Cache the loaded config with its modification time
|
|
143
155
|
try {
|
|
144
156
|
const mtime = statSync(resolvedConfigPath).mtimeMs;
|
|
@@ -154,8 +166,9 @@ export async function loadConfig(cwd = process.cwd()) {
|
|
|
154
166
|
return mergedConfig;
|
|
155
167
|
}
|
|
156
168
|
catch (error) {
|
|
157
|
-
if (error instanceof
|
|
158
|
-
|
|
169
|
+
if (error instanceof ConfigValidationError ||
|
|
170
|
+
(error instanceof Error && error.message.includes('Invalid ISG configuration'))) {
|
|
171
|
+
// Validation errors bubble up with context: the build cannot proceed with an invalid config
|
|
159
172
|
throw error;
|
|
160
173
|
}
|
|
161
174
|
console.error(`Error loading config from ${configPath}:`, error);
|
package/dist/constants.d.ts
CHANGED
|
@@ -82,4 +82,30 @@ export declare const DEFAULT_BUNDLES: readonly [{
|
|
|
82
82
|
readonly entryPoint: "main.ts";
|
|
83
83
|
readonly bundleName: "main";
|
|
84
84
|
}];
|
|
85
|
+
/** Output directory for generated and copied images, relative to the build output */
|
|
86
|
+
export declare const IMAGES_OUTPUT_DIR = "_img";
|
|
87
|
+
/** Subdirectory of {@link IMAGES_OUTPUT_DIR} for generated Open Graph images */
|
|
88
|
+
export declare const OG_IMAGES_SUBDIR = "og";
|
|
89
|
+
/** Cache subdirectory (inside `.stati`) holding the images manifest */
|
|
90
|
+
export declare const IMAGES_CACHE_SUBDIR = "images";
|
|
91
|
+
/** Package that implements responsive images and Open Graph image generation */
|
|
92
|
+
export declare const IMAGES_PACKAGE_NAME = "@stati/images";
|
|
93
|
+
/** Default candidate widths for responsive images */
|
|
94
|
+
export declare const DEFAULT_IMAGE_WIDTHS: readonly number[];
|
|
95
|
+
/** Default modern formats for responsive images */
|
|
96
|
+
export declare const DEFAULT_IMAGE_FORMATS: readonly ('avif' | 'webp')[];
|
|
97
|
+
/** Default encoder quality per format; PNG stays lossless unless configured */
|
|
98
|
+
export declare const DEFAULT_IMAGE_QUALITY: {
|
|
99
|
+
readonly avif: 50;
|
|
100
|
+
readonly webp: 75;
|
|
101
|
+
readonly jpeg: 80;
|
|
102
|
+
};
|
|
103
|
+
/** Default `sizes` attribute for responsive images */
|
|
104
|
+
export declare const DEFAULT_IMAGE_SIZES = "100vw";
|
|
105
|
+
/** Default Open Graph image template name */
|
|
106
|
+
export declare const DEFAULT_OG_TEMPLATE = "og.eta";
|
|
107
|
+
/** Default Open Graph image width in pixels */
|
|
108
|
+
export declare const DEFAULT_OG_WIDTH = 1200;
|
|
109
|
+
/** Default Open Graph image height in pixels */
|
|
110
|
+
export declare const DEFAULT_OG_HEIGHT = 630;
|
|
85
111
|
//# sourceMappingURL=constants.d.ts.map
|
package/dist/constants.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,iDAAiD;AACjD,eAAO,MAAM,eAAe,SAAS,CAAC;AAEtC,+CAA+C;AAC/C,eAAO,MAAM,eAAe,SAAS,CAAC;AAEtC,sCAAsC;AACtC,eAAO,MAAM,kBAAkB,WAAW,CAAC;AAE3C,iCAAiC;AACjC,eAAO,MAAM,cAAc,WAAW,CAAC;AAEvC,4DAA4D;AAC5D,eAAO,MAAM,YAAY,UAAU,CAAC;AAEpC,kCAAkC;AAClC,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAIjD,qEAAqE;AACrE,eAAO,MAAM,sBAAsB,iCAAkC,CAAC;AAEtE,mCAAmC;AACnC,eAAO,MAAM,gBAAgB,iBAAiB,CAAC;AAE/C,2CAA2C;AAC3C,eAAO,MAAM,oBAAoB,UAEhC,CAAC;AAIF,sCAAsC;AACtC,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC,kCAAkC;AAClC,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAEzC,sCAAsC;AACtC,eAAO,MAAM,gBAAgB,cAAc,CAAC;AAE5C,0CAA0C;AAC1C,eAAO,MAAM,oBAAoB,0BAAmD,CAAC;AAIrF,wDAAwD;AACxD,eAAO,MAAM,mBAAmB,QAAQ,CAAC;AAEzC,+DAA+D;AAC/D,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C,qEAAqE;AACrE,eAAO,MAAM,wBAAwB,QAAQ,CAAC;AAI9C,4BAA4B;AAC5B,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC,0BAA0B;AAC1B,eAAO,MAAM,gBAAgB,QAA0B,CAAC;AAExD,yBAAyB;AACzB,eAAO,MAAM,eAAe,QAAwB,CAAC;AAErD,0BAA0B;AAC1B,eAAO,MAAM,gBAAgB,QAAsB,CAAC;AAEpD,8BAA8B;AAC9B,eAAO,MAAM,oBAAoB,QAAyB,CAAC;AAI3D,oDAAoD;AACpD,eAAO,MAAM,aAAa,QAAyB,CAAC;AAEpD,iDAAiD;AACjD,eAAO,MAAM,cAAc,QAA0B,CAAC;AAEtD,6CAA6C;AAC7C,eAAO,MAAM,UAAU,QAAmB,CAAC;AAE3C,iDAAiD;AACjD,eAAO,MAAM,WAAW,QAAuB,CAAC;AAEhD,uCAAuC;AACvC,eAAO,MAAM,WAAW,QAAsB,CAAC;AAE/C,2CAA2C;AAC3C,eAAO,MAAM,SAAS,QAAkB,CAAC;AAEzC,6CAA6C;AAC7C,eAAO,MAAM,UAAU,QAAmB,CAAC;AAI3C,kCAAkC;AAClC,eAAO,MAAM,kBAAkB,SAAS,CAAC;AAEzC,8BAA8B;AAC9B,eAAO,MAAM,kBAAkB,QAAQ,CAAC;AAExC,+BAA+B;AAC/B,eAAO,MAAM,eAAe,eAAgC,CAAC;AAE7D,mEAAmE;AACnE,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAIvC,yBAAyB;AACzB,eAAO,MAAM,kBAAkB,kBAAkB,CAAC;AAElD,8CAA8C;AAC9C,eAAO,MAAM,cAAc,UAAU,CAAC;AAItC,0CAA0C;AAC1C,eAAO,MAAM,kBAAkB,QAAQ,CAAC;AAExC,qEAAqE;AACrE,eAAO,MAAM,kBAAkB,YAAY,CAAC;AAE5C;;;GAGG;AACH,eAAO,MAAM,eAAe;;;EAA2D,CAAC"}
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"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"}
|
package/dist/constants.js
CHANGED
|
@@ -88,3 +88,26 @@ export const DEFAULT_TS_OUT_DIR = '_assets';
|
|
|
88
88
|
* Creates a single global bundle from 'main.ts' entry point.
|
|
89
89
|
*/
|
|
90
90
|
export const DEFAULT_BUNDLES = [{ entryPoint: 'main.ts', bundleName: 'main' }];
|
|
91
|
+
// === Image Constants ===
|
|
92
|
+
/** Output directory for generated and copied images, relative to the build output */
|
|
93
|
+
export const IMAGES_OUTPUT_DIR = '_img';
|
|
94
|
+
/** Subdirectory of {@link IMAGES_OUTPUT_DIR} for generated Open Graph images */
|
|
95
|
+
export const OG_IMAGES_SUBDIR = 'og';
|
|
96
|
+
/** Cache subdirectory (inside `.stati`) holding the images manifest */
|
|
97
|
+
export const IMAGES_CACHE_SUBDIR = 'images';
|
|
98
|
+
/** Package that implements responsive images and Open Graph image generation */
|
|
99
|
+
export const IMAGES_PACKAGE_NAME = '@stati/images';
|
|
100
|
+
/** Default candidate widths for responsive images */
|
|
101
|
+
export const DEFAULT_IMAGE_WIDTHS = [480, 960, 1440];
|
|
102
|
+
/** Default modern formats for responsive images */
|
|
103
|
+
export const DEFAULT_IMAGE_FORMATS = ['avif', 'webp'];
|
|
104
|
+
/** Default encoder quality per format; PNG stays lossless unless configured */
|
|
105
|
+
export const DEFAULT_IMAGE_QUALITY = { avif: 50, webp: 75, jpeg: 80 };
|
|
106
|
+
/** Default `sizes` attribute for responsive images */
|
|
107
|
+
export const DEFAULT_IMAGE_SIZES = '100vw';
|
|
108
|
+
/** Default Open Graph image template name */
|
|
109
|
+
export const DEFAULT_OG_TEMPLATE = 'og.eta';
|
|
110
|
+
/** Default Open Graph image width in pixels */
|
|
111
|
+
export const DEFAULT_OG_WIDTH = 1200;
|
|
112
|
+
/** Default Open Graph image height in pixels */
|
|
113
|
+
export const DEFAULT_OG_HEIGHT = 630;
|
package/dist/core/build.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/core/build.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/core/build.ts"],"names":[],"mappings":"AAuEA,OAAO,KAAK,EAEV,UAAU,EACV,MAAM,EASP,MAAM,mBAAmB,CAAC;AAK3B,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGxE;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,gCAAgC;IAChC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,YAAY;IAC3B,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,uCAAuC;IACvC,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACpC,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,8BAA8B;IAC9B,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,+BAA+B;IAC/B,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,iCAAiC;IACjC,OAAO,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;IACrC,uEAAuE;IACvE,eAAe,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACtC,sGAAsG;IACtG,gBAAgB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,mFAAmF;IACnF,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,WAAY,SAAQ,UAAU;IAC7C,wDAAwD;IACxD,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAkGD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAsB5E"}
|
package/dist/core/build.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import { ensureDir, writeFile, remove, pathExists, stat, readdir, copyFile, resolveOutDir, resolveStaticDir, resolveCacheDir, enableInventoryTracking, disableInventoryTracking, clearInventory, writeTailwindClassInventory, getInventorySize, isTailwindUsed, loadPreviousInventory, compileTypeScript, detectExistingBundles, autoInjectBundles, getBundlePathsForPage, formatBytes, } from './utils/index.js';
|
|
1
|
+
import { ensureDir, writeFile, remove, pathExists, stat, readdir, copyFile, resolveOutDir, resolveStaticDir, resolveCacheDir, enableInventoryTracking, disableInventoryTracking, clearInventory, writeTailwindClassInventory, getInventorySize, isTailwindUsed, loadPreviousInventory, compileTypeScript, detectExistingBundles, autoInjectBundles, getBundlePathsForPage, formatBytes, isVirtualPage, getPageOutputFile, } from './utils/index.js';
|
|
2
2
|
import { join, dirname, relative, posix } from 'node:path';
|
|
3
3
|
import { performance } from 'node:perf_hooks';
|
|
4
4
|
import { loadConfig } from '../config/loader.js';
|
|
5
5
|
import { loadContent } from './content.js';
|
|
6
|
+
import { expandPages } from './expand.js';
|
|
7
|
+
import { createSummaryHasher, resolveCollectionPath, resolveSummaryFields, } from './collections.js';
|
|
6
8
|
import { createMarkdownProcessor, renderMarkdown, extractToc } from './markdown.js';
|
|
7
9
|
import { createTemplateEngine, renderPage } from './templates.js';
|
|
8
10
|
import { buildNavigation } from './navigation.js';
|
|
9
|
-
import { loadCacheManifest, saveCacheManifest, shouldRebuildPage, createCacheEntry, updateCacheEntry, withBuildLock, computeNavigationHash, computeContentHash, clearFileHashCache, clearTemplatePathCache, } from './isg/index.js';
|
|
11
|
+
import { loadCacheManifest, saveCacheManifest, shouldRebuildPage, createCacheEntry, updateCacheEntry, hasRecordedReads, pruneOrphanedPages, withBuildLock, computeNavigationHash, computeContentHash, clearFileHashCache, clearTemplatePathCache, } from './isg/index.js';
|
|
10
12
|
import { generateSitemap, generateRobotsTxtFromConfig, autoInjectSEO, } from '../seo/index.js';
|
|
11
13
|
import { generateRSSFeeds, validateRSSConfig } from '../rss/index.js';
|
|
12
|
-
import { computeSearchIndexFilename, generateSearchIndex, writeSearchIndex, autoInjectSearchMeta, } from '../search/index.js';
|
|
14
|
+
import { computeSearchIndexFilename, generateSearchIndex, writeSearchIndex, autoInjectSearchMeta, isGeneratedPageExcludedFromSearch, } from '../search/index.js';
|
|
13
15
|
import { getEnv, isDevelopment } from '../env.js';
|
|
14
16
|
import { DEFAULT_OUT_DIR } from '../constants.js';
|
|
17
|
+
import { ImageRuntime } from '../images/runtime.js';
|
|
15
18
|
import { createMetricRecorder, noopMetricRecorder } from '../metrics/index.js';
|
|
16
19
|
/**
|
|
17
20
|
* Recursively calculates the total size of a directory in bytes.
|
|
@@ -179,12 +182,18 @@ async function setupCacheAndManifest(cacheDir) {
|
|
|
179
182
|
return { manifest };
|
|
180
183
|
}
|
|
181
184
|
/**
|
|
182
|
-
* Loads content and builds navigation.
|
|
185
|
+
* Loads content, expands generated and paginated pages, and builds navigation.
|
|
183
186
|
*/
|
|
184
187
|
async function loadContentAndBuildNavigation(config, options, logger) {
|
|
185
|
-
// Load all content
|
|
186
|
-
|
|
187
|
-
|
|
188
|
+
// Load all content, then replace [param].md templates with their pages and add the
|
|
189
|
+
// pages of paginated listings. Navigation, SEO, sitemap and search see the result.
|
|
190
|
+
const loaded = await loadContent(config, options.includeDrafts);
|
|
191
|
+
const pages = await expandPages(loaded, config, logger);
|
|
192
|
+
const virtualCount = pages.filter((page) => isVirtualPage(page)).length;
|
|
193
|
+
logger.status(`Found ${pages.length - virtualCount} pages`);
|
|
194
|
+
if (virtualCount > 0) {
|
|
195
|
+
logger.status(`Generated ${virtualCount} more from templates and pagination`);
|
|
196
|
+
}
|
|
188
197
|
// Build navigation from pages
|
|
189
198
|
if (logger.step) {
|
|
190
199
|
console.log(); // Add spacing before navigation step
|
|
@@ -201,12 +210,35 @@ async function loadContentAndBuildNavigation(config, options, logger) {
|
|
|
201
210
|
// Create processors
|
|
202
211
|
const md = await createMarkdownProcessor(config);
|
|
203
212
|
const eta = createTemplateEngine(config);
|
|
204
|
-
return { pages, navigation, md, eta, navigationHash };
|
|
213
|
+
return { pages, markdownFiles: loaded.length, navigation, md, eta, navigationHash };
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Output file and ISG cache key of a page.
|
|
217
|
+
*/
|
|
218
|
+
function getPageOutput(page, outDir) {
|
|
219
|
+
const outputPath = join(outDir, getPageOutputFile(page.url));
|
|
220
|
+
// Cache key is the output path relative to outDir
|
|
221
|
+
const relativePath = relative(outDir, outputPath).replace(/\\/g, '/');
|
|
222
|
+
const cacheKey = relativePath.startsWith('/') ? relativePath : `/${relativePath}`;
|
|
223
|
+
return { outputPath, cacheKey };
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Hashes a value for the render inputs, or returns a value that never matches when it
|
|
227
|
+
* cannot be serialized, so the page rebuilds instead of keeping stale output.
|
|
228
|
+
*/
|
|
229
|
+
function hashableData(value) {
|
|
230
|
+
try {
|
|
231
|
+
JSON.stringify(value);
|
|
232
|
+
return value ?? null;
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return `unserializable:${Date.now()}:${Math.random()}`;
|
|
236
|
+
}
|
|
205
237
|
}
|
|
206
238
|
/**
|
|
207
239
|
* Processes pages with ISG caching logic.
|
|
208
240
|
*/
|
|
209
|
-
async function processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder = noopMetricRecorder, searchIndexFilename) {
|
|
241
|
+
async function processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder = noopMetricRecorder, searchIndexFilename, imageRuntime = null, navigationHash = computeNavigationHash(navigation)) {
|
|
210
242
|
let cacheHits = 0;
|
|
211
243
|
let cacheMisses = 0;
|
|
212
244
|
const searchablePages = [];
|
|
@@ -218,6 +250,18 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
|
|
|
218
250
|
await config.hooks.beforeAll(buildContext);
|
|
219
251
|
endHookBeforeAll();
|
|
220
252
|
}
|
|
253
|
+
// Collection summaries, hashed after beforeAll because the hook can change front matter
|
|
254
|
+
const summaries = createSummaryHasher(pages, resolveSummaryFields(config));
|
|
255
|
+
// The pages of a listing share one array of page URLs, hashed once for all of them
|
|
256
|
+
const urlListHashes = new WeakMap();
|
|
257
|
+
const hashUrlList = (urls) => {
|
|
258
|
+
let hash = urlListHashes.get(urls);
|
|
259
|
+
if (hash === undefined) {
|
|
260
|
+
hash = computeContentHash('', { urls });
|
|
261
|
+
urlListHashes.set(urls, hash);
|
|
262
|
+
}
|
|
263
|
+
return hash;
|
|
264
|
+
};
|
|
221
265
|
// Render each page with progress tracking and ISG
|
|
222
266
|
if (logger.step) {
|
|
223
267
|
logger.step(2, 3, 'Rendering pages');
|
|
@@ -236,24 +280,14 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
|
|
|
236
280
|
const isDevMode = isDevelopment();
|
|
237
281
|
const pendingWrites = [];
|
|
238
282
|
const pendingCacheUpdates = [];
|
|
283
|
+
// Image settings that change generated HTML, part of every page's render inputs
|
|
284
|
+
const imageFingerprint = imageRuntime?.fingerprint();
|
|
239
285
|
for (let i = 0; i < pages.length; i++) {
|
|
240
286
|
const page = pages[i];
|
|
241
287
|
if (!page)
|
|
242
288
|
continue; // Safety check
|
|
243
|
-
// Determine output path
|
|
244
|
-
|
|
245
|
-
if (page.url === '/') {
|
|
246
|
-
outputPath = join(outDir, 'index.html');
|
|
247
|
-
}
|
|
248
|
-
else if (page.url.endsWith('/')) {
|
|
249
|
-
outputPath = join(outDir, page.url, 'index.html');
|
|
250
|
-
}
|
|
251
|
-
else {
|
|
252
|
-
outputPath = join(outDir, `${page.url}.html`);
|
|
253
|
-
}
|
|
254
|
-
// Get cache key (use output path relative to outDir)
|
|
255
|
-
const relativePath = relative(outDir, outputPath).replace(/\\/g, '/');
|
|
256
|
-
const cacheKey = relativePath.startsWith('/') ? relativePath : `/${relativePath}`;
|
|
289
|
+
// Determine output path and cache key
|
|
290
|
+
const { outputPath, cacheKey } = getPageOutput(page, outDir);
|
|
257
291
|
const existingEntry = manifest.entries[cacheKey];
|
|
258
292
|
const bundlePaths = getBundlePathsForPage(page.url, compiledBundles);
|
|
259
293
|
const assets = {
|
|
@@ -263,16 +297,58 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
|
|
|
263
297
|
searchIndexPath: `/${searchIndexFilename}`,
|
|
264
298
|
}),
|
|
265
299
|
};
|
|
266
|
-
|
|
300
|
+
// Collection the page sees as stati.collection; a page whose collection appears,
|
|
301
|
+
// disappears or changes rebuilds even when it never read it
|
|
302
|
+
const collectionPath = resolveCollectionPath(page, pages);
|
|
303
|
+
const staticRenderInputs = {
|
|
267
304
|
...assets,
|
|
268
305
|
bundleAutoInject: config.typescript?.autoInject !== false,
|
|
269
306
|
searchAutoInject: config.search?.autoInjectMetaTag !== false,
|
|
307
|
+
// Omitted with images: false, so the hash matches builds without the image pipeline
|
|
308
|
+
...(imageFingerprint && { images: imageFingerprint }),
|
|
309
|
+
collection: collectionPath ?? null,
|
|
310
|
+
// A listing page depends on its own slice and on the page count, which every page
|
|
311
|
+
// shows in its numbered links
|
|
312
|
+
...(page.pagination && {
|
|
313
|
+
pagination: {
|
|
314
|
+
pageNumber: page.pagination.pageNumber,
|
|
315
|
+
totalPages: page.pagination.totalPages,
|
|
316
|
+
urls: hashUrlList(page.pagination.urls),
|
|
317
|
+
items: summaries.pages(page.pagination.items),
|
|
318
|
+
},
|
|
319
|
+
}),
|
|
320
|
+
// A generated page depends on its own items only, not on the whole collection
|
|
321
|
+
...(page.generated && {
|
|
322
|
+
generated: {
|
|
323
|
+
key: page.generated.key,
|
|
324
|
+
label: page.generated.label,
|
|
325
|
+
template: page.generated.template,
|
|
326
|
+
items: summaries.pages(page.generated.items),
|
|
327
|
+
data: hashableData(page.generated.data),
|
|
328
|
+
},
|
|
329
|
+
}),
|
|
330
|
+
};
|
|
331
|
+
// The data a render read: the collection summary and the navigation tree
|
|
332
|
+
const renderInputsHashFor = (reads) => computeContentHash('', {
|
|
333
|
+
...staticRenderInputs,
|
|
334
|
+
...(reads?.collection && {
|
|
335
|
+
collectionSummary: collectionPath !== undefined ? summaries.collection(collectionPath) : 'none',
|
|
336
|
+
}),
|
|
337
|
+
...(reads?.nav && { navigation: navigationHash }),
|
|
270
338
|
});
|
|
339
|
+
// Entries written before Stati recorded what pages read are rebuilt once to record it
|
|
340
|
+
const readsKnown = existingEntry !== undefined && hasRecordedReads(existingEntry);
|
|
341
|
+
const renderInputsHash = renderInputsHashFor(readsKnown
|
|
342
|
+
? { collection: existingEntry.readsCollection, nav: existingEntry.readsNav }
|
|
343
|
+
: undefined);
|
|
271
344
|
// Check if we should rebuild this page (considering ISG logic)
|
|
272
345
|
const shouldRebuildStart = performance.now();
|
|
273
346
|
const shouldRebuild = options.force ||
|
|
274
347
|
config.isg?.enabled === false ||
|
|
275
348
|
!(await pathExists(outputPath)) ||
|
|
349
|
+
(existingEntry !== undefined && !readsKnown) ||
|
|
350
|
+
// A cached page whose generated images are gone is rebuilt to regenerate them
|
|
351
|
+
(imageRuntime !== null && !imageRuntime.outputsExist(existingEntry?.images)) ||
|
|
276
352
|
(await shouldRebuildPage(page, existingEntry, config, pages, buildTime, renderInputsHash));
|
|
277
353
|
const shouldRebuildTime = performance.now() - shouldRebuildStart;
|
|
278
354
|
totalShouldRebuildTime += shouldRebuildTime;
|
|
@@ -286,7 +362,7 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
|
|
|
286
362
|
recorder.recordPageTiming(page.url, 0, true);
|
|
287
363
|
// Collect searchable page data for cached pages if search is enabled
|
|
288
364
|
// Extract TOC without full HTML rendering for efficiency
|
|
289
|
-
if (config.search?.enabled === true) {
|
|
365
|
+
if (config.search?.enabled === true && !isGeneratedPageExcludedFromSearch(page)) {
|
|
290
366
|
const tocEnabled = config.markdown?.toc !== false;
|
|
291
367
|
const toc = tocEnabled ? extractToc(page.content, md) : [];
|
|
292
368
|
searchablePages.push({ page, toc, markdownContent: page.content });
|
|
@@ -302,12 +378,16 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
|
|
|
302
378
|
await config.hooks.beforeRender({ page, config });
|
|
303
379
|
recorder.addToPhase('hookBeforeRenderTotalMs', performance.now() - hookStart);
|
|
304
380
|
}
|
|
381
|
+
// Image state for this page: markup, dependencies and generated files
|
|
382
|
+
const pageImages = imageRuntime?.createPageContext(page);
|
|
305
383
|
// Render markdown to HTML with TOC extraction
|
|
306
384
|
const tocEnabled = config.markdown?.toc !== false;
|
|
307
|
-
const { html: htmlContent, toc } = renderMarkdown(page.content, md, tocEnabled
|
|
385
|
+
const { html: htmlContent, toc } = renderMarkdown(page.content, md, tocEnabled, {
|
|
386
|
+
images: pageImages,
|
|
387
|
+
});
|
|
308
388
|
// Render with template
|
|
309
389
|
const renderPageStart = performance.now();
|
|
310
|
-
const renderResult = await renderPage(page, htmlContent, config, eta, navigation, pages, assets, toc, logger);
|
|
390
|
+
const renderResult = await renderPage(page, htmlContent, config, eta, navigation, pages, assets, toc, logger, { images: pageImages });
|
|
311
391
|
const renderPageTime = performance.now() - renderPageStart;
|
|
312
392
|
totalRenderTime += renderPageTime;
|
|
313
393
|
let finalHtml = renderResult.html;
|
|
@@ -324,6 +404,9 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
|
|
|
324
404
|
if (config.seo?.debug !== undefined) {
|
|
325
405
|
injectOptions.debug = config.seo.debug;
|
|
326
406
|
}
|
|
407
|
+
if (renderResult.ogImage) {
|
|
408
|
+
injectOptions.generatedImage = renderResult.ogImage;
|
|
409
|
+
}
|
|
327
410
|
finalHtml = autoInjectSEO(finalHtml, injectOptions);
|
|
328
411
|
}
|
|
329
412
|
// Auto-inject search index meta tag if enabled (default: true)
|
|
@@ -336,6 +419,17 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
|
|
|
336
419
|
if (config.typescript?.autoInject !== false && assets.bundlePaths.length > 0) {
|
|
337
420
|
finalHtml = autoInjectBundles(finalHtml, assets.bundlePaths);
|
|
338
421
|
}
|
|
422
|
+
// Start the image jobs the final HTML needs and record its image files for the cache,
|
|
423
|
+
// along with what the render read, which decides the hash it is stored under
|
|
424
|
+
const committedImages = pageImages && imageRuntime
|
|
425
|
+
? imageRuntime.commitPage(pageImages, finalHtml, { outputPath, cacheKey })
|
|
426
|
+
: undefined;
|
|
427
|
+
const renderedAssets = {
|
|
428
|
+
assetDeps: committedImages?.assetDeps ?? [],
|
|
429
|
+
images: committedImages?.images ?? [],
|
|
430
|
+
reads: renderResult.reads,
|
|
431
|
+
};
|
|
432
|
+
const renderedInputsHash = renderInputsHashFor(renderResult.reads);
|
|
339
433
|
const renderTime = Math.round(performance.now() - startTime);
|
|
340
434
|
// Record page timing for rendered pages (includes template count)
|
|
341
435
|
recorder.recordPageTiming(page.url, renderTime, false, renderResult.templatesLoaded);
|
|
@@ -345,7 +439,13 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
|
|
|
345
439
|
// In dev mode, batch writes for parallel execution to avoid Windows filesystem slowdown
|
|
346
440
|
if (isDevMode) {
|
|
347
441
|
pendingWrites.push({ outputPath, content: finalHtml });
|
|
348
|
-
pendingCacheUpdates.push({
|
|
442
|
+
pendingCacheUpdates.push({
|
|
443
|
+
cacheKey,
|
|
444
|
+
page,
|
|
445
|
+
existingEntry,
|
|
446
|
+
renderInputsHash: renderedInputsHash,
|
|
447
|
+
renderedAssets,
|
|
448
|
+
});
|
|
349
449
|
}
|
|
350
450
|
else {
|
|
351
451
|
// Production mode: write immediately
|
|
@@ -357,17 +457,17 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
|
|
|
357
457
|
if (!options.skipCacheUpdate) {
|
|
358
458
|
const cacheEntryStart = performance.now();
|
|
359
459
|
if (existingEntry) {
|
|
360
|
-
manifest.entries[cacheKey] = await updateCacheEntry(existingEntry, page, config, pages, buildTime, false,
|
|
460
|
+
manifest.entries[cacheKey] = await updateCacheEntry(existingEntry, page, config, pages, buildTime, false, renderedInputsHash, renderedAssets);
|
|
361
461
|
}
|
|
362
462
|
else {
|
|
363
|
-
manifest.entries[cacheKey] = await createCacheEntry(page, config, pages, buildTime,
|
|
463
|
+
manifest.entries[cacheKey] = await createCacheEntry(page, config, pages, buildTime, renderedInputsHash, renderedAssets);
|
|
364
464
|
}
|
|
365
465
|
totalCacheEntryTime += performance.now() - cacheEntryStart;
|
|
366
466
|
}
|
|
367
467
|
}
|
|
368
468
|
// Collect searchable page data if search is enabled
|
|
369
469
|
// Uses TOC entries and markdown content instead of parsing rendered HTML
|
|
370
|
-
if (config.search?.enabled === true) {
|
|
470
|
+
if (config.search?.enabled === true && !isGeneratedPageExcludedFromSearch(page)) {
|
|
371
471
|
searchablePages.push({ page, toc, markdownContent: page.content });
|
|
372
472
|
}
|
|
373
473
|
// Run afterRender hook
|
|
@@ -390,14 +490,14 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
|
|
|
390
490
|
// This avoids expensive template dependency tracking on each incremental rebuild
|
|
391
491
|
if (!options.skipCacheUpdate) {
|
|
392
492
|
const cacheEntryStart = performance.now();
|
|
393
|
-
for (const { cacheKey, page, existingEntry, renderInputsHash } of pendingCacheUpdates) {
|
|
493
|
+
for (const { cacheKey, page, existingEntry, renderInputsHash, renderedAssets, } of pendingCacheUpdates) {
|
|
394
494
|
if (existingEntry) {
|
|
395
495
|
// Fast update: reuse existing deps, just update hashes and timestamp
|
|
396
496
|
manifest.entries[cacheKey] = await updateCacheEntry(existingEntry, page, config, pages, buildTime, true, // fastUpdate = true in dev mode
|
|
397
|
-
renderInputsHash);
|
|
497
|
+
renderInputsHash, renderedAssets);
|
|
398
498
|
}
|
|
399
499
|
else {
|
|
400
|
-
manifest.entries[cacheKey] = await createCacheEntry(page, config, pages, buildTime, renderInputsHash);
|
|
500
|
+
manifest.entries[cacheKey] = await createCacheEntry(page, config, pages, buildTime, renderInputsHash, renderedAssets);
|
|
401
501
|
}
|
|
402
502
|
}
|
|
403
503
|
totalCacheEntryTime = performance.now() - cacheEntryStart;
|
|
@@ -501,6 +601,14 @@ async function buildInternal(options = {}) {
|
|
|
501
601
|
await remove(cacheDir);
|
|
502
602
|
}
|
|
503
603
|
await ensureDir(outDir);
|
|
604
|
+
// Image pipeline; null with images: false. Fails here, before any page renders, when a
|
|
605
|
+
// feature needs @stati/images and it is missing or incompatible.
|
|
606
|
+
const imageRuntime = await ImageRuntime.create(config, {
|
|
607
|
+
outDir,
|
|
608
|
+
cacheDir,
|
|
609
|
+
logger,
|
|
610
|
+
development: isDevelopment(),
|
|
611
|
+
});
|
|
504
612
|
// Enable Tailwind class inventory tracking only if Tailwind is detected
|
|
505
613
|
const tailwindInitStart = performance.now();
|
|
506
614
|
const hasTailwind = await isTailwindUsed();
|
|
@@ -534,14 +642,13 @@ async function buildInternal(options = {}) {
|
|
|
534
642
|
console.log(); // Add spacing before content loading
|
|
535
643
|
}
|
|
536
644
|
const endContentSpan = recorder.startSpan('contentDiscoveryMs');
|
|
537
|
-
const { pages, navigation, md, eta, navigationHash } = await loadContentAndBuildNavigation(config, options, logger);
|
|
645
|
+
const { pages, markdownFiles, navigation, md, eta, navigationHash } = await loadContentAndBuildNavigation(config, options, logger);
|
|
538
646
|
endContentSpan();
|
|
539
|
-
// Record content discovery counts.
|
|
540
|
-
//
|
|
541
|
-
// because loadContent() only processes *.md files, so all pages are markdown files.
|
|
647
|
+
// Record content discovery counts. Pages include the ones generated from [param].md
|
|
648
|
+
// templates and pagination; Markdown files include the templates.
|
|
542
649
|
recorder.increment('totalPages', pages.length);
|
|
543
|
-
recorder.increment('markdownFilesProcessed',
|
|
544
|
-
//
|
|
650
|
+
recorder.increment('markdownFilesProcessed', markdownFiles);
|
|
651
|
+
// Recorded in the manifest; pages that read the navigation also hash it into their inputs
|
|
545
652
|
manifest.navigationHash = navigationHash;
|
|
546
653
|
// Compile TypeScript if enabled
|
|
547
654
|
// In dev mode, skip compilation since esbuild watcher handles it
|
|
@@ -593,9 +700,40 @@ async function buildInternal(options = {}) {
|
|
|
593
700
|
}
|
|
594
701
|
const endPageRenderSpan = recorder.startSpan('pageRenderingMs');
|
|
595
702
|
const buildTime = new Date();
|
|
596
|
-
const pageProcessingResult = await processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder, searchIndexFilename);
|
|
703
|
+
const pageProcessingResult = await processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder, searchIndexFilename, imageRuntime, navigationHash);
|
|
597
704
|
endPageRenderSpan();
|
|
598
705
|
const { cacheHits, cacheMisses, searchablePages } = pageProcessingResult;
|
|
706
|
+
// Remove the output of pages that no longer exist, such as a deleted post or a tag no post
|
|
707
|
+
// uses anymore. Runs before static assets are copied, so a file in the static directory at
|
|
708
|
+
// the same path is copied afterwards.
|
|
709
|
+
const liveCacheKeys = new Set(pages.map((page) => getPageOutput(page, outDir).cacheKey));
|
|
710
|
+
const pruned = await pruneOrphanedPages(manifest, liveCacheKeys, outDir);
|
|
711
|
+
for (const failure of pruned.failed) {
|
|
712
|
+
logger.warning(`Could not remove the stale page ${failure.key}: ${failure.message}`);
|
|
713
|
+
}
|
|
714
|
+
if (pruned.removed.length > 0) {
|
|
715
|
+
const count = pruned.removed.length;
|
|
716
|
+
logger.info(count === 1
|
|
717
|
+
? 'Removed 1 page that no longer exists'
|
|
718
|
+
: `Removed ${count} pages that no longer exist`);
|
|
719
|
+
}
|
|
720
|
+
// Wait for image jobs; pages whose images failed are rewritten to show plain images
|
|
721
|
+
let imageFailures = [];
|
|
722
|
+
if (imageRuntime) {
|
|
723
|
+
const endImageSpan = recorder.startSpan('imageSettleMs');
|
|
724
|
+
const settled = await imageRuntime.settle(manifest);
|
|
725
|
+
await imageRuntime.save();
|
|
726
|
+
endImageSpan();
|
|
727
|
+
imageFailures = settled.failures;
|
|
728
|
+
const { generated, cached, failed } = imageRuntime.stats;
|
|
729
|
+
recorder.increment('imagesGenerated', generated);
|
|
730
|
+
recorder.increment('imagesCached', cached);
|
|
731
|
+
recorder.increment('imagesFailed', failed);
|
|
732
|
+
if (generated > 0 || failed > 0) {
|
|
733
|
+
logger.info('');
|
|
734
|
+
logger.info(`Images: ${generated} generated, ${cached} reused${failed > 0 ? `, ${failed} failed` : ''}`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
599
737
|
// Generate search index if enabled
|
|
600
738
|
// Uses markdown content and TOC entries
|
|
601
739
|
let searchIndexMetadata;
|
|
@@ -638,6 +776,13 @@ async function buildInternal(options = {}) {
|
|
|
638
776
|
await saveCacheManifest(cacheDir, manifest);
|
|
639
777
|
}
|
|
640
778
|
endManifestSaveSpan();
|
|
779
|
+
// In the dev server a failed Open Graph image goes to the error overlay, like a template error
|
|
780
|
+
const ogFailures = imageFailures.filter((failure) => failure.og);
|
|
781
|
+
if (isDevMode && ogFailures.length > 0) {
|
|
782
|
+
throw new Error(ogFailures
|
|
783
|
+
.map((failure) => `Open Graph image failed for ${failure.label}: ${failure.message}`)
|
|
784
|
+
.join('\n'));
|
|
785
|
+
}
|
|
641
786
|
// Copy static assets and count them (skip for template-only changes in dev mode)
|
|
642
787
|
const endAssetSpan = recorder.startSpan('assetCopyMs');
|
|
643
788
|
let assetsCount = 0;
|
|
@@ -646,6 +791,21 @@ async function buildInternal(options = {}) {
|
|
|
646
791
|
}
|
|
647
792
|
endAssetSpan();
|
|
648
793
|
recorder.increment('assetsCopied', assetsCount);
|
|
794
|
+
// Delete generated images no page of this build uses. Production only: dev builds skip
|
|
795
|
+
// responsive and Open Graph images by default and must not delete them.
|
|
796
|
+
if (imageRuntime && getEnv() === 'production') {
|
|
797
|
+
const liveImages = new Set();
|
|
798
|
+
const liveSources = new Set();
|
|
799
|
+
for (const page of pages) {
|
|
800
|
+
const entry = manifest.entries[getPageOutput(page, outDir).cacheKey];
|
|
801
|
+
entry?.images?.forEach((image) => liveImages.add(image));
|
|
802
|
+
entry?.assetDeps?.forEach((source) => liveSources.add(source));
|
|
803
|
+
}
|
|
804
|
+
const removed = await imageRuntime.prune(liveImages, liveSources);
|
|
805
|
+
if (removed > 0) {
|
|
806
|
+
logger.info(`Removed ${removed} unused generated image${removed === 1 ? '' : 's'}`);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
649
809
|
// Get current environment
|
|
650
810
|
const currentEnv = getEnv();
|
|
651
811
|
// Generate sitemap if enabled (only in production mode)
|