@stati/core 1.1.0 â 1.2.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 +2 -0
- package/dist/core/build.d.ts.map +1 -1
- package/dist/core/build.js +120 -27
- 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 +243 -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
package/README.md
CHANGED
|
@@ -107,18 +107,30 @@ const server = await createDevServer(config, {
|
|
|
107
107
|
});
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
#### `invalidate(
|
|
110
|
+
#### `invalidate(query?: string): Promise<InvalidationResult>`
|
|
111
111
|
|
|
112
|
-
Invalidate cache by tags or
|
|
112
|
+
Invalidate cache by tags, paths, patterns, or age.
|
|
113
113
|
|
|
114
114
|
```typescript
|
|
115
115
|
import { invalidate } from '@stati/core';
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
117
|
+
// Invalidate by tag
|
|
118
|
+
await invalidate('tag:blog');
|
|
119
|
+
|
|
120
|
+
// Invalidate by path prefix
|
|
121
|
+
await invalidate('path:/posts');
|
|
122
|
+
|
|
123
|
+
// Invalidate by glob pattern
|
|
124
|
+
await invalidate('glob:/blog/**');
|
|
125
|
+
|
|
126
|
+
// Invalidate content younger than 3 months
|
|
127
|
+
await invalidate('age:3months');
|
|
128
|
+
|
|
129
|
+
// Multiple criteria (OR logic)
|
|
130
|
+
await invalidate('tag:blog age:1month');
|
|
131
|
+
|
|
132
|
+
// Clear entire cache
|
|
133
|
+
await invalidate();
|
|
122
134
|
```
|
|
123
135
|
|
|
124
136
|
### Configuration
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAsB/C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,UAAU,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,WAAW,CAAC,CA2DlF"}
|
package/dist/config/loader.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'fs';
|
|
2
2
|
import { join, resolve } from 'path';
|
|
3
3
|
import { pathToFileURL } from 'url';
|
|
4
|
+
import { validateISGConfig, ISGConfigurationError } from '../core/isg/validation.js';
|
|
4
5
|
/**
|
|
5
6
|
* Default configuration values for Stati.
|
|
6
7
|
* Used as fallback when no configuration file is found.
|
|
@@ -59,15 +60,36 @@ export async function loadConfig(cwd = process.cwd()) {
|
|
|
59
60
|
const configUrl = pathToFileURL(resolve(configPath)).href;
|
|
60
61
|
const module = await import(configUrl);
|
|
61
62
|
const userConfig = module.default || module;
|
|
62
|
-
|
|
63
|
+
const mergedConfig = {
|
|
63
64
|
...DEFAULT_CONFIG,
|
|
64
65
|
...userConfig,
|
|
65
66
|
site: { ...DEFAULT_CONFIG.site, ...userConfig.site },
|
|
66
67
|
isg: { ...DEFAULT_CONFIG.isg, ...userConfig.isg },
|
|
67
68
|
};
|
|
69
|
+
// Validate ISG configuration
|
|
70
|
+
try {
|
|
71
|
+
if (mergedConfig.isg) {
|
|
72
|
+
validateISGConfig(mergedConfig.isg);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error instanceof ISGConfigurationError) {
|
|
77
|
+
throw new Error(`Invalid ISG configuration in ${configPath}:\n` +
|
|
78
|
+
`${error.code}: ${error.message}\n` +
|
|
79
|
+
`Field: ${error.field}, Value: ${JSON.stringify(error.value)}\n\n` +
|
|
80
|
+
`Please check your stati.config.ts file and correct the ISG configuration.`);
|
|
81
|
+
}
|
|
82
|
+
throw error; // Re-throw non-ISG errors
|
|
83
|
+
}
|
|
84
|
+
return mergedConfig;
|
|
68
85
|
}
|
|
69
86
|
catch (error) {
|
|
70
|
-
|
|
87
|
+
if (error instanceof Error && error.message.includes('Invalid ISG configuration')) {
|
|
88
|
+
// ISG validation errors should bubble up with context
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
console.error(`Error loading config from ${configPath}:`, error);
|
|
92
|
+
console.error('Falling back to default configuration.');
|
|
71
93
|
return DEFAULT_CONFIG;
|
|
72
94
|
}
|
|
73
95
|
}
|
package/dist/core/build.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export interface BuildOptions {
|
|
|
30
30
|
/**
|
|
31
31
|
* Builds the static site by processing content files and generating HTML pages.
|
|
32
32
|
* This is the main entry point for Stati's build process.
|
|
33
|
+
* Uses build locking to prevent concurrent builds from corrupting cache.
|
|
33
34
|
*
|
|
34
35
|
* @param options - Build configuration options
|
|
35
36
|
*
|
|
@@ -51,6 +52,7 @@ export interface BuildOptions {
|
|
|
51
52
|
* @throws {Error} When configuration loading fails
|
|
52
53
|
* @throws {Error} When content processing fails
|
|
53
54
|
* @throws {Error} When template rendering fails
|
|
55
|
+
* @throws {Error} When build lock cannot be acquired
|
|
54
56
|
*/
|
|
55
57
|
export declare function build(options?: BuildOptions): Promise<BuildStats>;
|
|
56
58
|
//# sourceMappingURL=build.d.ts.map
|
package/dist/core/build.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/core/build.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/core/build.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAgB,UAAU,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAEpE;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,YAAY;IAC3B,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA2HD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,UAAU,CAAC,CAQ3E"}
|
package/dist/core/build.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import fse from 'fs-extra';
|
|
2
2
|
const { ensureDir, writeFile, remove, pathExists, stat, readdir, copyFile } = fse;
|
|
3
|
-
import { join, dirname } from 'path';
|
|
3
|
+
import { join, dirname, relative } from 'path';
|
|
4
|
+
import { posix } from 'path';
|
|
4
5
|
import { loadConfig } from '../config/loader.js';
|
|
5
6
|
import { loadContent } from './content.js';
|
|
6
7
|
import { createMarkdownProcessor, renderMarkdown } from './markdown.js';
|
|
7
8
|
import { createTemplateEngine, renderPage } from './templates.js';
|
|
8
9
|
import { buildNavigation } from './navigation.js';
|
|
10
|
+
import { loadCacheManifest, saveCacheManifest } from './isg/manifest.js';
|
|
11
|
+
import { shouldRebuildPage, createCacheEntry, updateCacheEntry } from './isg/builder.js';
|
|
12
|
+
import { withBuildLock } from './isg/build-lock.js';
|
|
9
13
|
/**
|
|
10
14
|
* Recursively calculates the total size of a directory in bytes.
|
|
11
15
|
* Used for build statistics.
|
|
@@ -50,7 +54,7 @@ async function copyStaticAssetsWithLogging(sourceDir, destDir, logger, basePath
|
|
|
50
54
|
for (const item of items) {
|
|
51
55
|
const sourcePath = join(sourceDir, item.name);
|
|
52
56
|
const destPath = join(destDir, basePath, item.name);
|
|
53
|
-
const relativePath = join(basePath, item.name)
|
|
57
|
+
const relativePath = posix.normalize(posix.join(basePath, item.name));
|
|
54
58
|
if (item.isDirectory()) {
|
|
55
59
|
// Recursively copy directories
|
|
56
60
|
await ensureDir(destPath);
|
|
@@ -92,17 +96,17 @@ const defaultLogger = {
|
|
|
92
96
|
function formatBuildStats(stats) {
|
|
93
97
|
const sizeKB = (stats.outputSizeBytes / 1024).toFixed(1);
|
|
94
98
|
const timeSeconds = (stats.buildTimeMs / 1000).toFixed(2);
|
|
95
|
-
let output =
|
|
99
|
+
let output = `Build Statistics:
|
|
96
100
|
âââââââââââââââââââââââââââââââââââââââââââ
|
|
97
|
-
â
|
|
101
|
+
â Build time: ${timeSeconds}s`.padEnd(41) + 'â';
|
|
98
102
|
output += `\nâ đ Pages built: ${stats.totalPages}`.padEnd(42) + 'â';
|
|
99
103
|
output += `\nâ đĻ Assets copied: ${stats.assetsCount}`.padEnd(42) + 'â';
|
|
100
|
-
output += `\nâ
|
|
104
|
+
output += `\nâ Output size: ${sizeKB} KB`.padEnd(42) + 'â';
|
|
101
105
|
if (stats.cacheHits !== undefined && stats.cacheMisses !== undefined) {
|
|
102
106
|
const totalCacheRequests = stats.cacheHits + stats.cacheMisses;
|
|
103
107
|
const hitRate = totalCacheRequests > 0 ? ((stats.cacheHits / totalCacheRequests) * 100).toFixed(1) : '0';
|
|
104
108
|
output +=
|
|
105
|
-
`\nâ
|
|
109
|
+
`\nâ Cache hits: ${stats.cacheHits}/${totalCacheRequests} (${hitRate}%)`.padEnd(42) + 'â';
|
|
106
110
|
}
|
|
107
111
|
output += '\nâââââââââââââââââââââââââââââââââââââââââââ';
|
|
108
112
|
return output;
|
|
@@ -110,6 +114,7 @@ function formatBuildStats(stats) {
|
|
|
110
114
|
/**
|
|
111
115
|
* Builds the static site by processing content files and generating HTML pages.
|
|
112
116
|
* This is the main entry point for Stati's build process.
|
|
117
|
+
* Uses build locking to prevent concurrent builds from corrupting cache.
|
|
113
118
|
*
|
|
114
119
|
* @param options - Build configuration options
|
|
115
120
|
*
|
|
@@ -131,8 +136,21 @@ function formatBuildStats(stats) {
|
|
|
131
136
|
* @throws {Error} When configuration loading fails
|
|
132
137
|
* @throws {Error} When content processing fails
|
|
133
138
|
* @throws {Error} When template rendering fails
|
|
139
|
+
* @throws {Error} When build lock cannot be acquired
|
|
134
140
|
*/
|
|
135
141
|
export async function build(options = {}) {
|
|
142
|
+
const cacheDir = join(process.cwd(), '.stati');
|
|
143
|
+
// Use build lock to prevent concurrent builds, with force option to override
|
|
144
|
+
return await withBuildLock(cacheDir, () => buildInternal(options), {
|
|
145
|
+
force: Boolean(options.force || options.clean), // Allow force if user explicitly requests it
|
|
146
|
+
timeout: 60000, // 1 minute timeout
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Internal build implementation without locking.
|
|
151
|
+
* Separated for cleaner error handling and testing.
|
|
152
|
+
*/
|
|
153
|
+
async function buildInternal(options = {}) {
|
|
136
154
|
const buildStartTime = Date.now();
|
|
137
155
|
const logger = options.logger || defaultLogger;
|
|
138
156
|
logger.building('Building your site...');
|
|
@@ -143,9 +161,22 @@ export async function build(options = {}) {
|
|
|
143
161
|
// Create .stati cache directory
|
|
144
162
|
const cacheDir = join(process.cwd(), '.stati');
|
|
145
163
|
await ensureDir(cacheDir);
|
|
164
|
+
// Load cache manifest for ISG
|
|
165
|
+
let cacheManifest = await loadCacheManifest(cacheDir);
|
|
166
|
+
// If no cache manifest exists, create an empty one
|
|
167
|
+
if (!cacheManifest) {
|
|
168
|
+
cacheManifest = {
|
|
169
|
+
entries: {},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
// At this point cacheManifest is guaranteed to be non-null
|
|
173
|
+
const manifest = cacheManifest;
|
|
174
|
+
// Initialize cache stats
|
|
175
|
+
let cacheHits = 0;
|
|
176
|
+
let cacheMisses = 0;
|
|
146
177
|
// Clean output directory if requested
|
|
147
178
|
if (options.clean) {
|
|
148
|
-
logger.info('
|
|
179
|
+
logger.info('Cleaning output directory...');
|
|
149
180
|
await remove(outDir);
|
|
150
181
|
}
|
|
151
182
|
await ensureDir(outDir);
|
|
@@ -158,7 +189,7 @@ export async function build(options = {}) {
|
|
|
158
189
|
logger.step(1, 3, 'Building navigation');
|
|
159
190
|
}
|
|
160
191
|
const navigation = buildNavigation(pages);
|
|
161
|
-
logger.info(
|
|
192
|
+
logger.info(`Built navigation with ${navigation.length} top-level items`);
|
|
162
193
|
// Display navigation tree if the logger supports it
|
|
163
194
|
if (logger.navigationTree) {
|
|
164
195
|
logger.navigationTree(navigation);
|
|
@@ -171,30 +202,28 @@ export async function build(options = {}) {
|
|
|
171
202
|
if (config.hooks?.beforeAll) {
|
|
172
203
|
await config.hooks.beforeAll(buildContext);
|
|
173
204
|
}
|
|
174
|
-
// Render each page with progress tracking
|
|
205
|
+
// Render each page with tree-based progress tracking and ISG
|
|
175
206
|
if (logger.step) {
|
|
176
207
|
logger.step(2, 3, 'Rendering pages');
|
|
177
208
|
}
|
|
209
|
+
// Initialize rendering tree
|
|
210
|
+
if (logger.startRenderingTree) {
|
|
211
|
+
logger.startRenderingTree('Page Rendering Process');
|
|
212
|
+
}
|
|
213
|
+
const buildTime = new Date();
|
|
178
214
|
for (let i = 0; i < pages.length; i++) {
|
|
179
215
|
const page = pages[i];
|
|
180
216
|
if (!page)
|
|
181
217
|
continue; // Safety check
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
218
|
+
const pageId = `page-${i}`;
|
|
219
|
+
// Add page to rendering tree
|
|
220
|
+
if (logger.addTreeNode) {
|
|
221
|
+
logger.addTreeNode('root', pageId, page.url, 'running', { url: page.url });
|
|
185
222
|
}
|
|
186
223
|
else {
|
|
187
|
-
logger.processing(`
|
|
224
|
+
logger.processing(`Checking ${page.url}`);
|
|
188
225
|
}
|
|
189
|
-
//
|
|
190
|
-
if (config.hooks?.beforeRender) {
|
|
191
|
-
await config.hooks.beforeRender({ page, config });
|
|
192
|
-
}
|
|
193
|
-
// Render markdown to HTML
|
|
194
|
-
const htmlContent = renderMarkdown(page.content, md);
|
|
195
|
-
// Render with template
|
|
196
|
-
const finalHtml = await renderPage(page, htmlContent, config, eta, navigation, pages);
|
|
197
|
-
// Determine output path - fix the logic here
|
|
226
|
+
// Determine output path
|
|
198
227
|
let outputPath;
|
|
199
228
|
if (page.url === '/') {
|
|
200
229
|
outputPath = join(outDir, 'index.html');
|
|
@@ -205,14 +234,78 @@ export async function build(options = {}) {
|
|
|
205
234
|
else {
|
|
206
235
|
outputPath = join(outDir, `${page.url}.html`);
|
|
207
236
|
}
|
|
237
|
+
// Get cache key (use output path relative to outDir)
|
|
238
|
+
const relativePath = relative(outDir, outputPath).replace(/\\/g, '/');
|
|
239
|
+
const cacheKey = relativePath.startsWith('/') ? relativePath : `/${relativePath}`;
|
|
240
|
+
const existingEntry = manifest.entries[cacheKey];
|
|
241
|
+
// Check if we should rebuild this page (considering ISG logic)
|
|
242
|
+
const shouldRebuild = options.force || (await shouldRebuildPage(page, existingEntry, config, buildTime));
|
|
243
|
+
if (!shouldRebuild) {
|
|
244
|
+
// Cache hit - skip rendering
|
|
245
|
+
cacheHits++;
|
|
246
|
+
if (logger.updateTreeNode) {
|
|
247
|
+
logger.updateTreeNode(pageId, 'cached', { cacheHit: true, url: page.url });
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
logger.processing(`đ Cached ${page.url}`);
|
|
251
|
+
}
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
// Cache miss - need to rebuild
|
|
255
|
+
cacheMisses++;
|
|
256
|
+
const startTime = Date.now();
|
|
257
|
+
// Add rendering substeps to tree
|
|
258
|
+
const markdownId = `${pageId}-markdown`;
|
|
259
|
+
const templateId = `${pageId}-template`;
|
|
260
|
+
if (logger.addTreeNode) {
|
|
261
|
+
logger.addTreeNode(pageId, markdownId, 'Processing Markdown', 'running');
|
|
262
|
+
logger.addTreeNode(pageId, templateId, 'Applying Template', 'pending');
|
|
263
|
+
}
|
|
264
|
+
// Run beforeRender hook
|
|
265
|
+
if (config.hooks?.beforeRender) {
|
|
266
|
+
await config.hooks.beforeRender({ page, config });
|
|
267
|
+
}
|
|
268
|
+
// Render markdown to HTML
|
|
269
|
+
const htmlContent = renderMarkdown(page.content, md);
|
|
270
|
+
if (logger.updateTreeNode) {
|
|
271
|
+
logger.updateTreeNode(markdownId, 'completed');
|
|
272
|
+
logger.updateTreeNode(templateId, 'running');
|
|
273
|
+
}
|
|
274
|
+
// Render with template
|
|
275
|
+
const finalHtml = await renderPage(page, htmlContent, config, eta, navigation, pages);
|
|
276
|
+
const renderTime = Date.now() - startTime;
|
|
277
|
+
if (logger.updateTreeNode) {
|
|
278
|
+
logger.updateTreeNode(templateId, 'completed');
|
|
279
|
+
logger.updateTreeNode(pageId, 'completed', {
|
|
280
|
+
timing: renderTime,
|
|
281
|
+
url: page.url,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
208
284
|
// Ensure directory exists and write file
|
|
209
285
|
await ensureDir(dirname(outputPath));
|
|
210
286
|
await writeFile(outputPath, finalHtml, 'utf-8');
|
|
287
|
+
// Update cache manifest
|
|
288
|
+
if (existingEntry) {
|
|
289
|
+
manifest.entries[cacheKey] = await updateCacheEntry(existingEntry, page, config, buildTime);
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
manifest.entries[cacheKey] = await createCacheEntry(page, config, buildTime);
|
|
293
|
+
}
|
|
211
294
|
// Run afterRender hook
|
|
212
295
|
if (config.hooks?.afterRender) {
|
|
213
296
|
await config.hooks.afterRender({ page, config });
|
|
214
297
|
}
|
|
215
298
|
}
|
|
299
|
+
// Display final rendering tree and clear it
|
|
300
|
+
if (logger.showRenderingTree) {
|
|
301
|
+
console.log(); // Add spacing
|
|
302
|
+
logger.showRenderingTree();
|
|
303
|
+
if (logger.clearRenderingTree) {
|
|
304
|
+
logger.clearRenderingTree();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// Save updated cache manifest
|
|
308
|
+
await saveCacheManifest(cacheDir, manifest);
|
|
216
309
|
// Copy static assets and count them
|
|
217
310
|
let assetsCount = 0;
|
|
218
311
|
const staticDir = join(process.cwd(), config.staticDir);
|
|
@@ -221,9 +314,9 @@ export async function build(options = {}) {
|
|
|
221
314
|
if (logger.step) {
|
|
222
315
|
logger.step(3, 3, 'Copying static assets');
|
|
223
316
|
}
|
|
224
|
-
logger.info(
|
|
317
|
+
logger.info(`Copying static assets from ${config.staticDir}`);
|
|
225
318
|
assetsCount = await copyStaticAssetsWithLogging(staticDir, outDir, logger);
|
|
226
|
-
logger.info(
|
|
319
|
+
logger.info(`Copied ${assetsCount} static assets`);
|
|
227
320
|
}
|
|
228
321
|
// Run afterAll hook
|
|
229
322
|
if (config.hooks?.afterAll) {
|
|
@@ -236,9 +329,9 @@ export async function build(options = {}) {
|
|
|
236
329
|
assetsCount,
|
|
237
330
|
buildTimeMs: buildEndTime - buildStartTime,
|
|
238
331
|
outputSizeBytes: await getDirectorySize(outDir),
|
|
239
|
-
//
|
|
240
|
-
cacheHits
|
|
241
|
-
cacheMisses
|
|
332
|
+
// Include ISG cache statistics
|
|
333
|
+
cacheHits,
|
|
334
|
+
cacheMisses,
|
|
242
335
|
};
|
|
243
336
|
console.log(); // Add spacing before statistics
|
|
244
337
|
// Use table format if available, otherwise fall back to formatted string
|
package/dist/core/dev.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/core/dev.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/core/dev.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,aAAa,CAAC;AAMvD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC,CA0ZxF"}
|
package/dist/core/dev.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { createServer } from 'http';
|
|
2
2
|
import { join, extname } from 'path';
|
|
3
|
+
import { posix } from 'path';
|
|
3
4
|
import { readFile, stat } from 'fs/promises';
|
|
4
5
|
import { WebSocketServer } from 'ws';
|
|
5
6
|
import chokidar from 'chokidar';
|
|
6
7
|
import { build } from './build.js';
|
|
7
8
|
import { loadConfig } from '../config/loader.js';
|
|
9
|
+
import { loadCacheManifest, saveCacheManifest } from './isg/manifest.js';
|
|
8
10
|
/**
|
|
9
11
|
* Creates and configures a development server with live reload functionality.
|
|
10
12
|
*
|
|
@@ -47,7 +49,6 @@ export async function createDevServer(options = {}) {
|
|
|
47
49
|
* Performs an initial build to ensure dist/ exists
|
|
48
50
|
*/
|
|
49
51
|
async function initialBuild() {
|
|
50
|
-
logger.info?.('đī¸ Performing initial build...');
|
|
51
52
|
try {
|
|
52
53
|
await build({
|
|
53
54
|
logger,
|
|
@@ -55,7 +56,6 @@ export async function createDevServer(options = {}) {
|
|
|
55
56
|
clean: false,
|
|
56
57
|
...(configPath && { configPath }),
|
|
57
58
|
});
|
|
58
|
-
logger.success?.('â
Initial build complete');
|
|
59
59
|
}
|
|
60
60
|
catch (error) {
|
|
61
61
|
logger.error?.(`Initial build failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -63,7 +63,7 @@ export async function createDevServer(options = {}) {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
|
-
* Performs incremental rebuild when files change
|
|
66
|
+
* Performs incremental rebuild when files change, using ISG logic for smart rebuilds
|
|
67
67
|
*/
|
|
68
68
|
async function incrementalRebuild(changedPath) {
|
|
69
69
|
if (isBuilding) {
|
|
@@ -71,14 +71,22 @@ export async function createDevServer(options = {}) {
|
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
73
73
|
isBuilding = true;
|
|
74
|
-
logger.info?.(`đ Rebuilding due to change: ${changedPath}`);
|
|
75
74
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
75
|
+
// Check if the changed file is a template/partial
|
|
76
|
+
if (changedPath.endsWith('.eta') || changedPath.includes('_partials')) {
|
|
77
|
+
logger.info?.(`đ¨ Template changed: ${changedPath}`);
|
|
78
|
+
await handleTemplateChange(changedPath);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// Content or static file changed - use normal rebuild
|
|
82
|
+
logger.info?.(`đ Content changed: ${changedPath}`);
|
|
83
|
+
await build({
|
|
84
|
+
logger,
|
|
85
|
+
force: false,
|
|
86
|
+
clean: false,
|
|
87
|
+
...(configPath && { configPath }),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
82
90
|
// Notify all connected clients to reload
|
|
83
91
|
if (wsServer) {
|
|
84
92
|
wsServer.clients.forEach((client) => {
|
|
@@ -89,7 +97,7 @@ export async function createDevServer(options = {}) {
|
|
|
89
97
|
}
|
|
90
98
|
});
|
|
91
99
|
}
|
|
92
|
-
logger.success?.('
|
|
100
|
+
logger.success?.('Rebuild complete');
|
|
93
101
|
}
|
|
94
102
|
catch (error) {
|
|
95
103
|
logger.error?.(`Rebuild failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -98,6 +106,62 @@ export async function createDevServer(options = {}) {
|
|
|
98
106
|
isBuilding = false;
|
|
99
107
|
}
|
|
100
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Handles template/partial file changes by invalidating affected pages
|
|
111
|
+
*/
|
|
112
|
+
async function handleTemplateChange(templatePath) {
|
|
113
|
+
const cacheDir = join(process.cwd(), '.stati');
|
|
114
|
+
try {
|
|
115
|
+
// Load existing cache manifest
|
|
116
|
+
let cacheManifest = await loadCacheManifest(cacheDir);
|
|
117
|
+
if (!cacheManifest) {
|
|
118
|
+
// No cache exists, perform full rebuild
|
|
119
|
+
logger.info?.('No cache found, performing full rebuild...');
|
|
120
|
+
await build({
|
|
121
|
+
logger,
|
|
122
|
+
force: false,
|
|
123
|
+
clean: false,
|
|
124
|
+
...(configPath && { configPath }),
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Find pages that depend on this template
|
|
129
|
+
const affectedPages = [];
|
|
130
|
+
for (const [pagePath, entry] of Object.entries(cacheManifest.entries)) {
|
|
131
|
+
if (entry.deps.some((dep) => dep.includes(posix.normalize(templatePath.replace(/\\/g, '/'))))) {
|
|
132
|
+
affectedPages.push(pagePath);
|
|
133
|
+
// Remove from cache to force rebuild
|
|
134
|
+
delete cacheManifest.entries[pagePath];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (affectedPages.length > 0) {
|
|
138
|
+
logger.info?.(`đ¯ Invalidating ${affectedPages.length} affected pages:`);
|
|
139
|
+
affectedPages.forEach((page) => logger.info?.(` đ ${page}`));
|
|
140
|
+
// Save updated cache manifest
|
|
141
|
+
await saveCacheManifest(cacheDir, cacheManifest);
|
|
142
|
+
// Perform incremental rebuild (only affected pages will be rebuilt)
|
|
143
|
+
await build({
|
|
144
|
+
logger,
|
|
145
|
+
force: false,
|
|
146
|
+
clean: false,
|
|
147
|
+
...(configPath && { configPath }),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
logger.info?.('âšī¸ No pages affected by template change');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
logger.warning?.(`Template dependency analysis failed, performing full rebuild: ${error instanceof Error ? error.message : String(error)}`);
|
|
156
|
+
// Fallback to full rebuild
|
|
157
|
+
await build({
|
|
158
|
+
logger,
|
|
159
|
+
force: false,
|
|
160
|
+
clean: false,
|
|
161
|
+
...(configPath && { configPath }),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
101
165
|
/**
|
|
102
166
|
* Gets MIME type for a file based on its extension
|
|
103
167
|
*/
|
|
@@ -133,15 +197,15 @@ export async function createDevServer(options = {}) {
|
|
|
133
197
|
ws.onmessage = function(event) {
|
|
134
198
|
const data = JSON.parse(event.data);
|
|
135
199
|
if (data.type === 'reload') {
|
|
136
|
-
console.log('
|
|
200
|
+
console.log('Reloading page due to file changes...');
|
|
137
201
|
window.location.reload();
|
|
138
202
|
}
|
|
139
203
|
};
|
|
140
204
|
ws.onopen = function() {
|
|
141
|
-
console.log('
|
|
205
|
+
console.log('Connected to Stati dev server');
|
|
142
206
|
};
|
|
143
207
|
ws.onclose = function() {
|
|
144
|
-
console.log('
|
|
208
|
+
console.log('Lost connection to Stati dev server');
|
|
145
209
|
// Try to reconnect after a delay
|
|
146
210
|
setTimeout(() => window.location.reload(), 1000);
|
|
147
211
|
};
|
|
@@ -241,7 +305,7 @@ export async function createDevServer(options = {}) {
|
|
|
241
305
|
logger.info?.('đ Browser connected for live reload');
|
|
242
306
|
const websocket = ws;
|
|
243
307
|
websocket.on('close', () => {
|
|
244
|
-
logger.info?.('
|
|
308
|
+
logger.info?.('Browser disconnected from live reload');
|
|
245
309
|
});
|
|
246
310
|
});
|
|
247
311
|
// Start HTTP server
|
|
@@ -269,9 +333,11 @@ export async function createDevServer(options = {}) {
|
|
|
269
333
|
watcher.on('unlink', (path) => {
|
|
270
334
|
void incrementalRebuild(path);
|
|
271
335
|
});
|
|
272
|
-
logger.success?.(
|
|
273
|
-
logger.info?.(
|
|
274
|
-
logger.info?.(
|
|
336
|
+
logger.success?.(`Dev server running at ${url}`);
|
|
337
|
+
logger.info?.(`\nServing from:`);
|
|
338
|
+
logger.info?.(` đ ${outDir}`);
|
|
339
|
+
logger.info?.('Watching:');
|
|
340
|
+
watchPaths.forEach((path) => logger.info?.(` đ ${path}`));
|
|
275
341
|
// Open browser if requested
|
|
276
342
|
if (open) {
|
|
277
343
|
try {
|
|
@@ -1,2 +1,68 @@
|
|
|
1
|
-
|
|
1
|
+
import type { CacheEntry } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Invalidation result containing affected cache entries.
|
|
4
|
+
*/
|
|
5
|
+
export interface InvalidationResult {
|
|
6
|
+
/** Number of cache entries invalidated */
|
|
7
|
+
invalidatedCount: number;
|
|
8
|
+
/** Paths of invalidated pages */
|
|
9
|
+
invalidatedPaths: string[];
|
|
10
|
+
/** Whether the entire cache was cleared */
|
|
11
|
+
clearedAll: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Parses an invalidation query string into individual query terms.
|
|
15
|
+
* Supports space-separated values and quoted strings.
|
|
16
|
+
*
|
|
17
|
+
* @param query - The query string to parse
|
|
18
|
+
* @returns Array of parsed query terms
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* parseInvalidationQuery('tag:blog path:/posts') // ['tag:blog', 'path:/posts']
|
|
23
|
+
* parseInvalidationQuery('"tag:my tag" path:"/my path"') // ['tag:my tag', 'path:/my path']
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseInvalidationQuery(query: string): string[];
|
|
27
|
+
/**
|
|
28
|
+
* Checks if a cache entry matches a specific invalidation term.
|
|
29
|
+
*
|
|
30
|
+
* @param entry - Cache entry to check
|
|
31
|
+
* @param path - The page path for this entry
|
|
32
|
+
* @param term - Invalidation term to match against
|
|
33
|
+
* @returns True if the entry matches the term
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* matchesInvalidationTerm(entry, '/blog/post-1', 'tag:blog') // true if entry has 'blog' tag
|
|
38
|
+
* matchesInvalidationTerm(entry, '/blog/post-1', 'path:/blog') // true (path prefix match)
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function matchesInvalidationTerm(entry: CacheEntry, path: string, term: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Invalidates cache entries based on a query string.
|
|
44
|
+
* Supports tag-based, path-based, pattern-based, and time-based invalidation.
|
|
45
|
+
*
|
|
46
|
+
* @param query - Invalidation query string, or undefined to clear all cache
|
|
47
|
+
* @returns Promise resolving to invalidation result
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* // Invalidate all pages with 'blog' tag
|
|
52
|
+
* await invalidate('tag:blog');
|
|
53
|
+
*
|
|
54
|
+
* // Invalidate specific path
|
|
55
|
+
* await invalidate('path:/about');
|
|
56
|
+
*
|
|
57
|
+
* // Invalidate content younger than 3 months
|
|
58
|
+
* await invalidate('age:3months');
|
|
59
|
+
*
|
|
60
|
+
* // Invalidate multiple criteria
|
|
61
|
+
* await invalidate('tag:blog age:1week');
|
|
62
|
+
*
|
|
63
|
+
* // Clear entire cache
|
|
64
|
+
* await invalidate();
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export declare function invalidate(query?: string): Promise<InvalidationResult>;
|
|
2
68
|
//# sourceMappingURL=invalidate.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"invalidate.d.ts","sourceRoot":"","sources":["../../src/core/invalidate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"invalidate.d.ts","sourceRoot":"","sources":["../../src/core/invalidate.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,0CAA0C;IAC1C,gBAAgB,EAAE,MAAM,CAAC;IACzB,iCAAiC;IACjC,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,2CAA2C;IAC3C,UAAU,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAkC9D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAkC9F;AAiLD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAoD5E"}
|