@stati/core 1.4.0 → 1.6.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 (36) hide show
  1. package/README.md +1 -1
  2. package/dist/core/dev.d.ts.map +1 -1
  3. package/dist/core/dev.js +154 -87
  4. package/dist/core/isg/hash.d.ts.map +1 -1
  5. package/dist/core/isg/hash.js +13 -1
  6. package/dist/core/markdown.d.ts.map +1 -1
  7. package/dist/core/markdown.js +21 -2
  8. package/dist/core/preview.d.ts.map +1 -1
  9. package/dist/core/preview.js +17 -21
  10. package/dist/core/templates.d.ts.map +1 -1
  11. package/dist/core/templates.js +84 -10
  12. package/dist/core/utils/error-overlay.d.ts +31 -0
  13. package/dist/core/utils/error-overlay.d.ts.map +1 -0
  14. package/dist/core/utils/error-overlay.js +562 -0
  15. package/dist/core/utils/partial-validation.d.ts +6 -0
  16. package/dist/core/utils/partial-validation.d.ts.map +1 -0
  17. package/dist/core/utils/partial-validation.js +129 -0
  18. package/dist/core/utils/server.d.ts +23 -0
  19. package/dist/core/utils/server.d.ts.map +1 -0
  20. package/dist/core/utils/server.js +61 -0
  21. package/dist/core/utils/template-errors.d.ts +28 -0
  22. package/dist/core/utils/template-errors.d.ts.map +1 -0
  23. package/dist/core/utils/template-errors.js +128 -0
  24. package/dist/core/utils/template-utils.d.ts +20 -0
  25. package/dist/core/utils/template-utils.d.ts.map +1 -0
  26. package/dist/core/utils/template-utils.js +39 -0
  27. package/dist/core/utils/version.d.ts +6 -0
  28. package/dist/core/utils/version.d.ts.map +1 -0
  29. package/dist/core/utils/version.js +20 -0
  30. package/dist/env.d.ts +3 -0
  31. package/dist/env.d.ts.map +1 -0
  32. package/dist/env.js +7 -0
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +1 -0
  36. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @stati/core
2
2
 
3
- The core engine for Stati, a lightweight TypeScript static site generator built with Vite-inspired architecture.
3
+ The core engine for Stati, a lightweight TypeScript static site generator built with modern architecture.
4
4
 
5
5
  ## Installation
6
6
 
@@ -1 +1 @@
1
- {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/core/dev.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAS7D,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;AAyMD,wBAAsB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC,CAgTxF"}
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/core/dev.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAa7D,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;AAoOD,wBAAsB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC,CA2XxF"}
package/dist/core/dev.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createServer } from 'http';
2
2
  import { join, extname } from 'path';
3
3
  import { posix } from 'path';
4
- import { readFile, stat } from 'fs/promises';
4
+ import { readFile } from 'fs/promises';
5
5
  import { WebSocketServer } from 'ws';
6
6
  import chokidar from 'chokidar';
7
7
  import { build } from './build.js';
@@ -9,6 +9,10 @@ import { invalidate } from './invalidate.js';
9
9
  import { loadConfig } from '../config/loader.js';
10
10
  import { loadCacheManifest, saveCacheManifest } from './isg/manifest.js';
11
11
  import { resolveDevPaths, resolveCacheDir } from './utils/paths.js';
12
+ import { resolvePrettyUrl } from './utils/server.js';
13
+ import { createErrorOverlay, parseErrorDetails } from './utils/error-overlay.js';
14
+ import { TemplateError } from './utils/template-errors.js';
15
+ import { setEnv, getEnv } from '../env.js';
12
16
  import { DEFAULT_DEV_PORT, DEFAULT_DEV_HOST, TEMPLATE_EXTENSION } from '../constants.js';
13
17
  /**
14
18
  * Loads and validates configuration for the dev server.
@@ -33,7 +37,7 @@ async function loadDevConfig(configPath, logger) {
33
37
  /**
34
38
  * Performs an initial build to ensure dist/ exists
35
39
  */
