@stati/core 1.21.0 → 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.
Files changed (65) hide show
  1. package/dist/config/loader.d.ts +4 -0
  2. package/dist/config/loader.d.ts.map +1 -1
  3. package/dist/config/loader.js +49 -4
  4. package/dist/core/build.d.ts +6 -0
  5. package/dist/core/build.d.ts.map +1 -1
  6. package/dist/core/build.js +134 -24
  7. package/dist/core/dev.d.ts.map +1 -1
  8. package/dist/core/dev.js +86 -19
  9. package/dist/core/isg/builder.d.ts +4 -1
  10. package/dist/core/isg/builder.d.ts.map +1 -1
  11. package/dist/core/isg/builder.js +89 -2
  12. package/dist/core/isg/deps.d.ts +5 -0
  13. package/dist/core/isg/deps.d.ts.map +1 -1
  14. package/dist/core/isg/deps.js +38 -3
  15. package/dist/core/isg/dev-server-lock.d.ts +85 -0
  16. package/dist/core/isg/dev-server-lock.d.ts.map +1 -0
  17. package/dist/core/isg/dev-server-lock.js +248 -0
  18. package/dist/core/isg/hash.d.ts +4 -0
  19. package/dist/core/isg/hash.d.ts.map +1 -1
  20. package/dist/core/isg/hash.js +24 -1
  21. package/dist/core/isg/index.d.ts +3 -2
  22. package/dist/core/isg/index.d.ts.map +1 -1
  23. package/dist/core/isg/index.js +3 -2
  24. package/dist/core/markdown.d.ts +6 -0
  25. package/dist/core/markdown.d.ts.map +1 -1
  26. package/dist/core/markdown.js +23 -0
  27. package/dist/core/templates.js +5 -5
  28. package/dist/core/utils/index.d.ts +1 -1
  29. package/dist/core/utils/index.d.ts.map +1 -1
  30. package/dist/core/utils/index.js +1 -1
  31. package/dist/core/utils/partial-validation.utils.js +2 -2
  32. package/dist/core/utils/paths.utils.d.ts +18 -0
  33. package/dist/core/utils/paths.utils.d.ts.map +1 -1
  34. package/dist/core/utils/paths.utils.js +23 -0
  35. package/dist/core/utils/tailwind-inventory.utils.d.ts +1 -16
  36. package/dist/core/utils/tailwind-inventory.utils.d.ts.map +1 -1
  37. package/dist/core/utils/tailwind-inventory.utils.js +35 -3
  38. package/dist/core/utils/typescript.utils.d.ts +9 -0
  39. package/dist/core/utils/typescript.utils.d.ts.map +1 -1
  40. package/dist/core/utils/typescript.utils.js +41 -0
  41. package/dist/env.d.ts +45 -0
  42. package/dist/env.d.ts.map +1 -1
  43. package/dist/env.js +51 -0
  44. package/dist/index.d.ts +2 -2
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +2 -2
  47. package/dist/metrics/index.d.ts +1 -1
  48. package/dist/metrics/index.d.ts.map +1 -1
  49. package/dist/metrics/index.js +2 -0
  50. package/dist/metrics/recorder.d.ts.map +1 -1
  51. package/dist/metrics/types.d.ts +31 -0
  52. package/dist/metrics/types.d.ts.map +1 -1
  53. package/dist/metrics/utils/html-report.utils.d.ts +24 -0
  54. package/dist/metrics/utils/html-report.utils.d.ts.map +1 -0
  55. package/dist/metrics/utils/html-report.utils.js +1547 -0
  56. package/dist/metrics/utils/index.d.ts +1 -0
  57. package/dist/metrics/utils/index.d.ts.map +1 -1
  58. package/dist/metrics/utils/index.js +2 -0
  59. package/dist/metrics/utils/writer.utils.d.ts +6 -2
  60. package/dist/metrics/utils/writer.utils.d.ts.map +1 -1
  61. package/dist/metrics/utils/writer.utils.js +20 -4
  62. package/dist/search/generator.d.ts +1 -9
  63. package/dist/search/generator.d.ts.map +1 -1
  64. package/dist/search/generator.js +26 -2
  65. package/package.json +1 -1
