(code: string)
| 12 | } |
| 13 | |
| 14 | export function normalizeCode(code: string): string { |
| 15 | const trimmed = stripCodeFences(code.trim()); |
| 16 | if (!trimmed.trim()) return "async () => {}"; |
| 17 | |
| 18 | const source = trimmed.trim(); |
| 19 | |
| 20 | try { |
| 21 | const ast = acorn.parse(source, { |
| 22 | ecmaVersion: "latest", |
| 23 | sourceType: "module" |
| 24 | }); |
| 25 | |
| 26 | // Already an arrow function — pass through |
| 27 | if (ast.body.length === 1 && ast.body[0].type === "ExpressionStatement") { |
| 28 | const expr = (ast.body[0] as acorn.ExpressionStatement).expression; |
| 29 | if (expr.type === "ArrowFunctionExpression") return source; |
| 30 | } |
| 31 | |
| 32 | // export default <expression> → unwrap to just the expression |
| 33 | if ( |
| 34 | ast.body.length === 1 && |
| 35 | ast.body[0].type === "ExportDefaultDeclaration" |
| 36 | ) { |
| 37 | const decl = (ast.body[0] as acorn.ExportDefaultDeclaration).declaration; |
| 38 | const inner = source.slice(decl.start, decl.end); |
| 39 | |
| 40 | // Anonymous function/class declarations aren't valid as standalone |
| 41 | // statements — wrap them as expressions directly. |
| 42 | if ( |
| 43 | decl.type === "FunctionDeclaration" && |
| 44 | !(decl as acorn.FunctionDeclaration).id |
| 45 | ) { |
| 46 | return `async () => {\nreturn (${inner})();\n}`; |
| 47 | } |
| 48 | if ( |
| 49 | decl.type === "ClassDeclaration" && |
| 50 | !(decl as acorn.ClassDeclaration).id |
| 51 | ) { |
| 52 | return `async () => {\nreturn (${inner});\n}`; |
| 53 | } |
| 54 | |
| 55 | return normalizeCode(inner); |
| 56 | } |
| 57 | |
| 58 | // Single named function declaration → wrap and call it |
| 59 | if (ast.body.length === 1 && ast.body[0].type === "FunctionDeclaration") { |
| 60 | const fn = ast.body[0] as acorn.FunctionDeclaration; |
| 61 | const name = fn.id?.name ?? "fn"; |
| 62 | return `async () => {\n${source}\nreturn ${name}();\n}`; |
| 63 | } |
| 64 | |
| 65 | // Last statement is expression → splice in return |
| 66 | const last = ast.body[ast.body.length - 1]; |
| 67 | if (last?.type === "ExpressionStatement") { |
| 68 | const exprStmt = last as acorn.ExpressionStatement; |
| 69 | const before = source.slice(0, last.start); |
| 70 | const exprText = source.slice( |
| 71 | exprStmt.expression.start, |
no test coverage detected