@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.
Files changed (76) 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 +176 -66
  7. package/dist/core/dev.d.ts.map +1 -1
  8. package/dist/core/dev.js +100 -28
  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/preview.js +1 -1
  28. package/dist/core/templates.js +5 -5
  29. package/dist/core/utils/bundle-matching.utils.d.ts +2 -0
  30. package/dist/core/utils/bundle-matching.utils.d.ts.map +1 -1
  31. package/dist/core/utils/index.d.ts +1 -1
  32. package/dist/core/utils/index.d.ts.map +1 -1
  33. package/dist/core/utils/index.js +1 -1
  34. package/dist/core/utils/logger.utils.d.ts.map +1 -1
  35. package/dist/core/utils/logger.utils.js +1 -0
  36. package/dist/core/utils/partial-validation.utils.js +2 -2
  37. package/dist/core/utils/paths.utils.d.ts +18 -0
  38. package/dist/core/utils/paths.utils.d.ts.map +1 -1
  39. package/dist/core/utils/paths.utils.js +23 -0
  40. package/dist/core/utils/tailwind-inventory.utils.d.ts +1 -16
  41. package/dist/core/utils/tailwind-inventory.utils.d.ts.map +1 -1
  42. package/dist/core/utils/tailwind-inventory.utils.js +35 -3
  43. package/dist/core/utils/typescript.utils.d.ts +13 -0
  44. package/dist/core/utils/typescript.utils.d.ts.map +1 -1
  45. package/dist/core/utils/typescript.utils.js +82 -3
  46. package/dist/env.d.ts +45 -0
  47. package/dist/env.d.ts.map +1 -1
  48. package/dist/env.js +51 -0
  49. package/dist/index.d.ts +2 -2
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +2 -2
  52. package/dist/metrics/index.d.ts +1 -1
  53. package/dist/metrics/index.d.ts.map +1 -1
  54. package/dist/metrics/index.js +2 -0
  55. package/dist/metrics/recorder.d.ts.map +1 -1
  56. package/dist/metrics/types.d.ts +31 -0
  57. package/dist/metrics/types.d.ts.map +1 -1
  58. package/dist/metrics/utils/html-report.utils.d.ts +24 -0
  59. package/dist/metrics/utils/html-report.utils.d.ts.map +1 -0
  60. package/dist/metrics/utils/html-report.utils.js +1547 -0
  61. package/dist/metrics/utils/index.d.ts +1 -0
  62. package/dist/metrics/utils/index.d.ts.map +1 -1
  63. package/dist/metrics/utils/index.js +2 -0
  64. package/dist/metrics/utils/writer.utils.d.ts +6 -2
  65. package/dist/metrics/utils/writer.utils.d.ts.map +1 -1
  66. package/dist/metrics/utils/writer.utils.js +20 -4
  67. package/dist/search/generator.d.ts +1 -9
  68. package/dist/search/generator.d.ts.map +1 -1
  69. package/dist/search/generator.js +26 -2
  70. package/dist/seo/generator.d.ts.map +1 -1
  71. package/dist/seo/generator.js +1 -0
  72. package/dist/seo/utils/escape-and-validation.utils.d.ts.map +1 -1
  73. package/dist/seo/utils/escape-and-validation.utils.js +1 -16
  74. package/dist/types/logging.d.ts +31 -12
  75. package/dist/types/logging.d.ts.map +1 -1
  76. 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":"AAsDA,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;AA4FD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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, } 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
  /**
@@ -50,8 +50,9 @@ async function getDirectorySize(dirPath) {
50
50
  */
