agent's codemod renamed imports, then renamed them again on re-run because the matcher still matched the new names
Fixes a rename codemod that renames imports twice because its matcher still matches the new names. Use when re-running an import rename corrupts already-renamed files. Key trigger: the second run of the codemod reports changes when it should report zero.
TL;DR: Match only the exact old import specifier and refuse to touch anything already pointing at the new one. Anchor the matcher on the literal old source string so it can never match the renamed output, and add a skip for files that already import the new name. Verify with two dry runs: the second must report zero files changed.
The problem as reported:
agent's codemod renamed imports, then renamed them again on re-run because the matcher still matched the new names- Open the codemod and find the matcher. Confirm it's broader than the old name - typically a regex, a contains-check, or a match on any import from a scope. Expected: you can see why the new name still matches.
- Tighten the predicate to exact equality on the old specifier: only match when the import source is exactly 'old-package'. If it must be a regex, anchor both ends so it cannot match 'new-package'. Expected: the matcher now rejects the new name by construction.
- Add a file-level skip: if the file already contains an import from 'new-package', leave the file alone entirely. Expected: already-migrated files are never even visited.
- Dry-run twice: run the codemod in dry mode over the source tree, twice in a row. Expected: the first run lists changed files; the second run lists none.
- Keep the guard in the transform even after the migration finishes. Expected: any accidental re-run months later is a harmless no-op.
Use this when
- A rename codemod isn't idempotent and corrupts already-renamed files on re-run.
- The import matcher is a loose pattern rather than an exact string.
- You need the transform to be safely re-runnable.
Not for this skill when
- The rename is a one-shot script that will be deleted right after the migration.
- The breakage is in the renamed code's runtime behavior, not in the matcher firing twice.
Variant phrasings
- codemod renamed imports twice
- import rename codemod not idempotent
- jscodeshift renames already renamed imports
- rename transform matches its own output
Why it happens
The matcher's predicate is true for both the old name and the new name, so the transform's output is valid input for itself. Each run feeds the next.
Edge cases
- Barrel files re-exporting both the old and new names: handle re-exports explicitly or the matcher will chase its tail through them.
- Dynamic imports built from template strings: static matching can't see them - list them for manual review.
- Old and new package names sharing a prefix: never use startsWith-style matching here; exact equality only.
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_MQwBBvtIbUP3ForTHTS83Q