@stati/core 1.16.3 → 1.17.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.
@@ -1,7 +1,22 @@
1
1
  import type { BuildStats, Logger } from '../types/index.js';
2
+ import type { BuildMetrics } from '../metrics/index.js';
3
+ /**
4
+ * Options for metrics collection during builds.
5
+ */
6
+ export interface MetricsOptions {
7
+ /** Enable metrics collection */
8
+ enabled?: boolean;
9
+ /** Include per-page timing details */
10
+ detailed?: boolean;
11
+ }
2
12
  /**
3
13
  * Options for customizing the build process.
4
14
  *
15
+ * @remarks
16
+ * The `| undefined` on optional properties is intentional due to
17
+ * `exactOptionalPropertyTypes: true` in tsconfig.json, allowing callers
18
+ * to explicitly pass `undefined` values.
19
+ *
5
20
  * @example
6
21
  * ```typescript
7
22
  * const options: BuildOptions = {
@@ -9,23 +24,33 @@ import type { BuildStats, Logger } from '../types/index.js';
9
24
  * clean: true, // Clean output directory before build
10
25
  * configPath: './custom.config.js', // Custom config file path
11
26
  * includeDrafts: true, // Include draft pages in build
12
- * version: '1.0.0' // Version to display in build messages
27
+ * version: '1.0.0', // Version to display in build messages
28
+ * metrics: { enabled: true, detailed: true } // Enable metrics collection
13
29
  * };
14
30
  * ```
15
31
  */
16
32
  export interface BuildOptions {
17
33
  /** Force rebuild of all pages, ignoring cache */
18
- force?: boolean;
34
+ force?: boolean | undefined;
19
35
  /** Clean the output directory before building */
20
- clean?: boolean;
36
+ clean?: boolean | undefined;
21
37
  /** Path to a custom configuration file */
22
- configPath?: string;
38
+ configPath?: string | undefined;
23
39
  /** Include draft pages in the build */
24
- includeDrafts?: boolean;
40
+ includeDrafts?: boolean | undefined;
25
41
  /** Custom logger for build output */
26
- logger?: Logger;
42
+ logger?: Logger | undefined;
27
43
  /** Version information to display in build messages */
28
- version?: string;
44
+ version?: string | undefined;
45
+ /** Metrics collection options */
46
+ metrics?: MetricsOptions | undefined;
47
+ }
48
+ /**
49
+ * Extended build result including optional metrics.
50
+ */
51
+ export interface BuildResult extends BuildStats {
52
+ /** Build metrics (only present when metrics enabled) */
53
+ buildMetrics?: BuildMetrics;
29
54
  }
30
55
  /**
31
56
  * Builds the Stati site with ISG support.
@@ -43,20 +68,24 @@ export interface BuildOptions {
43
68
  * 5. Update cache manifest
44
69
  *
45
70
  * @param options - Build configuration options
46
- * @returns Promise resolving to build statistics
71
+ * @returns Promise resolving to build result with statistics and optional metrics
47
72
  * @throws {Error} When configuration is invalid
48
73
  * @throws {Error} When template rendering fails
49
74
  * @throws {Error} When build lock cannot be acquired
50
75
  *
51
76
  * @example
52
77
  * ```typescript
53
- * const stats = await build({
78
+ * const result = await build({
54
79
  * force: true,
55
80
  * clean: true,
56
- * includeDrafts: false
81
+ * includeDrafts: false,
82
+ * metrics: { enabled: true }
57
83
  * });
58
- * console.log(`Built ${stats.pageCount} pages in ${stats.buildTime}ms`);
84
+ * console.log(`Built ${result.totalPages} pages in ${result.buildTimeMs}ms`);
85
+ * if (result.buildMetrics) {
86
+ * console.log(`Cache hit rate: ${result.buildMetrics.isg.cacheHitRate}`);
87
+ * }
59
88
  * ```
60
89
  */
61
- export declare function build(options?: BuildOptions): Promise<BuildStats>;
90
+ export declare function build(options?: BuildOptions): Promise<BuildResult>;
62
91
  //# sourceMappingURL=build.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/core/build.ts"],"names":[],"mappings":"AA+CA,OAAO,KAAK,EAEV,UAAU,EACV,MAAM,EAMP,MAAM,mBAAmB,CAAC;AAE3B;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,YAAY;IAC3B,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA4FD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAsB,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,UAAU,CAAC,CAW3E"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/core/build.ts"],"names":[],"mappings":"AAgDA,OAAO,KAAK,EAEV,UAAU,EACV,MAAM,EAMP,MAAM,mBAAmB,CAAC;AAC3B,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,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,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,5 +1,6 @@
