@stati/core 1.3.2 → 1.5.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 +176 -48
- 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 +19 -0
- package/dist/core/preview.d.ts.map +1 -0
- package/dist/core/preview.js +159 -0
- package/dist/core/templates.d.ts.map +1 -1
- package/dist/core/templates.js +83 -11
- 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/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 +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error overlay utility for displaying pretty errors in development mode.
|
|
3
|
+
* Provides a user-friendly interface for template rendering and build errors.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Creates a styled error overlay HTML that displays comprehensive error information.
|
|
7
|
+
*
|
|
8
|
+
* @param error - The error details to display
|
|
9
|
+
* @param requestPath - The path that triggered the error
|
|
10
|
+
* @returns HTML string for the error overlay
|
|
11
|
+
*/
|
|
12
|
+
export function createErrorOverlay(error, requestPath) {
|
|
13
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
14
|
+
return `<!DOCTYPE html>
|
|
15
|
+
<html lang="en">
|
|
16
|
+
<head>
|
|
17
|
+
<meta charset="UTF-8">
|
|
18
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
19
|
+
<title>Stati Development Error</title>
|
|
20
|
+
<style>
|
|
21
|
+
* {
|
|
22
|
+
margin: 0;
|
|
23
|
+
padding: 0;
|
|
24
|
+
box-sizing: border-box;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
body {
|
|
28
|
+
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
|
29
|
+
background: linear-gradient(135deg, #1e1e1e 0%, #2d2d2d 100%);
|
|
30
|
+
color: #ffffff;
|
|
31
|
+
line-height: 1.6;
|
|
32
|
+
min-height: 100vh;
|
|
33
|
+
overflow-y: auto;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.error-overlay {
|
|
37
|
+
min-height: 100vh;
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-direction: column;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.error-header {
|
|
43
|
+
background: linear-gradient(90deg, #ff6b6b 0%, #ee5a52 100%);
|
|
44
|
+
padding: 24px 32px;
|
|
45
|
+
box-shadow: 0 4px 20px rgba(255, 107, 107, 0.3);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.error-title {
|
|
49
|
+
font-size: 24px;
|
|
50
|
+
font-weight: 600;
|
|
51
|
+
margin-bottom: 8px;
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
gap: 12px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.error-icon {
|
|
58
|
+
width: 32px;
|
|
59
|
+
height: 32px;
|
|
60
|
+
background: rgba(255, 255, 255, 0.2);
|
|
61
|
+
border-radius: 50%;
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
justify-content: center;
|
|
65
|
+
font-size: 18px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.error-subtitle {
|
|
69
|
+
font-size: 14px;
|
|
70
|
+
opacity: 0.9;
|
|
71
|
+
font-weight: 400;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.error-content {
|
|
75
|
+
flex: 1;
|
|
76
|
+
padding: 32px;
|
|
77
|
+
max-width: 1200px;
|
|
78
|
+
margin: 0 auto;
|
|
79
|
+
width: 100%;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.error-section {
|
|
83
|
+
background: rgba(255, 255, 255, 0.05);
|
|
84
|
+
border-radius: 12px;
|
|
85
|
+
padding: 24px;
|
|
86
|
+
margin-bottom: 24px;
|
|
87
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.section-title {
|
|
91
|
+
font-size: 18px;
|
|
92
|
+
font-weight: 600;
|
|
93
|
+
margin-bottom: 16px;
|
|
94
|
+
color: #ff6b6b;
|
|
95
|
+
display: flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
gap: 8px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.error-message {
|
|
101
|
+
background: rgba(255, 107, 107, 0.1);
|
|
102
|
+
border: 1px solid rgba(255, 107, 107, 0.3);
|
|
103
|
+
border-radius: 8px;
|
|
104
|
+
padding: 16px;
|
|
105
|
+
font-size: 16px;
|
|
106
|
+
line-height: 1.5;
|
|
107
|
+
margin-bottom: 16px;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.error-location {
|
|
111
|
+
display: flex;
|
|
112
|
+
gap: 16px;
|
|
113
|
+
margin-bottom: 16px;
|
|
114
|
+
flex-wrap: wrap;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.location-item {
|
|
118
|
+
background: rgba(255, 255, 255, 0.1);
|
|
119
|
+
padding: 8px 12px;
|
|
120
|
+
border-radius: 6px;
|
|
121
|
+
font-size: 14px;
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
gap: 6px;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.location-label {
|
|
128
|
+
color: #888;
|
|
129
|
+
font-weight: 500;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.location-value {
|
|
133
|
+
color: #ffffff;
|
|
134
|
+
font-weight: 600;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.code-block {
|
|
138
|
+
background: #1a1a1a;
|
|
139
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
140
|
+
border-radius: 8px;
|
|
141
|
+
overflow: hidden;
|
|
142
|
+
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.code-header {
|
|
146
|
+
background: rgba(255, 255, 255, 0.05);
|
|
147
|
+
padding: 12px 16px;
|
|
148
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
149
|
+
font-size: 14px;
|
|
150
|
+
color: #888;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.code-content {
|
|
154
|
+
padding: 0;
|
|
155
|
+
overflow-x: auto;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.code-line {
|
|
159
|
+
display: flex;
|
|
160
|
+
align-items: center;
|
|
161
|
+
padding: 0 16px;
|
|
162
|
+
min-height: 24px;
|
|
163
|
+
font-size: 14px;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.code-line:hover {
|
|
167
|
+
background: rgba(255, 255, 255, 0.05);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.line-number {
|
|
171
|
+
color: #666;
|
|
172
|
+
margin-right: 16px;
|
|
173
|
+
min-width: 40px;
|
|
174
|
+
text-align: right;
|
|
175
|
+
font-weight: 500;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.line-error {
|
|
179
|
+
background: rgba(255, 107, 107, 0.2);
|
|
180
|
+
border-left: 4px solid #ff6b6b;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.line-error .line-number {
|
|
184
|
+
color: #ff6b6b;
|
|
185
|
+
font-weight: 600;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.line-context {
|
|
189
|
+
background: rgba(255, 255, 255, 0.03);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.suggestions {
|
|
193
|
+
background: rgba(52, 211, 153, 0.1);
|
|
194
|
+
border: 1px solid rgba(52, 211, 153, 0.3);
|
|
195
|
+
border-radius: 8px;
|
|
196
|
+
padding: 16px;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.suggestions-title {
|
|
200
|
+
color: #34d399;
|
|
201
|
+
font-weight: 600;
|
|
202
|
+
margin-bottom: 12px;
|
|
203
|
+
display: flex;
|
|
204
|
+
align-items: center;
|
|
205
|
+
gap: 8px;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.suggestion-item {
|
|
209
|
+
background: rgba(52, 211, 153, 0.05);
|
|
210
|
+
border-radius: 6px;
|
|
211
|
+
padding: 12px;
|
|
212
|
+
margin-bottom: 8px;
|
|
213
|
+
border-left: 3px solid #34d399;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.suggestion-item:last-child {
|
|
217
|
+
margin-bottom: 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.stack-trace {
|
|
221
|
+
background: #1a1a1a;
|
|
222
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
223
|
+
border-radius: 8px;
|
|
224
|
+
padding: 16px;
|
|
225
|
+
font-size: 13px;
|
|
226
|
+
line-height: 1.4;
|
|
227
|
+
overflow-x: auto;
|
|
228
|
+
white-space: pre-wrap;
|
|
229
|
+
color: #ccc;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.footer {
|
|
233
|
+
background: rgba(255, 255, 255, 0.05);
|
|
234
|
+
padding: 16px 32px;
|
|
235
|
+
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
236
|
+
display: flex;
|
|
237
|
+
justify-content: space-between;
|
|
238
|
+
align-items: center;
|
|
239
|
+
font-size: 14px;
|
|
240
|
+
color: #888;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.footer-info {
|
|
244
|
+
display: flex;
|
|
245
|
+
gap: 24px;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.refresh-button {
|
|
249
|
+
background: #ff6b6b;
|
|
250
|
+
color: white;
|
|
251
|
+
border: none;
|
|
252
|
+
padding: 8px 16px;
|
|
253
|
+
border-radius: 6px;
|
|
254
|
+
font-weight: 500;
|
|
255
|
+
cursor: pointer;
|
|
256
|
+
transition: background 0.2s;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.refresh-button:hover {
|
|
260
|
+
background: #ee5a52;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.type-badge {
|
|
264
|
+
background: rgba(255, 107, 107, 0.2);
|
|
265
|
+
color: #ff6b6b;
|
|
266
|
+
padding: 4px 8px;
|
|
267
|
+
border-radius: 4px;
|
|
268
|
+
font-size: 12px;
|
|
269
|
+
font-weight: 600;
|
|
270
|
+
text-transform: uppercase;
|
|
271
|
+
letter-spacing: 0.5px;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.type-template { background: rgba(147, 51, 234, 0.2); color: #a855f7; }
|
|
275
|
+
.type-build { background: rgba(245, 101, 101, 0.2); color: #f56565; }
|
|
276
|
+
.type-markdown { background: rgba(59, 130, 246, 0.2); color: #3b82f6; }
|
|
277
|
+
.type-config { background: rgba(251, 191, 36, 0.2); color: #fbbf24; }
|
|
278
|
+
|
|
279
|
+
@media (max-width: 768px) {
|
|
280
|
+
.error-content {
|
|
281
|
+
padding: 16px;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.error-header {
|
|
285
|
+
padding: 16px 24px;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.error-title {
|
|
289
|
+
font-size: 20px;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.error-location {
|
|
293
|
+
flex-direction: column;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
</style>
|
|
297
|
+
</head>
|
|
298
|
+
<body>
|
|
299
|
+
<div class="error-overlay">
|
|
300
|
+
<div class="error-header">
|
|
301
|
+
<div class="error-title">
|
|
302
|
+
<div class="error-icon">⚠️</div>
|
|
303
|
+
Stati Development Error
|
|
304
|
+
<span class="type-badge type-${error.type}">${error.type}</span>
|
|
305
|
+
</div>
|
|
306
|
+
<div class="error-subtitle">
|
|
307
|
+
${error.type === 'template'
|
|
308
|
+
? 'Template rendering failed'
|
|
309
|
+
: error.type === 'build'
|
|
310
|
+
? 'Build process failed'
|
|
311
|
+
: error.type === 'markdown'
|
|
312
|
+
? 'Markdown processing failed'
|
|
313
|
+
: 'Configuration error'} • ${timestamp}
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
<div class="error-content">
|
|
318
|
+
<div class="error-section">
|
|
319
|
+
<div class="section-title">
|
|
320
|
+
🚨 Error Details
|
|
321
|
+
</div>
|
|
322
|
+
<div class="error-message">
|
|
323
|
+
${escapeHtml(error.message)}
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
${error.file || error.line
|
|
327
|
+
? `
|
|
328
|
+
<div class="error-location">
|
|
329
|
+
${error.file
|
|
330
|
+
? `<div class="location-item">
|
|
331
|
+
<span class="location-label">File:</span>
|
|
332
|
+
<span class="location-value">${escapeHtml(error.file)}</span>
|
|
333
|
+
</div>`
|
|
334
|
+
: ''}
|
|
335
|
+
${error.line
|
|
336
|
+
? `<div class="location-item">
|
|
337
|
+
<span class="location-label">Line:</span>
|
|
338
|
+
<span class="location-value">${error.line}${error.column ? `:${error.column}` : ''}</span>
|
|
339
|
+
</div>`
|
|
340
|
+
: ''}
|
|
341
|
+
<div class="location-item">
|
|
342
|
+
<span class="location-label">Route:</span>
|
|
343
|
+
<span class="location-value">${escapeHtml(requestPath)}</span>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
`
|
|
347
|
+
: ''}
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
${error.code || error.context
|
|
351
|
+
? `
|
|
352
|
+
<div class="error-section">
|
|
353
|
+
<div class="section-title">
|
|
354
|
+
📄 Code Context
|
|
355
|
+
</div>
|
|
356
|
+
<div class="code-block">
|
|
357
|
+
${error.file ? `<div class="code-header">${escapeHtml(error.file)}</div>` : ''}
|
|
358
|
+
<div class="code-content">
|
|
359
|
+
${formatCodeContext(error)}
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
`
|
|
364
|
+
: ''}
|
|
365
|
+
|
|
366
|
+
${error.suggestions && error.suggestions.length > 0
|
|
367
|
+
? `
|
|
368
|
+
<div class="error-section">
|
|
369
|
+
<div class="suggestions">
|
|
370
|
+
<div class="suggestions-title">
|
|
371
|
+
💡 Suggestions
|
|
372
|
+
</div>
|
|
373
|
+
${error.suggestions
|
|
374
|
+
.map((suggestion) => `
|
|
375
|
+
<div class="suggestion-item">
|
|
376
|
+
${escapeHtml(suggestion)}
|
|
377
|
+
</div>
|
|
378
|
+
`)
|
|
379
|
+
.join('')}
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
`
|
|
383
|
+
: ''}
|
|
384
|
+
|
|
385
|
+
${error.stack
|
|
386
|
+
? `
|
|
387
|
+
<div class="error-section">
|
|
388
|
+
<div class="section-title">
|
|
389
|
+
🔍 Stack Trace
|
|
390
|
+
</div>
|
|
391
|
+
<div class="stack-trace">${escapeHtml(error.stack)}</div>
|
|
392
|
+
</div>
|
|
393
|
+
`
|
|
394
|
+
: ''}
|
|
395
|
+
</div>
|
|
396
|
+
|
|
397
|
+
<div class="footer">
|
|
398
|
+
<div class="footer-info">
|
|
399
|
+
<span>Stati Development Server</span>
|
|
400
|
+
<span>•</span>
|
|
401
|
+
<span>Fix the error and save to automatically reload</span>
|
|
402
|
+
</div>
|
|
403
|
+
<button class="refresh-button" onclick="window.location.reload()">
|
|
404
|
+
Refresh
|
|
405
|
+
</button>
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
|
|
409
|
+
<script>
|
|
410
|
+
// Auto-refresh when files change (if WebSocket connection exists)
|
|
411
|
+
if (typeof WebSocket !== 'undefined') {
|
|
412
|
+
try {
|
|
413
|
+
const ws = new WebSocket('ws://localhost:3000/__ws');
|
|
414
|
+
ws.onmessage = function(event) {
|
|
415
|
+
const data = JSON.parse(event.data);
|
|
416
|
+
if (data.type === 'reload') {
|
|
417
|
+
window.location.reload();
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
} catch (e) {
|
|
421
|
+
// WebSocket connection failed, that's okay
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Keyboard shortcut to refresh (Ctrl/Cmd + R)
|
|
426
|
+
document.addEventListener('keydown', function(e) {
|
|
427
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'r') {
|
|
428
|
+
e.preventDefault();
|
|
429
|
+
window.location.reload();
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
</script>
|
|
433
|
+
</body>
|
|
434
|
+
</html>`;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Escapes HTML characters in a string for safe display.
|
|
438
|
+
*/
|
|
439
|
+
function escapeHtml(text) {
|
|
440
|
+
return text
|
|
441
|
+
.replace(/&/g, '&')
|
|
442
|
+
.replace(/</g, '<')
|
|
443
|
+
.replace(/>/g, '>')
|
|
444
|
+
.replace(/"/g, '"')
|
|
445
|
+
.replace(/'/g, ''');
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Formats code context with line numbers and highlighting.
|
|
449
|
+
*/
|
|
450
|
+
function formatCodeContext(error) {
|
|
451
|
+
if (!error.context && !error.code) {
|
|
452
|
+
return '<div class="code-line"><span class="line-number">-</span>No code context available</div>';
|
|
453
|
+
}
|
|
454
|
+
const lines = [];
|
|
455
|
+
const errorLine = error.line || 0;
|
|
456
|
+
let currentLine = errorLine;
|
|
457
|
+
if (error.context?.before) {
|
|
458
|
+
currentLine = errorLine - error.context.before.length;
|
|
459
|
+
error.context.before.forEach((line) => {
|
|
460
|
+
lines.push(`<div class="code-line line-context">
|
|
461
|
+
<span class="line-number">${currentLine}</span>
|
|
462
|
+
<span class="line-content">${escapeHtml(line)}</span>
|
|
463
|
+
</div>`);
|
|
464
|
+
currentLine++;
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
if (error.code) {
|
|
468
|
+
lines.push(`<div class="code-line line-error">
|
|
469
|
+
<span class="line-number">${errorLine}</span>
|
|
470
|
+
<span class="line-content">${escapeHtml(error.code)}</span>
|
|
471
|
+
</div>`);
|
|
472
|
+
currentLine = errorLine + 1;
|
|
473
|
+
}
|
|
474
|
+
if (error.context?.after) {
|
|
475
|
+
error.context.after.forEach((line) => {
|
|
476
|
+
lines.push(`<div class="code-line line-context">
|
|
477
|
+
<span class="line-number">${currentLine}</span>
|
|
478
|
+
<span class="line-content">${escapeHtml(line)}</span>
|
|
479
|
+
</div>`);
|
|
480
|
+
currentLine++;
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
return lines.join('');
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Parses error information from various error types to create structured ErrorDetails.
|
|
487
|
+
*/
|
|
488
|
+
export function parseErrorDetails(error, type, filePath) {
|
|
489
|
+
const details = {
|
|
490
|
+
type,
|
|
491
|
+
message: error.message,
|
|
492
|
+
};
|
|
493
|
+
if (error.stack) {
|
|
494
|
+
details.stack = error.stack;
|
|
495
|
+
}
|
|
496
|
+
// Try to extract file and line information from the stack trace
|
|
497
|
+
if (error.stack) {
|
|
498
|
+
const stackLines = error.stack.split('\n');
|
|
499
|
+
for (const line of stackLines) {
|
|
500
|
+
// Look for file paths in the stack trace
|
|
501
|
+
const match = line.match(/at\s+.*\((.+):(\d+):(\d+)\)/);
|
|
502
|
+
if (match && match[1] && match[2] && match[3] && !match[1].includes('node_modules')) {
|
|
503
|
+
details.file = match[1];
|
|
504
|
+
details.line = parseInt(match[2], 10);
|
|
505
|
+
details.column = parseInt(match[3], 10);
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
// Override with provided file path if available
|
|
511
|
+
if (filePath) {
|
|
512
|
+
details.file = filePath;
|
|
513
|
+
}
|
|
514
|
+
// Add type-specific suggestions
|
|
515
|
+
details.suggestions = getErrorSuggestions(error, type);
|
|
516
|
+
return details;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Provides helpful suggestions based on error type and message.
|
|
520
|
+
*/
|
|
521
|
+
function getErrorSuggestions(error, type) {
|
|
522
|
+
const suggestions = [];
|
|
523
|
+
const message = error.message.toLowerCase();
|
|
524
|
+
if (type === 'template') {
|
|
525
|
+
if (message.includes('not found') || message.includes('cannot resolve')) {
|
|
526
|
+
suggestions.push('Check if the template file exists in the correct location');
|
|
527
|
+
suggestions.push('Verify the template file path is correct in your layout or include statement');
|
|
528
|
+
suggestions.push('Ensure the template file has the correct .eta extension');
|
|
529
|
+
}
|
|
530
|
+
if (message.includes('syntax') || message.includes('unexpected token')) {
|
|
531
|
+
suggestions.push('Check for missing or extra brackets, quotes, or commas in your template');
|
|
532
|
+
suggestions.push('Verify that all Eta template syntax is correctly formatted');
|
|
533
|
+
suggestions.push('Look for unclosed tags or expressions');
|
|
534
|
+
}
|
|
535
|
+
if (message.includes('undefined') || message.includes('null')) {
|
|
536
|
+
suggestions.push('Check if all required data is being passed to the template');
|
|
537
|
+
suggestions.push('Add conditional checks for optional data in your template');
|
|
538
|
+
suggestions.push('Verify that page frontMatter and content are properly structured');
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
else if (type === 'build') {
|
|
542
|
+
if (message.includes('permission') || message.includes('eacces')) {
|
|
543
|
+
suggestions.push('Check file and directory permissions');
|
|
544
|
+
suggestions.push('Ensure Stati has write access to the output directory');
|
|
545
|
+
}
|
|
546
|
+
if (message.includes('space') || message.includes('enospc')) {
|
|
547
|
+
suggestions.push('Free up disk space on your system');
|
|
548
|
+
suggestions.push('Check if the output directory has sufficient space');
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
else if (type === 'markdown') {
|
|
552
|
+
if (message.includes('front-matter') || message.includes('yaml')) {
|
|
553
|
+
suggestions.push('Check the YAML front-matter syntax for errors');
|
|
554
|
+
suggestions.push('Ensure front-matter is properly enclosed in --- markers');
|
|
555
|
+
suggestions.push('Verify that all YAML values are properly quoted if they contain special characters');
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
// General suggestions
|
|
559
|
+
suggestions.push('Save the file and the page will automatically reload');
|
|
560
|
+
suggestions.push('Check the Stati documentation for more help');
|
|
561
|
+
return suggestions;
|
|
562
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a development-mode Proxy for the partials object that throws errors
|
|
3
|
+
* when accessing non-existent partials instead of returning undefined
|
|
4
|
+
*/
|
|
5
|
+
export declare function createValidatingPartialsProxy(partials: Record<string, string>): Record<string, string>;
|
|
6
|
+
//# sourceMappingURL=partial-validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"partial-validation.d.ts","sourceRoot":"","sources":["../../../src/core/utils/partial-validation.ts"],"names":[],"mappings":"AA4FA;;;GAGG;AACH,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAmDxB"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates inline error overlay HTML for missing partials
|
|
3
|
+
*/
|
|
4
|
+
function createInlineErrorOverlay(partialName, suggestions) {
|
|
5
|
+
const suggestionText = suggestions.length > 0
|
|
6
|
+
? `Did you mean: ${suggestions.map((s) => `"${s}"`).join(', ')}?`
|
|
7
|
+
: 'No similar partials found';
|
|
8
|
+
return `
|
|
9
|
+
<!-- Stati Development Error Overlay -->
|
|
10
|
+
<div style="
|
|
11
|
+
position: fixed;
|
|
12
|
+
top: 20px;
|
|
13
|
+
right: 20px;
|
|
14
|
+
z-index: 999999;
|
|
15
|
+
background: #dc2626;
|
|
16
|
+
color: white;
|
|
17
|
+
padding: 16px;
|
|
18
|
+
border-radius: 8px;
|
|
19
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
20
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
21
|
+
font-size: 14px;
|
|
22
|
+
line-height: 1.4;
|
|
23
|
+
max-width: 400px;
|
|
24
|
+
border: 2px solid #fca5a5;
|
|
25
|
+
">
|
|
26
|
+
<div style="font-weight: 600; margin-bottom: 8px; font-size: 16px;">
|
|
27
|
+
⚠️ Template Error
|
|
28
|
+
</div>
|
|
29
|
+
<div style="margin-bottom: 8px;">
|
|
30
|
+
<strong>Partial "${partialName}" not found</strong>
|
|
31
|
+
</div>
|
|
32
|
+
<div style="color: #fca5a5; font-size: 13px;">
|
|
33
|
+
${suggestionText}
|
|
34
|
+
</div>
|
|
35
|
+
<div style="margin-top: 12px; padding-top: 8px; border-top: 1px solid #fca5a5; font-size: 12px; opacity: 0.8;">
|
|
36
|
+
Fix the partial name in your template to dismiss this error
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
<!-- End Stati Error Overlay -->`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Finds similar partial names to suggest when a partial is not found
|
|
43
|
+
*/
|
|
44
|
+
function findSimilarPartialNames(targetName, availableNames) {
|
|
45
|
+
if (availableNames.length === 0)
|
|
46
|
+
return [];
|
|
47
|
+
// Simple similarity check based on common prefixes and substrings
|
|
48
|
+
const target = targetName.toLowerCase();
|
|
49
|
+
const suggestions = availableNames
|
|
50
|
+
.map((name) => {
|
|
51
|
+
const candidate = name.toLowerCase();
|
|
52
|
+
let score = 0;
|
|
53
|
+
// Exact match gets highest score
|
|
54
|
+
if (candidate === target)
|
|
55
|
+
return { name, score: 1000 };
|
|
56
|
+
// Check if one is contained in the other
|
|
57
|
+
if (candidate.includes(target) || target.includes(candidate)) {
|
|
58
|
+
score += 100;
|
|
59
|
+
}
|
|
60
|
+
// Check common prefix length
|
|
61
|
+
let prefixLen = 0;
|
|
62
|
+
for (let i = 0; i < Math.min(target.length, candidate.length); i++) {
|
|
63
|
+
if (target[i] === candidate[i]) {
|
|
64
|
+
prefixLen++;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
score += prefixLen * 10;
|
|
71
|
+
// Check common characters
|
|
72
|
+
const targetChars = new Set(target);
|
|
73
|
+
const candidateChars = new Set(candidate);
|
|
74
|
+
const commonChars = [...targetChars].filter((char) => candidateChars.has(char));
|
|
75
|
+
score += commonChars.length;
|
|
76
|
+
return { name, score };
|
|
77
|
+
})
|
|
78
|
+
.filter((item) => item.score > 0) // Only include items with some similarity
|
|
79
|
+
.sort((a, b) => b.score - a.score) // Sort by score descending
|
|
80
|
+
.slice(0, 3) // Top 3 suggestions
|
|
81
|
+
.map((item) => item.name);
|
|
82
|
+
return suggestions;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Creates a development-mode Proxy for the partials object that throws errors
|
|
86
|
+
* when accessing non-existent partials instead of returning undefined
|
|
87
|
+
*/
|
|
88
|
+
export function createValidatingPartialsProxy(partials) {
|
|
89
|
+
// In production, return partials as-is
|
|
90
|
+
// Only skip validation if explicitly set to production
|
|
91
|
+
if (process.env.NODE_ENV === 'production') {
|
|
92
|
+
return partials;
|
|
93
|
+
}
|
|
94
|
+
return new Proxy(partials, {
|
|
95
|
+
get(target, prop, receiver) {
|
|
96
|
+
// Allow normal object operations
|
|
97
|
+
if (typeof prop === 'symbol') {
|
|
98
|
+
return Reflect.get(target, prop, receiver);
|
|
99
|
+
}
|
|
100
|
+
const propName = String(prop);
|
|
101
|
+
// Check if the property exists
|
|
102
|
+
if (propName in target) {
|
|
103
|
+
return target[propName];
|
|
104
|
+
}
|
|
105
|
+
// Special case: allow accessing length, toString, etc.
|
|
106
|
+
if (propName in Object.prototype || propName === 'length') {
|
|
107
|
+
return Reflect.get(target, prop, receiver);
|
|
108
|
+
}
|
|
109
|
+
// Property doesn't exist - return error overlay HTML instead of throwing
|
|
110
|
+
const availablePartials = Object.keys(target);
|
|
111
|
+
const suggestions = findSimilarPartialNames(propName, availablePartials);
|
|
112
|
+
// Special case: throw error if no partials are available at all
|
|
113
|
+
if (availablePartials.length === 0) {
|
|
114
|
+
throw new Error('No partials are available');
|
|
115
|
+
}
|
|
116
|
+
// In development, render an inline error overlay
|
|
117
|
+
return createInlineErrorOverlay(propName, suggestions);
|
|
118
|
+
},
|
|
119
|
+
has(target, prop) {
|
|
120
|
+
return prop in target;
|
|
121
|
+
},
|
|
122
|
+
ownKeys(target) {
|
|
123
|
+
return Reflect.ownKeys(target);
|
|
124
|
+
},
|
|
125
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
126
|
+
return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|