@@ -1,4 +1,8 @@
1
1
  import type { StatiConfig } from '../types/index.js';
2
+ /**
3
+ * Clear the config cache. Useful when config file changes are detected.
4
+ */
5
+ export declare function clearConfigCache(): void;
2
6
  /**
3
7
  * Builds config file paths for a given directory.
4
8
  * @param cwd - Directory to search for config files
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAiCrD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAExD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,UAAU,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,WAAW,CAAC,CA0ElF"}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AA6BrD;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAqBD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAExD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,UAAU,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,WAAW,CAAC,CA6GlF"}
@@ -1,9 +1,20 @@
1
- import { existsSync } from 'node:fs';
1
+ import { existsSync, statSync } from 'node:fs';
2
2
  import { join, resolve } from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import { validateISGConfig, ISGConfigurationError } from '../core/isg/validation.js';
5
5
  import { compileStatiConfig, cleanupCompiledConfig } from '../core/utils/index.js';
6
6
  import { DEFAULT_SRC_DIR, DEFAULT_OUT_DIR, DEFAULT_STATIC_DIR, DEFAULT_SITE_TITLE, DEFAULT_DEV_BASE_URL, DEFAULT_TTL_SECONDS, DEFAULT_MAX_AGE_CAP_DAYS, CONFIG_FILE_PATTERNS, } from '../constants.js';
7
+ /**
8
+ * Config cache to prevent repeated dynamic imports during dev server rebuilds.
9
+ * Key is the resolved config file path.
10
+ */
11
+ const configCache = new Map();
12
+ /**
13
+ * Clear the config cache. Useful when config file changes are detected.
14
+ */
15
+ export function clearConfigCache() {
16
+ configCache.clear();
17
+ }
7
18
  /**
8
19
  * Default configuration values for Stati.
9
20
  * Used as fallback when no configuration file is found.
@@ -62,14 +73,35 @@ export async function loadConfig(cwd = process.cwd()) {
62
73
  if (!configPath) {
63
74
  return DEFAULT_CONFIG;
64
75
  }
76
+ const resolvedConfigPath = resolve(configPath);
77
+ // Check if we have a cached config and if it's still valid
78
+ const cached = configCache.get(resolvedConfigPath);
79
+ if (cached) {
80
+ try {
81
+ const currentMtime = statSync(resolvedConfigPath).mtimeMs;
82
+ if (currentMtime === cached.mtime) {
83
+ // Config file hasn't changed, return cached version
84
+ return cached.config;
85
+ }
86
+ // Config changed, clear this cache entry
87
+ configCache.delete(resolvedConfigPath);
88
+ }
89
+ catch {
90
+ // If we can't stat the file, clear cache and reload
91
+ configCache.delete(resolvedConfigPath);
92
+ }
93
+ }
65
94
  try {
66
95
  let configModule;
67
96
  let compiledPath;
97
+ // Get current mtime for cache-busting
98
+ const currentMtime = statSync(resolvedConfigPath).mtimeMs;
68
99
  // If it's a .ts file, compile it first
69
100
  if (configPath.endsWith('.ts')) {
70
101
  try {
71
- compiledPath = await compileStatiConfig(resolve(configPath));
72
- const configUrl = pathToFileURL(compiledPath).href;
102
+ compiledPath = await compileStatiConfig(resolvedConfigPath);
103
+ // Add cache-busting query parameter to force Node.js to re-evaluate the module
104
+ const configUrl = `${pathToFileURL(compiledPath).href}?t=${currentMtime}`;
73
105
  configModule = await import(configUrl);
74
106
  }
75
107
  finally {
@@ -81,7 +113,8 @@ export async function loadConfig(cwd = process.cwd()) {
81
113
  }
82
114
  else {
83
115
  // Existing logic for .js/.mjs
84
- const configUrl = pathToFileURL(resolve(configPath)).href;
116
+ // Add cache-busting query parameter to force Node.js to re-evaluate the module
117
+ const configUrl = `${pathToFileURL(resolvedConfigPath).href}?t=${currentMtime}`;
85
118
  configModule = await import(configUrl);
86
119
  }
87
120
  const userConfig = configModule.default || configModule;
@@ -106,6 +139,18 @@ export async function loadConfig(cwd = process.cwd()) {
106
139
  }
107
140
  throw error; // Re-throw non-ISG errors
108
141
  }
142
+ // Cache the loaded config with its modification time
143
+ try {
144
+ const mtime = statSync(resolvedConfigPath).mtimeMs;
145
+ configCache.set(resolvedConfigPath, {
146
+ config: mergedConfig,
147
+ configPath: resolvedConfigPath,
148
+ mtime,
149
+ });
150
+ }
151
+ catch {
152
+ // If we can't stat, still return the config but don't cache
153
+ }
109
154
  return mergedConfig;
110
155
  }
111
156
  catch (error) {
@@ -46,6 +46,12 @@ export interface BuildOptions {
46
46
  coreVersion?: string | undefined;
47
47
  /** Metrics collection options */
