@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.
- package/README.md +1 -1
- package/dist/core/dev.d.ts.map +1 -1
- package/dist/core/dev.js +154 -87
- package/dist/core/isg/hash.d.ts.map +1 -1
- package/dist/core/isg/hash.js +13 -1
- package/dist/core/markdown.d.ts.map +1 -1
- package/dist/core/markdown.js +21 -2
- package/dist/core/preview.d.ts.map +1 -1
- package/dist/core/preview.js +17 -21
- package/dist/core/templates.d.ts.map +1 -1
- package/dist/core/templates.js +84 -10
- package/dist/core/utils/error-overlay.d.ts +31 -0
- package/dist/core/utils/error-overlay.d.ts.map +1 -0
- package/dist/core/utils/error-overlay.js +562 -0
- package/dist/core/utils/partial-validation.d.ts +6 -0
- package/dist/core/utils/partial-validation.d.ts.map +1 -0
- package/dist/core/utils/partial-validation.js +129 -0
- package/dist/core/utils/server.d.ts +23 -0
- package/dist/core/utils/server.d.ts.map +1 -0
- package/dist/core/utils/server.js +61 -0
- package/dist/core/utils/template-errors.d.ts +28 -0
- package/dist/core/utils/template-errors.d.ts.map +1 -0
- package/dist/core/utils/template-errors.js +128 -0
- package/dist/core/utils/template-utils.d.ts +20 -0
- package/dist/core/utils/template-utils.d.ts.map +1 -0
- package/dist/core/utils/template-utils.js +39 -0
- package/dist/core/utils/version.d.ts +6 -0
- package/dist/core/utils/version.d.ts.map +1 -0
- package/dist/core/utils/version.js +20 -0
- package/dist/env.d.ts +3 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +7 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
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":"AAMA,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,mBAAmB,CAAC;
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
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: ${
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
//
|
|
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: '
|
|
394
|
+
content: '500 - Error reading file',
|
|
336
395
|
mimeType: 'text/plain',
|
|
337
|
-
statusCode:
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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,
|
|
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"}
|
package/dist/core/isg/hash.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
|
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"}
|
package/dist/core/markdown.js
CHANGED
|
@@ -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
|
|
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
|
|
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;
|
|
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"}
|
package/dist/core/preview.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createServer } from 'http';
|
|
2
2
|
import { join, extname } from 'path';
|
|
3
|
-
import { readFile
|
|
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
|
-
|
|
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
|
-
//
|
|
94
|
+
// This should rarely happen since resolvePrettyUrl already checked the file exists
|
|
99
95
|
return {
|
|
100
|
-
content: '
|
|
96
|
+
content: '500 - Error reading file',
|
|
101
97
|
mimeType: 'text/plain',
|
|
102
|
-
statusCode:
|
|
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;
|
|
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"}
|
package/dist/core/templates.js
CHANGED
|
@@ -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:
|
|
160
|
-
cacheFilepaths:
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
|
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
|
}
|