@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.
- package/README.md +19 -7
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +24 -2
- package/dist/core/build.d.ts +21 -15
- package/dist/core/build.d.ts.map +1 -1
- package/dist/core/build.js +141 -42
- package/dist/core/dev.d.ts.map +1 -1
- package/dist/core/dev.js +84 -18
- package/dist/core/invalidate.d.ts +67 -1
- package/dist/core/invalidate.d.ts.map +1 -1
- package/dist/core/invalidate.js +321 -4
- package/dist/core/isg/build-lock.d.ts +116 -0
- package/dist/core/isg/build-lock.d.ts.map +1 -0
- package/dist/core/isg/build-lock.js +245 -0
- package/dist/core/isg/builder.d.ts +51 -0
- package/dist/core/isg/builder.d.ts.map +1 -0
- package/dist/core/isg/builder.js +321 -0
- package/dist/core/isg/deps.d.ts +63 -0
- package/dist/core/isg/deps.d.ts.map +1 -0
- package/dist/core/isg/deps.js +332 -0
- package/dist/core/isg/hash.d.ts +48 -0
- package/dist/core/isg/hash.d.ts.map +1 -0
- package/dist/core/isg/hash.js +82 -0
- package/dist/core/isg/manifest.d.ts +47 -0
- package/dist/core/isg/manifest.d.ts.map +1 -0
- package/dist/core/isg/manifest.js +233 -0
- package/dist/core/isg/ttl.d.ts +101 -0
- package/dist/core/isg/ttl.d.ts.map +1 -0
- package/dist/core/isg/ttl.js +222 -0
- package/dist/core/isg/validation.d.ts +71 -0
- package/dist/core/isg/validation.d.ts.map +1 -0
- package/dist/core/isg/validation.js +226 -0
- package/dist/core/templates.d.ts.map +1 -1
- package/dist/core/templates.js +3 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +110 -20
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { join, dirname, relative, posix } from 'path';
|
|
2
|
+
import fse from 'fs-extra';
|
|
3
|
+
const { pathExists, readFile } = fse;
|
|
4
|
+
import glob from 'fast-glob';
|
|
5
|
+
/**
|
|
6
|
+
* Error thrown when a circular dependency is detected in templates.
|
|
7
|
+
*/
|
|
8
|
+
export class CircularDependencyError extends Error {
|
|
9
|
+
dependencyChain;
|
|
10
|
+
constructor(dependencyChain, message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.dependencyChain = dependencyChain;
|
|
13
|
+
this.name = 'CircularDependencyError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Tracks all template dependencies for a given page.
|
|
18
|
+
* This includes the layout file and all accessible partials.
|
|
19
|
+
* Includes circular dependency detection.
|
|
20
|
+
*
|
|
21
|
+
* @param page - The page model to track dependencies for
|
|
22
|
+
* @param config - Stati configuration
|
|
23
|
+
* @returns Array of absolute paths to dependency files
|
|
24
|
+
* @throws {CircularDependencyError} When circular dependencies are detected
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* try {
|
|
29
|
+
* const deps = await trackTemplateDependencies(page, config);
|
|
30
|
+
* console.log(`Page depends on ${deps.length} template files`);
|
|
31
|
+
* } catch (error) {
|
|
32
|
+
* if (error instanceof CircularDependencyError) {
|
|
33
|
+
* console.error(`Circular dependency: ${error.dependencyChain.join(' -> ')}`);
|
|
34
|
+
* }
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export async function trackTemplateDependencies(page, config) {
|
|
39
|
+
// Early return if required config values are missing
|
|
40
|
+
if (!config.srcDir) {
|
|
41
|
+
console.warn('Config srcDir is missing, cannot track template dependencies');
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const deps = [];
|
|
45
|
+
const srcDir = join(process.cwd(), config.srcDir);
|
|
46
|
+
const relativePath = relative(srcDir, page.sourcePath);
|
|
47
|
+
// Track dependencies with circular detection
|
|
48
|
+
const visited = new Set();
|
|
49
|
+
const currentPath = new Set();
|
|
50
|
+
// 1. Find the layout file that will be used for this page
|
|
51
|
+
const layoutPath = await discoverLayout(relativePath, config, page.frontMatter.layout, isCollectionIndexPage(page));
|
|
52
|
+
if (layoutPath) {
|
|
53
|
+
const absoluteLayoutPath = join(srcDir, layoutPath);
|
|
54
|
+
deps.push(absoluteLayoutPath);
|
|
55
|
+
// Check for circular dependencies in layout chain
|
|
56
|
+
await detectCircularDependencies(absoluteLayoutPath, srcDir, visited, currentPath);
|
|
57
|
+
}
|
|
58
|
+
// 2. Find all partials accessible to this page
|
|
59
|
+
const partialDeps = await findPartialDependencies(relativePath, config);
|
|
60
|
+
deps.push(...partialDeps);
|
|
61
|
+
return deps;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Finds all partial dependencies for a given page path.
|
|
65
|
+
* Searches up the directory hierarchy for _* folders containing .eta files.
|
|
66
|
+
*
|
|
67
|
+
* @param pagePath - Relative path to the page from srcDir
|
|
68
|
+
* @param config - Stati configuration
|
|
69
|
+
* @returns Array of absolute paths to partial files
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* const partials = await findPartialDependencies('blog/post.md', config);
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export async function findPartialDependencies(pagePath, config) {
|
|
77
|
+
// Early return if required config values are missing
|
|
78
|
+
if (!config.srcDir) {
|
|
79
|
+
console.warn('Config srcDir is missing, cannot find partial dependencies');
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
const deps = [];
|
|
83
|
+
const srcDir = join(process.cwd(), config.srcDir);
|
|
84
|
+
// Get the directory of the current page
|
|
85
|
+
const pageDir = dirname(pagePath);
|
|
86
|
+
const pathSegments = pageDir === '.' ? [] : pageDir.split(/[/\\]/);
|
|
87
|
+
// Build list of directories to search (current dir up to root)
|
|
88
|
+
const dirsToSearch = [];
|
|
89
|
+
// Add directories from current up to root
|
|
90
|
+
if (pathSegments.length > 0) {
|
|
91
|
+
for (let i = pathSegments.length; i >= 0; i--) {
|
|
92
|
+
if (i === 0) {
|
|
93
|
+
dirsToSearch.push(''); // Root directory
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
dirsToSearch.push(pathSegments.slice(0, i).join('/'));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
dirsToSearch.push(''); // Root directory only
|
|
102
|
+
}
|
|
103
|
+
// Search each directory for _* folders containing .eta files
|
|
104
|
+
for (const dir of dirsToSearch) {
|
|
105
|
+
const searchDir = dir ? join(srcDir, dir) : srcDir;
|
|
106
|
+
try {
|
|
107
|
+
// Find all .eta files in _* subdirectories
|
|
108
|
+
// Use posix.join to ensure forward slashes for glob patterns
|
|
109
|
+
const normalizedSearchDir = searchDir.replace(/\\/g, '/');
|
|
110
|
+
const pattern = posix.join(normalizedSearchDir, '_*/**/*.eta');
|
|
111
|
+
const partialFiles = await glob(pattern, { absolute: true });
|
|
112
|
+
deps.push(...partialFiles);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Continue if directory doesn't exist or can't be read
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return deps;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Resolves a template name to its file path.
|
|
123
|
+
* Used for explicit layout specifications in front matter.
|
|
124
|
+
*
|
|
125
|
+
* @param layout - Layout name (without .eta extension)
|
|
126
|
+
* @param config - Stati configuration
|
|
127
|
+
* @returns Absolute path to template file, or null if not found
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```typescript
|
|
131
|
+
* const templatePath = await resolveTemplatePath('post', config);
|
|
132
|
+
* if (templatePath) {
|
|
133
|
+
* console.log(`Found template at: ${templatePath}`);
|
|
134
|
+
* }
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export async function resolveTemplatePath(layout, config) {
|
|
138
|
+
const srcDir = join(process.cwd(), config.srcDir);
|
|
139
|
+
const layoutPath = join(srcDir, `${layout}.eta`);
|
|
140
|
+
if (await pathExists(layoutPath)) {
|
|
141
|
+
return layoutPath;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Helper function to determine if a page is a collection index page.
|
|
147
|
+
* Duplicated from templates.ts to avoid circular dependencies.
|
|
148
|
+
*/
|
|
149
|
+
function isCollectionIndexPage(page) {
|
|
150
|
+
// This is a simplified version - in a real implementation,
|
|
151
|
+
// we'd need access to all pages to determine this properly.
|
|
152
|
+
// For now, we'll assume any page ending in /index or at root is a collection page.
|
|
153
|
+
return page.url === '/' || page.url.endsWith('/index') || page.slug === 'index';
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Helper function to discover layout files.
|
|
157
|
+
* Duplicated from templates.ts to avoid circular dependencies.
|
|
158
|
+
*/
|
|
159
|
+
async function discoverLayout(pagePath, config, explicitLayout, isIndexPage) {
|
|
160
|
+
// Early return if required config values are missing
|
|
161
|
+
if (!config.srcDir) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const srcDir = join(process.cwd(), config.srcDir);
|
|
165
|
+
// If explicit layout is specified, use it
|
|
166
|
+
if (explicitLayout) {
|
|
167
|
+
const layoutPath = join(srcDir, `${explicitLayout}.eta`);
|
|
168
|
+
if (await pathExists(layoutPath)) {
|
|
169
|
+
return `${explicitLayout}.eta`;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Get the directory of the current page
|
|
173
|
+
const pageDir = dirname(pagePath);
|
|
174
|
+
const pathSegments = pageDir === '.' ? [] : pageDir.split(/[/\\]/);
|
|
175
|
+
// Search for layout.eta from current directory up to root
|
|
176
|
+
const dirsToSearch = [];
|
|
177
|
+
// Add current directory if not root
|
|
178
|
+
if (pathSegments.length > 0) {
|
|
179
|
+
for (let i = pathSegments.length; i > 0; i--) {
|
|
180
|
+
dirsToSearch.push(pathSegments.slice(0, i).join('/'));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Add root directory
|
|
184
|
+
dirsToSearch.push('');
|
|
185
|
+
for (const dir of dirsToSearch) {
|
|
186
|
+
// For index pages, first check for index.eta in each directory
|
|
187
|
+
if (isIndexPage) {
|
|
188
|
+
const indexLayoutPath = dir ? join(srcDir, dir, 'index.eta') : join(srcDir, 'index.eta');
|
|
189
|
+
if (await pathExists(indexLayoutPath)) {
|
|
190
|
+
const relativePath = dir ? `${dir}/index.eta` : 'index.eta';
|
|
191
|
+
return posix.normalize(relativePath);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Then check for layout.eta as fallback
|
|
195
|
+
const layoutPath = dir ? join(srcDir, dir, 'layout.eta') : join(srcDir, 'layout.eta');
|
|
196
|
+
if (await pathExists(layoutPath)) {
|
|
197
|
+
const relativePath = dir ? `${dir}/layout.eta` : 'layout.eta';
|
|
198
|
+
return posix.normalize(relativePath);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Detects circular dependencies in template includes/extends.
|
|
205
|
+
* Uses DFS to traverse the dependency graph and detect cycles.
|
|
206
|
+
*
|
|
207
|
+
* @param templatePath - Absolute path to template file to check
|
|
208
|
+
* @param srcDir - Source directory for resolving relative template paths
|
|
209
|
+
* @param visited - Set of all visited template paths (for optimization)
|
|
210
|
+
* @param currentPath - Set of templates in current DFS path (for cycle detection)
|
|
211
|
+
* @throws {CircularDependencyError} When a circular dependency is detected
|
|
212
|
+
*/
|
|
213
|
+
async function detectCircularDependencies(templatePath, srcDir, visited, currentPath) {
|
|
214
|
+
// Skip if already processed
|
|
215
|
+
if (visited.has(templatePath)) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// Check for circular dependency
|
|
219
|
+
if (currentPath.has(templatePath)) {
|
|
220
|
+
const chain = Array.from(currentPath);
|
|
221
|
+
chain.push(templatePath);
|
|
222
|
+
throw new CircularDependencyError(chain, `Circular dependency detected in templates: ${chain.join(' -> ')}`);
|
|
223
|
+
}
|
|
224
|
+
// Check if template file exists
|
|
225
|
+
if (!(await pathExists(templatePath))) {
|
|
226
|
+
// Don't treat missing files as circular dependencies
|
|
227
|
+
// They will be handled by the missing file error handling
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
// Add to current path
|
|
231
|
+
currentPath.add(templatePath);
|
|
232
|
+
visited.add(templatePath);
|
|
233
|
+
try {
|
|
234
|
+
// Read template content to find includes/extends
|
|
235
|
+
const content = await readFile(templatePath, 'utf-8');
|
|
236
|
+
const dependencies = await parseTemplateDependencies(content, templatePath, srcDir);
|
|
237
|
+
// Recursively check dependencies
|
|
238
|
+
for (const depPath of dependencies) {
|
|
239
|
+
await detectCircularDependencies(depPath, srcDir, visited, currentPath);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
// If we can't read the file, don't treat it as a circular dependency
|
|
244
|
+
if (error instanceof Error && !error.message.includes('Circular dependency')) {
|
|
245
|
+
console.warn(`Warning: Could not read template ${templatePath}: ${error.message}`);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
throw error; // Re-throw circular dependency errors
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
// Remove from current path when backtracking
|
|
253
|
+
currentPath.delete(templatePath);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Parses a template file to find included/extended templates.
|
|
258
|
+
* This is a basic implementation that looks for common Eta patterns.
|
|
259
|
+
*
|
|
260
|
+
* @param content - Template file content
|
|
261
|
+
* @param templatePath - Path to the template file (for error context)
|
|
262
|
+
* @param srcDir - Source directory for resolving relative paths
|
|
263
|
+
* @returns Array of absolute paths to dependent templates
|
|
264
|
+
*/
|
|
265
|
+
async function parseTemplateDependencies(content, templatePath, srcDir) {
|
|
266
|
+
const dependencies = [];
|
|
267
|
+
// Look for Eta include patterns: <%~ include('template') %>
|
|
268
|
+
const includePatterns = [
|
|
269
|
+
/<%[~-]?\s*include\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g,
|
|
270
|
+
/<%[~-]?\s*include\s*\(\s*['"`]([^'"`]+)['"`]\s*,/g, // with parameters
|
|
271
|
+
];
|
|
272
|
+
for (const pattern of includePatterns) {
|
|
273
|
+
let match;
|
|
274
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
275
|
+
const includePath = match[1];
|
|
276
|
+
if (includePath) {
|
|
277
|
+
const resolvedPath = await resolveTemplatePathInternal(includePath, srcDir);
|
|
278
|
+
if (resolvedPath) {
|
|
279
|
+
dependencies.push(resolvedPath);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// Look for Eta layout/extends patterns: <%~ layout('template') %>
|
|
285
|
+
const layoutPatterns = [
|
|
286
|
+
/<%[~-]?\s*layout\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g,
|
|
287
|
+
/<%[~-]?\s*extends?\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g,
|
|
288
|
+
];
|
|
289
|
+
for (const pattern of layoutPatterns) {
|
|
290
|
+
let match;
|
|
291
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
292
|
+
const layoutPath = match[1];
|
|
293
|
+
if (layoutPath) {
|
|
294
|
+
const resolvedPath = await resolveTemplatePathInternal(layoutPath, srcDir);
|
|
295
|
+
if (resolvedPath) {
|
|
296
|
+
dependencies.push(resolvedPath);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return dependencies;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Internal helper to resolve template paths (renamed to avoid naming conflict).
|
|
305
|
+
* Handles relative paths and adds .eta extension if needed.
|
|
306
|
+
*
|
|
307
|
+
* @param templateRef - Template reference from include/layout statement
|
|
308
|
+
* @param srcDir - Source directory for resolving paths
|
|
309
|
+
* @returns Absolute path to template file, or null if not found
|
|
310
|
+
*/
|
|
311
|
+
async function resolveTemplatePathInternal(templateRef, srcDir) {
|
|
312
|
+
// Add .eta extension if not present
|
|
313
|
+
const templateName = templateRef.endsWith('.eta') ? templateRef : `${templateRef}.eta`;
|
|
314
|
+
// Try absolute path from srcDir
|
|
315
|
+
const absolutePath = join(srcDir, templateName);
|
|
316
|
+
if (await pathExists(absolutePath)) {
|
|
317
|
+
return absolutePath;
|
|
318
|
+
}
|
|
319
|
+
// Try resolving relative to template directories
|
|
320
|
+
// This is a simplified version - real implementation might need more sophisticated resolution
|
|
321
|
+
const possiblePaths = [
|
|
322
|
+
join(srcDir, '_templates', templateName),
|
|
323
|
+
join(srcDir, '_partials', templateName),
|
|
324
|
+
join(srcDir, '_layouts', templateName),
|
|
325
|
+
];
|
|
326
|
+
for (const path of possiblePaths) {
|
|
327
|
+
if (await pathExists(path)) {
|
|
328
|
+
return path;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Computes a SHA-256 hash of page content and front matter.
|
|
3
|
+
* Used to detect when page content has changed.
|
|
4
|
+
*
|
|
5
|
+
* @param content - The raw markdown content of the page
|
|
6
|
+
* @param frontMatter - The parsed front matter object
|
|
7
|
+
* @returns SHA-256 hash as a hex string
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const hash = computeContentHash('# Hello World', { title: 'My Post' });
|
|
12
|
+
* console.log(hash); // "sha256-abc123..."
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare function computeContentHash(content: string, frontMatter: Record<string, unknown>): string;
|
|
16
|
+
/**
|
|
17
|
+
* Computes a SHA-256 hash of a file's contents.
|
|
18
|
+
* Used for tracking template and partial file changes.
|
|
19
|
+
*
|
|
20
|
+
* @param filePath - Absolute path to the file
|
|
21
|
+
* @returns Promise resolving to SHA-256 hash as a hex string, or null if file doesn't exist
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* const hash = await computeFileHash('/path/to/template.eta');
|
|
26
|
+
* if (hash) {
|
|
27
|
+
* console.log(`Template hash: ${hash}`);
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function computeFileHash(filePath: string): Promise<string | null>;
|
|
32
|
+
/**
|
|
33
|
+
* Computes a combined hash from content hash and dependency hashes.
|
|
34
|
+
* This represents the complete input state for a page.
|
|
35
|
+
*
|
|
36
|
+
* @param contentHash - Hash of the page content and front matter
|
|
37
|
+
* @param depsHashes - Array of dependency file hashes
|
|
38
|
+
* @returns Combined SHA-256 hash as a hex string
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* const contentHash = computeContentHash(content, frontMatter);
|
|
43
|
+
* const depsHashes = ['sha256-dep1...', 'sha256-dep2...'];
|
|
44
|
+
* const inputsHash = computeInputsHash(contentHash, depsHashes);
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function computeInputsHash(contentHash: string, depsHashes: string[]): string;
|
|
48
|
+
//# sourceMappingURL=hash.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../../../src/core/isg/hash.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAWhG;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiB9E;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,CAanF"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import fse from 'fs-extra';
|
|
3
|
+
const { readFile, pathExists } = fse;
|
|
4
|
+
/**
|
|
5
|
+
* Computes a SHA-256 hash of page content and front matter.
|
|
6
|
+
* Used to detect when page content has changed.
|
|
7
|
+
*
|
|
8
|
+
* @param content - The raw markdown content of the page
|
|
9
|
+
* @param frontMatter - The parsed front matter object
|
|
10
|
+
* @returns SHA-256 hash as a hex string
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const hash = computeContentHash('# Hello World', { title: 'My Post' });
|
|
15
|
+
* console.log(hash); // "sha256-abc123..."
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function computeContentHash(content, frontMatter) {
|
|
19
|
+
const hash = createHash('sha256');
|
|
20
|
+
// Hash the content
|
|
21
|
+
hash.update(content, 'utf-8');
|
|
22
|
+
// Hash the front matter (sorted for consistency)
|
|
23
|
+
const sortedFrontMatter = JSON.stringify(frontMatter, Object.keys(frontMatter).sort());
|
|
24
|
+
hash.update(sortedFrontMatter, 'utf-8');
|
|
25
|
+
return `sha256-${hash.digest('hex')}`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Computes a SHA-256 hash of a file's contents.
|
|
29
|
+
* Used for tracking template and partial file changes.
|
|
30
|
+
*
|
|
31
|
+
* @param filePath - Absolute path to the file
|
|
32
|
+
* @returns Promise resolving to SHA-256 hash as a hex string, or null if file doesn't exist
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* const hash = await computeFileHash('/path/to/template.eta');
|
|
37
|
+
* if (hash) {
|
|
38
|
+
* console.log(`Template hash: ${hash}`);
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export async function computeFileHash(filePath) {
|
|
43
|
+
try {
|
|
44
|
+
if (!(await pathExists(filePath))) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const content = await readFile(filePath, 'utf-8');
|
|
48
|
+
const hash = createHash('sha256');
|
|
49
|
+
hash.update(content, 'utf-8');
|
|
50
|
+
return `sha256-${hash.digest('hex')}`;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
console.warn(`Failed to compute hash for ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Computes a combined hash from content hash and dependency hashes.
|
|
59
|
+
* This represents the complete input state for a page.
|
|
60
|
+
*
|
|
61
|
+
* @param contentHash - Hash of the page content and front matter
|
|
62
|
+
* @param depsHashes - Array of dependency file hashes
|
|
63
|
+
* @returns Combined SHA-256 hash as a hex string
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* const contentHash = computeContentHash(content, frontMatter);
|
|
68
|
+
* const depsHashes = ['sha256-dep1...', 'sha256-dep2...'];
|
|
69
|
+
* const inputsHash = computeInputsHash(contentHash, depsHashes);
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export function computeInputsHash(contentHash, depsHashes) {
|
|
73
|
+
const hash = createHash('sha256');
|
|
74
|
+
// Hash the content hash
|
|
75
|
+
hash.update(contentHash, 'utf-8');
|
|
76
|
+
// Hash each dependency hash in sorted order for consistency
|
|
77
|
+
const sortedDepsHashes = [...depsHashes].sort();
|
|
78
|
+
for (const depHash of sortedDepsHashes) {
|
|
79
|
+
hash.update(depHash, 'utf-8');
|
|
80
|
+
}
|
|
81
|
+
return `sha256-${hash.digest('hex')}`;
|
|
82
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { CacheManifest } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Loads the ISG cache manifest from the cache directory.
|
|
4
|
+
* Returns null if no manifest exists or if it's corrupted.
|
|
5
|
+
* Provides detailed error reporting for different failure scenarios.
|
|
6
|
+
*
|
|
7
|
+
* @param cacheDir - Path to the .stati cache directory
|
|
8
|
+
* @returns Promise resolving to the cache manifest or null
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const manifest = await loadCacheManifest('.stati');
|
|
13
|
+
* if (manifest) {
|
|
14
|
+
* console.log(`Found ${Object.keys(manifest.entries).length} cached entries`);
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare function loadCacheManifest(cacheDir: string): Promise<CacheManifest | null>;
|
|
19
|
+
/**
|
|
20
|
+
* Saves the ISG cache manifest to the cache directory.
|
|
21
|
+
* Creates the cache directory if it doesn't exist.
|
|
22
|
+
* Provides detailed error handling for common file system issues.
|
|
23
|
+
*
|
|
24
|
+
* @param cacheDir - Path to the .stati cache directory
|
|
25
|
+
* @param manifest - The cache manifest to save
|
|
26
|
+
* @throws {Error} If the manifest cannot be saved
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* const manifest: CacheManifest = { entries: {} };
|
|
31
|
+
* await saveCacheManifest('.stati', manifest);
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function saveCacheManifest(cacheDir: string, manifest: CacheManifest): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Creates an empty cache manifest with no entries.
|
|
37
|
+
*
|
|
38
|
+
* @returns A new empty cache manifest
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* const manifest = createEmptyManifest();
|
|
43
|
+
* console.log(Object.keys(manifest.entries).length); // 0
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare function createEmptyManifest(): CacheManifest;
|
|
47
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../src/core/isg/manifest.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,gBAAgB,CAAC;AAchE;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAyFvF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA6ChG;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,IAAI,aAAa,CAInD"}
|