@stati/core 1.28.0 → 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 (63) hide show
  1. package/README.md +6 -1
  2. package/dist/core/build.d.ts.map +1 -1
  3. package/dist/core/build.js +122 -34
  4. package/dist/core/collections.d.ts +139 -0
  5. package/dist/core/collections.d.ts.map +1 -0
  6. package/dist/core/collections.js +342 -0
  7. package/dist/core/content.d.ts.map +1 -1
  8. package/dist/core/content.js +2 -1
  9. package/dist/core/dev.d.ts.map +1 -1
  10. package/dist/core/dev.js +11 -62
  11. package/dist/core/expand.d.ts +86 -0
  12. package/dist/core/expand.d.ts.map +1 -0
  13. package/dist/core/expand.js +484 -0
  14. package/dist/core/isg/builder.d.ts +15 -2
  15. package/dist/core/isg/builder.d.ts.map +1 -1
  16. package/dist/core/isg/builder.js +44 -6
  17. package/dist/core/isg/hash.d.ts +4 -2
  18. package/dist/core/isg/hash.d.ts.map +1 -1
  19. package/dist/core/isg/hash.js +16 -2
  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 +3 -1
  23. package/dist/core/isg/manifest.js +6 -0
  24. package/dist/core/isg/prune.d.ts +32 -0
  25. package/dist/core/isg/prune.d.ts.map +1 -0
  26. package/dist/core/isg/prune.js +113 -0
  27. package/dist/core/navigation.d.ts +4 -2
  28. package/dist/core/navigation.d.ts.map +1 -1
  29. package/dist/core/navigation.js +17 -2
  30. package/dist/core/templates.d.ts +3 -0
  31. package/dist/core/templates.d.ts.map +1 -1
  32. package/dist/core/templates.js +67 -127
  33. package/dist/core/utils/fs.utils.d.ts +8 -0
  34. package/dist/core/utils/fs.utils.d.ts.map +1 -1
  35. package/dist/core/utils/fs.utils.js +13 -1
  36. package/dist/core/utils/index.d.ts +3 -3
  37. package/dist/core/utils/index.d.ts.map +1 -1
  38. package/dist/core/utils/index.js +3 -3
  39. package/dist/core/utils/paths.utils.d.ts +15 -0
  40. package/dist/core/utils/paths.utils.d.ts.map +1 -1
  41. package/dist/core/utils/paths.utils.js +20 -0
  42. package/dist/core/utils/template-discovery.utils.d.ts +25 -1
  43. package/dist/core/utils/template-discovery.utils.d.ts.map +1 -1
  44. package/dist/core/utils/template-discovery.utils.js +61 -11
  45. package/dist/index.d.ts +1 -1
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/rss/generator.d.ts.map +1 -1
  48. package/dist/rss/generator.js +5 -2
  49. package/dist/search/generator.d.ts +9 -0
  50. package/dist/search/generator.d.ts.map +1 -1
  51. package/dist/search/generator.js +20 -0
  52. package/dist/search/index.d.ts +1 -1
  53. package/dist/search/index.d.ts.map +1 -1
  54. package/dist/search/index.js +1 -1
  55. package/dist/types/config.d.ts +55 -0
  56. package/dist/types/config.d.ts.map +1 -1
  57. package/dist/types/content.d.ts +129 -1
  58. package/dist/types/content.d.ts.map +1 -1
  59. package/dist/types/index.d.ts +2 -2
  60. package/dist/types/index.d.ts.map +1 -1
  61. package/dist/types/isg.d.ts +12 -0
  62. package/dist/types/isg.d.ts.map +1 -1
  63. package/package.json +1 -1
@@ -7,8 +7,9 @@
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
11
  export type { RenderedAssets } from './builder.js';
12
+ export { pruneOrphanedPages } from './prune.js';
12
13
  export { BuildLockManager, withBuildLock } from './build-lock.js';
13
14
  export { DevServerLockManager } from './dev-server-lock.js';
14
15
  export { CircularDependencyError, trackTemplateDependencies, resolveTemplatePath, clearTemplatePathCache, } from './deps.js';
