@stacksjs/error-handling 0.70.57 → 0.70.59
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/dist/error-page-highlighter.d.ts +4 -0
- package/dist/error-page-highlighter.js +39 -0
- package/dist/error-page-renderer.d.ts +11 -0
- package/dist/error-page-renderer.js +21 -0
- package/dist/error-page-styles.js +620 -0
- package/dist/error-page-template.d.ts +98 -0
- package/dist/error-page-template.js +224 -0
- package/dist/error-page-view-model.d.ts +70 -0
- package/dist/error-page-view-model.js +166 -0
- package/dist/error-page.js +373 -0
- package/dist/handler.js +108 -0
- package/dist/http.js +165 -0
- package/dist/index.js +20 -907
- package/dist/model.js +8 -0
- package/dist/utils.js +17 -0
- package/package.json +6 -6
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape HTML special characters
|
|
3
|
+
*/
|
|
4
|
+
export declare function escapeHtml(str: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Read source lines around a stack frame for the code snippet panel.
|
|
7
|
+
*/
|
|
8
|
+
export declare function readCodeSnippet(filePath: string, line: number, radius?: number): CodeSnippetResult | null;
|
|
9
|
+
export declare function renderCodeSnippet(snippet: CodeSnippetResult): string;
|
|
10
|
+
export declare function renderTraceFrame(frame: ParsedFrame, index: number, snippetLines: number): string;
|
|
11
|
+
export declare function renderVendorGroup(frames: ParsedFrame[], snippetLines: number): string;
|
|
12
|
+
export declare function groupTraceFrames(frames: ParsedFrame[]): Array<ParsedFrame | ParsedFrame[]>;
|
|
13
|
+
export declare function renderExceptionTrace(frames: ParsedFrame[], snippetLines: number): string;
|
|
14
|
+
export declare function renderContextTabs(opts: {
|
|
15
|
+
request?: RequestContext
|
|
16
|
+
routing?: RoutingContext
|
|
17
|
+
user?: UserContext
|
|
18
|
+
queries?: QueryInfo[]
|
|
19
|
+
showEnvironment?: boolean
|
|
20
|
+
}): string;
|
|
21
|
+
export declare function buildErrorMarkdown(opts: {
|
|
22
|
+
statusTitle: string
|
|
23
|
+
errorName: string
|
|
24
|
+
errorMessage: string
|
|
25
|
+
status: number
|
|
26
|
+
file?: string
|
|
27
|
+
line?: number
|
|
28
|
+
request?: RequestContext
|
|
29
|
+
framework?: { name: string, version?: string }
|
|
30
|
+
frames: ParsedFrame[]
|
|
31
|
+
}): string;
|
|
32
|
+
export declare function wrapErrorPage(body: string, markdown: string): string;
|
|
33
|
+
export declare const ERROR_PAGE_SCRIPT: `
|
|
34
|
+
function initErrorPage() {
|
|
35
|
+
const copyBtn = document.getElementById('copy-markdown');
|
|
36
|
+
const markdown = document.getElementById('error-markdown');
|
|
37
|
+
if (copyBtn && markdown) {
|
|
38
|
+
copyBtn.addEventListener('click', async () => {
|
|
39
|
+
try {
|
|
40
|
+
await navigator.clipboard.writeText(markdown.textContent || '');
|
|
41
|
+
copyBtn.textContent = 'Copied!';
|
|
42
|
+
copyBtn.classList.add('copied');
|
|
43
|
+
setTimeout(() => {
|
|
44
|
+
copyBtn.textContent = 'Copy as Markdown';
|
|
45
|
+
copyBtn.classList.remove('copied');
|
|
46
|
+
}, 2000);
|
|
47
|
+
} catch {
|
|
48
|
+
copyBtn.textContent = 'Copy failed';
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
document.querySelectorAll('.context-tab').forEach(tab => {
|
|
54
|
+
tab.addEventListener('click', () => {
|
|
55
|
+
const id = tab.getAttribute('data-tab');
|
|
56
|
+
document.querySelectorAll('.context-tab').forEach(t => t.classList.remove('active'));
|
|
57
|
+
document.querySelectorAll('.context-panel').forEach(p => p.classList.remove('active'));
|
|
58
|
+
tab.classList.add('active');
|
|
59
|
+
const panel = document.querySelector('[data-panel="' + id + '"]');
|
|
60
|
+
if (panel) panel.classList.add('active');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
document.addEventListener('DOMContentLoaded', initErrorPage);
|
|
65
|
+
`;
|
|
66
|
+
export declare interface ParsedFrame {
|
|
67
|
+
file: string
|
|
68
|
+
line: number
|
|
69
|
+
column?: number
|
|
70
|
+
function?: string
|
|
71
|
+
absoluteFile: string
|
|
72
|
+
isFramework: boolean
|
|
73
|
+
}
|
|
74
|
+
declare interface QueryInfo {
|
|
75
|
+
query: string
|
|
76
|
+
time?: number
|
|
77
|
+
connection?: string
|
|
78
|
+
}
|
|
79
|
+
declare interface RequestContext {
|
|
80
|
+
method: string
|
|
81
|
+
url: string
|
|
82
|
+
headers: Record<string, string>
|
|
83
|
+
queryParams?: Record<string, string>
|
|
84
|
+
body?: unknown
|
|
85
|
+
}
|
|
86
|
+
declare interface RoutingContext {
|
|
87
|
+
controller?: string
|
|
88
|
+
routeName?: string
|
|
89
|
+
middleware?: string[]
|
|
90
|
+
}
|
|
91
|
+
declare interface UserContext {
|
|
92
|
+
id?: string | number
|
|
93
|
+
email?: string
|
|
94
|
+
name?: string
|
|
95
|
+
}
|
|
96
|
+
export declare interface CodeSnippetResult {
|
|
97
|
+
lines: Array<{ number: number, content: string, highlight: boolean }>
|
|
98
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
var {require}=import.meta;import { ERROR_PAGE_CSS } from "./error-page-styles";
|
|
2
|
+
export function escapeHtml(str) {
|
|
3
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
4
|
+
}
|
|
5
|
+
export function readCodeSnippet(filePath, line, radius = 6) {
|
|
6
|
+
try {
|
|
7
|
+
const fs = require("node:fs");
|
|
8
|
+
if (!filePath || !fs.existsSync(filePath))
|
|
9
|
+
return null;
|
|
10
|
+
const allLines = fs.readFileSync(filePath, "utf-8").split(`
|
|
11
|
+
`), start = Math.max(0, line - radius - 1), end = Math.min(allLines.length, line + radius);
|
|
12
|
+
return {
|
|
13
|
+
lines: allLines.slice(start, end).map((code, i) => ({
|
|
14
|
+
number: start + i + 1,
|
|
15
|
+
content: code,
|
|
16
|
+
highlight: start + i + 1 === line
|
|
17
|
+
}))
|
|
18
|
+
};
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function renderCodeSnippet(snippet) {
|
|
24
|
+
return `<div class="code-block">${snippet.lines.map((l) => `
|
|
25
|
+
<div class="code-line${l.highlight ? " highlight" : ""}">
|
|
26
|
+
<span class="code-ln">${l.number}</span>
|
|
27
|
+
<span class="code-txt">${escapeHtml(l.content || " ")}</span>
|
|
28
|
+
</div>`).join("")}
|
|
29
|
+
</div>`;
|
|
30
|
+
}
|
|
31
|
+
export function renderTraceFrame(frame, index, snippetLines) {
|
|
32
|
+
const snippet = readCodeSnippet(frame.absoluteFile, frame.line, snippetLines), snippetHtml = snippet ? renderCodeSnippet(snippet) : "";
|
|
33
|
+
return `<div class="trace-frame${index === 0 ? " expanded" : ""}" data-frame>
|
|
34
|
+
<div class="trace-frame-header" onclick="this.parentElement.classList.toggle('expanded')">
|
|
35
|
+
<span class="trace-frame-fn">${escapeHtml(frame.function || "<anonymous>")}</span>
|
|
36
|
+
<span class="trace-frame-file">${escapeHtml(frame.file)}:${frame.line}</span>
|
|
37
|
+
</div>
|
|
38
|
+
${snippetHtml ? `<div class="trace-frame-body">${snippetHtml}</div>` : ""}
|
|
39
|
+
</div>`;
|
|
40
|
+
}
|
|
41
|
+
export function renderVendorGroup(frames, snippetLines) {
|
|
42
|
+
const count = frames.length, label = count === 1 ? "1 framework frame" : `${count} framework frames`, inner = frames.map((f, i) => renderTraceFrame(f, i + 1, snippetLines)).join("");
|
|
43
|
+
return `<div class="trace-vendor" data-vendor-group>
|
|
44
|
+
<div class="trace-vendor-toggle" onclick="this.parentElement.classList.toggle('expanded')">
|
|
45
|
+
<span>${escapeHtml(label)}</span>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="trace-vendor-frames">${inner}</div>
|
|
48
|
+
</div>`;
|
|
49
|
+
}
|
|
50
|
+
export function groupTraceFrames(frames) {
|
|
51
|
+
const groups = [];
|
|
52
|
+
let vendorBatch = [];
|
|
53
|
+
const flushVendor = () => {
|
|
54
|
+
if (vendorBatch.length > 0) {
|
|
55
|
+
groups.push(vendorBatch.length === 1 ? vendorBatch[0] : [...vendorBatch]);
|
|
56
|
+
vendorBatch = [];
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
for (const frame of frames)
|
|
60
|
+
if (frame.isFramework)
|
|
61
|
+
vendorBatch.push(frame);
|
|
62
|
+
else {
|
|
63
|
+
flushVendor();
|
|
64
|
+
groups.push(frame);
|
|
65
|
+
}
|
|
66
|
+
flushVendor();
|
|
67
|
+
return groups;
|
|
68
|
+
}
|
|
69
|
+
export function renderExceptionTrace(frames, snippetLines) {
|
|
70
|
+
if (frames.length === 0)
|
|
71
|
+
return "";
|
|
72
|
+
return `<section class="trace">
|
|
73
|
+
<h2 class="trace-title">Exception trace</h2>
|
|
74
|
+
${groupTraceFrames(frames).map((group, i) => {
|
|
75
|
+
if (Array.isArray(group))
|
|
76
|
+
return renderVendorGroup(group, snippetLines);
|
|
77
|
+
return renderTraceFrame(group, i, snippetLines);
|
|
78
|
+
}).join("")}
|
|
79
|
+
</section>`;
|
|
80
|
+
}
|
|
81
|
+
export function renderContextTabs(opts) {
|
|
82
|
+
const tabs = [];
|
|
83
|
+
if (opts.request) {
|
|
84
|
+
const headerRows = Object.entries(opts.request.headers).map(([k, v]) => `<tr><td>${escapeHtml(k)}</td><td>${escapeHtml(v)}</td></tr>`).join("");
|
|
85
|
+
tabs.push({
|
|
86
|
+
id: "headers",
|
|
87
|
+
label: "Headers",
|
|
88
|
+
html: headerRows ? `<table class="kv-table">${headerRows}</table>` : '<p style="color:var(--neutral-500);font-size:0.875rem">No headers</p>'
|
|
89
|
+
});
|
|
90
|
+
const bodyContent = opts.request.body ? `<div class="json-block">${escapeHtml(JSON.stringify(opts.request.body, null, 2))}</div>` : '<p style="color:var(--neutral-500);font-size:0.875rem">// No request body</p>';
|
|
91
|
+
tabs.push({
|
|
92
|
+
id: "body",
|
|
93
|
+
label: "Body",
|
|
94
|
+
html: bodyContent
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (opts.routing) {
|
|
98
|
+
const rows = [
|
|
99
|
+
opts.routing.controller ? `<tr><td>controller</td><td>${escapeHtml(opts.routing.controller)}</td></tr>` : "",
|
|
100
|
+
opts.routing.routeName ? `<tr><td>route name</td><td>${escapeHtml(opts.routing.routeName)}</td></tr>` : "",
|
|
101
|
+
opts.routing.middleware?.length ? `<tr><td>middleware</td><td>${escapeHtml(opts.routing.middleware.join(", "))}</td></tr>` : ""
|
|
102
|
+
].filter(Boolean).join("");
|
|
103
|
+
if (rows)
|
|
104
|
+
tabs.push({
|
|
105
|
+
id: "routing",
|
|
106
|
+
label: "Routing",
|
|
107
|
+
html: `<table class="kv-table">${rows}</table>`
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (opts.user) {
|
|
111
|
+
const rows = [
|
|
112
|
+
opts.user.id !== void 0 ? `<tr><td>id</td><td>${escapeHtml(String(opts.user.id))}</td></tr>` : "",
|
|
113
|
+
opts.user.email ? `<tr><td>email</td><td>${escapeHtml(opts.user.email)}</td></tr>` : "",
|
|
114
|
+
opts.user.name ? `<tr><td>name</td><td>${escapeHtml(opts.user.name)}</td></tr>` : ""
|
|
115
|
+
].filter(Boolean).join("");
|
|
116
|
+
if (rows)
|
|
117
|
+
tabs.push({
|
|
118
|
+
id: "user",
|
|
119
|
+
label: "User",
|
|
120
|
+
html: `<table class="kv-table">${rows}</table>`
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (opts.queries && opts.queries.length > 0)
|
|
124
|
+
tabs.push({
|
|
125
|
+
id: "queries",
|
|
126
|
+
label: `Queries (${opts.queries.length})`,
|
|
127
|
+
html: opts.queries.map((q) => `
|
|
128
|
+
<div class="query-item">
|
|
129
|
+
${escapeHtml(q.query)}
|
|
130
|
+
${q.time !== void 0 ? `<div class="query-time">${q.time.toFixed(2)}ms${q.connection ? ` \u2022 ${escapeHtml(q.connection)}` : ""}</div>` : ""}
|
|
131
|
+
</div>`).join("")
|
|
132
|
+
});
|
|
133
|
+
if (opts.showEnvironment)
|
|
134
|
+
tabs.push({
|
|
135
|
+
id: "environment",
|
|
136
|
+
label: "Environment",
|
|
137
|
+
html: `<table class="kv-table">
|
|
138
|
+
<tr><td>runtime</td><td>${typeof process < "u" ? escapeHtml(process.version) : "N/A"}</td></tr>
|
|
139
|
+
<tr><td>platform</td><td>${typeof process < "u" ? escapeHtml(process.platform) : "N/A"}</td></tr>
|
|
140
|
+
<tr><td>arch</td><td>${typeof process < "u" ? escapeHtml(process.arch) : "N/A"}</td></tr>
|
|
141
|
+
</table>`
|
|
142
|
+
});
|
|
143
|
+
if (tabs.length === 0)
|
|
144
|
+
return "";
|
|
145
|
+
const tabButtons = tabs.map((t, i) => `<button class="context-tab${i === 0 ? " active" : ""}" data-tab="${t.id}" type="button">${escapeHtml(t.label)}</button>`).join(""), panels = tabs.map((t, i) => `<div class="context-panel${i === 0 ? " active" : ""}" data-panel="${t.id}">${t.html}</div>`).join("");
|
|
146
|
+
return `<section class="context">
|
|
147
|
+
<div class="context-tabs">${tabButtons}</div>
|
|
148
|
+
${panels}
|
|
149
|
+
</section>`;
|
|
150
|
+
}
|
|
151
|
+
export function buildErrorMarkdown(opts) {
|
|
152
|
+
const lines = [
|
|
153
|
+
`# ${opts.statusTitle}`,
|
|
154
|
+
"",
|
|
155
|
+
`## ${opts.errorName}`,
|
|
156
|
+
"",
|
|
157
|
+
opts.errorMessage,
|
|
158
|
+
""
|
|
159
|
+
];
|
|
160
|
+
if (opts.file)
|
|
161
|
+
lines.push(`**${opts.file}${opts.line ? `:${opts.line}` : ""}**`, "");
|
|
162
|
+
if (opts.framework)
|
|
163
|
+
lines.push(`**${opts.framework.name.toUpperCase()}** ${opts.framework.version ?? ""}`.trim(), "");
|
|
164
|
+
lines.push(`**${opts.status}**`, "");
|
|
165
|
+
if (opts.request)
|
|
166
|
+
lines.push(`\`${opts.request.method}\` ${opts.request.url}`, "");
|
|
167
|
+
if (opts.frames.length > 0) {
|
|
168
|
+
lines.push("## Exception trace", "");
|
|
169
|
+
for (const frame of opts.frames.slice(0, 20))
|
|
170
|
+
lines.push(`\`${frame.function || "<anonymous>"}\` \u2014 ${frame.file}:${frame.line}`);
|
|
171
|
+
}
|
|
172
|
+
return lines.join(`
|
|
173
|
+
`);
|
|
174
|
+
}
|
|
175
|
+
export const ERROR_PAGE_SCRIPT = `
|
|
176
|
+
function initErrorPage() {
|
|
177
|
+
const copyBtn = document.getElementById('copy-markdown');
|
|
178
|
+
const markdown = document.getElementById('error-markdown');
|
|
179
|
+
if (copyBtn && markdown) {
|
|
180
|
+
copyBtn.addEventListener('click', async () => {
|
|
181
|
+
try {
|
|
182
|
+
await navigator.clipboard.writeText(markdown.textContent || '');
|
|
183
|
+
copyBtn.textContent = 'Copied!';
|
|
184
|
+
copyBtn.classList.add('copied');
|
|
185
|
+
setTimeout(() => {
|
|
186
|
+
copyBtn.textContent = 'Copy as Markdown';
|
|
187
|
+
copyBtn.classList.remove('copied');
|
|
188
|
+
}, 2000);
|
|
189
|
+
} catch {
|
|
190
|
+
copyBtn.textContent = 'Copy failed';
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
document.querySelectorAll('.context-tab').forEach(tab => {
|
|
196
|
+
tab.addEventListener('click', () => {
|
|
197
|
+
const id = tab.getAttribute('data-tab');
|
|
198
|
+
document.querySelectorAll('.context-tab').forEach(t => t.classList.remove('active'));
|
|
199
|
+
document.querySelectorAll('.context-panel').forEach(p => p.classList.remove('active'));
|
|
200
|
+
tab.classList.add('active');
|
|
201
|
+
const panel = document.querySelector('[data-panel="' + id + '"]');
|
|
202
|
+
if (panel) panel.classList.add('active');
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
document.addEventListener('DOMContentLoaded', initErrorPage);
|
|
207
|
+
`;
|
|
208
|
+
export function wrapErrorPage(body, markdown) {
|
|
209
|
+
return `<!DOCTYPE html>
|
|
210
|
+
<html lang="en">
|
|
211
|
+
<head>
|
|
212
|
+
<meta charset="UTF-8">
|
|
213
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
214
|
+
<style>${ERROR_PAGE_CSS}</style>
|
|
215
|
+
</head>
|
|
216
|
+
<body>
|
|
217
|
+
<div class="page">
|
|
218
|
+
${body}
|
|
219
|
+
</div>
|
|
220
|
+
<script id="error-markdown" type="text/plain">${escapeHtml(markdown)}</script>
|
|
221
|
+
<script>${ERROR_PAGE_SCRIPT}</script>
|
|
222
|
+
</body>
|
|
223
|
+
</html>`;
|
|
224
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { languageForFile } from './error-page-highlighter';
|
|
2
|
+
import type { ErrorPageConfig, QueryInfo, RequestContext, RoutingContext, UserContext } from './error-page';
|
|
3
|
+
declare function parseStackTrace(stack: string | undefined, basePaths?: string[], options?: { includeFrameworkFrames?: boolean }): ParsedFrame[];
|
|
4
|
+
declare function groupFrames(frames: ParsedFrame[]): Array<ParsedFrame | ParsedFrame[]>;
|
|
5
|
+
export declare function buildErrorPageViewModel(opts: {
|
|
6
|
+
error: Error
|
|
7
|
+
status: number
|
|
8
|
+
config: ErrorPageConfig
|
|
9
|
+
framework?: { name: string, version?: string }
|
|
10
|
+
request?: RequestContext
|
|
11
|
+
routing?: RoutingContext
|
|
12
|
+
user?: UserContext
|
|
13
|
+
queries?: QueryInfo[]
|
|
14
|
+
}): Promise<ErrorPageViewModel>;
|
|
15
|
+
export declare interface FrameViewModel {
|
|
16
|
+
function: string
|
|
17
|
+
file: string
|
|
18
|
+
line: number
|
|
19
|
+
expanded: boolean
|
|
20
|
+
hasCode: boolean
|
|
21
|
+
codeHtml: string
|
|
22
|
+
isVendor: boolean
|
|
23
|
+
}
|
|
24
|
+
export declare interface TraceGroupViewModel {
|
|
25
|
+
type: 'frame' | 'vendor'
|
|
26
|
+
frames: FrameViewModel[]
|
|
27
|
+
vendorCount: number
|
|
28
|
+
}
|
|
29
|
+
export declare interface KeyValueRow {
|
|
30
|
+
key: string
|
|
31
|
+
value: string
|
|
32
|
+
}
|
|
33
|
+
export declare interface ErrorPageViewModel {
|
|
34
|
+
pageTitle: string
|
|
35
|
+
statusTitle: string
|
|
36
|
+
markdownJson: string
|
|
37
|
+
exceptionClass: string
|
|
38
|
+
message: string
|
|
39
|
+
fileLine: string
|
|
40
|
+
statusCode: number
|
|
41
|
+
errorCode: string
|
|
42
|
+
frameworkLabel: string
|
|
43
|
+
frameworkVersion: string
|
|
44
|
+
runtimeLabel: string
|
|
45
|
+
runtimeVersion: string
|
|
46
|
+
requestMethod: string
|
|
47
|
+
requestUrl: string
|
|
48
|
+
hasRequest: boolean
|
|
49
|
+
hintsHtml: string
|
|
50
|
+
traceGroups: TraceGroupViewModel[]
|
|
51
|
+
queries: Array<{ connection: string, sql: string, sqlHtml: string, time: string }>
|
|
52
|
+
queryCount: number
|
|
53
|
+
headers: KeyValueRow[]
|
|
54
|
+
bodyContent: string
|
|
55
|
+
routing: KeyValueRow[]
|
|
56
|
+
routeParamsJson: string
|
|
57
|
+
userRows: KeyValueRow[]
|
|
58
|
+
environmentRows: KeyValueRow[]
|
|
59
|
+
hasUser: boolean
|
|
60
|
+
hasEnvironment: boolean
|
|
61
|
+
hasQueries: boolean
|
|
62
|
+
hasHeaders: boolean
|
|
63
|
+
hasBody: boolean
|
|
64
|
+
hasRouting: boolean
|
|
65
|
+
hasRouteParams: boolean
|
|
66
|
+
highlightCss: string
|
|
67
|
+
enableCopyMarkdown: boolean
|
|
68
|
+
}
|
|
69
|
+
// Re-export for tests
|
|
70
|
+
export { parseStackTrace, groupFrames, languageForFile };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { HTTP_ERRORS as HTTP_ERROR_MAP, isFrameworkFrame, renderHttpErrorHints } from "./error-page";
|
|
2
|
+
import { highlightSnippet, languageForFile } from "./error-page-highlighter";
|
|
3
|
+
import { buildErrorMarkdown, escapeHtml, readCodeSnippet } from "./error-page-template";
|
|
4
|
+
function parseStackTrace(stack, basePaths, options = {}) {
|
|
5
|
+
if (!stack)
|
|
6
|
+
return [];
|
|
7
|
+
const lines = stack.split(`
|
|
8
|
+
`).slice(1), frames = [], includeAll = options.includeFrameworkFrames === !0;
|
|
9
|
+
for (const line of lines) {
|
|
10
|
+
const match = line.match(/^\s*at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?$/);
|
|
11
|
+
if (match) {
|
|
12
|
+
let file = match[2];
|
|
13
|
+
if (file === void 0)
|
|
14
|
+
continue;
|
|
15
|
+
const original = file, isFramework = isFrameworkFrame(original);
|
|
16
|
+
if (basePaths) {
|
|
17
|
+
for (const basePath of basePaths)
|
|
18
|
+
if (file.startsWith(basePath)) {
|
|
19
|
+
file = file.slice(basePath.length + 1);
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (!includeAll && isFramework)
|
|
24
|
+
continue;
|
|
25
|
+
frames.push({
|
|
26
|
+
function: match[1] || "<anonymous>",
|
|
27
|
+
file,
|
|
28
|
+
absoluteFile: original,
|
|
29
|
+
isFramework,
|
|
30
|
+
line: parseInt(match[3] ?? "0", 10),
|
|
31
|
+
column: parseInt(match[4] ?? "0", 10)
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!includeAll && frames.length === 0)
|
|
36
|
+
return parseStackTrace(stack, basePaths, { includeFrameworkFrames: !0 });
|
|
37
|
+
return frames;
|
|
38
|
+
}
|
|
39
|
+
function groupFrames(frames) {
|
|
40
|
+
const groups = [];
|
|
41
|
+
let vendorBatch = [];
|
|
42
|
+
const flushVendor = () => {
|
|
43
|
+
if (vendorBatch.length > 0) {
|
|
44
|
+
groups.push(vendorBatch.length === 1 ? vendorBatch[0] : [...vendorBatch]);
|
|
45
|
+
vendorBatch = [];
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
for (const frame of frames)
|
|
49
|
+
if (frame.isFramework)
|
|
50
|
+
vendorBatch.push(frame);
|
|
51
|
+
else {
|
|
52
|
+
flushVendor();
|
|
53
|
+
groups.push(frame);
|
|
54
|
+
}
|
|
55
|
+
flushVendor();
|
|
56
|
+
return groups;
|
|
57
|
+
}
|
|
58
|
+
async function buildFrameView(frame, index, snippetLines) {
|
|
59
|
+
const snippet = readCodeSnippet(frame.absoluteFile, frame.line, snippetLines);
|
|
60
|
+
let codeHtml = "";
|
|
61
|
+
if (snippet) {
|
|
62
|
+
const code = snippet.lines.map((l) => l.content).join(`
|
|
63
|
+
`), startingLine = snippet.lines[0]?.number ?? frame.line;
|
|
64
|
+
codeHtml = (await highlightSnippet(code, frame.file, frame.line, startingLine)).html;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
function: frame.function || "<anonymous>",
|
|
68
|
+
file: frame.file,
|
|
69
|
+
line: frame.line,
|
|
70
|
+
expanded: index === 0,
|
|
71
|
+
hasCode: codeHtml.length > 0,
|
|
72
|
+
codeHtml,
|
|
73
|
+
isVendor: frame.isFramework
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export async function buildErrorPageViewModel(opts) {
|
|
77
|
+
const { error, status, config } = opts, statusTitle = HTTP_ERROR_MAP[status]?.title ?? "Error", frames = parseStackTrace(error.stack, config.basePaths, {
|
|
78
|
+
includeFrameworkFrames: config.showFrameworkFrames === !0
|
|
79
|
+
}), topFrame = frames[0], runtimeVersion = typeof process < "u" ? process.version.replace(/^v/, "") : "", runtimeLabel = typeof process < "u" && process.versions.bun ? "BUN" : "NODE", errorCode = error.code, traceGroups = [];
|
|
80
|
+
let frameIndex = 0;
|
|
81
|
+
for (const group of groupFrames(frames))
|
|
82
|
+
if (Array.isArray(group)) {
|
|
83
|
+
const vendorFrames = await Promise.all(group.map((f, i) => buildFrameView(f, frameIndex + i, config.snippetLines ?? 8)));
|
|
84
|
+
traceGroups.push({ type: "vendor", frames: vendorFrames, vendorCount: vendorFrames.length });
|
|
85
|
+
frameIndex += vendorFrames.length;
|
|
86
|
+
} else {
|
|
87
|
+
traceGroups.push({
|
|
88
|
+
type: "frame",
|
|
89
|
+
frames: [await buildFrameView(group, frameIndex, config.snippetLines ?? 8)],
|
|
90
|
+
vendorCount: 0
|
|
91
|
+
});
|
|
92
|
+
frameIndex += 1;
|
|
93
|
+
}
|
|
94
|
+
const headers = opts.request ? Object.entries(opts.request.headers).map(([key, value]) => ({ key, value })) : [], routing = [];
|
|
95
|
+
if (opts.routing?.controller)
|
|
96
|
+
routing.push({ key: "controller", value: opts.routing.controller });
|
|
97
|
+
if (opts.routing?.routeName)
|
|
98
|
+
routing.push({ key: "route name", value: opts.routing.routeName });
|
|
99
|
+
if (opts.routing?.middleware?.length)
|
|
100
|
+
routing.push({ key: "middleware", value: opts.routing.middleware.join(", ") });
|
|
101
|
+
const userRows = [];
|
|
102
|
+
if (opts.user?.id !== void 0)
|
|
103
|
+
userRows.push({ key: "id", value: String(opts.user.id) });
|
|
104
|
+
if (opts.user?.email)
|
|
105
|
+
userRows.push({ key: "email", value: opts.user.email });
|
|
106
|
+
if (opts.user?.name)
|
|
107
|
+
userRows.push({ key: "name", value: opts.user.name });
|
|
108
|
+
const environmentRows = config.showEnvironment && typeof process < "u" ? [
|
|
109
|
+
{ key: "runtime", value: process.version },
|
|
110
|
+
{ key: "platform", value: process.platform },
|
|
111
|
+
{ key: "arch", value: process.arch }
|
|
112
|
+
] : [], queries = (opts.queries ?? []).slice(0, 100).map((q) => ({
|
|
113
|
+
connection: q.connection ?? "default",
|
|
114
|
+
sql: q.query,
|
|
115
|
+
sqlHtml: escapeHtml(q.query),
|
|
116
|
+
time: q.time !== void 0 ? q.time.toFixed(2) : "0.00"
|
|
117
|
+
})), bodyContent = opts.request?.body ? JSON.stringify(opts.request.body, null, 2) : "// No request body", routeParamsJson = opts.request?.queryParams && Object.keys(opts.request.queryParams).length > 0 ? JSON.stringify(opts.request.queryParams, null, 2) : "", markdown = buildErrorMarkdown({
|
|
118
|
+
statusTitle,
|
|
119
|
+
errorName: error.name || "Error",
|
|
120
|
+
errorMessage: error.message,
|
|
121
|
+
status,
|
|
122
|
+
file: topFrame?.file,
|
|
123
|
+
line: topFrame?.line,
|
|
124
|
+
request: opts.request,
|
|
125
|
+
framework: opts.framework,
|
|
126
|
+
frames
|
|
127
|
+
}), { getSharedHighlighterCss } = await import("./error-page-highlighter");
|
|
128
|
+
return {
|
|
129
|
+
pageTitle: statusTitle,
|
|
130
|
+
statusTitle,
|
|
131
|
+
markdownJson: JSON.stringify(markdown),
|
|
132
|
+
exceptionClass: error.name || "Error",
|
|
133
|
+
message: error.message,
|
|
134
|
+
fileLine: topFrame ? `${topFrame.file}:${topFrame.line}` : "",
|
|
135
|
+
statusCode: status,
|
|
136
|
+
errorCode: errorCode !== void 0 ? String(errorCode) : "0",
|
|
137
|
+
frameworkLabel: opts.framework?.name?.toUpperCase() ?? "STACKS",
|
|
138
|
+
frameworkVersion: opts.framework?.version ?? "",
|
|
139
|
+
runtimeLabel,
|
|
140
|
+
runtimeVersion,
|
|
141
|
+
requestMethod: opts.request?.method ?? "GET",
|
|
142
|
+
requestUrl: opts.request?.url ?? "",
|
|
143
|
+
hasRequest: !!opts.request,
|
|
144
|
+
hintsHtml: renderHttpErrorHints(status),
|
|
145
|
+
traceGroups,
|
|
146
|
+
queries,
|
|
147
|
+
queryCount: queries.length,
|
|
148
|
+
headers,
|
|
149
|
+
bodyContent,
|
|
150
|
+
routing,
|
|
151
|
+
routeParamsJson,
|
|
152
|
+
userRows,
|
|
153
|
+
environmentRows,
|
|
154
|
+
hasUser: userRows.length > 0,
|
|
155
|
+
hasEnvironment: environmentRows.length > 0,
|
|
156
|
+
hasQueries: queries.length > 0 && config.showQueries !== !1,
|
|
157
|
+
hasHeaders: headers.length > 0 && config.showRequest !== !1,
|
|
158
|
+
hasBody: config.showRequest !== !1,
|
|
159
|
+
hasRouting: routing.length > 0,
|
|
160
|
+
hasRouteParams: routeParamsJson.length > 0,
|
|
161
|
+
highlightCss: getSharedHighlighterCss(),
|
|
162
|
+
enableCopyMarkdown: config.enableCopyMarkdown !== !1
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export { parseStackTrace, groupFrames, languageForFile };
|