@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.
Files changed (136) hide show
  1. package/README.md +6 -1
  2. package/dist/config/errors.d.ts +10 -0
  3. package/dist/config/errors.d.ts.map +1 -0
  4. package/dist/config/errors.js +10 -0
  5. package/dist/config/loader.d.ts.map +1 -1
  6. package/dist/config/loader.js +15 -2
  7. package/dist/constants.d.ts +26 -0
  8. package/dist/constants.d.ts.map +1 -1
  9. package/dist/constants.js +23 -0
  10. package/dist/core/build.d.ts.map +1 -1
  11. package/dist/core/build.js +201 -41
  12. package/dist/core/collections.d.ts +139 -0
  13. package/dist/core/collections.d.ts.map +1 -0
  14. package/dist/core/collections.js +342 -0
  15. package/dist/core/content.d.ts.map +1 -1
  16. package/dist/core/content.js +2 -1
  17. package/dist/core/dev.d.ts.map +1 -1
  18. package/dist/core/dev.js +11 -62
  19. package/dist/core/expand.d.ts +86 -0
  20. package/dist/core/expand.d.ts.map +1 -0
  21. package/dist/core/expand.js +484 -0
  22. package/dist/core/isg/builder.d.ts +27 -2
  23. package/dist/core/isg/builder.d.ts.map +1 -1
  24. package/dist/core/isg/builder.js +83 -11
  25. package/dist/core/isg/deps.d.ts +9 -0
  26. package/dist/core/isg/deps.d.ts.map +1 -1
  27. package/dist/core/isg/deps.js +30 -1
  28. package/dist/core/isg/hash.d.ts +14 -2
  29. package/dist/core/isg/hash.d.ts.map +1 -1
  30. package/dist/core/isg/hash.js +30 -2
  31. package/dist/core/isg/index.d.ts +4 -2
  32. package/dist/core/isg/index.d.ts.map +1 -1
  33. package/dist/core/isg/index.js +4 -2
  34. package/dist/core/isg/manifest.js +14 -0
  35. package/dist/core/isg/prune.d.ts +32 -0
  36. package/dist/core/isg/prune.d.ts.map +1 -0
  37. package/dist/core/isg/prune.js +113 -0
  38. package/dist/core/markdown.d.ts +10 -1
  39. package/dist/core/markdown.d.ts.map +1 -1
  40. package/dist/core/markdown.js +13 -3
  41. package/dist/core/navigation.d.ts +4 -2
  42. package/dist/core/navigation.d.ts.map +1 -1
  43. package/dist/core/navigation.js +17 -2
  44. package/dist/core/templates.d.ts +15 -1
  45. package/dist/core/templates.d.ts.map +1 -1
  46. package/dist/core/templates.js +121 -126
  47. package/dist/core/utils/fs.utils.d.ts +29 -1
  48. package/dist/core/utils/fs.utils.d.ts.map +1 -1
  49. package/dist/core/utils/fs.utils.js +74 -1
  50. package/dist/core/utils/index.d.ts +3 -3
  51. package/dist/core/utils/index.d.ts.map +1 -1
  52. package/dist/core/utils/index.js +3 -3
  53. package/dist/core/utils/paths.utils.d.ts +15 -0
  54. package/dist/core/utils/paths.utils.d.ts.map +1 -1
  55. package/dist/core/utils/paths.utils.js +20 -0
  56. package/dist/core/utils/template-discovery.utils.d.ts +25 -1
  57. package/dist/core/utils/template-discovery.utils.d.ts.map +1 -1
  58. package/dist/core/utils/template-discovery.utils.js +61 -11
  59. package/dist/images/config.d.ts +52 -0
  60. package/dist/images/config.d.ts.map +1 -0
  61. package/dist/images/config.js +200 -0
  62. package/dist/images/index.d.ts +15 -0
  63. package/dist/images/index.d.ts.map +1 -0
  64. package/dist/images/index.js +11 -0
  65. package/dist/images/manifest.d.ts +36 -0
  66. package/dist/images/manifest.d.ts.map +1 -0
  67. package/dist/images/manifest.js +69 -0
  68. package/dist/images/markdown-rule.d.ts +26 -0
  69. package/dist/images/markdown-rule.d.ts.map +1 -0
  70. package/dist/images/markdown-rule.js +60 -0
  71. package/dist/images/markup.d.ts +39 -0
  72. package/dist/images/markup.d.ts.map +1 -0
  73. package/dist/images/markup.js +72 -0
  74. package/dist/images/og.d.ts +52 -0
  75. package/dist/images/og.d.ts.map +1 -0
  76. package/dist/images/og.js +165 -0
  77. package/dist/images/plan.d.ts +101 -0
  78. package/dist/images/plan.d.ts.map +1 -0
  79. package/dist/images/plan.js +128 -0
  80. package/dist/images/provider.d.ts +35 -0
  81. package/dist/images/provider.d.ts.map +1 -0
  82. package/dist/images/provider.js +70 -0
  83. package/dist/images/resolve.d.ts +45 -0
  84. package/dist/images/resolve.d.ts.map +1 -0
  85. package/dist/images/resolve.js +51 -0
  86. package/dist/images/runtime.d.ts +258 -0
  87. package/dist/images/runtime.d.ts.map +1 -0
  88. package/dist/images/runtime.js +659 -0
  89. package/dist/images/sources.d.ts +47 -0
  90. package/dist/images/sources.d.ts.map +1 -0
  91. package/dist/images/sources.js +97 -0
  92. package/dist/images/utils/image-probe.utils.d.ts +30 -0
  93. package/dist/images/utils/image-probe.utils.d.ts.map +1 -0
  94. package/dist/images/utils/image-probe.utils.js +307 -0
  95. package/dist/images/utils/index.d.ts +7 -0
  96. package/dist/images/utils/index.d.ts.map +1 -0
  97. package/dist/images/utils/index.js +5 -0
  98. package/dist/index.d.ts +2 -1
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/metrics/noop.d.ts.map +1 -1
  101. package/dist/metrics/noop.js +3 -0
  102. package/dist/metrics/recorder.d.ts.map +1 -1
  103. package/dist/metrics/recorder.js +3 -0
  104. package/dist/metrics/types.d.ts +8 -0
  105. package/dist/metrics/types.d.ts.map +1 -1
  106. package/dist/rss/generator.d.ts.map +1 -1
  107. package/dist/rss/generator.js +5 -2
  108. package/dist/search/generator.d.ts +9 -0
  109. package/dist/search/generator.d.ts.map +1 -1
  110. package/dist/search/generator.js +20 -0
  111. package/dist/search/index.d.ts +1 -1
  112. package/dist/search/index.d.ts.map +1 -1
  113. package/dist/search/index.js +1 -1
  114. package/dist/seo/auto-inject.d.ts +3 -0
  115. package/dist/seo/auto-inject.d.ts.map +1 -1
  116. package/dist/seo/auto-inject.js +1 -0
  117. package/dist/seo/generator.d.ts +24 -0
  118. package/dist/seo/generator.d.ts.map +1 -1
  119. package/dist/seo/generator.js +65 -1
  120. package/dist/seo/index.d.ts +1 -1
  121. package/dist/seo/index.d.ts.map +1 -1
  122. package/dist/seo/index.js +1 -1
  123. package/dist/types/config.d.ts +60 -0
  124. package/dist/types/config.d.ts.map +1 -1
  125. package/dist/types/content.d.ts +150 -3
  126. package/dist/types/content.d.ts.map +1 -1
  127. package/dist/types/images.d.ts +185 -0
  128. package/dist/types/images.d.ts.map +1 -0
  129. package/dist/types/images.js +8 -0
  130. package/dist/types/index.d.ts +3 -2
  131. package/dist/types/index.d.ts.map +1 -1
  132. package/dist/types/isg.d.ts +19 -0
  133. package/dist/types/isg.d.ts.map +1 -1
  134. package/dist/types/seo.d.ts +9 -0
  135. package/dist/types/seo.d.ts.map +1 -1
  136. package/package.json +1 -1