@@ -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,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrF,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAGnD,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"}
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"}
@@ -9,7 +9,9 @@
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';
@@ -237,6 +237,12 @@ function validateCacheEntry(entry, path) {
237
237
  return false;
238
238
  }
239
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
+ }
240
246
  // Validate that deps and tags arrays contain strings
241
247
  if (!entryObj.deps.every((dep) => typeof dep === 'string')) {
242
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
+ }
@@ -2,8 +2,10 @@ import type { PageModel, NavNode } from '../types/index.js';
2
2
  /**
3
3
  * Builds a hierarchical navigation structure from pages.
4
4
  * Groups pages by directory structure and sorts them appropriately.
5
+ * Generated pages are left out unless their template sets `nav: true`, and so are the
6
+ * later pages of paginated listings.
5
7
  *
6
- * @param pages - Array of page models to build navigation from
8
+ * @param allPages - Array of page models to build navigation from
7
9
  * @returns Array of top-level navigation nodes
8
10
  *
9
11
  * @example
@@ -17,5 +19,5 @@ import type { PageModel, NavNode } from '../types/index.js';
17
19
  * // Results in hierarchical structure with sorted blog posts
18
20
  * ```
19
21
  */
20
- export declare function buildNavigation(pages: PageModel[]): NavNode[];
22
+ export declare function buildNavigation(allPages: PageModel[]): NavNode[];
21
23
  //# sourceMappingURL=navigation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../src/core/navigation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAE5D;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,OAAO,EAAE,CAyC7D"}
1
+ {"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../src/core/navigation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAe5D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,OAAO,EAAE,CA2ChE"}
@@ -1,9 +1,23 @@
1
1
  import { basename } from 'node:path';
2
+ import { isVirtualPage } from './utils/index.js';
3
+ /**
4
+ * Whether a page appears in navigation. Pages generated from a `[param].md` template appear
5
+ * only when the template sets `nav: true`; later pages of a paginated listing never do.
6
+ */
7
+ function isInNavigation(page) {
8
+ if (!isVirtualPage(page)) {
9
+ return true;
10
+ }
11
+ const firstPage = (page.pagination?.pageNumber ?? 1) === 1;
12
+ return page.generated !== undefined && firstPage && page.frontMatter.nav === true;
13
+ }
2
14
  /**
3
15
  * Builds a hierarchical navigation structure from pages.
4
16
  * Groups pages by directory structure and sorts them appropriately.
17
+ * Generated pages are left out unless their template sets `nav: true`, and so are the
18
+ * later pages of paginated listings.
5
19
  *
6
- * @param pages - Array of page models to build navigation from
20
+ * @param allPages - Array of page models to build navigation from
7
21
  * @returns Array of top-level navigation nodes
8
22
  *
9
23
  * @example
@@ -17,7 +31,8 @@ import { basename } from 'node:path';
17
31
  * // Results in hierarchical structure with sorted blog posts
18
32
  * ```
19
33
  */