36
- async function performInitialBuild(configPath, logger) {
40
+ async function performInitialBuild(configPath, logger, onError) {
37
41
  try {
38
42
  // Clear cache to ensure fresh build on dev server start
39
43
  logger.info?.('Clearing cache for fresh development build...');
@@ -44,16 +48,24 @@ async function performInitialBuild(configPath, logger) {
44
48
  clean: false,
45
49
  ...(configPath && { configPath }),
46
50
  });
51
+ // Clear any previous errors on successful build
52
+ if (onError) {
53
+ onError(null);
54
+ }
47
55
  }
48
56
  catch (error) {
49
- logger.error?.(`Initial build failed: ${error instanceof Error ? error.message : String(error)}`);
50
- throw error;
57
+ const buildError = error instanceof Error ? error : new Error(String(error));
58
+ logger.error?.(`Initial build failed: ${buildError.message}`);
59
+ // Store the error for display in browser - DON'T clear it even if partial rebuild succeeds
60
+ onError?.(buildError);
61
+ // In development, don't throw - let the dev server start and show errors in browser
62
+ logger.warning?.('Dev server will start with build errors. Check browser for error overlay.');
51
63
  }
52
64
  }
53
65
  /**
54
66
  * Performs incremental rebuild when files change, using ISG logic for smart rebuilds
55
67
  */
