@stati/core 1.27.1 → 1.27.2

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.
@@ -273,7 +273,7 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
273
273
  const shouldRebuild = options.force ||
274
274
  config.isg?.enabled === false ||
275
275
  !(await pathExists(outputPath)) ||
276
- (await shouldRebuildPage(page, existingEntry, config, buildTime, renderInputsHash));
276
+ (await shouldRebuildPage(page, existingEntry, config, pages, buildTime, renderInputsHash));
277
277
  const shouldRebuildTime = performance.now() - shouldRebuildStart;
278
278
  totalShouldRebuildTime += shouldRebuildTime;
279
279
  if (!shouldRebuild) {
@@ -357,10 +357,10 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
357
357
  if (!options.skipCacheUpdate) {
358
358
  const cacheEntryStart = performance.now();
359
359
  if (existingEntry) {
360
- manifest.entries[cacheKey] = await updateCacheEntry(existingEntry, page, config, buildTime, false, renderInputsHash);
360
+ manifest.entries[cacheKey] = await updateCacheEntry(existingEntry, page, config, pages, buildTime, false, renderInputsHash);
361
361
  }
362
362
  else {
363
- manifest.entries[cacheKey] = await createCacheEntry(page, config, buildTime, renderInputsHash);
363
+ manifest.entries[cacheKey] = await createCacheEntry(page, config, pages, buildTime, renderInputsHash);
364
364
  }
365
365
  totalCacheEntryTime += performance.now() - cacheEntryStart;
366
366
  }
@@ -393,11 +393,11 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
393
393
  for (const { cacheKey, page, existingEntry, renderInputsHash } of pendingCacheUpdates) {
394
394
  if (existingEntry) {
395
395
  // Fast update: reuse existing deps, just update hashes and timestamp
396
- manifest.entries[cacheKey] = await updateCacheEntry(existingEntry, page, config, buildTime, true, // fastUpdate = true in dev mode
396
+ manifest.entries[cacheKey] = await updateCacheEntry(existingEntry, page, config, pages, buildTime, true, // fastUpdate = true in dev mode
397
397
  renderInputsHash);
398
398
  }
399
399
  else {
400
- manifest.entries[cacheKey] = await createCacheEntry(page, config, buildTime, renderInputsHash);
400
+ manifest.entries[cacheKey] = await createCacheEntry(page, config, pages, buildTime, renderInputsHash);
401
401
  }
402
402
  }
403
403
  totalCacheEntryTime = performance.now() - cacheEntryStart;
@@ -7,34 +7,36 @@ import type { PageModel, CacheEntry, StatiConfig } from '../../types/index.js';
7
7
  * @param page - The page model to check
8
8
  * @param entry - Existing cache entry, or undefined if not cached
9
9
  * @param config - Stati configuration
10
+ * @param allPages - All pages in the site, used to resolve the same layout as rendering
10
11
  * @param now - Current date/time
11
12
  * @param renderInputsHash - Optional hash of asset URLs and injection settings
12
13
  * @returns True if the page should be rebuilt
13
14
  *
14
15
  * @example
15
16
  * ```typescript
16
- * const shouldRebuild = await shouldRebuildPage(page, cacheEntry, config, new Date());
17
+ * const shouldRebuild = await shouldRebuildPage(page, cacheEntry, config, pages, new Date());
17
18
  * if (shouldRebuild) {
18
19
  * console.log('Page needs rebuilding');
19
20
  * }
20
21
  * ```
21
22
  */
22
- export declare function shouldRebuildPage(page: PageModel, entry: CacheEntry | undefined, config: StatiConfig, now: Date, renderInputsHash?: string): Promise<boolean>;
23
+ export declare function shouldRebuildPage(page: PageModel, entry: CacheEntry | undefined, config: StatiConfig, allPages: PageModel[], now: Date, renderInputsHash?: string): Promise<boolean>;
23
24
  /**
24
25
  * Creates a new cache entry for a page after it has been rendered.
25
26
  *
26
27
  * @param page - The page model
27
28
  * @param config - Stati configuration
29
+ * @param allPages - All pages in the site, used to resolve the same layout as rendering
28
30
  * @param renderedAt - When the page was rendered
29
31
  * @param renderInputsHash - Optional hash of asset URLs and injection settings
30
32
  * @returns New cache entry
31
33
  *
32
34
  * @example
33
35
  * ```typescript
34
- * const entry = await createCacheEntry(page, config, new Date());
36
+ * const entry = await createCacheEntry(page, config, pages, new Date());
35
37
  * ```
36
38
  */
37
- export declare function createCacheEntry(page: PageModel, config: StatiConfig, renderedAt: Date, renderInputsHash?: string): Promise<CacheEntry>;
39
+ export declare function createCacheEntry(page: PageModel, config: StatiConfig, allPages: PageModel[], renderedAt: Date, renderInputsHash?: string): Promise<CacheEntry>;
38
40
  /**
39
41
  * Updates an existing cache entry with new information after rebuilding.
40
42
  * In dev mode, this is optimized to reuse the existing deps array since
@@ -43,6 +45,7 @@ export declare function createCacheEntry(page: PageModel, config: StatiConfig, r
43
45
  * @param entry - Existing cache entry
44
46
  * @param page - The page model
45
47
  * @param config - Stati configuration
48
+ * @param allPages - All pages in the site, used to resolve the same layout as rendering
46
49
  * @param renderedAt - When the page was rendered
47
50
  * @param fastUpdate - If true, skip expensive dep tracking (dev mode optimization)
48
51
  * @param renderInputsHash - Optional hash of asset URLs and injection settings
@@ -50,8 +53,8 @@ export declare function createCacheEntry(page: PageModel, config: StatiConfig, r
50
53
  *
51
54
  * @example
52
55
  * ```typescript
53
- * const updatedEntry = await updateCacheEntry(existingEntry, page, config, new Date());
56
+ * const updatedEntry = await updateCacheEntry(existingEntry, page, config, pages, new Date());
54
57
  * ```
55
58
  */
56
- export declare function updateCacheEntry(entry: CacheEntry, page: PageModel, config: StatiConfig, renderedAt: Date, fastUpdate?: boolean, renderInputsHash?: string): Promise<CacheEntry>;
59
+ export declare function updateCacheEntry(entry: CacheEntry, page: PageModel, config: StatiConfig, allPages: PageModel[], renderedAt: Date, fastUpdate?: boolean, renderInputsHash?: string): Promise<CacheEntry>;
57
60
  //# sourceMappingURL=builder.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../../src/core/isg/builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAqE/E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,SAAS,EACf,KAAK,EAAE,UAAU,GAAG,SAAS,EAC7B,MAAM,EAAE,WAAW,EACnB,GAAG,EAAE,IAAI,EACT,gBAAgB,CAAC,EAAE,MAAM,GACxB,OAAO,CAAC,OAAO,CAAC,CAoKlB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,IAAI,EAChB,gBAAgB,CAAC,EAAE,MAAM,GACxB,OAAO,CAAC,UAAU,CAAC,CAqFrB;AAkDD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,UAAU,EACjB,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,IAAI,EAChB,UAAU,UAAQ,EAClB,gBAAgB,CAAC,EAAE,MAAM,GACxB,OAAO,CAAC,UAAU,CAAC,CA4CrB"}
1
+ {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../../src/core/isg/builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAqE/E;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,SAAS,EACf,KAAK,EAAE,UAAU,GAAG,SAAS,EAC7B,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,SAAS,EAAE,EACrB,GAAG,EAAE,IAAI,EACT,gBAAgB,CAAC,EAAE,MAAM,GACxB,OAAO,CAAC,OAAO,CAAC,CAoKlB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,SAAS,EAAE,EACrB,UAAU,EAAE,IAAI,EAChB,gBAAgB,CAAC,EAAE,MAAM,GACxB,OAAO,CAAC,UAAU,CAAC,CAqFrB;AAkDD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,UAAU,EACjB,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,SAAS,EAAE,EACrB,UAAU,EAAE,IAAI,EAChB,UAAU,UAAQ,EAClB,gBAAgB,CAAC,EAAE,MAAM,GACxB,OAAO,CAAC,UAAU,CAAC,CAmDrB"}
@@ -1,5 +1,5 @@
1
1
  import { computeContentHash, computeFileHash, computeInputsHash } from './hash.js';
2
- import { trackTemplateDependencies } from './deps.js';
2
+ import { trackTemplateDependencies, resolveLayoutDependency } from './deps.js';
3
3
  import { computeEffectiveTTL, computeNextRebuildAt, isPageFrozen } from './ttl.js';
4
4
  import { validatePageISGOverrides, extractNumericOverride } from './validation.js';
5
5
  import { getFrontMatterDate } from '../utils/index.js';
@@ -65,19 +65,20 @@ function isValidCacheEntry(entry) {
65
65
  * @param page - The page model to check
66
66
  * @param entry - Existing cache entry, or undefined if not cached
67
67
  * @param config - Stati configuration
68
+ * @param allPages - All pages in the site, used to resolve the same layout as rendering
68
69
  * @param now - Current date/time
69
70
  * @param renderInputsHash - Optional hash of asset URLs and injection settings
70
71
  * @returns True if the page should be rebuilt
71
72
  *
72
73
  * @example
73
74
  * ```typescript
74
- * const shouldRebuild = await shouldRebuildPage(page, cacheEntry, config, new Date());
75
+ * const shouldRebuild = await shouldRebuildPage(page, cacheEntry, config, pages, new Date());
75
76
  * if (shouldRebuild) {
76
77
  * console.log('Page needs rebuilding');
77
78
  * }
78
79
  * ```
79
80
  */
80
- export async function shouldRebuildPage(page, entry, config, now, renderInputsHash) {
81
+ export async function shouldRebuildPage(page, entry, config, allPages, now, renderInputsHash) {
81
82
  if (config.isg?.enabled === false) {
82
83
  return true;
83
84
  }
@@ -96,7 +97,7 @@ export async function shouldRebuildPage(page, entry, config, now, renderInputsHa
96
97
  // Track dependencies with error handling
97
98
  let deps;
98
99
  try {
99
- deps = await trackTemplateDependencies(page, config);
100
+ deps = await trackTemplateDependencies(page, config, allPages);
100
101
  }
101
102
  catch (error) {
102
103
  if (error instanceof Error && error.name === 'CircularDependencyError') {
@@ -211,16 +212,17 @@ export async function shouldRebuildPage(page, entry, config, now, renderInputsHa
211
212
  *
212
213
  * @param page - The page model
213
214
  * @param config - Stati configuration
215
+ * @param allPages - All pages in the site, used to resolve the same layout as rendering
214
216
  * @param renderedAt - When the page was rendered
215
217
  * @param renderInputsHash - Optional hash of asset URLs and injection settings
216
218
  * @returns New cache entry
217
219
  *
218
220
  * @example
219
221
  * ```typescript
220
- * const entry = await createCacheEntry(page, config, new Date());
222
+ * const entry = await createCacheEntry(page, config, pages, new Date());
221
223
  * ```
222
224
  */
223
- export async function createCacheEntry(page, config, renderedAt, renderInputsHash) {
225
+ export async function createCacheEntry(page, config, allPages, renderedAt, renderInputsHash) {
224
226
  const timings = {};
225
227
  let start = performance.now();
226
228
  // Validate page-level ISG overrides first
@@ -232,7 +234,7 @@ export async function createCacheEntry(page, config, renderedAt, renderInputsHas
232
234
  timings.contentHash = performance.now() - start;
233
235
  // Track all template dependencies
234
236
  start = performance.now();
235
- const deps = await trackTemplateDependencies(page, config);
237
+ const deps = await trackTemplateDependencies(page, config, allPages);
236
238
  timings.trackDeps = performance.now() - start;
237
239
  // Compute hashes for all dependencies
238
240
  start = performance.now();
@@ -339,6 +341,7 @@ async function hasTemplateStructureChanged(entry) {
339
341
  * @param entry - Existing cache entry
340
342
  * @param page - The page model
341
343
  * @param config - Stati configuration
344
+ * @param allPages - All pages in the site, used to resolve the same layout as rendering
342
345
  * @param renderedAt - When the page was rendered
343
346
  * @param fastUpdate - If true, skip expensive dep tracking (dev mode optimization)
344
347
  * @param renderInputsHash - Optional hash of asset URLs and injection settings
@@ -346,19 +349,24 @@ async function hasTemplateStructureChanged(entry) {
346
349
  *
347
350
  * @example
348
351
  * ```typescript
349
- * const updatedEntry = await updateCacheEntry(existingEntry, page, config, new Date());
352
+ * const updatedEntry = await updateCacheEntry(existingEntry, page, config, pages, new Date());
350
353
  * ```
351
354
  */
352
- export async function updateCacheEntry(entry, page, config, renderedAt, fastUpdate = false, renderInputsHash) {
355
+ export async function updateCacheEntry(entry, page, config, allPages, renderedAt, fastUpdate = false, renderInputsHash) {
353
356
  // In fast update mode (dev), reuse existing deps to avoid expensive tracking
354
357
  if (fastUpdate && entry.deps.length > 0) {
355
358
  // Detect if template structure has changed (new includes/extends added)
356
359
  // by checking if the layout file has been modified or if template content
357
360
  // contains different dependency patterns
358
361
  const structureChanged = await hasTemplateStructureChanged(entry);
359
- if (structureChanged) {
360
- // Template structure changed - fall through to full dependency tracking
361
- // This ensures we pick up new includes/extends
362
+ // The page can also switch layouts without any template file changing, for example
363
+ // when a collection gains its first child page or a front matter layout is edited.
364
+ // deps[0] is always the layout, so compare it against the layout used now.
365
+ const layoutChanged = !structureChanged &&
366
+ (await resolveLayoutDependency(page, config, allPages)) !== entry.deps[0];
367
+ if (structureChanged || layoutChanged) {
368
+ // Template structure or layout changed - fall through to full dependency tracking
369
+ // This ensures we pick up new includes/extends and the newly selected layout
362
370
  }
363
371
  else {
364
372
  // Structure unchanged - safe to reuse existing deps
@@ -381,7 +389,7 @@ export async function updateCacheEntry(entry, page, config, renderedAt, fastUpda
381
389
  }
382
390
  }
383
391
  // Full update - create new entry and preserve original publishedAt
384
- const newEntry = await createCacheEntry(page, config, renderedAt, renderInputsHash);
392
+ const newEntry = await createCacheEntry(page, config, allPages, renderedAt, renderInputsHash);
385
393
  // Preserve original publishedAt if no new one is specified
386
394
  if (!newEntry.publishedAt && entry.publishedAt) {
387
395
  newEntry.publishedAt = entry.publishedAt;
@@ -22,13 +22,14 @@ export declare class CircularDependencyError extends Error {
22
22
  *
23
23
  * @param page - The page model to track dependencies for
24
24
  * @param config - Stati configuration
25
+ * @param allPages - All pages in the site, used to detect collection index pages the same way rendering does
25
26
  * @returns Array of absolute paths to actually-used template files (POSIX format)
26
27
  * @throws {CircularDependencyError} When circular dependencies are detected in templates
27
28
  *
28
29
  * @example
29
30
  * ```typescript
30
31
  * try {
31
- * const deps = await trackTemplateDependencies(page, config);
32
+ * const deps = await trackTemplateDependencies(page, config, allPages);
32
33
  * console.log(`Page depends on ${deps.length} template files`);
33
34
  * } catch (error) {
34
35
  * if (error instanceof CircularDependencyError) {
@@ -37,7 +38,18 @@ export declare class CircularDependencyError extends Error {
37
38
  * }
38
39
  * ```
39
40
  */
40
- export declare function trackTemplateDependencies(page: PageModel, config: StatiConfig): Promise<string[]>;
41
+ export declare function trackTemplateDependencies(page: PageModel, config: StatiConfig, allPages: PageModel[]): Promise<string[]>;
42
+ /**
43
+ * Resolves the layout a page renders with, in the form stored as the first entry of its
44
+ * cache dependencies. Uses the same lookup as rendering, so callers can tell whether the
45
+ * page's layout has changed since its dependencies were recorded.
46
+ *
47
+ * @param page - The page model to resolve the layout for
48
+ * @param config - Stati configuration
49
+ * @param allPages - All pages in the site, used to detect collection index pages the same way rendering does
50
+ * @returns Absolute path to the layout file (POSIX format), or null if no layout applies
51
+ */
52
+ export declare function resolveLayoutDependency(page: PageModel, config: StatiConfig, allPages: PageModel[]): Promise<string | null>;
41
53
  /**
42
54
  * Resolves a template name to its file path.
43
55
  * 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;AAenE;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAG7C;AAED;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;aAE9B,eAAe,EAAE,MAAM,EAAE;gBAAzB,eAAe,EAAE,MAAM,EAAE,EACzC,OAAO,EAAE,MAAM;CAKlB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,EAAE,CAAC,CA+CnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CASxB"}
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;AAenE;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAG7C;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,CAuCnB;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;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CASxB"}
@@ -42,13 +42,14 @@ export class CircularDependencyError extends Error {
42
42
  *
43
43
  * @param page - The page model to track dependencies for
44
44
  * @param config - Stati configuration
45
+ * @param allPages - All pages in the site, used to detect collection index pages the same way rendering does
45
46
  * @returns Array of absolute paths to actually-used template files (POSIX format)
46
47
  * @throws {CircularDependencyError} When circular dependencies are detected in templates
47
48
  *
48
49
  * @example
49
50
  * ```typescript
50
51
  * try {
51
- * const deps = await trackTemplateDependencies(page, config);
52
+ * const deps = await trackTemplateDependencies(page, config, allPages);
52
53
  * console.log(`Page depends on ${deps.length} template files`);
53
54
  * } catch (error) {
54
55
  * if (error instanceof CircularDependencyError) {
@@ -57,7 +58,7 @@ export class CircularDependencyError extends Error {
57
58
  * }
58
59
  * ```
59
60
  */
60
- export async function trackTemplateDependencies(page, config) {
61
+ export async function trackTemplateDependencies(page, config, allPages) {
61
62
  // Early return if required config values are missing
62
63
  if (!config.srcDir) {
63
64
  console.warn('Config srcDir is missing, cannot track template dependencies');
@@ -65,16 +66,13 @@ export async function trackTemplateDependencies(page, config) {
65
66
  }
66
67
  const deps = [];
67
68
  const srcDir = resolveSrcDir(config);
68
- const relativePath = relative(srcDir, page.sourcePath);
69
69
  // Track dependencies with circular detection
70
70
  // The visited set will contain all templates that are actually used (not just accessible)
71
71
  const visited = new Set();
72
72
  const currentPath = new Set();
73
73
  // 1. Find the layout file that will be used for this page
74
- const layoutPath = await discoverLayout(relativePath, config, page.frontMatter.layout, isCollectionIndexPage(page));
75
- if (layoutPath) {
76
- // Normalize to POSIX format for consistent manifest output across platforms
77
- const absoluteLayoutPath = posix.join(srcDir.replace(/\\/g, '/'), layoutPath);
74
+ const absoluteLayoutPath = await resolveLayoutDependency(page, config, allPages);
75
+ if (absoluteLayoutPath) {
78
76
  deps.push(absoluteLayoutPath);
79
77
  // Recursively traverse the template dependency tree
80
78
  // This populates 'visited' with all actually-used templates (not just accessible ones)
@@ -93,6 +91,25 @@ export async function trackTemplateDependencies(page, config) {
93
91
  }
94
92
  return deps;
95
93
  }
94
+ /**
95
+ * Resolves the layout a page renders with, in the form stored as the first entry of its
96
+ * cache dependencies. Uses the same lookup as rendering, so callers can tell whether the
97
+ * page's layout has changed since its dependencies were recorded.
98
+ *
99
+ * @param page - The page model to resolve the layout for
100
+ * @param config - Stati configuration
101
+ * @param allPages - All pages in the site, used to detect collection index pages the same way rendering does
102
+ * @returns Absolute path to the layout file (POSIX format), or null if no layout applies
103
+ */
104
+ export async function resolveLayoutDependency(page, config, allPages) {
105
+ if (!config.srcDir) {
106
+ return null;
107
+ }
108
+ const srcDir = resolveSrcDir(config);
109
+ const layoutPath = await discoverLayout(relative(srcDir, page.sourcePath), config, page.frontMatter.layout, isCollectionIndexPage(page, allPages));
110
+ // Normalize to POSIX format for consistent manifest output across platforms
111
+ return layoutPath ? posix.join(srcDir.replace(/\\/g, '/'), layoutPath) : null;
112
+ }
96
113
  /**
97
114
  * Resolves a template name to its file path.
98
115
  * Used for explicit layout specifications in front matter.
@@ -1,17 +1,19 @@
1
1
  import type { StatiConfig, PageModel } from '../../types/index.js';
2
- /**
3
- * Shared template discovery utilities.
4
- * Extracted from templates.ts and isg/deps.ts to eliminate duplication.
5
- */
6
2
  /**
7
3
  * Determines if the given page is a collection index page.
8
4
  * A collection index page is one whose URL matches a directory path that contains other pages.
9
5
  *
6
+ * Rendering and ISG dependency tracking must both call this with the same page list,
7
+ * otherwise they can pick different layouts for the same page.
8
+ *
9
+ * Page URLs are read once per `allPages` array, at the first call; see
10
+ * `directoryPrefixCache` for when changes to the pages are picked up.
11
+ *
10
12
  * @param page - The page to check
11
- * @param allPages - All pages in the site (optional for simplified check)
13
+ * @param allPages - All pages in the site (treated as unchanging after the first call)
12
14
  * @returns True if the page is a collection index page
13
15
  */
14
- export declare function isCollectionIndexPage(page: PageModel, allPages?: PageModel[]): boolean;
16
+ export declare function isCollectionIndexPage(page: PageModel, allPages: PageModel[]): boolean;
15
17
  /**
16
18
  * Discovers layout files by searching up the directory hierarchy.
17
19
  * Supports both explicit layout specification and automatic discovery.
@@ -1 +1 @@
1
- {"version":3,"file":"template-discovery.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/template-discovery.utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAInE;;;GAGG;AAEH;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,CAAC,EAAE,SAAS,EAAE,GAAG,OAAO,CAkBtF;AAED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,WAAW,EACnB,cAAc,CAAC,EAAE,MAAM,EACvB,WAAW,CAAC,EAAE,OAAO,GACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAsDxB;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAkBhE"}
1
+ {"version":3,"file":"template-discovery.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/template-discovery.utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAoDnE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,OAAO,CAUrF;AAED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,WAAW,EACnB,cAAc,CAAC,EAAE,MAAM,EACvB,WAAW,CAAC,EAAE,OAAO,GACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAsDxB;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAkBhE"}
@@ -3,15 +3,51 @@ import { pathExists } from './fs.utils.js';
3
3
  import { LAYOUT_TEMPLATE } from '../../constants.js';
4
4
  import { resolveSrcDir } from './paths.utils.js';
5
5
  /**
6
- * Shared template discovery utilities.
7
- * Extracted from templates.ts and isg/deps.ts to eliminate duplication.
6
+ * Directory prefix index per page list, so a build computes it once instead of
7
+ * scanning every page for every page.
8
+ *
9
+ * The index is a snapshot of the page URLs taken at the first lookup for a given array,
10
+ * and every later lookup for that array reuses it. In a build the first lookup happens
11
+ * in the render loop, after the `beforeAll` hook, so URL changes made there are included.
12
+ * Changes made to the same array or its pages after that are not seen; a fresh build
13
+ * loads a new array and gets a new snapshot. Taking one snapshot keeps rendering and
14
+ * ISG dependency tracking in agreement for the whole build.
8
15
  */
16
+ const directoryPrefixCache = new WeakMap();
17
+ function getDirectoryPrefixIndex(allPages) {
18
+ const cached = directoryPrefixCache.get(allPages);
19
+ if (cached) {
20
+ return cached;
21
+ }
22
+ const index = new Map();
23
+ for (const { url } of allPages) {
24
+ // Every prefix ending at a '/' is a directory this URL lives in (or is, for trailing-slash URLs)
25
+ for (let i = url.indexOf('/'); i !== -1; i = url.indexOf('/', i + 1)) {
26
+ const prefix = url.slice(0, i + 1);
27
+ const existing = index.get(prefix);
28
+ if (existing === undefined) {
29
+ index.set(prefix, url);
30
+ }
31
+ else if (existing !== null && existing !== url) {
32
+ index.set(prefix, null);
33
+ }
34
+ }
35
+ }
36
+ directoryPrefixCache.set(allPages, index);
37
+ return index;
38
+ }
9
39
  /**
10
40
  * Determines if the given page is a collection index page.
11
41
  * A collection index page is one whose URL matches a directory path that contains other pages.
12
42
  *
43
+ * Rendering and ISG dependency tracking must both call this with the same page list,
44
+ * otherwise they can pick different layouts for the same page.
45
+ *
46
+ * Page URLs are read once per `allPages` array, at the first call; see
47
+ * `directoryPrefixCache` for when changes to the pages are picked up.
48
+ *
13
49
  * @param page - The page to check
14
- * @param allPages - All pages in the site (optional for simplified check)
50
+ * @param allPages - All pages in the site (treated as unchanging after the first call)
15
51
  * @returns True if the page is a collection index page
16
52
  */
17
53
  export function isCollectionIndexPage(page, allPages) {
@@ -19,15 +55,10 @@ export function isCollectionIndexPage(page, allPages) {
19
55
  if (page.url === '/') {
20
56
  return true;
21
57
  }
22
- // If we have all pages, do a proper check
23
- if (allPages) {
24
- // Check if this page's URL is a directory path that contains other pages
25
- const pageUrlAsDir = page.url.endsWith('/') ? page.url : page.url + '/';
26
- return allPages.some((otherPage) => otherPage.url !== page.url && otherPage.url.startsWith(pageUrlAsDir));
27
- }
28
- // Simplified version when we don't have all pages
29
- // Assume any page ending in /index or at root is a collection page
30
- return page.url.endsWith('/index') || page.slug === 'index';
58
+ // The page is an index when a different page's URL lives under this page's URL as a directory
59
+ const pageUrlAsDir = page.url.endsWith('/') ? page.url : page.url + '/';
60
+ const containingUrl = getDirectoryPrefixIndex(allPages).get(pageUrlAsDir);
61
+ return containingUrl === null || (containingUrl !== undefined && containingUrl !== page.url);
31
62
  }
32
63
  /**
33
64
  * Discovers layout files by searching up the directory hierarchy.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stati/core",
3
- "version": "1.27.1",
3
+ "version": "1.27.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",