20
- export function buildNavigation(pages) {
34
+ export function buildNavigation(allPages) {
35
+ const pages = allPages.filter(isInNavigation);
21
36
  // Group pages by their collection (directory)
22
37
  const collections = new Map();
23
38
  for (const page of pages) {
@@ -1,5 +1,6 @@
1
1
  import { Eta } from 'eta';
2
2
  import type { StatiConfig, PageModel, NavNode, TocEntry, Logger } from '../types/index.js';
3
+ import type { PageReads } from './collections.js';
3
4
  import type { PageImages } from '../images/runtime.js';
4
5
  import type { GeneratedOgImage } from '../types/index.js';
5
6
  /**
@@ -12,6 +13,8 @@ export interface RenderResult {
12
13
  templatesLoaded: number;
13
14
  /** Open Graph image generated for the page, if any */
14
15
  ogImage?: GeneratedOgImage | undefined;
16
+ /** Shared data the render read, which the cache entry records */
17
+ reads: PageReads;
15
18
  }
16
19
  /**
17
20
  * Page-specific state for {@link renderPage}.
@@ -1 +1 @@
1
- {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/core/templates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAG1B,OAAO,KAAK,EACV,WAAW,EACX,SAAS,EACT,OAAO,EAEP,QAAQ,EACR,MAAM,EACP,MAAM,mBAAmB,CAAC;AAkB3B,OAAO,KAAK,EAAE,UAAU,EAAqB,MAAM,sBAAsB,CAAC;AAC1E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,eAAe,EAAE,MAAM,CAAC;IACxB,sDAAsD;IACtD,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC;CACxC;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,MAAM,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;CACjC;AAgOD;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,GAAG,CA2B7D;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,EACnB,GAAG,EAAE,GAAG,EACR,UAAU,CAAC,EAAE,OAAO,EAAE,EACtB,QAAQ,CAAC,EAAE,SAAS,EAAE,EACtB,MAAM,CAAC,EAAE,OAAO,mBAAmB,EAAE,WAAW,EAChD,GAAG,CAAC,EAAE,QAAQ,EAAE,EAChB,MAAM,CAAC,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,gBAAgB,GACxB,OAAO,CAAC,YAAY,CAAC,CA0PvB"}
1
+ {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/core/templates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAG1B,OAAO,KAAK,EACV,WAAW,EACX,SAAS,EACT,OAAO,EAEP,QAAQ,EACR,MAAM,EACP,MAAM,mBAAmB,CAAC;AAuB3B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGlD,OAAO,KAAK,EAAE,UAAU,EAAqB,MAAM,sBAAsB,CAAC;AAC1E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,eAAe,EAAE,MAAM,CAAC;IACxB,sDAAsD;IACtD,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACvC,iEAAiE;IACjE,KAAK,EAAE,SAAS,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,MAAM,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;CACjC;AAuHD;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,GAAG,CA2B7D;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,EACnB,GAAG,EAAE,GAAG,EACR,UAAU,CAAC,EAAE,OAAO,EAAE,EACtB,QAAQ,CAAC,EAAE,SAAS,EAAE,EACtB,MAAM,CAAC,EAAE,OAAO,mBAAmB,EAAE,WAAW,EAChD,GAAG,CAAC,EAAE,QAAQ,EAAE,EAChB,MAAM,CAAC,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,gBAAgB,GACxB,OAAO,CAAC,YAAY,CAAC,CA8RvB"}
@@ -3,7 +3,8 @@ import { join, dirname, relative, basename, posix } from 'node:path';
3
3
  import glob from 'fast-glob';
4
4
  import { createFallbackLogger } from './utils/index.js';
5
5
  import { TEMPLATE_EXTENSION } from '../constants.js';
6
- import { getStatiVersion, isCollectionIndexPage, discoverLayout, getCollectionPathForPage, resolveSrcDir, createTemplateError, createValidatingPartialsProxy, createValidatingContextProxy, propValue, wrapPartialsAsCallable, createNavigationHelpers, } from './utils/index.js';
6
+ import { getStatiVersion, isCollectionIndexPage, discoverLayout, resolveSrcDir, createTemplateError, createValidatingPartialsProxy, createValidatingContextProxy, propValue, slugify, wrapPartialsAsCallable, createNavigationHelpers, } from './utils/index.js';
7
+ import { getCollections, resolveCollectionPath, createCollectionView, copyPaginationData, copyGeneratedData, } from './collections.js';
7
8
  import { isDevelopment, isProduction } from '../env.js';
8
9
  import { generateSEO, resolveGeneratedImagePlaceholders } from '../seo/index.js';
9
10
  /**
@@ -22,118 +23,28 @@ function renderPlainImage(src, options = {}) {
22
23
  return `<img ${attributes.join(' ')}>`;
23
24
  }
24
25
  /**
25
- * Groups pages by their tags for aggregation purposes.
26
- *
27
- * @param pages - Pages to group
28
- * @returns Object mapping tag names to arrays of pages
29
- */
30
- function groupPagesByTags(pages) {
31
- const pagesByTag = {};
32
- for (const page of pages) {
33
- const tags = page.frontMatter.tags;
34
- if (Array.isArray(tags)) {
35
- for (const tag of tags) {
36
- if (typeof tag === 'string') {
37
- if (!pagesByTag[tag]) {
38
- pagesByTag[tag] = [];
39
- }
40
- pagesByTag[tag].push(page);
41
- }
42
- }
43
- }
44
- }
45
- return pagesByTag;
46
- }
47
- /**
48
- * Sorts pages by publishedAt date (most recent first), falling back to alphabetical by title.
49
- *
50
- * @param pages - Pages to sort
51
- * @returns Sorted array of pages
52
- */
53
- function sortPagesByDate(pages) {
54
- return pages.sort((a, b) => {
55
- // Sort by publishedAt (newest first)
56
- if (a.publishedAt && b.publishedAt) {
57
- return b.publishedAt.getTime() - a.publishedAt.getTime();
58
- }
59
- if (a.publishedAt && !b.publishedAt)
60
- return -1;
61
- if (!a.publishedAt && b.publishedAt)
62
- return 1;
63
- // Fallback to title
64
- const titleA = a.frontMatter.title || basename(a.url) || 'Untitled';
65
- const titleB = b.frontMatter.title || basename(b.url) || 'Untitled';
66
- return titleA.localeCompare(titleB);
67
- });
68
- }
69
- /**
70
- * Builds collection aggregation data for index pages.
71
- * Provides all the data needed for index pages to list and organize their collection's content.
72
- *
73
- * @param currentPage - The current page being rendered (must be an index page)
74
- * @param allPages - All pages in the site
75
- * @returns Collection data for template rendering
76
- */
77
- function buildCollectionData(currentPage, allPages) {
78
- const collectionPath = currentPage.url === '/' ? '/' : currentPage.url;
79
- const collectionName = collectionPath === '/' ? 'Home' : basename(collectionPath);
80
- // Find all pages that belong to this collection
81
- const collectionPages = allPages.filter((page) => {
82
- if (page.url === currentPage.url) {
83
- return false; // Don't include the index page itself
84
- }
85
- if (collectionPath === '/') {
86
- // For root collection, include pages that are direct children of root
87
- const pageCollectionPath = getCollectionPathForPage(page.url);
88
- return pageCollectionPath === '/';
89
- }
90
- else {
91
- // For other collections, include pages that start with the collection path
92
- const pageUrlAsDir = collectionPath.endsWith('/') ? collectionPath : collectionPath + '/';
93
- return page.url.startsWith(pageUrlAsDir);
94
- }
95
- });
96
- // Find direct children (one level down)
97
- const children = collectionPages.filter((page) => {
98
- const relativePath = page.url.substring(collectionPath.length);
99
- const cleanPath = relativePath.startsWith('/') ? relativePath.substring(1) : relativePath;
100
- return !cleanPath.includes('/'); // No further slashes means direct child
101
- });
102
- // Sort pages by date for recent posts
103
- const recentPages = sortPagesByDate([...collectionPages]);
104
- // Group by tags
105
- const pagesByTag = groupPagesByTags(collectionPages);
106
- return {
107
- pages: collectionPages,
108
- children,
109
- recentPages,
110
- pagesByTag,
111
- metadata: {
112
- totalPages: collectionPages.length,
113
- hasChildren: children.length > 0,
114
- collectionPath,
115
- collectionName,
116
- },
117
- };
118
- }
119
- /**
120
- * Builds collection data for a child page (showing parent collection data).
121
- * Finds the parent collection and returns its data.
122
- *
123
- * @param currentPage - The current page being rendered
124
- * @param allPages - All pages in the site
125
- * @returns Collection data for the parent collection or undefined
26
+ * Wraps the navigation helpers for one render so that reading any of them sets `reads.nav`.
27
+ * Like the collection view, it uses accessors on a plain object, so spreading the template
28
+ * context does not count as a read. The helpers return the shared nodes unchanged.
126
29
  */
127
- function buildParentCollectionData(currentPage, allPages) {
128
- // Get the parent collection path
129
- const parentPath = getCollectionPathForPage(currentPage.url);
130
- // Find the index page for the parent collection
131
- const parentIndexPage = allPages.find((p) => p.url === parentPath);
132
- if (parentIndexPage) {
133
- // Build collection data for the parent
134
- return buildCollectionData(parentIndexPage, allPages);
30
+ function createNavView(helpers, reads) {
31
+ const view = {};
32
+ for (const [key, initial] of Object.entries(helpers)) {
33
+ let value = initial;
34
+ Object.defineProperty(view, key, {
35
+ enumerable: true,
36
+ configurable: true,
37
+ get() {
38
+ reads.nav = true;
39
+ return value;
40
+ },
41
+ set(next) {
42
+ reads.nav = true;
43
+ value = next;
44
+ },
45
+ });
135
46
  }
136
- return undefined;
47
+ return view;
137
48
  }
138
49
  /**
139
50
  * Discovers partials in the hierarchy for a given page path.
@@ -245,17 +156,19 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
245
156
  const partialPaths = await discoverPartials(relativePath, config);
246
157
  // Count templates: partials + layout (if found)
247
158
  const partialsCount = Object.keys(partialPaths).length;
248
- // Build collection data based on page type
159
+ // What this render reads from shared data, so the cache can rebuild the page when it changes
160
+ const reads = { collection: false, nav: false };
161
+ // The page's collection: its own on index pages, the one it belongs to on other pages, or
162
+ // the one its front matter names. Collections are computed once per page list.
249
163
  let collectionData;
250
164
  const isIndexPage = allPages && isCollectionIndexPage(page, allPages);
251
165
  if (allPages) {
252
- if (isIndexPage) {
253
- // For index pages, show their own collection data
254
- collectionData = buildCollectionData(page, allPages);
255
- }
256
- else {
257
- // For child pages, show parent collection data
258
- collectionData = buildParentCollectionData(page, allPages);
166
+ const collectionPath = resolveCollectionPath(page, allPages);
167
+ const data = collectionPath !== undefined
168
+ ? getCollections(allPages).byPath.get(collectionPath)
169
+ : undefined;
170
+ if (data) {
171
+ collectionData = createCollectionView(data, reads);
259
172
  }
260
173
  }
261
174
  // Discover the appropriate layout using hierarchical layout.eta convention
@@ -280,16 +193,28 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
280
193
  if (conflictingKeys.length > 0) {
281
194
  log.warning(`Frontmatter in "${page.sourcePath}" contains reserved keys that will be overwritten: ${conflictingKeys.join(', ')}`);
282
195
  }
196
+ // The current page's navigation node counts as a navigation read
197
+ const pageContext = { ...page.frontMatter, ...pageOverrides };
198
+ Object.defineProperty(pageContext, 'navNode', {
199
+ enumerable: true,
200
+ configurable: true,
201
+ get() {
202
+ reads.nav = true;
203
+ return currentNavNode;
204
+ },
205
+ });
283
206
  // Create base context for partial rendering
284
207
  const baseContext = {
285
208
  site: config.site,
286
- page: {
287
- ...page.frontMatter,
288
- ...pageOverrides,
289
- },
209
+ page: pageContext,
290
210
  content: body,
291
- nav: navHelpers, // Replace navigation with nav helpers
211
+ nav: createNavView(navHelpers, reads), // Replace navigation with nav helpers
292
212
  collection: collectionData, // Add collection data
213
+ // Set on paginated listings and on pages generated from [param].md templates. Placed before
214
+ // the filters, like slugify, so that an existing filter with the same name keeps working.
215
+ pagination: page.pagination ? copyPaginationData(page.pagination) : undefined,
216
+ generated: page.generated ? copyGeneratedData(page.generated) : undefined,
217
+ slugify,
293
218
  // Add custom filters to context
294
219
  ...(config.eta?.filters || {}),
295
220
  generator: {
@@ -382,10 +307,14 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
382
307
  // Render the Open Graph template with the layout's context, minus the page body, so the
383
308
  // generated image is known before the layout renders its SEO tags
384
309
  if (pageImages && ogState.pending) {
310
+ // Copy the page's property descriptors so that navNode stays a getter: spreading would
311
+ // read it and count every page with a card as reading the navigation
312
+ const ogPage = Object.defineProperties({}, Object.getOwnPropertyDescriptors(baseContext.page));
313
+ ogPage.content = '';
385
314
  const ogBaseContext = {
386
315
  ...baseContext,
387
316
  content: '',
388
- page: { ...baseContext.page, content: '' },
317
+ page: ogPage,
389
318
  og: pageImages.ogSize(),
390
319
  };
391
320
  const ogContext = createValidatingContextProxy({
@@ -405,13 +334,19 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
405
334
  try {
406
335
  if (!layoutPath) {
407
336
  log.warning('No layout template found, using fallback');
408
- return { html: createFallbackHtml(page, body), templatesLoaded, ogImage: ogState.image };
337
+ return {
338
+ html: createFallbackHtml(page, body),
339
+ templatesLoaded,
340
+ ogImage: ogState.image,
341
+ reads,
342
+ };
409
343
  }
410
344
  const html = await eta.renderAsync(layoutPath, context);
411
345
  return {
412
346
  html: resolveGeneratedImagePlaceholders(html, ogState.image),
413
347
  templatesLoaded,
414
348
  ogImage: ogState.image,
349
+ reads,
415
350
  };
416
351
  }
417
352
  catch (error) {
@@ -421,7 +356,12 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
421
356
  const templateError = createTemplateError(error instanceof Error ? error : new Error(String(error)), layoutPath || undefined);
422
357
  throw templateError;
423
358
  }
424
- return { html: createFallbackHtml(page, body), templatesLoaded, ogImage: ogState.image };
359
+ return {
360
+ html: createFallbackHtml(page, body),
361
+ templatesLoaded,
362
+ ogImage: ogState.image,
363
+ reads,
364
+ };
425
365
  }
426
366
  }
427
367
  function createFallbackHtml(page, body) {
@@ -24,6 +24,14 @@ export declare function remove(path: string): Promise<void>;
24
24
  * Gets file stats with consistent error handling.
25
25
  */
26
26
  export declare function stat(filePath: string): Promise<fse.Stats>;
27
+ /**
28
+ * Gets file stats without following a symbolic link at the path, with consistent error handling.
29
+ */
30
+ export declare function lstat(filePath: string): Promise<fse.Stats>;
31
+ /**
32
+ * Resolves a path to its real location, following symbolic links, with consistent error handling.
33
+ */
34
+ export declare function realpath(filePath: string): Promise<string>;
27
35
  /**
28
36
  * Reads directory contents with consistent error handling.
29
37
  */
@@ -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;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
+ {"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;AAqDvD;;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,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAEhE;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEhE;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,7 +1,7 @@
1
1
  import fse from 'fs-extra';
2
2
  import { readFileSync, statSync, readdirSync } from 'node:fs';
3
3
  import { join, relative } from 'node:path';
4
- const { readFile: fseReadFile, writeFile: fseWriteFile, pathExists: fsePathExists, ensureDir: fseEnsureDir, remove: fseRemove, stat: fseStat, readdir: fseReaddir, copyFile: fseCopyFile, } = fse;
4
+ const { readFile: fseReadFile, writeFile: fseWriteFile, pathExists: fsePathExists, ensureDir: fseEnsureDir, remove: fseRemove, stat: fseStat, lstat: fseLstat, realpath: fseRealpath, readdir: fseReaddir, copyFile: fseCopyFile, } = fse;
5
5
  /**
6
6
  * Wraps fs-extra operations with consistent error handling.
7
7
  */
@@ -65,6 +65,18 @@ export async function remove(path) {
65
65
  export async function stat(filePath) {
66
66
  return wrapFsOperation('get stats for', fseStat(filePath), filePath);
67
67
  }
68
+ /**
69
+ * Gets file stats without following a symbolic link at the path, with consistent error handling.
70
+ */
71
+ export async function lstat(filePath) {
72
+ return wrapFsOperation('get stats for', fseLstat(filePath), filePath);
73
+ }
74
+ /**
75
+ * Resolves a path to its real location, following symbolic links, with consistent error handling.
76
+ */
77
+ export async function realpath(filePath) {
78
+ return wrapFsOperation('resolve', fseRealpath(filePath), filePath);
79
+ }
68
80
  /**
69
81
  * Reads directory contents with consistent error handling.
70
82
  */
@@ -2,9 +2,9 @@
2
2
  * Core utilities index
3
3
  * @module core/utils
4
4
  */
5
- export { readFile, writeFile, pathExists, ensureDir, remove, copyFile, readdir, stat, readFileBuffer, readFileBufferSync, statSyncOrNull, listFilesRecursiveSync, } from './fs.utils.js';
6
- export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, normalizePathForComparison, isPathWithinDirectory, } from './paths.utils.js';
7
- export { discoverLayout, isCollectionIndexPage, getCollectionPathForPage, } from './template-discovery.utils.js';
5
+ export { readFile, writeFile, pathExists, ensureDir, remove, copyFile, readdir, stat, lstat, realpath, readFileBuffer, readFileBufferSync, statSyncOrNull, listFilesRecursiveSync, } from './fs.utils.js';
6
+ export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, normalizePathForComparison, isPathWithinDirectory, getPageOutputFile, } from './paths.utils.js';
7
+ export { discoverLayout, isCollectionIndexPage, isVirtualPage, getFirstPaginationPage, getCollectionPathForPage, } from './template-discovery.utils.js';
8
8
  export { propValue } from './template.utils.js';
9
9
  export { slugify } from './slugify.utils.js';
10
10
  export { trackTailwindClass, enableInventoryTracking, disableInventoryTracking, clearInventory, getInventory, getInventorySize, isTrackingEnabled, writeTailwindClassInventory, isTailwindUsed, resetTailwindDetection, loadPreviousInventory, } from './tailwind-inventory.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,EACJ,cAAc,EACd,kBAAkB,EAClB,cAAc,EACd,sBAAsB,GACvB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,0BAA0B,EAC1B,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,wBAAwB,GACzB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGhD,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAG7C,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,wBAAwB,EACxB,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,2BAA2B,EAC3B,cAAc,EACd,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AAG9E,OAAO,EACL,4BAA4B,EAC5B,qBAAqB,EACrB,2BAA2B,EAC3B,4BAA4B,GAC7B,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAGvE,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAC3F,YAAY,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAGpE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAG/F,OAAO,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAGxE,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,wBAAwB,EACxB,mBAAmB,GACpB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AACjF,YAAY,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAG7D,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGpE,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,4BAA4B,CAAC;AACpC,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAGrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAGzD,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,EAClB,qBAAqB,EACrB,iBAAiB,EACjB,WAAW,EACX,qBAAqB,GACtB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAG1E,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAG/E,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAG5E,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAGlD,OAAO,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAC7F,YAAY,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/utils/index.ts"],"names":[],"mappings":"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"}
@@ -3,11 +3,11 @@
3
3
  * @module core/utils
4
4
  */
5
5
  // File system utilities
6
- export { readFile, writeFile, pathExists, ensureDir, remove, copyFile, readdir, stat, readFileBuffer, readFileBufferSync, statSyncOrNull, listFilesRecursiveSync, } from './fs.utils.js';
6
+ export { readFile, writeFile, pathExists, ensureDir, remove, copyFile, readdir, stat, lstat, realpath, readFileBuffer, readFileBufferSync, statSyncOrNull, listFilesRecursiveSync, } from './fs.utils.js';
7
7
  // Path resolution utilities
8
- export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, normalizePathForComparison, isPathWithinDirectory, } from './paths.utils.js';
8
+ export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, normalizePathForComparison, isPathWithinDirectory, getPageOutputFile, } from './paths.utils.js';
9
9
  // Template discovery utilities
10
- export { discoverLayout, isCollectionIndexPage, getCollectionPathForPage, } from './template-discovery.utils.js';
10
+ export { discoverLayout, isCollectionIndexPage, isVirtualPage, getFirstPaginationPage, getCollectionPathForPage, } from './template-discovery.utils.js';
11
11
  // Template utilities
12
12
  export { propValue } from './template.utils.js';
13
13
  // Slugify utilities