Make dependency migrations declarative and enforceable.
package.json records which dependencies are installed. It does not record which of them are deprecated, what replaces them, or whether their usage is shrinking. That knowledge lives in wiki pages and Slack threads, and every week someone adds a new moment import because the last one they saw was fine.
migration-tracker puts that knowledge in the repository:
- Declare deprecated packages and their replacements in
migrations.json. - Baseline the import sites that exist today in
.migrations/baseline.json. - Enforce in CI and in the editor: existing sites pass, new ones fail.
- Ratchet the baseline down as legacy imports disappear. It never grows back.
- Report progress per group, per owner, over time.
pnpm add -D @migration-tracker/cli
pnpm exec migration-tracker init # writes migrations.json and .migrations/README.md
# edit migrations.json: list old packages, their replacements, and a mode
pnpm exec migration-tracker validate
pnpm exec migration-tracker baseline create # writes .migrations/baseline.json
git add migrations.json .migrations && git commit -m "Adopt migration-tracker"
pnpm exec migration-tracker check # run this in CIA minimal migrations.json:
{
"$schema": "./node_modules/@migration-tracker/core/schema.json",
"version": 1,
"groups": {
"forms": {
"old": ["redux-form"],
"new": ["react-hook-form"],
"enforcement": { "mode": "ratchet" },
"docs": "https://engineering.example.com/migrations/forms"
}
}
}When a pull request adds a new import, migration-tracker check exits 1:
✖ forms: src/features/checkout/PaymentForm.tsx:8
New import of "redux-form" is not allowed.
Preferred replacement: "react-hook-form"
Migration guide: https://engineering.example.com/migrations/forms
Legacy sites: 108 remaining | 12 retired | 1 new violation
When legacy imports are removed, lock in the progress:
pnpm exec migration-tracker baseline updateOn GitHub, use the action. It builds the CLI from its own checkout, so pin it to a commit SHA:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: <owner>/migration-tracker@<commit-sha>| Command | What it does |
|---|---|
migration-tracker init [--force] |
Write a starter migrations.json and .migrations/README.md. |
migration-tracker validate |
Validate migrations.json. |
migration-tracker scan |
List current legacy import sites. |
migration-tracker baseline create [--force] |
Record current sites in .migrations/baseline.json. |
migration-tracker baseline update |
Remove sites that no longer exist. Never adds. |
migration-tracker baseline migrate |
Rewrite a schemaVersion: 1 baseline with file-level site ids. |
migration-tracker baseline verify --base <ref> |
Fail if the baseline gained entries since <ref>. |
migration-tracker check |
Fail on new sites in ratchet groups and any site in ban groups. Formats: text, json, markdown, github. |
migration-tracker report |
Build an HTML dashboard or JSON snapshot with progress, adoption and ownership, optionally appending to history. See dashboard.md. |
migration-tracker depcruise-plugin-path |
Print the absolute path of the dependency-cruiser reporter plugin. |
Exit codes: 0 success, 1 policy failure, 2 invalid configuration, missing baseline, or runtime error. The binary is also available as migration-tracker.
imports policy outputs
dependency-cruiser ─┐
(default engine) │
├──> @migration-tracker/core ──┬──> migration-tracker CLI ──> text / json / markdown / github
TypeScript scanner ─┘ config, matching, │ └──────> HTML dashboard (migration-tracker report)
(positions, site ids, baselines, ├──> depcruise reporter plugin (inside your depcruise run)
suppressions, evaluation, reports └──> ESLint rule (imports read from the ESLint AST)
--engine scanner)
dependency-cruiser, through @migration-tracker/depcruise, extracts the imports of each file. The scanner adds line numbers and suppression comments for the files that have legacy imports, and can replace dependency-cruiser entirely with --engine scanner. Core turns imports into file-level sites (group|file|kind|specifier) and compares them with the baseline. The CLI, the reporter plugin and the ESLint rule all compute sites and violations with the same core functions, so they agree on what is new. See depcruise.md and ratcheting.md.
| Package | Description |
|---|---|
@migration-tracker/cli |
The migration-tracker command (alias mt). Reads config, scans, writes baselines, checks, reports. Also ships the dependency-cruiser reporter plugin (@migration-tracker/cli/depcruise-plugin). |
@migration-tracker/core |
Pure policy logic: config validation, matching, site identity, baselines, evaluation, reports. Ships the migrations.json JSON schema. |
@migration-tracker/depcruise |
Runs dependency-cruiser and maps its result to imports. The default import extraction engine. |
@migration-tracker/scanner |
Finds imports in JS and TS files with the TypeScript parser. Adds positions and suppressions, and is the --engine scanner fallback. |
eslint-plugin-migration-tracker |
ESLint 9 rule no-legacy-imports for editor feedback. |
@migration-tracker/dashboard |
The self-contained HTML report. |
- Getting started: the full adoption workflow
- Configuration: every
migrations.jsonfield, matching rules, modes - Ratcheting: site identity, baseline safety, migration, known limitations
- dependency-cruiser: the default engine and the reporter plugin
- CI: GitHub Action, output formats, exit codes
- ESLint: editor integration
- Exceptions: suppressions, exceptions, expiry, approvals
- Dashboard: progress, adoption, and ownership reports
Examples: examples/vite-react and examples/pnpm-monorepo.
- No codemods. migration-tracker tells you where legacy imports are and stops new ones. It does not rewrite code.
- No SaaS. Everything runs locally and in your CI. State lives in your repository.
- No runtime tracing. Usage is measured from static imports, not from what runs in production.
- No dynamic specifiers.
require(name)andimport(`./${x}`)are not detected. Specifiers are matched as written, so an alias that points at a legacy package is not counted. - JavaScript and TypeScript only. One policy per repository.
Requires Node 20.12 or later (dependency-cruiser supports ^20.12 || ^22 || >=24) and pnpm (version pinned in package.json packageManager).
pnpm install
pnpm build
pnpm typecheck
pnpm testTo try the CLI against an example after building:
node packages/cli/dist/bin.js check --root examples/vite-react