@@ -183,3 +183,23 @@ export function isPathWithinDirectory(baseDir, targetPath) {
183
183
  // We also allow exact match (resolvedTarget === resolvedBase) for the root
184
184
  return resolvedTarget === resolvedBase || resolvedTarget.startsWith(normalizedBase);
185
185
  }
186
+ /**
187
+ * The file a page URL is written to, relative to the output directory. The build joins it to
188
+ * the output directory, which also normalizes separators and `.` segments.
189
+ *
190
+ * @param url - The page URL
191
+ * @returns The output file
192
+ *
193
+ * @example
194
+ * ```typescript
195
+ * getPageOutputFile('/'); // 'index.html'
196
+ * getPageOutputFile('/blog/'); // '/blog/index.html'
197
+ * getPageOutputFile('/blog/post'); // '/blog/post.html'
198
+ * ```
199
+ */
200
+ export function getPageOutputFile(url) {
201
+ if (url === '/') {
202
+ return 'index.html';
203
+ }
204
+ return url.endsWith('/') ? `${url}index.html` : `${url}.html`;
205
+ }
@@ -1,13 +1,37 @@
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
+ /**
7
+ * Whether a page was created by Stati rather than loaded from a Markdown file: a page expanded
8
+ * from a `[param].md` template, or page 2 or later of a paginated listing.
9
+ *
10
+ * Virtual pages are never collection members, never make a folder a collection, and are left
11
+ * out of navigation, search and RSS unless they opt in.
12
+ *
13
+ * @param page - The page to check
14
+ * @returns True for generated pages and for pagination pages after the first
15
+ */
16
+ export declare function isVirtualPage(page: PageModel): boolean;
17
+ /**
18
+ * Finds the first page of a paginated listing, which later pages render like.
19
+ *
20
+ * @param page - A page with `pagination`
21
+ * @param allPages - All pages in the site
22
+ * @returns The listing's first page, or undefined when it is not in the list
23
+ */
24
+ export declare function getFirstPaginationPage(page: PageModel, allPages: PageModel[]): PageModel | undefined;
2
25
  /**
3
26
  * Determines if the given page is a collection index page.
4
27
  * A collection index page is one whose URL matches a directory path that contains other pages.
28
+ * Generated pages never are; pagination pages after the first are when their first page is.
5
29
  *
6
30
  * Rendering and ISG dependency tracking must both call this with the same page list,
7
31
  * otherwise they can pick different layouts for the same page.
8
32
  *
9
33
  * Page URLs are read once per `allPages` array, at the first call; see
10
- * `directoryPrefixCache` for when changes to the pages are picked up.
34
+ * `pageListCache` for when changes to the pages are picked up.
11
35
  *
12
36
  * @param page - The page to check
13
37
  * @param allPages - All pages in the site (treated as unchanging after the first call)
@@ -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;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"}
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;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAEtD;AAiED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,SAAS,EACf,QAAQ,EAAE,SAAS,EAAE,GACpB,SAAS,GAAG,SAAS,CAMvB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,OAAO,CAoBrF;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"}
@@ -2,6 +2,23 @@ import { join, dirname, posix } from 'node:path';
2
2
  import { pathExists } from './fs.utils.js';
3
3
  import { LAYOUT_TEMPLATE } from '../../constants.js';
4
4
  import { resolveSrcDir } from './paths.utils.js';
5
+ /**
6
+ * Shared template discovery utilities.
7
+ * Extracted from templates.ts and isg/deps.ts to eliminate duplication.
8
+ */
9
+ /**
10
+ * Whether a page was created by Stati rather than loaded from a Markdown file: a page expanded
11
+ * from a `[param].md` template, or page 2 or later of a paginated listing.
12
+ *
13
+ * Virtual pages are never collection members, never make a folder a collection, and are left
14
+ * out of navigation, search and RSS unless they opt in.
15
+ *
16
+ * @param page - The page to check
17
+ * @returns True for generated pages and for pagination pages after the first
18
+ */
19
+ export function isVirtualPage(page) {
20
+ return page.generated !== undefined || (page.pagination?.pageNumber ?? 1) > 1;
21
+ }
5
22
  /**
6
23
  * Directory prefix index per page list, so a build computes it once instead of
7
24
  * scanning every page for every page.
@@ -13,51 +30,84 @@ import { resolveSrcDir } from './paths.utils.js';
13
30
  * loads a new array and gets a new snapshot. Taking one snapshot keeps rendering and
14
31
  * ISG dependency tracking in agreement for the whole build.
15
32
  */
