Skip to content

Commit 0d3398e

Browse files
Merge pull request #1436 from opencomponents/preview-error-ui
add preview overlay
2 parents 7a8c8f0 + 3fa96a2 commit 0d3398e

File tree

8 files changed

+528
-24
lines changed

8 files changed

+528
-24
lines changed

package-lock.json

Lines changed: 30 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@
9898
"multer": "^1.4.3",
9999
"nice-cache": "^0.0.5",
100100
"oc-client": "^4.0.2",
101-
"oc-client-browser": "^2.1.4",
101+
"oc-client-browser": "^2.1.5",
102102
"oc-empty-response-handler": "^1.0.2",
103103
"oc-get-unix-utc-timestamp": "^1.0.6",
104104
"oc-s3-storage-adapter": "^2.2.0",
@@ -117,6 +117,7 @@
117117
"semver": "^7.7.1",
118118
"semver-extra": "^3.0.0",
119119
"serialize-error": "^8.1.0",
120+
"source-map": "^0.7.6",
120121
"targz": "^1.0.1",
121122
"try-require": "^1.2.1",
122123
"undici": "^6.21.1",
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
// import fs from 'fs';
2+
import path from 'node:path';
3+
import source from 'source-map';
4+
5+
function extractInlineSourceMap(code: string) {
6+
try {
7+
const map = code.match(
8+
/\/\/# sourceMappingURL=data:application\/json;charset=utf-8;base64,(.*)/
9+
)?.[1];
10+
if (map) {
11+
return atob(map);
12+
}
13+
return null;
14+
} catch {
15+
return null;
16+
}
17+
}
18+
19+
export async function processStackTrace({
20+
stackTrace,
21+
code
22+
}: {
23+
stackTrace: string;
24+
code: string;
25+
}) {
26+
const rawSourceMap = extractInlineSourceMap(code);
27+
if (!rawSourceMap) return null;
28+
const consumer = await new source.SourceMapConsumer(rawSourceMap);
29+
const lines = stackTrace.split('\n').filter((l) => l.trim().startsWith('at'));
30+
31+
const result = {
32+
stack: [] as string[],
33+
codeFrame: [] as string[]
34+
};
35+
36+
for (const line of lines) {
37+
// More flexible regex to handle different stack trace formats
38+
let match = line.match(/at (.+) \((.+):(\d+):(\d+)\)/);
39+
if (!match) {
40+
// Handle lines without function names like "at /path/file.js:line:col"
41+
match = line.match(/at (.+):(\d+):(\d+)/);
42+
if (match) {
43+
const [, file, lineStr, colStr] = match;
44+
match = [null, null, file, lineStr, colStr] as any;
45+
}
46+
}
47+
48+
if (!match) {
49+
result.stack.push(`${line.trim()} (could not parse)`);
50+
continue;
51+
}
52+
53+
const [, functionName, file, lineStr, colStr] = match;
54+
const lineNum = parseInt(lineStr, 10);
55+
const colNum = parseInt(colStr, 10);
56+
57+
// Check if this line is from the file we have a source map for
58+
if (!file.includes('server.js')) {
59+
result.stack.push(`${line.trim()} (no source map)`);
60+
continue;
61+
}
62+
63+
const original = consumer.originalPositionFor({
64+
line: lineNum,
65+
column: colNum
66+
});
67+
68+
if (original.source && original.line !== null) {
69+
// Filter out frames that map to external libraries or oc-server internals AFTER mapping
70+
if (
71+
original.source.includes('node_modules') ||
72+
original.source.includes('oc-server') ||
73+
original.source.includes('__oc_higherOrderServer')
74+
) {
75+
// Don't show filtered frames
76+
continue;
77+
}
78+
79+
// Try to get the function name from multiple sources
80+
let displayName = original.name || functionName || '<anonymous>';
81+
82+
// Clean up the function name if it includes object/class info
83+
if (functionName && !original.name) {
84+
displayName = functionName;
85+
}
86+
87+
// Make file paths relative to current directory for better readability
88+
let relativePath = original.source;
89+
try {
90+
if (path.isAbsolute(original.source)) {
91+
relativePath = path.relative(process.cwd(), original.source);
92+
}
93+
} catch {
94+
// Keep original path if relative conversion fails
95+
}
96+
97+
const stackLine = `at ${displayName} (${relativePath}:${original.line}:${original.column})`;
98+
result.stack.push(stackLine);
99+
100+
// Show source code context if available
101+
const sourceContent = consumer.sourceContentFor(original.source, true);
102+
if (sourceContent && original.line) {
103+
const codeFrame = getCodeFrame(
104+
sourceContent,
105+
original.line,
106+
original.column || 0
107+
);
108+
if (codeFrame) {
109+
result.codeFrame.push(codeFrame);
110+
}
111+
}
112+
} else {
113+
// Fallback to original line if source mapping fails
114+
result.stack.push(`${line.trim()} (source map failed)`);
115+
}
116+
}
117+
118+
// Don't forget to destroy the consumer
119+
try {
120+
consumer.destroy();
121+
} catch {}
122+
123+
return {
124+
stack: result.stack.join('\n'),
125+
frame: [
126+
// For some reason, the first block lacks some indentation
127+
...result.codeFrame.slice(0, 1).map((x) => ` ${x}`),
128+
...result.codeFrame.slice(1)
129+
].join('\n')
130+
};
131+
}
132+
133+
// Helper function to show code context around the error
134+
function getCodeFrame(
135+
sourceContent: string,
136+
line: number,
137+
column: number,
138+
contextLines = 2
139+
) {
140+
try {
141+
const lines = sourceContent.split('\n');
142+
const targetLine = line - 1; // Convert to 0-based
143+
144+
if (targetLine < 0 || targetLine >= lines.length) {
145+
return null;
146+
}
147+
148+
const start = Math.max(0, targetLine - contextLines);
149+
const end = Math.min(lines.length, targetLine + contextLines + 1);
150+
151+
let result = '';
152+
for (let i = start; i < end; i++) {
153+
const lineNumber = i + 1;
154+
const isTarget = i === targetLine;
155+
const prefix = isTarget ? '> ' : ' ';
156+
const lineNumberStr = String(lineNumber).padStart(3, ' ');
157+
158+
result += `${prefix}${lineNumberStr} | ${lines[i]}\n`;
159+
160+
// Add pointer to exact column for target line
161+
if (isTarget && column > 0) {
162+
const pointer = ' '.repeat(column); // Account for prefix and line number
163+
result += `${' '.repeat(5)} | ${pointer}^\n`;
164+
}
165+
}
166+
167+
return result.trim();
168+
} catch {
169+
return null;
170+
}
171+
}
172+
173+
// async function main() {
174+
// const stackTrace = `
175+
// TypeError: Cannot read properties of undefined (reading 'name')
176+
// at HandledServer.initial (/Users/ricardo.agullo/Dev/octests/helpai/_package/server.js:196:38)
177+
// at async ocServerWrapper (/Users/ricardo.agullo/Dev/octests/helpai/_package/server.js:83:19)
178+
// `;
179+
// const rawSourceMap = fs.readFileSync(
180+
// './helpai/_package/server.js.map',
181+
// 'utf8'
182+
// );
183+
// const { stack, codeFrame } = await processStackTrace({
184+
// stackTrace,
185+
// rawSourceMap
186+
// });
187+
188+
// // Log the stack trace
189+
// for (const line of stack) {
190+
// console.log(` ${line}`);
191+
// }
192+
193+
// // Log the code frames
194+
// // for (const frame of codeFrame) {
195+
// // console.log(`\n${frame}\n`);
196+
// // }
197+
// for (let i = 0; i < codeFrame.length; i++) {
198+
// if (i === 0) {
199+
// console.log(`\n ${codeFrame[i]}\n`);
200+
// } else {
201+
// console.log(`\n${codeFrame[i]}\n`);
202+
// }
203+
// }
204+
// }
205+
206+
// main().catch(console.error);

