@stati/core 1.27.2 → 1.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/config/errors.d.ts +10 -0
- package/dist/config/errors.d.ts.map +1 -0
- package/dist/config/errors.js +10 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +15 -2
- package/dist/constants.d.ts +26 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +23 -0
- package/dist/core/build.d.ts.map +1 -1
- package/dist/core/build.js +201 -41
- package/dist/core/collections.d.ts +139 -0
- package/dist/core/collections.d.ts.map +1 -0
- package/dist/core/collections.js +342 -0
- package/dist/core/content.d.ts.map +1 -1
- package/dist/core/content.js +2 -1
- package/dist/core/dev.d.ts.map +1 -1
- package/dist/core/dev.js +11 -62
- package/dist/core/expand.d.ts +86 -0
- package/dist/core/expand.d.ts.map +1 -0
- package/dist/core/expand.js +484 -0
- package/dist/core/isg/builder.d.ts +27 -2
- package/dist/core/isg/builder.d.ts.map +1 -1
- package/dist/core/isg/builder.js +83 -11
- package/dist/core/isg/deps.d.ts +9 -0
- package/dist/core/isg/deps.d.ts.map +1 -1
- package/dist/core/isg/deps.js +30 -1
- package/dist/core/isg/hash.d.ts +14 -2
- package/dist/core/isg/hash.d.ts.map +1 -1
- package/dist/core/isg/hash.js +30 -2
- package/dist/core/isg/index.d.ts +4 -2
- package/dist/core/isg/index.d.ts.map +1 -1
- package/dist/core/isg/index.js +4 -2
- package/dist/core/isg/manifest.js +14 -0
- package/dist/core/isg/prune.d.ts +32 -0
- package/dist/core/isg/prune.d.ts.map +1 -0
- package/dist/core/isg/prune.js +113 -0
- package/dist/core/markdown.d.ts +10 -1
- package/dist/core/markdown.d.ts.map +1 -1
- package/dist/core/markdown.js +13 -3
- package/dist/core/navigation.d.ts +4 -2
- package/dist/core/navigation.d.ts.map +1 -1
- package/dist/core/navigation.js +17 -2
- package/dist/core/templates.d.ts +15 -1
- package/dist/core/templates.d.ts.map +1 -1
- package/dist/core/templates.js +121 -126
- package/dist/core/utils/fs.utils.d.ts +29 -1
- package/dist/core/utils/fs.utils.d.ts.map +1 -1
- package/dist/core/utils/fs.utils.js +74 -1
- package/dist/core/utils/index.d.ts +3 -3
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +3 -3
- package/dist/core/utils/paths.utils.d.ts +15 -0
- package/dist/core/utils/paths.utils.d.ts.map +1 -1
- package/dist/core/utils/paths.utils.js +20 -0
- package/dist/core/utils/template-discovery.utils.d.ts +25 -1
- package/dist/core/utils/template-discovery.utils.d.ts.map +1 -1
- package/dist/core/utils/template-discovery.utils.js +61 -11
- package/dist/images/config.d.ts +52 -0
- package/dist/images/config.d.ts.map +1 -0
- package/dist/images/config.js +200 -0
- package/dist/images/index.d.ts +15 -0
- package/dist/images/index.d.ts.map +1 -0
- package/dist/images/index.js +11 -0
- package/dist/images/manifest.d.ts +36 -0
- package/dist/images/manifest.d.ts.map +1 -0
- package/dist/images/manifest.js +69 -0
- package/dist/images/markdown-rule.d.ts +26 -0
- package/dist/images/markdown-rule.d.ts.map +1 -0
- package/dist/images/markdown-rule.js +60 -0
- package/dist/images/markup.d.ts +39 -0
- package/dist/images/markup.d.ts.map +1 -0
- package/dist/images/markup.js +72 -0
- package/dist/images/og.d.ts +52 -0
- package/dist/images/og.d.ts.map +1 -0
- package/dist/images/og.js +165 -0
- package/dist/images/plan.d.ts +101 -0
- package/dist/images/plan.d.ts.map +1 -0
- package/dist/images/plan.js +128 -0
- package/dist/images/provider.d.ts +35 -0
- package/dist/images/provider.d.ts.map +1 -0
- package/dist/images/provider.js +70 -0
- package/dist/images/resolve.d.ts +45 -0
- package/dist/images/resolve.d.ts.map +1 -0
- package/dist/images/resolve.js +51 -0
- package/dist/images/runtime.d.ts +258 -0
- package/dist/images/runtime.d.ts.map +1 -0
- package/dist/images/runtime.js +659 -0
- package/dist/images/sources.d.ts +47 -0
- package/dist/images/sources.d.ts.map +1 -0
- package/dist/images/sources.js +97 -0
- package/dist/images/utils/image-probe.utils.d.ts +30 -0
- package/dist/images/utils/image-probe.utils.d.ts.map +1 -0
- package/dist/images/utils/image-probe.utils.js +307 -0
- package/dist/images/utils/index.d.ts +7 -0
- package/dist/images/utils/index.d.ts.map +1 -0
- package/dist/images/utils/index.js +5 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/metrics/noop.d.ts.map +1 -1
- package/dist/metrics/noop.js +3 -0
- package/dist/metrics/recorder.d.ts.map +1 -1
- package/dist/metrics/recorder.js +3 -0
- package/dist/metrics/types.d.ts +8 -0
- package/dist/metrics/types.d.ts.map +1 -1
- package/dist/rss/generator.d.ts.map +1 -1
- package/dist/rss/generator.js +5 -2
- package/dist/search/generator.d.ts +9 -0
- package/dist/search/generator.d.ts.map +1 -1
- package/dist/search/generator.js +20 -0
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +1 -1
- package/dist/seo/auto-inject.d.ts +3 -0
- package/dist/seo/auto-inject.d.ts.map +1 -1
- package/dist/seo/auto-inject.js +1 -0
- package/dist/seo/generator.d.ts +24 -0
- package/dist/seo/generator.d.ts.map +1 -1
- package/dist/seo/generator.js +65 -1
- package/dist/seo/index.d.ts +1 -1
- package/dist/seo/index.d.ts.map +1 -1
- package/dist/seo/index.js +1 -1
- package/dist/types/config.d.ts +60 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/content.d.ts +150 -3
- package/dist/types/content.d.ts.map +1 -1
- package/dist/types/images.d.ts +185 -0
- package/dist/types/images.d.ts.map +1 -0
- package/dist/types/images.js +8 -0
- package/dist/types/index.d.ts +3 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/isg.d.ts +19 -0
- package/dist/types/isg.d.ts.map +1 -1
- package/dist/types/seo.d.ts +9 -0
- package/dist/types/seo.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/core/isg/builder.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computeContentHash, computeFileHash, computeInputsHash } from './hash.js';
|
|
1
|
+
import { computeContentHash, computeFileHash, computeBinaryFileHash, computeInputsHash, } from './hash.js';
|
|
2
2
|
import { trackTemplateDependencies, resolveLayoutDependency } from './deps.js';
|
|
3
3
|
import { computeEffectiveTTL, computeNextRebuildAt, isPageFrozen } from './ttl.js';
|
|
4
4
|
import { validatePageISGOverrides, extractNumericOverride } from './validation.js';
|
|
@@ -55,8 +55,74 @@ function isValidCacheEntry(entry) {
|
|
|
55
55
|
if (entry.maxAgeCapDays !== undefined && typeof entry.maxAgeCapDays !== 'number') {
|
|
56
56
|
return false;
|
|
57
57
|
}
|
|
58
|
+
for (const list of [entry.assetDeps, entry.images]) {
|
|
59
|
+
if (list !== undefined && (!Array.isArray(list) || !list.every((v) => typeof v === 'string'))) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
for (const flag of [entry.readsCollection, entry.readsNav]) {
|
|
64
|
+
if (flag !== undefined && typeof flag !== 'boolean') {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
58
68
|
return true;
|
|
59
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Whether a cache entry records what its page read while rendering. Entries written before
|
|
72
|
+
* Stati recorded it don't, and their page must be rebuilt once: whether it depends on its
|
|
73
|
+
* collection or the navigation tree is unknown, and assuming it doesn't could keep stale
|
|
74
|
+
* listings or prev/next links.
|
|
75
|
+
*
|
|
76
|
+
* @param entry - The cache entry
|
|
77
|
+
* @returns True when `readsCollection` and `readsNav` are both recorded
|
|
78
|
+
*/
|
|
79
|
+
export function hasRecordedReads(entry) {
|
|
80
|
+
return typeof entry.readsCollection === 'boolean' && typeof entry.readsNav === 'boolean';
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Tags a page's cache entry carries for `stati invalidate "tag:…"`: its front matter tags,
|
|
84
|
+
* plus the key and label of a generated page, so `tag:css` also refreshes `/tags/css`.
|
|
85
|
+
*/
|
|
86
|
+
function getCacheTags(page) {
|
|
87
|
+
const tags = Array.isArray(page.frontMatter.tags)
|
|
88
|
+
? page.frontMatter.tags.filter((tag) => typeof tag === 'string')
|
|
89
|
+
: [];
|
|
90
|
+
if (page.generated) {
|
|
91
|
+
for (const tag of [page.generated.key, page.generated.label]) {
|
|
92
|
+
if (tag && !tags.includes(tag)) {
|
|
93
|
+
tags.push(tag);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return tags;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Hashes render-discovered dependencies by content. Missing files hash to a sentinel.
|
|
101
|
+
*/
|
|
102
|
+
async function hashAssetDeps(assetDeps) {
|
|
103
|
+
const hashes = [];
|
|
104
|
+
for (const assetPath of assetDeps ?? []) {
|
|
105
|
+
hashes.push(await computeBinaryFileHash(assetPath));
|
|
106
|
+
}
|
|
107
|
+
return hashes;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Returns the entry with its `assetDeps` and `images` replaced by this render's,
|
|
111
|
+
* dropping the fields when a render found none, and with what the render read.
|
|
112
|
+
*/
|
|
113
|
+
function withRenderedAssets(entry, rendered) {
|
|
114
|
+
const { assetDeps: _assetDeps, images: _images, readsCollection: _readsCollection, readsNav: _readsNav, ...rest } = entry;
|
|
115
|
+
return {
|
|
116
|
+
...rest,
|
|
117
|
+
...(rendered && rendered.assetDeps.length > 0 && { assetDeps: [...rendered.assetDeps] }),
|
|
118
|
+
...(rendered && rendered.images.length > 0 && { images: [...rendered.images] }),
|
|
119
|
+
// Always written, false included: a missing flag means "unknown" and forces a rebuild
|
|
120
|
+
...(rendered?.reads && {
|
|
121
|
+
readsCollection: rendered.reads.collection,
|
|
122
|
+
readsNav: rendered.reads.nav,
|
|
123
|
+
}),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
60
126
|
/**
|
|
61
127
|
* Determines if a page should be rebuilt based on ISG logic.
|
|
62
128
|
* Checks content changes, dependency changes, TTL expiration, and freeze status.
|
|
@@ -133,6 +199,8 @@ export async function shouldRebuildPage(page, entry, config, allPages, now, rend
|
|
|
133
199
|
console.warn(`${dependencyErrors} dependency errors for ${page.url}, forcing rebuild`);
|
|
134
200
|
return true;
|
|
135
201
|
}
|
|
202
|
+
// Files found during the last render, such as images, are hashed by content
|
|
203
|
+
depsHashes.push(...(await hashAssetDeps(entry.assetDeps)));
|
|
136
204
|
const currentInputsHash = computeInputsHash(currentContentHash, depsHashes, renderInputsHash);
|
|
137
205
|
// If inputs changed, always rebuild
|
|
138
206
|
if (currentInputsHash !== entry.inputsHash) {
|
|
@@ -215,6 +283,7 @@ export async function shouldRebuildPage(page, entry, config, allPages, now, rend
|
|
|
215
283
|
* @param allPages - All pages in the site, used to resolve the same layout as rendering
|
|
216
284
|
* @param renderedAt - When the page was rendered
|
|
217
285
|
* @param renderInputsHash - Optional hash of asset URLs and injection settings
|
|
286
|
+
* @param rendered - Files the render discovered, such as images
|
|
218
287
|
* @returns New cache entry
|
|
219
288
|
*
|
|
220
289
|
* @example
|
|
@@ -222,7 +291,7 @@ export async function shouldRebuildPage(page, entry, config, allPages, now, rend
|
|
|
222
291
|
* const entry = await createCacheEntry(page, config, pages, new Date());
|
|
223
292
|
* ```
|
|
224
293
|
*/
|
|
225
|
-
export async function createCacheEntry(page, config, allPages, renderedAt, renderInputsHash) {
|
|
294
|
+
export async function createCacheEntry(page, config, allPages, renderedAt, renderInputsHash, rendered) {
|
|
226
295
|
const timings = {};
|
|
227
296
|
let start = performance.now();
|
|
228
297
|
// Validate page-level ISG overrides first
|
|
@@ -245,13 +314,11 @@ export async function createCacheEntry(page, config, allPages, renderedAt, rende
|
|
|
245
314
|
depsHashes.push(depHash);
|
|
246
315
|
}
|
|
247
316
|
}
|
|
317
|
+
depsHashes.push(...(await hashAssetDeps(rendered?.assetDeps)));
|
|
248
318
|
timings.depsHashes = performance.now() - start;
|
|
249
319
|
const inputsHash = computeInputsHash(contentHash, depsHashes, renderInputsHash);
|
|
250
320
|
// Extract tags from front matter
|
|
251
|
-
|
|
252
|
-
if (Array.isArray(page.frontMatter.tags)) {
|
|
253
|
-
tags = page.frontMatter.tags.filter((tag) => typeof tag === 'string');
|
|
254
|
-
}
|
|
321
|
+
const tags = getCacheTags(page);
|
|
255
322
|
// Get published date
|
|
256
323
|
const publishedAt = getPublishedDateISO(page);
|
|
257
324
|
// Compute effective TTL
|
|
@@ -287,7 +354,7 @@ export async function createCacheEntry(page, config, allPages, renderedAt, rende
|
|
|
287
354
|
if (maxAgeCapDays !== undefined) {
|
|
288
355
|
cacheEntry.maxAgeCapDays = maxAgeCapDays;
|
|
289
356
|
}
|
|
290
|
-
return cacheEntry;
|
|
357
|
+
return withRenderedAssets(cacheEntry, rendered);
|
|
291
358
|
}
|
|
292
359
|
/**
|
|
293
360
|
* Checks if the template structure has changed by comparing the layout file's
|
|
@@ -345,6 +412,7 @@ async function hasTemplateStructureChanged(entry) {
|
|
|
345
412
|
* @param renderedAt - When the page was rendered
|
|
346
413
|
* @param fastUpdate - If true, skip expensive dep tracking (dev mode optimization)
|
|
347
414
|
* @param renderInputsHash - Optional hash of asset URLs and injection settings
|
|
415
|
+
* @param rendered - Files the render discovered, such as images
|
|
348
416
|
* @returns Updated cache entry
|
|
349
417
|
*
|
|
350
418
|
* @example
|
|
@@ -352,7 +420,7 @@ async function hasTemplateStructureChanged(entry) {
|
|
|
352
420
|
* const updatedEntry = await updateCacheEntry(existingEntry, page, config, pages, new Date());
|
|
353
421
|
* ```
|
|
354
422
|
*/
|
|
355
|
-
export async function updateCacheEntry(entry, page, config, allPages, renderedAt, fastUpdate = false, renderInputsHash) {
|
|
423
|
+
export async function updateCacheEntry(entry, page, config, allPages, renderedAt, fastUpdate = false, renderInputsHash, rendered) {
|
|
356
424
|
// In fast update mode (dev), reuse existing deps to avoid expensive tracking
|
|
357
425
|
if (fastUpdate && entry.deps.length > 0) {
|
|
358
426
|
// Detect if template structure has changed (new includes/extends added)
|
|
@@ -380,16 +448,20 @@ export async function updateCacheEntry(entry, page, config, allPages, renderedAt
|
|
|
380
448
|
depsHashes.push(depHash);
|
|
381
449
|
}
|
|
382
450
|
}
|
|
451
|
+
// Asset deps come from this render, not from the previous entry
|
|
452
|
+
depsHashes.push(...(await hashAssetDeps(rendered?.assetDeps)));
|
|
383
453
|
const inputsHash = computeInputsHash(contentHash, depsHashes, renderInputsHash);
|
|
384
|
-
return {
|
|
454
|
+
return withRenderedAssets({
|
|
385
455
|
...entry,
|
|
386
456
|
inputsHash,
|
|
457
|
+
// Front matter tags, and a generated page's label, can change without a layout change
|
|
458
|
+
tags: getCacheTags(page),
|
|
387
459
|
renderedAt: renderedAt.toISOString(),
|
|
388
|
-
};
|
|
460
|
+
}, rendered);
|
|
389
461
|
}
|
|
390
462
|
}
|
|
391
463
|
// Full update - create new entry and preserve original publishedAt
|
|
392
|
-
const newEntry = await createCacheEntry(page, config, allPages, renderedAt, renderInputsHash);
|
|
464
|
+
const newEntry = await createCacheEntry(page, config, allPages, renderedAt, renderInputsHash, rendered);
|
|
393
465
|
// Preserve original publishedAt if no new one is specified
|
|
394
466
|
if (!newEntry.publishedAt && entry.publishedAt) {
|
|
395
467
|
newEntry.publishedAt = entry.publishedAt;
|
package/dist/core/isg/deps.d.ts
CHANGED
|
@@ -50,6 +50,15 @@ export declare function trackTemplateDependencies(page: PageModel, config: Stati
|
|
|
50
50
|
* @returns Absolute path to the layout file (POSIX format), or null if no layout applies
|
|
51
51
|
*/
|
|
52
52
|
export declare function resolveLayoutDependency(page: PageModel, config: StatiConfig, allPages: PageModel[]): Promise<string | null>;
|
|
53
|
+
/**
|
|
54
|
+
* Resolves the Open Graph template a page renders with, when Open Graph images are on and the
|
|
55
|
+
* page does not opt out. Pages that use the built-in template have no file dependency.
|
|
56
|
+
*
|
|
57
|
+
* @param page - The page model
|
|
58
|
+
* @param config - Stati configuration
|
|
59
|
+
* @returns Absolute path to the template (POSIX format), or null
|
|
60
|
+
*/
|
|
61
|
+
export declare function resolveOgTemplateDependency(page: PageModel, config: StatiConfig): Promise<string | null>;
|
|
53
62
|
/**
|
|
54
63
|
* Resolves a template name to its file path.
|
|
55
64
|
* Used for explicit layout specifications in front matter.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deps.d.ts","sourceRoot":"","sources":["../../../src/core/isg/deps.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"deps.d.ts","sourceRoot":"","sources":["../../../src/core/isg/deps.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAgBnE;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAI7C;AAED;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;aAE9B,eAAe,EAAE,MAAM,EAAE;gBAAzB,eAAe,EAAE,MAAM,EAAE,EACzC,OAAO,EAAE,MAAM;CAKlB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,SAAS,EAAE,GACpB,OAAO,CAAC,MAAM,EAAE,CAAC,CA+CnB;AAED;;;;;;;;;GASG;AACH,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,SAAS,EAAE,GACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAexB;AAED;;;;;;;GAOG;AACH,wBAAsB,2BAA2B,CAC/C,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAexB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CASxB"}
|
package/dist/core/isg/deps.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { join, dirname, relative, posix } from 'node:path';
|
|
2
2
|
import { pathExists, readFile, isCollectionIndexPage, discoverLayout, resolveSrcDir, } from '../utils/index.js';
|
|
3
3
|
import glob from 'fast-glob';
|
|
4
|
-
import { TEMPLATE_EXTENSION } from '../../constants.js';
|
|
4
|
+
import { TEMPLATE_EXTENSION, DEFAULT_OG_TEMPLATE } from '../../constants.js';
|
|
5
|
+
import { clearOgTemplateCache, findOgTemplatePath, isOgImageExcluded } from '../../images/og.js';
|
|
5
6
|
/**
|
|
6
7
|
* Per-build cache for template path resolution (glob results).
|
|
7
8
|
* Cleared at the start of each build via clearTemplatePathCache().
|
|
@@ -19,6 +20,7 @@ const templateContentCache = new Map();
|
|
|
19
20
|
export function clearTemplatePathCache() {
|
|
20
21
|
templatePathCache.clear();
|
|
21
22
|
templateContentCache.clear();
|
|
23
|
+
clearOgTemplateCache();
|
|
22
24
|
}
|
|
23
25
|
/**
|
|
24
26
|
* Error thrown when a circular dependency is detected in templates.
|
|
@@ -78,6 +80,13 @@ export async function trackTemplateDependencies(page, config, allPages) {
|
|
|
78
80
|
// This populates 'visited' with all actually-used templates (not just accessible ones)
|
|
79
81
|
await collectTemplateDependencies(absoluteLayoutPath, srcDir, visited, currentPath);
|
|
80
82
|
}
|
|
83
|
+
// The Open Graph template renders with the page, so it and its partials are dependencies too.
|
|
84
|
+
// It comes after the layout, which must stay first.
|
|
85
|
+
const ogTemplatePath = await resolveOgTemplateDependency(page, config);
|
|
86
|
+
if (ogTemplatePath) {
|
|
87
|
+
deps.push(ogTemplatePath);
|
|
88
|
+
await collectTemplateDependencies(ogTemplatePath, srcDir, visited, currentPath);
|
|
89
|
+
}
|
|
81
90
|
// 2. Add all actually-used templates from the visited set
|
|
82
91
|
// Filter to only include underscore directories (partials/components)
|
|
83
92
|
// and normalize paths to POSIX format
|
|
@@ -110,6 +119,26 @@ export async function resolveLayoutDependency(page, config, allPages) {
|
|
|
110
119
|
// Normalize to POSIX format for consistent manifest output across platforms
|
|
111
120
|
return layoutPath ? posix.join(srcDir.replace(/\\/g, '/'), layoutPath) : null;
|
|
112
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Resolves the Open Graph template a page renders with, when Open Graph images are on and the
|
|
124
|
+
* page does not opt out. Pages that use the built-in template have no file dependency.
|
|
125
|
+
*
|
|
126
|
+
* @param page - The page model
|
|
127
|
+
* @param config - Stati configuration
|
|
128
|
+
* @returns Absolute path to the template (POSIX format), or null
|
|
129
|
+
*/
|
|
130
|
+
export async function resolveOgTemplateDependency(page, config) {
|
|
131
|
+
const og = config.images ? config.images.og : undefined;
|
|
132
|
+
if (!og || !config.srcDir) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const options = og === true ? {} : og;
|
|
136
|
+
if (isOgImageExcluded(page, options.exclude ?? [])) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const templatePath = await findOgTemplatePath(page.sourcePath, resolveSrcDir(config), options.template ?? DEFAULT_OG_TEMPLATE);
|
|
140
|
+
return templatePath ? templatePath.replace(/\\/g, '/') : null;
|
|
141
|
+
}
|
|
113
142
|
/**
|
|
114
143
|
* Resolves a template name to its file path.
|
|
115
144
|
* Used for explicit layout specifications in front matter.
|
package/dist/core/isg/hash.d.ts
CHANGED
|
@@ -33,6 +33,16 @@ export declare function computeContentHash(content: string, frontMatter: Record<
|
|
|
33
33
|
* ```
|
|
34
34
|
*/
|
|
35
35
|
export declare function computeFileHash(filePath: string): Promise<string | null>;
|
|
36
|
+
/**
|
|
37
|
+
* Computes a hash of a binary file, such as an image a page displays.
|
|
38
|
+
* Unlike {@link computeFileHash}, it hashes raw bytes, and a missing file hashes to a
|
|
39
|
+
* fixed sentinel: a page that references a missing image stays cached until the file
|
|
40
|
+
* appears, and then rebuilds.
|
|
41
|
+
*
|
|
42
|
+
* @param filePath - Absolute path to the file
|
|
43
|
+
* @returns `sha256-…` for an existing file, `missing:<path>` otherwise
|
|
44
|
+
*/
|
|
45
|
+
export declare function computeBinaryFileHash(filePath: string): Promise<string>;
|
|
36
46
|
/**
|
|
37
47
|
* Computes a combined hash from content hash and dependency hashes.
|
|
38
48
|
* This represents the complete input state for a page.
|
|
@@ -51,8 +61,10 @@ export declare function computeFileHash(filePath: string): Promise<string | null
|
|
|
51
61
|
*/
|
|
52
62
|
export declare function computeInputsHash(contentHash: string, depsHashes: string[], renderInputsHash?: string): string;
|
|
53
63
|
/**
|
|
54
|
-
* Computes a hash of the navigation tree
|
|
55
|
-
*
|
|
64
|
+
* Computes a hash of the navigation tree.
|
|
65
|
+
* Covers every field of a navigation node (title, url, path, order, publishedAt,
|
|
66
|
+
* isCollection and children), so pages that read the navigation rebuild when any of
|
|
67
|
+
* what they can show changes.
|
|
56
68
|
*
|
|
57
69
|
* @param navigation - The navigation tree to hash
|
|
58
70
|
* @returns SHA-256 hash as a hex string
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../../../src/core/isg/hash.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../../../src/core/isg/hash.ts"],"names":[],"mappings":"AAgCA;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAmBhG;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA8B9E;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG7E;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAAE,EACpB,gBAAgB,CAAC,EAAE,MAAM,GACxB,MAAM,CASR;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,OAAO,EAAE,GAAG,MAAM,CA+CnE"}
|
package/dist/core/isg/hash.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { readFile, pathExists } from '../utils/index.js';
|
|
3
|
+
import { inspectSource } from '../../images/sources.js';
|
|
3
4
|
/**
|
|
4
5
|
* Creates a SHA-256 hash instance, updates it with data, and returns the hex digest.
|
|
5
6
|
* Internal utility to eliminate duplicate hash creation patterns.
|
|
@@ -51,6 +52,9 @@ export function computeContentHash(content, frontMatter) {
|
|
|
51
52
|
const sortKeys = (obj) => {
|
|
52
53
|
if (obj === null || typeof obj !== 'object')
|
|
53
54
|
return obj;
|
|
55
|
+
// A date has no keys to sort; kept as it is, it serializes as its ISO string, not {}
|
|
56
|
+
if (obj instanceof Date)
|
|
57
|
+
return obj;
|
|
54
58
|
if (Array.isArray(obj))
|
|
55
59
|
return obj.map(sortKeys);
|
|
56
60
|
const sorted = {};
|
|
@@ -104,6 +108,19 @@ export async function computeFileHash(filePath) {
|
|
|
104
108
|
return null;
|
|
105
109
|
}
|
|
106
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Computes a hash of a binary file, such as an image a page displays.
|
|
113
|
+
* Unlike {@link computeFileHash}, it hashes raw bytes, and a missing file hashes to a
|
|
114
|
+
* fixed sentinel: a page that references a missing image stays cached until the file
|
|
115
|
+
* appears, and then rebuilds.
|
|
116
|
+
*
|
|
117
|
+
* @param filePath - Absolute path to the file
|
|
118
|
+
* @returns `sha256-…` for an existing file, `missing:<path>` otherwise
|
|
119
|
+
*/
|
|
120
|
+
export async function computeBinaryFileHash(filePath) {
|
|
121
|
+
const info = await inspectSource(filePath);
|
|
122
|
+
return info ? `sha256-${info.hash}` : `missing:${filePath.replace(/\\/g, '/')}`;
|
|
123
|
+
}
|
|
107
124
|
/**
|
|
108
125
|
* Computes a combined hash from content hash and dependency hashes.
|
|
109
126
|
* This represents the complete input state for a page.
|
|
@@ -130,8 +147,10 @@ export function computeInputsHash(contentHash, depsHashes, renderInputsHash) {
|
|
|
130
147
|
]);
|
|
131
148
|
}
|
|
132
149
|
/**
|
|
133
|
-
* Computes a hash of the navigation tree
|
|
134
|
-
*
|
|
150
|
+
* Computes a hash of the navigation tree.
|
|
151
|
+
* Covers every field of a navigation node (title, url, path, order, publishedAt,
|
|
152
|
+
* isCollection and children), so pages that read the navigation rebuild when any of
|
|
153
|
+
* what they can show changes.
|
|
135
154
|
*
|
|
136
155
|
* @param navigation - The navigation tree to hash
|
|
137
156
|
* @returns SHA-256 hash as a hex string
|
|
@@ -148,9 +167,18 @@ export function computeNavigationHash(navigation) {
|
|
|
148
167
|
title: String(node.title ?? ''),
|
|
149
168
|
url: String(node.url ?? ''),
|
|
150
169
|
};
|
|
170
|
+
if (typeof node.path === 'string') {
|
|
171
|
+
normalized.path = node.path;
|
|
172
|
+
}
|
|
151
173
|
if (node.order !== undefined && typeof node.order === 'number') {
|
|
152
174
|
normalized.order = node.order;
|
|
153
175
|
}
|
|
176
|
+
if (node.publishedAt instanceof Date && !Number.isNaN(node.publishedAt.getTime())) {
|
|
177
|
+
normalized.publishedAt = node.publishedAt.toISOString();
|
|
178
|
+
}
|
|
179
|
+
if (typeof node.isCollection === 'boolean') {
|
|
180
|
+
normalized.isCollection = node.isCollection;
|
|
181
|
+
}
|
|
154
182
|
if (node.children && Array.isArray(node.children) && node.children.length > 0) {
|
|
155
183
|
normalized.children = node.children.map((child) => normalizeNavNode(child));
|
|
156
184
|
}
|
package/dist/core/isg/index.d.ts
CHANGED
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
* @module isg
|
|
8
8
|
*/
|
|
9
9
|
export { loadCacheManifest, saveCacheManifest, createEmptyManifest } from './manifest.js';
|
|
10
|
-
export { shouldRebuildPage, createCacheEntry, updateCacheEntry } from './builder.js';
|
|
10
|
+
export { shouldRebuildPage, createCacheEntry, updateCacheEntry, hasRecordedReads, } from './builder.js';
|
|
11
|
+
export type { RenderedAssets } from './builder.js';
|
|
12
|
+
export { pruneOrphanedPages } from './prune.js';
|
|
11
13
|
export { BuildLockManager, withBuildLock } from './build-lock.js';
|
|
12
14
|
export { DevServerLockManager } from './dev-server-lock.js';
|
|
13
15
|
export { CircularDependencyError, trackTemplateDependencies, resolveTemplatePath, clearTemplatePathCache, } from './deps.js';
|
|
14
|
-
export { computeContentHash, computeFileHash, computeInputsHash, computeNavigationHash, clearFileHashCache, } from './hash.js';
|
|
16
|
+
export { computeContentHash, computeFileHash, computeBinaryFileHash, computeInputsHash, computeNavigationHash, clearFileHashCache, } from './hash.js';
|
|
15
17
|
export { getSafeCurrentTime, parseSafeDate, computeEffectiveTTL, computeNextRebuildAt, isPageFrozen, applyAgingRules, } from './ttl.js';
|
|
16
18
|
export { ISGConfigurationError, validateISGConfig, validatePageISGOverrides, extractNumericOverride, } from './validation.js';
|
|
17
19
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/isg/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAG1F,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/isg/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAG1F,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAGnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAGhD,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAG5D,OAAO,EACL,uBAAuB,EACvB,yBAAyB,EACzB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,mBAAmB,EACnB,oBAAoB,EACpB,YAAY,EACZ,eAAe,GAChB,MAAM,UAAU,CAAC;AAGlB,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EACjB,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,iBAAiB,CAAC"}
|
package/dist/core/isg/index.js
CHANGED
|
@@ -9,14 +9,16 @@
|
|
|
9
9
|
// Manifest management
|
|
10
10
|
export { loadCacheManifest, saveCacheManifest, createEmptyManifest } from './manifest.js';
|
|
11
11
|
// Build decision logic
|
|
12
|
-
export { shouldRebuildPage, createCacheEntry, updateCacheEntry } from './builder.js';
|
|
12
|
+
export { shouldRebuildPage, createCacheEntry, updateCacheEntry, hasRecordedReads, } from './builder.js';
|
|
13
|
+
// Removal of pages that no longer exist
|
|
14
|
+
export { pruneOrphanedPages } from './prune.js';
|
|
13
15
|
// Build locking
|
|
14
16
|
export { BuildLockManager, withBuildLock } from './build-lock.js';
|
|
15
17
|
export { DevServerLockManager } from './dev-server-lock.js';
|
|
16
18
|
// Dependency tracking
|
|
17
19
|
export { CircularDependencyError, trackTemplateDependencies, resolveTemplatePath, clearTemplatePathCache, } from './deps.js';
|
|
18
20
|
// Hash computation
|
|
19
|
-
export { computeContentHash, computeFileHash, computeInputsHash, computeNavigationHash, clearFileHashCache, } from './hash.js';
|
|
21
|
+
export { computeContentHash, computeFileHash, computeBinaryFileHash, computeInputsHash, computeNavigationHash, clearFileHashCache, } from './hash.js';
|
|
20
22
|
// TTL and aging
|
|
21
23
|
export { getSafeCurrentTime, parseSafeDate, computeEffectiveTTL, computeNextRebuildAt, isPageFrozen, applyAgingRules, } from './ttl.js';
|
|
22
24
|
// Validation
|
|
@@ -229,6 +229,20 @@ function validateCacheEntry(entry, path) {
|
|
|
229
229
|
console.warn(`Invalid cache entry for ${path}: "maxAgeCapDays" must be a number if present`);
|
|
230
230
|
return false;
|
|
231
231
|
}
|
|
232
|
+
for (const field of ['assetDeps', 'images']) {
|
|
233
|
+
const value = entryObj[field];
|
|
234
|
+
if (value !== undefined &&
|
|
235
|
+
(!Array.isArray(value) || !value.every((item) => typeof item === 'string'))) {
|
|
236
|
+
console.warn(`Invalid cache entry for ${path}: "${field}" must be an array of strings`);
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
for (const field of ['readsCollection', 'readsNav']) {
|
|
241
|
+
if (entryObj[field] !== undefined && typeof entryObj[field] !== 'boolean') {
|
|
242
|
+
console.warn(`Invalid cache entry for ${path}: "${field}" must be a boolean if present`);
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
232
246
|
// Validate that deps and tags arrays contain strings
|
|
233
247
|
if (!entryObj.deps.every((dep) => typeof dep === 'string')) {
|
|
234
248
|
console.warn(`Invalid cache entry for ${path}: all "deps" must be strings`);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { CacheManifest } from '../../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Result of {@link pruneOrphanedPages}.
|
|
4
|
+
*/
|
|
5
|
+
export interface PruneResult {
|
|
6
|
+
/** Cache keys whose output file was deleted */
|
|
7
|
+
removed: string[];
|
|
8
|
+
/** Output files that could not be deleted, with the reason */
|
|
9
|
+
failed: Array<{
|
|
10
|
+
key: string;
|
|
11
|
+
message: string;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Removes the output of pages that no longer exist, such as a deleted post or a tag that no
|
|
16
|
+
* post uses anymore.
|
|
17
|
+
*
|
|
18
|
+
* Every manifest entry whose cache key is not one of this build's pages is an orphan: its
|
|
19
|
+
* HTML file is deleted, then its entry is dropped from the manifest, and so are the folders
|
|
20
|
+
* that deleting it leaves empty. Only files the manifest records are deleted, so files from
|
|
21
|
+
* the static directory or from other tools in the output directory stay. Nothing is deleted
|
|
22
|
+
* outside the output directory, including through a folder that links to somewhere else, and
|
|
23
|
+
* neither is a file that one of this build's pages writes under another path.
|
|
24
|
+
*
|
|
25
|
+
* @param manifest - The cache manifest; orphaned entries are removed from it, except those
|
|
26
|
+
* whose file could not be deleted, so the next build tries again
|
|
27
|
+
* @param liveKeys - Cache keys of this build's pages, rendered or served from the cache
|
|
28
|
+
* @param outDir - The output directory
|
|
29
|
+
* @returns The deleted outputs and the ones that could not be deleted
|
|
30
|
+
*/
|
|
31
|
+
export declare function pruneOrphanedPages(manifest: CacheManifest, liveKeys: ReadonlySet<string>, outDir: string): Promise<PruneResult>;
|
|
32
|
+
//# sourceMappingURL=prune.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prune.d.ts","sourceRoot":"","sources":["../../../src/core/isg/prune.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAW1D;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,+CAA+C;IAC/C,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,8DAA8D;IAC9D,MAAM,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACjD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,aAAa,EACvB,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,EAC7B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,WAAW,CAAC,CAuEtB"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { dirname, resolve } from 'node:path';
|
|
2
|
+
import { isPathWithinDirectory, lstat, pathExists, readdir, realpath, remove, stat, } from '../utils/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Removes the output of pages that no longer exist, such as a deleted post or a tag that no
|
|
5
|
+
* post uses anymore.
|
|
6
|
+
*
|
|
7
|
+
* Every manifest entry whose cache key is not one of this build's pages is an orphan: its
|
|
8
|
+
* HTML file is deleted, then its entry is dropped from the manifest, and so are the folders
|
|
9
|
+
* that deleting it leaves empty. Only files the manifest records are deleted, so files from
|
|
10
|
+
* the static directory or from other tools in the output directory stay. Nothing is deleted
|
|
11
|
+
* outside the output directory, including through a folder that links to somewhere else, and
|
|
12
|
+
* neither is a file that one of this build's pages writes under another path.
|
|
13
|
+
*
|
|
14
|
+
* @param manifest - The cache manifest; orphaned entries are removed from it, except those
|
|
15
|
+
* whose file could not be deleted, so the next build tries again
|
|
16
|
+
* @param liveKeys - Cache keys of this build's pages, rendered or served from the cache
|
|
17
|
+
* @param outDir - The output directory
|
|
18
|
+
* @returns The deleted outputs and the ones that could not be deleted
|
|
19
|
+
*/
|
|
20
|
+
export async function pruneOrphanedPages(manifest, liveKeys, outDir) {
|
|
21
|
+
const result = { removed: [], failed: [] };
|
|
22
|
+
const outRoot = resolve(outDir);
|
|
23
|
+
let realRoot;
|
|
24
|
+
// Files of this build's pages, read the first time a file is about to be deleted. A stale
|
|
25
|
+
// entry can name one of them by another path: through a linked folder, or in other letter
|
|
26
|
+
// case after renaming About.md to about.md on a case-insensitive file system.
|
|
27
|
+
let liveFiles;
|
|
28
|
+
const isLiveFile = async (path) => {
|
|
29
|
+
const id = await fileIdentity(path);
|
|
30
|
+
if (id === undefined) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (!liveFiles) {
|
|
34
|
+
liveFiles = new Set();
|
|
35
|
+
for (const key of liveKeys) {
|
|
36
|
+
const liveId = await fileIdentity(resolve(outRoot, key.replace(/^\/+/, '')));
|
|
37
|
+
if (liveId !== undefined) {
|
|
38
|
+
liveFiles.add(liveId);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return liveFiles.has(id);
|
|
43
|
+
};
|
|
44
|
+
for (const key of Object.keys(manifest.entries)) {
|
|
45
|
+
if (liveKeys.has(key)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
// Cache keys are output paths relative to the output directory, such as /blog/post.html
|
|
49
|
+
const relativePath = key.replace(/^\/+/, '');
|
|
50
|
+
const outputPath = resolve(outRoot, relativePath);
|
|
51
|
+
let removed = false;
|
|
52
|
+
try {
|
|
53
|
+
if (relativePath.endsWith('.html') &&
|
|
54
|
+
outputPath !== outRoot &&
|
|
55
|
+
isPathWithinDirectory(outRoot, outputPath) &&
|
|
56
|
+
(await pathExists(outputPath)) &&
|
|
57
|
+
(await stat(outputPath)).isFile()) {
|
|
58
|
+
// Resolves links, so a folder linked to somewhere outside is left alone
|
|
59
|
+
realRoot ??= await realpath(outRoot);
|
|
60
|
+
if (isPathWithinDirectory(realRoot, await realpath(dirname(outputPath))) &&
|
|
61
|
+
!(await isLiveFile(outputPath))) {
|
|
62
|
+
await remove(outputPath);
|
|
63
|
+
removed = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
// The entry stays, so the next build tries again
|
|
69
|
+
result.failed.push({ key, message: error instanceof Error ? error.message : String(error) });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
delete manifest.entries[key];
|
|
73
|
+
if (removed) {
|
|
74
|
+
result.removed.push(key);
|
|
75
|
+
try {
|
|
76
|
+
await removeEmptyFolders(dirname(outputPath), outRoot);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// An empty folder left behind does no harm
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Identifies a file by its device and file number, which every path to the file shares.
|
|
87
|
+
*
|
|
88
|
+
* @returns The identity, or undefined when the file is missing or the file system reports no
|
|
89
|
+
* file numbers
|
|
90
|
+
*/
|
|
91
|
+
async function fileIdentity(path) {
|
|
92
|
+
try {
|
|
93
|
+
const stats = await stat(path);
|
|
94
|
+
return stats.ino ? `${stats.dev}:${stats.ino}` : undefined;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Deletes a folder and its parents while they are empty, stopping at the output directory
|
|
102
|
+
* and at links, which stay even when the folder they point to is empty.
|
|
103
|
+
*/
|
|
104
|
+
async function removeEmptyFolders(folder, outRoot) {
|
|
105
|
+
let current = folder;
|
|
106
|
+
while (current !== outRoot && isPathWithinDirectory(outRoot, current)) {
|
|
107
|
+
if ((await lstat(current)).isSymbolicLink() || (await readdir(current)).length > 0) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
await remove(current);
|
|
111
|
+
current = dirname(current);
|
|
112
|
+
}
|
|
113
|
+
}
|
package/dist/core/markdown.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import MarkdownIt from 'markdown-it';
|
|
2
2
|
import type { StatiConfig, TocEntry } from '../types/index.js';
|
|
3
|
+
import type { PageImages } from '../images/runtime.js';
|
|
3
4
|
/**
|
|
4
5
|
* Result of rendering markdown content.
|
|
5
6
|
*/
|
|
@@ -29,13 +30,21 @@ export declare function createMarkdownProcessor(config: StatiConfig): Promise<Ma
|
|
|
29
30
|
* @returns Array of TOC entries extracted from headings (levels 2-6)
|
|
30
31
|
*/
|
|
31
32
|
export declare function extractToc(content: string, md: MarkdownIt): TocEntry[];
|
|
33
|
+
/**
|
|
34
|
+
* Options for {@link renderMarkdown}.
|
|
35
|
+
*/
|
|
36
|
+
export interface RenderMarkdownOptions {
|
|
37
|
+
/** Image state of the page being rendered; without it images are left as authored */
|
|
38
|
+
images?: PageImages | undefined;
|
|
39
|
+
}
|
|
32
40
|
/**
|
|
33
41
|
* Renders markdown content to HTML with optional TOC extraction.
|
|
34
42
|
*
|
|
35
43
|
* @param content - The markdown content to render
|
|
36
44
|
* @param md - The configured MarkdownIt instance
|
|
37
45
|
* @param tocEnabled - Whether to extract TOC and inject heading anchors (default: true)
|
|
46
|
+
* @param options - Page-specific rendering state
|
|
38
47
|
* @returns Object containing rendered HTML and TOC entries
|
|
39
48
|
*/
|
|
40
|
-
export declare function renderMarkdown(content: string, md: MarkdownIt, tocEnabled?: boolean): MarkdownResult;
|
|
49
|
+
export declare function renderMarkdown(content: string, md: MarkdownIt, tocEnabled?: boolean, options?: RenderMarkdownOptions): MarkdownResult;
|
|
41
50
|
//# sourceMappingURL=markdown.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/core/markdown.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,aAAa,CAAC;AAKrC,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAI/D;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,GAAG,EAAE,QAAQ,EAAE,CAAC;CACjB;AAQD;;;GAGG;AACH,wBAAgB,2BAA2B,IAAI,IAAI,CAElD;AAmBD;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,
|
|
1
|
+
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/core/markdown.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,aAAa,CAAC;AAKrC,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAI/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,GAAG,EAAE,QAAQ,EAAE,CAAC;CACjB;AAQD;;;GAGG;AACH,wBAAgB,2BAA2B,IAAI,IAAI,CAElD;AAmBD;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAsDtF;AA6ED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,GAAG,QAAQ,EAAE,CAGtE;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,qFAAqF;IACrF,MAAM,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;CACjC;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,UAAU,EACd,UAAU,GAAE,OAAc,EAC1B,OAAO,GAAE,qBAA0B,GAClC,cAAc,CAkBhB"}
|
package/dist/core/markdown.js
CHANGED
|
@@ -4,6 +4,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { slugify } from './utils/index.js';
|
|
6
6
|
import { isDevelopment } from '../env.js';
|
|
7
|
+
import { installImageRule, selectEagerImage, IMAGES_ENV_KEY } from '../images/markdown-rule.js';
|
|
7
8
|
/**
|
|
8
9
|
* Cached markdown processor for dev mode to avoid re-creating on each rebuild.
|
|
9
10
|
* This significantly improves rebuild performance by avoiding repeated plugin loading.
|
|
@@ -48,6 +49,9 @@ export async function createMarkdownProcessor(config) {
|
|
|
48
49
|
linkify: true,
|
|
49
50
|
typographer: true,
|
|
50
51
|
});
|
|
52
|
+
// Image dimensions, lazy loading and responsive variants. Installed first so plugins and
|
|
53
|
+
// markdown.configure can still replace the image rule.
|
|
54
|
+
installImageRule(md);
|
|
51
55
|
// Apply plugins from array format
|
|
52
56
|
if (config.markdown?.plugins) {
|
|
53
57
|
for (const plugin of config.markdown.plugins) {
|
|
@@ -168,14 +172,20 @@ export function extractToc(content, md) {
|
|
|
168
172
|
* @param content - The markdown content to render
|
|
169
173
|
* @param md - The configured MarkdownIt instance
|
|
170
174
|
* @param tocEnabled - Whether to extract TOC and inject heading anchors (default: true)
|
|
175
|
+
* @param options - Page-specific rendering state
|
|
171
176
|
* @returns Object containing rendered HTML and TOC entries
|
|
172
177
|
*/
|
|
173
|
-
export function renderMarkdown(content, md, tocEnabled = true) {
|
|
178
|
+
export function renderMarkdown(content, md, tocEnabled = true, options = {}) {
|
|
179
|
+
// The same env object goes to parsing and rendering so renderer rules see the page's state
|
|
180
|
+
const env = options.images ? { [IMAGES_ENV_KEY]: options.images } : {};
|
|
174
181
|
// Parse content into tokens
|
|
175
|
-
const tokens = md.parse(content,
|
|
182
|
+
const tokens = md.parse(content, env);
|
|
176
183
|
// Extract TOC and inject anchor IDs
|
|
177
184
|
const toc = extractAndInjectAnchors(tokens, tocEnabled);
|
|
185
|
+
if (options.images) {
|
|
186
|
+
options.images.eagerToken = selectEagerImage(tokens);
|
|
187
|
+
}
|
|
178
188
|
// Render tokens to HTML
|
|
179
|
-
const html = md.renderer.render(tokens, md.options,
|
|
189
|
+
const html = md.renderer.render(tokens, md.options, env);
|
|
180
190
|
return { html, toc };
|
|
181
191
|
}
|