@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.
- package/README.md +217 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +24 -2
- package/dist/core/build.d.ts +9 -2
- package/dist/core/build.d.ts.map +1 -1
- package/dist/core/build.js +200 -46
- package/dist/core/dev.d.ts +21 -0
- package/dist/core/dev.d.ts.map +1 -0
- package/dist/core/dev.js +371 -0
- package/dist/core/invalidate.d.ts +67 -1
- package/dist/core/invalidate.d.ts.map +1 -1
- package/dist/core/invalidate.js +321 -4
- package/dist/core/isg/build-lock.d.ts +116 -0
- package/dist/core/isg/build-lock.d.ts.map +1 -0
- package/dist/core/isg/build-lock.js +243 -0
- package/dist/core/isg/builder.d.ts +51 -0
- package/dist/core/isg/builder.d.ts.map +1 -0
- package/dist/core/isg/builder.js +321 -0
- package/dist/core/isg/deps.d.ts +63 -0
- package/dist/core/isg/deps.d.ts.map +1 -0
- package/dist/core/isg/deps.js +332 -0
- package/dist/core/isg/hash.d.ts +48 -0
- package/dist/core/isg/hash.d.ts.map +1 -0
- package/dist/core/isg/hash.js +82 -0
- package/dist/core/isg/manifest.d.ts +47 -0
- package/dist/core/isg/manifest.d.ts.map +1 -0
- package/dist/core/isg/manifest.js +233 -0
- package/dist/core/isg/ttl.d.ts +101 -0
- package/dist/core/isg/ttl.d.ts.map +1 -0
- package/dist/core/isg/ttl.js +222 -0
- package/dist/core/isg/validation.d.ts +71 -0
- package/dist/core/isg/validation.d.ts.map +1 -0
- package/dist/core/isg/validation.js +226 -0
- package/dist/core/templates.d.ts.map +1 -1
- package/dist/core/templates.js +23 -5
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/types.d.ts +172 -0
- package/dist/types.d.ts.map +1 -1
- 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;
|
|
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"}
|
package/dist/config/loader.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/core/build.d.ts
CHANGED
|
@@ -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
|
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":"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"}
|
package/dist/core/build.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import fse from 'fs-extra';
|
|
2
|
-
const { ensureDir, writeFile,
|
|
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
|
-
*
|
|
36
|
-
*
|
|
39
|
+
* Recursively copies static assets from source to destination directory
|
|
40
|
+
* while logging each file being copied.
|
|
37
41
|
*
|
|
38
|
-
* @param
|
|
39
|
-
* @
|
|
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
|
|
42
|
-
|
|
48
|
+
async function copyStaticAssetsWithLogging(sourceDir, destDir, logger, basePath = '') {
|
|
49
|
+
let filesCopied = 0;
|
|
50
|
+
if (!(await pathExists(sourceDir))) {
|
|
43
51
|
return 0;
|
|
44
52
|
}
|
|
45
|
-
|
|
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
|
|
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
|
-
|
|
59
|
+
// Recursively copy directories
|
|
60
|
+
await ensureDir(destPath);
|
|
61
|
+
filesCopied += await copyStaticAssetsWithLogging(sourcePath, destPath, logger, relativePath);
|
|
51
62
|
}
|
|
52
63
|
else {
|
|
53
|
-
|
|
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
|
|
76
|
+
return filesCopied;
|
|
57
77
|
}
|
|
58
78
|
/**
|
|
59
|
-
*
|
|
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 =
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
//
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
//
|
|
185
|
-
cacheHits
|
|
186
|
-
cacheMisses
|
|
332
|
+
// Include ISG cache statistics
|
|
333
|
+
cacheHits,
|
|
334
|
+
cacheMisses,
|
|
187
335
|
};
|
|
188
|
-
console.log(
|
|
189
|
-
|
|
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
|