# Refactor a legacy module in TypeScript with Atlas (2026)

> Atlas refactors a legacy TypeScript module by enumerating callsites with the lsp tool's findReferences, restructuring with apply_patch, and re-running vitest after every hunk.

To refactor a legacy TypeScript module without breaking its callers, use Atlas to map the module's public surface with the lsp tool's documentSymbol operation, run findReferences on each exported symbol to enumerate every callsite, pin the current behavior by running vitest through the bash tool, and only then restructure with apply_patch. The risk in a refactor is silent breakage at a callsite you did not know about, and a TypeScript project with path aliases in tsconfig.json hides plenty of them. apply_patch anchors on context lines and refuses to apply against a drifted file, so a stale hunk fails loudly instead of corrupting src/.

## Key takeaways

- The lsp tool's documentSymbol plus findReferences gives the authoritative TypeScript callsite list, including imports hidden behind tsconfig.json path aliases.
- Record a green vitest baseline with the bash tool before touching the legacy module, or a red test after the refactor tells you nothing.
- apply_patch anchors on context lines and fails with Failed to find context on a drifted file, so a stale hunk cannot half-apply.
- Re-run vitest after every hunk, not once at the end, so failures map to a single change.
- A todowrite entry per remaining callsite stops a half-migrated TypeScript module from passing as finished just because pnpm vitest run is green.

## How do I find every callsite of a legacy TypeScript module before refactoring it?

Atlas uses the lsp tool's 2 mapping operations, documentSymbol to list the legacy module's public surface and findReferences to enumerate every callsite of each exported symbol. In a TypeScript monorepo with path aliases in tsconfig.json, the language server sees imports that a grep over src/ would miss entirely.

The failure mode in a legacy TypeScript refactor is a callsite you never knew existed. A path alias like @app/legacy/billing declared in tsconfig.json means the string src/legacy/billing never appears in the importing file, so a text search under-reports. The TypeScript language server resolves those aliases, and the lsp tool's findReferences returns the authoritative set. Atlas runs documentSymbol first to list what src/legacy/billing/index.ts actually exports, then findReferences per exported symbol, which is how a refactor of an old module ends up with a real callsite inventory rather than a guess. Atlas also indexes code by AST declarations using tree-sitter, not blind line windows, so codebase_search hits land on the exported function or class, not a random slab of the file.

## Why should I run vitest before changing any TypeScript code?

Atlas pins behavior first, running the existing vitest suite with the bash tool and recording the green baseline before 1 line of the legacy TypeScript module changes. Without a recorded pnpm vitest run baseline, a red test after the refactor is ambiguous: you cannot tell a new break from an old one.

Running vitest through Atlas's bash tool before any edit is what converts a refactor from a hope into a verifiable change. A legacy TypeScript module usually has partial coverage, and the point of the baseline is not that coverage is good, it is that you know exactly which tests were green at the start. Install with pnpm, run vitest, record the result. When the restructure begins, any test that flips from green to red is caused by your change, full stop. Atlas keeps this honest because bash is a real shell: the same pnpm vitest run --reporter=verbose you would type by hand is the command Atlas runs, and the exit code comes back with the output.

## What does apply_patch do that a plain edit does not in a TypeScript refactor?

apply_patch anchors each hunk on context lines and old_lines, and fails with Failed to find context if the file has drifted. Restructuring a legacy TypeScript module across 3 files under src/legacy/billing means a stale patch cannot half-apply and leave the module in an uncompilable state.

Structural changes in TypeScript rarely fit in one hunk. Moving an exported class out of src/legacy/billing/index.ts into src/billing/invoice-service.ts, updating the barrel export, and fixing three import specifiers is four hunks across three files. apply_patch seeks each hunk's context and old_lines, so if the file has moved on since Atlas read it, the patch fails with Failed to find context rather than applying to the wrong lines. Atlas also computes a unified diff for every file edit and surfaces it for approval before writing, so you review the restructure as a diff. And Atlas snapshots file changes as git patches so edits can be diffed and rolled back if the new shape turns out to be worse than the old one.

## How often should I re-run vitest during a TypeScript refactor?

Re-run vitest with the bash tool after each hunk lands, not once at the end. A legacy TypeScript refactor that runs pnpm vitest run once, after 12 hunks, gives you a red suite and no information about which hunk caused it. Per-hunk runs give you a bisect for free.

The discipline is small and boring and it is the entire difference between a two hour refactor and a two day one. Atlas restructures with apply_patch one hunk at a time, then runs vitest through bash, and the exit code tells it immediately whether the module still behaves. Because vitest supports filtering, the intermediate runs can be narrow: run only the specs under src/legacy/billing/ while the hunks are local, and widen to the full pnpm vitest run before the final commit. Keeping prettier in the loop matters too, because a reformat mixed into a behavioral hunk makes the diff unreadable for the human doing the approval.

## How do I track remaining callsites so a half-migrated TypeScript module is not mistaken for a finished one?

