@stati/core 1.0.0 → 1.2.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 (41) hide show
  1. package/README.md +217 -0
  2. package/dist/config/loader.d.ts.map +1 -1
  3. package/dist/config/loader.js +24 -2
  4. package/dist/core/build.d.ts +9 -2
  5. package/dist/core/build.d.ts.map +1 -1
  6. package/dist/core/build.js +200 -46
  7. package/dist/core/dev.d.ts +21 -0
  8. package/dist/core/dev.d.ts.map +1 -0
  9. package/dist/core/dev.js +371 -0
  10. package/dist/core/invalidate.d.ts +67 -1
  11. package/dist/core/invalidate.d.ts.map +1 -1
  12. package/dist/core/invalidate.js +321 -4
  13. package/dist/core/isg/build-lock.d.ts +116 -0
  14. package/dist/core/isg/build-lock.d.ts.map +1 -0
  15. package/dist/core/isg/build-lock.js +243 -0
  16. package/dist/core/isg/builder.d.ts +51 -0
  17. package/dist/core/isg/builder.d.ts.map +1 -0
  18. package/dist/core/isg/builder.js +321 -0
  19. package/dist/core/isg/deps.d.ts +63 -0
  20. package/dist/core/isg/deps.d.ts.map +1 -0
  21. package/dist/core/isg/deps.js +332 -0
  22. package/dist/core/isg/hash.d.ts +48 -0
  23. package/dist/core/isg/hash.d.ts.map +1 -0
  24. package/dist/core/isg/hash.js +82 -0
  25. package/dist/core/isg/manifest.d.ts +47 -0
  26. package/dist/core/isg/manifest.d.ts.map +1 -0
  27. package/dist/core/isg/manifest.js +233 -0
  28. package/dist/core/isg/ttl.d.ts +101 -0
  29. package/dist/core/isg/ttl.d.ts.map +1 -0
  30. package/dist/core/isg/ttl.js +222 -0
  31. package/dist/core/isg/validation.d.ts +71 -0
  32. package/dist/core/isg/validation.d.ts.map +1 -0
  33. package/dist/core/isg/validation.js +226 -0
  34. package/dist/core/templates.d.ts.map +1 -1
  35. package/dist/core/templates.js +23 -5
  36. package/dist/index.d.ts +3 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -0
  39. package/dist/types.d.ts +172 -0
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +7 -3
package/README.md ADDED
@@ -0,0 +1,217 @@
1
+ # @stati/core
2
+
3
+ The core engine for Stati, a lightweight TypeScript static site generator built with Vite-inspired architecture.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @stati/core
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Basic Setup
14
+
15
+ ```typescript
16
+ import { build, createDevServer, defineConfig } from '@stati/core';
17
+
18
+ // Define configuration
19
+ const config = defineConfig({
20
+ site: './site',
21
+ output: './dist',
22
+ public: './public',
23
+ });
24
+
25
+ // Build site
26
+ await build(config);
27
+
28
+ // Or start development server
29
+ const server = await createDevServer(config, {
30
+ port: 3000,
31
+ open: true,
32
+ });
33
+ ```
34
+
35
+ ### Configuration
36
+
37
+ ```typescript
38
+ import { defineConfig } from '@stati/core/config';
39
+
40
+ export default defineConfig({
41
+ // Site source directory
42
+ site: './site',
43
+
44
+ // Output directory for built site
45
+ output: './dist',
46
+
47
+ // Static assets directory
48
+ public: './public',
49
+
50
+ // Site metadata
51
+ meta: {
52
+ title: 'My Site',
53
+ description: 'A great static site',
54
+ url: 'https://example.com',
55
+ },
56
+
57
+ // Markdown configuration
58
+ markdown: {
59
+ plugins: ['markdown-it-anchor'],
60
+ options: {
61
+ html: true,
62
+ linkify: true,
63
+ typographer: true,
64
+ },
65
+ },
66
+
67
+ // Template configuration
68
+ templates: {
69
+ engine: 'eta',
70
+ options: {
71
+ views: './site',
72
+ cache: true,
73
+ },
74
+ },
75
+ });
76
+ ```
77
+
78
+ ## API
79
+
80
+ ### Core Functions
81
+
82
+ #### `build(options: BuildOptions): Promise<void>`
83
+
84
+ Build a static site.
85
+
86
+ ```typescript
87
+ import { build } from '@stati/core';
88
+
89
+ await build({
90
+ config: './stati.config.js',
91
+ force: false,
92
+ clean: false,
93
+ includeDrafts: false,
94
+ });
95
+ ```
96
+
97
+ #### `createDevServer(config: StatiConfig, options: DevServerOptions): Promise<DevServer>`
98
+
99
+ Create a development server with live reload.
100
+
101
+ ```typescript
102
+ import { createDevServer } from '@stati/core';
103
+
104
+ const server = await createDevServer(config, {
105
+ port: 3000,
106
+ open: true,
107
+ });
108
+ ```
109
+
110
+ #### `invalidate(query?: string): Promise<InvalidationResult>`
111
+
112
+ Invalidate cache by tags, paths, patterns, or age.
113
+
114
+ ```typescript
115
+ import { invalidate } from '@stati/core';
116
+
117
+ // Invalidate by tag
118
+ await invalidate('tag:blog');
119
+
120
+ // Invalidate by path prefix
121
+ await invalidate('path:/posts');
122
+
123
+ // Invalidate by glob pattern
124
+ await invalidate('glob:/blog/**');
125
+
126
+ // Invalidate content younger than 3 months
127
+ await invalidate('age:3months');
128
+
129
+ // Multiple criteria (OR logic)
130
+ await invalidate('tag:blog age:1month');
131
+
132
+ // Clear entire cache
133
+ await invalidate();
134
+ ```
135
+
136
+ ### Configuration
137
+
138
+ #### `defineConfig(config: StatiConfig): StatiConfig`
139
+
140
+ Define a type-safe configuration with full TypeScript support.
141
+
142
+ ```typescript
143
+ import { defineConfig } from '@stati/core/config';
144
+
145
+ export default defineConfig({
146
+ // Your configuration here
147
+ });
148
+ ```
149
+
150
+ ## Types
151
+
152
+ The package exports comprehensive TypeScript types:
153
+
154
+ ```typescript
155
+ import type {
156
+ StatiConfig,
157
+ BuildOptions,
158
+ DevServerOptions,
159
+ InvalidateOptions,
160
+ Page,
161
+ Navigation,
162
+ MarkdownOptions,
163
+ TemplateOptions,
164
+ } from '@stati/core/types';
165
+ ```
166
+
167
+ ## Features
168
+
169
+ ### Markdown Processing
170
+
171
+ - **Front-matter support** with YAML, TOML, or JSON
172
+ - **Plugin system** using markdown-it ecosystem
173
+ - **Custom rendering** with configurable options
174
+ - **Draft pages** with `draft: true` in front-matter
175
+
176
+ ### Template Engine
177
+
178
+ - **Eta templates** with layouts and partials
179
+ - **Template inheritance** with `layout` front-matter property
180
+ - **Custom helpers** and filters
181
+ - **Hot reload** during development
182
+
183
+ ### Navigation System
184
+
185
+ - **Automatic hierarchy** based on filesystem structure
186
+ - **Breadcrumbs** and navigation trees
187
+ - **Custom sorting** with `order` front-matter property
188
+ - **Index pages** with special handling
189
+
190
+ ### Development Server
191
+
192
+ - **Live reload** with WebSocket integration
193
+ - **Hot rebuilding** on file changes
194
+ - **Static asset serving** from public directory
195
+ - **Error overlay** for development debugging
196
+
197
+ ### Caching & Performance
198
+
199
+ - **Smart caching** based on file modification times
200
+ - **Incremental builds** for faster rebuilds
201
+ - **Tag-based invalidation** for selective cache clearing
202
+ - **Memory optimization** for large sites
203
+
204
+ ## Architecture
205
+
206
+ Stati Core is built with a modular architecture:
207
+
208
+ - **Content processing** - Markdown parsing and front-matter extraction
209
+ - **Template rendering** - Eta engine with layouts and partials
210
+ - **Navigation building** - Automatic hierarchy generation
211
+ - **Asset handling** - Static file copying and optimization
212
+ - **Development server** - Live reload and hot rebuilding
213
+ - **Build system** - Production optimization and output generation
214
+
215
+ ## License
216
+
217
+ MIT
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAqB/C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,UAAU,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,WAAW,CAAC,CAkClF"}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAsB/C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,UAAU,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,WAAW,CAAC,CA2DlF"}
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from 'fs';
2
2
  import { join, resolve } from 'path';
