Skip to content

Improve error messages with source lines #655

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions lib/runner/CircuitRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { RootCircuit } from "@tscircuit/core"
import * as React from "react"
import { importEvalPath } from "webworker/import-eval-path"
import { setupDefaultEntrypointIfNeeded } from "./setupDefaultEntrypointIfNeeded"
import { addSourceLineToError } from "../utils/addSourceLineToError"

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

await importEvalPath(entrypoint!, this._executionContext)
try {
await importEvalPath(entrypoint!, this._executionContext)
} catch (error: any) {
await addSourceLineToError(
error,
this._executionContext.fsMap,
this._executionContext.sourceMaps,
)
throw error
}
}

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

await importEvalPath("./entrypoint.tsx", this._executionContext)
try {
await importEvalPath("./entrypoint.tsx", this._executionContext)
} catch (error: any) {
await addSourceLineToError(
error,
this._executionContext.fsMap,
this._executionContext.sourceMaps,
)
throw error
}
}

on(event: string, callback: (...args: any[]) => void) {
Expand Down
40 changes: 40 additions & 0 deletions lib/utils/addSourceLineToError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { SourceMapConsumer } from "source-map"
import type { RawSourceMap } from "source-map"

export async function addSourceLineToError(
error: Error,
fsMap: Record<string, string>,
sourceMaps?: Record<string, RawSourceMap>,
) {
if (!error || typeof error !== "object") return
const stack = (error as any).stack as string | undefined
if (!stack) return

const escapeRegExp = (str: string) =>
str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")

for (const path of Object.keys(fsMap)) {
const regex = new RegExp(escapeRegExp(path) + ":(\\d+):(\\d+)")
const match = stack.match(regex)
if (match) {
const line = parseInt(match[1], 10)
let originalLine = line
if (sourceMaps && sourceMaps[path]) {
const consumer = await new SourceMapConsumer(sourceMaps[path])
const pos = consumer.originalPositionFor({ line: line - 6, column: 0 })
consumer.destroy()
if (pos && pos.line) {
originalLine = pos.line
}
}
const lines = fsMap[path].split(/\r?\n/)
const sourceLine = lines[originalLine - 1]
if (sourceLine !== undefined) {
error.message += `\n\n> ${path}:${originalLine}\n> ${sourceLine.trim()}`
} else {
error.message += `\n\n> ${path}:${originalLine}`
}
break
}
}
}
1 change: 1 addition & 0 deletions lib/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./get-imports-from-code"
export * from "./addSourceLineToError"
24 changes: 24 additions & 0 deletions tests/execution-error-line.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { expect, test } from "bun:test"
import { createCircuitWebWorker } from "lib/index"

// Ensure that when runtime errors occur, the offending line is included

test("execution error includes source line", async () => {
const worker = await createCircuitWebWorker({
webWorkerUrl: new URL("../webworker/entrypoint.ts", import.meta.url),
})

await expect(
worker.executeWithFsMap({
entrypoint: "index.tsx",
fsMap: {
"index.tsx": `
circuit.add(<board width="10mm" height="10mm" />)
throw new Error("boom")
`,
},
}),
).rejects.toThrow(/boom\n[\s\S]*index.tsx:3/)

await worker.kill()
})
23 changes: 21 additions & 2 deletions webworker/entrypoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { importEvalPath } from "./import-eval-path"
import { normalizeFsMap } from "../lib/runner/normalizeFsMap"
import type { RootCircuit } from "@tscircuit/core"
import { setupDefaultEntrypointIfNeeded } from "lib/runner/setupDefaultEntrypointIfNeeded"
import { addSourceLineToError } from "lib/utils/addSourceLineToError"

globalThis.React = React

Expand Down Expand Up @@ -76,7 +77,16 @@ const webWorkerApi = {
entrypoint = `./${entrypoint}`
}

await importEvalPath(entrypoint, executionContext)
try {
await importEvalPath(entrypoint, executionContext)
} catch (error: any) {
await addSourceLineToError(
error,
executionContext.fsMap,
executionContext.sourceMaps,
)
throw error
}
},

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

