@stati/core 1.27.1 → 1.28.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/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 +98 -26
- package/dist/core/isg/builder.d.ts +21 -6
- package/dist/core/isg/builder.d.ts.map +1 -1
- package/dist/core/isg/builder.js +59 -17
- package/dist/core/isg/deps.d.ts +23 -2
- package/dist/core/isg/deps.d.ts.map +1 -1
- package/dist/core/isg/deps.js +54 -8
- package/dist/core/isg/hash.d.ts +10 -0
- package/dist/core/isg/hash.d.ts.map +1 -1
- package/dist/core/isg/hash.js +14 -0
- package/dist/core/isg/index.d.ts +2 -1
- package/dist/core/isg/index.d.ts.map +1 -1
- package/dist/core/isg/index.js +1 -1
- package/dist/core/isg/manifest.js +8 -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/templates.d.ts +12 -1
- package/dist/core/templates.d.ts.map +1 -1
- package/dist/core/templates.js +61 -6
- package/dist/core/utils/fs.utils.d.ts +21 -1
- package/dist/core/utils/fs.utils.d.ts.map +1 -1
- package/dist/core/utils/fs.utils.js +61 -0
- package/dist/core/utils/index.d.ts +1 -1
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +1 -1
- package/dist/core/utils/template-discovery.utils.d.ts +8 -6
- package/dist/core/utils/template-discovery.utils.d.ts.map +1 -1
- package/dist/core/utils/template-discovery.utils.js +43 -12
- 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 +1 -0
- 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/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 +5 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/content.d.ts +21 -2
- 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 +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/isg.d.ts +7 -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/dist/core/templates.js
CHANGED
|
@@ -5,7 +5,22 @@ import { createFallbackLogger } from './utils/index.js';
|
|
|
5
5
|
import { TEMPLATE_EXTENSION } from '../constants.js';
|
|
6
6
|
import { getStatiVersion, isCollectionIndexPage, discoverLayout, getCollectionPathForPage, resolveSrcDir, createTemplateError, createValidatingPartialsProxy, createValidatingContextProxy, propValue, wrapPartialsAsCallable, createNavigationHelpers, } from './utils/index.js';
|
|
7
7
|
import { isDevelopment, isProduction } from '../env.js';
|
|
8
|
-
import { generateSEO } from '../seo/index.js';
|
|
8
|
+
import { generateSEO, resolveGeneratedImagePlaceholders } from '../seo/index.js';
|
|
9
|
+
/**
|
|
10
|
+
* `stati.image()` when images are turned off: a plain `<img>`, attributes escaped.
|
|
11
|
+
*/
|
|
12
|
+
function renderPlainImage(src, options = {}) {
|
|
13
|
+
if (typeof src !== 'string' || !src.trim()) {
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
const attributes = [
|
|
17
|
+
`src="${escapeHtml(src)}"`,
|
|
18
|
+
`alt="${escapeHtml(options.alt ?? '')}"`,
|
|
19
|
+
...(options.title ? [`title="${escapeHtml(options.title)}"`] : []),
|
|
20
|
+
...(options.class ? [`class="${escapeHtml(options.class)}"`] : []),
|
|
21
|
+
];
|
|
22
|
+
return `<img ${attributes.join(' ')}>`;
|
|
23
|
+
}
|
|
9
24
|
/**
|
|
10
25
|
* Groups pages by their tags for aggregation purposes.
|
|
11
26
|
*
|
|
@@ -215,8 +230,15 @@ export function createTemplateEngine(config) {
|
|
|
215
230
|
}
|
|
216
231
|
return eta;
|
|
217
232
|
}
|
|
218
|
-
export async function renderPage(page, body, config, eta, navigation, allPages, assets, toc, logger) {
|
|
233
|
+
export async function renderPage(page, body, config, eta, navigation, allPages, assets, toc, logger, extras) {
|
|
219
234
|
const log = logger || createFallbackLogger();
|
|
235
|
+
const pageImages = extras?.images;
|
|
236
|
+
// The generated Open Graph image is known once the og template has rendered, after the
|
|
237
|
+
// partials. Until then stati.generateSEO() emits placeholders for its tags.
|
|
238
|
+
const ogState = {
|
|
239
|
+
image: undefined,
|
|
240
|
+
pending: pageImages?.wantsOg() ?? false,
|
|
241
|
+
};
|
|
220
242
|
// Discover partials for this page's directory hierarchy
|
|
221
243
|
const srcDir = resolveSrcDir(config);
|
|
222
244
|
const relativePath = relative(srcDir, page.sourcePath);
|
|
@@ -274,12 +296,24 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
|
|
|
274
296
|
version: getStatiVersion(),
|
|
275
297
|
},
|
|
276
298
|
// Stati utilities object with helper functions
|
|
277
|
-
generateSEO: (tags) => generateSEO({
|
|
299
|
+
generateSEO: (tags) => generateSEO({
|
|
300
|
+
page,
|
|
301
|
+
config,
|
|
302
|
+
site: config.site,
|
|
303
|
+
generatedImage: ogState.image,
|
|
304
|
+
deferGeneratedImage: ogState.pending,
|
|
305
|
+
}, tags),
|
|
278
306
|
propValue,
|
|
307
|
+
// Image with dimensions, lazy loading and, with images.responsive, srcset variants
|
|
308
|
+
image: (src, options) => pageImages ? pageImages.renderHelperImage(src, options) : renderPlainImage(src, options),
|
|
279
309
|
assets, // TypeScript bundle assets (available when typescript.enabled is true)
|
|
280
310
|
// Set by callable partials invoked with props; always present so that
|
|
281
311
|
// stati.props?.x works under the dev-mode context validation
|
|
282
312
|
props: undefined,
|
|
313
|
+
// Set while the Open Graph template renders: { width, height }
|
|
314
|
+
og: undefined,
|
|
315
|
+
// The page's generated Open Graph image, once rendered: { url, path, width, height, alt }
|
|
316
|
+
ogImage: undefined,
|
|
283
317
|
};
|
|
284
318
|
// Render partials and store their content
|
|
285
319
|
// Use multiple passes to allow partials to reference other partials
|
|
@@ -345,6 +379,23 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
|
|
|
345
379
|
break;
|
|
346
380
|
}
|
|
347
381
|
}
|
|
382
|
+
// Render the Open Graph template with the layout's context, minus the page body, so the
|
|
383
|
+
// generated image is known before the layout renders its SEO tags
|
|
384
|
+
if (pageImages && ogState.pending) {
|
|
385
|
+
const ogBaseContext = {
|
|
386
|
+
...baseContext,
|
|
387
|
+
content: '',
|
|
388
|
+
page: { ...baseContext.page, content: '' },
|
|
389
|
+
og: pageImages.ogSize(),
|
|
390
|
+
};
|
|
391
|
+
const ogContext = createValidatingContextProxy({
|
|
392
|
+
...ogBaseContext,
|
|
393
|
+
partials: createValidatingPartialsProxy(wrapPartialsAsCallable(eta, renderedPartials, partialPaths, ogBaseContext)),
|
|
394
|
+
});
|
|
395
|
+
ogState.image = (await pageImages.renderOg(ogContext, eta)) ?? undefined;
|
|
396
|
+
ogState.pending = false;
|
|
397
|
+
baseContext.ogImage = ogState.image;
|
|
398
|
+
}
|
|
348
399
|
// Wrap final rendered partials as callable before passing to layout context
|
|
349
400
|
const callablePartials = wrapPartialsAsCallable(eta, renderedPartials, partialPaths, baseContext);
|
|
350
401
|
const context = createValidatingContextProxy({
|
|
@@ -354,10 +405,14 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
|
|
|
354
405
|
try {
|
|
355
406
|
if (!layoutPath) {
|
|
356
407
|
log.warning('No layout template found, using fallback');
|
|
357
|
-
return { html: createFallbackHtml(page, body), templatesLoaded };
|
|
408
|
+
return { html: createFallbackHtml(page, body), templatesLoaded, ogImage: ogState.image };
|
|
358
409
|
}
|
|
359
410
|
const html = await eta.renderAsync(layoutPath, context);
|
|
360
|
-
return {
|
|
411
|
+
return {
|
|
412
|
+
html: resolveGeneratedImagePlaceholders(html, ogState.image),
|
|
413
|
+
templatesLoaded,
|
|
414
|
+
ogImage: ogState.image,
|
|
415
|
+
};
|
|
361
416
|
}
|
|
362
417
|
catch (error) {
|
|
363
418
|
log.error(`Error rendering layout ${layoutPath || 'unknown'}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -366,7 +421,7 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
|
|
|
366
421
|
const templateError = createTemplateError(error instanceof Error ? error : new Error(String(error)), layoutPath || undefined);
|
|
367
422
|
throw templateError;
|
|
368
423
|
}
|
|
369
|
-
return { html: createFallbackHtml(page, body), templatesLoaded };
|
|
424
|
+
return { html: createFallbackHtml(page, body), templatesLoaded, ogImage: ogState.image };
|
|
370
425
|
}
|
|
371
426
|
}
|
|
372
427
|
function createFallbackHtml(page, body) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fse from 'fs-extra';
|
|
2
|
-
import type { WriteFileOptions } from 'node:fs';
|
|
2
|
+
import type { WriteFileOptions, Stats } from 'node:fs';
|
|
3
3
|
/**
|
|
4
4
|
* Safely reads a file with consistent error handling.
|
|
5
5
|
*/
|
|
@@ -34,4 +34,24 @@ export declare function readdir<T extends boolean = false>(dirPath: string, opti
|
|
|
34
34
|
* Copies a file with consistent error handling.
|
|
35
35
|
*/
|
|
36
36
|
export declare function copyFile(src: string, dest: string): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Reads a file as raw bytes. Returns null if the file does not exist.
|
|
39
|
+
*/
|
|
40
|
+
export declare function readFileBuffer(filePath: string): Promise<Buffer | null>;
|
|
41
|
+
/**
|
|
42
|
+
* Reads a file as raw bytes synchronously. Returns null if the file does not exist.
|
|
43
|
+
* For code that must stay synchronous, such as template helpers.
|
|
44
|
+
*/
|
|
45
|
+
export declare function readFileBufferSync(filePath: string): Buffer | null;
|
|
46
|
+
/**
|
|
47
|
+
* Gets file stats synchronously. Returns null if the path does not exist.
|
|
48
|
+
*/
|
|
49
|
+
export declare function statSyncOrNull(filePath: string): Stats | null;
|
|
50
|
+
/**
|
|
51
|
+
* Lists every file under a directory, recursively.
|
|
52
|
+
*
|
|
53
|
+
* @param dirPath - Directory to list
|
|
54
|
+
* @returns Paths relative to `dirPath` with forward slashes; empty if the directory does not exist
|
|
55
|
+
*/
|
|
56
|
+
export declare function listFilesRecursiveSync(dirPath: string): string[];
|
|
37
57
|
//# sourceMappingURL=fs.utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fs.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/fs.utils.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"fs.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/fs.utils.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,UAAU,CAAC;AAE3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAmDvD;;GAEG;AACH,wBAAsB,QAAQ,CAC5B,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAE,OAAO,GAAG,MAAgB,GACnC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAExB;AAED;;GAEG;AACH,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,IAAI,CAAC,CAEf;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAEnE;AAED;;GAEG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9D;AAED;;GAEG;AACH,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAExD;AAED;;GAEG;AACH,wBAAsB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAE/D;AAED;;GAEG;AACH,wBAAsB,OAAO,CAAC,CAAC,SAAS,OAAO,GAAG,KAAK,EACrD,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,CAAC,CAAA;CAAE,GAC9B,OAAO,CAAC,CAAC,SAAS,IAAI,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,CAcnD;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEvE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAE7E;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUlE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI,CAU7D;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAchE"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import fse from 'fs-extra';
|
|
2
|
+
import { readFileSync, statSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { join, relative } from 'node:path';
|
|
2
4
|
const { readFile: fseReadFile, writeFile: fseWriteFile, pathExists: fsePathExists, ensureDir: fseEnsureDir, remove: fseRemove, stat: fseStat, readdir: fseReaddir, copyFile: fseCopyFile, } = fse;
|
|
3
5
|
/**
|
|
4
6
|
* Wraps fs-extra operations with consistent error handling.
|
|
@@ -86,3 +88,62 @@ export async function readdir(dirPath, options) {
|
|
|
86
88
|
export async function copyFile(src, dest) {
|
|
87
89
|
return wrapFsOperation('copy', fseCopyFile(src, dest), `${src} to ${dest}`);
|
|
88
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Reads a file as raw bytes. Returns null if the file does not exist.
|
|
93
|
+
*/
|
|
94
|
+
export async function readFileBuffer(filePath) {
|
|
95
|
+
return wrapFsOperationNullable('read file', fseReadFile(filePath), filePath);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Reads a file as raw bytes synchronously. Returns null if the file does not exist.
|
|
99
|
+
* For code that must stay synchronous, such as template helpers.
|
|
100
|
+
*/
|
|
101
|
+
export function readFileBufferSync(filePath) {
|
|
102
|
+
try {
|
|
103
|
+
return readFileSync(filePath);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const nodeError = error;
|
|
107
|
+
if (nodeError.code === 'ENOENT' || nodeError.code === 'ENOTDIR') {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
throw new Error(`Failed to read file ${filePath}: ${nodeError.message}`, { cause: error });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Gets file stats synchronously. Returns null if the path does not exist.
|
|
115
|
+
*/
|
|
116
|
+
export function statSyncOrNull(filePath) {
|
|
117
|
+
try {
|
|
118
|
+
return statSync(filePath);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
const nodeError = error;
|
|
122
|
+
if (nodeError.code === 'ENOENT' || nodeError.code === 'ENOTDIR') {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
throw new Error(`Failed to get stats for ${filePath}: ${nodeError.message}`, { cause: error });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Lists every file under a directory, recursively.
|
|
130
|
+
*
|
|
131
|
+
* @param dirPath - Directory to list
|
|
132
|
+
* @returns Paths relative to `dirPath` with forward slashes; empty if the directory does not exist
|
|
133
|
+
*/
|
|
134
|
+
export function listFilesRecursiveSync(dirPath) {
|
|
135
|
+
let entries;
|
|
136
|
+
try {
|
|
137
|
+
entries = readdirSync(dirPath, { withFileTypes: true, recursive: true });
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
const nodeError = error;
|
|
141
|
+
if (nodeError.code === 'ENOENT' || nodeError.code === 'ENOTDIR') {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
throw new Error(`Failed to read directory ${dirPath}: ${nodeError.message}`, { cause: error });
|
|
145
|
+
}
|
|
146
|
+
return entries
|
|
147
|
+
.filter((entry) => entry.isFile())
|
|
148
|
+
.map((entry) => relative(dirPath, join(entry.parentPath, entry.name)).replace(/\\/g, '/'));
|
|
149
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Core utilities index
|
|
3
3
|
* @module core/utils
|
|
4
4
|
*/
|
|
5
|
-
export { readFile, writeFile, pathExists, ensureDir, remove, copyFile, readdir, stat, } from './fs.utils.js';
|
|
5
|
+
export { readFile, writeFile, pathExists, ensureDir, remove, copyFile, readdir, stat, readFileBuffer, readFileBufferSync, statSyncOrNull, listFilesRecursiveSync, } from './fs.utils.js';
|
|
6
6
|
export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, normalizePathForComparison, isPathWithinDirectory, } from './paths.utils.js';
|
|
7
7
|
export { discoverLayout, isCollectionIndexPage, getCollectionPathForPage, } from './template-discovery.utils.js';
|
|
8
8
|
export { propValue } from './template.utils.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/utils/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EACL,QAAQ,EACR,SAAS,EACT,UAAU,EACV,SAAS,EACT,MAAM,EACN,QAAQ,EACR,OAAO,EACP,IAAI,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/utils/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EACL,QAAQ,EACR,SAAS,EACT,UAAU,EACV,SAAS,EACT,MAAM,EACN,QAAQ,EACR,OAAO,EACP,IAAI,EACJ,cAAc,EACd,kBAAkB,EAClB,cAAc,EACd,sBAAsB,GACvB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,0BAA0B,EAC1B,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,wBAAwB,GACzB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGhD,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAG7C,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,wBAAwB,EACxB,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,2BAA2B,EAC3B,cAAc,EACd,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AAG9E,OAAO,EACL,4BAA4B,EAC5B,qBAAqB,EACrB,2BAA2B,EAC3B,4BAA4B,GAC7B,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAGvE,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAC3F,YAAY,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAGpE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAG/F,OAAO,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAGxE,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,wBAAwB,EACxB,mBAAmB,GACpB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AACjF,YAAY,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAG7D,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGpE,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,4BAA4B,CAAC;AACpC,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAGrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAGzD,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,EAClB,qBAAqB,EACrB,iBAAiB,EACjB,WAAW,EACX,qBAAqB,GACtB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAG1E,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAG/E,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAG5E,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAGlD,OAAO,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAC7F,YAAY,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAC"}
|
package/dist/core/utils/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @module core/utils
|
|
4
4
|
*/
|
|
5
5
|
// File system utilities
|
|
6
|
-
export { readFile, writeFile, pathExists, ensureDir, remove, copyFile, readdir, stat, } from './fs.utils.js';
|
|
6
|
+
export { readFile, writeFile, pathExists, ensureDir, remove, copyFile, readdir, stat, readFileBuffer, readFileBufferSync, statSyncOrNull, listFilesRecursiveSync, } from './fs.utils.js';
|
|
7
7
|
// Path resolution utilities
|
|
8
8
|
export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, normalizePathForComparison, isPathWithinDirectory, } from './paths.utils.js';
|
|
9
9
|
// Template discovery utilities
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import type { StatiConfig, PageModel } from '../../types/index.js';
|
|
2
|
-
/**
|
|
3
|
-
* Shared template discovery utilities.
|
|
4
|
-
* Extracted from templates.ts and isg/deps.ts to eliminate duplication.
|
|
5
|
-
*/
|
|
6
2
|
/**
|
|
7
3
|
* Determines if the given page is a collection index page.
|
|
8
4
|
* A collection index page is one whose URL matches a directory path that contains other pages.
|
|
9
5
|
*
|
|
6
|
+
* Rendering and ISG dependency tracking must both call this with the same page list,
|
|
7
|
+
* otherwise they can pick different layouts for the same page.
|
|
8
|
+
*
|
|
9
|
+
* Page URLs are read once per `allPages` array, at the first call; see
|
|
10
|
+
* `directoryPrefixCache` for when changes to the pages are picked up.
|
|
11
|
+
*
|
|
10
12
|
* @param page - The page to check
|
|
11
|
-
* @param allPages - All pages in the site (
|
|
13
|
+
* @param allPages - All pages in the site (treated as unchanging after the first call)
|
|
12
14
|
* @returns True if the page is a collection index page
|
|
13
15
|
*/
|
|
14
|
-
export declare function isCollectionIndexPage(page: PageModel, allPages
|
|
16
|
+
export declare function isCollectionIndexPage(page: PageModel, allPages: PageModel[]): boolean;
|
|
15
17
|
/**
|
|
16
18
|
* Discovers layout files by searching up the directory hierarchy.
|
|
17
19
|
* Supports both explicit layout specification and automatic discovery.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template-discovery.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/template-discovery.utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"template-discovery.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/template-discovery.utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAoDnE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,OAAO,CAUrF;AAED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,WAAW,EACnB,cAAc,CAAC,EAAE,MAAM,EACvB,WAAW,CAAC,EAAE,OAAO,GACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAsDxB;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAkBhE"}
|
|
@@ -3,15 +3,51 @@ import { pathExists } from './fs.utils.js';
|
|
|
3
3
|
import { LAYOUT_TEMPLATE } from '../../constants.js';
|
|
4
4
|
import { resolveSrcDir } from './paths.utils.js';
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Directory prefix index per page list, so a build computes it once instead of
|
|
7
|
+
* scanning every page for every page.
|
|
8
|
+
*
|
|
9
|
+
* The index is a snapshot of the page URLs taken at the first lookup for a given array,
|
|
10
|
+
* and every later lookup for that array reuses it. In a build the first lookup happens
|
|
11
|
+
* in the render loop, after the `beforeAll` hook, so URL changes made there are included.
|
|
12
|
+
* Changes made to the same array or its pages after that are not seen; a fresh build
|
|
13
|
+
* loads a new array and gets a new snapshot. Taking one snapshot keeps rendering and
|
|
14
|
+
* ISG dependency tracking in agreement for the whole build.
|
|
8
15
|
*/
|
|
16
|
+
const directoryPrefixCache = new WeakMap();
|
|
17
|
+
function getDirectoryPrefixIndex(allPages) {
|
|
18
|
+
const cached = directoryPrefixCache.get(allPages);
|
|
19
|
+
if (cached) {
|
|
20
|
+
return cached;
|
|
21
|
+
}
|
|
22
|
+
const index = new Map();
|
|
23
|
+
for (const { url } of allPages) {
|
|
24
|
+
// Every prefix ending at a '/' is a directory this URL lives in (or is, for trailing-slash URLs)
|
|
25
|
+
for (let i = url.indexOf('/'); i !== -1; i = url.indexOf('/', i + 1)) {
|
|
26
|
+
const prefix = url.slice(0, i + 1);
|
|
27
|
+
const existing = index.get(prefix);
|
|
28
|
+
if (existing === undefined) {
|
|
29
|
+
index.set(prefix, url);
|
|
30
|
+
}
|
|
31
|
+
else if (existing !== null && existing !== url) {
|
|
32
|
+
index.set(prefix, null);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
directoryPrefixCache.set(allPages, index);
|
|
37
|
+
return index;
|
|
38
|
+
}
|
|
9
39
|
/**
|
|
10
40
|
* Determines if the given page is a collection index page.
|
|
11
41
|
* A collection index page is one whose URL matches a directory path that contains other pages.
|
|
12
42
|
*
|
|
43
|
+
* Rendering and ISG dependency tracking must both call this with the same page list,
|
|
44
|
+
* otherwise they can pick different layouts for the same page.
|
|
45
|
+
*
|
|
46
|
+
* Page URLs are read once per `allPages` array, at the first call; see
|
|
47
|
+
* `directoryPrefixCache` for when changes to the pages are picked up.
|
|
48
|
+
*
|
|
13
49
|
* @param page - The page to check
|
|
14
|
-
* @param allPages - All pages in the site (
|
|
50
|
+
* @param allPages - All pages in the site (treated as unchanging after the first call)
|
|
15
51
|
* @returns True if the page is a collection index page
|
|
16
52
|
*/
|
|
17
53
|
export function isCollectionIndexPage(page, allPages) {
|
|
@@ -19,15 +55,10 @@ export function isCollectionIndexPage(page, allPages) {
|
|
|
19
55
|
if (page.url === '/') {
|
|
20
56
|
return true;
|
|
21
57
|
}
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return allPages.some((otherPage) => otherPage.url !== page.url && otherPage.url.startsWith(pageUrlAsDir));
|
|
27
|
-
}
|
|
28
|
-
// Simplified version when we don't have all pages
|
|
29
|
-
// Assume any page ending in /index or at root is a collection page
|
|
30
|
-
return page.url.endsWith('/index') || page.slug === 'index';
|
|
58
|
+
// The page is an index when a different page's URL lives under this page's URL as a directory
|
|
59
|
+
const pageUrlAsDir = page.url.endsWith('/') ? page.url : page.url + '/';
|
|
60
|
+
const containingUrl = getDirectoryPrefixIndex(allPages).get(pageUrlAsDir);
|
|
61
|
+
return containingUrl === null || (containingUrl !== undefined && containingUrl !== page.url);
|
|
31
62
|
}
|
|
32
63
|
/**
|
|
33
64
|
* Discovers layout files by searching up the directory hierarchy.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation and defaults for the `images` configuration block.
|
|
3
|
+
* @module images/config
|
|
4
|
+
*/
|
|
5
|
+
import type { ImageVariantFormat, OgImageFontConfig, StatiConfig } from '../types/index.js';
|
|
6
|
+
/** Responsive image options with defaults applied. */
|
|
7
|
+
export interface ResolvedResponsiveConfig {
|
|
8
|
+
/** Candidate widths, ascending and unique */
|
|
9
|
+
widths: number[];
|
|
10
|
+
formats: ImageVariantFormat[];
|
|
11
|
+
quality: {
|
|
12
|
+
avif: number;
|
|
13
|
+
webp: number;
|
|
14
|
+
jpeg: number;
|
|
15
|
+
png?: number;
|
|
16
|
+
};
|
|
17
|
+
sizes: string;
|
|
18
|
+
}
|
|
19
|
+
/** Open Graph image options with defaults applied. */
|
|
20
|
+
export interface ResolvedOgConfig {
|
|
21
|
+
template: string;
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
format: 'png' | 'jpeg';
|
|
25
|
+
fonts: OgImageFontConfig[];
|
|
26
|
+
exclude: string[];
|
|
27
|
+
}
|
|
28
|
+
/** The `images` block with defaults applied. */
|
|
29
|
+
export interface ResolvedImagesConfig {
|
|
30
|
+
eagerFirst: boolean;
|
|
31
|
+
/** `null` when responsive images are off */
|
|
32
|
+
responsive: ResolvedResponsiveConfig | null;
|
|
33
|
+
/** `null` when Open Graph images are off */
|
|
34
|
+
og: ResolvedOgConfig | null;
|
|
35
|
+
dev: 'fast' | 'full';
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Validates the `images` configuration block.
|
|
39
|
+
*
|
|
40
|
+
* @param images - The `images` value from the config
|
|
41
|
+
* @throws {ConfigValidationError} When an option has the wrong type or value
|
|
42
|
+
*/
|
|
43
|
+
export declare function validateImagesConfig(images: unknown): void;
|
|
44
|
+
/**
|
|
45
|
+
* Applies defaults to the `images` configuration block.
|
|
46
|
+
* Expects a block that passed {@link validateImagesConfig}.
|
|
47
|
+
*
|
|
48
|
+
* @param images - The `images` value from the config
|
|
49
|
+
* @returns The resolved options, or `null` when images are turned off with `images: false`
|
|
50
|
+
*/
|
|
51
|
+
export declare function resolveImagesConfig(images: StatiConfig['images']): ResolvedImagesConfig | null;
|
|
52
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/images/config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAY5F,sDAAsD;AACtD,MAAM,WAAW,wBAAwB;IACvC,6CAA6C;IAC7C,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACpE,KAAK,EAAE,MAAM,CAAC;CACf;AAED,sDAAsD;AACtD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;IACvB,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC3B,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,gDAAgD;AAChD,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,OAAO,CAAC;IACpB,4CAA4C;IAC5C,UAAU,EAAE,wBAAwB,GAAG,IAAI,CAAC;IAC5C,4CAA4C;IAC5C,EAAE,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC5B,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;CACtB;AA6JD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAqB1D;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,GAAG,oBAAoB,GAAG,IAAI,CA0C9F"}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation and defaults for the `images` configuration block.
|
|
3
|
+
* @module images/config
|
|
4
|
+
*/
|
|
5
|
+
import { ConfigValidationError } from '../config/errors.js';
|
|
6
|
+
import { DEFAULT_IMAGE_FORMATS, DEFAULT_IMAGE_QUALITY, DEFAULT_IMAGE_SIZES, DEFAULT_IMAGE_WIDTHS, DEFAULT_OG_HEIGHT, DEFAULT_OG_TEMPLATE, DEFAULT_OG_WIDTH, } from '../constants.js';
|
|
7
|
+
const IMAGES_KEYS = ['eagerFirst', 'responsive', 'og', 'dev'];
|
|
8
|
+
const RESPONSIVE_KEYS = ['widths', 'formats', 'quality', 'sizes'];
|
|
9
|
+
const QUALITY_KEYS = ['avif', 'webp', 'jpeg', 'png'];
|
|
10
|
+
const OG_KEYS = ['template', 'width', 'height', 'format', 'fonts', 'exclude'];
|
|
11
|
+
const FONT_KEYS = ['name', 'path', 'weight', 'style'];
|
|
12
|
+
const VARIANT_FORMATS = ['avif', 'webp'];
|
|
13
|
+
const MAX_IMAGE_WIDTH = 16384;
|
|
14
|
+
const MAX_OG_SIZE = 4096;
|
|
15
|
+
function fail(message) {
|
|
16
|
+
throw new ConfigValidationError(message);
|
|
17
|
+
}
|
|
18
|
+
function describe(value) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.stringify(value) ?? String(value);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return String(value);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function isPlainObject(value) {
|
|
27
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
function checkKeys(value, allowed, path) {
|
|
30
|
+
for (const key of Object.keys(value)) {
|
|
31
|
+
if (!allowed.includes(key)) {
|
|
32
|
+
fail(`Unknown option ${path}.${key}. Allowed options: ${allowed.join(', ')}.`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function isIntegerInRange(value, min, max) {
|
|
37
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= min && value <= max;
|
|
38
|
+
}
|
|
39
|
+
function validateResponsive(value) {
|
|
40
|
+
if (typeof value === 'boolean')
|
|
41
|
+
return;
|
|
42
|
+
if (!isPlainObject(value)) {
|
|
43
|
+
fail(`images.responsive must be true, false or an object (got ${describe(value)}).`);
|
|
44
|
+
}
|
|
45
|
+
checkKeys(value, RESPONSIVE_KEYS, 'images.responsive');
|
|
46
|
+
if (value.widths !== undefined) {
|
|
47
|
+
if (!Array.isArray(value.widths) ||
|
|
48
|
+
value.widths.length === 0 ||
|
|
49
|
+
!value.widths.every((w) => isIntegerInRange(w, 1, MAX_IMAGE_WIDTH))) {
|
|
50
|
+
fail(`images.responsive.widths must be a non-empty array of whole pixel widths between 1 and ${MAX_IMAGE_WIDTH} (got ${describe(value.widths)}).`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (value.formats !== undefined) {
|
|
54
|
+
if (!Array.isArray(value.formats) ||
|
|
55
|
+
!value.formats.every((f) => typeof f === 'string' && VARIANT_FORMATS.includes(f))) {
|
|
56
|
+
fail(`images.responsive.formats must be an array of 'avif' and 'webp' (got ${describe(value.formats)}).`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (value.quality !== undefined) {
|
|
60
|
+
if (!isPlainObject(value.quality)) {
|
|
61
|
+
fail(`images.responsive.quality must be an object (got ${describe(value.quality)}).`);
|
|
62
|
+
}
|
|
63
|
+
checkKeys(value.quality, QUALITY_KEYS, 'images.responsive.quality');
|
|
64
|
+
for (const [format, quality] of Object.entries(value.quality)) {
|
|
65
|
+
if (quality !== undefined && !isIntegerInRange(quality, 1, 100)) {
|
|
66
|
+
fail(`images.responsive.quality.${format} must be a whole number between 1 and 100 (got ${describe(quality)}).`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (value.sizes !== undefined && (typeof value.sizes !== 'string' || !value.sizes.trim())) {
|
|
71
|
+
fail(`images.responsive.sizes must be a non-empty string (got ${describe(value.sizes)}).`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function validateFont(font, index) {
|
|
75
|
+
const path = `images.og.fonts[${index}]`;
|
|
76
|
+
if (!isPlainObject(font)) {
|
|
77
|
+
fail(`${path} must be an object with name and path (got ${describe(font)}).`);
|
|
78
|
+
}
|
|
79
|
+
checkKeys(font, FONT_KEYS, path);
|
|
80
|
+
if (typeof font.name !== 'string' || !font.name.trim()) {
|
|
81
|
+
fail(`${path}.name must be a non-empty string.`);
|
|
82
|
+
}
|
|
83
|
+
if (typeof font.path !== 'string' || !font.path.trim()) {
|
|
84
|
+
fail(`${path}.path must be a non-empty string.`);
|
|
85
|
+
}
|
|
86
|
+
if (font.weight !== undefined && !isIntegerInRange(font.weight, 100, 900)) {
|
|
87
|
+
fail(`${path}.weight must be a whole number between 100 and 900 (got ${describe(font.weight)}).`);
|
|
88
|
+
}
|
|
89
|
+
if (font.style !== undefined && font.style !== 'normal' && font.style !== 'italic') {
|
|
90
|
+
fail(`${path}.style must be 'normal' or 'italic' (got ${describe(font.style)}).`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function validateOg(value) {
|
|
94
|
+
if (typeof value === 'boolean')
|
|
95
|
+
return;
|
|
96
|
+
if (!isPlainObject(value)) {
|
|
97
|
+
fail(`images.og must be true, false or an object (got ${describe(value)}).`);
|
|
98
|
+
}
|
|
99
|
+
checkKeys(value, OG_KEYS, 'images.og');
|
|
100
|
+
if (value.template !== undefined) {
|
|
101
|
+
if (typeof value.template !== 'string' ||
|
|
102
|
+
!value.template.endsWith('.eta') ||
|
|
103
|
+
/[/\\]/.test(value.template)) {
|
|
104
|
+
fail(`images.og.template must be a file name ending in .eta, such as 'og.eta' (got ${describe(value.template)}).`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const dimension of ['width', 'height']) {
|
|
108
|
+
if (value[dimension] !== undefined && !isIntegerInRange(value[dimension], 1, MAX_OG_SIZE)) {
|
|
109
|
+
fail(`images.og.${dimension} must be a whole number of pixels between 1 and ${MAX_OG_SIZE} (got ${describe(value[dimension])}).`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (value.format !== undefined && value.format !== 'png' && value.format !== 'jpeg') {
|
|
113
|
+
fail(`images.og.format must be 'png' or 'jpeg' (got ${describe(value.format)}).`);
|
|
114
|
+
}
|
|
115
|
+
if (value.fonts !== undefined) {
|
|
116
|
+
if (!Array.isArray(value.fonts)) {
|
|
117
|
+
fail(`images.og.fonts must be an array (got ${describe(value.fonts)}).`);
|
|
118
|
+
}
|
|
119
|
+
value.fonts.forEach(validateFont);
|
|
120
|
+
}
|
|
121
|
+
if (value.exclude !== undefined) {
|
|
122
|
+
if (!Array.isArray(value.exclude) || !value.exclude.every((p) => typeof p === 'string')) {
|
|
123
|
+
fail(`images.og.exclude must be an array of URL glob patterns (got ${describe(value.exclude)}).`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Validates the `images` configuration block.
|
|
129
|
+
*
|
|
130
|
+
* @param images - The `images` value from the config
|
|
131
|
+
* @throws {ConfigValidationError} When an option has the wrong type or value
|
|
132
|
+
*/
|
|
133
|
+
export function validateImagesConfig(images) {
|
|
134
|
+
if (images === undefined || images === false) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!isPlainObject(images)) {
|
|
138
|
+
fail(`images must be an object or false (got ${describe(images)}).`);
|
|
139
|
+
}
|
|
140
|
+
checkKeys(images, IMAGES_KEYS, 'images');
|
|
141
|
+
if (images.eagerFirst !== undefined && typeof images.eagerFirst !== 'boolean') {
|
|
142
|
+
fail(`images.eagerFirst must be true or false (got ${describe(images.eagerFirst)}).`);
|
|
143
|
+
}
|
|
144
|
+
if (images.dev !== undefined && images.dev !== 'fast' && images.dev !== 'full') {
|
|
145
|
+
fail(`images.dev must be 'fast' or 'full' (got ${describe(images.dev)}).`);
|
|
146
|
+
}
|
|
147
|
+
if (images.responsive !== undefined) {
|
|
148
|
+
validateResponsive(images.responsive);
|
|
149
|
+
}
|
|
150
|
+
if (images.og !== undefined) {
|
|
151
|
+
validateOg(images.og);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Applies defaults to the `images` configuration block.
|
|
156
|
+
* Expects a block that passed {@link validateImagesConfig}.
|
|
157
|
+
*
|
|
158
|
+
* @param images - The `images` value from the config
|
|
159
|
+
* @returns The resolved options, or `null` when images are turned off with `images: false`
|
|
160
|
+
*/
|
|
161
|
+
export function resolveImagesConfig(images) {
|
|
162
|
+
if (images === false) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
const config = images ?? {};
|
|
166
|
+
let responsive = null;
|
|
167
|
+
if (config.responsive) {
|
|
168
|
+
const options = config.responsive === true ? {} : config.responsive;
|
|
169
|
+
const quality = options.quality ?? {};
|
|
170
|
+
responsive = {
|
|
171
|
+
widths: [...new Set(options.widths ?? DEFAULT_IMAGE_WIDTHS)].sort((a, b) => a - b),
|
|
172
|
+
formats: [...new Set(options.formats ?? DEFAULT_IMAGE_FORMATS)],
|
|
173
|
+
quality: {
|
|
174
|
+
avif: quality.avif ?? DEFAULT_IMAGE_QUALITY.avif,
|
|
175
|
+
webp: quality.webp ?? DEFAULT_IMAGE_QUALITY.webp,
|
|
176
|
+
jpeg: quality.jpeg ?? DEFAULT_IMAGE_QUALITY.jpeg,
|
|
177
|
+
...(quality.png !== undefined && { png: quality.png }),
|
|
178
|
+
},
|
|
179
|
+
sizes: options.sizes ?? DEFAULT_IMAGE_SIZES,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
let og = null;
|
|
183
|
+
if (config.og) {
|
|
184
|
+
const options = config.og === true ? {} : config.og;
|
|
185
|
+
og = {
|
|
186
|
+
template: options.template ?? DEFAULT_OG_TEMPLATE,
|
|
187
|
+
width: options.width ?? DEFAULT_OG_WIDTH,
|
|
188
|
+
height: options.height ?? DEFAULT_OG_HEIGHT,
|
|
189
|
+
format: options.format ?? 'png',
|
|
190
|
+
fonts: options.fonts ?? [],
|
|
191
|
+
exclude: options.exclude ?? [],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
eagerFirst: config.eagerFirst ?? true,
|
|
196
|
+
responsive,
|
|
197
|
+
og,
|
|
198
|
+
dev: config.dev ?? 'fast',
|
|
199
|
+
};
|
|
200
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Image pipeline exports
|
|
3
|
+
* Intrinsic sizes, lazy loading and co-located images in core, plus the plumbing for
|
|
4
|
+
* responsive variants and Open Graph images rendered by `@stati/images`.
|
|
5
|
+
*/
|
|
6
|
+
export { resolveImagesConfig, validateImagesConfig } from './config.js';
|
|
7
|
+
export type { ResolvedImagesConfig, ResolvedResponsiveConfig, ResolvedOgConfig } from './config.js';
|
|
8
|
+
export { ImageRuntime, PageImages, validateOgBaseUrl } from './runtime.js';
|
|
9
|
+
export type { StatiImageOptions, SettleResult, ImageRuntimeOptions } from './runtime.js';
|
|
10
|
+
export { installImageRule, selectEagerImage } from './markdown-rule.js';
|
|
11
|
+
export { findOgTemplatePath, clearOgTemplateCache } from './og.js';
|
|
12
|
+
export { loadImageProvider, resolveImagesPackage, missingPackageMessage } from './provider.js';
|
|
13
|
+
export { probeImage } from './utils/index.js';
|
|
14
|
+
export type { ImageProbeResult } from './utils/index.js';
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/images/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxE,YAAY,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpG,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAC3E,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACzF,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAC/F,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC"}
|