@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.
Files changed (39) hide show
  1. package/README.md +19 -7
  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 +21 -15
  5. package/dist/core/build.d.ts.map +1 -1
  6. package/dist/core/build.js +141 -42
  7. package/dist/core/dev.d.ts.map +1 -1
  8. package/dist/core/dev.js +84 -18
  9. package/dist/core/invalidate.d.ts +67 -1
  10. package/dist/core/invalidate.d.ts.map +1 -1
  11. package/dist/core/invalidate.js +321 -4
  12. package/dist/core/isg/build-lock.d.ts +116 -0
  13. package/dist/core/isg/build-lock.d.ts.map +1 -0
  14. package/dist/core/isg/build-lock.js +245 -0
  15. package/dist/core/isg/builder.d.ts +51 -0
  16. package/dist/core/isg/builder.d.ts.map +1 -0
  17. package/dist/core/isg/builder.js +321 -0
  18. package/dist/core/isg/deps.d.ts +63 -0
  19. package/dist/core/isg/deps.d.ts.map +1 -0
  20. package/dist/core/isg/deps.js +332 -0
  21. package/dist/core/isg/hash.d.ts +48 -0
  22. package/dist/core/isg/hash.d.ts.map +1 -0
  23. package/dist/core/isg/hash.js +82 -0
  24. package/dist/core/isg/manifest.d.ts +47 -0
  25. package/dist/core/isg/manifest.d.ts.map +1 -0
  26. package/dist/core/isg/manifest.js +233 -0
  27. package/dist/core/isg/ttl.d.ts +101 -0
  28. package/dist/core/isg/ttl.d.ts.map +1 -0
  29. package/dist/core/isg/ttl.js +222 -0
  30. package/dist/core/isg/validation.d.ts +71 -0
  31. package/dist/core/isg/validation.d.ts.map +1 -0
  32. package/dist/core/isg/validation.js +226 -0
  33. package/dist/core/templates.d.ts.map +1 -1
  34. package/dist/core/templates.js +3 -2
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/types.d.ts +110 -20
  38. package/dist/types.d.ts.map +1 -1
  39. 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(options: InvalidateOptions): Promise<void>`
110
+ #### `invalidate(query?: string): Promise<InvalidationResult>`
111
111
 
112
- Invalidate cache by tags or paths.
112
+ Invalidate cache by tags, paths, patterns, or age.
113
113
 
114
114
  ```typescript
115
115
  import { invalidate } from '@stati/core';
116
116
 
117
- await invalidate({
118
- tags: ['blog', 'posts'],
119
- paths: ['/blog', '/about'],
120
- config: './stati.config.js',
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;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
  }
@@ -28,29 +28,35 @@ export interface BuildOptions {
28
28
  version?: string;
29
29
  }
30
30
  /**
31
- * Builds the static site by processing content files and generating HTML pages.
32
- * This is the main entry point for Stati's build process.
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
- * import { build } from 'stati';
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
- * configPath: './custom.config.js'
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
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/core/build.ts"],"names":[],"mappings":"AAQA,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;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,UAAU,CAAC,CAwI3E"}
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"}
@@ -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).replace(/\\/g, '/');
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 = `📊 Build Statistics:
99
+ let output = `Build Statistics:
96
100
  ┌─────────────────────────────────────────┐
97
- │ âąī¸ Build time: ${timeSeconds}s`.padEnd(41) + '│';
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│ 💾 Output size: ${sizeKB} KB`.padEnd(42) + '│';
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│ đŸŽ¯ Cache hits: ${stats.cacheHits}/${totalCacheRequests} (${hitRate}%)`.padEnd(42) + '│';
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 static site by processing content files and generating HTML pages.
112
- * This is the main entry point for Stati's build process.
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
- * import { build } from 'stati';
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
- * configPath: './custom.config.js'
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('🧹 Cleaning output directory...');
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(`🧭 Built navigation with ${navigation.length} top-level items`);
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
- // Show progress for page rendering
183
- if (logger.progress) {
184
- logger.progress(i + 1, pages.length, `Rendering ${page.url}`);
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(`Building ${page.url}`);
230
+ logger.processing(`Checking ${page.url}`);
188
231
  }
189
- // Run beforeRender hook
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(`đŸ“Ļ Copying static assets from ${config.staticDir}`);
323
+ logger.info(`Copying static assets from ${config.staticDir}`);
225
324
  assetsCount = await copyStaticAssetsWithLogging(staticDir, outDir, logger);
226
- logger.info(`đŸ“Ļ Copied ${assetsCount} static assets`);
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
- // Cache stats would be populated here when caching is implemented
240
- cacheHits: 0,
241
- cacheMisses: 0,
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
@@ -1 +1 @@
1
- {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/core/dev.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,aAAa,CAAC;AAKvD,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,CAgVxF"}
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
- await build({
77
- logger,
78
- force: false,
79
- clean: false,
80
- ...(configPath && { configPath }),
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?.('✅ Rebuild complete');
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('🔄 Reloading page due to file changes...');
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('🔗 Connected to Stati dev server');
205
+ console.log('Connected to Stati dev server');
142
206
  };
143
207
  ws.onclose = function() {
144
- console.log('❌ Lost connection to Stati dev server');
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?.('❌ Browser disconnected from live reload');
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?.(`🚀 Dev server running at ${url}`);
273
- logger.info?.(`📁 Serving from: ${outDir}`);
274
- logger.info?.(`👀 Watching: ${watchPaths.join(', ')}`);
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 {