@exodus/typestripped-diff
v1.0.0
Published
Compare TypeScript source files (with type annotations stripped) against the original JavaScript at a git base ref. Reviewer aid for JS→TS migration PRs.
Readme
@exodus/typestripped-diff
A code-review helper for JS→TS migration PRs.
After a migration, git diff master -- <pkg>/ is dominated by TS additions (annotations, interfaces) and git mv renames, making behavioral changes hard to spot. This tool strips type annotations from the package's TS source (via Node's stripTypeScriptTypes, position-preserving), pairs each with the original JS at a git base ref, and diffs.
What survives in the output: identifier renames, control-flow refactors, added/removed runtime checks, import/export changes — in source AND test files.
Requirements
- Node 22.6+ (uses built-in
node:module.stripTypeScriptTypes) - A POSIX
diff(default mode) — already on every dev machine - Optionally Prettier in the consuming repo (used opportunistically to
normalise both halves before diffing — silently skipped if absent;
pass
--no-prettierto skip explicitly) - Optionally
difftastic(brew install difftastic) for--difft - Optionally the
codeCLI shim for--vscode
Install
yarn add -D @exodus/typestripped-diffOr invoke without installing:
npx @exodus/typestripped-diff <package-path> <pre-migration-base-ref>Usage
# Default: per-file unified diff (Prettier-normalised + diff -w -B)
typestripped-diff features/<pkg> <pre-migration-base-ref>
# Skip Prettier normalisation (if it's slow, missing, or unwanted):
typestripped-diff features/<pkg> <ref> --no-prettier
# Semantic AST diff per file (whitespace-tolerant by design):
typestripped-diff features/<pkg> <ref> --difft
# Open each file pair in VS Code's side-by-side diff view:
typestripped-diff features/<pkg> <ref> --vscodeFor piping to less while keeping color:
LESS='-RFX' typestripped-diff <pkg> <ref> | less
LESS='-RFX' DFT_COLOR=always typestripped-diff <pkg> <ref> --difft | lessHow it works
- Reads the package's
tsconfig.{build.,}jsonto determine:- the source root (
compilerOptions.rootDiror a single non-globinclude[]entry; defaults tosrc) - the output dir to skip during walk (
compilerOptions.outDir, plus the always-skippednode_modules,lib,dist).
- the source root (
- Walks the source root for
.ts,.tsx,.cts,.mtsfiles (skipping only.d.ts). Test files (*.test.*and anything in__tests__/) are included — reviewers want to see test changes alongside source changes. - Strips TypeScript type annotations using Node's
stripTypeScriptTypes(source, { mode: 'strip' }). Position-preserving — type annotations become whitespace of the same length, source positions unchanged. - Pairs each stripped file with its original at the base ref via
git show <ref>:<path>. Tries both<pkg>/<rel>(migration moved files INTO<sourceRoot>/) and<pkg>/<sourceRoot>/<rel>(migration kept files in<sourceRoot>/and just renamed the extension). - If Prettier is available in the consuming repo (and
--no-prettierwas not passed), pipes both halves throughprettier --stdin-filepath <original-path>so the host repo's local config drives normalisation. Failures fall back to raw content with a one-time warning. - Diffs with
diff -u -w -B(default) or the chosen tool.
Why Prettier normalization
stripTypeScriptTypes preserves positions exactly, but that's not enough on its own:
- Multi-line type annotations (e.g. discriminated-union return types) leave multi-line whitespace blocks against a single-line original —
diff -w -Bdoes not merge lines, so the structural reflow appears as a {- single-line +} {+ multi-line +} pair. - Migrators sometimes rewrap arguments or expressions while moving to TS; without Prettier those show up as noise on top of real changes.
Running both halves through the host repo's Prettier collapses semicolons, quote style, indentation, trailing commas, and wrapping — so only behavioural changes remain. diff -w -B is kept as a safety net for --no-prettier mode and for files Prettier fails on.
Extensions handled
| TS source | Looks for original at |
| --------- | --------------------------------- |
| *.ts | *.js |
| *.tsx | *.jsx (then *.js as fallback) |
| *.cts | *.cjs |
| *.mts | *.mjs |
Exit codes
| Code | Meaning |
| ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0 | Run completed; diffs (if any) printed. Diffs do not constitute failure. |
| 2 | Bad invocation, unsupported TS (e.g. enum rejected by stripTypeScriptTypes), unknown git ref, or a hard tool failure (missing diff/difft, etc.). |
The tool is a review aid, not a CI gate — a 0 exit does not mean "no diffs."
Limitations
- Source location is derived from
tsconfig.json. Packages with no tsconfig fall back tosrc/. If your TS sources live somewhere unusual without anincludeorrootDirentry, set one or pass that path as<package-path>. - Cross-extension migrations (
.tsx → .jsx,.ts → .js) are supported; arbitrary file renames within the migration are not — pairing is purely by relative path with the extension swapped.
