Migrating an Existing Project
Moving an established codebase onto the standards is done in stages, so each change stays small enough to check properly. This is the order that worked on a real 600-file project.
Already following the standards, and just updating to a newer version? You don't need this guide. Read the Version History from your project's version onward, and do only what it lists.
Agree the standards first. Print the list in Setup, step 3, and ask the step 4 questions. Skip any stage for a standard the person turned down.
The stages
| Stage | What | Setup standard | Why this order |
|---|---|---|---|
| 0 | Regression check | — | Every later stage is measured against it |
| 1 | Formatting | 7, 9, 10 | The reformat touches every file, so it goes first, alone |
| 2 | ESLint | 6 | Fix the code, never the rules |
| 3 | Names | 1 | The lint errors show most of the names to change |
| 4 | Constants | 12.1 | Components move next, and will import these |
| 5 | Components | 12, 13, 14 | Pure moves first, then shared code, which needs the paths |
| 6 | Docs | 18 | The mechanical copy first, then the rewrite |
| 7 | AGENTS.md, skills and version | 21 | Describes the final layout, so it goes last |
How to work through the stages
- One branch per stage, stacked. Each stage branches from the one before it. Stop after each stage for the person to review and commit. This is what keeps a large migration reviewable.
- Each stage changes one kind of thing. A stage that moves files does not also change what they do.
- Run
pnpm lint,pnpm format:check, the tests and the build at the end of every stage, and the regression check from Stage 0 if the project has one. - Record anything left for later under "Unfinished changes" in
docs/development/coding-standards.md. See Finishing a change later.
If the project squash-merges pull requests, the reformat commit's hash disappears from main when the stack is merged, and .git-blame-ignore-revs stops working. Decide before starting:
- Land Stages 0 and 1 on
mainby themselves first, with a merge that keeps the commits, so the reformat survives as its own commit. Then stack the rest on top. Recommended. - Or accept losing blame-ignore, and squash everything.
Before you start: third-party code is exempt
The standards apply to code we write, not to packages we pull in. Do not lint, reformat, rename or restructure third-party code — see Third-party code is exempt.
node_modules/is ignored automatically.- Third-party code copied into the repo belongs in a
vendor/folder, which the configs ignore. - Add
vendor/to.prettierignoretoo. - shadcn components are not third-party. They are our code: convert them to the standards rather than moving them to
vendor/. See Adding a shadcn component.
Stage 0 — Regression check
Optional, and recommended when Stages 4 to 6 will move a lot of code. Set up the regression check and take its baseline from the last known-good page, before anything changes: the live production site, or the branch's last commit before the migration when its page has already changed from production. Record which. Every later stage must pass it unchanged.
Take the baseline first, then move one kind of thing at a time. The constants moves (Stage 4) and the folder moves (Stage 5a) are separate steps, each followed by a full regression run. When a run fails, only one kind of move can have caused it.
Stage 1 — Formatting
Applies setup standards 7, 9 and 10. Three commits, in this order:
-
The config files. Rename every config to
.mjs(why). Addprettier.config.mjsfrom the Prettier page, with itsformatandformat:checkscripts,.editorconfig,.nvmrcand.envrc. Do not reformat anything yet.pnpm add -D prettier @ianvs/prettier-plugin-sort-imports prettier-plugin-tailwindcss -
The reformat, on its own. Run
pnpm formatand commit the result with nothing else in it. Every file changes, and none of it changes behaviour. -
.git-blame-ignore-revs. Create it with the reformat commit's full hash and a comment saying what it was. GitHub reads it automatically. For localgit blame, each person runs once:git config blame.ignoreRevsFile .git-blame-ignore-revs
Add a .prettierignore for generated files and anything Prettier must not format. See What Prettier skips.
Stage 2 — ESLint
Applies setup standard 6.
Copy the standard config for the project's framework to eslint.config.mjs, unchanged, and eslint.overrides.mjs beside it. To see the size of the job first, turn the noisiest rules down to warnings in eslint.overrides.mjs, never in eslint.config.mjs:
// Temporary, while migrating: see Unfinished changes in
// docs/development/coding-standards.md.
export const projectOverrides = [
{
rules: {
"@typescript-eslint/no-explicit-any": "warn",
"unicorn/filename-case": "warn",
"unicorn/name-replacements": "warn",
},
},
];
Record the temporary warnings under "Unfinished changes". Run pnpm lint, and fix one kind of error at a time, such as every any. Fix the code, not the rules. When the warnings are gone, empty eslint.overrides.mjs again.
Stage 3 — Names
Applies setup standard 1. Its own branch, after ESLint: the lint errors show most of the names to change.
Renaming is the biggest job in most migrations, and the easiest place to get confused. Every name we write becomes Human-Readable Names. Work through it in this order.
1. Decide what stays
Before renaming anything, sort the short names into three piles:
| Pile | What to do | Where to check |
|---|---|---|
A dictionary word — id, docs, app, url, api | Leave it | Dictionary |
A third-party name — React's ref, Next.js's params, shadcn/ui's cn, Linear's pageInfo, Node's cwd option | Leave it exactly as it is | Third-party names |
Our own shorthand — btn, lib/, helpers/, utilities/ | Rename it | The Never / Always table |
Not sure which pile? Don't guess and don't rename it. Ask, and if it is a dictionary word, add it to the dictionary first. Everyone then makes the same call.
2. The common renames
These come up in nearly every project:
| Before | After |
|---|---|
utilities/, helpers/, helpers.ts | utils/, or a file named after what it does |
ButtonProperties, properties | ButtonProps, props |
MAX_, MIN_ | MAXIMUM_, MINIMUM_ |
"sm" | "md" | "lg" | "small" | "medium" | "large" |
auth, useAuth | authentication, useAuthentication |
(e) =>, (i) => | (event) =>, (index) => |
docs/, docsSite/, DOCS_URL and pnpm docs:dev already follow the standard. Leave them.
3. Rename in this order
- Folders, with
git mvso the history follows the files. Fix the imports at the same time. - Types, so the
@typescript-eslint/naming-conventionerrors clear. - Variables, functions and properties. Use your editor's Rename Symbol (F2), not find-and-replace: it renames every use and nothing else. An agent working from the command line has no editor, so it uses the rename script instead.
- Everything the linter can't see — form fields, union values, routes, scripts, environment variables. Search for each one by hand.
4. Names that leave the codebase
Some names are used outside the code. Renaming them needs more than a code change:
- Database columns need a migration.
- Environment variables must be renamed in every
.env.localand in Vercel's settings too, or the app gets an empty value. - API routes and request fields used by another app need both names to work until that app is updated.
- Routes people have bookmarked need a redirect from the old path.
Tell the person about each of these, so nobody gets caught out.
Renaming from the command line
An AI agent working in a terminal has no editor, so it cannot press F2. The package ships a script that does the same job, using the project's own TypeScript and the same rename engine as the editor:
node node_modules/@activatus/code-standards/scripts/renameSymbol.mjs <file> <currentName> <newName>
# Rename a type everywhere it is used
node node_modules/@activatus/code-standards/scripts/renameSymbol.mjs components/ui/button/types.ts ButtonProperties ButtonProps
Many renames at once: list them in a text file, one per line, and pass it with --list. Blank lines and lines starting with # are skipped, and the renames run in order:
# Version 3 renames
components/ui/button/types.ts ButtonProperties ButtonProps
lib/terminal/tabs.ts surfaceReference surfaceRef
node node_modules/@activatus/code-standards/scripts/renameSymbol.mjs --list renames.txt
- Give it the file where the name is declared. It renames that symbol and every use of it, across the whole project, and leaves anything else with the same name alone.
- Shorthand stays correct.
{ properties }becomes{ properties: props }, so objects keep their shape. - It only renames TypeScript and JavaScript names. Folders, files, form fields, routes and environment variables still need renaming by hand.
- It cannot reach keys in plain object literals that aren't typed, such as the expected value in a test's
toEqual({ properties: … }). Search the tests for the old name and fix those keys by hand. - Run the type check and tests after each batch of renames.
Stage 4 — Constants
Applies setup standard 12.1. Move every link into r and every piece of text people see or hear into s, in lib/constants/. Every other value stays with the code that uses it until more than one file needs it; a shared value moves to the file in lib/constants/ that fits. See Constants.
- Move the values, nothing else. The page must look and work exactly as before.
- A value one file uses moves to the top of that file. A component's own constants file is folded into the files that use it, or into
lib/constants/for the values more than one file shares. - Move animation and artwork objects whole. Never pull their numbers out into
numbers.ts. - Where each value goes is the project's call. Read the Constants table and decide. When it is not clear, put the value in
lib/constants/unsortedValues.ts, carry on, and ask the person at the end of the stage: see When it is not clear where a value goes. - An id moves into the object that uses it. CSS written as a string moves to
globals.css, never into a constants file. A media query the code reads stays in its object inanimation.ts. - Only text people see or hear goes in
s. Internal strings, such as ids, keys and log messages, stay where they are used: a key in the object that uses them, or a named constant at the top of the file. - A list of items with links keeps its words in
sand its links inrunder the same keys, and a loop looks each one up by its key: see the example. Delete any separate lookup object that maps a key to its link. - Every object and list constant in
lib/constants/exceptrandsgets a real type fromlib/types/, neveras const. A constant at the top of the file that uses it is typed from that file's own.types.ts. A single number or string needs none. Move the types it uses there too.lib/types/reads a constant's type withimport typestraight from its file. See Types for constants.
Stage 5 — Components
Applies setup standards 12, 13 and 14. Two branches:
- 5a: move the component folders, and nothing else. Use
git mvso the history follows, and rewrite the import paths. No code inside a file changes. Pure moves are quick to review. Follow Where components live: the homepage inapp/(homepage)/, the site frame incomponents/layout/, and nocomponents/features/. - 5b: move the code several components share to its standard home, now that the final paths are known: a shared component up one level, a hook beside the component that owns it, and sort, find, get, filter and format functions into
utils/. Sub-components sit flat in their parent'scomponents/folder.
Stage 6 — Docs
Applies setup standard 18. Two branches:
- 6a: the mechanical part. Copy the doc templates and the
docsSite/viewer, move images intodocs/assets/, and delete the old viewer's leftovers. - 6b: rewrite the content as Decision Docs. This is the part that needs judgement, so it is reviewed on its own. Follow Converting existing docs.
Keep every docs folder and file from the templates, even ones that do not apply yet. This rule is final: no docs folder is deleted, whatever the size of the project. See Setting it up.
Stage 7 — AGENTS.md, skills and version
Applies setup standard 21.
- Copy the AGENTS.md template to the project root and replace every
[placeholder]. It describes the final layout, so it goes last. - Copy the eight standard skill files into
.agents/skills/, and again into.claude/commands/. - Set
**Code standards version:**to the installed package's full version, such as3.2.2. - Run the standards check, and fix what it finds.
Tips
- Don't mix formatting and logic changes. Reformat on its own, then change the code.
- Generated files are ignored with an entry in
eslint.overrides.mjs, such as{ ignores: ["generated/**"] }, and a line in.prettierignore. Record the reason incoding-standards.md. - New files follow the full standards from day one, even before the migration is finished.