@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.
Files changed (108) hide show
  1. package/dist/config/errors.d.ts +10 -0
  2. package/dist/config/errors.d.ts.map +1 -0
  3. package/dist/config/errors.js +10 -0
  4. package/dist/config/loader.d.ts.map +1 -1
  5. package/dist/config/loader.js +15 -2
  6. package/dist/constants.d.ts +26 -0
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/constants.js +23 -0
  9. package/dist/core/build.d.ts.map +1 -1
  10. package/dist/core/build.js +98 -26
  11. package/dist/core/isg/builder.d.ts +21 -6
  12. package/dist/core/isg/builder.d.ts.map +1 -1
  13. package/dist/core/isg/builder.js +59 -17
  14. package/dist/core/isg/deps.d.ts +23 -2
  15. package/dist/core/isg/deps.d.ts.map +1 -1
  16. package/dist/core/isg/deps.js +54 -8
  17. package/dist/core/isg/hash.d.ts +10 -0
  18. package/dist/core/isg/hash.d.ts.map +1 -1
  19. package/dist/core/isg/hash.js +14 -0
  20. package/dist/core/isg/index.d.ts +2 -1
  21. package/dist/core/isg/index.d.ts.map +1 -1
  22. package/dist/core/isg/index.js +1 -1
  23. package/dist/core/isg/manifest.js +8 -0
  24. package/dist/core/markdown.d.ts +10 -1
  25. package/dist/core/markdown.d.ts.map +1 -1
  26. package/dist/core/markdown.js +13 -3
  27. package/dist/core/templates.d.ts +12 -1
  28. package/dist/core/templates.d.ts.map +1 -1
  29. package/dist/core/templates.js +61 -6
  30. package/dist/core/utils/fs.utils.d.ts +21 -1
  31. package/dist/core/utils/fs.utils.d.ts.map +1 -1
  32. package/dist/core/utils/fs.utils.js +61 -0
  33. package/dist/core/utils/index.d.ts +1 -1
  34. package/dist/core/utils/index.d.ts.map +1 -1
  35. package/dist/core/utils/index.js +1 -1
  36. package/dist/core/utils/template-discovery.utils.d.ts +8 -6
  37. package/dist/core/utils/template-discovery.utils.d.ts.map +1 -1
  38. package/dist/core/utils/template-discovery.utils.js +43 -12
  39. package/dist/images/config.d.ts +52 -0
  40. package/dist/images/config.d.ts.map +1 -0
  41. package/dist/images/config.js +200 -0
  42. package/dist/images/index.d.ts +15 -0
  43. package/dist/images/index.d.ts.map +1 -0
  44. package/dist/images/index.js +11 -0
  45. package/dist/images/manifest.d.ts +36 -0
  46. package/dist/images/manifest.d.ts.map +1 -0
  47. package/dist/images/manifest.js +69 -0
  48. package/dist/images/markdown-rule.d.ts +26 -0
  49. package/dist/images/markdown-rule.d.ts.map +1 -0
  50. package/dist/images/markdown-rule.js +60 -0
  51. package/dist/images/markup.d.ts +39 -0
  52. package/dist/images/markup.d.ts.map +1 -0
  53. package/dist/images/markup.js +72 -0
  54. package/dist/images/og.d.ts +52 -0
  55. package/dist/images/og.d.ts.map +1 -0
  56. package/dist/images/og.js +165 -0
  57. package/dist/images/plan.d.ts +101 -0
  58. package/dist/images/plan.d.ts.map +1 -0
  59. package/dist/images/plan.js +128 -0
  60. package/dist/images/provider.d.ts +35 -0
  61. package/dist/images/provider.d.ts.map +1 -0
  62. package/dist/images/provider.js +70 -0
  63. package/dist/images/resolve.d.ts +45 -0
  64. package/dist/images/resolve.d.ts.map +1 -0
  65. package/dist/images/resolve.js +51 -0
  66. package/dist/images/runtime.d.ts +258 -0
  67. package/dist/images/runtime.d.ts.map +1 -0
  68. package/dist/images/runtime.js +659 -0
  69. package/dist/images/sources.d.ts +47 -0
  70. package/dist/images/sources.d.ts.map +1 -0
  71. package/dist/images/sources.js +97 -0
  72. package/dist/images/utils/image-probe.utils.d.ts +30 -0
  73. package/dist/images/utils/image-probe.utils.d.ts.map +1 -0
  74. package/dist/images/utils/image-probe.utils.js +307 -0
  75. package/dist/images/utils/index.d.ts +7 -0
  76. package/dist/images/utils/index.d.ts.map +1 -0
  77. package/dist/images/utils/index.js +5 -0
  78. package/dist/index.d.ts +1 -0
  79. package/dist/index.d.ts.map +1 -1
  80. package/dist/metrics/noop.d.ts.map +1 -1
  81. package/dist/metrics/noop.js +3 -0
  82. package/dist/metrics/recorder.d.ts.map +1 -1
  83. package/dist/metrics/recorder.js +3 -0
  84. package/dist/metrics/types.d.ts +8 -0
  85. package/dist/metrics/types.d.ts.map +1 -1
  86. package/dist/seo/auto-inject.d.ts +3 -0
  87. package/dist/seo/auto-inject.d.ts.map +1 -1
  88. package/dist/seo/auto-inject.js +1 -0
  89. package/dist/seo/generator.d.ts +24 -0
  90. package/dist/seo/generator.d.ts.map +1 -1
  91. package/dist/seo/generator.js +65 -1
  92. package/dist/seo/index.d.ts +1 -1
  93. package/dist/seo/index.d.ts.map +1 -1
  94. package/dist/seo/index.js +1 -1
  95. package/dist/types/config.d.ts +5 -0
  96. package/dist/types/config.d.ts.map +1 -1
  97. package/dist/types/content.d.ts +21 -2
  98. package/dist/types/content.d.ts.map +1 -1
  99. package/dist/types/images.d.ts +185 -0
  100. package/dist/types/images.d.ts.map +1 -0
  101. package/dist/types/images.js +8 -0
  102. package/dist/types/index.d.ts +1 -0
  103. package/dist/types/index.d.ts.map +1 -1
  104. package/dist/types/isg.d.ts +7 -0
  105. package/dist/types/isg.d.ts.map +1 -1
  106. package/dist/types/seo.d.ts +9 -0
  107. package/dist/types/seo.d.ts.map +1 -1
  108. package/package.json +1 -1
@@ -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({ page, config, site: config.site }, tags),
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 { html, templatesLoaded };
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;AAC3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAkDhD;;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"}
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,GACL,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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/utils/index.ts"],"names":[],"mappings":"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"}
@@ -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 (optional for simplified check)
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?: PageModel[]): boolean;
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;AAInE;;;GAGG;AAEH;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,CAAC,EAAE,SAAS,EAAE,GAAG,OAAO,CAkBtF;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"}
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
- * Shared template discovery utilities.
7
- * Extracted from templates.ts and isg/deps.ts to eliminate duplication.
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 (optional for simplified check)
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
- // If we have all pages, do a proper check
23
- if (allPages) {
24
- // Check if this page's URL is a directory path that contains other pages
25
- const pageUrlAsDir = page.url.endsWith('/') ? page.url : page.url + '/';
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"}