await importEvalPath("./entrypoint.tsx", executionContext)
try {
await importEvalPath("./entrypoint.tsx", executionContext)
} catch (error: any) {
await addSourceLineToError(
error,
executionContext.fsMap,
executionContext.sourceMaps,
)
throw error
}
},

on: (event: string, callback: (...args: any[]) => void) => {
Expand Down
2 changes: 2 additions & 0 deletions webworker/eval-compiled-js.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export function evalCompiledJs(
compiledCode: string,
preSuppliedImports: Record<string, any>,
cwd?: string,
sourceUrl?: string,
) {
;(globalThis as any).__tscircuit_require = (name: string) => {
const resolvedFilePath = resolveFilePath(name, preSuppliedImports, cwd)
Expand Down Expand Up @@ -56,6 +57,7 @@ export function evalCompiledJs(
var module = { exports };
var circuit = globalThis.__tscircuit_circuit;
${compiledCode};
//# sourceURL=${sourceUrl ?? "compiled.ts"}
return module;`.trim()
return Function(functionBody).call(globalThis)
}
2 changes: 2 additions & 0 deletions webworker/execution-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface ExecutionContext extends WebWorkerConfiguration {
fsMap: Record<string, string>
entrypoint: string
preSuppliedImports: Record<string, any>
sourceMaps: Record<string, any>
circuit: RootCircuit
}

Expand Down Expand Up @@ -45,6 +46,7 @@ export function createExecutionContext(
// ignore type imports in getImportsFromCode
"@tscircuit/props": {},
},
sourceMaps: {},
circuit,
...webWorkerConfiguration,
}
Expand Down
14 changes: 13 additions & 1 deletion webworker/import-local-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as Babel from "@babel/standalone"
import { resolveFilePathOrThrow } from "lib/runner/resolveFilePath"
import { dirname } from "lib/utils/dirname"
import { getImportsFromCode } from "lib/utils/get-imports-from-code"
import { addSourceLineToError } from "lib/utils/addSourceLineToError"
import { evalCompiledJs } from "./eval-compiled-js"
import type { ExecutionContext } from "./execution-context"
import { importEvalPath } from "./import-eval-path"
Expand Down Expand Up @@ -38,8 +39,12 @@ export const importLocalFile = async (
const result = Babel.transform(fileContent, {
presets: ["react", "typescript"],
plugins: ["transform-modules-commonjs"],
filename: "virtual.tsx",
filename: fsPath,
sourceMaps: true,
})
if (result.map) {
ctx.sourceMaps[fsPath] = result.map
}

if (!result || !result.code) {
throw new Error("Failed to transform code")
Expand All @@ -50,9 +55,11 @@ export const importLocalFile = async (
result.code,
preSuppliedImports,
dirname(fsPath),
fsPath,
)
preSuppliedImports[fsPath] = importRunResult.exports
} catch (error: any) {
await addSourceLineToError(error, ctx.fsMap, ctx.sourceMaps)
throw new Error(
`Eval compiled js error for "${importName}": ${error.message}`,
)
Expand All @@ -63,7 +70,11 @@ export const importLocalFile = async (
presets: ["env"],
plugins: ["transform-modules-commonjs"],
filename: fsPath,
sourceMaps: true,
})
if (result.map) {
ctx.sourceMaps[fsPath] = result.map
}

if (!result || !result.code) {
throw new Error("Failed to transform JS code")
Expand All @@ -73,6 +84,7 @@ export const importLocalFile = async (
result.code,
preSuppliedImports,
dirname(fsPath),
fsPath,
).exports
} else {
throw new Error(
Expand Down
2 changes: 2 additions & 0 deletions webworker/import-snippet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export async function importSnippet(
preSuppliedImports[importName] = evalCompiledJs(
cjs!,
preSuppliedImports,
undefined,
importName,
).exports
} catch (e) {
console.error("Error importing snippet", e)
Expand Down