feat(coding-agent/eval): stripped TypeScript syntax before import rewrites

- Added a stripTypeScript pass that heuristically detects TypeScript-like syntax and transpiles it with Bun's ts loader before rewriting.
- Integrated the new pass into wrapCode so import and lexical rewrites operate on transpiled input.
- Added an executeJs test that verifies interfaces, type annotations, and type assertions execute to the expected numeric result.
This commit is contained in:
can1357
2026-05-12 08:42:52 +02:00
parent 2d4d70e47e
commit 2f7b2aaccd
2 changed files with 42 additions and 1 deletions
@@ -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 };
@@ -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");
});
});