Skip to content

Commit 6005a90

Browse files
committed
Show source line on execution errors
1 parent c1a08e3 commit 6005a90

File tree

9 files changed

+125
-5
lines changed

9 files changed

+125
-5
lines changed

lib/runner/CircuitRunner.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { RootCircuit } from "@tscircuit/core"
1010
import * as React from "react"
1111
import { importEvalPath } from "webworker/import-eval-path"
1212
import { setupDefaultEntrypointIfNeeded } from "./setupDefaultEntrypointIfNeeded"
13+
import { addSourceLineToError } from "../utils/addSourceLineToError"
1314

1415
export class CircuitRunner implements CircuitRunnerApi {
1516
_executionContext: ReturnType<typeof createExecutionContext> | null = null
@@ -62,7 +63,16 @@ export class CircuitRunner implements CircuitRunnerApi {
6263
? opts.entrypoint
6364
: `./${opts.entrypoint}`
6465

65-
await importEvalPath(entrypoint!, this._executionContext)
66+
try {
67+
await importEvalPath(entrypoint!, this._executionContext)
68+
} catch (error: any) {
69+
await addSourceLineToError(
70+
error,
71+
this._executionContext.fsMap,
72+
this._executionContext.sourceMaps,
73+
)
74+
throw error
75+
}
6676
}
6777

6878
async execute(code: string, opts: { name?: string } = {}) {
@@ -84,7 +94,16 @@ export class CircuitRunner implements CircuitRunnerApi {
8494
this._executionContext.fsMap["entrypoint.tsx"] = code
8595
;(globalThis as any).__tscircuit_circuit = this._executionContext.circuit
8696

87-
await importEvalPath("./entrypoint.tsx", this._executionContext)
97+
try {
98+
await importEvalPath("./entrypoint.tsx", this._executionContext)
99+
} catch (error: any) {
100+
await addSourceLineToError(
101+
error,
102+
this._executionContext.fsMap,
103+
this._executionContext.sourceMaps,
104+
)
105+
throw error
106+
}
88107
}
89108

90109
on(event: string, callback: (...args: any[]) => void) {

lib/utils/addSourceLineToError.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { SourceMapConsumer, RawSourceMap } from "source-map"
2+
3+
export async function addSourceLineToError(
4+
error: Error,
5+
fsMap: Record<string, string>,
6+
sourceMaps?: Record<string, RawSourceMap>,
7+
) {
8+
if (!error || typeof error !== "object") return
9+
const stack = (error as any).stack as string | undefined
10+
if (!stack) return
11+
12+
const escapeRegExp = (str: string) =>
13+
str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
14+
15+
for (const path of Object.keys(fsMap)) {
16+
const regex = new RegExp(escapeRegExp(path) + ":(\\d+):(\\d+)")
17+
const match = stack.match(regex)
18+
if (match) {
19+
const line = parseInt(match[1], 10)
20+
let originalLine = line
21+
if (sourceMaps && sourceMaps[path]) {
22+
const consumer = await new SourceMapConsumer(sourceMaps[path])
23+
const pos = consumer.originalPositionFor({ line: line - 6, column: 0 })
24+
consumer.destroy()
25+
if (pos && pos.line) {
26+
originalLine = pos.line
27+
}
28+
}
29+
const lines = fsMap[path].split(/\r?\n/)
30+
const sourceLine = lines[originalLine - 1]
31+
if (sourceLine !== undefined) {
32+
error.message += `\n\n> ${path}:${originalLine}\n> ${sourceLine.trim()}`
33+
} else {
34+
error.message += `\n\n> ${path}:${originalLine}`
35+
}
36+
break
37+
}
38+
}
39+
}

lib/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from "./get-imports-from-code"
2+
export * from "./addSourceLineToError"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { expect, test } from "bun:test"
2+
import { createCircuitWebWorker } from "lib/index"
3+
4+
// Ensure that when runtime errors occur, the offending line is included
5+
6+
test("execution error includes source line", async () => {
7+
const worker = await createCircuitWebWorker({
8+
webWorkerUrl: new URL("../webworker/entrypoint.ts", import.meta.url),
9+
})
10+
11+
await expect(
12+
worker.executeWithFsMap({
13+
entrypoint: "index.tsx",
14+
fsMap: {
15+
"index.tsx": `
16+
circuit.add(<board width="10mm" height="10mm" />)
17+
throw new Error("boom")
18+
`,
19+
},
20+
}),
21+
).rejects.toThrow(/boom\n[\s\S]*index.tsx:3/)
22+
23+
await worker.kill()
24+
})

webworker/entrypoint.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { importEvalPath } from "./import-eval-path"
1414
import { normalizeFsMap } from "../lib/runner/normalizeFsMap"
1515
import type { RootCircuit } from "@tscircuit/core"
1616
import { setupDefaultEntrypointIfNeeded } from "lib/runner/setupDefaultEntrypointIfNeeded"
17+
import { addSourceLineToError } from "lib/utils/addSourceLineToError"
1718

1819
globalThis.React = React
1920

@@ -76,7 +77,16 @@ const webWorkerApi = {
7677
entrypoint = `./${entrypoint}`
7778
}
7879

79-
await importEvalPath(entrypoint, executionContext)
80+
try {
81+
await importEvalPath(entrypoint, executionContext)
82+
} catch (error: any) {
83+
await addSourceLineToError(
84+
error,
85+
executionContext.fsMap,
86+
executionContext.sourceMaps,
87+
)
88+
throw error
89+
}
8090
},
8191

8292
async execute(code: string, opts: { name?: string } = {}) {
@@ -91,7 +101,16 @@ const webWorkerApi = {
91101
executionContext.fsMap["entrypoint.tsx"] = code
92102
;(globalThis as any).__tscircuit_circuit = executionContext.circuit
93103

94-
await importEvalPath("./entrypoint.tsx", executionContext)
104+
try {
105+
await importEvalPath("./entrypoint.tsx", executionContext)
106+
} catch (error: any) {
107+
await addSourceLineToError(
108+
error,
109+
executionContext.fsMap,
110+
executionContext.sourceMaps,
111+
)
112+
throw error
113+
}
95114
},
96115

97116
on: (event: string, callback: (...args: any[]) => void) => {

webworker/eval-compiled-js.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export function evalCompiledJs(
44
compiledCode: string,
55
preSuppliedImports: Record<string, any>,
66
cwd?: string,
7+
sourceUrl?: string,
78
) {
89
;(globalThis as any).__tscircuit_require = (name: string) => {
910
const resolvedFilePath = resolveFilePath(name, preSuppliedImports, cwd)
@@ -56,6 +57,7 @@ export function evalCompiledJs(
5657
var module = { exports };
5758
var circuit = globalThis.__tscircuit_circuit;
5859
${compiledCode};
60+
//# sourceURL=${sourceUrl ?? "compiled.ts"}
5961
return module;`.trim()
6062
return Function(functionBody).call(globalThis)
6163
}

webworker/execution-context.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface ExecutionContext extends WebWorkerConfiguration {
1111
fsMap: Record<string, string>
1212
entrypoint: string
1313
preSuppliedImports: Record<string, any>
14+
sourceMaps: Record<string, any>
1415
circuit: RootCircuit
1516
}
1617

@@ -45,6 +46,7 @@ export function createExecutionContext(
4546
// ignore type imports in getImportsFromCode
4647
"@tscircuit/props": {},
4748
},
49+
sourceMaps: {},
4850
circuit,
4951
...webWorkerConfiguration,
5052
}

webworker/import-local-file.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as Babel from "@babel/standalone"
22
import { resolveFilePathOrThrow } from "lib/runner/resolveFilePath"
33
import { dirname } from "lib/utils/dirname"
44
import { getImportsFromCode } from "lib/utils/get-imports-from-code"
5+
import { addSourceLineToError } from "lib/utils/addSourceLineToError"
56
import { evalCompiledJs } from "./eval-compiled-js"
67
import type { ExecutionContext } from "./execution-context"
78
import { importEvalPath } from "./import-eval-path"
@@ -38,8 +39,12 @@ export const importLocalFile = async (
3839
const result = Babel.transform(fileContent, {
3940
presets: ["react", "typescript"],
4041
plugins: ["transform-modules-commonjs"],
41-
filename: "virtual.tsx",
42+
filename: fsPath,
43+
sourceMaps: true,
4244
})
45+
if (result.map) {
46+
ctx.sourceMaps[fsPath] = result.map
47+
}
4348

4449
if (!result || !result.code) {
4550
throw new Error("Failed to transform code")
@@ -50,9 +55,11 @@ export const importLocalFile = async (
5055
result.code,
5156
preSuppliedImports,
5257
dirname(fsPath),
58+
fsPath,
5359
)
5460
preSuppliedImports[fsPath] = importRunResult.exports
5561
} catch (error: any) {
62+
await addSourceLineToError(error, ctx.fsMap, ctx.sourceMaps)
5663
throw new Error(
5764
`Eval compiled js error for "${importName}": ${error.message}`,
5865
)
@@ -63,7 +70,11 @@ export const importLocalFile = async (
6370
presets: ["env"],
6471
plugins: ["transform-modules-commonjs"],
6572
filename: fsPath,
73+
sourceMaps: true,
6674
})
75+
if (result.map) {
76+
ctx.sourceMaps[fsPath] = result.map
77+
}
6778

6879
if (!result || !result.code) {
6980
throw new Error("Failed to transform JS code")
@@ -73,6 +84,7 @@ export const importLocalFile = async (
7384
result.code,
7485
preSuppliedImports,
7586
dirname(fsPath),
87+
fsPath,
7688
).exports
7789
} else {
7890
throw new Error(

webworker/import-snippet.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export async function importSnippet(
2525
preSuppliedImports[importName] = evalCompiledJs(
2626
cjs!,
2727
preSuppliedImports,
28+
undefined,
29+
importName,
2830
).exports
2931
} catch (e) {
3032
console.error("Error importing snippet", e)

0 commit comments

Comments
 (0)