@stati/core 1.1.0 â 1.3.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 +19 -7
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +24 -2
- package/dist/core/build.d.ts +21 -15
- package/dist/core/build.d.ts.map +1 -1
- package/dist/core/build.js +141 -42
- package/dist/core/dev.d.ts.map +1 -1
- package/dist/core/dev.js +84 -18
- 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 +245 -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 +3 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +110 -20
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -107,18 +107,30 @@ const server = await createDevServer(config, {
|
|
|
107
107
|
});
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
#### `invalidate(
|
|
110
|
+
#### `invalidate(query?: string): Promise<InvalidationResult>`
|
|
111
111
|
|
|
112
|
-
Invalidate cache by tags or
|
|
112
|
+
Invalidate cache by tags, paths, patterns, or age.
|
|
113
113
|
|
|
114
114
|
```typescript
|
|
115
115
|
import { invalidate } from '@stati/core';
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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();
|
|
122
134
|
```
|
|
123
135
|
|
|
124
136
|
### Configuration
|
|
@@ -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
|
@@ -28,29 +28,35 @@ export interface BuildOptions {
|
|
|
28
28
|
version?: string;
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
* Builds the
|
|
32
|
-
*
|
|
31
|
+
* Builds the Stati site with ISG support.
|
|
32
|
+
* Processes all pages and assets, with smart caching for incremental builds.
|
|
33
|
+
*
|
|
34
|
+
* Uses build locking to prevent concurrent builds from corrupting cache.
|
|
35
|
+
* The manifest tracks build dependencies and cache entries for efficient rebuilds.
|
|
36
|
+
* Pages are only rebuilt if their content, templates, or dependencies have changed.
|
|
37
|
+
*
|
|
38
|
+
* Build process:
|
|
39
|
+
* 1. Load configuration and content
|
|
40
|
+
* 2. Check cache manifest for existing entries
|
|
41
|
+
* 3. Process each page (rebuild only if needed)
|
|
42
|
+
* 4. Copy static assets
|
|
43
|
+
* 5. Update cache manifest
|
|
33
44
|
*
|
|
34
45
|
* @param options - Build configuration options
|
|
46
|
+
* @returns Promise resolving to build statistics
|
|
47
|
+
* @throws {Error} When configuration is invalid
|
|
48
|
+
* @throws {Error} When template rendering fails
|
|
49
|
+
* @throws {Error} When build lock cannot be acquired
|
|
35
50
|
*
|
|
36
51
|
* @example
|
|
37
52
|
* ```typescript
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* // Basic build
|
|
41
|
-
* await build();
|
|
42
|
-
*
|
|
43
|
-
* // Build with options
|
|
44
|
-
* await build({
|
|
45
|
-
* clean: true,
|
|
53
|
+
* const stats = await build({
|
|
46
54
|
* force: true,
|
|
47
|
-
*
|
|
55
|
+
* clean: true,
|
|
56
|
+
* includeDrafts: false
|
|
48
57
|
* });
|
|
58
|
+
* console.log(`Built ${stats.pageCount} pages in ${stats.buildTime}ms`);
|
|
49
59
|
* ```
|
|
50
|
-
*
|
|
51
|
-
* @throws {Error} When configuration loading fails
|
|
52
|
-
* @throws {Error} When content processing fails
|
|
53
|
-
* @throws {Error} When template rendering fails
|
|
54
60
|
*/
|
|
55
61
|
export declare function build(options?: BuildOptions): Promise<BuildStats>;
|
|
56
62
|
//# 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAsB,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,UAAU,CAAC,CAW3E"}
|
package/dist/core/build.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import fse from 'fs-extra';
|
|
2
2
|
const { ensureDir, writeFile, remove, pathExists, stat, readdir, copyFile } = fse;
|
|
3
|
-
import { join, dirname } from 'path';
|
|
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.
|
|
@@ -50,7 +54,7 @@ async function copyStaticAssetsWithLogging(sourceDir, destDir, logger, basePath
|
|
|
50
54
|
for (const item of items) {
|
|
51
55
|
const sourcePath = join(sourceDir, item.name);
|
|
52
56
|
const destPath = join(destDir, basePath, item.name);
|
|
53
|
-
const relativePath = join(basePath, item.name)
|
|
57
|
+
const relativePath = posix.normalize(posix.join(basePath, item.name));
|
|
54
58
|
if (item.isDirectory()) {
|
|
55
59
|
// Recursively copy directories
|
|
56
60
|
await ensureDir(destPath);
|
|
@@ -92,47 +96,67 @@ const defaultLogger = {
|
|
|
92
96
|
function formatBuildStats(stats) {
|
|
93
97
|
const sizeKB = (stats.outputSizeBytes / 1024).toFixed(1);
|
|
94
98
|
const timeSeconds = (stats.buildTimeMs / 1000).toFixed(2);
|
|
95
|
-
let output =
|
|
99
|
+
let output = `Build Statistics:
|
|
96
100
|
âââââââââââââââââââââââââââââââââââââââââââ
|
|
97
|
-
â
|
|
101
|
+
â Build time: ${timeSeconds}s`.padEnd(41) + 'â';
|
|
98
102
|
output += `\nâ đ Pages built: ${stats.totalPages}`.padEnd(42) + 'â';
|
|
99
103
|
output += `\nâ đĻ Assets copied: ${stats.assetsCount}`.padEnd(42) + 'â';
|
|
100
|
-
output += `\nâ
|
|
104
|
+
output += `\nâ Output size: ${sizeKB} KB`.padEnd(42) + 'â';
|
|
101
105
|
if (stats.cacheHits !== undefined && stats.cacheMisses !== undefined) {
|
|
102
106
|
const totalCacheRequests = stats.cacheHits + stats.cacheMisses;
|
|
103
107
|
const hitRate = totalCacheRequests > 0 ? ((stats.cacheHits / totalCacheRequests) * 100).toFixed(1) : '0';
|
|
104
108
|
output +=
|
|
105
|
-
`\nâ
|
|
109
|
+
`\nâ Cache hits: ${stats.cacheHits}/${totalCacheRequests} (${hitRate}%)`.padEnd(42) + 'â';
|
|
106
110
|
}
|
|
107
111
|
output += '\nâââââââââââââââââââââââââââââââââââââââââââ';
|
|
108
112
|
return output;
|
|
109
113
|
}
|
|
110
114
|
/**
|
|
111
|
-
* Builds the
|
|
112
|
-
*
|
|
115
|
+
* Builds the Stati site with ISG support.
|
|
116
|
+
* Processes all pages and assets, with smart caching for incremental builds.
|
|
117
|
+
*
|
|
118
|
+
* Uses build locking to prevent concurrent builds from corrupting cache.
|
|
119
|
+
* The manifest tracks build dependencies and cache entries for efficient rebuilds.
|
|
120
|
+
* Pages are only rebuilt if their content, templates, or dependencies have changed.
|
|
121
|
+
*
|
|
122
|
+
* Build process:
|
|
123
|
+
* 1. Load configuration and content
|
|
124
|
+
* 2. Check cache manifest for existing entries
|
|
125
|
+
* 3. Process each page (rebuild only if needed)
|
|
126
|
+
* 4. Copy static assets
|
|
127
|
+
* 5. Update cache manifest
|
|
113
128
|
*
|
|
114
129
|
* @param options - Build configuration options
|
|
130
|
+
* @returns Promise resolving to build statistics
|
|
131
|
+
* @throws {Error} When configuration is invalid
|
|
132
|
+
* @throws {Error} When template rendering fails
|
|
133
|
+
* @throws {Error} When build lock cannot be acquired
|
|
115
134
|
*
|
|
116
135
|
* @example
|
|
117
136
|
* ```typescript
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* // Basic build
|
|
121
|
-
* await build();
|
|
122
|
-
*
|
|
123
|
-
* // Build with options
|
|
124
|
-
* await build({
|
|
125
|
-
* clean: true,
|
|
137
|
+
* const stats = await build({
|
|
126
138
|
* force: true,
|
|
127
|
-
*
|
|
139
|
+
* clean: true,
|
|
140
|
+
* includeDrafts: false
|
|
128
141
|
* });
|
|
142
|
+
* console.log(`Built ${stats.pageCount} pages in ${stats.buildTime}ms`);
|
|
129
143
|
* ```
|
|
130
|
-
*
|
|
131
|
-
* @throws {Error} When configuration loading fails
|
|
132
|
-
* @throws {Error} When content processing fails
|
|
133
|
-
* @throws {Error} When template rendering fails
|
|
134
144
|
*/
|
|
135
145
|
export async function build(options = {}) {
|
|
146
|
+
const cacheDir = join(process.cwd(), '.stati');
|
|
147
|
+
// Ensure cache directory exists before acquiring build lock
|
|
148
|
+
await ensureDir(cacheDir);
|
|
149
|
+
// Use build lock to prevent concurrent builds, with force option to override
|
|
150
|
+
return await withBuildLock(cacheDir, () => buildInternal(options), {
|
|
151
|
+
force: Boolean(options.force || options.clean), // Allow force if user explicitly requests it
|
|
152
|
+
timeout: 60000, // 1 minute timeout
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Internal build implementation without locking.
|
|
157
|
+
* Separated for cleaner error handling and testing.
|
|
158
|
+
*/
|
|
159
|
+
async function buildInternal(options = {}) {
|
|
136
160
|
const buildStartTime = Date.now();
|
|
137
161
|
const logger = options.logger || defaultLogger;
|
|
138
162
|
logger.building('Building your site...');
|
|
@@ -143,9 +167,22 @@ export async function build(options = {}) {
|
|
|
143
167
|
// Create .stati cache directory
|
|
144
168
|
const cacheDir = join(process.cwd(), '.stati');
|
|
145
169
|
await ensureDir(cacheDir);
|
|
170
|
+
// Load cache manifest for ISG
|
|
171
|
+
let cacheManifest = await loadCacheManifest(cacheDir);
|
|
172
|
+
// If no cache manifest exists, create an empty one
|
|
173
|
+
if (!cacheManifest) {
|
|
174
|
+
cacheManifest = {
|
|
175
|
+
entries: {},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
// At this point cacheManifest is guaranteed to be non-null
|
|
179
|
+
const manifest = cacheManifest;
|
|
180
|
+
// Initialize cache stats
|
|
181
|
+
let cacheHits = 0;
|
|
182
|
+
let cacheMisses = 0;
|
|
146
183
|
// Clean output directory if requested
|
|
147
184
|
if (options.clean) {
|
|
148
|
-
logger.info('
|
|
185
|
+
logger.info('Cleaning output directory...');
|
|
149
186
|
await remove(outDir);
|
|
150
187
|
}
|
|
151
188
|
await ensureDir(outDir);
|
|
@@ -158,7 +195,7 @@ export async function build(options = {}) {
|
|
|
158
195
|
logger.step(1, 3, 'Building navigation');
|
|
159
196
|
}
|
|
160
197
|
const navigation = buildNavigation(pages);
|
|
161
|
-
logger.info(
|
|
198
|
+
logger.info(`Built navigation with ${navigation.length} top-level items`);
|
|
162
199
|
// Display navigation tree if the logger supports it
|
|
163
200
|
if (logger.navigationTree) {
|
|
164
201
|
logger.navigationTree(navigation);
|
|
@@ -171,30 +208,28 @@ export async function build(options = {}) {
|
|
|
171
208
|
if (config.hooks?.beforeAll) {
|
|
172
209
|
await config.hooks.beforeAll(buildContext);
|
|
173
210
|
}
|
|
174
|
-
// Render each page with progress tracking
|
|
211
|
+
// Render each page with tree-based progress tracking and ISG
|
|
175
212
|
if (logger.step) {
|
|
176
213
|
logger.step(2, 3, 'Rendering pages');
|
|
177
214
|
}
|
|
215
|
+
// Initialize rendering tree
|
|
216
|
+
if (logger.startRenderingTree) {
|
|
217
|
+
logger.startRenderingTree('Page Rendering Process');
|
|
218
|
+
}
|
|
219
|
+
const buildTime = new Date();
|
|
178
220
|
for (let i = 0; i < pages.length; i++) {
|
|
179
221
|
const page = pages[i];
|
|
180
222
|
if (!page)
|
|
181
223
|
continue; // Safety check
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
224
|
+
const pageId = `page-${i}`;
|
|
225
|
+
// Add page to rendering tree
|
|
226
|
+
if (logger.addTreeNode) {
|
|
227
|
+
logger.addTreeNode('root', pageId, page.url, 'running', { url: page.url });
|
|
185
228
|
}
|
|
186
229
|
else {
|
|
187
|
-
logger.processing(`
|
|
230
|
+
logger.processing(`Checking ${page.url}`);
|
|
188
231
|
}
|
|
189
|
-
//
|
|
190
|
-
if (config.hooks?.beforeRender) {
|
|
191
|
-
await config.hooks.beforeRender({ page, config });
|
|
192
|
-
}
|
|
193
|
-
// Render markdown to HTML
|
|
194
|
-
const htmlContent = renderMarkdown(page.content, md);
|
|
195
|
-
// Render with template
|
|
196
|
-
const finalHtml = await renderPage(page, htmlContent, config, eta, navigation, pages);
|
|
197
|
-
// Determine output path - fix the logic here
|
|
232
|
+
// Determine output path
|
|
198
233
|
let outputPath;
|
|
199
234
|
if (page.url === '/') {
|
|
200
235
|
outputPath = join(outDir, 'index.html');
|
|
@@ -205,14 +240,78 @@ export async function build(options = {}) {
|
|
|
205
240
|
else {
|
|
206
241
|
outputPath = join(outDir, `${page.url}.html`);
|
|
207
242
|
}
|
|
243
|
+
// Get cache key (use output path relative to outDir)
|
|
244
|
+
const relativePath = relative(outDir, outputPath).replace(/\\/g, '/');
|
|
245
|
+
const cacheKey = relativePath.startsWith('/') ? relativePath : `/${relativePath}`;
|
|
246
|
+
const existingEntry = manifest.entries[cacheKey];
|
|
247
|
+
// Check if we should rebuild this page (considering ISG logic)
|
|
248
|
+
const shouldRebuild = options.force || (await shouldRebuildPage(page, existingEntry, config, buildTime));
|
|
249
|
+
if (!shouldRebuild) {
|
|
250
|
+
// Cache hit - skip rendering
|
|
251
|
+
cacheHits++;
|
|
252
|
+
if (logger.updateTreeNode) {
|
|
253
|
+
logger.updateTreeNode(pageId, 'cached', { cacheHit: true, url: page.url });
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
logger.processing(`đ Cached ${page.url}`);
|
|
257
|
+
}
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
// Cache miss - need to rebuild
|
|
261
|
+
cacheMisses++;
|
|
262
|
+
const startTime = Date.now();
|
|
263
|
+
// Add rendering substeps to tree
|
|
264
|
+
const markdownId = `${pageId}-markdown`;
|
|
265
|
+
const templateId = `${pageId}-template`;
|
|
266
|
+
if (logger.addTreeNode) {
|
|
267
|
+
logger.addTreeNode(pageId, markdownId, 'Processing Markdown', 'running');
|
|
268
|
+
logger.addTreeNode(pageId, templateId, 'Applying Template', 'pending');
|
|
269
|
+
}
|
|
270
|
+
// Run beforeRender hook
|
|
271
|
+
if (config.hooks?.beforeRender) {
|
|
272
|
+
await config.hooks.beforeRender({ page, config });
|
|
273
|
+
}
|
|
274
|
+
// Render markdown to HTML
|
|
275
|
+
const htmlContent = renderMarkdown(page.content, md);
|
|
276
|
+
if (logger.updateTreeNode) {
|
|
277
|
+
logger.updateTreeNode(markdownId, 'completed');
|
|
278
|
+
logger.updateTreeNode(templateId, 'running');
|
|
279
|
+
}
|
|
280
|
+
// Render with template
|
|
281
|
+
const finalHtml = await renderPage(page, htmlContent, config, eta, navigation, pages);
|
|
282
|
+
const renderTime = Date.now() - startTime;
|
|
283
|
+
if (logger.updateTreeNode) {
|
|
284
|
+
logger.updateTreeNode(templateId, 'completed');
|
|
285
|
+
logger.updateTreeNode(pageId, 'completed', {
|
|
286
|
+
timing: renderTime,
|
|
287
|
+
url: page.url,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
208
290
|
// Ensure directory exists and write file
|
|
209
291
|
await ensureDir(dirname(outputPath));
|
|
210
292
|
await writeFile(outputPath, finalHtml, 'utf-8');
|
|
293
|
+
// Update cache manifest
|
|
294
|
+
if (existingEntry) {
|
|
295
|
+
manifest.entries[cacheKey] = await updateCacheEntry(existingEntry, page, config, buildTime);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
manifest.entries[cacheKey] = await createCacheEntry(page, config, buildTime);
|
|
299
|
+
}
|
|
211
300
|
// Run afterRender hook
|
|
212
301
|
if (config.hooks?.afterRender) {
|
|
213
302
|
await config.hooks.afterRender({ page, config });
|
|
214
303
|
}
|
|
215
304
|
}
|
|
305
|
+
// Display final rendering tree and clear it
|
|
306
|
+
if (logger.showRenderingTree) {
|
|
307
|
+
console.log(); // Add spacing
|
|
308
|
+
logger.showRenderingTree();
|
|
309
|
+
if (logger.clearRenderingTree) {
|
|
310
|
+
logger.clearRenderingTree();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// Save updated cache manifest
|
|
314
|
+
await saveCacheManifest(cacheDir, manifest);
|
|
216
315
|
// Copy static assets and count them
|
|
217
316
|
let assetsCount = 0;
|
|
218
317
|
const staticDir = join(process.cwd(), config.staticDir);
|
|
@@ -221,9 +320,9 @@ export async function build(options = {}) {
|
|
|
221
320
|
if (logger.step) {
|
|
222
321
|
logger.step(3, 3, 'Copying static assets');
|
|
223
322
|
}
|
|
224
|
-
logger.info(
|
|
323
|
+
logger.info(`Copying static assets from ${config.staticDir}`);
|
|
225
324
|
assetsCount = await copyStaticAssetsWithLogging(staticDir, outDir, logger);
|
|
226
|
-
logger.info(
|
|
325
|
+
logger.info(`Copied ${assetsCount} static assets`);
|
|
227
326
|
}
|
|
228
327
|
// Run afterAll hook
|
|
229
328
|
if (config.hooks?.afterAll) {
|
|
@@ -236,9 +335,9 @@ export async function build(options = {}) {
|
|
|
236
335
|
assetsCount,
|
|
237
336
|
buildTimeMs: buildEndTime - buildStartTime,
|
|
238
337
|
outputSizeBytes: await getDirectorySize(outDir),
|
|
239
|
-
//
|
|
240
|
-
cacheHits
|
|
241
|
-
cacheMisses
|
|
338
|
+
// Include ISG cache statistics
|
|
339
|
+
cacheHits,
|
|
340
|
+
cacheMisses,
|
|
242
341
|
};
|
|
243
342
|
console.log(); // Add spacing before statistics
|
|
244
343
|
// Use table format if available, otherwise fall back to formatted string
|
package/dist/core/dev.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/core/dev.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/core/dev.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,aAAa,CAAC;AAMvD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC,CA0ZxF"}
|
package/dist/core/dev.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { createServer } from 'http';
|
|
2
2
|
import { join, extname } from 'path';
|
|
3
|
+
import { posix } from 'path';
|
|
3
4
|
import { readFile, stat } from 'fs/promises';
|
|
4
5
|
import { WebSocketServer } from 'ws';
|
|
5
6
|
import chokidar from 'chokidar';
|
|
6
7
|
import { build } from './build.js';
|
|
7
8
|
import { loadConfig } from '../config/loader.js';
|
|
9
|
+
import { loadCacheManifest, saveCacheManifest } from './isg/manifest.js';
|
|
8
10
|
/**
|
|
9
11
|
* Creates and configures a development server with live reload functionality.
|
|
10
12
|
*
|
|
@@ -47,7 +49,6 @@ export async function createDevServer(options = {}) {
|
|
|
47
49
|
* Performs an initial build to ensure dist/ exists
|
|
48
50
|
*/
|
|
49
51
|
async function initialBuild() {
|
|
50
|
-
logger.info?.('đī¸ Performing initial build...');
|
|
51
52
|
try {
|
|
52
53
|
await build({
|
|
53
54
|
logger,
|
|
@@ -55,7 +56,6 @@ export async function createDevServer(options = {}) {
|
|
|
55
56
|
clean: false,
|
|
56
57
|
...(configPath && { configPath }),
|
|
57
58
|
});
|
|
58
|
-
logger.success?.('â
Initial build complete');
|
|
59
59
|
}
|
|
60
60
|
catch (error) {
|
|
61
61
|
logger.error?.(`Initial build failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -63,7 +63,7 @@ export async function createDevServer(options = {}) {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
|
-
* Performs incremental rebuild when files change
|
|
66
|
+
* Performs incremental rebuild when files change, using ISG logic for smart rebuilds
|
|
67
67
|
*/
|
|
68
68
|
async function incrementalRebuild(changedPath) {
|
|
69
69
|
if (isBuilding) {
|
|
@@ -71,14 +71,22 @@ export async function createDevServer(options = {}) {
|
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
73
73
|
isBuilding = true;
|
|
74
|
-
logger.info?.(`đ Rebuilding due to change: ${changedPath}`);
|
|
75
74
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
75
|
+
// Check if the changed file is a template/partial
|
|
76
|
+
if (changedPath.endsWith('.eta') || changedPath.includes('_partials')) {
|
|
77
|
+
logger.info?.(`đ¨ Template changed: ${changedPath}`);
|
|
78
|
+
await handleTemplateChange(changedPath);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// Content or static file changed - use normal rebuild
|
|
82
|
+
logger.info?.(`đ Content changed: ${changedPath}`);
|
|
83
|
+
await build({
|
|
84
|
+
logger,
|
|
85
|
+
force: false,
|
|
86
|
+
clean: false,
|
|
87
|
+
...(configPath && { configPath }),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
82
90
|
// Notify all connected clients to reload
|
|
83
91
|
if (wsServer) {
|
|
84
92
|
wsServer.clients.forEach((client) => {
|
|
@@ -89,7 +97,7 @@ export async function createDevServer(options = {}) {
|
|
|
89
97
|
}
|
|
90
98
|
});
|
|
91
99
|
}
|
|
92
|
-
logger.success?.('
|
|
100
|
+
logger.success?.('Rebuild complete');
|
|
93
101
|
}
|
|
94
102
|
catch (error) {
|
|
95
103
|
logger.error?.(`Rebuild failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -98,6 +106,62 @@ export async function createDevServer(options = {}) {
|
|
|
98
106
|
isBuilding = false;
|
|
99
107
|
}
|
|
100
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Handles template/partial file changes by invalidating affected pages
|
|
111
|
+
*/
|
|
112
|
+
async function handleTemplateChange(templatePath) {
|
|
113
|
+
const cacheDir = join(process.cwd(), '.stati');
|
|
114
|
+
try {
|
|
115
|
+
// Load existing cache manifest
|
|
116
|
+
let cacheManifest = await loadCacheManifest(cacheDir);
|
|
117
|
+
if (!cacheManifest) {
|
|
118
|
+
// No cache exists, perform full rebuild
|
|
119
|
+
logger.info?.('No cache found, performing full rebuild...');
|
|
120
|
+
await build({
|
|
121
|
+
logger,
|
|
122
|
+
force: false,
|
|
123
|
+
clean: false,
|
|
124
|
+
...(configPath && { configPath }),
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Find pages that depend on this template
|
|
129
|
+
const affectedPages = [];
|
|
130
|
+
for (const [pagePath, entry] of Object.entries(cacheManifest.entries)) {
|
|
131
|
+
if (entry.deps.some((dep) => dep.includes(posix.normalize(templatePath.replace(/\\/g, '/'))))) {
|
|
132
|
+
affectedPages.push(pagePath);
|
|
133
|
+
// Remove from cache to force rebuild
|
|
134
|
+
delete cacheManifest.entries[pagePath];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (affectedPages.length > 0) {
|
|
138
|
+
logger.info?.(`đ¯ Invalidating ${affectedPages.length} affected pages:`);
|
|
139
|
+
affectedPages.forEach((page) => logger.info?.(` đ ${page}`));
|
|
140
|
+
// Save updated cache manifest
|
|
141
|
+
await saveCacheManifest(cacheDir, cacheManifest);
|
|
142
|
+
// Perform incremental rebuild (only affected pages will be rebuilt)
|
|
143
|
+
await build({
|
|
144
|
+
logger,
|
|
145
|
+
force: false,
|
|
146
|
+
clean: false,
|
|
147
|
+
...(configPath && { configPath }),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
logger.info?.('âšī¸ No pages affected by template change');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
logger.warning?.(`Template dependency analysis failed, performing full rebuild: ${error instanceof Error ? error.message : String(error)}`);
|
|
156
|
+
// Fallback to full rebuild
|
|
157
|
+
await build({
|
|
158
|
+
logger,
|
|
159
|
+
force: false,
|
|
160
|
+
clean: false,
|
|
161
|
+
...(configPath && { configPath }),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
101
165
|
/**
|
|
102
166
|
* Gets MIME type for a file based on its extension
|
|
103
167
|
*/
|
|
@@ -133,15 +197,15 @@ export async function createDevServer(options = {}) {
|
|
|
133
197
|
ws.onmessage = function(event) {
|
|
134
198
|
const data = JSON.parse(event.data);
|
|
135
199
|
if (data.type === 'reload') {
|
|
136
|
-
console.log('
|
|
200
|
+
console.log('Reloading page due to file changes...');
|
|
137
201
|
window.location.reload();
|
|
138
202
|
}
|
|
139
203
|
};
|
|
140
204
|
ws.onopen = function() {
|
|
141
|
-
console.log('
|
|
205
|
+
console.log('Connected to Stati dev server');
|
|
142
206
|
};
|
|
143
207
|
ws.onclose = function() {
|
|
144
|
-
console.log('
|
|
208
|
+
console.log('Lost connection to Stati dev server');
|
|
145
209
|
// Try to reconnect after a delay
|
|
146
210
|
setTimeout(() => window.location.reload(), 1000);
|
|
147
211
|
};
|
|
@@ -241,7 +305,7 @@ export async function createDevServer(options = {}) {
|
|
|
241
305
|
logger.info?.('đ Browser connected for live reload');
|
|
242
306
|
const websocket = ws;
|
|
243
307
|
websocket.on('close', () => {
|
|
244
|
-
logger.info?.('
|
|
308
|
+
logger.info?.('Browser disconnected from live reload');
|
|
245
309
|
});
|
|
246
310
|
});
|
|
247
311
|
// Start HTTP server
|
|
@@ -269,9 +333,11 @@ export async function createDevServer(options = {}) {
|
|
|
269
333
|
watcher.on('unlink', (path) => {
|
|
270
334
|
void incrementalRebuild(path);
|
|
271
335
|
});
|
|
272
|
-
logger.success?.(
|
|
273
|
-
logger.info?.(
|
|
274
|
-
logger.info?.(
|
|
336
|
+
logger.success?.(`Dev server running at ${url}`);
|
|
337
|
+
logger.info?.(`\nServing from:`);
|
|
338
|
+
logger.info?.(` đ ${outDir}`);
|
|
339
|
+
logger.info?.('Watching:');
|
|
340
|
+
watchPaths.forEach((path) => logger.info?.(` đ ${path}`));
|
|
275
341
|
// Open browser if requested
|
|
276
342
|
if (open) {
|
|
277
343
|
try {
|