3
3
  import { pathToFileURL } from 'url';
4
+ import { validateISGConfig, ISGConfigurationError } from '../core/isg/validation.js';
4
5
  /**
5
6
  * Default configuration values for Stati.
6
7
  * Used as fallback when no configuration file is found.
@@ -59,15 +60,36 @@ export async function loadConfig(cwd = process.cwd()) {
59
60
  const configUrl = pathToFileURL(resolve(configPath)).href;
60
61
  const module = await import(configUrl);
61
62
  const userConfig = module.default || module;
62
- return {
63
+ const mergedConfig = {
63
64
  ...DEFAULT_CONFIG,
64
65
  ...userConfig,
65
66
  site: { ...DEFAULT_CONFIG.site, ...userConfig.site },
66
67
  isg: { ...DEFAULT_CONFIG.isg, ...userConfig.isg },
67
68
  };
69
+ // Validate ISG configuration
70
+ try {
71
+ if (mergedConfig.isg) {
72
+ validateISGConfig(mergedConfig.isg);
73
+ }
74
+ }
75
+ catch (error) {
76
+ if (error instanceof ISGConfigurationError) {
77
+ throw new Error(`Invalid ISG configuration in ${configPath}:\n` +
78
+ `${error.code}: ${error.message}\n` +
79
+ `Field: ${error.field}, Value: ${JSON.stringify(error.value)}\n\n` +
80
+ `Please check your stati.config.ts file and correct the ISG configuration.`);
81
+ }
82
+ throw error; // Re-throw non-ISG errors
83
+ }
84
+ return mergedConfig;
68
85
  }
69
86
  catch (error) {
70
- console.error('Error loading config:', error);
87
+ if (error instanceof Error && error.message.includes('Invalid ISG configuration')) {
88
+ // ISG validation errors should bubble up with context
89
+ throw error;
90
+ }
91
+ console.error(`Error loading config from ${configPath}:`, error);
92
+ console.error('Falling back to default configuration.');
71
93
  return DEFAULT_CONFIG;
72
94
  }
73
95
  }
@@ -1,4 +1,4 @@
1
- import type { BuildStats } from '../types.js';
1
+ import type { BuildStats, Logger } from '../types.js';
2
2
  /**
3
3
  * Options for customizing the build process.
4
4
  *
@@ -8,7 +8,8 @@ import type { BuildStats } from '../types.js';
8
8
  * force: true, // Force rebuild of all pages
9
9
  * clean: true, // Clean output directory before build
10
10
  * configPath: './custom.config.js', // Custom config file path
11
- * includeDrafts: true // Include draft pages in build
11
+ * includeDrafts: true, // Include draft pages in build
12
+ * version: '1.0.0' // Version to display in build messages
12
13
  * };
13
14
  * ```
14
15
  */