16
- const directoryPrefixCache = new WeakMap();
17
- function getDirectoryPrefixIndex(allPages) {
18
- const cached = directoryPrefixCache.get(allPages);
33
+ const pageListCache = new WeakMap();
34
+ function getPageListIndex(allPages) {
35
+ const cached = pageListCache.get(allPages);
19
36
  if (cached) {
20
37
  return cached;
21
38
  }
22
- const index = new Map();
23
- for (const { url } of allPages) {
39
+ const prefixes = new Map();
40
+ const byUrl = new Map();
41
+ for (const page of allPages) {
42
+ if (!byUrl.has(page.url)) {
43
+ byUrl.set(page.url, page);
44
+ }
45
+ // Generated and pagination pages don't turn the folders they live in into collections
46
+ if (isVirtualPage(page)) {
47
+ continue;
48
+ }
49
+ const { url } = page;
24
50
  // Every prefix ending at a '/' is a directory this URL lives in (or is, for trailing-slash URLs)
25
51
  for (let i = url.indexOf('/'); i !== -1; i = url.indexOf('/', i + 1)) {
26
52
  const prefix = url.slice(0, i + 1);
27
- const existing = index.get(prefix);
53
+ const existing = prefixes.get(prefix);
28
54
  if (existing === undefined) {
29
- index.set(prefix, url);
55
+ prefixes.set(prefix, url);
30
56
  }
31
57
  else if (existing !== null && existing !== url) {
32
- index.set(prefix, null);
58
+ prefixes.set(prefix, null);
33
59
  }
34
60
  }
35
61
  }
36
- directoryPrefixCache.set(allPages, index);
62
+ const index = { prefixes, byUrl };
63
+ pageListCache.set(allPages, index);
37
64
  return index;
38
65
  }
66
+ /**
67
+ * Finds the first page of a paginated listing, which later pages render like.
68
+ *
69
+ * @param page - A page with `pagination`
70
+ * @param allPages - All pages in the site
71
+ * @returns The listing's first page, or undefined when it is not in the list
72
+ */
73
+ export function getFirstPaginationPage(page, allPages) {
74
+ const firstUrl = page.pagination?.urls[0];
75
+ if (firstUrl === undefined || firstUrl === page.url) {
76
+ return undefined;
77
+ }
78
+ return getPageListIndex(allPages).byUrl.get(firstUrl);
79
+ }
39
80
  /**
40
81
  * Determines if the given page is a collection index page.
41
82
  * A collection index page is one whose URL matches a directory path that contains other pages.
83
+ * Generated pages never are; pagination pages after the first are when their first page is.
42
84
  *
43
85
  * Rendering and ISG dependency tracking must both call this with the same page list,
44
86
  * otherwise they can pick different layouts for the same page.
45
87
  *
46
88
  * Page URLs are read once per `allPages` array, at the first call; see
47
- * `directoryPrefixCache` for when changes to the pages are picked up.
89
+ * `pageListCache` for when changes to the pages are picked up.
48
90
  *
49
91
  * @param page - The page to check
50
92
  * @param allPages - All pages in the site (treated as unchanging after the first call)
51
93
  * @returns True if the page is a collection index page
52
94
  */
53
95
  export function isCollectionIndexPage(page, allPages) {
96
+ if (page.generated) {
97
+ return false;
98
+ }
99
+ // Later pages of a listing render with the layout of its first page
100
+ if ((page.pagination?.pageNumber ?? 1) > 1) {
101
+ const first = getFirstPaginationPage(page, allPages);
102
+ return first !== undefined && !isVirtualPage(first) && isCollectionIndexPage(first, allPages);
103
+ }
54
104
  // Root index page is always a collection index
55
105
  if (page.url === '/') {
56
106
  return true;
57
107
  }
58
108
  // The page is an index when a different page's URL lives under this page's URL as a directory
59
109
  const pageUrlAsDir = page.url.endsWith('/') ? page.url : page.url + '/';
60
- const containingUrl = getDirectoryPrefixIndex(allPages).get(pageUrlAsDir);
110
+ const containingUrl = getPageListIndex(allPages).prefixes.get(pageUrlAsDir);
61
111
  return containingUrl === null || (containingUrl !== undefined && containingUrl !== page.url);
62
112
  }
63
113
  /**
@@ -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"}
@@ -0,0 +1,11 @@
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 { ImageRuntime, PageImages, validateOgBaseUrl } from './runtime.js';
8
+ export { installImageRule, selectEagerImage } from './markdown-rule.js';
9
+ export { findOgTemplatePath, clearOgTemplateCache } from './og.js';
10
+ export { loadImageProvider, resolveImagesPackage, missingPackageMessage } from './provider.js';
11
+ export { probeImage } from './utils/index.js';
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The images manifest, `.stati/images/manifest.json`: which files each image job produced,
3
+ * and cached hashes and probes of source images.
4
+ * @module images/manifest
5
+ */
6
+ import type { SourceInfo } from './sources.js';
7
+ /**
8
+ * Persisted state of the image pipeline.
9
+ */
10
+ export interface ImagesManifest {
11
+ version: number;
12
+ /** Cached facts about source images, keyed by absolute path */
13
+ sources: Record<string, SourceInfo>;
14
+ /** Files each job wrote, relative to the output directory, keyed by job key */
15
+ outputs: Record<string, {
16
+ files: string[];
17
+ }>;
18
+ }
19
+ /**
20
+ * Creates an empty images manifest.
21
+ */
22
+ export declare function createEmptyImagesManifest(): ImagesManifest;
23
+ /**
24
+ * Path of the images manifest inside the cache directory.
25
+ */
26
+ export declare function getImagesManifestPath(cacheDir: string): string;
27
+ /**
28
+ * Loads the images manifest. A missing, unreadable or outdated manifest yields an empty one,
29
+ * which only means images are checked again.
30
+ */
31
+ export declare function loadImagesManifest(cacheDir: string): Promise<ImagesManifest>;
32
+ /**
33
+ * Saves the images manifest.
34
+ */
35
+ export declare function saveImagesManifest(cacheDir: string, manifest: ImagesManifest): Promise<void>;
36
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/images/manifest.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAK/C;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACpC,+EAA+E;IAC/E,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;CAC9C;AAED;;GAEG;AACH,wBAAgB,yBAAyB,IAAI,cAAc,CAE1D;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE9D;AAMD;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAqClF;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,cAAc,GACvB,OAAO,CAAC,IAAI,CAAC,CAIf"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The images manifest, `.stati/images/manifest.json`: which files each image job produced,
3
+ * and cached hashes and probes of source images.
4
+ * @module images/manifest
5
+ */
6
+ import { join } from 'node:path';
7
+ import { ensureDir, readFile, writeFile } from '../core/utils/index.js';
8
+ import { IMAGES_CACHE_SUBDIR, MANIFEST_FILENAME } from '../constants.js';
9
+ /** Bumped when the manifest layout changes; older manifests are discarded. */
10
+ const IMAGES_MANIFEST_VERSION = 1;
11
+ /**
12
+ * Creates an empty images manifest.
13
+ */
14
+ export function createEmptyImagesManifest() {
15
+ return { version: IMAGES_MANIFEST_VERSION, sources: {}, outputs: {} };
16
+ }
17
+ /**
18
+ * Path of the images manifest inside the cache directory.
19
+ */
20
+ export function getImagesManifestPath(cacheDir) {
21
+ return join(cacheDir, IMAGES_CACHE_SUBDIR, MANIFEST_FILENAME);
22
+ }
23
+ function isStringArray(value) {
24
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
25
+ }
26
+ /**
27
+ * Loads the images manifest. A missing, unreadable or outdated manifest yields an empty one,
28
+ * which only means images are checked again.
29
+ */
30
+ export async function loadImagesManifest(cacheDir) {
31
+ try {
32
+ const content = await readFile(getImagesManifestPath(cacheDir));
33
+ if (!content)
34
+ return createEmptyImagesManifest();
35
+ const parsed = JSON.parse(content);
36
+ if (parsed?.version !== IMAGES_MANIFEST_VERSION ||
37
+ typeof parsed.sources !== 'object' ||
38
+ typeof parsed.outputs !== 'object') {
39
+ return createEmptyImagesManifest();
40
+ }
41
+ const outputs = {};
42
+ for (const [key, record] of Object.entries(parsed.outputs ?? {})) {
43
+ if (record && isStringArray(record.files)) {
44
+ outputs[key] = { files: record.files };
45
+ }
46
+ }
47
+ const sources = {};
48
+ for (const [path, info] of Object.entries(parsed.sources ?? {})) {
49
+ if (info &&
50
+ typeof info.size === 'number' &&
51
+ typeof info.mtimeMs === 'number' &&
52
+ typeof info.hash === 'string') {
53
+ sources[path] = info;
54
+ }
55
+ }
56
+ return { version: IMAGES_MANIFEST_VERSION, sources, outputs };
57
+ }
58
+ catch {
59
+ return createEmptyImagesManifest();
60
+ }
61
+ }
62
+ /**
63
+ * Saves the images manifest.
64
+ */
65
+ export async function saveImagesManifest(cacheDir, manifest) {
66
+ const path = getImagesManifestPath(cacheDir);
67
+ await ensureDir(join(cacheDir, IMAGES_CACHE_SUBDIR));
68
+ await writeFile(path, JSON.stringify(manifest, null, 2), 'utf-8');
69
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The markdown-it `image` rule that adds dimensions, lazy loading and responsive variants.
3
+ * @module images/markdown-rule
4
+ */
5
+ import type MarkdownIt from 'markdown-it';
6
+ import type Token from 'markdown-it/lib/token.mjs';
7
+ /** Key of the page's image state in the markdown-it `env`. */
8
+ export declare const IMAGES_ENV_KEY = "statiImages";
9
+ /**
10
+ * Installs the image rule. It holds no per-page state: each render passes its page's image
11
+ * state in `env`, and renders without it fall through to the previous rule. Installed before
12
+ * plugins and `markdown.configure`, so a custom image rule there replaces it.
13
+ *
14
+ * @param md - The markdown-it instance
15
+ */
16
+ export declare function installImageRule(md: MarkdownIt): void;
17
+ /**
18
+ * Picks the image `eagerFirst` loads eagerly: the document's first image, when it sits in
19
+ * one of the first top-level blocks. A first image further down is not the page's largest
20
+ * contentful paint, so it stays lazy.
21
+ *
22
+ * @param tokens - Parsed block tokens
23
+ * @returns The image token, or undefined
24
+ */
25
+ export declare function selectEagerImage(tokens: Token[]): Token | undefined;
26
+ //# sourceMappingURL=markdown-rule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown-rule.d.ts","sourceRoot":"","sources":["../../src/images/markdown-rule.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,UAAU,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,KAAK,MAAM,2BAA2B,CAAC;AAGnD,8DAA8D;AAC9D,eAAO,MAAM,cAAc,gBAAgB,CAAC;AAO5C;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,UAAU,GAAG,IAAI,CAwBrD;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,GAAG,SAAS,CAanE"}