1
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';
2
2
  import { join, dirname, relative, posix } from 'node:path';
3
+ import { performance } from 'node:perf_hooks';
3
4
  import { loadConfig } from '../config/loader.js';
4
5
  import { loadContent } from './content.js';
5
6
  import { createMarkdownProcessor, renderMarkdown } from './markdown.js';
@@ -10,6 +11,7 @@ import { generateSitemap, generateRobotsTxtFromConfig, autoInjectSEO, } from '..
10
11
  import { generateRSSFeeds, validateRSSConfig } from '../rss/index.js';
11
12
  import { getEnv } from '../env.js';
12
13
  import { DEFAULT_OUT_DIR } from '../constants.js';
14
+ import { createMetricRecorder, noopMetricRecorder } from '../metrics/index.js';
13
15
  /**
14
16
  * Recursively calculates the total size of a directory in bytes.
15
17
  * Used for build statistics.
@@ -103,19 +105,23 @@ const defaultLogger = {
103
105
  * 5. Update cache manifest
104
106
  *
105
107
  * @param options - Build configuration options
106
- * @returns Promise resolving to build statistics
108
+ * @returns Promise resolving to build result with statistics and optional metrics
107
109
  * @throws {Error} When configuration is invalid
108
110
  * @throws {Error} When template rendering fails
109
111
  * @throws {Error} When build lock cannot be acquired
110
112
  *
111
113
  * @example
112
114
  * ```typescript
113
- * const stats = await build({
115
+ * const result = await build({
114
116
  * force: true,
115
117
  * clean: true,
116
- * includeDrafts: false
118
+ * includeDrafts: false,
119
+ * metrics: { enabled: true }
117
120
  * });
118
- * console.log(`Built ${stats.pageCount} pages in ${stats.buildTime}ms`);
121
+ * console.log(`Built ${result.totalPages} pages in ${result.buildTimeMs}ms`);
122
+ * if (result.buildMetrics) {
123
+ * console.log(`Cache hit rate: ${result.buildMetrics.isg.cacheHitRate}`);
124
+ * }
119
125
  * ```
120
126
  */