@@ -21,10 +22,15 @@ export interface BuildOptions {
21
22
  configPath?: string;
22
23
  /** Include draft pages in the build */
23
24
  includeDrafts?: boolean;
25
+ /** Custom logger for build output */
26
+ logger?: Logger;
27
+ /** Version information to display in build messages */
28
+ version?: string;
24
29
  }
25
30
  /**
26
31
  * Builds the static site by processing content files and generating HTML pages.
27
32
  * This is the main entry point for Stati's build process.
33
+ * Uses build locking to prevent concurrent builds from corrupting cache.
28
34
  *
29
35
  * @param options - Build configuration options
30
36
  *
@@ -46,6 +52,7 @@ export interface BuildOptions {
46
52
  * @throws {Error} When configuration loading fails
47
53
  * @throws {Error} When content processing fails
48
54
  * @throws {Error} When template rendering fails
55
+ * @throws {Error} When build lock cannot be acquired
49
56
  */
50
57
  export declare function build(options?: BuildOptions): Promise<BuildStats>;
51
58
  //# sourceMappingURL=build.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/core/build.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAgB,UAAU,EAAE,MAAM,aAAa,CAAC;AAE5D;;;;;;;;;;;;GAYG;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;CACzB;AAoFD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,UAAU,CAAC,CAyG3E"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/core/build.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAgB,UAAU,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAEpE;;;;;;;;;;;;;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;AA2HD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,UAAU,CAAC,CAQ3E"}