56
- async function performIncrementalRebuild(changedPath, configPath, logger, wsServer, isBuildingRef) {
68
+ async function performIncrementalRebuild(changedPath, configPath, logger, wsServer, isBuildingRef, onError) {
57
69
  if (isBuildingRef.value) {
58
70
  logger.info?.('Build in progress, skipping...');
59
71
  return;
@@ -88,6 +100,10 @@ async function performIncrementalRebuild(changedPath, configPath, logger, wsServ
88
100
  ...(configPath && { configPath }),
89
101
  });
90
102
  }
103
+ // Clear any previous errors on successful build
104
+ if (onError) {
105
+ onError(null);
106
+ }
91
107
  // Notify all connected clients to reload
92
108
  if (wsServer) {
93
109
  wsServer.clients.forEach((client) => {
@@ -102,8 +118,13 @@ async function performIncrementalRebuild(changedPath, configPath, logger, wsServ
102
118
  logger.info?.(`⚡ ${relativePath} rebuilt in ${duration}ms`);
103
119
  }
104
120
  catch (error) {
121
+ const buildError = error instanceof Error ? error : new Error(String(error));
105
122
  const duration = Date.now() - startTime;
106
- logger.error?.(`❌ Rebuild failed after ${duration}ms: ${error instanceof Error ? error.message : String(error)}`);
123
+ logger.error?.(`❌ Rebuild failed after ${duration}ms: ${buildError.message}`);
124
+ // Store the error for display in browser
125
+ if (onError) {
126
+ onError(buildError);
127
+ }
107
128
  }
108
129
  finally {
109
130
  isBuildingRef.value = false;
@@ -165,14 +186,19 @@ async function handleTemplateChange(templatePath, configPath, logger) {
165
186
  });
166
187
  }
167
188
  }
168
- catch {
169
- // Fallback to full rebuild
170
- await build({
171
- logger,
172
- force: false,
173
- clean: false,
174
- ...(configPath && { configPath }),
175
- });
189
+ catch (_error) {
190
+ try {
191
+ // Fallback to full rebuild
192
+ await build({
193
+ logger,
194
+ force: false,
195
+ clean: false,
196
+ ...(configPath && { configPath }),
197
+ });
198
+ }
199
+ catch (fallbackError) {
200
+ throw fallbackError instanceof Error ? fallbackError : new Error(String(fallbackError));
201
+ }
176
202
  }
177
203
  }
178
204
  export async function createDevServer(options = {}) {
@@ -185,9 +211,16 @@ export async function createDevServer(options = {}) {
185
211
  processing: (msg) => console.log(msg),
186
212
  stats: (msg) => console.log(msg),
187
213
  }, } = options;
214
+ setEnv('development');
188
215
  const url = `http://${host}:${port}`;
189
216
  let httpServer = null;
190
217
  let wsServer = null;
218
+ // Track build errors for display in browser
219
+ let lastBuildError = null;
220
+ // Function to set build errors for error overlay display
221
+ const setLastBuildError = (error) => {
222
+ lastBuildError = error;
223
+ };
191
224
  let watcher = null;
192
225
  const isBuildingRef = { value: false };
193
226
  // Load configuration
@@ -253,34 +286,90 @@ export async function createDevServer(options = {}) {
253
286
  * Serves files from the dist directory
254
287
  */
255
288
  async function serveFile(requestPath) {
256
- let filePath = join(outDir, requestPath === '/' ? 'index.html' : requestPath);
257
- try {
258
- const stats = await stat(filePath);
259
- if (stats.isDirectory()) {
260
- // Try to serve index.html from directory
261
- const indexPath = join(filePath, 'index.html');
262
- try {
263
- await stat(indexPath);
264
- filePath = indexPath;
289
+ // Use the global build error
290
+ const errorToShow = lastBuildError;
291
+ // If there's a build error, show error overlay for HTML requests
292
+ if (errorToShow &&
293
+ (requestPath === '/' || requestPath.endsWith('.html') || !requestPath.includes('.'))) {
294
+ let errorDetails;
295
+ // Use enhanced error details for TemplateError instances
296
+ if (errorToShow instanceof TemplateError) {
297
+ errorDetails = await errorToShow.toErrorDetails();
298
+ }
299
+ else {
300
+ // Determine error type based on error message
301
+ const message = errorToShow.message.toLowerCase();
302
+ const name = errorToShow.name ? errorToShow.name.toLowerCase() : '';
303
+ const errorType = message.includes('template') ||
304
+ message.includes('eta') ||
305
+ message.includes('layout') ||
306
+ name.includes('template')
307
+ ? 'template'
308
+ : message.includes('front-matter') ||
309
+ message.includes('yaml') ||
310
+ name.includes('yaml') ||
311
+ name.includes('yamlexception')
312
+ ? 'markdown'
313
+ : message.includes('config') || name.includes('config')
314
+ ? 'config'
315
+ : 'build';
316
+ errorDetails = parseErrorDetails(errorToShow, errorType);
317
+ }
318
+ const errorHtml = createErrorOverlay(errorDetails, requestPath);
319
+ return {
320
+ content: errorHtml,
321
+ mimeType: 'text/html',
322
+ statusCode: 500,
323
+ };
324
+ }
325
+ const originalFilePath = join(outDir, requestPath === '/' ? 'index.html' : requestPath);
326
+ // Use the shared pretty URL resolver
327
+ const { filePath, found } = await resolvePrettyUrl(outDir, requestPath, originalFilePath);
328
+ if (!found || !filePath) {
329
+ // If we have a build error and this is an HTML request, show error overlay instead of 404
330
+ const errorToShow = lastBuildError;
331
+ if (errorToShow &&
332
+ (requestPath === '/' || requestPath.endsWith('.html') || !requestPath.includes('.'))) {
333
+ // Use enhanced error details for TemplateError instances
334
+ let errorDetails;
335
+ if (errorToShow instanceof TemplateError) {
336
+ errorDetails = await errorToShow.toErrorDetails();
265
337
  }
266
- catch {
267
- // If no index.html in directory, try to serve corresponding .html file
268
- // For example: /examples/ -> examples.html
269
- const directoryName = requestPath.replace(/\/$/, ''); // Remove trailing slash
270
- const fallbackPath = join(outDir, `${directoryName}.html`);
271
- try {
272
- await stat(fallbackPath);
273
- filePath = fallbackPath;
274
- }
275
- catch {
276
- return {
277
- content: '404 - Directory listing not available',
278
- mimeType: 'text/plain',
279
- statusCode: 404,
280
- };
281
- }
338
+ else {
339
+ // Determine error type based on error message
340
+ const message = errorToShow.message.toLowerCase();
341
+ const name = errorToShow.name ? errorToShow.name.toLowerCase() : '';
342
+ const errorType = message.includes('template') ||
343
+ message.includes('eta') ||
344
+ message.includes('layout') ||
345
+ name.includes('template')
346
+ ? 'template'
347
+ : message.includes('front-matter') ||
348
+ message.includes('yaml') ||
349
+ name.includes('yaml') ||
350
+ name.includes('yamlexception')
351
+ ? 'markdown'
352
+ : message.includes('config') || name.includes('config')
353
+ ? 'config'
354
+ : 'build';
355
+ errorDetails = parseErrorDetails(errorToShow, errorType);
282
356
  }
357
+ const errorHtml = createErrorOverlay(errorDetails, requestPath);
358
+ return {
359
+ content: errorHtml,
360
+ mimeType: 'text/html',
361
+ statusCode: 500,
362
+ };
283
363
  }
364
+ return {
365
+ content: requestPath.endsWith('/')
366
+ ? '404 - Directory listing not available'
367
+ : '404 - File not found',
368
+ mimeType: 'text/plain',
369
+ statusCode: 404,
370
+ };
371
+ }
372
+ try {
284
373
  const mimeType = getMimeType(filePath);
285
374
  const content = await readFile(filePath);
286
375
  // Inject live reload script into HTML files
@@ -300,41 +389,11 @@ export async function createDevServer(options = {}) {
300
389
  };
301
390
  }
302
391
  catch {
303
- // File not found, try some fallback strategies for pretty URLs
304
- if (requestPath.endsWith('/')) {
305
- // For requests ending with /, try the corresponding .html file
306
- const pathWithoutSlash = requestPath.slice(0, -1);
307
- const htmlPath = join(outDir, `${pathWithoutSlash}.html`);
308
- try {
309
- const stats = await stat(htmlPath);
310
- if (stats.isFile()) {
311
- const mimeType = getMimeType(htmlPath);
312
- const content = await readFile(htmlPath);
313
- if (mimeType === 'text/html') {
314
- const html = content.toString('utf-8');
315
- const injectedHtml = injectLiveReloadScript(html);
316
- return {
317
- content: injectedHtml,
318
- mimeType,
319
- statusCode: 200,
320
- };
321
- }
322
- return {
323
- content,
324
- mimeType,
325
- statusCode: 200,
326
- };
327
- }
328
- }
329
- catch {
330
- // Continue to 404
331
- }
332
- }
333
- // File not found
392
+ // This should rarely happen since resolvePrettyUrl already checked the file exists
334
393
  return {
335
- content: '404 - File not found',
394
+ content: '500 - Error reading file',
336
395
  mimeType: 'text/plain',
337
- statusCode: 404,
396
+ statusCode: 500,
338
397
  };
339
398
  }
340
399
  }
@@ -342,7 +401,7 @@ export async function createDevServer(options = {}) {
342
401
  url,
343
402
  async start() {
344
403
  // Perform initial build
345
- await performInitialBuild(configPath, logger);
404
+ await performInitialBuild(configPath, logger, setLastBuildError);
346
405
  // Create HTTP server
347
406
  httpServer = createServer(async (req, res) => {
348
407
  const requestPath = req.url || '/';
@@ -367,17 +426,25 @@ export async function createDevServer(options = {}) {
367
426
  }
368
427
  });
369
428
  // Create WebSocket server for live reload
370
- wsServer = new WebSocketServer({
371
- server: httpServer,
372
- path: '/__ws',
373
- });
374
- wsServer.on('connection', (ws) => {
375
- logger.info?.('Browser connected for live reload');
376
- const websocket = ws;
377
- websocket.on('close', () => {
378
- logger.info?.('Browser disconnected from live reload');
379
- });
380
- });
429
+ if (getEnv() !== 'test') {
430
+ try {
431
+ wsServer = new WebSocketServer({
432
+ server: httpServer,
433
+ path: '/__ws',
434
+ });
435
+ wsServer.on('connection', (ws) => {
436
+ logger.info?.('Browser connected for live reload');
437
+ const websocket = ws;
438
+ websocket.on('close', () => {
439
+ logger.info?.('Browser disconnected from live reload');
440
+ });
441
+ });
442
+ }
443
+ catch (_error) {
444
+ logger.warning?.('WebSocket server creation failed, live reload will not be available');
445
+ wsServer = null;
446
+ }
447
+ }
381
448
  // Start HTTP server
382
449
  await new Promise((resolve, reject) => {
383
450
  httpServer.listen(port, host, () => {
@@ -395,13 +462,13 @@ export async function createDevServer(options = {}) {
395
462
  ignoreInitial: true,
396
463
  });
397
464
  watcher.on('change', (path) => {
398
- void performIncrementalRebuild(path, configPath, logger, wsServer, isBuildingRef);
465
+ void performIncrementalRebuild(path, configPath, logger, wsServer, isBuildingRef, setLastBuildError);
399
466
  });
400
467
  watcher.on('add', (path) => {
401
- void performIncrementalRebuild(path, configPath, logger, wsServer, isBuildingRef);
468
+ void performIncrementalRebuild(path, configPath, logger, wsServer, isBuildingRef, setLastBuildError);
402
469
  });
403
470
  watcher.on('unlink', (path) => {
404
- void performIncrementalRebuild(path, configPath, logger, wsServer, isBuildingRef);
471
+ void performIncrementalRebuild(path, configPath, logger, wsServer, isBuildingRef, setLastBuildError);
405
472
  });
406
473
  logger.success?.(`Dev server running at ${url}`);
407
474
  logger.info?.(`\nServing from:`);
@@ -1 +1 @@
1
- {"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../../../src/core/isg/hash.ts"],"names":[],"mappings":"AAwBA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAKhG;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiB9E;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,CAKnF"}
1
+ {"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../../../src/core/isg/hash.ts"],"names":[],"mappings":"AAwBA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAiBhG;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiB9E;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,CAKnF"}
@@ -35,7 +35,19 @@ function createSha256Hash(data) {
35
35
  */
36
36
  export function computeContentHash(content, frontMatter) {
37
37
  // Hash the front matter (sorted for consistency)
38
- const sortedFrontMatter = JSON.stringify(frontMatter, Object.keys(frontMatter).sort());
38
+ // Note: We need to sort keys at all levels, not just top level
39
+ const sortKeys = (obj) => {
40
+ if (obj === null || typeof obj !== 'object')
41
+ return obj;
42
+ if (Array.isArray(obj))
43
+ return obj.map(sortKeys);
44
+ const sorted = {};
45
+ for (const key of Object.keys(obj).sort()) {
46
+ sorted[key] = sortKeys(obj[key]);
47
+ }
48
+ return sorted;
49
+ };
50
+ const sortedFrontMatter = JSON.stringify(sortKeys(frontMatter));
39
51
  return createSha256Hash([content, sortedFrontMatter]);
40
52
  }
41
53
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/core/markdown.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,aAAa,CAAC;AACrC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD;;;GAGG;AACH,wBAAsB,uBAAuB,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAuCtF;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,GAAG,MAAM,CAEtE"}
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/core/markdown.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,aAAa,CAAC;AAIrC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAmBrD;;;GAGG;AACH,wBAAsB,uBAAuB,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAuCtF;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,GAAG,MAAM,CAEtE"}
@@ -1,4 +1,23 @@
1
1
  import MarkdownIt from 'markdown-it';
2
+ import { createRequire } from 'module';
3
+ import { pathToFileURL } from 'url';
4
+ import path from 'path';
5
+ /**
6
+ * Load a markdown plugin, trying different resolution strategies
7
+ */
8
+ async function loadMarkdownPlugin(pluginName) {
9
+ const fullPluginName = `markdown-it-${pluginName}`;
10
+ // Try importing from current working directory first (for projects using Stati)
11
+ try {
12
+ const require = createRequire(pathToFileURL(path.resolve(process.cwd(), 'package.json')));
13
+ const pluginPath = require.resolve(fullPluginName);
14
+ return await import(pathToFileURL(pluginPath).href);
15
+ }
16
+ catch {
17
+ // Fallback to standard resolution (for core package dependencies)
18
+ return await import(fullPluginName);
19
+ }
20
+ }
2
21
  /**
3
22
  * Creates and configures a MarkdownIt processor based on the provided configuration.
4
23
  * Supports both plugin array format and configure function format.
@@ -15,7 +34,7 @@ export async function createMarkdownProcessor(config) {
15
34
  if (typeof plugin === 'string') {
16
35
  // Plugin name only
17
36
  try {
18
- const pluginModule = await import(`markdown-it-${plugin}`);
37
+ const pluginModule = await loadMarkdownPlugin(plugin);
19
38
  const pluginFunction = pluginModule.default || pluginModule;
20
39
  md.use(pluginFunction);
21
40
  }
@@ -27,7 +46,7 @@ export async function createMarkdownProcessor(config) {
27
46
  // Plugin name with options [name, options]
28
47
  const [pluginName, options] = plugin;
29
48
  try {
30
- const pluginModule = await import(`markdown-it-${pluginName}`);
49
+ const pluginModule = await loadMarkdownPlugin(pluginName);
31
50
  const pluginFunction = pluginModule.default || pluginModule;
32
51
  md.use(pluginFunction, options);
33
52
  }
@@ -1 +1 @@
1
- {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../src/core/preview.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAKhD,MAAM,WAAW,oBAAoB;IACnC,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,aAAa;IAC5B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;CACb;AAyBD;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,aAAa,CAAC,CA+JxB"}
1
+ {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../src/core/preview.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAMhD,MAAM,WAAW,oBAAoB;IACnC,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,aAAa;IAC5B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;CACb;AAyBD;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,aAAa,CAAC,CA2JxB"}
@@ -1,8 +1,9 @@
1
1
  import { createServer } from 'http';
2
2
  import { join, extname } from 'path';
3
- import { readFile, stat } from 'fs/promises';
3
+ import { readFile } from 'fs/promises';
4
4
  import { loadConfig } from '../config/loader.js';
5
5
  import { resolveDevPaths } from './utils/paths.js';
6
+ import { resolvePrettyUrl } from './utils/server.js';
6
7
  import { DEFAULT_DEV_PORT, DEFAULT_DEV_HOST } from '../constants.js';
7
8
  /**
8
9
  * Loads and validates configuration for the preview server.
@@ -67,24 +68,19 @@ export async function createPreviewServer(options = {}) {
67
68
  * Serves files from the dist directory
68
69
  */
69
70
  async function serveFile(requestPath) {
70
- let filePath = join(outDir, requestPath === '/' ? 'index.html' : requestPath);
71
+ const originalFilePath = join(outDir, requestPath === '/' ? 'index.html' : requestPath);
72
+ // Use the shared pretty URL resolver
73
+ const { filePath, found } = await resolvePrettyUrl(outDir, requestPath, originalFilePath);
74
+ if (!found || !filePath) {
75
+ return {
76
+ content: requestPath.endsWith('/')
77
+ ? '404 - Directory listing not available'
78
+ : '404 - File not found',
79
+ mimeType: 'text/plain',
80
+ statusCode: 404,
81
+ };
82
+ }
71
83
  try {
72
- const stats = await stat(filePath);
73
- if (stats.isDirectory()) {
74
- // Try to serve index.html from directory
75
- const indexPath = join(filePath, 'index.html');
76
- try {
77
- await stat(indexPath);
78
- filePath = indexPath;
79
- }
80
- catch {
81
- return {
82
- content: '404 - Directory listing not available',
83
- mimeType: 'text/plain',
84
- statusCode: 404,
85
- };
86
- }
87
- }
88
84
  const mimeType = getMimeType(filePath);
89
85
  const content = await readFile(filePath);
90
86
  // Unlike dev server, we don't inject live reload script in preview mode
@@ -95,11 +91,11 @@ export async function createPreviewServer(options = {}) {
95
91
  };
96
92
  }
97
93
  catch {
98
- // File not found
94
+ // This should rarely happen since resolvePrettyUrl already checked the file exists
99
95
  return {
100
- content: '404 - File not found',
96
+ content: '500 - Error reading file',
101
97
  mimeType: 'text/plain',
102
- statusCode: 404,
98
+ statusCode: 500,
103
99
  };
104
100
  }
105
101
  }
@@ -1 +1 @@
1
- {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/core/templates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAG1B,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAkB,MAAM,mBAAmB,CAAC;AAiLzF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,GAAG,CAU7D;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,EACnB,GAAG,EAAE,GAAG,EACR,UAAU,CAAC,EAAE,OAAO,EAAE,EACtB,QAAQ,CAAC,EAAE,SAAS,EAAE,GACrB,OAAO,CAAC,MAAM,CAAC,CAkEjB"}
1
+ {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/core/templates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAG1B,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAkB,MAAM,mBAAmB,CAAC;AAsLzF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,GAAG,CAW7D;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,EACnB,GAAG,EAAE,GAAG,EACR,UAAU,CAAC,EAAE,OAAO,EAAE,EACtB,QAAQ,CAAC,EAAE,SAAS,EAAE,GACrB,OAAO,CAAC,MAAM,CAAC,CA4JjB"}
@@ -2,8 +2,13 @@ import { Eta } from 'eta';
2
2
  import { join, dirname, relative, basename, posix } from 'path';
3
3
  import glob from 'fast-glob';
4
4
  import { TEMPLATE_EXTENSION } from '../constants.js';
5
+ import { getStatiVersion } from './utils/version.js';
6
+ import { getEnv } from '../env.js';
5
7
  import { isCollectionIndexPage, discoverLayout, getCollectionPathForPage, } from './utils/template-discovery.js';
6
8
  import { resolveSrcDir } from './utils/paths.js';
9
+ import { createTemplateError } from './utils/template-errors.js';
10
+ import { createValidatingPartialsProxy } from './utils/partial-validation.js';
11
+ import { propValue } from './utils/template-utils.js';
7
12
  /**
8
13
  * Groups pages by their tags for aggregation purposes.
9
14
  *
@@ -156,8 +161,9 @@ export function createTemplateEngine(config) {
156
161
  const templateDir = resolveSrcDir(config);
157
162
  const eta = new Eta({
158
163
  views: templateDir,
159
- cache: process.env.NODE_ENV === 'production',
160
- cacheFilepaths: process.env.NODE_ENV === 'production',
164
+ cache: getEnv() === 'production',
165
+ cacheFilepaths: getEnv() === 'production',
166
+ varName: 'stati',
161
167
  });
162
168
  return eta;
163
169
  }
@@ -189,22 +195,85 @@ export async function renderPage(page, body, config, eta, navigation, allPages)
189
195
  collection: collectionData, // Add collection data for index pages
190
196
  // Add custom filters to context
191
197
  ...(config.eta?.filters || {}),
198
+ generator: {
199
+ version: getStatiVersion(),
200
+ },
201
+ // Template utilities
202
+ propValue,
192
203
  };
193
204
  // Render partials and store their content
205
+ // Use multiple passes to allow partials to reference other partials
194
206
  const renderedPartials = {};
195
- for (const [partialName, partialPath] of Object.entries(partialPaths)) {
196
- try {
197
- const renderedContent = await eta.renderAsync(partialPath, baseContext);
198
- renderedPartials[partialName] = renderedContent;
207
+ const maxPasses = 3; // Prevent infinite loops
208
+ for (let pass = 0; pass < maxPasses; pass++) {
209
+ let partialsToRender;
210
+ if (pass === 0) {
211
+ // First pass: render all partials
212
+ partialsToRender = { ...partialPaths };
213
+ }
214
+ else {
215
+ // Subsequent passes: re-render partials that might need updated dependencies
216
+ // For simplicity, re-render all partials to ensure they have access to all previously rendered ones
217
+ // TODO: Optimize by tracking which partials changed or have dependencies
218
+ partialsToRender = { ...partialPaths };
219
+ }
220
+ if (Object.keys(partialsToRender).length === 0) {
221
+ break;
222
+ }
223
+ let progressMade = false;
224
+ const passRenderedPartials = {};
225
+ for (const [partialName, partialPath] of Object.entries(partialsToRender)) {
226
+ try {
227
+ // Create context with all previously rendered partials available
228
+ const combinedPartials = { ...renderedPartials, ...passRenderedPartials };
229
+ const partialContext = {
230
+ ...baseContext,
231
+ partials: createValidatingPartialsProxy(combinedPartials), // Include both previous and current pass partials with validation
232
+ };
233
+ const renderedContent = await eta.renderAsync(partialPath, partialContext);
234
+ passRenderedPartials[partialName] = renderedContent;
235
+ progressMade = true;
236
+ }
237
+ catch (error) {
238
+ // If this is the last pass, log the error and create placeholder
239
+ if (pass === maxPasses - 1) {
240
+ console.warn(`Warning: Failed to render partial ${partialName} at ${partialPath}:`, error);
241
+ // In development mode, throw enhanced template error for partials too
242
+ if (getEnv() === 'development') {
243
+ const templateError = createTemplateError(error instanceof Error ? error : new Error(String(error)), partialPath);
244
+ throw templateError;
245
+ }
246
+ passRenderedPartials[partialName] = `<!-- Error rendering partial: ${partialName} -->`;
247
+ progressMade = true;
248
+ }
249
+ // Otherwise, use existing content if available, or skip for retry
250
+ else if (renderedPartials[partialName]) {
251
+ passRenderedPartials[partialName] = renderedPartials[partialName];
252
+ }
253
+ // For failed partials on non-last pass, still count as progress to allow retries
254
+ else {
255
+ progressMade = true;
256
+ }
257
+ }
199
258
  }
200
- catch (error) {
201
- console.warn(`Warning: Failed to render partial ${partialName} at ${partialPath}:`, error);
202
- renderedPartials[partialName] = `<!-- Error rendering partial: ${partialName} -->`;
259
+ // Update the rendered partials with this pass's results
260
+ Object.assign(renderedPartials, passRenderedPartials);
261
+ // If no progress was made, break to avoid infinite loop
262
+ if (!progressMade) {
263
+ break;
264
+ }
265
+ // If this is pass 0, always do at least one more pass to allow interdependencies
266
+ if (pass === 0) {
267
+ continue;
268
+ }
269
+ // For subsequent passes, only continue if we're not at max passes yet
270
+ if (pass >= maxPasses - 1) {
271
+ break;
203
272
  }
204
273
  }
205
274
  const context = {
206
275
  ...baseContext,
207
- partials: renderedPartials, // Add rendered partials to template context
276
+ partials: createValidatingPartialsProxy(renderedPartials), // Add rendered partials with validation
208
277
  };
209
278
  try {
210
279
  if (!layoutPath) {
@@ -215,6 +284,11 @@ export async function renderPage(page, body, config, eta, navigation, allPages)
215
284
  }
216
285
  catch (error) {
217
286
  console.error(`Error rendering layout ${layoutPath || 'unknown'}:`, error);
287
+ // In development mode, throw enhanced template error for better debugging
288
+ if (getEnv() === 'development') {
289
+ const templateError = createTemplateError(error instanceof Error ? error : new Error(String(error)), layoutPath || undefined);
290
+ throw templateError;
291
+ }
218
292
  return createFallbackHtml(page, body);
219
293
  }
220
294
  }