48
48
  metrics?: MetricsOptions | undefined;
49
+ /** Skip updating cache entries after render (dev mode optimization) */
50
+ skipCacheUpdate?: boolean | undefined;
51
+ /** Skip saving cache manifest at end of build (dev mode optimization when caller already saved it) */
52
+ skipManifestSave?: boolean | undefined;
53
+ /** Skip copying static assets (dev mode optimization for template-only changes) */
54
+ skipAssetCopy?: boolean | undefined;
49
55
  }
50
56
  /**
51
57
  * Extended build result including optional metrics.
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/core/build.ts"],"names":[],"mappings":"AAuDA,OAAO,KAAK,EAEV,UAAU,EACV,MAAM,EAQP,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGxE;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,gCAAgC;IAChC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,YAAY;IAC3B,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,uCAAuC;IACvC,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACpC,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,8BAA8B;IAC9B,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,+BAA+B;IAC/B,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,iCAAiC;IACjC,OAAO,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;CACtC;AAED;;GAEG;AACH,MAAM,WAAW,WAAY,SAAQ,UAAU;IAC7C,wDAAwD;IACxD,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAkGD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAW5E"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/core/build.ts"],"names":[],"mappings":"AA0DA,OAAO,KAAK,EAEV,UAAU,EACV,MAAM,EASP,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGxE;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,gCAAgC;IAChC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,YAAY;IAC3B,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,uCAAuC;IACvC,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACpC,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,8BAA8B;IAC9B,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,+BAA+B;IAC/B,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,iCAAiC;IACjC,OAAO,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;IACrC,uEAAuE;IACvE,eAAe,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACtC,sGAAsG;IACtG,gBAAgB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,mFAAmF;IACnF,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,WAAY,SAAQ,UAAU;IAC7C,wDAAwD;IACxD,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAkGD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAsB5E"}
@@ -1,4 +1,4 @@
1
- import { ensureDir, writeFile, remove, pathExists, stat, readdir, copyFile, resolveOutDir, resolveStaticDir, resolveCacheDir, enableInventoryTracking, disableInventoryTracking, clearInventory, writeTailwindClassInventory, getInventorySize, isTailwindUsed, loadPreviousInventory, compileTypeScript, autoInjectBundles, getBundlePathsForPage, formatBytes, } from './utils/index.js';
1
+ import { ensureDir, writeFile, remove, pathExists, stat, readdir, copyFile, resolveOutDir, resolveStaticDir, resolveCacheDir, enableInventoryTracking, disableInventoryTracking, clearInventory, writeTailwindClassInventory, getInventorySize, isTailwindUsed, loadPreviousInventory, compileTypeScript, detectExistingBundles, autoInjectBundles, getBundlePathsForPage, formatBytes, } from './utils/index.js';
2
2
  import { join, dirname, relative, posix } from 'node:path';
3
3
  import { performance } from 'node:perf_hooks';
4
4
  import { loadConfig } from '../config/loader.js';
@@ -6,11 +6,11 @@ import { loadContent } from './content.js';
6
6
  import { createMarkdownProcessor, renderMarkdown, extractToc } from './markdown.js';
7
7
  import { createTemplateEngine, renderPage } from './templates.js';
8
8
  import { buildNavigation } from './navigation.js';
9
- import { loadCacheManifest, saveCacheManifest, shouldRebuildPage, createCacheEntry, updateCacheEntry, withBuildLock, computeNavigationHash, } from './isg/index.js';
9
+ import { loadCacheManifest, saveCacheManifest, shouldRebuildPage, createCacheEntry, updateCacheEntry, withBuildLock, computeNavigationHash, clearFileHashCache, clearTemplatePathCache, } from './isg/index.js';
10
10
  import { generateSitemap, generateRobotsTxtFromConfig, autoInjectSEO, } from '../seo/index.js';
11
11
  import { generateRSSFeeds, validateRSSConfig } from '../rss/index.js';
12
12
  import { computeSearchIndexFilename, generateSearchIndex, writeSearchIndex, autoInjectSearchMeta, } from '../search/index.js';
13
- import { getEnv } from '../env.js';
13
+ import { getEnv, isDevelopment } from '../env.js';
14
14
  import { DEFAULT_OUT_DIR } from '../constants.js';
15
15
  import { createMetricRecorder, noopMetricRecorder } from '../metrics/index.js';
16
16
  /**
@@ -133,10 +133,19 @@ const defaultLogger = {
133
133
  */
