ProvenPin Install

Babel codemod: rename bindings and prune console.log: working versions (tested 2026-10-15)

Parse TypeScript with @babel/parser, rename identifier bindings through scope.rename, drop console.log calls with @babel/traverse, and emit code with @babel/generator while handling the ESM/CJS default-export interop offline.

passed5 passed · 0 failed2026-10-15 22:58 UTC
Status
passed
Last passed
2026-10-15 22:58 UTC
Tests
5 / 5
Language
TypeScript
Runtime
node v22.23.3
pinned versions6
@babel/parser@8.0.7@babel/traverse@8.0.7@babel/generator@8.0.6@babel/types@8.0.6@types/node@26.6.5tsx@4.23.15
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 38820570375

The code

Exactly the files that passed. Your agent can pull them with get_recipe("babel-codemod-parse-traverse") (install), or see them on GitHub.

package.json
{
  "name": "babel-codemod-parse-traverse",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {
    "@babel/generator": "8.0.6",
    "@babel/parser": "8.0.7",
    "@babel/traverse": "8.0.7",
    "@babel/types": "8.0.6"
  },
  "devDependencies": {
    "@types/node": "26.6.5",
    "tsx": "4.23.15"
  }
}
src/codemod.ts
import { parse } from "@babel/parser";
import * as traverseModule from "@babel/traverse";
import * as generatorModule from "@babel/generator";
import * as t from "@babel/types";
import type { NodePath } from "@babel/traverse";

const traverse = (traverseModule as any).default ?? traverseModule;
const generate = (generatorModule as any).default ?? generatorModule;

export type RenameSpec = {
  from: string;
  to: string;
};

export type CodemodOptions = {
  rename?: RenameSpec;
  removeConsoleLogs?: boolean;
};

export type CodemodResult = {
  code: string;
  renamed: number;
  removedLogs: number;
};

function isConsoleLogCallee(path: NodePath<t.CallExpression>): boolean {
  const callee = path.get("callee") as unknown as {
    isMemberExpression(): boolean;
    isOptionalMemberExpression(): boolean;
    node: t.Node;
  };
  if (!callee.isMemberExpression() && !callee.isOptionalMemberExpression()) {
    return false;
  }
  const node = callee.node as t.MemberExpression;
  if (!t.isIdentifier(node.object, { name: "console" })) {
    return false;
  }
  if (node.computed) {
    return t.isStringLiteral(node.property, { value: "log" });
  }
  return t.isIdentifier(node.property, { name: "log" });
}

export function codemod(source: string, options: CodemodOptions = {}): CodemodResult {
  const ast = parse(source, {
    sourceType: "module",
    plugins: ["typescript"],
  });

  let renamed = 0;
  let removedLogs = 0;

  traverse(ast, {
    Scopable(path: NodePath<any>) {
      const spec = options.rename;
      if (spec && spec.from !== spec.to && path.scope.hasOwnBinding(spec.from)) {
        path.scope.rename(spec.from, spec.to);
        renamed += 1;
      }
    },
    CallExpression(path: NodePath<t.CallExpression>) {
      if (!options.removeConsoleLogs) {
        return;
      }
      if (!isConsoleLogCallee(path)) {
        return;
      }
      const statement = path.parentPath;
      if (statement && statement.isExpressionStatement()) {
        statement.remove();
        removedLogs += 1;
      }
    },
  });

  const { code } = generate(ast);
  return { code, renamed, removedLogs };
}

export function renameIdentifier(source: string, from: string, to: string): string {
  return codemod(source, { rename: { from, to } }).code;
}

export function removeConsoleLogs(source: string): string {
  return codemod(source, { removeConsoleLogs: true }).code;
}
test/codemod.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { codemod, renameIdentifier, removeConsoleLogs } from "../src/codemod.ts";

test("renames a binding and all of its references", () => {
  const out = renameIdentifier(
    "const count = 1;\nfunction inc() {\n  return count + 1;\n}\n",
    "count",
    "total",
  );
  assert.match(out, /const total = 1;/);
  assert.match(out, /return total \+ 1;/);
  assert.doesNotMatch(out, /\bcount\b/);
});

test("renames bindings per scope, including shadowed ones", () => {
  const out = renameIdentifier(
    "const value = 1;\nfunction f() {\n  const value = 2;\n  return value;\n}\n",
    "value",
    "renamed",
  );
  assert.equal((out.match(/const renamed =/g) ?? []).length, 2);
  assert.match(out, /return renamed;/);
  assert.doesNotMatch(out, /\bvalue\b/);
});

test("respects TypeScript types while renaming values", () => {
  const source =
    'interface User { id: string }\nconst user: User = { id: "1" };\nexport function getId(u: User): string {\n  return u.id;\n}\n';
  const out = renameIdentifier(source, "user", "account");
  assert.match(out, /interface User/);
  assert.match(out, /const account: User =/);
  assert.doesNotMatch(out, /\buser\b/);
});

test("removes console.log calls but keeps other statements", () => {
  const source =
    'const x = 1;\nconsole.log("hello", x);\nconsole["log"]("computed");\nfunction f() {\n  console.log("nested");\n  return x;\n}\nconsole.error("keep me");\n';
  const out = removeConsoleLogs(source);
  assert.doesNotMatch(out, /console\.log/);
  assert.doesNotMatch(out, /console\["log"\]/);
  assert.match(out, /console\.error/);
  assert.match(out, /const x = 1;/);
  assert.match(out, /return x;/);
});

test("combined codemod reports counts and generates code", () => {
  const source =
    "const count = 1;\nconsole.log(count);\nfunction inc() {\n  return count + 1;\n}\n";
  const result = codemod(source, {
    rename: { from: "count", to: "total" },
    removeConsoleLogs: true,
  });
  assert.equal(result.renamed, 1);
  assert.equal(result.removedLogs, 1);
  assert.match(result.code, /const total = 1;/);
  assert.match(result.code, /return total \+ 1;/);
  assert.doesNotMatch(result.code, /console\.log/);
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.