@@ -1,11 +1,15 @@
1
1
  import fse from 'fs-extra';
2
- const { ensureDir, writeFile, copy, remove, pathExists, stat, readdir } = fse;
3
- import { join, dirname } from 'path';
2
+ const { ensureDir, writeFile, remove, pathExists, stat, readdir, copyFile } = fse;
3
+ import { join, dirname, relative } from 'path';
4
+ import { posix } from 'path';
4
5
  import { loadConfig } from '../config/loader.js';
5
6
  import { loadContent } from './content.js';
6
7
  import { createMarkdownProcessor, renderMarkdown } from './markdown.js';
7
8
  import { createTemplateEngine, renderPage } from './templates.js';
8
9
  import { buildNavigation } from './navigation.js';
10
+ import { loadCacheManifest, saveCacheManifest } from './isg/manifest.js';
11
+ import { shouldRebuildPage, createCacheEntry, updateCacheEntry } from './isg/builder.js';
12
+ import { withBuildLock } from './isg/build-lock.js';
9
13
  /**
10
14
  * Recursively calculates the total size of a directory in bytes.
11
15
  * Used for build statistics.
@@ -32,31 +36,59 @@ async function getDirectorySize(dirPath) {
32
36
  return totalSize;
33
37
  }
34
38
  /**
35
- * Counts the number of files in a directory recursively.
36
- * Used for build statistics.
39
+ * Recursively copies static assets from source to destination directory
40
+ * while logging each file being copied.
37
41
  *
38
- * @param dirPath - Path to the directory
39
- * @returns Total number of files
42
+ * @param sourceDir - Source directory containing static assets
43
+ * @param destDir - Destination directory to copy assets to
44
+ * @param logger - Logger instance for output
45
+ * @param basePath - Base path for relative path calculation (for recursion)
46
+ * @returns Total number of files copied
40
47
  */