121
127
  export async function build(options = {}) {
@@ -184,14 +190,16 @@ async function loadContentAndBuildNavigation(config, options, logger) {
184
190
  /**
185
191
  * Processes pages with ISG caching logic.
186
192
  */
187
- async function processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles) {
193
+ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder = noopMetricRecorder) {
188
194
  let cacheHits = 0;
189
195
  let cacheMisses = 0;
190
196
  // Build context
191
197
  const buildContext = { config, pages };
192
198
  // Run beforeAll hook
193
199
  if (config.hooks?.beforeAll) {
200
+ const endHookBeforeAll = recorder.startSpan('hookBeforeAllMs');
194
201
  await config.hooks.beforeAll(buildContext);
202
+ endHookBeforeAll();
195
203
  }
196
204
  // Render each page with tree-based progress tracking and ISG
197
205
  if (logger.step) {
@@ -239,14 +247,18 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
239
247
  else {
240
248
  logger.processing(`📋 Cached ${page.url}`);
241
249
  }
250
+ // Record page timing for cached pages (0ms render time)
251
+ recorder.recordPageTiming(page.url, 0, true);
242
252
  continue;
243
253
  }
244
254
  // Cache miss - need to rebuild
245
255
  cacheMisses++;
246
- const startTime = Date.now();
256
+ const startTime = performance.now();
247
257
  // Run beforeRender hook
248
258
  if (config.hooks?.beforeRender) {
259
+ const hookStart = performance.now();
249
260
  await config.hooks.beforeRender({ page, config });
261
+ recorder.addToPhase('hookBeforeRenderTotalMs', performance.now() - hookStart);
250
262
  }
251
263
  // Render markdown to HTML
252
264
  const htmlContent = renderMarkdown(page.content, md);
@@ -254,7 +266,10 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
254
266
  const bundlePaths = getBundlePathsForPage(page.url, compiledBundles);
255
267
  const assets = bundlePaths.length > 0 ? { bundlePaths } : undefined;
256
268
  // Render with template
257
- let finalHtml = await renderPage(page, htmlContent, config, eta, navigation, pages, assets);
269
+ const renderResult = await renderPage(page, htmlContent, config, eta, navigation, pages, assets);
270
+ let finalHtml = renderResult.html;
271
+ // Record templates loaded for this page
272
+ recorder.increment('templatesLoaded', renderResult.templatesLoaded);
258
273
  // Auto-inject SEO tags if enabled
259
274
  if (config.seo?.autoInject !== false) {
260
275
  const injectOptions = {
@@ -273,7 +288,9 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
273
288
  if (shouldAutoInject && assets && assets.bundlePaths.length > 0) {
274
289
  finalHtml = autoInjectBundles(finalHtml, assets.bundlePaths);
275
290
  }
276
- const renderTime = Date.now() - startTime;
291
+ const renderTime = Math.round(performance.now() - startTime);
292
+ // Record page timing for rendered pages (includes template count)
293
+ recorder.recordPageTiming(page.url, renderTime, false, renderResult.templatesLoaded);
277
294
  if (logger.updateTreeNode) {
278
295
  logger.updateTreeNode(pageId, 'completed', {
279
296
  timing: renderTime,
@@ -292,7 +309,9 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
292
309
  }
293
310
  // Run afterRender hook
294
311
  if (config.hooks?.afterRender) {
312
+ const hookStart = performance.now();
295
313
  await config.hooks.afterRender({ page, config });
314
+ recorder.addToPhase('hookAfterRenderTotalMs', performance.now() - hookStart);
296
315
  }
297
316
  }
298
317
  // Display final rendering tree and clear it
@@ -347,11 +366,29 @@ async function generateBuildStats(pages, assetsCount, buildStartTime, outDir, ca
347
366
  * Separated for cleaner error handling and testing.
348
367
  */
349
368
  async function buildInternal(options = {}) {
369
+ // Date.now() is used for user-facing build duration display (wall-clock time, in milliseconds).
370
+ // Note: Date.now() can be affected by system clock changes and is not monotonic.
371
+ // Internal metrics use performance.now() via the MetricRecorder for higher precision and monotonic timing,
372
+ // which is not affected by system clock adjustments.
350
373
  const buildStartTime = Date.now();
351
374
  const logger = options.logger || defaultLogger;
375
+ // Create metric recorder (noop if disabled)
376
+ const recorder = createMetricRecorder({
377
+ enabled: options.metrics?.enabled,
378
+ detailed: options.metrics?.detailed,
379
+ command: 'build',
380
+ flags: {
381
+ force: options.force,
382
+ clean: options.clean,
383
+ includeDrafts: options.includeDrafts,
384
+ },
385
+ statiVersion: options.version,
386
+ });
352
387
  logger.building('Building your site...');
353
- // Load configuration
388
+ // Load configuration (instrumented)
389
+ const endConfigSpan = recorder.startSpan('configLoadMs');
354
390
  const { config, outDir, cacheDir } = await loadAndValidateConfig(options);
391
+ endConfigSpan();
355
392
  // Initialize cache stats
356
393
  let cacheHits = 0;
357
394
  let cacheMisses = 0;
@@ -384,17 +421,27 @@ async function buildInternal(options = {}) {
384
421
  }
385
422
  }
386
423
  // Load cache manifest for ISG (after potential clean operation)
424
+ const endManifestLoadSpan = recorder.startSpan('cacheManifestLoadMs');
387
425
  const { manifest } = await setupCacheAndManifest(cacheDir);
426
+ endManifestLoadSpan();
388
427
  // Load content and build navigation
389
428
  if (logger.step) {
390
429
  console.log(); // Add spacing before content loading
391
430
  }
431
+ const endContentSpan = recorder.startSpan('contentDiscoveryMs');
392
432
  const { pages, navigation, md, eta, navigationHash } = await loadContentAndBuildNavigation(config, options, logger);
433
+ endContentSpan();
434
+ // Record content discovery counts.
435
+ // Both totalPages and markdownFilesProcessed are incremented by pages.length
436
+ // because loadContent() only processes *.md files, so all pages are markdown files.
437
+ recorder.increment('totalPages', pages.length);
438
+ recorder.increment('markdownFilesProcessed', pages.length);
393
439
  // Store navigation hash in manifest for change detection in dev server
394
440
  manifest.navigationHash = navigationHash;
395
441
  // Compile TypeScript if enabled
396
442
  let compiledBundles = [];
397
443
  if (config.typescript?.enabled) {
444
+ const endTsSpan = recorder.startSpan('typescriptCompileMs');
398
445
  compiledBundles = await compileTypeScript({
399
446
  projectRoot: process.cwd(),
400
447
  config: config.typescript,
@@ -402,15 +449,21 @@ async function buildInternal(options = {}) {
402
449
  mode: getEnv() === 'production' ? 'production' : 'development',
403
450
  logger,
404
451
  });
452
+ endTsSpan();
405
453
  }
406
454
  // Process pages with ISG caching logic
407
455
  if (logger.step) {
408
456
  console.log(); // Add spacing before page processing
409
457
  }
458
+ const endPageRenderSpan = recorder.startSpan('pageRenderingMs');
410
459
  const buildTime = new Date();
411
- const pageProcessingResult = await processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles);
460
+ const pageProcessingResult = await processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder);
461
+ endPageRenderSpan();
412
462
  cacheHits = pageProcessingResult.cacheHits;
413
463
  cacheMisses = pageProcessingResult.cacheMisses;
464
+ // Record page rendering counts
465
+ recorder.increment('renderedPages', cacheMisses);
466
+ recorder.increment('cachedPages', cacheHits);
414
467
  // Write Tailwind class inventory after all templates have been rendered (if Tailwind is used)
415
468
  if (hasTailwind) {
416
469
  const inventorySize = getInventorySize();
@@ -423,10 +476,15 @@ async function buildInternal(options = {}) {
423
476
  disableInventoryTracking();
424
477
  }
425
478
  // Save updated cache manifest
479
+ const endManifestSaveSpan = recorder.startSpan('cacheManifestSaveMs');
426
480
  await saveCacheManifest(cacheDir, manifest);
481
+ endManifestSaveSpan();
427
482
  // Copy static assets and count them
483
+ const endAssetSpan = recorder.startSpan('assetCopyMs');
428
484
  let assetsCount = 0;
429
485
  assetsCount = await copyStaticAssets(config, outDir, logger);
486
+ endAssetSpan();
487
+ recorder.increment('assetsCopied', assetsCount);
430
488
  // Get current environment
431
489
  const currentEnv = getEnv();
432
490
  // Generate sitemap if enabled (only in production mode)
@@ -435,8 +493,10 @@ async function buildInternal(options = {}) {
435
493
  console.log(); // Add spacing before sitemap generation
436
494
  }
437
495
  logger.info('Generating sitemap...');
496
+ const endSitemapSpan = recorder.startSpan('sitemapGenerationMs');
438
497
  const sitemapResult = generateSitemap(pages, config, config.sitemap);
439
498
  await writeFile(join(outDir, 'sitemap.xml'), sitemapResult.xml);
499
+ endSitemapSpan();
440
500
  // Write additional sitemap files if split
441
501
  if (sitemapResult.sitemaps) {
442
502
  for (const { filename, xml } of sitemapResult.sitemaps) {
@@ -506,9 +566,30 @@ async function buildInternal(options = {}) {
506
566
  }
507
567
  // Run afterAll hook
508
568
  if (config.hooks?.afterAll) {
569
+ const endHookAfterAll = recorder.startSpan('hookAfterAllMs');
509
570
  await config.hooks.afterAll({ config, pages });
571
+ endHookAfterAll();
510
572
  }
511
573
  // Calculate build statistics
512
574
  const buildStats = await generateBuildStats(pages, assetsCount, buildStartTime, outDir, cacheHits, cacheMisses, logger);
513
- return buildStats;
575
+ // Set ISG metrics
576
+ const totalPages = pages.length;
577
+ const cacheHitRate = totalPages > 0 ? cacheHits / totalPages : 0;
578
+ recorder.setISGMetrics({
579
+ enabled: config.isg?.enabled !== false,
580
+ cacheHitRate,
581
+ manifestEntries: Object.keys(manifest.entries).length,
582
+ invalidatedEntries: cacheMisses,
583
+ });
584
+ // Take final memory snapshot
585
+ recorder.snapshotMemory();
586
+ // Build result with optional metrics
587
+ const result = {
588
+ ...buildStats,
589
+ };
590
+ // Add metrics if enabled
591
+ if (recorder.enabled) {
592
+ result.buildMetrics = recorder.finalize();
593
+ }
594
+ return result;
514
595
  }
@@ -3,7 +3,7 @@
3
3
  * Barrel file for all core Stati functionality including build, dev server, preview, and invalidation.
4
4
  */
5
5
  export { build } from './build.js';
6
- export type { BuildOptions } from './build.js';
6
+ export type { BuildOptions, MetricsOptions, BuildResult } from './build.js';
7
7
  export { createDevServer } from './dev.js';
8
8
  export type { DevServerOptions } from './dev.js';
9
9
  export { createPreviewServer } from './preview.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC3C,YAAY,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAGjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,YAAY,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAGzD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAG5E,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC3C,YAAY,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAGjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,YAAY,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAGzD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC"}
@@ -1,5 +1,14 @@
1
1
  import { Eta } from 'eta';
2
2
  import type { StatiConfig, PageModel, NavNode } from '../types/index.js';
3
+ /**
4
+ * Result of rendering a page template.
5
+ */
6
+ export interface RenderResult {
7
+ /** The rendered HTML content */
8
+ html: string;
9
+ /** Number of templates loaded (layout + partials) */
10
+ templatesLoaded: number;
11
+ }
3
12
  export declare function createTemplateEngine(config: StatiConfig): Eta;
4
- export declare function renderPage(page: PageModel, body: string, config: StatiConfig, eta: Eta, navigation?: NavNode[], allPages?: PageModel[], assets?: import('../types/index.js').StatiAssets): Promise<string>;
13
+ export declare function renderPage(page: PageModel, body: string, config: StatiConfig, eta: Eta, navigation?: NavNode[], allPages?: PageModel[], assets?: import('../types/index.js').StatiAssets): Promise<RenderResult>;
5
14
  //# sourceMappingURL=templates.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/core/templates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAG1B,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAkB,MAAM,mBAAmB,CAAC;AAmNzF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,GAAG,CAW7D;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,EACnB,GAAG,EAAE,GAAG,EACR,UAAU,CAAC,EAAE,OAAO,EAAE,EACtB,QAAQ,CAAC,EAAE,SAAS,EAAE,EACtB,MAAM,CAAC,EAAE,OAAO,mBAAmB,EAAE,WAAW,GAC/C,OAAO,CAAC,MAAM,CAAC,CA+KjB"}
1
+ {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/core/templates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAG1B,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAkB,MAAM,mBAAmB,CAAC;AAiBzF;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,eAAe,EAAE,MAAM,CAAC;CACzB;AAoMD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,GAAG,CAW7D;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,EACnB,GAAG,EAAE,GAAG,EACR,UAAU,CAAC,EAAE,OAAO,EAAE,EACtB,QAAQ,CAAC,EAAE,SAAS,EAAE,EACtB,MAAM,CAAC,EAAE,OAAO,mBAAmB,EAAE,WAAW,GAC/C,OAAO,CAAC,YAAY,CAAC,CAsLvB"}
@@ -187,6 +187,8 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
187
187
  const srcDir = resolveSrcDir(config);
188
188
  const relativePath = relative(srcDir, page.sourcePath);
189
189
  const partialPaths = await discoverPartials(relativePath, config);
190
+ // Count templates: partials + layout (if found)
191
+ const partialsCount = Object.keys(partialPaths).length;
190
192
  // Build collection data based on page type
191
193
  let collectionData;
192
194
  const isIndexPage = allPages && isCollectionIndexPage(page, allPages);
@@ -203,6 +205,8 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
203
205
  // Discover the appropriate layout using hierarchical layout.eta convention
204
206
  // Pass isIndexPage flag to enable index.eta lookup for aggregation pages
205
207
  const layoutPath = await discoverLayout(relativePath, config, page.frontMatter.layout, isIndexPage);
208
+ // Calculate total templates loaded (partials + layout if present)
209
+ const templatesLoaded = partialsCount + (layoutPath ? 1 : 0);
206
210
  // Create navigation helpers
207
211
  const navTree = navigation || [];
208
212
  const navHelpers = createNavigationHelpers(navTree, page);
@@ -303,9 +307,10 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
303
307
  try {
304
308
  if (!layoutPath) {
305
309
  console.warn('No layout template found, using fallback');
306
- return createFallbackHtml(page, body);
310
+ return { html: createFallbackHtml(page, body), templatesLoaded };
307
311
  }
308
- return await eta.renderAsync(layoutPath, context);
312
+ const html = await eta.renderAsync(layoutPath, context);
313
+ return { html, templatesLoaded };
309
314
  }
310
315
  catch (error) {
311
316
  console.error(`Error rendering layout ${layoutPath || 'unknown'}:`, error);
@@ -314,7 +319,7 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
314
319
  const templateError = createTemplateError(error instanceof Error ? error : new Error(String(error)), layoutPath || undefined);
315
320
  throw templateError;
316
321
  }
317
- return createFallbackHtml(page, body);
322
+ return { html: createFallbackHtml(page, body), templatesLoaded };
318
323
  }
319
324
  }
320
325
  function createFallbackHtml(page, body) {
package/dist/index.d.ts CHANGED
@@ -23,8 +23,10 @@ export type { StatiConfig, PageModel, FrontMatter, BuildContext, PageContext, Bu
23
23
  export type { SEOMetadata, SEOConfig, SEOContext, SEOValidationResult, SEOTagType, RobotsConfig, OpenGraphConfig, OpenGraphImage, OpenGraphArticle, TwitterCardConfig, AuthorConfig, } from './types/index.js';
24
24
  export type { SitemapConfig, SitemapEntry, SitemapGenerationResult, ChangeFrequency, } from './types/index.js';
25
25
  export type { RSSConfig, RSSFeedConfig, RSSGenerationResult } from './types/index.js';
26
- export type { BuildOptions, DevServerOptions, PreviewServerOptions, InvalidationResult, } from './core/index.js';
26
+ export type { BuildOptions, MetricsOptions, BuildResult, DevServerOptions, PreviewServerOptions, InvalidationResult, } from './core/index.js';
27
27
  export { build, createDevServer, createPreviewServer, invalidate } from './core/index.js';
28
+ export type { BuildMetrics, MetricsMeta, MetricsFlags, MetricsTotals, MetricsPhases, MetricsCounts, MetricsISG, PageTiming, IncrementalMetrics, MetricRecorder, MetricRecorderOptions, PhaseName, CounterName, } from './metrics/index.js';
29
+ export { createMetricRecorder, noopMetricRecorder, writeMetrics, formatMetricsSummary, } from './metrics/index.js';
28
30
  export type { AutoInjectOptions } from './seo/index.js';
29
31
  export { generateSEOMetadata, generateSEO, generateOpenGraphTags, generateTwitterCardTags, generateSitemap, generateSitemapEntry, generateSitemapXml, generateSitemapIndexXml, generateRobotsTxt, generateRobotsTxtFromConfig, escapeHtml, generateRobotsContent, validateSEOMetadata, detectExistingSEOTags, normalizeUrlPath, resolveAbsoluteUrl, isValidUrl, autoInjectSEO, shouldAutoInject, } from './seo/index.js';
30
32
  export type { RSSValidationResult } from './rss/index.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,YAAY,EACV,WAAW,EACX,SAAS,EACT,WAAW,EACX,YAAY,EACZ,WAAW,EACX,UAAU,EACV,OAAO,EACP,SAAS,EACT,SAAS,EACT,UAAU,EACV,YAAY,GACb,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EACV,WAAW,EACX,SAAS,EACT,UAAU,EACV,mBAAmB,EACnB,UAAU,EACV,YAAY,EACZ,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,iBAAiB,EACjB,YAAY,GACb,MAAM,kBAAkB,CAAC;AAE1B,YAAY,EACV,aAAa,EACb,YAAY,EACZ,uBAAuB,EACvB,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAGtF,YAAY,EACV,YAAY,EACZ,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAG1F,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EACL,mBAAmB,EACnB,WAAW,EACX,qBAAqB,EACrB,uBAAuB,EACvB,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,uBAAuB,EACvB,iBAAiB,EACjB,2BAA2B,EAC3B,UAAU,EACV,qBAAqB,EACrB,mBAAmB,EACnB,qBAAqB,EACrB,gBAAgB,EAChB,kBAAkB,EAClB,UAAU,EACV,aAAa,EACb,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AAGxB,YAAY,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAG1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,CAE7D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,YAAY,EACV,WAAW,EACX,SAAS,EACT,WAAW,EACX,YAAY,EACZ,WAAW,EACX,UAAU,EACV,OAAO,EACP,SAAS,EACT,SAAS,EACT,UAAU,EACV,YAAY,GACb,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EACV,WAAW,EACX,SAAS,EACT,UAAU,EACV,mBAAmB,EACnB,UAAU,EACV,YAAY,EACZ,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,iBAAiB,EACjB,YAAY,GACb,MAAM,kBAAkB,CAAC;AAE1B,YAAY,EACV,aAAa,EACb,YAAY,EACZ,uBAAuB,EACvB,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAGtF,YAAY,EACV,YAAY,EACZ,cAAc,EACd,WAAW,EACX,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAG1F,YAAY,EACV,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,aAAa,EACb,aAAa,EACb,aAAa,EACb,UAAU,EACV,UAAU,EACV,kBAAkB,EAClB,cAAc,EACd,qBAAqB,EACrB,SAAS,EACT,WAAW,GACZ,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,oBAAoB,EACpB,kBAAkB,EAClB,YAAY,EACZ,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAG5B,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EACL,mBAAmB,EACnB,WAAW,EACX,qBAAqB,EACrB,uBAAuB,EACvB,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,uBAAuB,EACvB,iBAAiB,EACjB,2BAA2B,EAC3B,UAAU,EACV,qBAAqB,EACrB,mBAAmB,EACnB,qBAAqB,EACrB,gBAAgB,EAChB,kBAAkB,EAClB,UAAU,EACV,aAAa,EACb,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AAGxB,YAAY,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAG1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,CAE7D"}
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@
20
20
  * ```
21
21
  */
22
22
  export { build, createDevServer, createPreviewServer, invalidate } from './core/index.js';
23
+ export { createMetricRecorder, noopMetricRecorder, writeMetrics, formatMetricsSummary, } from './metrics/index.js';
23
24
  export { generateSEOMetadata, generateSEO, generateOpenGraphTags, generateTwitterCardTags, generateSitemap, generateSitemapEntry, generateSitemapXml, generateSitemapIndexXml, generateRobotsTxt, generateRobotsTxtFromConfig, escapeHtml, generateRobotsContent, validateSEOMetadata, detectExistingSEOTags, normalizeUrlPath, resolveAbsoluteUrl, isValidUrl, autoInjectSEO, shouldAutoInject, } from './seo/index.js';
24
25
  export { generateRSSFeed, generateRSSFeeds, validateRSSConfig, validateRSSFeedConfig, } from './rss/index.js';
25
26
  // Re-export config and env utilities
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Stati Build Metrics System
3
+ *
4
+ * Provides performance measurement and reporting for Stati builds.
5
+ * Uses Node.js built-ins only - no external dependencies.
6
+ *
7
+ * @example Basic Usage
8
+ * ```typescript
9
+ * import { createMetricRecorder, writeMetrics, formatMetricsSummary } from '@stati/core';
10
+ *
11
+ * // Create recorder (noop when disabled)
12
+ * const recorder = createMetricRecorder({ enabled: true });
13
+ *
14
+ * // Record phases with spans
15
+ * const endConfig = recorder.startSpan('configLoadMs');
16
+ * await loadConfig();
17
+ * endConfig();
18
+ *
19
+ * // Increment counters
20
+ * recorder.increment('renderedPages');
21
+ *
22
+ * // Finalize and write
23
+ * const metrics = recorder.finalize();
24
+ * await writeMetrics(metrics, { cacheDir: '.stati' });
25
+ *
26
+ * // Format for CLI
27
+ * const summary = formatMetricsSummary(metrics);
28
+ * summary.forEach(line => console.log(line));
29
+ * ```
30
+ *
31
+ * @packageDocumentation
32
+ */
33
+ export type { BuildMetrics, MetricsMeta, MetricsFlags, MetricsTotals, MetricsPhases, MetricsCounts, MetricsISG, PageTiming, IncrementalMetrics, MetricRecorder, MetricRecorderOptions, PhaseName, CounterName, GaugeName, } from './types.js';
34
+ export { createMetricRecorder } from './recorder.js';
35
+ export { noopMetricRecorder } from './noop.js';
36
+ export { writeMetrics, formatMetricsSummary, generateMetricsFilename, DEFAULT_METRICS_DIR, isCI, getGitCommit, getGitBranch, getCpuCount, getPlatform, getArch, getNodeVersion, getMemoryUsage, } from './utils/index.js';
37
+ export type { WriteMetricsOptions, WriteMetricsResult } from './utils/index.js';
38
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/metrics/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAGH,YAAY,EACV,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,aAAa,EACb,aAAa,EACb,aAAa,EACb,UAAU,EACV,UAAU,EACV,kBAAkB,EAClB,cAAc,EACd,qBAAqB,EACrB,SAAS,EACT,WAAW,EACX,SAAS,GACV,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAGrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAG/C,OAAO,EAEL,YAAY,EACZ,oBAAoB,EACpB,uBAAuB,EACvB,mBAAmB,EAEnB,IAAI,EACJ,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,WAAW,EACX,OAAO,EACP,cAAc,EACd,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Stati Build Metrics System
3
+ *
4
+ * Provides performance measurement and reporting for Stati builds.
5
+ * Uses Node.js built-ins only - no external dependencies.
6
+ *
7
+ * @example Basic Usage
8
+ * ```typescript
9
+ * import { createMetricRecorder, writeMetrics, formatMetricsSummary } from '@stati/core';
10
+ *
11
+ * // Create recorder (noop when disabled)
12
+ * const recorder = createMetricRecorder({ enabled: true });
13
+ *
14
+ * // Record phases with spans
15
+ * const endConfig = recorder.startSpan('configLoadMs');
16
+ * await loadConfig();
17
+ * endConfig();
18
+ *
19
+ * // Increment counters
20
+ * recorder.increment('renderedPages');
21
+ *
22
+ * // Finalize and write
23
+ * const metrics = recorder.finalize();
24
+ * await writeMetrics(metrics, { cacheDir: '.stati' });
25
+ *
26
+ * // Format for CLI
27
+ * const summary = formatMetricsSummary(metrics);
28
+ * summary.forEach(line => console.log(line));
29
+ * ```
30
+ *
31
+ * @packageDocumentation
32
+ */
33
+ // Recorder factory
34
+ export { createMetricRecorder } from './recorder.js';
35
+ // Noop recorder singleton
36
+ export { noopMetricRecorder } from './noop.js';
37
+ // Utilities (file writing, system info)
38
+ export {
39
+ // Writer utilities
40
+ writeMetrics, formatMetricsSummary, generateMetricsFilename, DEFAULT_METRICS_DIR,
41
+ // System utilities
42
+ isCI, getGitCommit, getGitBranch, getCpuCount, getPlatform, getArch, getNodeVersion, getMemoryUsage, } from './utils/index.js';
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Noop MetricRecorder implementation.
3
+ * Used when metrics collection is disabled - has zero overhead.
4
+ * (Null Object Pattern)
5
+ */
6
+ import type { MetricRecorder } from './types.js';
7
+ /**
8
+ * Singleton noop recorder instance.
9
+ * Use this when metrics are disabled for zero-overhead operation.
10
+ */
11
+ export declare const noopMetricRecorder: MetricRecorder;
12
+ //# sourceMappingURL=noop.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"noop.d.ts","sourceRoot":"","sources":["../../src/metrics/noop.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,cAAc,EAOf,MAAM,YAAY,CAAC;AAiGpB;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,cAAyC,CAAC"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Noop MetricRecorder implementation.
3
+ * Used when metrics collection is disabled - has zero overhead.
4
+ * (Null Object Pattern)
5
+ */
6
+ /**
7
+ * Empty function that does nothing.
8
+ */
9
+ const noop = () => {
10
+ /* intentionally empty */
11
+ };
12
+ /**
13
+ * Noop implementation of MetricRecorder.
14
+ * All methods are no-ops with minimal overhead.
15
+ */
16
+ class NoopMetricRecorder {
17
+ enabled = false;
18
+ detailed = false;
19
+ startSpan(_name) {
20
+ return noop;
21
+ }
22
+ recordPhase(_name, _durationMs) {
23
+ /* noop */
24
+ }
25
+ addToPhase(_name, _durationMs) {
26
+ /* noop */
27
+ }
28
+ increment(_name, _amount) {
29
+ /* noop */
30
+ }
31
+ setGauge(_name, _value) {
32
+ /* noop */
33
+ }
34
+ recordPageTiming(_url, _durationMs, _cached, _templatesLoaded) {
35
+ /* noop */
36
+ }
37
+ snapshotMemory() {
38
+ /* noop */
39
+ }
40
+ setISGMetrics(_metrics) {
41
+ /* noop */
42
+ }
43
+ setIncrementalMetrics(_metrics) {
44
+ /* noop */
45
+ }
46
+ finalize() {
47
+ // Return minimal valid metrics object
48
+ return {
49
+ schemaVersion: '1',
50
+ meta: {
51
+ timestamp: new Date().toISOString(),
52
+ ci: false,
53
+ nodeVersion: process.version.replace(/^v/, ''),
54
+ platform: process.platform,
55
+ arch: process.arch,
56
+ cpuCount: 1,
57
+ statiVersion: 'unknown',
58
+ command: 'build',
59
+ flags: {},
60
+ },
61
+ totals: {
62
+ durationMs: 0,
63
+ peakRssBytes: 0,
64
+ heapUsedBytes: 0,
65
+ },
66
+ phases: {},
67
+ counts: {
68
+ totalPages: 0,
69
+ renderedPages: 0,
70
+ cachedPages: 0,
71
+ assetsCopied: 0,
72
+ templatesLoaded: 0,
73
+ markdownFilesProcessed: 0,
74
+ },
75
+ isg: {
76
+ enabled: false,
77
+ cacheHitRate: 0,
78
+ manifestEntries: 0,
79
+ invalidatedEntries: 0,
80
+ },
81
+ };
82
+ }
83
+ }
84
+ /**
85
+ * Singleton noop recorder instance.
86
+ * Use this when metrics are disabled for zero-overhead operation.
87
+ */
88
+ export const noopMetricRecorder = new NoopMetricRecorder();