@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/config/loader.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/config/loader.js
CHANGED
|
@@ -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(
|
|
72
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/core/build.d.ts
CHANGED
|
@@ -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.
|
package/dist/core/build.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/core/build.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/core/build.ts"],"names":[],"mappings":"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"}
|
package/dist/core/build.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
211
|
-
if (logger.
|
|
212
|
-
logger.
|
|
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.
|
|
247
|
-
logger.
|
|
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.
|
|
317
|
-
logger.
|
|
318
|
-
timing: renderTime,
|
|
319
|
-
url: page.url,
|
|
320
|
-
});
|
|
336
|
+
if (logger.updateProgress) {
|
|
337
|
+
logger.updateProgress('rendered', page.url, renderTime);
|
|
321
338
|
}
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
620
|
-
const buildStats =
|
|
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;
|
package/dist/core/dev.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/core/dev.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,mBAAmB,CAAC;
|
|
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"}
|