branch:
normalize.ts
2652 bytesRaw
import * as acorn from "acorn";

/**
 * Strip markdown code fences that LLMs commonly wrap code in.
 * Handles ```js, ```javascript, ```typescript, ```ts, or bare ```.
 */
function stripCodeFences(code: string): string {
  const fenced =
    /^```(?:js|javascript|typescript|ts|tsx|jsx)?\s*\n([\s\S]*?)```\s*$/;
  const match = code.match(fenced);
  return match ? match[1] : code;
}

export function normalizeCode(code: string): string {
  const trimmed = stripCodeFences(code.trim());
  if (!trimmed.trim()) return "async () => {}";

  const source = trimmed.trim();

  try {
    const ast = acorn.parse(source, {
      ecmaVersion: "latest",
      sourceType: "module"
    });

    // Already an arrow function — pass through
    if (ast.body.length === 1 && ast.body[0].type === "ExpressionStatement") {
      const expr = (ast.body[0] as acorn.ExpressionStatement).expression;
      if (expr.type === "ArrowFunctionExpression") return source;
    }

    // export default <expression> → unwrap to just the expression
    if (
      ast.body.length === 1 &&
      ast.body[0].type === "ExportDefaultDeclaration"
    ) {
      const decl = (ast.body[0] as acorn.ExportDefaultDeclaration).declaration;
      const inner = source.slice(decl.start, decl.end);

      // Anonymous function/class declarations aren't valid as standalone
      // statements — wrap them as expressions directly.
      if (
        decl.type === "FunctionDeclaration" &&
        !(decl as acorn.FunctionDeclaration).id
      ) {
        return `async () => {\nreturn (${inner})();\n}`;
      }
      if (
        decl.type === "ClassDeclaration" &&
        !(decl as acorn.ClassDeclaration).id
      ) {
        return `async () => {\nreturn (${inner});\n}`;
      }

      return normalizeCode(inner);
    }

    // Single named function declaration → wrap and call it
    if (ast.body.length === 1 && ast.body[0].type === "FunctionDeclaration") {
      const fn = ast.body[0] as acorn.FunctionDeclaration;
      const name = fn.id?.name ?? "fn";
      return `async () => {\n${source}\nreturn ${name}();\n}`;
    }

    // Last statement is expression → splice in return
    const last = ast.body[ast.body.length - 1];
    if (last?.type === "ExpressionStatement") {
      const exprStmt = last as acorn.ExpressionStatement;
      const before = source.slice(0, last.start);
      const exprText = source.slice(
        exprStmt.expression.start,
        exprStmt.expression.end
      );
      return `async () => {\n${before}return (${exprText})\n}`;
    }

    return `async () => {\n${source}\n}`;
  } catch {
    return `async () => {\n${source}\n}`;
  }
}