Skip to content

Commit 3997e54

Browse files
ankitranjan7claude
andcommitted
fix(browser): report the caller's line and column for browser-run compile errors
A program that fails to compile was surfaced as "QuickJS promise rejected: invalid number literal" with no position, because new AsyncFunction(source) runs inside the promise and its SyntaxError carried lineNumber/columnNumber that were then discarded. The prefix pointed at async plumbing rather than the program, so readers hunted heredocs and await instead of the actual defect: an unescaped quote in a string literal holding a URL. Compile the program before racing it, and turn a compile failure into a BROWSER_RUN_SYNTAX_ERROR carrying the caller's own line and column (the AsyncFunction wrapper adds two lines, so subtract them), a windowed excerpt with a caret, and a hint naming the quoting cause. Errors that already carry a BROWSER_RUN_ code keep their message instead of gaining a host stage prefix. runner.test.ts imported MemorySnapshotBaselineStore twice, so the file failed to load and its 74 tests never ran; the duplicate is removed here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent ad17a0c commit 3997e54

3 files changed

Lines changed: 53 additions & 2 deletions

File tree

src/browser/run/quickjs-host.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,9 @@ export class QuickJSHost {
601601
#toError(prefix: string, errorHandle: QuickJSHandle): Error {
602602
const dumped = this.#context.dump(errorHandle);
603603
const normalized = this.#normalizeHostError(dumped);
604+
// Errors the sandbox raised deliberately already say what went wrong; prefixing
605+
// them with a host-internal stage name only misdirects whoever reads the failure.
606+
if (normalized.code?.startsWith('BROWSER_RUN_')) return normalized;
604607
normalized.message = `${prefix}: ${normalized.message}`;
605608
return normalized;
606609
}

src/browser/run/runner.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {
2020
} from 'playwright-core';
2121
import { LocalBrowserRunArtifactSink } from './artifacts.js';
2222
import { MemorySnapshotBaselineStore } from '../snapshot/index.js';
23-
import { MemorySnapshotBaselineStore } from '../snapshot/index.js';
2423
import { unsupportedApiMessage } from './playwright-transport.js';
2524
import { QuickJSHost } from './quickjs-host.js';
2625
import { runBrowserProgram } from './runner.js';
@@ -56,6 +55,15 @@ function run(source: string, options = {}, input = {}) {
5655
}, source, options);
5756
}
5857

58+
async function runError(source: string): Promise<Error & { code?: string; hint?: string }> {
59+
try {
60+
await run(source);
61+
} catch (cause) {
62+
return cause as Error & { code?: string; hint?: string };
63+
}
64+
throw new Error('expected the program to fail');
65+
}
66+
5967
describeWithChromium('runBrowserProgram', () => {
6068
beforeAll(async () => {
6169
browser = await chromium.launch({ headless: true });
@@ -232,6 +240,17 @@ afterAll(async () => {
232240

233241
expect(output.result).toBe('undefined');
234242
});
243+
it('reports the caller line, column, and source for a compile error', async () => {
244+
// An unescaped quote ends the string early; the parser trips on what follows.
245+
// Line/column must be the caller's own, not the AsyncFunction wrapper's.
246+
const error = await runError("const a = 1;\nconst s = 'x'y';\nreturn a;");
247+
248+
expect(error.code).toBe('BROWSER_RUN_SYNTAX_ERROR');
249+
expect(error.message).toContain('at line 2, column 14');
250+
expect(error.message).toContain("const s = 'x'y';");
251+
expect(error.message).not.toContain('QuickJS promise rejected');
252+
expect(error.hint).toContain('unescaped quote');
253+
});
235254
it('publishes the browser-run package subpath', () => {
236255
const packageJson = JSON.parse(
237256
fs.readFileSync(new URL('../../../package.json', import.meta.url), 'utf8'),

src/browser/run/runner.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,14 +489,43 @@ export async function runBrowserProgram(
489489
connection.close(message);
490490
rejectRun?.(new Error(message));
491491
};
492+
globalThis.__webcmdCompileError = (source, cause) => {
493+
// new AsyncFunction(body) prepends two lines of wrapper, so QuickJS reports
494+
// a line two ahead of the one the caller actually wrote.
495+
const lines = source.split('\\n');
496+
const line = (cause?.lineNumber ?? 0) - 2;
497+
const column = cause?.columnNumber ?? 0;
498+
const text = line >= 1 && line <= lines.length ? lines[line - 1] : undefined;
499+
let excerpt = '';
500+
if (text !== undefined) {
501+
const start = Math.max(0, column - 60);
502+
const caret = ' '.repeat(Math.max(0, column - 1 - start)) + '^';
503+
excerpt = '\\n ' + text.slice(start, column + 20) + '\\n ' + caret;
504+
}
505+
const where = line >= 1 ? ' at line ' + line + ', column ' + column : '';
506+
const message = cause?.message ?? 'Program failed to compile.';
507+
const error = new SyntaxError(message + where + excerpt);
508+
error.code = 'BROWSER_RUN_SYNTAX_ERROR';
509+
error.hint = 'Your program failed to compile; the browser was never touched. '
510+
+ 'A common cause is an unescaped quote when embedding a URL or HTML in a '
511+
+ 'string literal — use double quotes or a template literal for values that '
512+
+ 'contain apostrophes.';
513+
return error;
514+
};
492515
globalThis.__webcmdRun = async source => {
493516
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
517+
let program;
518+
try {
519+
program = new AsyncFunction(source);
520+
} catch (cause) {
521+
throw __webcmdCompileError(source, cause);
522+
}
494523
let value;
495524
try {
496525
const cancellation = new Promise((_resolve, reject) => {
497526
rejectRun = reject;
498527
});
499-
value = await Promise.race([new AsyncFunction(source)(), cancellation]);
528+
value = await Promise.race([program(), cancellation]);
500529
} finally {
501530
rejectRun = undefined;
502531
}

0 commit comments

Comments
 (0)