51
51
  async function copyStaticAssetsWithLogging(sourceDir, destDir, logger, basePath = '') {
52
52
  let filesCopied = 0;
53
+ let totalBytes = 0;
53
54
  if (!(await pathExists(sourceDir))) {
54
- return 0;
55
+ return { count: 0, totalBytes: 0 };
55
56
  }
56
57
  const items = await readdir(sourceDir, { withFileTypes: true });
57
58
  for (const item of items) {
@@ -61,22 +62,26 @@ async function copyStaticAssetsWithLogging(sourceDir, destDir, logger, basePath
61
62
  if (item.isDirectory()) {
62
63
  // Recursively copy directories
63
64
  await ensureDir(destPath);
64
- filesCopied += await copyStaticAssetsWithLogging(sourcePath, destPath, logger);
65
+ const result = await copyStaticAssetsWithLogging(sourcePath, destPath, logger);
66
+ filesCopied += result.count;
67
+ totalBytes += result.totalBytes;
65
68
  }
66
69
  else {
67
70
  // Copy individual files
68
71
  await ensureDir(dirname(destPath));
69
72
  await copyFile(sourcePath, destPath);
73
+ const fileStats = await stat(sourcePath);
70
74
  if (logger.file) {
71
- logger.file('copy', relativePath);
75
+ logger.file('copy', relativePath, fileStats.size);
72
76
  }
73
77
  else {
74
- logger.processing(`📄 ${relativePath}`);
78
+ logger.processing(`• ${relativePath}`);
75
79
  }
76
80
  filesCopied++;
81
+ totalBytes += fileStats.size;
77
82
  }
78
83
  }
79
- return filesCopied;
84
+ return { count: filesCopied, totalBytes };
80
85
  }
81
86
  /**
82
87
  * Default console logger implementation.
@@ -86,6 +91,7 @@ const defaultLogger = {
86
91
  success: (message) => console.log(message),
87
92
  warning: (message) => console.warn(message),
88
93
  error: (message) => console.error(message),
94
+ status: (message) => console.log(message),
89
95
  building: (message) => console.log(message),
90
96
  processing: (message) => console.log(message),
91
97
  stats: (message) => console.log(message),
@@ -127,10 +133,19 @@ const defaultLogger = {
127
133
  */
128
134
  export async function build(options = {}) {
129
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();
130
140
  // Ensure cache directory exists before acquiring build lock
131
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
+ }
132
147
  // Use build lock to prevent concurrent builds, with force option to override
133
- return await withBuildLock(cacheDir, () => buildInternal(options), {
148
+ return withBuildLock(cacheDir, () => buildInternal(options), {
134
149
  force: Boolean(options.force || options.clean), // Allow force if user explicitly requests it
135
150
  timeout: 60000, // 1 minute timeout
136
151
  });
@@ -169,7 +184,7 @@ async function setupCacheAndManifest(cacheDir) {
169
184
  async function loadContentAndBuildNavigation(config, options, logger) {
170
185
  // Load all content
171
186
  const pages = await loadContent(config, options.includeDrafts);
172
- logger.info(`📄 Found ${pages.length} pages`);
187
+ logger.status(`Found ${pages.length} pages`);
173
188
  // Build navigation from pages
174
189
  if (logger.step) {
175
190
  console.log(); // Add spacing before navigation step
@@ -203,26 +218,28 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
203
218
  await config.hooks.beforeAll(buildContext);
204
219
  endHookBeforeAll();
205
220
  }
206
- // Render each page with tree-based progress tracking and ISG
221
+ // Render each page with progress tracking and ISG
207
222
  if (logger.step) {
208
223
  logger.step(2, 3, 'Rendering pages');
224
+ console.log(); // Add spacing after step header
209
225
  }
210
- // Initialize rendering tree
211
- if (logger.startRenderingTree) {
212
- logger.startRenderingTree('Page Rendering Process');
226
+ // Initialize progress tracking
227
+ if (logger.startProgress) {
228
+ logger.startProgress(pages.length);
213
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 = [];
214
239
  for (let i = 0; i < pages.length; i++) {
215
240
  const page = pages[i];
216
241
  if (!page)
217
242
  continue; // Safety check
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 });
222
- }
223
- else {
224
- logger.processing(`Checking ${page.url}`);
225
- }
226
243
  // Determine output path
227
244
  let outputPath;
228
245
  if (page.url === '/') {
@@ -239,15 +256,15 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
239
256
  const cacheKey = relativePath.startsWith('/') ? relativePath : `/${relativePath}`;
240
257
  const existingEntry = manifest.entries[cacheKey];
241
258
  // Check if we should rebuild this page (considering ISG logic)
259
+ const shouldRebuildStart = performance.now();
242
260
  const shouldRebuild = options.force || (await shouldRebuildPage(page, existingEntry, config, buildTime));
261
+ const shouldRebuildTime = performance.now() - shouldRebuildStart;
262
+ totalShouldRebuildTime += shouldRebuildTime;
243
263
  if (!shouldRebuild) {
244
264
  // Cache hit - skip rendering
245
265
  cacheHits++;
246
- if (logger.updateTreeNode) {
247
- logger.updateTreeNode(pageId, 'cached', { cacheHit: true, url: page.url });
248
- }
249
- else {
250
- logger.processing(`📋 Cached ${page.url}`);
266
+ if (logger.updateProgress) {
267
+ logger.updateProgress('cached', page.url);
251
268
  }
252
269
  // Record page timing for cached pages (0ms render time)
253
270
  recorder.recordPageTiming(page.url, 0, true);
@@ -283,7 +300,10 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
283
300
  }),
284
301
  };
285
302
  // Render with template
303
+ const renderPageStart = performance.now();
286
304
  const renderResult = await renderPage(page, htmlContent, config, eta, navigation, pages, assets, toc, logger);
305
+ const renderPageTime = performance.now() - renderPageStart;
306
+ totalRenderTime += renderPageTime;
287
307
  let finalHtml = renderResult.html;
288
308
  // Record templates loaded for this page
289
309
  recorder.increment('templatesLoaded', renderResult.templatesLoaded);
@@ -313,21 +333,31 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
313
333
  const renderTime = Math.round(performance.now() - startTime);
314
334
  // Record page timing for rendered pages (includes template count)
315
335
  recorder.recordPageTiming(page.url, renderTime, false, renderResult.templatesLoaded);
316
- if (logger.updateTreeNode) {
317
- logger.updateTreeNode(pageId, 'completed', {
318
- timing: renderTime,
319
- url: page.url,
320
- });
336
+ if (logger.updateProgress) {
337
+ logger.updateProgress('rendered', page.url, renderTime);
321
338
  }
322
- // Ensure directory exists and write file
323
- await ensureDir(dirname(outputPath));
324
- await writeFile(outputPath, finalHtml, 'utf-8');
325
- // Update cache manifest
326
- if (existingEntry) {
327
- 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 });
328
343
  }
329
344
  else {
330
- 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
+ }
331
361
  }
332
362
  // Collect searchable page data if search is enabled
333
363
  // Uses TOC entries and markdown content instead of parsing rendered HTML
@@ -341,11 +371,41 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
341
371
  recorder.addToPhase('hookAfterRenderTotalMs', performance.now() - hookStart);
342
372
  }
343
373
  }
344
- // Display final rendering tree and clear it
345
- if (logger.showRenderingTree) {
346
- logger.showRenderingTree();
347
- if (logger.clearRenderingTree) {
348
- logger.clearRenderingTree();
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);
404
+ // Display final progress summary
405
+ if (logger.endProgress) {
406
+ logger.endProgress();
407
+ if (logger.showRenderingSummary) {
408
+ logger.showRenderingSummary();
349
409
  }
350
410
  }
351
411
  return { cacheHits, cacheMisses, searchablePages };
@@ -363,20 +423,30 @@ async function copyStaticAssets(config, outDir, logger) {
363
423
  logger.step(3, 3, 'Copying static assets');
364
424
  }
365
425
  logger.info(`Copying static assets from ${config.staticDir}`);
366
- const assetsCount = await copyStaticAssetsWithLogging(staticDir, outDir, logger);
367
- logger.info(`Copied ${assetsCount} static assets`);
426
+ const { count: assetsCount, totalBytes } = await copyStaticAssetsWithLogging(staticDir, outDir, logger);
427
+ if (assetsCount > 0) {
428
+ const totalSizeStr = formatBytes(totalBytes);
429
+ logger.success(`Copied ${assetsCount} static assets (${totalSizeStr})`);
430
+ }
431
+ else {
432
+ logger.info(`Copied 0 static assets`);
433
+ }
368
434
  return assetsCount;
369
435
  }
370
436
  /**
371
437
  * Generates build statistics.
372
438
  */
373
- async function generateBuildStats(pages, assetsCount, buildStartTime, outDir, cacheHits, cacheMisses, logger) {
439
+ async function generateBuildStats(pages, assetsCount, buildStartTime, outDir, cacheHits, cacheMisses, logger, recorder = noopMetricRecorder) {
374
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);
375
445
  const buildStats = {
376
446
  totalPages: pages.length,
377
447
  assetsCount,
378
448
  buildTimeMs: buildEndTime - buildStartTime,
379
- outputSizeBytes: await getDirectorySize(outDir),
449
+ outputSizeBytes,
380
450
  // Include ISG cache statistics
381
451
  cacheHits,
382
452
  cacheMisses,
@@ -412,7 +482,7 @@ async function buildInternal(options = {}) {
412
482
  cliVersion: options.cliVersion,
413
483
  coreVersion: options.coreVersion,
414
484
  });
415
- logger.building('Building your site...');
485
+ logger.status('Started building your site...');
416
486
  // Load configuration (instrumented)
417
487
  const endConfigSpan = recorder.startSpan('configLoadMs');
418
488
  const { config, outDir, cacheDir } = await loadAndValidateConfig(options);
@@ -422,12 +492,13 @@ async function buildInternal(options = {}) {
422
492
  let cacheMisses = 0;
423
493
  // Clean output directory and cache if requested
424
494
  if (options.clean) {
425
- logger.info('Cleaning output directory and ISG cache...');
495
+ logger.status('Cleaning output directory and ISG cache...');
426
496
  await remove(outDir);
427
497
  await remove(cacheDir);
428
498
  }
429
499
  await ensureDir(outDir);
430
500
  // Enable Tailwind class inventory tracking only if Tailwind is detected
501
+ const tailwindInitStart = performance.now();
431
502
  const hasTailwind = await isTailwindUsed();
432
503
  if (hasTailwind) {
433
504
  enableInventoryTracking();
@@ -438,16 +509,18 @@ async function buildInternal(options = {}) {
438
509
  // Write the initial inventory file immediately so Tailwind can scan it
439
510
  // This is critical for dev server where Tailwind starts watching before template rendering
440
511
  await writeTailwindClassInventory(cacheDir);
441
- logger.info(`📦 Loaded ${loadedCount} classes from previous build for Tailwind scanner`);
512
+ logger.status(`Loaded ${loadedCount} classes from previous build for Tailwind scanner`);
442
513
  }
443
514
  else {
444
515
  // No previous inventory found - write an empty placeholder file
445
516
  // This ensures Tailwind has a file to scan even on first build
446
517
  // It will be populated with actual classes after template rendering
447
518
  await writeTailwindClassInventory(cacheDir);
448
- logger.info(`📦 Created inventory file for Tailwind scanner (will be populated after rendering)`);
519
+ logger.status(`Created inventory file for Tailwind scanner (will be populated after rendering)`);
449
520
  }
450
521
  }
522
+ const tailwindInitTime = performance.now() - tailwindInitStart;
523
+ recorder.recordPhase('tailwindInitMs', tailwindInitTime);
451
524
  // Load cache manifest for ISG (after potential clean operation)
452
525
  const endManifestLoadSpan = recorder.startSpan('cacheManifestLoadMs');
453
526
  const { manifest } = await setupCacheAndManifest(cacheDir);
@@ -467,22 +540,35 @@ async function buildInternal(options = {}) {
467
540
  // Store navigation hash in manifest for change detection in dev server
468
541
  manifest.navigationHash = navigationHash;
469
542
  // Compile TypeScript if enabled
543
+ // In dev mode, skip compilation since esbuild watcher handles it
470
544
  let compiledBundles = [];
471
- 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) {
472
556
  const endTsSpan = recorder.startSpan('typescriptCompileMs');
473
557
  compiledBundles = await compileTypeScript({
474
558
  projectRoot: process.cwd(),
475
559
  config: config.typescript,
476
560
  outDir: config.outDir || DEFAULT_OUT_DIR,
477
- mode: getEnv() === 'production' ? 'production' : 'development',
561
+ mode: 'production',
478
562
  logger,
479
563
  });
480
564
  endTsSpan();
481
565
  }
482
566
  // Pre-compute search index filename if search is enabled
567
+ // In dev mode, use a stable filename to simlplify testing and debugging
483
568
  let searchIndexFilename;
484
569
  if (config.search?.enabled) {
485
- searchIndexFilename = computeSearchIndexFilename(config.search, buildStartTime.toString());
570
+ const buildId = isDevMode ? 'dev' : buildStartTime.toString();
571
+ searchIndexFilename = computeSearchIndexFilename(config.search, buildId);
486
572
  }
487
573
  // Process pages with ISG caching logic
488
574
  if (logger.step) {
@@ -503,32 +589,46 @@ async function buildInternal(options = {}) {
503
589
  logger.info(`Generating search index to ${searchIndexFilename}`);
504
590
  const endSearchIndexSpan = recorder.startSpan('searchIndexGenerationMs');
505
591
  const searchIndex = generateSearchIndex(searchablePages, config.search);
506
- searchIndexMetadata = await writeSearchIndex(searchIndex, outDir, searchIndexFilename);
507
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);
508
599
  logger.success(`Generated search index with ${searchIndexMetadata.documentCount} documents`);
509
600
  }
510
601
  // Record page rendering counts
511
602
  recorder.increment('renderedPages', cacheMisses);
512
603
  recorder.increment('cachedPages', cacheHits);
513
604
  // Write Tailwind class inventory after all templates have been rendered (if Tailwind is used)
605
+ const tailwindStart = performance.now();
514
606
  if (hasTailwind) {
515
607
  const inventorySize = getInventorySize();
516
608
  if (inventorySize > 0) {
517
- await writeTailwindClassInventory(cacheDir);
609
+ // In dev mode, skip write if inventory size hasn't changed
610
+ const skipIfUnchanged = isDevMode;
611
+ await writeTailwindClassInventory(cacheDir, skipIfUnchanged);
518
612
  logger.info('');
519
- logger.info(`📝 Generated Tailwind class inventory (${inventorySize} classes tracked)`);
613
+ logger.status(`Generated Tailwind class inventory (${inventorySize} classes tracked)`);
520
614
  }
521
615
  // Disable inventory tracking after build
522
616
  disableInventoryTracking();
523
617
  }
524
- // 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)
525
621
  const endManifestSaveSpan = recorder.startSpan('cacheManifestSaveMs');
526
- await saveCacheManifest(cacheDir, manifest);
622
+ if (!options.skipManifestSave) {
623
+ await saveCacheManifest(cacheDir, manifest);
624
+ }
527
625
  endManifestSaveSpan();
528
- // Copy static assets and count them
626
+ // Copy static assets and count them (skip for template-only changes in dev mode)
529
627
  const endAssetSpan = recorder.startSpan('assetCopyMs');
530
628
  let assetsCount = 0;
531
- assetsCount = await copyStaticAssets(config, outDir, logger);
629
+ if (!options.skipAssetCopy) {
630
+ assetsCount = await copyStaticAssets(config, outDir, logger);
631
+ }
532
632
  endAssetSpan();
533
633
  recorder.increment('assetsCopied', assetsCount);
534
634
  // Get current environment
@@ -551,7 +651,7 @@ async function buildInternal(options = {}) {
551
651
  logger.success(`Generated sitemap index with ${sitemapResult.sitemaps.length} sitemaps (${sitemapResult.entryCount} entries)`);
552
652
  }
553
653
  else {
554
- logger.success(`Generated sitemap with ${sitemapResult.entryCount} entries`);
654
+ logger.success(`Generated sitemap.xml with ${sitemapResult.entryCount} entries (${(sitemapResult.sizeInBytes / 1024).toFixed(2)} KB)`);
555
655
  }
556
656
  }
557
657
  // Generate robots.txt if enabled (only in production mode)
@@ -562,7 +662,8 @@ async function buildInternal(options = {}) {
562
662
  logger.info('Generating robots.txt...');
563
663
  const robotsContent = generateRobotsTxtFromConfig(config.robots, config.site.baseUrl);
564
664
  await writeFile(join(outDir, 'robots.txt'), robotsContent);
565
- logger.success('Generated robots.txt');
665
+ const robotsSizeBytes = Buffer.byteLength(robotsContent, 'utf8');
666
+ logger.success(`Generated robots.txt (${robotsSizeBytes} bytes)`);
566
667
  }
567
668
  // Generate RSS feeds if enabled (only in production mode)
568
669
  if (config.rss?.enabled && currentEnv === 'production') {
@@ -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;AAuXD,wBAAsB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC,CAgexF"}
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"}