41
- async function countFilesInDirectory(dirPath) {
42
- if (!(await pathExists(dirPath))) {
48
+ async function copyStaticAssetsWithLogging(sourceDir, destDir, logger, basePath = '') {
49
+ let filesCopied = 0;
50
+ if (!(await pathExists(sourceDir))) {
43
51
  return 0;
44
52
  }
45
- let fileCount = 0;
46
- const items = await readdir(dirPath, { withFileTypes: true });
53
+ const items = await readdir(sourceDir, { withFileTypes: true });
47
54
  for (const item of items) {
48
- const itemPath = join(dirPath, item.name);
55
+ const sourcePath = join(sourceDir, item.name);
56
+ const destPath = join(destDir, basePath, item.name);
57
+ const relativePath = posix.normalize(posix.join(basePath, item.name));
49
58
  if (item.isDirectory()) {
50
- fileCount += await countFilesInDirectory(itemPath);
59
+ // Recursively copy directories
60
+ await ensureDir(destPath);
61
+ filesCopied += await copyStaticAssetsWithLogging(sourcePath, destPath, logger, relativePath);
51
62
  }
52
63
  else {
53
- fileCount++;
64
+ // Copy individual files
65
+ await ensureDir(dirname(destPath));
66
+ await copyFile(sourcePath, destPath);
67
+ if (logger.file) {
68
+ logger.file('copy', relativePath);
69
+ }
70
+ else {
71
+ logger.processing(`📄 ${relativePath}`);
72
+ }
73
+ filesCopied++;
54
74
  }
55
75
  }
56
- return fileCount;
76
+ return filesCopied;
57
77
  }
58
78
  /**
59
- * Formats build statistics for display.
79
+ * Default console logger implementation.
80
+ */
81
+ const defaultLogger = {
82
+ info: (message) => console.log(message),
83
+ success: (message) => console.log(message),
84
+ warning: (message) => console.warn(message),
85
+ error: (message) => console.error(message),
86
+ building: (message) => console.log(message),
87
+ processing: (message) => console.log(message),
88
+ stats: (message) => console.log(message),
89
+ };
90
+ /**
91
+ * Formats build statistics for display with prettier output.
60
92
  *
61
93
  * @param stats - Build statistics to format
62
94
  * @returns Formatted statistics string
@@ -64,22 +96,25 @@ async function countFilesInDirectory(dirPath) {
64
96
  function formatBuildStats(stats) {
65
97
  const sizeKB = (stats.outputSizeBytes / 1024).toFixed(1);
66
98
  const timeSeconds = (stats.buildTimeMs / 1000).toFixed(2);
67
- let output = `📊 Build Statistics:
68
- ⏱️ Build time: ${timeSeconds}s
69
- 📄 Pages built: ${stats.totalPages}
70
- 📦 Assets copied: ${stats.assetsCount}
71
- 💾 Output size: ${sizeKB} KB`;
99
+ let output = `Build Statistics:
100
+ ┌─────────────────────────────────────────┐
101
+ Build time: ${timeSeconds}s`.padEnd(41) + '│';
102
+ output += `\n│ 📄 Pages built: ${stats.totalPages}`.padEnd(42) + '│';
103
+ output += `\n│ 📦 Assets copied: ${stats.assetsCount}`.padEnd(42) + '│';
104
+ output += `\n│ Output size: ${sizeKB} KB`.padEnd(42) + '│';
72
105
  if (stats.cacheHits !== undefined && stats.cacheMisses !== undefined) {
73
106
  const totalCacheRequests = stats.cacheHits + stats.cacheMisses;
74
107
  const hitRate = totalCacheRequests > 0 ? ((stats.cacheHits / totalCacheRequests) * 100).toFixed(1) : '0';
75
- output += `
76
- 🎯 Cache hits: ${stats.cacheHits}/${totalCacheRequests} (${hitRate}%)`;
108
+ output +=
109
+ `\n│ Cache hits: ${stats.cacheHits}/${totalCacheRequests} (${hitRate}%)`.padEnd(42) + '│';
77
110
  }
111
+ output += '\n└─────────────────────────────────────────┘';
78
112
  return output;
79
113
  }
80
114
  /**
81
115
  * Builds the static site by processing content files and generating HTML pages.
82
116
  * This is the main entry point for Stati's build process.
117
+ * Uses build locking to prevent concurrent builds from corrupting cache.
83
118
  *
84
119
  * @param options - Build configuration options
85
120
  *
@@ -101,29 +136,64 @@ function formatBuildStats(stats) {
101
136
  * @throws {Error} When configuration loading fails
102
137
  * @throws {Error} When content processing fails
103
138
  * @throws {Error} When template rendering fails
139
+ * @throws {Error} When build lock cannot be acquired
104
140
  */
105
141
  export async function build(options = {}) {
142
+ const cacheDir = join(process.cwd(), '.stati');
143
+ // Use build lock to prevent concurrent builds, with force option to override
144
+ return await withBuildLock(cacheDir, () => buildInternal(options), {
145
+ force: Boolean(options.force || options.clean), // Allow force if user explicitly requests it
146
+ timeout: 60000, // 1 minute timeout
147
+ });
148
+ }
149
+ /**
150
+ * Internal build implementation without locking.
151
+ * Separated for cleaner error handling and testing.
152
+ */
153
+ async function buildInternal(options = {}) {
106
154
  const buildStartTime = Date.now();
107
- console.log('🏗️ Building site...');
155
+ const logger = options.logger || defaultLogger;
156
+ logger.building('Building your site...');
157
+ console.log(); // Add spacing after build start
108
158
  // Load configuration
109
159
  const config = await loadConfig(options.configPath ? dirname(options.configPath) : process.cwd());
110
160
  const outDir = join(process.cwd(), config.outDir);
111
161
  // Create .stati cache directory
112
162
  const cacheDir = join(process.cwd(), '.stati');
113
163
  await ensureDir(cacheDir);
164
+ // Load cache manifest for ISG
165
+ let cacheManifest = await loadCacheManifest(cacheDir);
166
+ // If no cache manifest exists, create an empty one
167
+ if (!cacheManifest) {
168
+ cacheManifest = {
169
+ entries: {},
170
+ };
171
+ }
172
+ // At this point cacheManifest is guaranteed to be non-null
173
+ const manifest = cacheManifest;
174
+ // Initialize cache stats
175
+ let cacheHits = 0;
176
+ let cacheMisses = 0;
114
177
  // Clean output directory if requested
115
178
  if (options.clean) {
116
- console.log('🧹 Cleaning output directory...');
179
+ logger.info('Cleaning output directory...');
117
180
  await remove(outDir);
118
181
  }
119
182
  await ensureDir(outDir);
120
183
  // Load all content
121
184
  const pages = await loadContent(config, options.includeDrafts);
122
- console.log(`📄 Found ${pages.length} pages`);
185
+ logger.info(`📄 Found ${pages.length} pages`);
123
186
  // Build navigation from pages
187
+ console.log(); // Add spacing before navigation step
188
+ if (logger.step) {
189
+ logger.step(1, 3, 'Building navigation');
190
+ }
124
191
  const navigation = buildNavigation(pages);
125
- console.log(`🧭 Built navigation with ${navigation.length} top-level items`);
126
- // Create processors
192
+ logger.info(`Built navigation with ${navigation.length} top-level items`);
193
+ // Display navigation tree if the logger supports it
194
+ if (logger.navigationTree) {
195
+ logger.navigationTree(navigation);
196
+ } // Create processors
127
197
  const md = await createMarkdownProcessor(config);
128
198
  const eta = createTemplateEngine(config);
129
199
  // Build context
@@ -132,18 +202,28 @@ export async function build(options = {}) {
132
202
  if (config.hooks?.beforeAll) {
133
203
  await config.hooks.beforeAll(buildContext);
134
204
  }
135
- // Render each page
136
- for (const page of pages) {
137
- console.log(` Building ${page.url}`);
138
- // Run beforeRender hook
139
- if (config.hooks?.beforeRender) {
140
- await config.hooks.beforeRender({ page, config });
205
+ // Render each page with tree-based progress tracking and ISG
206
+ if (logger.step) {
207
+ logger.step(2, 3, 'Rendering pages');
208
+ }
209
+ // Initialize rendering tree
210
+ if (logger.startRenderingTree) {
211
+ logger.startRenderingTree('Page Rendering Process');
212
+ }
213
+ const buildTime = new Date();
214
+ for (let i = 0; i < pages.length; i++) {
215
+ const page = pages[i];
216
+ if (!page)
217
+ 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 });
141
222
  }
142
- // Render markdown to HTML
143
- const htmlContent = renderMarkdown(page.content, md);
144
- // Render with template
145
- const finalHtml = await renderPage(page, htmlContent, config, eta, navigation, pages);
146
- // Determine output path - fix the logic here
223
+ else {
224
+ logger.processing(`Checking ${page.url}`);
225
+ }
226
+ // Determine output path
147
227
  let outputPath;
148
228
  if (page.url === '/') {
149
229
  outputPath = join(outDir, 'index.html');
@@ -154,21 +234,89 @@ export async function build(options = {}) {
154
234
  else {
155
235
  outputPath = join(outDir, `${page.url}.html`);
156
236
  }
237
+ // Get cache key (use output path relative to outDir)
238
+ const relativePath = relative(outDir, outputPath).replace(/\\/g, '/');
239
+ const cacheKey = relativePath.startsWith('/') ? relativePath : `/${relativePath}`;
240
+ const existingEntry = manifest.entries[cacheKey];
241
+ // Check if we should rebuild this page (considering ISG logic)
242
+ const shouldRebuild = options.force || (await shouldRebuildPage(page, existingEntry, config, buildTime));
243
+ if (!shouldRebuild) {
244
+ // Cache hit - skip rendering
245
+ cacheHits++;
246
+ if (logger.updateTreeNode) {
247
+ logger.updateTreeNode(pageId, 'cached', { cacheHit: true, url: page.url });
248
+ }
249
+ else {
250
+ logger.processing(`📋 Cached ${page.url}`);
251
+ }
252
+ continue;
253
+ }
254
+ // Cache miss - need to rebuild
255
+ cacheMisses++;
256
+ const startTime = Date.now();
257
+ // Add rendering substeps to tree
258
+ const markdownId = `${pageId}-markdown`;
259
+ const templateId = `${pageId}-template`;
260
+ if (logger.addTreeNode) {
261
+ logger.addTreeNode(pageId, markdownId, 'Processing Markdown', 'running');
262
+ logger.addTreeNode(pageId, templateId, 'Applying Template', 'pending');
263
+ }
264
+ // Run beforeRender hook
265
+ if (config.hooks?.beforeRender) {
266
+ await config.hooks.beforeRender({ page, config });
267
+ }
268
+ // Render markdown to HTML
269
+ const htmlContent = renderMarkdown(page.content, md);
270
+ if (logger.updateTreeNode) {
271
+ logger.updateTreeNode(markdownId, 'completed');
272
+ logger.updateTreeNode(templateId, 'running');
273
+ }
274
+ // Render with template
275
+ const finalHtml = await renderPage(page, htmlContent, config, eta, navigation, pages);
276
+ const renderTime = Date.now() - startTime;
277
+ if (logger.updateTreeNode) {
278
+ logger.updateTreeNode(templateId, 'completed');
279
+ logger.updateTreeNode(pageId, 'completed', {
280
+ timing: renderTime,
281
+ url: page.url,
282
+ });
283
+ }
157
284
  // Ensure directory exists and write file
158
285
  await ensureDir(dirname(outputPath));
159
286
  await writeFile(outputPath, finalHtml, 'utf-8');
287
+ // Update cache manifest
288
+ if (existingEntry) {
289
+ manifest.entries[cacheKey] = await updateCacheEntry(existingEntry, page, config, buildTime);
290
+ }
291
+ else {
292
+ manifest.entries[cacheKey] = await createCacheEntry(page, config, buildTime);
293
+ }
160
294
  // Run afterRender hook
161
295
  if (config.hooks?.afterRender) {
162
296
  await config.hooks.afterRender({ page, config });
163
297
  }
164
298
  }
299
+ // Display final rendering tree and clear it
300
+ if (logger.showRenderingTree) {
301
+ console.log(); // Add spacing
302
+ logger.showRenderingTree();
303
+ if (logger.clearRenderingTree) {
304
+ logger.clearRenderingTree();
305
+ }
306
+ }
307
+ // Save updated cache manifest
308
+ await saveCacheManifest(cacheDir, manifest);
165
309
  // Copy static assets and count them
166
310
  let assetsCount = 0;
167
311
  const staticDir = join(process.cwd(), config.staticDir);
168
312
  if (await pathExists(staticDir)) {
169
- await copy(staticDir, outDir, { overwrite: true });
170
- assetsCount = await countFilesInDirectory(staticDir);
171
- console.log(`📦 Copied ${assetsCount} static assets`);
313
+ console.log(); // Add spacing before asset copying
314
+ if (logger.step) {
315
+ logger.step(3, 3, 'Copying static assets');
316
+ }
317
+ logger.info(`Copying static assets from ${config.staticDir}`);
318
+ assetsCount = await copyStaticAssetsWithLogging(staticDir, outDir, logger);
319
+ logger.info(`Copied ${assetsCount} static assets`);
172
320
  }
173
321
  // Run afterAll hook
174
322
  if (config.hooks?.afterAll) {
@@ -181,11 +329,17 @@ export async function build(options = {}) {
181
329
  assetsCount,
182
330
  buildTimeMs: buildEndTime - buildStartTime,
183
331
  outputSizeBytes: await getDirectorySize(outDir),
184
- // Cache stats would be populated here when caching is implemented
185
- cacheHits: 0,
186
- cacheMisses: 0,
332
+ // Include ISG cache statistics
333
+ cacheHits,
334
+ cacheMisses,
187
335
  };
188
- console.log('✅ Build complete!');
189
- console.log(formatBuildStats(buildStats));
336
+ console.log(); // Add spacing before statistics
337
+ // Use table format if available, otherwise fall back to formatted string
338
+ if (logger.statsTable) {
339
+ logger.statsTable(buildStats);
340
+ }
341
+ else {
342
+ logger.stats(formatBuildStats(buildStats));
343
+ }
190
344
  return buildStats;
191
345
  }
@@ -0,0 +1,21 @@
1
+ import type { Logger } from '../types.js';
2
+ export interface DevServerOptions {
3
+ port?: number;
4
+ host?: string;
5
+ open?: boolean;
6
+ configPath?: string;
7
+ logger?: Logger;
8
+ }
9
+ export interface DevServer {
10
+ start(): Promise<void>;
11
+ stop(): Promise<void>;
12
+ url: string;
13
+ }
14
+ /**
15
+ * Creates and configures a development server with live reload functionality.
16
+ *
17
+ * @param options - Development server configuration options
18
+ * @returns Promise resolving to a DevServer instance
19
+ */
20
+ export declare function createDevServer(options?: DevServerOptions): Promise<DevServer>;
21
+ //# sourceMappingURL=dev.d.ts.map