src/registry/routes/helpers/get-component.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import * as urlBuilder from '../../domain/url-builder';
2727
import * as validator from '../../domain/validators';
2828
import { validateTemplateOcVersion } from '../../domain/validators';
2929
import applyDefaultValues from './apply-default-values';
30+
import { processStackTrace } from './format-error-stack';
3031
import * as getComponentFallback from './get-component-fallback';
3132
import GetComponentRetrievingInfo from './get-component-retrieving-info';
3233

@@ -142,7 +143,7 @@ export default function getComponent(conf: Config, repository: Repository) {
142143
return env;
143144
};
144145

145-
const renderer = (
146+
const renderer = async (
146147
options: RendererOptions,
147148
cb: (result: GetComponentResult) => void
148149
) => {
@@ -325,7 +326,7 @@ export default function getComponent(conf: Config, repository: Repository) {
325326
);
326327
};
327328

328-
const returnComponent = (err: any, data: any) => {
329+
const returnComponent = async (err: any, data: any) => {
329330
if (componentCallbackDone) {
330331
return;
331332
}
@@ -395,7 +396,7 @@ export default function getComponent(conf: Config, repository: Repository) {
395396
error: err
396397
});
397398

398-
return callback({
399+
const response = {
399400
status,
400401
response: {
401402
code: 'GENERIC_ERROR',
@@ -405,10 +406,29 @@ export default function getComponent(conf: Config, repository: Repository) {
405406
details: {
406407
message: err.message,
407408
stack: err.stack,
408-
originalError: err
409+
originalError: err,
410+
frame: undefined as string | undefined
409411
}
410412
}
411-
});
413+
};
414+
415+
if (conf.local && err.stack) {
416+
const { content } = await repository
417+
.getDataProvider(component.name, component.version)
418+
.catch(() => ({ content: null }));
419+
if (content) {
420+
const processedStack = await processStackTrace({
421+
stackTrace: err.stack,
422+
code: content
423+
}).catch(() => null);
424+
if (processedStack) {
425+
response.response.details.stack = processedStack.stack;
426+
response.response.details.frame = processedStack.frame;
427+
}
428+
}
429+
}
430+
431+
return callback(response);
412432
}
413433

414434
const response: {

0 commit comments

Comments
 (0)