@stati/core 1.20.3 → 1.22.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/dist/config/loader.d.ts +4 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +49 -4
- package/dist/core/build.d.ts +6 -0
- package/dist/core/build.d.ts.map +1 -1
- package/dist/core/build.js +176 -66
- package/dist/core/dev.d.ts.map +1 -1
- package/dist/core/dev.js +100 -28
- package/dist/core/isg/builder.d.ts +4 -1
- package/dist/core/isg/builder.d.ts.map +1 -1
- package/dist/core/isg/builder.js +89 -2
- package/dist/core/isg/deps.d.ts +5 -0
- package/dist/core/isg/deps.d.ts.map +1 -1
- package/dist/core/isg/deps.js +38 -3
- package/dist/core/isg/dev-server-lock.d.ts +85 -0
- package/dist/core/isg/dev-server-lock.d.ts.map +1 -0
- package/dist/core/isg/dev-server-lock.js +248 -0
- package/dist/core/isg/hash.d.ts +4 -0
- package/dist/core/isg/hash.d.ts.map +1 -1
- package/dist/core/isg/hash.js +24 -1
- package/dist/core/isg/index.d.ts +3 -2
- package/dist/core/isg/index.d.ts.map +1 -1
- package/dist/core/isg/index.js +3 -2
- package/dist/core/markdown.d.ts +6 -0
- package/dist/core/markdown.d.ts.map +1 -1
- package/dist/core/markdown.js +23 -0
- package/dist/core/preview.js +1 -1
- package/dist/core/templates.js +5 -5
- package/dist/core/utils/bundle-matching.utils.d.ts +2 -0
- package/dist/core/utils/bundle-matching.utils.d.ts.map +1 -1
- package/dist/core/utils/index.d.ts +1 -1
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +1 -1
- package/dist/core/utils/logger.utils.d.ts.map +1 -1
- package/dist/core/utils/logger.utils.js +1 -0
- package/dist/core/utils/partial-validation.utils.js +2 -2
- package/dist/core/utils/paths.utils.d.ts +18 -0
- package/dist/core/utils/paths.utils.d.ts.map +1 -1
- package/dist/core/utils/paths.utils.js +23 -0
- package/dist/core/utils/tailwind-inventory.utils.d.ts +1 -16
- package/dist/core/utils/tailwind-inventory.utils.d.ts.map +1 -1
- package/dist/core/utils/tailwind-inventory.utils.js +35 -3
- package/dist/core/utils/typescript.utils.d.ts +13 -0
- package/dist/core/utils/typescript.utils.d.ts.map +1 -1
- package/dist/core/utils/typescript.utils.js +82 -3
- package/dist/env.d.ts +45 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +51 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/metrics/index.d.ts +1 -1
- package/dist/metrics/index.d.ts.map +1 -1
- package/dist/metrics/index.js +2 -0
- package/dist/metrics/recorder.d.ts.map +1 -1
- package/dist/metrics/types.d.ts +31 -0
- package/dist/metrics/types.d.ts.map +1 -1
- package/dist/metrics/utils/html-report.utils.d.ts +24 -0
- package/dist/metrics/utils/html-report.utils.d.ts.map +1 -0
- package/dist/metrics/utils/html-report.utils.js +1547 -0
- package/dist/metrics/utils/index.d.ts +1 -0
- package/dist/metrics/utils/index.d.ts.map +1 -1
- package/dist/metrics/utils/index.js +2 -0
- package/dist/metrics/utils/writer.utils.d.ts +6 -2
- package/dist/metrics/utils/writer.utils.d.ts.map +1 -1
- package/dist/metrics/utils/writer.utils.js +20 -4
- package/dist/search/generator.d.ts +1 -9
- package/dist/search/generator.d.ts.map +1 -1
- package/dist/search/generator.js +26 -2
- package/dist/seo/generator.d.ts.map +1 -1
- package/dist/seo/generator.js +1 -0
- package/dist/seo/utils/escape-and-validation.utils.d.ts.map +1 -1
- package/dist/seo/utils/escape-and-validation.utils.js +1 -16
- package/dist/types/logging.d.ts +31 -12
- package/dist/types/logging.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/core/dev.js
CHANGED
|
@@ -6,7 +6,7 @@ import chokidar from 'chokidar';
|
|
|
6
6
|
import { build } from './build.js';
|
|
7
7
|
import { invalidate } from './invalidate.js';
|
|
8
8
|
import { loadConfig } from '../config/loader.js';
|
|
9
|
-
import { loadCacheManifest, saveCacheManifest, computeNavigationHash } from './isg/index.js';
|
|
9
|
+
import { loadCacheManifest, saveCacheManifest, computeNavigationHash, DevServerLockManager, } from './isg/index.js';
|
|
10
10
|
import { loadContent } from './content.js';
|
|
11
11
|
import { buildNavigation } from './navigation.js';
|
|
12
12
|
import { resolveDevPaths, resolveCacheDir, resolvePrettyUrl, createErrorOverlay, parseErrorDetails, TemplateError, createFallbackLogger, mergeServerOptions, createTypeScriptWatcher, normalizePathForComparison, isPathWithinDirectory, } from './utils/index.js';
|
|
@@ -38,7 +38,7 @@ async function loadDevConfig(configPath, logger) {
|
|
|
38
38
|
async function performInitialBuild(configPath, logger, onError) {
|
|
39
39
|
try {
|
|
40
40
|
// Clear cache to ensure fresh build on dev server start
|
|
41
|
-
logger.
|
|
41
|
+
logger.status('Clearing cache for fresh development build...');
|
|
42
42
|
await invalidate();
|
|
43
43
|
await build({
|
|
44
44
|
logger,
|
|
@@ -87,15 +87,29 @@ async function performIncrementalRebuild(changedPath, eventType, configPath, sta
|
|
|
87
87
|
const startTime = Date.now();
|
|
88
88
|
// All changes being processed in this build (primary + batched)
|
|
89
89
|
const allChanges = [{ path: changedPath, eventType }, ...batchedChanges];
|
|
90
|
-
// Create a
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
90
|
+
// Create a dev logger that shows progress for template rebuilds
|
|
91
|
+
// but suppresses other verbose output
|
|
92
|
+
const createDevLogger = (showProgress) => {
|
|
93
|
+
const baseLogger = {
|
|
94
|
+
info: () => { }, // Suppress info messages
|
|
95
|
+
success: () => { }, // Suppress success messages
|
|
96
|
+
error: logger.error || (() => { }),
|
|
97
|
+
warning: logger.warning || (() => { }),
|
|
98
|
+
status: () => { }, // Suppress status messages
|
|
99
|
+
building: () => { }, // Suppress building messages
|
|
100
|
+
processing: () => { }, // Suppress processing messages
|
|
101
|
+
stats: () => { }, // Suppress stats messages
|
|
102
|
+
};
|
|
103
|
+
// Enable progress tracking for template rebuilds (but not the full summary)
|
|
104
|
+
if (showProgress && logger.startProgress && logger.updateProgress && logger.endProgress) {
|
|
105
|
+
return {
|
|
106
|
+
...baseLogger,
|
|
107
|
+
startProgress: logger.startProgress,
|
|
108
|
+
updateProgress: logger.updateProgress,
|
|
109
|
+
endProgress: logger.endProgress,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return baseLogger;
|
|
99
113
|
};
|
|
100
114
|
// Helper to check if a path is a static asset
|
|
101
115
|
const normalizedStaticDir = staticDir.replace(/\\/g, '/');
|
|
@@ -105,16 +119,22 @@ async function performIncrementalRebuild(changedPath, eventType, configPath, sta
|
|
|
105
119
|
};
|
|
106
120
|
// Helper to get relative path
|
|
107
121
|
const getRelativePath = (path) => path.replace(process.cwd(), '').replace(/\\/g, '/').replace(/^\//, '');
|
|
122
|
+
// Track template change result for better output
|
|
123
|
+
let templateChangeResult = null;
|
|
108
124
|
try {
|
|
109
125
|
// Check if the changed file is a template/partial
|
|
110
126
|
if (changedPath.endsWith(TEMPLATE_EXTENSION) || changedPath.includes('_partials')) {
|
|
111
|
-
|
|
127
|
+
// Use progress-enabled logger for template changes (rebuilds multiple pages)
|
|
128
|
+
const devLogger = createDevLogger(true);
|
|
129
|
+
templateChangeResult = await handleTemplateChange(changedPath, configPath, devLogger);
|
|
112
130
|
}
|
|
113
131
|
else if (changedPath.endsWith('.md')) {
|
|
132
|
+
const devLogger = createDevLogger(false);
|
|
114
133
|
await handleMarkdownChange(changedPath, configPath, devLogger);
|
|
115
134
|
}
|
|
116
135
|
else {
|
|
117
136
|
// Static file changed - use normal rebuild
|
|
137
|
+
const devLogger = createDevLogger(false);
|
|
118
138
|
await build({
|
|
119
139
|
logger: devLogger,
|
|
120
140
|
force: false,
|
|
@@ -137,7 +157,7 @@ async function performIncrementalRebuild(changedPath, eventType, configPath, sta
|
|
|
137
157
|
});
|
|
138
158
|
}
|
|
139
159
|
const duration = Date.now() - startTime;
|
|
140
|
-
// Log
|
|
160
|
+
// Log rebuild result with affected page info for template changes
|
|
141
161
|
for (const change of allChanges) {
|
|
142
162
|
const relativePath = getRelativePath(change.path);
|
|
143
163
|
let action;
|
|
@@ -150,14 +170,22 @@ async function performIncrementalRebuild(changedPath, eventType, configPath, sta
|
|
|
150
170
|
else {
|
|
151
171
|
action = 'rebuilt';
|
|
152
172
|
}
|
|
153
|
-
|
|
173
|
+
// For template changes, include affected page count
|
|
174
|
+
if (templateChangeResult && templateChangeResult.affectedPages > 0) {
|
|
175
|
+
const { affectedPages, totalPages } = templateChangeResult;
|
|
176
|
+
logger.info?.(`▸ ${relativePath} ${action}`);
|
|
177
|
+
logger.info?.(` ${affectedPages}/${totalPages} pages rebuilt in ${duration}ms`);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
logger.info?.(`▸ ${relativePath} ${action}`);
|
|
181
|
+
logger.info?.(` Done in ${duration}ms`);
|
|
182
|
+
}
|
|
154
183
|
}
|
|
155
|
-
logger.info?.(` Done in ${duration}ms`);
|
|
156
184
|
}
|
|
157
185
|
catch (error) {
|
|
158
186
|
const buildError = error instanceof Error ? error : new Error(String(error));
|
|
159
187
|
const duration = Date.now() - startTime;
|
|
160
|
-
logger.error?.(
|
|
188
|
+
logger.error?.(`× Rebuild failed after ${duration}ms: ${buildError.message}`);
|
|
161
189
|
// Store the error for display in browser
|
|
162
190
|
if (onError) {
|
|
163
191
|
onError(buildError);
|
|
@@ -183,6 +211,8 @@ async function performIncrementalRebuild(changedPath, eventType, configPath, sta
|
|
|
183
211
|
* Handles template/partial file changes by invalidating affected pages.
|
|
184
212
|
* Uses proper path normalization to ensure reliable matching between
|
|
185
213
|
* file watcher paths and cached dependency paths.
|
|
214
|
+
*
|
|
215
|
+
* @returns Object with affected and total page counts
|
|
186
216
|
*/
|
|
187
217
|
async function handleTemplateChange(templatePath, configPath, logger) {
|
|
188
218
|
const cacheDir = resolveCacheDir();
|
|
@@ -197,38 +227,50 @@ async function handleTemplateChange(templatePath, configPath, logger) {
|
|
|
197
227
|
clean: false,
|
|
198
228
|
...(configPath && { configPath }),
|
|
199
229
|
});
|
|
200
|
-
return;
|
|
230
|
+
return { affectedPages: 0, totalPages: 0 };
|
|
201
231
|
}
|
|
202
232
|
// Normalize the changed template path to absolute POSIX format for reliable comparison
|
|
203
233
|
// This handles cases where the watcher provides relative paths, Windows paths, or different
|
|
204
234
|
// path representations than what's stored in the cache manifest
|
|
235
|
+
// NOTE: Only used for string comparison, never for file system operations
|
|
205
236
|
const normalizedTemplatePath = normalizePathForComparison(templatePath);
|
|
237
|
+
// Get total page count from manifest
|
|
238
|
+
const totalPages = Object.keys(cacheManifest.entries).length;
|
|
206
239
|
// Find pages that depend on this template
|
|
207
240
|
let affectedPagesCount = 0;
|
|
208
241
|
for (const [pagePath, entry] of Object.entries(cacheManifest.entries)) {
|
|
209
242
|
// Check if any of the page's dependencies match the changed template
|
|
210
243
|
const hasMatchingDep = entry.deps.some((dep) => {
|
|
211
|
-
// Normalize the cached dependency path to the same format
|
|
244
|
+
// Normalize the cached dependency path to the same format for comparison only
|
|
212
245
|
const normalizedDep = normalizePathForComparison(dep);
|
|
213
246
|
// Direct path comparison - both paths are now in consistent format
|
|
214
247
|
return normalizedDep === normalizedTemplatePath;
|
|
215
248
|
});
|
|
216
249
|
if (hasMatchingDep) {
|
|
217
250
|
affectedPagesCount++;
|
|
218
|
-
//
|
|
219
|
-
|
|
251
|
+
// Mark entry as stale by invalidating its inputsHash
|
|
252
|
+
// This forces a rebuild while preserving deps for fast update optimization
|
|
253
|
+
cacheManifest.entries[pagePath] = {
|
|
254
|
+
...entry,
|
|
255
|
+
inputsHash: 'STALE', // Forces rebuild in shouldRebuildPage
|
|
256
|
+
};
|
|
220
257
|
}
|
|
221
258
|
}
|
|
222
259
|
if (affectedPagesCount > 0) {
|
|
223
260
|
// Save updated cache manifest
|
|
224
261
|
await saveCacheManifest(cacheDir, cacheManifest);
|
|
225
262
|
// Perform incremental rebuild (only affected pages will be rebuilt)
|
|
263
|
+
// skipManifestSave: true because we already saved the manifest above
|
|
264
|
+
// skipAssetCopy: true because template changes don't affect static assets
|
|
226
265
|
await build({
|
|
227
266
|
logger,
|
|
228
267
|
force: false,
|
|
229
268
|
clean: false,
|
|
269
|
+
skipManifestSave: true,
|
|
270
|
+
skipAssetCopy: true,
|
|
230
271
|
...(configPath && { configPath }),
|
|
231
272
|
});
|
|
273
|
+
return { affectedPages: affectedPagesCount, totalPages };
|
|
232
274
|
}
|
|
233
275
|
else {
|
|
234
276
|
// If no affected pages were found but a template changed,
|
|
@@ -240,6 +282,7 @@ async function handleTemplateChange(templatePath, configPath, logger) {
|
|
|
240
282
|
clean: false,
|
|
241
283
|
...(configPath && { configPath }),
|
|
242
284
|
});
|
|
285
|
+
return { affectedPages: totalPages, totalPages };
|
|
243
286
|
}
|
|
244
287
|
}
|
|
245
288
|
catch (_error) {
|
|
@@ -255,6 +298,8 @@ async function handleTemplateChange(templatePath, configPath, logger) {
|
|
|
255
298
|
catch (fallbackError) {
|
|
256
299
|
throw fallbackError instanceof Error ? fallbackError : new Error(String(fallbackError));
|
|
257
300
|
}
|
|
301
|
+
// Return zeros on error fallback
|
|
302
|
+
return { affectedPages: 0, totalPages: 0 };
|
|
258
303
|
}
|
|
259
304
|
}
|
|
260
305
|
/**
|
|
@@ -286,7 +331,7 @@ async function handleMarkdownChange(_markdownPath, configPath, logger) {
|
|
|
286
331
|
// Compare navigation hashes
|
|
287
332
|
if (newNavigationHash !== cacheManifest.navigationHash) {
|
|
288
333
|
// Navigation structure changed - clear cache and force full rebuild
|
|
289
|
-
logger.
|
|
334
|
+
logger.status('Navigation structure changed, performing full rebuild...');
|
|
290
335
|
// Force rebuild bypasses ISG cache entirely
|
|
291
336
|
await build({
|
|
292
337
|
logger,
|
|
@@ -336,6 +381,9 @@ export async function createDevServer(options = {}) {
|
|
|
336
381
|
},
|
|
337
382
|
});
|
|
338
383
|
setEnv('development');
|
|
384
|
+
// Create dev server lock manager
|
|
385
|
+
const cacheDir = resolveCacheDir();
|
|
386
|
+
const devLock = new DevServerLockManager(cacheDir);
|
|
339
387
|
const url = `http://${host}:${port}`;
|
|
340
388
|
let httpServer = null;
|
|
341
389
|
let wsServer = null;
|
|
@@ -386,7 +434,7 @@ export async function createDevServer(options = {}) {
|
|
|
386
434
|
ws.onmessage = function(event) {
|
|
387
435
|
const data = JSON.parse(event.data);
|
|
388
436
|
if (data.type === 'reload') {
|
|
389
|
-
console.log('
|
|
437
|
+
console.log('▸ Reloading page due to file changes...');
|
|
390
438
|
window.location.reload();
|
|
391
439
|
}
|
|
392
440
|
};
|
|
@@ -531,18 +579,40 @@ export async function createDevServer(options = {}) {
|
|
|
531
579
|
const devServer = {
|
|
532
580
|
url,
|
|
533
581
|
async start() {
|
|
582
|
+
// Acquire dev server lock to prevent multiple dev servers in the same directory
|
|
583
|
+
try {
|
|
584
|
+
await devLock.acquireLock();
|
|
585
|
+
}
|
|
586
|
+
catch (error) {
|
|
587
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
588
|
+
logger.error?.(`Failed to start dev server:\n${message}`);
|
|
589
|
+
throw error;
|
|
590
|
+
}
|
|
534
591
|
// Perform initial build
|
|
535
592
|
await performInitialBuild(configPath, logger, setLastBuildError);
|
|
536
593
|
// Create HTTP server
|
|
537
594
|
httpServer = createServer(async (req, res) => {
|
|
538
595
|
const requestPath = req.url || '/';
|
|
596
|
+
const requestStart = Date.now();
|
|
539
597
|
// Handle WebSocket upgrade path
|
|
540
598
|
if (requestPath === '/__ws') {
|
|
541
599
|
return; // Let WebSocket server handle this
|
|
542
600
|
}
|
|
543
|
-
|
|
601
|
+
// Only log page requests, not static assets (files with extensions)
|
|
602
|
+
const hasFileExtension = requestPath.includes('.') && !requestPath.endsWith('.html');
|
|
603
|
+
if (!hasFileExtension) {
|
|
604
|
+
logger.processing?.(`${req.method} ${requestPath}`);
|
|
605
|
+
}
|
|
544
606
|
try {
|
|
545
607
|
const { content, mimeType, statusCode } = await serveFile(requestPath);
|
|
608
|
+
const responseTime = Date.now() - requestStart;
|
|
609
|
+
// Log slow responses with memory info
|
|
610
|
+
if (responseTime > 2000) {
|
|
611
|
+
const mem = process.memoryUsage();
|
|
612
|
+
const heapMB = Math.round(mem.heapUsed / 1024 / 1024);
|
|
613
|
+
const rssMB = Math.round(mem.rss / 1024 / 1024);
|
|
614
|
+
logger.warning?.(`Slow response: ${requestPath} took ${responseTime}ms (heap: ${heapMB}MB, rss: ${rssMB}MB)`);
|
|
615
|
+
}
|
|
546
616
|
res.writeHead(statusCode, {
|
|
547
617
|
'Content-Type': mimeType,
|
|
548
618
|
'Access-Control-Allow-Origin': '*',
|
|
@@ -595,7 +665,7 @@ export async function createDevServer(options = {}) {
|
|
|
595
665
|
outDir: config.outDir || DEFAULT_OUT_DIR,
|
|
596
666
|
logger,
|
|
597
667
|
onRebuild: (_results, compileTimeMs) => {
|
|
598
|
-
logger.info?.(
|
|
668
|
+
logger.info?.(`▸ TypeScript recompiled in ${compileTimeMs}ms`);
|
|
599
669
|
// Broadcast reload to WebSocket clients
|
|
600
670
|
if (wsServer) {
|
|
601
671
|
wsServer.clients.forEach((client) => {
|
|
@@ -615,7 +685,7 @@ export async function createDevServer(options = {}) {
|
|
|
615
685
|
console.log();
|
|
616
686
|
logger.error?.(`TypeScript setup failed: ${tsError.message}`);
|
|
617
687
|
logger.warning?.('──────────────────────────────────────────────────────────────');
|
|
618
|
-
logger.warning?.('
|
|
688
|
+
logger.warning?.('! TypeScript hot reload is DISABLED for this session.');
|
|
619
689
|
logger.warning?.(" Dev server will continue, but TypeScript changes won't auto-reload.");
|
|
620
690
|
logger.warning?.(' Fix your TypeScript configuration and restart the dev server.');
|
|
621
691
|
logger.warning?.('──────────────────────────────────────────────────────────────');
|
|
@@ -638,7 +708,7 @@ export async function createDevServer(options = {}) {
|
|
|
638
708
|
});
|
|
639
709
|
cssWatcher.on('change', (path) => {
|
|
640
710
|
const relativePath = path.replace(process.cwd(), '').replace(/\\/g, '/').replace(/^\//, '');
|
|
641
|
-
logger.info?.(
|
|
711
|
+
logger.info?.(`▸ ${relativePath} updated`);
|
|
642
712
|
// Just notify clients to reload - no rebuild needed since CSS was already compiled
|
|
643
713
|
if (wsServer) {
|
|
644
714
|
wsServer.clients.forEach((client) => {
|
|
@@ -660,9 +730,9 @@ export async function createDevServer(options = {}) {
|
|
|
660
730
|
});
|
|
661
731
|
logger.success?.(`Dev server running at ${url}`);
|
|
662
732
|
logger.info?.(`\nServing:`);
|
|
663
|
-
logger.info?.(`
|
|
733
|
+
logger.info?.(` • ${outDir}`);
|
|
664
734
|
logger.info?.('Watching:');
|
|
665
|
-
watchPaths.forEach((path) => logger.info?.(`
|
|
735
|
+
watchPaths.forEach((path) => logger.info?.(` • ${path}`));
|
|
666
736
|
logger.info?.('');
|
|
667
737
|
// Open browser if requested
|
|
668
738
|
if (open) {
|
|
@@ -679,6 +749,8 @@ export async function createDevServer(options = {}) {
|
|
|
679
749
|
if (isStopping)
|
|
680
750
|
return;
|
|
681
751
|
isStopping = true;
|
|
752
|
+
// Release dev server lock first to allow other servers to start
|
|
753
|
+
await devLock.releaseLock();
|
|
682
754
|
if (watcher) {
|
|
683
755
|
await watcher.close();
|
|
684
756
|
watcher = null;
|
|
@@ -35,11 +35,14 @@ export declare function shouldRebuildPage(page: PageModel, entry: CacheEntry | u
|
|
|
35
35
|
export declare function createCacheEntry(page: PageModel, config: StatiConfig, renderedAt: Date): Promise<CacheEntry>;
|
|
36
36
|
/**
|
|
37
37
|
* Updates an existing cache entry with new information after rebuilding.
|
|
38
|
+
* In dev mode, this is optimized to reuse the existing deps array since
|
|
39
|
+
* template structure rarely changes between incremental rebuilds.
|
|
38
40
|
*
|
|
39
41
|
* @param entry - Existing cache entry
|
|
40
42
|
* @param page - The page model
|
|
41
43
|
* @param config - Stati configuration
|
|
42
44
|
* @param renderedAt - When the page was rendered
|
|
45
|
+
* @param fastUpdate - If true, skip expensive dep tracking (dev mode optimization)
|
|
43
46
|
* @returns Updated cache entry
|
|
44
47
|
*
|
|
45
48
|
* @example
|
|
@@ -47,5 +50,5 @@ export declare function createCacheEntry(page: PageModel, config: StatiConfig, r
|
|
|
47
50
|
* const updatedEntry = await updateCacheEntry(existingEntry, page, config, new Date());
|
|
48
51
|
* ```
|
|
49
52
|
*/
|
|
50
|
-
export declare function updateCacheEntry(entry: CacheEntry, page: PageModel, config: StatiConfig, renderedAt: Date): Promise<CacheEntry>;
|
|
53
|
+
export declare function updateCacheEntry(entry: CacheEntry, page: PageModel, config: StatiConfig, renderedAt: Date, fastUpdate?: boolean): Promise<CacheEntry>;
|
|
51
54
|
//# 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;
|
|
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;AAoE/E;;;;;;;;;;;;;;;;;;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,CAqFrB;AAkDD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,UAAU,EACjB,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,IAAI,EAChB,UAAU,UAAQ,GACjB,OAAO,CAAC,UAAU,CAAC,CA4CrB"}
|
package/dist/core/isg/builder.js
CHANGED
|
@@ -2,6 +2,7 @@ import { computeContentHash, computeFileHash, computeInputsHash } from './hash.j
|
|
|
2
2
|
import { trackTemplateDependencies } from './deps.js';
|
|
3
3
|
import { computeEffectiveTTL, computeNextRebuildAt, isPageFrozen } from './ttl.js';
|
|
4
4
|
import { validatePageISGOverrides, extractNumericOverride } from './validation.js';
|
|
5
|
+
import { performance } from 'node:perf_hooks';
|
|
5
6
|
/**
|
|
6
7
|
* Determines the output path for a page.
|
|
7
8
|
*/
|
|
@@ -214,13 +215,21 @@ export async function shouldRebuildPage(page, entry, config, now) {
|
|
|
214
215
|
* ```
|
|
215
216
|
*/
|
|
216
217
|
export async function createCacheEntry(page, config, renderedAt) {
|
|
218
|
+
const timings = {};
|
|
219
|
+
let start = performance.now();
|
|
217
220
|
// Validate page-level ISG overrides first
|
|
218
221
|
validatePageISGOverrides(page.frontMatter, page.sourcePath);
|
|
222
|
+
timings.validate = performance.now() - start;
|
|
219
223
|
// Compute content hash
|
|
224
|
+
start = performance.now();
|
|
220
225
|
const contentHash = computeContentHash(page.content, page.frontMatter);
|
|
226
|
+
timings.contentHash = performance.now() - start;
|
|
221
227
|
// Track all template dependencies
|
|
228
|
+
start = performance.now();
|
|
222
229
|
const deps = await trackTemplateDependencies(page, config);
|
|
230
|
+
timings.trackDeps = performance.now() - start;
|
|
223
231
|
// Compute hashes for all dependencies
|
|
232
|
+
start = performance.now();
|
|
224
233
|
const depsHashes = [];
|
|
225
234
|
for (const dep of deps) {
|
|
226
235
|
const depHash = await computeFileHash(dep);
|
|
@@ -228,6 +237,7 @@ export async function createCacheEntry(page, config, renderedAt) {
|
|
|
228
237
|
depsHashes.push(depHash);
|
|
229
238
|
}
|
|
230
239
|
}
|
|
240
|
+
timings.depsHashes = performance.now() - start;
|
|
231
241
|
const inputsHash = computeInputsHash(contentHash, depsHashes);
|
|
232
242
|
// Extract tags from front matter
|
|
233
243
|
let tags = [];
|
|
@@ -271,13 +281,60 @@ export async function createCacheEntry(page, config, renderedAt) {
|
|
|
271
281
|
}
|
|
272
282
|
return cacheEntry;
|
|
273
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* Checks if the template structure has changed by comparing the layout file's
|
|
286
|
+
* modification time against when the cache entry was last rendered.
|
|
287
|
+
* This detects when new includes/extends have been added to templates.
|
|
288
|
+
*
|
|
289
|
+
* @param entry - The existing cache entry
|
|
290
|
+
* @returns True if template structure may have changed, false if structure is unchanged
|
|
291
|
+
*/
|
|
292
|
+
async function hasTemplateStructureChanged(entry) {
|
|
293
|
+
try {
|
|
294
|
+
// If the entry has a layout in its deps, check if that layout file has been modified
|
|
295
|
+
// since the last render. Layout files are typically the first dep or contain the
|
|
296
|
+
// includes/extends declarations.
|
|
297
|
+
if (entry.deps.length === 0) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
// Get the timestamp of the last render
|
|
301
|
+
const lastRendered = new Date(entry.renderedAt).getTime();
|
|
302
|
+
// Import stat from node:fs/promises at runtime to avoid circular dependency issues
|
|
303
|
+
const { stat } = await import('node:fs/promises');
|
|
304
|
+
// Check if any template files (layout or partials) have been modified
|
|
305
|
+
// We only need to check the first few deps (usually layout and immediate includes)
|
|
306
|
+
// as a heuristic to detect structural changes without expensive full tracking
|
|
307
|
+
const depsToCheck = entry.deps.slice(0, 3);
|
|
308
|
+
for (const depPath of depsToCheck) {
|
|
309
|
+
try {
|
|
310
|
+
const stats = await stat(depPath);
|
|
311
|
+
if (stats.mtimeMs > lastRendered) {
|
|
312
|
+
// Template file was modified after last render - structure may have changed
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catch (_error) {
|
|
317
|
+
// If we can't stat the file, assume it might have changed
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
catch (_error) {
|
|
324
|
+
// On any error, be conservative and assume structure changed
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
274
328
|
/**
|
|
275
329
|
* Updates an existing cache entry with new information after rebuilding.
|
|
330
|
+
* In dev mode, this is optimized to reuse the existing deps array since
|
|
331
|
+
* template structure rarely changes between incremental rebuilds.
|
|
276
332
|
*
|
|
277
333
|
* @param entry - Existing cache entry
|
|
278
334
|
* @param page - The page model
|
|
279
335
|
* @param config - Stati configuration
|
|
280
336
|
* @param renderedAt - When the page was rendered
|
|
337
|
+
* @param fastUpdate - If true, skip expensive dep tracking (dev mode optimization)
|
|
281
338
|
* @returns Updated cache entry
|
|
282
339
|
*
|
|
283
340
|
* @example
|
|
@@ -285,8 +342,38 @@ export async function createCacheEntry(page, config, renderedAt) {
|
|
|
285
342
|
* const updatedEntry = await updateCacheEntry(existingEntry, page, config, new Date());
|
|
286
343
|
* ```
|
|
287
344
|
*/
|
|
288
|
-
export async function updateCacheEntry(entry, page, config, renderedAt) {
|
|
289
|
-
//
|
|
345
|
+
export async function updateCacheEntry(entry, page, config, renderedAt, fastUpdate = false) {
|
|
346
|
+
// In fast update mode (dev), reuse existing deps to avoid expensive tracking
|
|
347
|
+
if (fastUpdate && entry.deps.length > 0) {
|
|
348
|
+
// Detect if template structure has changed (new includes/extends added)
|
|
349
|
+
// by checking if the layout file has been modified or if template content
|
|
350
|
+
// contains different dependency patterns
|
|
351
|
+
const structureChanged = await hasTemplateStructureChanged(entry);
|
|
352
|
+
if (structureChanged) {
|
|
353
|
+
// Template structure changed - fall through to full dependency tracking
|
|
354
|
+
// This ensures we pick up new includes/extends
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
// Structure unchanged - safe to reuse existing deps
|
|
358
|
+
// Just update the content hash and timestamp, keep existing deps
|
|
359
|
+
const contentHash = computeContentHash(page.content, page.frontMatter);
|
|
360
|
+
// Compute hashes for existing dependencies (fast - files are cached)
|
|
361
|
+
const depsHashes = [];
|
|
362
|
+
for (const dep of entry.deps) {
|
|
363
|
+
const depHash = await computeFileHash(dep);
|
|
364
|
+
if (depHash) {
|
|
365
|
+
depsHashes.push(depHash);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const inputsHash = computeInputsHash(contentHash, depsHashes);
|
|
369
|
+
return {
|
|
370
|
+
...entry,
|
|
371
|
+
inputsHash,
|
|
372
|
+
renderedAt: renderedAt.toISOString(),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// Full update - create new entry and preserve original publishedAt
|
|
290
377
|
const newEntry = await createCacheEntry(page, config, renderedAt);
|
|
291
378
|
// Preserve original publishedAt if no new one is specified
|
|
292
379
|
if (!newEntry.publishedAt && entry.publishedAt) {
|
package/dist/core/isg/deps.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { PageModel, StatiConfig } from '../../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Clears the template path and content caches.
|
|
4
|
+
* Should be called at the start of each build.
|
|
5
|
+
*/
|
|
6
|
+
export declare function clearTemplatePathCache(): void;
|
|
2
7
|
/**
|
|
3
8
|
* Error thrown when a circular dependency is detected in templates.
|
|
4
9
|
*/
|
|
@@ -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;
|
|
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"}
|
package/dist/core/isg/deps.js
CHANGED
|
@@ -2,6 +2,24 @@ import { join, dirname, relative, posix } from 'node:path';
|
|
|
2
2
|
import { pathExists, readFile, isCollectionIndexPage, discoverLayout, resolveSrcDir, } from '../utils/index.js';
|
|
3
3
|
import glob from 'fast-glob';
|
|
4
4
|
import { TEMPLATE_EXTENSION } from '../../constants.js';
|
|
5
|
+
/**
|
|
6
|
+
* Per-build cache for template path resolution (glob results).
|
|
7
|
+
* Cleared at the start of each build via clearTemplatePathCache().
|
|
8
|
+
*/
|
|
9
|
+
const templatePathCache = new Map();
|
|
10
|
+
/**
|
|
11
|
+
* Per-build cache for template content reads.
|
|
12
|
+
* Cleared at the start of each build via clearTemplatePathCache().
|
|
13
|
+
*/
|
|
14
|
+
const templateContentCache = new Map();
|
|
15
|
+
/**
|
|
16
|
+
* Clears the template path and content caches.
|
|
17
|
+
* Should be called at the start of each build.
|
|
18
|
+
*/
|
|
19
|
+
export function clearTemplatePathCache() {
|
|
20
|
+
templatePathCache.clear();
|
|
21
|
+
templateContentCache.clear();
|
|
22
|
+
}
|
|
5
23
|
/**
|
|
6
24
|
* Error thrown when a circular dependency is detected in templates.
|
|
7
25
|
*/
|
|
@@ -132,8 +150,15 @@ async function collectTemplateDependencies(templatePath, srcDir, visited, curren
|
|
|
132
150
|
currentPath.add(normalizedPath);
|
|
133
151
|
visited.add(normalizedPath);
|
|
134
152
|
try {
|
|
135
|
-
// Read template content to find includes/extends
|
|
136
|
-
|
|
153
|
+
// Read template content to find includes/extends (use cache)
|
|
154
|
+
let content;
|
|
155
|
+
if (templateContentCache.has(normalizedPath)) {
|
|
156
|
+
content = templateContentCache.get(normalizedPath);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
content = await readFile(templatePath, 'utf-8');
|
|
160
|
+
templateContentCache.set(normalizedPath, content ?? null);
|
|
161
|
+
}
|
|
137
162
|
if (!content) {
|
|
138
163
|
return;
|
|
139
164
|
}
|
|
@@ -274,12 +299,19 @@ async function parseTemplateDependencies(content, templatePath, srcDir) {
|
|
|
274
299
|
* @returns Absolute path to template file, or null if not found
|
|
275
300
|
*/
|
|
276
301
|
async function resolveTemplatePathInternal(templateRef, srcDir, currentDir) {
|
|
302
|
+
// Create cache key from all inputs
|
|
303
|
+
const cacheKey = `${templateRef}|${srcDir}|${currentDir ?? ''}`;
|
|
304
|
+
// Check cache first
|
|
305
|
+
if (templatePathCache.has(cacheKey)) {
|
|
306
|
+
return templatePathCache.get(cacheKey) ?? null;
|
|
307
|
+
}
|
|
277
308
|
const templateName = templateRef.endsWith(TEMPLATE_EXTENSION)
|
|
278
309
|
? templateRef
|
|
279
310
|
: `${templateRef}${TEMPLATE_EXTENSION}`;
|
|
280
311
|
// Try absolute path from srcDir
|
|
281
312
|
const absolutePath = join(srcDir, templateName);
|
|
282
313
|
if (await pathExists(absolutePath)) {
|
|
314
|
+
templatePathCache.set(cacheKey, absolutePath);
|
|
283
315
|
return absolutePath;
|
|
284
316
|
}
|
|
285
317
|
// Determine the starting directory for hierarchical search (relative to srcDir)
|
|
@@ -318,7 +350,9 @@ async function resolveTemplatePathInternal(templateRef, srcDir, currentDir) {
|
|
|
318
350
|
const matches = await glob(pattern, { absolute: true });
|
|
319
351
|
if (matches.length > 0) {
|
|
320
352
|
// Normalize to POSIX format for consistent cross-platform path handling
|
|
321
|
-
|
|
353
|
+
const result = matches[0].replace(/\\/g, '/');
|
|
354
|
+
templatePathCache.set(cacheKey, result);
|
|
355
|
+
return result;
|
|
322
356
|
}
|
|
323
357
|
}
|
|
324
358
|
catch {
|
|
@@ -326,5 +360,6 @@ async function resolveTemplatePathInternal(templateRef, srcDir, currentDir) {
|
|
|
326
360
|
continue;
|
|
327
361
|
}
|
|
328
362
|
}
|
|
363
|
+
templatePathCache.set(cacheKey, null);
|
|
329
364
|
return null;
|
|
330
365
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev server lock information stored in the lock file.
|
|
3
|
+
*/
|
|
4
|
+
interface DevServerLock {
|
|
5
|
+
pid: number;
|
|
6
|
+
timestamp: string;
|
|
7
|
+
hostname?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Manages dev server locking to prevent multiple dev servers from running in the same directory.
|
|
11
|
+
* Uses a simple file-based locking mechanism with process ID tracking.
|
|
12
|
+
*/
|
|
13
|
+
export declare class DevServerLockManager {
|
|
14
|
+
private lockPath;
|
|
15
|
+
private isLocked;
|
|
16
|
+
private cleanupHandlersRegistered;
|
|
17
|
+
constructor(cacheDir: string);
|
|
18
|
+
/**
|
|
19
|
+
* Attempts to acquire a dev server lock.
|
|
20
|
+
* Throws an error if another dev server is already running.
|
|
21
|
+
*
|
|
22
|
+
* @throws {Error} When lock cannot be acquired
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const lockManager = new DevServerLockManager('.stati');
|
|
27
|
+
* try {
|
|
28
|
+
* await lockManager.acquireLock();
|
|
29
|
+
* // Start dev server
|
|
30
|
+
* } finally {
|
|
31
|
+
* await lockManager.releaseLock();
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
acquireLock(): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Releases the dev server lock if this process owns it.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* await lockManager.releaseLock();
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
releaseLock(): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Checks if a dev server lock is currently held by any process.
|
|
47
|
+
*
|
|
48
|
+
* @returns True if a lock exists and the owning process is running
|
|
49
|
+
*/
|
|
50
|
+
isLockHeld(): Promise<boolean>;
|
|
51
|
+
/**
|
|
52
|
+
* Gets information about the current lock holder.
|
|
53
|
+
*
|
|
54
|
+
* @returns Lock information or null if no lock exists
|
|
55
|
+
*/
|
|
56
|
+
getLockInfo(): Promise<DevServerLock | null>;
|
|
57
|
+
/**
|
|
58
|
+
* Force removes the lock file without checking ownership.
|
|
59
|
+
* Should only be used in error recovery scenarios.
|
|
60
|
+
*/
|
|
61
|
+
private forceRemoveLock;
|
|
62
|
+
/**
|
|
63
|
+
* Creates a new lock file with current process information.
|
|
64
|
+
*/
|
|
65
|
+
private createLockFile;
|
|
66
|
+
/**
|
|
67
|
+
* Reads and parses the lock file.
|
|
68
|
+
*/
|
|
69
|
+
private readLockFile;
|
|
70
|
+
/**
|
|
71
|
+
* Checks if a process with the given PID is currently running.
|
|
72
|
+
*/
|
|
73
|
+
private isProcessRunning;
|
|
74
|
+
/**
|
|
75
|
+
* Gets the hostname for lock identification.
|
|
76
|
+
*/
|
|
77
|
+
private getHostname;
|
|
78
|
+
/**
|
|
79
|
+
* Registers process exit handlers to ensure lock is released.
|
|
80
|
+
* This prevents accidentally leaving the lock file behind.
|
|
81
|
+
*/
|
|
82
|
+
private registerCleanupHandlers;
|
|
83
|
+
}
|
|
84
|
+
export {};
|
|
85
|
+
//# sourceMappingURL=dev-server-lock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-server-lock.d.ts","sourceRoot":"","sources":["../../../src/core/isg/dev-server-lock.ts"],"names":[],"mappings":"AAYA;;GAEG;AACH,UAAU,aAAa;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,yBAAyB,CAAS;gBAE9B,QAAQ,EAAE,MAAM;IAI5B;;;;;;;;;;;;;;;;OAgBG;IACG,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAgDlC;;;;;;;OAOG;IACG,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAqBlC;;;;OAIG;IACG,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC;IAiBpC;;;;OAIG;IACG,WAAW,IAAI,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAYlD;;;OAGG;YACW,eAAe;IAQ7B;;OAEG;YACW,cAAc;IAc5B;;OAEG;YACW,YAAY;IAY1B;;OAEG;YACW,gBAAgB;IAY9B;;OAEG;IACH,OAAO,CAAC,WAAW;IAQnB;;;OAGG;IACH,OAAO,CAAC,uBAAuB;CAkDhC"}
|