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
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.