134
134
  export async function build(options = {}) {
135
135
  const cacheDir = resolveCacheDir();
136
+ // Clear per-build caches at the start of each build
137
+ // This prevents stale data from previous builds and ensures consistent state
138
+ clearFileHashCache();
139
+ clearTemplatePathCache();
136
140
  // Ensure cache directory exists before acquiring build lock
137
141
  await ensureDir(cacheDir);
142
+ // In dev mode, bypass file-based build lock (dev server handles concurrency via isBuildingRef
143
+ // and dev server lock prevents multiple dev servers in the same directory)
144
+ if (isDevelopment()) {
145
+ return buildInternal(options);
146
+ }
138
147
  // Use build lock to prevent concurrent builds, with force option to override
139
- return await withBuildLock(cacheDir, () => buildInternal(options), {
148
+ return withBuildLock(cacheDir, () => buildInternal(options), {
140
149
  force: Boolean(options.force || options.clean), // Allow force if user explicitly requests it
141
150
  timeout: 60000, // 1 minute timeout
142
151
  });
@@ -218,6 +227,15 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
218
227
  if (logger.startProgress) {
219
228
  logger.startProgress(pages.length);
220
229
  }
230
+ // Track timing for shouldRebuildPage
231
+ let totalShouldRebuildTime = 0;
232
+ let totalRenderTime = 0;
233
+ let totalFileWriteTime = 0;
234
+ let totalCacheEntryTime = 0;
235
+ // Batch pending writes for parallel execution (dev mode optimization)
236
+ const isDevMode = isDevelopment();
237
+ const pendingWrites = [];
238
+ const pendingCacheUpdates = [];
221
239
  for (let i = 0; i < pages.length; i++) {
222
240
  const page = pages[i];
223
241
  if (!page)
@@ -238,7 +256,10 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
238
256
  const cacheKey = relativePath.startsWith('/') ? relativePath : `/${relativePath}`;
239
257
  const existingEntry = manifest.entries[cacheKey];
240
258
  // Check if we should rebuild this page (considering ISG logic)
259
+ const shouldRebuildStart = performance.now();
241
260
  const shouldRebuild = options.force || (await shouldRebuildPage(page, existingEntry, config, buildTime));
261
+ const shouldRebuildTime = performance.now() - shouldRebuildStart;
262
+ totalShouldRebuildTime += shouldRebuildTime;
242
263
  if (!shouldRebuild) {
243
264
  // Cache hit - skip rendering
244
265
  cacheHits++;
@@ -279,7 +300,10 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
279
300
  }),
280
301
  };
281
302
  // Render with template
303
+ const renderPageStart = performance.now();
282
304
  const renderResult = await renderPage(page, htmlContent, config, eta, navigation, pages, assets, toc, logger);
305
+ const renderPageTime = performance.now() - renderPageStart;
306
+ totalRenderTime += renderPageTime;
283
307
  let finalHtml = renderResult.html;
284
308
  // Record templates loaded for this page
285
309
  recorder.increment('templatesLoaded', renderResult.templatesLoaded);
@@ -312,15 +336,28 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
312
336
  if (logger.updateProgress) {
313
337
  logger.updateProgress('rendered', page.url, renderTime);
314
338
  }
315
- // Ensure directory exists and write file
316
- await ensureDir(dirname(outputPath));
317
- await writeFile(outputPath, finalHtml, 'utf-8');
318
- // Update cache manifest
319
- if (existingEntry) {
320
- manifest.entries[cacheKey] = await updateCacheEntry(existingEntry, page, config, buildTime);
339
+ // In dev mode, batch writes for parallel execution to avoid Windows filesystem slowdown
340
+ if (isDevMode) {
341
+ pendingWrites.push({ outputPath, content: finalHtml });
342
+ pendingCacheUpdates.push({ cacheKey, page, existingEntry });
321
343
  }
322
344
  else {
323
- manifest.entries[cacheKey] = await createCacheEntry(page, config, buildTime);
345
+ // Production mode: write immediately
346
+ const fileWriteStart = performance.now();
347
+ await ensureDir(dirname(outputPath));
348
+ await writeFile(outputPath, finalHtml, 'utf-8');
349
+ totalFileWriteTime += performance.now() - fileWriteStart;
350
+ // Update cache manifest
351
+ if (!options.skipCacheUpdate) {
352
+ const cacheEntryStart = performance.now();
353
+ if (existingEntry) {
354
+ manifest.entries[cacheKey] = await updateCacheEntry(existingEntry, page, config, buildTime);
355
+ }
356
+ else {
357
+ manifest.entries[cacheKey] = await createCacheEntry(page, config, buildTime);
358
+ }
359
+ totalCacheEntryTime += performance.now() - cacheEntryStart;
360
+ }
324
361
  }
325
362
  // Collect searchable page data if search is enabled
326
363
  // Uses TOC entries and markdown content instead of parsing rendered HTML
@@ -334,6 +371,36 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
334
371
  recorder.addToPhase('hookAfterRenderTotalMs', performance.now() - hookStart);
335
372
  }
336
373
  }
374
+ // In dev mode, execute batched writes in parallel
375
+ if (isDevMode && pendingWrites.length > 0) {
376
+ const fileWriteStart = performance.now();
377
+ // Ensure all directories exist first
378
+ const uniqueDirs = [...new Set(pendingWrites.map((w) => dirname(w.outputPath)))];
379
+ await Promise.all(uniqueDirs.map((dir) => ensureDir(dir)));
380
+ // Write all files in parallel
381
+ await Promise.all(pendingWrites.map((w) => writeFile(w.outputPath, w.content, 'utf-8')));
382
+ totalFileWriteTime = performance.now() - fileWriteStart;
383
+ // Update cache entries SEQUENTIALLY with fast update (reuse existing deps)
384
+ // This avoids expensive template dependency tracking on each incremental rebuild
385
+ if (!options.skipCacheUpdate) {
386
+ const cacheEntryStart = performance.now();
387
+ for (const { cacheKey, page, existingEntry } of pendingCacheUpdates) {
388
+ if (existingEntry) {
389
+ // Fast update: reuse existing deps, just update hashes and timestamp
390
+ manifest.entries[cacheKey] = await updateCacheEntry(existingEntry, page, config, buildTime, true);
391
+ }
392
+ else {
393
+ manifest.entries[cacheKey] = await createCacheEntry(page, config, buildTime);
394
+ }
395
+ }
396
+ totalCacheEntryTime = performance.now() - cacheEntryStart;
397
+ }
398
+ }
399
+ // Record detailed phase timings in metrics
400
+ recorder.recordPhase('shouldRebuildTotalMs', totalShouldRebuildTime);
401
+ recorder.recordPhase('renderPageTotalMs', totalRenderTime);
402
+ recorder.recordPhase('fileWriteTotalMs', totalFileWriteTime);
403
+ recorder.recordPhase('cacheEntryTotalMs', totalCacheEntryTime);
337
404
  // Display final progress summary
338
405
  if (logger.endProgress) {
339
406
  logger.endProgress();
@@ -369,13 +436,17 @@ async function copyStaticAssets(config, outDir, logger) {
369
436
  /**
370
437
  * Generates build statistics.
371
438
  */
372
- async function generateBuildStats(pages, assetsCount, buildStartTime, outDir, cacheHits, cacheMisses, logger) {
439
+ async function generateBuildStats(pages, assetsCount, buildStartTime, outDir, cacheHits, cacheMisses, logger, recorder = noopMetricRecorder) {
373
440
  const buildEndTime = Date.now();
441
+ const outputSizeStart = performance.now();
442
+ const outputSizeBytes = await getDirectorySize(outDir);
443
+ const dirSizeTime = performance.now() - outputSizeStart;
444
+ recorder.recordPhase('getDirectorySizeMs', dirSizeTime);
374
445
  const buildStats = {
375
446
  totalPages: pages.length,
376
447
  assetsCount,
377
448
  buildTimeMs: buildEndTime - buildStartTime,
378
- outputSizeBytes: await getDirectorySize(outDir),
449
+ outputSizeBytes,
379
450
  // Include ISG cache statistics
380
451
  cacheHits,
381
452
  cacheMisses,
@@ -427,6 +498,7 @@ async function buildInternal(options = {}) {
427
498
  }
428
499
  await ensureDir(outDir);
429
500
  // Enable Tailwind class inventory tracking only if Tailwind is detected
501
+ const tailwindInitStart = performance.now();
430
502
  const hasTailwind = await isTailwindUsed();
431
503
  if (hasTailwind) {
432
504
  enableInventoryTracking();
@@ -447,6 +519,8 @@ async function buildInternal(options = {}) {
447
519
  logger.status(`Created inventory file for Tailwind scanner (will be populated after rendering)`);
448
520
  }
449
521
  }
522
+ const tailwindInitTime = performance.now() - tailwindInitStart;
523
+ recorder.recordPhase('tailwindInitMs', tailwindInitTime);
450
524
  // Load cache manifest for ISG (after potential clean operation)
451
525
  const endManifestLoadSpan = recorder.startSpan('cacheManifestLoadMs');
452
526
  const { manifest } = await setupCacheAndManifest(cacheDir);
@@ -466,22 +540,35 @@ async function buildInternal(options = {}) {
466
540
  // Store navigation hash in manifest for change detection in dev server
467
541
  manifest.navigationHash = navigationHash;
468
542
  // Compile TypeScript if enabled
543
+ // In dev mode, skip compilation since esbuild watcher handles it
469
544
  let compiledBundles = [];
470
- if (config.typescript?.enabled) {
545
+ const isDevMode = isDevelopment();
546
+ if (config.typescript?.enabled && isDevMode) {
547
+ // In dev mode, detect existing bundles compiled by esbuild watcher
548
+ compiledBundles = await detectExistingBundles({
549
+ projectRoot: process.cwd(),
550
+ config: config.typescript,
551
+ outDir: config.outDir || DEFAULT_OUT_DIR,
552
+ mode: 'development',
553
+ });
554
+ }
555
+ else if (config.typescript?.enabled) {
471
556
  const endTsSpan = recorder.startSpan('typescriptCompileMs');
472
557
  compiledBundles = await compileTypeScript({
473
558
  projectRoot: process.cwd(),
474
559
  config: config.typescript,
475
560
  outDir: config.outDir || DEFAULT_OUT_DIR,
476
- mode: getEnv() === 'production' ? 'production' : 'development',
561
+ mode: 'production',
477
562
  logger,
478
563
  });
479
564
  endTsSpan();
480
565
  }
481
566
  // Pre-compute search index filename if search is enabled
567
+ // In dev mode, use a stable filename to simlplify testing and debugging
482
568
  let searchIndexFilename;
483
569
  if (config.search?.enabled) {
484
- searchIndexFilename = computeSearchIndexFilename(config.search, buildStartTime.toString());
570
+ const buildId = isDevMode ? 'dev' : buildStartTime.toString();
571
+ searchIndexFilename = computeSearchIndexFilename(config.search, buildId);
485
572
  }
486
573
  // Process pages with ISG caching logic
487
574
  if (logger.step) {
@@ -502,32 +589,46 @@ async function buildInternal(options = {}) {
502
589
  logger.info(`Generating search index to ${searchIndexFilename}`);
503
590
  const endSearchIndexSpan = recorder.startSpan('searchIndexGenerationMs');
504
591
  const searchIndex = generateSearchIndex(searchablePages, config.search);
505
- searchIndexMetadata = await writeSearchIndex(searchIndex, outDir, searchIndexFilename);
506
592
  endSearchIndexSpan();
593
+ const writeStart = performance.now();
594
+ // In dev mode, skip write if content hasn't changed (template-only changes)
595
+ const skipIfUnchanged = isDevMode;
596
+ searchIndexMetadata = await writeSearchIndex(searchIndex, outDir, searchIndexFilename, skipIfUnchanged);
597
+ const writeTime = performance.now() - writeStart;
598
+ recorder.recordPhase('searchIndexWriteMs', writeTime);
507
599
  logger.success(`Generated search index with ${searchIndexMetadata.documentCount} documents`);
508
600
  }
509
601
  // Record page rendering counts
510
602
  recorder.increment('renderedPages', cacheMisses);
511
603
  recorder.increment('cachedPages', cacheHits);
512
604
  // Write Tailwind class inventory after all templates have been rendered (if Tailwind is used)
605
+ const tailwindStart = performance.now();
513
606
  if (hasTailwind) {
514
607
  const inventorySize = getInventorySize();
515
608
  if (inventorySize > 0) {
516
- await writeTailwindClassInventory(cacheDir);
609
+ // In dev mode, skip write if inventory size hasn't changed
610
+ const skipIfUnchanged = isDevMode;
611
+ await writeTailwindClassInventory(cacheDir, skipIfUnchanged);
517
612
  logger.info('');
518
613
  logger.status(`Generated Tailwind class inventory (${inventorySize} classes tracked)`);
519
614
  }
520
615
  // Disable inventory tracking after build
521
616
  disableInventoryTracking();
522
617
  }
523
- // Save updated cache manifest
618
+ const tailwindInventoryTime = performance.now() - tailwindStart;
619
+ recorder.recordPhase('tailwindInventoryMs', tailwindInventoryTime);
620
+ // Save updated cache manifest (skip if caller already saved it, e.g., dev mode template change)
524
621
  const endManifestSaveSpan = recorder.startSpan('cacheManifestSaveMs');
525
- await saveCacheManifest(cacheDir, manifest);
622
+ if (!options.skipManifestSave) {
623
+ await saveCacheManifest(cacheDir, manifest);
624
+ }
526
625
  endManifestSaveSpan();
527
- // Copy static assets and count them
626
+ // Copy static assets and count them (skip for template-only changes in dev mode)
528
627
  const endAssetSpan = recorder.startSpan('assetCopyMs');
529
628
  let assetsCount = 0;
530
- assetsCount = await copyStaticAssets(config, outDir, logger);
629
+ if (!options.skipAssetCopy) {
630
+ assetsCount = await copyStaticAssets(config, outDir, logger);
631
+ }
531
632
  endAssetSpan();
532
633
  recorder.increment('assetsCopied', assetsCount);
533
634
  // Get current environment
@@ -616,8 +717,17 @@ async function buildInternal(options = {}) {
616
717
  await config.hooks.afterAll({ config, pages });
617
718
  endHookAfterAll();
618
719
  }
619
- // Calculate build statistics
620
- const buildStats = await generateBuildStats(pages, assetsCount, buildStartTime, outDir, cacheHits, cacheMisses, logger);
720
+ // Generate build statistics (skip in dev mode for performance)
721
+ const buildStats = isDevMode
722
+ ? {
723
+ totalPages: pages.length,
724
+ assetsCount: 0,
725
+ buildTimeMs: Date.now() - buildStartTime,
726
+ outputSizeBytes: 0,
727
+ cacheHits,
728
+ cacheMisses,
729
+ }
730
+ : await generateBuildStats(pages, assetsCount, buildStartTime, outDir, cacheHits, cacheMisses, logger, recorder);
621
731
  // Set ISG metrics
622
732
  const totalPages = pages.length;
623
733
  const cacheHitRate = totalPages > 0 ? cacheHits / totalPages : 0;
@@ -1 +1 @@
1
- {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/core/dev.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,mBAAmB,CAAC;AA6B7D,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;AAwXD,wBAAsB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC,CAoexF"}
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/core/dev.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAkC7D,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;AAobD,wBAAsB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC,CAigBxF"}