@usebruno/js 0.46.1 → 0.48.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/package.json +9 -7
- package/src/bru.js +173 -75
- package/src/bruno-request.js +15 -7
- package/src/bruno-response.js +4 -0
- package/src/cookie-list.js +272 -0
- package/src/header-list.js +497 -0
- package/src/index.js +27 -2
- package/src/property-list.js +184 -0
- package/src/readonly-property-list.js +227 -0
- package/src/runtime/assert-runtime.js +164 -10
- package/src/runtime/script-runtime.js +55 -6
- package/src/runtime/test-runtime.js +19 -1
- package/src/runtime/vars-runtime.js +20 -3
- package/src/sandbox/bundle-browser-rollup.js +72 -66
- package/src/sandbox/bundle-libraries.js +10 -2
- package/src/sandbox/quickjs/index.js +8 -41
- package/src/sandbox/quickjs/shims/bru.js +31 -1
- package/src/sandbox/quickjs/shims/bruno-request.js +24 -3
- package/src/sandbox/quickjs/shims/bruno-response.js +57 -5
- package/src/sandbox/quickjs/shims/bruno-response.spec.js +91 -0
- package/src/sandbox/quickjs/shims/lib/uuid.spec.js +166 -0
- package/src/sandbox/quickjs/shims/require.js +56 -0
- package/src/sandbox/quickjs/shims/require.spec.js +154 -0
- package/src/sandbox/quickjs/shims/test.js +175 -2
- package/src/sandbox/quickjs/utils/property-list-bridge.js +190 -0
- package/src/sandbox/quickjs/utils/test-helpers.js +31 -0
- package/src/utils/error-formatter.js +345 -20
- package/src/utils/error-formatter.spec.js +683 -1
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
2
3
|
const YAML = require('yaml');
|
|
3
4
|
const { NODEVM_SCRIPT_WRAPPER_OFFSET, QUICKJS_SCRIPT_WRAPPER_OFFSET } = require('./sandbox');
|
|
4
5
|
|
|
6
|
+
const posixifyPath = (p) => (p ? p.replace(/\\/g, '/') : p);
|
|
7
|
+
|
|
5
8
|
const DEFAULT_CONTEXT_LINES = 5;
|
|
6
|
-
const ALLOWED_SOURCE_EXTENSIONS = ['.bru', '.yml'
|
|
9
|
+
const ALLOWED_SOURCE_EXTENSIONS = ['.bru', '.yml'];
|
|
7
10
|
|
|
8
11
|
const isAllowedSourceFile = (filePath) =>
|
|
9
12
|
typeof filePath === 'string' && ALLOWED_SOURCE_EXTENSIONS.some((ext) => filePath.endsWith(ext));
|
|
@@ -64,9 +67,45 @@ const findScriptBlockStartLine = (filePath, scriptType, cache = null) => {
|
|
|
64
67
|
return result;
|
|
65
68
|
};
|
|
66
69
|
|
|
70
|
+
/** Find the 1-indexed last content line of a script block in a .bru file (excludes closing }) */
|
|
71
|
+
const findScriptBlockEndLine = (filePath, scriptType, cache = null) => {
|
|
72
|
+
if (!filePath.endsWith('.bru')) return null;
|
|
73
|
+
|
|
74
|
+
const cacheKey = `bru-end:${filePath}:${scriptType}`;
|
|
75
|
+
if (cache?.has(cacheKey)) return cache.get(cacheKey);
|
|
76
|
+
|
|
77
|
+
const content = readFile(filePath, cache);
|
|
78
|
+
if (!content) return null;
|
|
79
|
+
|
|
80
|
+
const pattern = BLOCK_PATTERNS[scriptType];
|
|
81
|
+
if (!pattern) return null;
|
|
82
|
+
|
|
83
|
+
const lines = content.split('\n');
|
|
84
|
+
let inBlock = false;
|
|
85
|
+
let hasContent = false;
|
|
86
|
+
let result = null;
|
|
87
|
+
for (let i = 0; i < lines.length; i++) {
|
|
88
|
+
if (!inBlock && pattern.test(lines[i])) {
|
|
89
|
+
inBlock = true;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (inBlock) {
|
|
93
|
+
if (/^\}/.test(lines[i])) {
|
|
94
|
+
// Closing brace at 0-indexed position i; last content line is at 0-indexed (i-1) = 1-indexed i
|
|
95
|
+
result = hasContent ? i : null;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
hasContent = true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (cache) cache.set(cacheKey, result);
|
|
103
|
+
return result;
|
|
104
|
+
};
|
|
105
|
+
|
|
67
106
|
/** Find the 1-indexed line where a script block's content starts in a .yml file */
|
|
68
107
|
const findYmlScriptBlockStartLine = (filePath, scriptType, cache = null) => {
|
|
69
|
-
if (!filePath.endsWith('.yml')
|
|
108
|
+
if (!filePath.endsWith('.yml')) return null;
|
|
70
109
|
|
|
71
110
|
const cacheKey = `yml:${filePath}:${scriptType}`;
|
|
72
111
|
if (cache?.has(cacheKey)) return cache.get(cacheKey);
|
|
@@ -97,7 +136,52 @@ const findYmlScriptBlockStartLine = (filePath, scriptType, cache = null) => {
|
|
|
97
136
|
}
|
|
98
137
|
}
|
|
99
138
|
}
|
|
100
|
-
if (result) break;
|
|
139
|
+
if (result != null) break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
// invalid YAML
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (cache) cache.set(cacheKey, result);
|
|
147
|
+
return result;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/** Find the 1-indexed last content line of a script block in a .yml file */
|
|
151
|
+
const findYmlScriptBlockEndLine = (filePath, scriptType, cache = null) => {
|
|
152
|
+
if (!filePath.endsWith('.yml')) return null;
|
|
153
|
+
|
|
154
|
+
const cacheKey = `yml-end:${filePath}:${scriptType}`;
|
|
155
|
+
if (cache?.has(cacheKey)) return cache.get(cacheKey);
|
|
156
|
+
|
|
157
|
+
const content = readFile(filePath, cache);
|
|
158
|
+
if (!content) return null;
|
|
159
|
+
|
|
160
|
+
const ymlType = SCRIPT_TYPE_TO_YML[scriptType];
|
|
161
|
+
if (!ymlType) return null;
|
|
162
|
+
|
|
163
|
+
let result = null;
|
|
164
|
+
try {
|
|
165
|
+
const lineCounter = new YAML.LineCounter();
|
|
166
|
+
const doc = YAML.parseDocument(content, { lineCounter });
|
|
167
|
+
|
|
168
|
+
const scriptPaths = [['runtime', 'scripts'], ['request', 'scripts']];
|
|
169
|
+
for (const scriptPath of scriptPaths) {
|
|
170
|
+
const scripts = doc.getIn(scriptPath, true);
|
|
171
|
+
if (YAML.isSeq(scripts)) {
|
|
172
|
+
for (const item of scripts.items) {
|
|
173
|
+
if (!YAML.isMap(item)) continue;
|
|
174
|
+
if (item.get('type') === ymlType) {
|
|
175
|
+
const codeNode = item.get('code', true);
|
|
176
|
+
if (codeNode && codeNode.range) {
|
|
177
|
+
// range[1] is the end offset; go back 1 to get the last content character
|
|
178
|
+
const endOffset = Math.max(codeNode.range[1] - 1, codeNode.range[0]);
|
|
179
|
+
result = lineCounter.linePos(endOffset).line;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (result != null) break;
|
|
101
185
|
}
|
|
102
186
|
}
|
|
103
187
|
} catch {
|
|
@@ -111,7 +195,7 @@ const findYmlScriptBlockStartLine = (filePath, scriptType, cache = null) => {
|
|
|
111
195
|
/** Adjust a runtime-reported line number to the actual line in the .bru/.yml file */
|
|
112
196
|
const adjustLineNumber = (filePath, reportedLine, isQuickJS, scriptType = null, cache = null, scriptMetadata = null) => {
|
|
113
197
|
const isBruFile = filePath.endsWith('.bru');
|
|
114
|
-
const isYmlFile = filePath.endsWith('.yml')
|
|
198
|
+
const isYmlFile = filePath.endsWith('.yml');
|
|
115
199
|
|
|
116
200
|
if (!isBruFile && !isYmlFile) {
|
|
117
201
|
return reportedLine;
|
|
@@ -157,6 +241,13 @@ const adjustLineNumber = (filePath, reportedLine, isQuickJS, scriptType = null,
|
|
|
157
241
|
return scriptRelativeLine;
|
|
158
242
|
};
|
|
159
243
|
|
|
244
|
+
/** Look up the script block start line for a .bru or .yml file */
|
|
245
|
+
const findBlockStart = (filePath, scriptType, cache) => {
|
|
246
|
+
if (filePath.endsWith('.bru')) return findScriptBlockStartLine(filePath, scriptType, cache);
|
|
247
|
+
if (filePath.endsWith('.yml')) return findYmlScriptBlockStartLine(filePath, scriptType, cache);
|
|
248
|
+
return null;
|
|
249
|
+
};
|
|
250
|
+
|
|
160
251
|
/**
|
|
161
252
|
* Resolve an error in a collection/folder script segment to its source file and line.
|
|
162
253
|
* Uses the segments array in metadata to find which segment the error falls in,
|
|
@@ -171,19 +262,34 @@ const resolveSegmentError = (parsed, metadata, scriptType, cache) => {
|
|
|
171
262
|
|
|
172
263
|
for (const segment of metadata.segments) {
|
|
173
264
|
if (scriptRelativeLine >= segment.startLine && scriptRelativeLine <= segment.endLine) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
265
|
+
if (!isAllowedSourceFile(segment.filePath)) return null;
|
|
266
|
+
|
|
267
|
+
const blockStartLine = findBlockStart(segment.filePath, scriptType, cache);
|
|
268
|
+
if (!blockStartLine) {
|
|
269
|
+
// No script block on disk — only possible when user added a new script as a draft.
|
|
270
|
+
// If we have in-memory content, return it so the caller can show the code snippet.
|
|
271
|
+
if (segment.scriptContent) {
|
|
272
|
+
return {
|
|
273
|
+
line: null,
|
|
274
|
+
filePath: segment.filePath,
|
|
275
|
+
displayPath: segment.displayPath,
|
|
276
|
+
scriptContent: segment.scriptContent,
|
|
277
|
+
// segment.startLine points to the IIFE wrapper line (`await (async () => {`),
|
|
278
|
+
// so subtracting it yields a 1-based index into the user's script content.
|
|
279
|
+
lineInScript: scriptRelativeLine - segment.startLine
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
182
284
|
|
|
183
285
|
return {
|
|
184
286
|
line: blockStartLine + (scriptRelativeLine - segment.startLine) - 1,
|
|
185
287
|
filePath: segment.filePath,
|
|
186
|
-
displayPath: segment.displayPath
|
|
288
|
+
displayPath: segment.displayPath,
|
|
289
|
+
scriptContent: segment.scriptContent || null,
|
|
290
|
+
// segment.startLine points to the IIFE wrapper line (`await (async () => {`),
|
|
291
|
+
// so subtracting it yields a 1-based index into the user's script content.
|
|
292
|
+
lineInScript: scriptRelativeLine - segment.startLine
|
|
187
293
|
};
|
|
188
294
|
}
|
|
189
295
|
}
|
|
@@ -248,12 +354,10 @@ const parseErrorLocation = (error) => {
|
|
|
248
354
|
return parsed;
|
|
249
355
|
};
|
|
250
356
|
|
|
251
|
-
/**
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
if (!content) return null;
|
|
357
|
+
/** Build a context-lines object from an array of source lines around an error */
|
|
358
|
+
const buildContextLines = (lines, errorLine, contextLines) => {
|
|
359
|
+
if (errorLine < 1 || errorLine > lines.length) return null;
|
|
255
360
|
|
|
256
|
-
const lines = content.split('\n');
|
|
257
361
|
const startLine = Math.max(1, errorLine - contextLines);
|
|
258
362
|
const endLine = Math.min(lines.length, errorLine + contextLines);
|
|
259
363
|
|
|
@@ -269,6 +373,19 @@ const getSourceContext = (filePath, errorLine, contextLines = DEFAULT_CONTEXT_LI
|
|
|
269
373
|
return { lines: contextLinesArray, startLine, errorLine };
|
|
270
374
|
};
|
|
271
375
|
|
|
376
|
+
/** Read source file and extract context lines around the error location */
|
|
377
|
+
const getSourceContext = (filePath, errorLine, contextLines = DEFAULT_CONTEXT_LINES, cache = null) => {
|
|
378
|
+
const content = readFile(filePath, cache);
|
|
379
|
+
if (!content) return null;
|
|
380
|
+
return buildContextLines(content.split('\n'), errorLine, contextLines);
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
/** Extract context lines from in-memory script content (e.g. unsaved draft scripts) */
|
|
384
|
+
const getSourceContextFromContent = (content, errorLine, contextLines = DEFAULT_CONTEXT_LINES) => {
|
|
385
|
+
if (!content) return null;
|
|
386
|
+
return buildContextLines(content.split('\n'), errorLine, contextLines);
|
|
387
|
+
};
|
|
388
|
+
|
|
272
389
|
/** Build adjusted stack trace string from structured CallSite data */
|
|
273
390
|
const buildStackFromCallSites = (callSites, scriptType = null, cache = null, scriptMetadata = null) => {
|
|
274
391
|
return callSites.map((site) => {
|
|
@@ -280,7 +397,7 @@ const buildStackFromCallSites = (callSites, scriptType = null, cache = null, scr
|
|
|
280
397
|
if (adjusted === null && scriptMetadata?.segments) {
|
|
281
398
|
const parsed = { line: site.line, isQuickJS: false };
|
|
282
399
|
const resolved = resolveSegmentError(parsed, scriptMetadata, scriptType, cache);
|
|
283
|
-
if (resolved) {
|
|
400
|
+
if (resolved && resolved.line !== null) {
|
|
284
401
|
fileToUse = resolved.filePath;
|
|
285
402
|
lineToUse = resolved.line;
|
|
286
403
|
}
|
|
@@ -307,7 +424,7 @@ const adjustStackTrace = (stack, scriptType = null, cache = null, scriptMetadata
|
|
|
307
424
|
if (adjusted === null && scriptMetadata?.segments) {
|
|
308
425
|
const parsed = { line: match.line, isQuickJS };
|
|
309
426
|
const resolved = resolveSegmentError(parsed, scriptMetadata, scriptType, cache);
|
|
310
|
-
if (resolved) {
|
|
427
|
+
if (resolved && resolved.line !== null) {
|
|
311
428
|
const suffix = match.isQuickJS ? ')' : '';
|
|
312
429
|
return match.column !== null
|
|
313
430
|
? line.replace(`${match.filePath}:${match.line}:${match.column}${suffix}`, `${resolved.filePath}:${resolved.line}:${match.column}${suffix}`)
|
|
@@ -409,6 +526,210 @@ const formatErrorWithContext = (error, relativeFilePath = null, scriptType = nul
|
|
|
409
526
|
return lines.join('\n');
|
|
410
527
|
};
|
|
411
528
|
|
|
529
|
+
/**
|
|
530
|
+
* Build a structured error context object for the desktop UI's ScriptError component.
|
|
531
|
+
*
|
|
532
|
+
* formatErrorWithContext (V1) returns a pre-formatted string for CLI output.
|
|
533
|
+
* This function returns a structured object so the desktop UI can render it
|
|
534
|
+
* with its own layout (CodeSnippet component, collapsible stack, etc.).
|
|
535
|
+
*
|
|
536
|
+
* Key difference: line numbers in the returned object are block-relative
|
|
537
|
+
* (i.e. relative to the script block, starting at 1) rather than absolute
|
|
538
|
+
* file line numbers, because users edit scripts in a CodeMirror editor that
|
|
539
|
+
* starts numbering at line 1.
|
|
540
|
+
*
|
|
541
|
+
* @example
|
|
542
|
+
* Given a .bru file at /home/user/my-collection/requests/get-user.bru:
|
|
543
|
+
*
|
|
544
|
+
* meta { ← file line 1
|
|
545
|
+
* name: get-user ← file line 2
|
|
546
|
+
* } ← file line 3
|
|
547
|
+
* ← file line 4
|
|
548
|
+
* script:post-response { ← file line 5
|
|
549
|
+
* const data = res.body; ← file line 6 (script line 1)
|
|
550
|
+
* data.missing.prop; ← file line 7 (script line 2) ← error
|
|
551
|
+
* console.log(data); ← file line 8 (script line 3)
|
|
552
|
+
* } ← file line 9
|
|
553
|
+
*
|
|
554
|
+
* formatErrorWithContextV2(error, 'post-response', null, '/home/user/my-collection')
|
|
555
|
+
* → {
|
|
556
|
+
* errorType: 'TypeError',
|
|
557
|
+
* filePath: 'requests/get-user.bru', ← relative to collectionPath
|
|
558
|
+
* errorLine: 2, ← block-relative, not file line 7
|
|
559
|
+
* lines: [
|
|
560
|
+
* { lineNumber: 1, content: ' const data = res.body;', isError: false },
|
|
561
|
+
* { lineNumber: 2, content: ' data.missing.prop;', isError: true },
|
|
562
|
+
* { lineNumber: 3, content: ' console.log(data);', isError: false }
|
|
563
|
+
* ],
|
|
564
|
+
* stack: ' at …/requests/get-user.bru:7:3'
|
|
565
|
+
* }
|
|
566
|
+
*
|
|
567
|
+
* V1 (formatErrorWithContext) returns a flat string for the same error:
|
|
568
|
+
* File: requests/get-user.bru
|
|
569
|
+
*
|
|
570
|
+
* 5 | const data = res.body;
|
|
571
|
+
* > 6 | data.missing.prop;
|
|
572
|
+
* 7 | console.log(data);
|
|
573
|
+
*
|
|
574
|
+
* TypeError: Cannot read properties of undefined
|
|
575
|
+
* at …/requests/get-user.bru:7:3
|
|
576
|
+
*
|
|
577
|
+
* @param {Error} error - The error to build context for
|
|
578
|
+
* @param {string} scriptType - 'pre-request' | 'post-response' | 'test'
|
|
579
|
+
* @param {object} scriptMetadata - Optional metadata for line mapping in combined scripts
|
|
580
|
+
* @param {string} collectionPath - Absolute path to the collection root (used to compute relative display paths)
|
|
581
|
+
* @returns {object|null} Structured error context or null
|
|
582
|
+
*/
|
|
583
|
+
/**
|
|
584
|
+
* Resolve error context, preferring in-memory draft content over disk.
|
|
585
|
+
*
|
|
586
|
+
* Three resolution paths (tried in order):
|
|
587
|
+
* 1. Request-level error with in-memory draft content
|
|
588
|
+
* 2. Segment (collection/folder) error with in-memory draft content
|
|
589
|
+
* 3. Disk-based file read (original behavior)
|
|
590
|
+
*
|
|
591
|
+
* @returns {{ context, fromMemory, draftOnlyBlock }|null}
|
|
592
|
+
*/
|
|
593
|
+
const resolveErrorContext = ({ adjustedLine, scriptRelativeLine, metadata, segmentResult, filePath, sourceFile, sourceLine, scriptType, cache }) => {
|
|
594
|
+
// Request-level error with in-memory draft content
|
|
595
|
+
if (adjustedLine !== null && metadata?.requestScriptContent) {
|
|
596
|
+
// Check whether the script block exists on disk. When the user added a brand-new
|
|
597
|
+
// script that hasn't been saved yet, findBlockStart returns null and adjustLineNumber
|
|
598
|
+
// returned scriptRelativeLine (not a real .bru file line), so stack frame adjustment
|
|
599
|
+
// would produce misleading results — flag it as draft-only.
|
|
600
|
+
const blockStartLine = findBlockStart(filePath, scriptType, cache);
|
|
601
|
+
const draftOnlyBlock = !blockStartLine && isAllowedSourceFile(filePath);
|
|
602
|
+
// requestStartLine points to the IIFE wrapper line (`await (async () => {`),
|
|
603
|
+
// so subtracting it yields a 1-based index into the user's script content.
|
|
604
|
+
const lineInScript = scriptRelativeLine - metadata.requestStartLine;
|
|
605
|
+
const context = getSourceContextFromContent(metadata.requestScriptContent, lineInScript, 3);
|
|
606
|
+
if (context) return { context, fromMemory: true, draftOnlyBlock };
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Segment (collection/folder) error with in-memory draft content
|
|
610
|
+
if (adjustedLine === null && segmentResult?.scriptContent) {
|
|
611
|
+
const context = getSourceContextFromContent(segmentResult.scriptContent, segmentResult.lineInScript, 3);
|
|
612
|
+
// segmentResult.line is null when the block doesn't exist on disk
|
|
613
|
+
if (context) return { context, fromMemory: true, draftOnlyBlock: segmentResult.line === null };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Fall back to reading from disk
|
|
617
|
+
if (sourceLine !== null) {
|
|
618
|
+
const context = getSourceContext(sourceFile, sourceLine, 3, cache);
|
|
619
|
+
if (context) return { context, fromMemory: false, draftOnlyBlock: false };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return null;
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
const formatErrorWithContextV2 = (error, scriptType, scriptMetadata, collectionPath) => {
|
|
626
|
+
if (!error) return null;
|
|
627
|
+
|
|
628
|
+
try {
|
|
629
|
+
const cache = new Map();
|
|
630
|
+
const metadata = (error.scriptMetadata && Object.keys(error.scriptMetadata).length > 0)
|
|
631
|
+
? error.scriptMetadata
|
|
632
|
+
: scriptMetadata;
|
|
633
|
+
const parsed = parseErrorLocation(error);
|
|
634
|
+
if (!parsed) return null;
|
|
635
|
+
|
|
636
|
+
const { filePath } = parsed;
|
|
637
|
+
const wrapperOffset = parsed.isQuickJS ? QUICKJS_SCRIPT_WRAPPER_OFFSET : NODEVM_SCRIPT_WRAPPER_OFFSET;
|
|
638
|
+
const scriptRelativeLine = parsed.line - wrapperOffset;
|
|
639
|
+
const adjustedLine = adjustLineNumber(filePath, parsed.line, parsed.isQuickJS, scriptType, cache, metadata);
|
|
640
|
+
|
|
641
|
+
let sourceFile = filePath;
|
|
642
|
+
let sourceLine = adjustedLine;
|
|
643
|
+
|
|
644
|
+
// Handle collection/folder script segments
|
|
645
|
+
let segmentResult = null;
|
|
646
|
+
if (adjustedLine === null) {
|
|
647
|
+
segmentResult = resolveSegmentError(parsed, metadata, scriptType, cache);
|
|
648
|
+
if (!segmentResult) return null;
|
|
649
|
+
sourceFile = segmentResult.filePath;
|
|
650
|
+
sourceLine = segmentResult.line;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Resolve context: prefer in-memory draft content, fall back to disk
|
|
654
|
+
const resolved = resolveErrorContext({
|
|
655
|
+
adjustedLine, scriptRelativeLine, metadata, segmentResult,
|
|
656
|
+
filePath, sourceFile, sourceLine, scriptType, cache
|
|
657
|
+
});
|
|
658
|
+
if (!resolved || resolved.context.lines.length === 0) return null;
|
|
659
|
+
|
|
660
|
+
const { context, fromMemory, draftOnlyBlock } = resolved;
|
|
661
|
+
|
|
662
|
+
const resolvedDisplayPath = posixifyPath(
|
|
663
|
+
collectionPath ? path.relative(collectionPath, sourceFile) : sourceFile
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
const errorType = getErrorTypeName(error);
|
|
667
|
+
let stack = null;
|
|
668
|
+
if (error.stack) {
|
|
669
|
+
// When the script block only exists as a draft (not on disk), adjustLineNumber
|
|
670
|
+
// cannot map to real .bru file lines — skip adjustment to preserve original frames.
|
|
671
|
+
const rawStack = draftOnlyBlock
|
|
672
|
+
? error.stack
|
|
673
|
+
: adjustStackTrace(error.stack, scriptType, cache, metadata, parsed.isQuickJS);
|
|
674
|
+
const stackLines = rawStack.split('\n').slice(1).filter((l) => l.trim().startsWith('at'));
|
|
675
|
+
stack = stackLines.length ? stackLines.map((l) => ` ${l.trim()}`).join('\n') : null;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// When context came from in-memory content, lines are already block-relative
|
|
679
|
+
if (fromMemory) {
|
|
680
|
+
return {
|
|
681
|
+
errorType,
|
|
682
|
+
filePath: resolvedDisplayPath,
|
|
683
|
+
errorLine: context.errorLine,
|
|
684
|
+
lines: context.lines,
|
|
685
|
+
stack
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Compute block-relative line numbers for the desktop UI.
|
|
690
|
+
// Users edit scripts in a CodeMirror editor starting at line 1,
|
|
691
|
+
// so show lines relative to the script block, not absolute .bru file lines.
|
|
692
|
+
const blockStartLine = findBlockStart(sourceFile, scriptType, cache);
|
|
693
|
+
|
|
694
|
+
const isBru = sourceFile.endsWith('.bru');
|
|
695
|
+
const isYml = sourceFile.endsWith('.yml');
|
|
696
|
+
const blockEndLine = isBru
|
|
697
|
+
? findScriptBlockEndLine(sourceFile, scriptType, cache)
|
|
698
|
+
: isYml
|
|
699
|
+
? findYmlScriptBlockEndLine(sourceFile, scriptType, cache)
|
|
700
|
+
: null;
|
|
701
|
+
|
|
702
|
+
// If this is a .bru/.yml file but the script block is missing or empty, there's nothing to show
|
|
703
|
+
if ((isBru || isYml) && !blockEndLine) return null;
|
|
704
|
+
|
|
705
|
+
const blockOffset = blockStartLine ? blockStartLine - 1 : 0;
|
|
706
|
+
|
|
707
|
+
const filteredLines = context.lines
|
|
708
|
+
.filter((l) => {
|
|
709
|
+
const rel = l.lineNumber - blockOffset;
|
|
710
|
+
return rel >= 1 && (!blockEndLine || l.lineNumber <= blockEndLine);
|
|
711
|
+
})
|
|
712
|
+
.map((l) => ({
|
|
713
|
+
lineNumber: l.lineNumber - blockOffset,
|
|
714
|
+
content: l.content,
|
|
715
|
+
isError: l.isError
|
|
716
|
+
}));
|
|
717
|
+
|
|
718
|
+
if (filteredLines.length === 0) return null;
|
|
719
|
+
|
|
720
|
+
return {
|
|
721
|
+
errorType,
|
|
722
|
+
filePath: resolvedDisplayPath,
|
|
723
|
+
errorLine: sourceLine - blockOffset,
|
|
724
|
+
lines: filteredLines,
|
|
725
|
+
stack
|
|
726
|
+
};
|
|
727
|
+
} catch (e) {
|
|
728
|
+
console.warn('formatErrorWithContextV2 failed:', e);
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
|
|
412
733
|
module.exports = {
|
|
413
734
|
SCRIPT_TYPES,
|
|
414
735
|
DEFAULT_CONTEXT_LINES,
|
|
@@ -416,11 +737,15 @@ module.exports = {
|
|
|
416
737
|
parseErrorLocation,
|
|
417
738
|
buildStackFromCallSites,
|
|
418
739
|
getSourceContext,
|
|
740
|
+
getSourceContextFromContent,
|
|
419
741
|
formatErrorWithContext,
|
|
742
|
+
formatErrorWithContextV2,
|
|
420
743
|
adjustLineNumber,
|
|
421
744
|
resolveSegmentError,
|
|
422
745
|
findScriptBlockStartLine,
|
|
746
|
+
findScriptBlockEndLine,
|
|
423
747
|
findYmlScriptBlockStartLine,
|
|
748
|
+
findYmlScriptBlockEndLine,
|
|
424
749
|
adjustStackTrace,
|
|
425
750
|
getErrorTypeName
|
|
426
751
|
};
|