@stati/core 1.1.0 → 1.3.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 (39) hide show
  1. package/README.md +19 -7
  2. package/dist/config/loader.d.ts.map +1 -1
  3. package/dist/config/loader.js +24 -2
  4. package/dist/core/build.d.ts +21 -15
  5. package/dist/core/build.d.ts.map +1 -1
  6. package/dist/core/build.js +141 -42
  7. package/dist/core/dev.d.ts.map +1 -1
  8. package/dist/core/dev.js +84 -18
  9. package/dist/core/invalidate.d.ts +67 -1
  10. package/dist/core/invalidate.d.ts.map +1 -1
  11. package/dist/core/invalidate.js +321 -4
  12. package/dist/core/isg/build-lock.d.ts +116 -0
  13. package/dist/core/isg/build-lock.d.ts.map +1 -0
  14. package/dist/core/isg/build-lock.js +245 -0
  15. package/dist/core/isg/builder.d.ts +51 -0
  16. package/dist/core/isg/builder.d.ts.map +1 -0
  17. package/dist/core/isg/builder.js +321 -0
  18. package/dist/core/isg/deps.d.ts +63 -0
  19. package/dist/core/isg/deps.d.ts.map +1 -0
  20. package/dist/core/isg/deps.js +332 -0
  21. package/dist/core/isg/hash.d.ts +48 -0
  22. package/dist/core/isg/hash.d.ts.map +1 -0
  23. package/dist/core/isg/hash.js +82 -0
  24. package/dist/core/isg/manifest.d.ts +47 -0
  25. package/dist/core/isg/manifest.d.ts.map +1 -0
  26. package/dist/core/isg/manifest.js +233 -0
  27. package/dist/core/isg/ttl.d.ts +101 -0
  28. package/dist/core/isg/ttl.d.ts.map +1 -0
  29. package/dist/core/isg/ttl.js +222 -0
  30. package/dist/core/isg/validation.d.ts +71 -0
  31. package/dist/core/isg/validation.d.ts.map +1 -0
  32. package/dist/core/isg/validation.js +226 -0
  33. package/dist/core/templates.d.ts.map +1 -1
  34. package/dist/core/templates.js +3 -2
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/types.d.ts +110 -20
  38. package/dist/types.d.ts.map +1 -1
  39. package/package.json +1 -1
