@stacksjs/error-handling 0.70.88 → 0.70.91

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.
@@ -0,0 +1,66 @@
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: string;
34
+ export declare interface ParsedFrame {
35
+ file: string
36
+ line: number
37
+ column?: number
38
+ function?: string
39
+ absoluteFile: string
40
+ isFramework: boolean
41
+ }
42
+ declare interface QueryInfo {
43
+ query: string
44
+ time?: number
45
+ connection?: string
46
+ }
47
+ declare interface RequestContext {
48
+ method: string
49
+ url: string
50
+ headers: Record<string, string>
51
+ queryParams?: Record<string, string>
52
+ body?: unknown
53
+ }
54
+ declare interface RoutingContext {
55
+ controller?: string
56
+ routeName?: string
57
+ middleware?: string[]
58
+ }
59
+ declare interface UserContext {
60
+ id?: string | number
61
+ email?: string
62
+ name?: string
63
+ }
64
+ export declare interface CodeSnippetResult {
65
+ lines: Array<{ number: number, content: string, highlight: boolean }>
66
+ }
@@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
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 };