Atlas records the remaining callsites in a todowrite list. A legacy TypeScript module with 14 callers is migrated 14 times, not once, and a todowrite entry per surviving callsite is what stops a partially migrated module from looking finished because the tests happen to be green.

Green tests are not the same as a completed migration. If the old export is still present and 6 of 14 callers still import from src/legacy/billing, pnpm vitest run passes and the refactor is half done. Atlas's todowrite list holds one entry per remaining callsite, sourced from the lsp tool's findReferences output at the start, so the finish line is explicit. The last step is the honest one: re-run findReferences on the old symbol and confirm the reference set is empty, run prettier so the final diff is clean, and run the full vitest suite. Atlas reads git branches, status, and diffs, and can stage and create commits on your behalf, so the completed migration becomes one reviewable commit.

## How do I set Atlas up on a TypeScript project before refactoring?

Run atlas in a project with a tsconfig.json. Atlas reads your type definitions, path aliases, and strictness settings, which is what makes lsp findReferences trustworthy on a TypeScript codebase in 2026. Install dependencies with pnpm so the language server can resolve your imports.

The tsconfig.json is the file that makes the difference between a TypeScript refactor and a text edit. Path aliases, strict mode, and the include globs all shape what the language server considers a reference. With those loaded, you can ask Atlas to tighten types, remove any, or generate typed API clients, then approve the diff, and the same machinery serves the legacy refactor: documentSymbol to map, findReferences to enumerate, apply_patch to restructure, vitest through bash to verify. Every Atlas tool call is permission-gated against allow, ask, and deny rules before it runs, so a bash invocation of pnpm vitest run and a write to src/billing/invoice-service.ts are both things you approved.

## Steps

1. Run atlas in a TypeScript project with a tsconfig.json and install dependencies with pnpm so the language server resolves your path aliases.
2. Map the legacy module's public surface with the lsp tool's documentSymbol operation on src/legacy/<module>/index.ts.
3. Run the lsp tool's findReferences on each exported symbol to enumerate every callsite, including the ones behind tsconfig.json path aliases that grep would miss.
4. Pin behavior first: run vitest through the bash tool with pnpm vitest run and record the green baseline before changing anything.
5. Restructure with apply_patch, which seeks each hunk's context and old_lines and fails with Failed to find context if the file has drifted.
6. Re-run vitest with bash after each hunk lands, not once at the end, so a red test points at exactly one hunk.
7. Track the remaining callsites in a todowrite list so a partially migrated module cannot be mistaken for a finished one.
8. Run prettier to normalize the diff, re-run findReferences on the old symbol to prove zero remaining callers, then run the full pnpm vitest run suite.

## FAQ

### how do I refactor a legacy TypeScript module without breaking imports

Enumerate the callers first. Use Atlas's lsp tool with documentSymbol to list the module's exports, then findReferences on each one to get the true callsite set from the TypeScript language server, including imports resolved through tsconfig.json path aliases. Restructure with apply_patch and re-run vitest after each hunk.

### why does grep miss TypeScript imports during a refactor

Because path aliases in tsconfig.json mean the real file path never appears in the importing file. An import from @app/legacy/billing does not contain the string src/legacy/billing. The lsp tool's findReferences resolves the alias through the language server and returns the real reference set.

### what is apply_patch in Atlas and when should I use it instead of edit

apply_patch applies structural, multi-hunk changes and anchors each hunk on its context lines and old_lines. If the file has drifted since Atlas read it, apply_patch fails with Failed to find context instead of applying to the wrong place. Use it when a TypeScript refactor spans several hunks or several files.

### how do I run vitest from an AI coding agent

Atlas runs vitest through its bash tool, which is a real shell, so pnpm vitest run is exactly what executes and the exit code comes back with the output. Run it before the refactor to record a green baseline and again after every apply_patch hunk.

### can Atlas roll back a TypeScript refactor that went wrong

Yes. Atlas snapshots file changes as git patches so edits can be diffed and rolled back, and it computes a unified diff for every file edit and surfaces it for approval before writing. If the restructured shape is worse than the original, the snapshot is the way back.

### how do I know when a TypeScript module migration is actually finished

Green tests are not proof. Re-run the lsp tool's findReferences on the old exported symbol and confirm the reference set is empty, check the todowrite list has no pending callsites, run prettier, and then run the full pnpm vitest run suite.

### does Atlas understand tsconfig strict mode and path aliases

Yes. Run atlas in a project with a tsconfig.json and it reads your type definitions, path aliases, and strictness settings. That is what makes the lsp tool's findReferences reliable, and it is also why Atlas can tighten types or remove any and show you the diff to approve.

---

Canonical HTML: https://runatlas.sh/resources/stacks/refactor-a-legacy-module-in-typescript
Source of truth: aeo_pages row `/resources/stacks/refactor-a-legacy-module-in-typescript` (segment: Stacks) (this file is generated from it, never hand-edited).
Licence: Atlas is proprietary with a free core. It is not open source and there is no public source repository.