@@ -0,0 +1,245 @@
1
+ import fse from 'fs-extra';
2
+ const { writeFile, readFile, pathExists, remove, ensureDir } = fse;
3
+ import { join, dirname } from 'path';
4
+ import { hostname } from 'os';
5
+ /**
6
+ * Manages build process locking to prevent concurrent Stati builds from corrupting cache.
7
+ * Uses a simple file-based locking mechanism with process ID tracking.
8
+ */
9
+ export class BuildLockManager {
10
+ lockPath;
11
+ isLocked = false;
12
+ constructor(cacheDir) {
13
+ this.lockPath = join(cacheDir, '.build-lock');
14
+ }
15
+ /**
16
+ * Attempts to acquire a build lock.
17
+ * Throws an error if another build process is already running.
18
+ *
19
+ * @param options - Lock acquisition options
20
+ * @param options.force - Force acquire lock even if another process holds it
21
+ * @param options.timeout - Maximum time to wait for lock in milliseconds
22
+ * @throws {Error} When lock cannot be acquired
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * const lockManager = new BuildLockManager('.stati');
27
+ * try {
28
+ * await lockManager.acquireLock();
29
+ * // Proceed with build
30
+ * } finally {
31
+ * await lockManager.releaseLock();
32
+ * }
33
+ * ```
34
+ */
35
+ async acquireLock(options = {}) {
36
+ const { force = false, timeout = 30000 } = options;
37
+ const startTime = Date.now();
38
+ while (Date.now() - startTime < timeout) {
39
+ try {
40
+ // Check if lock file exists
41
+ if (await pathExists(this.lockPath)) {
42
+ const existingLock = await this.readLockFile();
43
+ if (existingLock && !force) {
44
+ // Check if the process is still running
45
+ if (await this.isProcessRunning(existingLock.pid)) {
46
+ // Wait a bit and try again
47
+ await this.sleep(1000);
48
+ continue;
49
+ }
50
+ else {
51
+ // Process is dead, remove stale lock
52
+ console.warn(`Removing stale build lock (PID ${existingLock.pid} no longer running)`);
53
+ await this.forceRemoveLock();
54
+ }
55
+ }
56
+ else if (force) {
57
+ console.warn('Force acquiring build lock, removing existing lock');
58
+ await this.forceRemoveLock();
59
+ }
60
+ }
61
+ // Try to create the lock
62
+ await this.createLockFile();
63
+ this.isLocked = true;
64
+ return;
65
+ }
66
+ catch (error) {
67
+ const nodeError = error;
68
+ if (nodeError.code === 'EEXIST') {
69
+ // Another process created the lock between our check and creation
70
+ await this.sleep(1000);
71
+ continue;
72
+ }
73
+ throw new Error(`Failed to acquire build lock: ${error instanceof Error ? error.message : String(error)}`);
74
+ }
75
+ }
76
+ throw new Error(`Build lock acquisition timed out after ${timeout}ms. ` +
77
+ `Another Stati build process may be running. Use --force to override.`);
78
+ }
79
+ /**
80
+ * Releases the build lock if this process owns it.
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * await lockManager.releaseLock();
85
+ * ```
86
+ */
87
+ async releaseLock() {
88
+ if (!this.isLocked) {
89
+ return;
90
+ }
91
+ try {
92
+ // Verify we still own the lock before removing it
93
+ const currentLock = await this.readLockFile();
94
+ if (currentLock && currentLock.pid === process.pid) {
95
+ await remove(this.lockPath);
96
+ }
97
+ }
98
+ catch (error) {
99
+ // Don't throw on release errors, just warn
100
+ console.warn(`Warning: Failed to release build lock: ${error instanceof Error ? error.message : String(error)}`);
101
+ }
102
+ finally {
103
+ this.isLocked = false;
104
+ }
105
+ }
106
+ /**
107
+ * Checks if a build lock is currently held by any process.
108
+ *
109
+ * @returns True if a lock exists and the owning process is running
110
+ *
111
+ * @example
112
+ * ```typescript
113
+ * if (await lockManager.isLocked()) {
114
+ * console.log('Another build is in progress');
115
+ * }
116
+ * ```
117
+ */
118
+ async isLockHeld() {
119
+ try {
120
+ if (!(await pathExists(this.lockPath))) {
121
+ return false;
122
+ }
123
+ const lock = await this.readLockFile();
124
+ if (!lock) {
125
+ return false;
126
+ }
127
+ return await this.isProcessRunning(lock.pid);
128
+ }
129
+ catch {
130
+ return false;
131
+ }
132
+ }
133
+ /**
134
+ * Gets information about the current lock holder.
135
+ *
136
+ * @returns Lock information or null if no lock exists
137
+ */
138
+ async getLockInfo() {
139
+ try {
140
+ if (!(await pathExists(this.lockPath))) {
141
+ return null;
142
+ }
143
+ return await this.readLockFile();
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ }
149
+ /**
150
+ * Force removes the lock file without checking ownership.
151
+ * Should only be used in error recovery scenarios.
152
+ */
153
+ async forceRemoveLock() {
154
+ try {
155
+ await remove(this.lockPath);
156
+ }
157
+ catch {
158
+ // Ignore errors when force removing
159
+ }
160
+ }
161
+ /**
162
+ * Creates a new lock file with current process information.
163
+ */
164
+ async createLockFile() {
165
+ const lockInfo = {
166
+ pid: process.pid,
167
+ timestamp: new Date().toISOString(),
168
+ hostname: this.getHostname(),
169
+ };
170
+ // Ensure the cache directory exists before creating the lock file
171
+ await ensureDir(dirname(this.lockPath));
172
+ // Use 'wx' flag to create file exclusively (fails if exists)
173
+ await writeFile(this.lockPath, JSON.stringify(lockInfo, null, 2), { flag: 'wx' });
174
+ }
175
+ /**
176
+ * Reads and parses the lock file.
177
+ */
178
+ async readLockFile() {
179
+ try {
180
+ const content = await readFile(this.lockPath, 'utf-8');
181
+ return JSON.parse(content);
182
+ }
183
+ catch {
184
+ return null;
185
+ }
186
+ }
187
+ /**
188
+ * Checks if a process with the given PID is currently running.
189
+ */
190
+ async isProcessRunning(pid) {
191
+ try {
192
+ // On Unix systems, sending signal 0 checks if process exists without affecting it
193
+ process.kill(pid, 0);
194
+ return true;
195
+ }
196
+ catch (error) {
197
+ const nodeError = error;
198
+ // ESRCH means process doesn't exist
199
+ return nodeError.code !== 'ESRCH';
200
+ }
201
+ }
202
+ /**
203
+ * Gets the hostname for lock identification.
204
+ */
205
+ getHostname() {
206
+ try {
207
+ return hostname();
208
+ }
209
+ catch {
210
+ return 'unknown';
211
+ }
212
+ }
213
+ /**
214
+ * Simple sleep utility for polling delays.
215
+ */
216
+ sleep(ms) {
217
+ return new Promise((resolve) => global.setTimeout(resolve, ms));
218
+ }
219
+ }
220
+ /**
221
+ * Convenience function to safely execute a build with automatic lock management.
222
+ *
223
+ * @param cacheDir - Path to the cache directory
224
+ * @param buildFn - Function to execute while holding the lock
225
+ * @param options - Lock options
226
+ * @returns Result of the build function
227
+ *
228
+ * @example
229
+ * ```typescript
230
+ * const result = await withBuildLock('.stati', async () => {
231
+ * // Your build logic here
232
+ * return await performBuild();
233
+ * });
234
+ * ```
235
+ */
236
+ export async function withBuildLock(cacheDir, buildFn, options = {}) {
237
+ const lockManager = new BuildLockManager(cacheDir);
238
+ try {
239
+ await lockManager.acquireLock(options);
240
+ return await buildFn();
241
+ }
242
+ finally {
243
+ await lockManager.releaseLock();
244
+ }
245
+ }
@@ -0,0 +1,51 @@
1
+ import type { PageModel, CacheEntry, StatiConfig } from '../../types.js';
2
+ /**
3
+ * Determines if a page should be rebuilt based on ISG logic.
4
+ * Checks content changes, dependency changes, TTL expiration, and freeze status.
5
+ * Handles edge cases like missing dates, corrupted cache entries, and dependency errors.
6
+ *
7
+ * @param page - The page model to check
8
+ * @param entry - Existing cache entry, or undefined if not cached
9
+ * @param config - Stati configuration
10
+ * @param now - Current date/time
11
+ * @returns True if the page should be rebuilt
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const shouldRebuild = await shouldRebuildPage(page, cacheEntry, config, new Date());
16
+ * if (shouldRebuild) {
17
+ * console.log('Page needs rebuilding');
18
+ * }
19
+ * ```
20
+ */
21
+ export declare function shouldRebuildPage(page: PageModel, entry: CacheEntry | undefined, config: StatiConfig, now: Date): Promise<boolean>;
22
+ /**
23
+ * Creates a new cache entry for a page after it has been rendered.
24
+ *
25
+ * @param page - The page model
26
+ * @param config - Stati configuration
27
+ * @param renderedAt - When the page was rendered
28
+ * @returns New cache entry
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * const entry = await createCacheEntry(page, config, new Date());
33
+ * ```
34
+ */
35
+ export declare function createCacheEntry(page: PageModel, config: StatiConfig, renderedAt: Date): Promise<CacheEntry>;
36
+ /**
37
+ * Updates an existing cache entry with new information after rebuilding.
38
+ *
39
+ * @param entry - Existing cache entry
40
+ * @param page - The page model
41
+ * @param config - Stati configuration
42
+ * @param renderedAt - When the page was rendered
43
+ * @returns Updated cache entry
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * const updatedEntry = await updateCacheEntry(existingEntry, page, config, new Date());
48
+ * ```
49
+ */
50
+ export declare function updateCacheEntry(entry: CacheEntry, page: PageModel, config: StatiConfig, renderedAt: Date): Promise<CacheEntry>;
51
+ //# sourceMappingURL=builder.d.ts.map
@@ -0,0 +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,gBAAgB,CAAC;AAmEzE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,SAAS,EACf,KAAK,EAAE,UAAU,GAAG,SAAS,EAC7B,MAAM,EAAE,WAAW,EACnB,GAAG,EAAE,IAAI,GACR,OAAO,CAAC,OAAO,CAAC,CAgKlB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,IAAI,GACf,OAAO,CAAC,UAAU,CAAC,CA2ErB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,UAAU,EACjB,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,IAAI,GACf,OAAO,CAAC,UAAU,CAAC,CAUrB"}
@@ -0,0 +1,321 @@
1
+ import { computeContentHash, computeFileHash, computeInputsHash } from './hash.js';
2
+ import { trackTemplateDependencies } from './deps.js';
3
+ import { computeEffectiveTTL, computeNextRebuildAt, isPageFrozen } from './ttl.js';
4
+ import { validatePageISGOverrides, extractNumericOverride } from './validation.js';
5
+ /**
6
+ * Determines the output path for a page.
7
+ */
8
+ function getOutputPath(page) {
9
+ if (page.url === '/') {
10
+ return '/index.html';
11
+ }
12
+ else if (page.url.endsWith('/')) {
13
+ return `${page.url}index.html`;
14
+ }
15
+ else {
16
+ return `${page.url}.html`;
17
+ }
18
+ }
19
+ /**
20
+ * Validates a cache entry structure to ensure it has all required fields.
21
+ * Used to detect corrupted cache entries that should trigger a rebuild.
22
+ */
23
+ function isValidCacheEntry(entry) {
24
+ if (!entry || typeof entry !== 'object') {
25
+ return false;
26
+ }
27
+ // Check required string fields
28
+ const requiredStringFields = ['path', 'inputsHash', 'renderedAt'];
29
+ for (const field of requiredStringFields) {
30
+ if (typeof entry[field] !== 'string') {
31
+ return false;
32
+ }
33
+ }
34
+ // Check required number fields
35
+ if (typeof entry.ttlSeconds !== 'number' || !Number.isFinite(entry.ttlSeconds)) {
36
+ return false;
37
+ }
38
+ // Check required array fields
39
+ if (!Array.isArray(entry.deps) || !Array.isArray(entry.tags)) {
40
+ return false;
41
+ }
42
+ // Check that arrays contain only strings
43
+ if (!entry.deps.every((dep) => typeof dep === 'string')) {
44
+ return false;
45
+ }
46
+ if (!entry.tags.every((tag) => typeof tag === 'string')) {
47
+ return false;
48
+ }
49
+ // Check optional fields
50
+ if (entry.publishedAt !== undefined && typeof entry.publishedAt !== 'string') {
51
+ return false;
52
+ }
53
+ if (entry.maxAgeCapDays !== undefined && typeof entry.maxAgeCapDays !== 'number') {
54
+ return false;
55
+ }
56
+ return true;
57
+ }
58
+ /**
59
+ * Determines if a page should be rebuilt based on ISG logic.
60
+ * Checks content changes, dependency changes, TTL expiration, and freeze status.
61
+ * Handles edge cases like missing dates, corrupted cache entries, and dependency errors.
62
+ *
63
+ * @param page - The page model to check
64
+ * @param entry - Existing cache entry, or undefined if not cached
65
+ * @param config - Stati configuration
66
+ * @param now - Current date/time
67
+ * @returns True if the page should be rebuilt
68
+ *
69
+ * @example
70
+ * ```typescript
71
+ * const shouldRebuild = await shouldRebuildPage(page, cacheEntry, config, new Date());
72
+ * if (shouldRebuild) {
73
+ * console.log('Page needs rebuilding');
74
+ * }
75
+ * ```
76
+ */
77
+ export async function shouldRebuildPage(page, entry, config, now) {
78
+ // Always rebuild if no cache entry exists
79
+ if (!entry) {
80
+ return true;
81
+ }
82
+ try {
83
+ // Validate the cache entry structure
84
+ if (!isValidCacheEntry(entry)) {
85
+ console.warn(`Invalid cache entry for ${page.url}, forcing rebuild`);
86
+ return true;
87
+ }
88
+ // Check if inputs (content + dependencies) have changed
89
+ const currentContentHash = computeContentHash(page.content, page.frontMatter);
90
+ // Track dependencies with error handling
91
+ let deps;
92
+ try {
93
+ deps = await trackTemplateDependencies(page, config);
94
+ }
95
+ catch (error) {
96
+ if (error instanceof Error && error.name === 'CircularDependencyError') {
97
+ console.error(`Circular dependency detected for ${page.url}: ${error.message}`);
98
+ throw error; // Re-throw circular dependency errors as they're fatal
99
+ }
100
+ // For other dependency errors, log warning and assume dependencies changed
101
+ console.warn(`Failed to track dependencies for ${page.url}: ${error instanceof Error ? error.message : String(error)}`);
102
+ console.warn('Assuming dependencies changed, forcing rebuild');
103
+ return true;
104
+ }
105
+ // Compute hashes for all dependencies with error handling
106
+ const depsHashes = [];
107
+ let dependencyErrors = 0;
108
+ for (const dep of deps) {
109
+ try {
110
+ const depHash = await computeFileHash(dep);
111
+ if (depHash) {
112
+ depsHashes.push(depHash);
113
+ }
114
+ else {
115
+ dependencyErrors++;
116
+ console.warn(`Missing dependency file: ${dep} (used by ${page.url})`);
117
+ }
118
+ }
119
+ catch (error) {
120
+ dependencyErrors++;
121
+ console.warn(`Failed to hash dependency ${dep}: ${error instanceof Error ? error.message : String(error)}`);
122
+ }
123
+ }
124
+ // If we had dependency errors, force rebuild to be safe
125
+ if (dependencyErrors > 0) {
126
+ console.warn(`${dependencyErrors} dependency errors for ${page.url}, forcing rebuild`);
127
+ return true;
128
+ }
129
+ const currentInputsHash = computeInputsHash(currentContentHash, depsHashes);
130
+ // If inputs changed, always rebuild
131
+ if (currentInputsHash !== entry.inputsHash) {
132
+ return true;
133
+ }
134
+ // If page is frozen (beyond max age cap), don't rebuild based on TTL
135
+ if (isPageFrozen(entry, now)) {
136
+ return false;
137
+ }
138
+ // Check if TTL has expired with safe date handling
139
+ let renderedAt;
140
+ try {
141
+ renderedAt = new Date(entry.renderedAt);
142
+ if (isNaN(renderedAt.getTime())) {
143
+ console.warn(`Invalid renderedAt date in cache entry for ${page.url}, forcing rebuild`);
144
+ return true;
145
+ }
146
+ }
147
+ catch (error) {
148
+ console.warn(`Failed to parse renderedAt for ${page.url}: ${error instanceof Error ? error.message : String(error)}`);
149
+ return true;
150
+ }
151
+ // Handle edge case: invalid TTL values
152
+ if (typeof entry.ttlSeconds !== 'number' ||
153
+ entry.ttlSeconds < 0 ||
154
+ !Number.isFinite(entry.ttlSeconds)) {
155
+ console.warn(`Invalid TTL value in cache entry for ${page.url}: ${entry.ttlSeconds}, forcing rebuild`);
156
+ return true;
157
+ }
158
+ const computeOptions = {
159
+ now: renderedAt,
160
+ ttlSeconds: entry.ttlSeconds,
161
+ };
162
+ // Handle publishedAt edge cases
163
+ if (entry.publishedAt) {
164
+ try {
165
+ const publishedDate = new Date(entry.publishedAt);
166
+ if (!isNaN(publishedDate.getTime())) {
167
+ computeOptions.publishedAt = publishedDate;
168
+ }
169
+ else {
170
+ console.warn(`Invalid publishedAt date in cache entry for ${page.url}, ignoring for TTL calculation`);
171
+ }
172
+ }
173
+ catch (error) {
174
+ console.warn(`Failed to parse publishedAt for ${page.url}: ${error instanceof Error ? error.message : String(error)}`);
175
+ }
176
+ }
177
+ // Handle maxAgeCapDays edge cases
178
+ if (entry.maxAgeCapDays !== undefined) {
179
+ if (typeof entry.maxAgeCapDays === 'number' &&
180
+ entry.maxAgeCapDays > 0 &&
181
+ Number.isFinite(entry.maxAgeCapDays)) {
182
+ computeOptions.maxAgeCapDays = entry.maxAgeCapDays;
183
+ }
184
+ else {
185
+ console.warn(`Invalid maxAgeCapDays in cache entry for ${page.url}: ${entry.maxAgeCapDays}, ignoring`);
186
+ }
187
+ }
188
+ const nextRebuildAt = computeNextRebuildAt(computeOptions);
189
+ // If no next rebuild time (frozen), don't rebuild
190
+ if (!nextRebuildAt) {
191
+ return false;
192
+ }
193
+ // Rebuild if TTL has expired
194
+ return now >= nextRebuildAt;
195
+ }
196
+ catch (error) {
197
+ // For any unexpected errors, log and force rebuild to be safe
198
+ console.warn(`Error checking rebuild status for ${page.url}: ${error instanceof Error ? error.message : String(error)}`);
199
+ console.warn('Forcing rebuild due to error');
200
+ return true;
201
+ }
202
+ }
203
+ /**
204
+ * Creates a new cache entry for a page after it has been rendered.
205
+ *
206
+ * @param page - The page model
207
+ * @param config - Stati configuration
208
+ * @param renderedAt - When the page was rendered
209
+ * @returns New cache entry
210
+ *
211
+ * @example
212
+ * ```typescript
213
+ * const entry = await createCacheEntry(page, config, new Date());
214
+ * ```
215
+ */
216
+ export async function createCacheEntry(page, config, renderedAt) {
217
+ // Validate page-level ISG overrides first
218
+ validatePageISGOverrides(page.frontMatter, page.sourcePath);
219
+ // Compute content hash
220
+ const contentHash = computeContentHash(page.content, page.frontMatter);
221
+ // Track all template dependencies
222
+ const deps = await trackTemplateDependencies(page, config);
223
+ // Compute hashes for all dependencies
224
+ const depsHashes = [];
225
+ for (const dep of deps) {
226
+ const depHash = await computeFileHash(dep);
227
+ if (depHash) {
228
+ depsHashes.push(depHash);
229
+ }
230
+ }
231
+ const inputsHash = computeInputsHash(contentHash, depsHashes);
232
+ // Extract tags from front matter
233
+ let tags = [];
234
+ if (Array.isArray(page.frontMatter.tags)) {
235
+ tags = page.frontMatter.tags.filter((tag) => typeof tag === 'string');
236
+ }
237
+ // Get published date
238
+ const publishedAt = getPublishedDateISO(page);
239
+ // Compute effective TTL
240
+ const isgConfig = config.isg || {};
241
+ const ttlSeconds = computeEffectiveTTL(page, isgConfig);
242
+ // Get max age cap from front matter or config using safe extraction
243
+ let maxAgeCapDays = isgConfig.maxAgeCapDays;
244
+ try {
245
+ const frontMatterMaxAge = extractNumericOverride(page.frontMatter.maxAgeCapDays, 'maxAgeCapDays', page.sourcePath);
246
+ if (frontMatterMaxAge !== undefined) {
247
+ maxAgeCapDays = frontMatterMaxAge;
248
+ }
249
+ }
250
+ catch (error) {
251
+ // Log validation error but continue with default value
252
+ console.warn(`ISG validation warning for ${page.sourcePath}: ${error instanceof Error ? error.message : String(error)}`);
253
+ }
254
+ // Determine output path
255
+ const outputPath = getOutputPath(page);
256
+ // Build cache entry with proper optional handling
257
+ const cacheEntry = {
258
+ path: outputPath,
259
+ inputsHash,
260
+ deps,
261
+ tags,
262
+ renderedAt: renderedAt.toISOString(),
263
+ ttlSeconds,
264
+ };
265
+ // Add optional fields only if they exist
266
+ if (publishedAt) {
267
+ cacheEntry.publishedAt = publishedAt;
268
+ }
269
+ if (maxAgeCapDays !== undefined) {
270
+ cacheEntry.maxAgeCapDays = maxAgeCapDays;
271
+ }
272
+ return cacheEntry;
273
+ }
274
+ /**
275
+ * Updates an existing cache entry with new information after rebuilding.
276
+ *
277
+ * @param entry - Existing cache entry
278
+ * @param page - The page model
279
+ * @param config - Stati configuration
280
+ * @param renderedAt - When the page was rendered
281
+ * @returns Updated cache entry
282
+ *
283
+ * @example
284
+ * ```typescript
285
+ * const updatedEntry = await updateCacheEntry(existingEntry, page, config, new Date());
286
+ * ```
287
+ */
288
+ export async function updateCacheEntry(entry, page, config, renderedAt) {
289
+ // Create a new entry and preserve the original publishedAt if not overridden
290
+ const newEntry = await createCacheEntry(page, config, renderedAt);
291
+ // Preserve original publishedAt if no new one is specified
292
+ if (!newEntry.publishedAt && entry.publishedAt) {
293
+ newEntry.publishedAt = entry.publishedAt;
294
+ }
295
+ return newEntry;
296
+ }
297
+ /**
298
+ * Helper function to get published date as ISO string from page front matter.
299
+ */
300
+ function getPublishedDateISO(page) {
301
+ const frontMatter = page.frontMatter;
302
+ // Try common field names for published date
303
+ const dateFields = ['publishedAt', 'published', 'date', 'createdAt'];
304
+ for (const field of dateFields) {
305
+ const value = frontMatter[field];
306
+ if (value) {
307
+ // Handle string dates
308
+ if (typeof value === 'string') {
309
+ const date = new Date(value);
310
+ if (!isNaN(date.getTime())) {
311
+ return date.toISOString();
312
+ }
313
+ }
314
+ // Handle Date objects
315
+ if (value instanceof Date && !isNaN(value.getTime())) {
316
+ return value.toISOString();
317
+ }
318
+ }
319
+ }
320
+ return undefined;
321
+ }
@@ -0,0 +1,63 @@
1
+ import type { PageModel, StatiConfig } from '../../types.js';
2
+ /**
3
+ * Error thrown when a circular dependency is detected in templates.
4
+ */
5
+ export declare class CircularDependencyError extends Error {
6
+ readonly dependencyChain: string[];
7
+ constructor(dependencyChain: string[], message: string);
8
+ }
9
+ /**
10
+ * Tracks all template dependencies for a given page.
11
+ * This includes the layout file and all accessible partials.
12
+ * Includes circular dependency detection.
13
+ *
14
+ * @param page - The page model to track dependencies for
15
+ * @param config - Stati configuration
16
+ * @returns Array of absolute paths to dependency files
17
+ * @throws {CircularDependencyError} When circular dependencies are detected
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * try {
22
+ * const deps = await trackTemplateDependencies(page, config);
23
+ * console.log(`Page depends on ${deps.length} template files`);
24
+ * } catch (error) {
25
+ * if (error instanceof CircularDependencyError) {
26
+ * console.error(`Circular dependency: ${error.dependencyChain.join(' -> ')}`);
27
+ * }
28
+ * }
29
+ * ```
30
+ */
31
+ export declare function trackTemplateDependencies(page: PageModel, config: StatiConfig): Promise<string[]>;
32
+ /**
33
+ * Finds all partial dependencies for a given page path.
34
+ * Searches up the directory hierarchy for _* folders containing .eta files.
35
+ *
36
+ * @param pagePath - Relative path to the page from srcDir
37
+ * @param config - Stati configuration
38
+ * @returns Array of absolute paths to partial files
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * const partials = await findPartialDependencies('blog/post.md', config);
43
+ * ```
44
+ */
45
+ export declare function findPartialDependencies(pagePath: string, config: StatiConfig): Promise<string[]>;
46
+ /**
47
+ * Resolves a template name to its file path.
48
+ * Used for explicit layout specifications in front matter.
49
+ *
50
+ * @param layout - Layout name (without .eta extension)
51
+ * @param config - Stati configuration
52
+ * @returns Absolute path to template file, or null if not found
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * const templatePath = await resolveTemplatePath('post', config);
57
+ * if (templatePath) {
58
+ * console.log(`Found template at: ${templatePath}`);
59
+ * }
60
+ * ```
61
+ */
62
+ export declare function resolveTemplatePath(layout: string, config: StatiConfig): Promise<string | null>;
63
+ //# sourceMappingURL=deps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deps.d.ts","sourceRoot":"","sources":["../../../src/core/isg/deps.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7D;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;aAE9B,eAAe,EAAE,MAAM,EAAE;gBAAzB,eAAe,EAAE,MAAM,EAAE,EACzC,OAAO,EAAE,MAAM;CAKlB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,EAAE,CAAC,CAoCnB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,EAAE,CAAC,CAiDnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CASxB"}