diff --git a/packages/coding-agent/src/eval/js/shared/rewrite-imports.ts b/packages/coding-agent/src/eval/js/shared/rewrite-imports.ts index 8312894bd..0cfe303b3 100644 --- a/packages/coding-agent/src/eval/js/shared/rewrite-imports.ts +++ b/packages/coding-agent/src/eval/js/shared/rewrite-imports.ts @@ -172,8 +172,35 @@ export function demoteTopLevelLexicals(code: string): string { return result; } + +/** + * Strip TypeScript syntax (type annotations, `interface`, `as`, `satisfies`, generics in + * call expressions, etc.) before the import/lexical rewriters parse the code. We use Bun's + * native transpiler in `ts` loader mode — fast, no JSX transforms, preserves `import`/ + * `export` declarations so the downstream Babel rewrites keep working. + * + * Skipped when the code parses as plain JavaScript already (Babel can accept it), so the + * common case avoids an extra transpile pass. We detect "looks like TS" with a cheap regex + * before invoking the transpiler. + */ +export function stripTypeScript(code: string): string { + if (!LOOKS_LIKE_TS.test(code)) return code; + try { + return new Bun.Transpiler({ loader: "ts" }).transformSync(code); + } catch { + // Transpiler failed (e.g. unrecoverable syntax). Hand the original source back so the + // downstream rewriter / VM surfaces the real error to the user. + return code; + } +} + +// Heuristic: any of the obvious TS-only tokens. Plain JS using `as` only inside strings +// won't match because we require a leading word boundary plus a colon/keyword neighbor. +const LOOKS_LIKE_TS = + /(?:\binterface\s+\w|\btype\s+\w+\s*=|\b(?:as|satisfies)\s+(?:[A-Z]|\bconst\b)|:\s*(?:string|number|boolean|any|unknown|void|never|object|[A-Z]\w*)\b|<\s*[A-Z]\w*\s*[,>])/; + export function wrapCode(code: string): { source: string; asyncWrapped: boolean } { - const rewritten = demoteTopLevelLexicals(rewriteStaticImports(code)); + const rewritten = demoteTopLevelLexicals(rewriteStaticImports(stripTypeScript(code))); const needsAsyncWrapper = /\bawait\b|\breturn\b/.test(rewritten); if (!needsAsyncWrapper) { return { source: rewritten, asyncWrapped: false }; diff --git a/packages/coding-agent/test/core/js-executor.test.ts b/packages/coding-agent/test/core/js-executor.test.ts index 2ec82a229..1d9dc2e7c 100644 --- a/packages/coding-agent/test/core/js-executor.test.ts +++ b/packages/coding-agent/test/core/js-executor.test.ts @@ -268,4 +268,18 @@ describe("executeJs", () => { expect(result.exitCode).toBe(0); expect(result.output.trim()).toBe(path.join("a", "b")); }); + + it("strips TypeScript syntax before executing user code", async () => { + const result = await executeJs( + [ + "interface Pair { a: number; b: number }", + "const make = (a: number, b: number): Pair => ({ a, b });", + "const p = make(3, 4) as Pair;", + "return p.a + p.b;", + ].join("\n"), + { sessionId, session, sessionFile }, + ); + expect(result.exitCode).toBe(0); + expect(result.output.trim()).toBe("7"); + }); });