Skip to main content

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​

StageWhatSetup standardWhy this order
0Regression check—Every later stage is measured against it
1Formatting7, 9, 10The reformat touches every file, so it goes first, alone
2ESLint6Fix the code, never the rules
3Names1The lint errors show most of the names to change
4Constants12.1Components move next, and will import these
5Components12, 13, 14Pure moves first, then shared code, which needs the paths
6Docs18The mechanical copy first, then the rewrite
7AGENTS.md, skills and version21Describes 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.
Squash merges lose the reformat commit

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 main by 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 .prettierignore too.
  • 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:

  1. The config files. Rename every config to .mjs (why). Add prettier.config.mjs from the Prettier page, with its format and format:check scripts, .editorconfig, .nvmrc and .envrc. Do not reformat anything yet.

    pnpm add -D prettier @ianvs/prettier-plugin-sort-imports prettier-plugin-tailwindcss
  2. The reformat, on its own. Run pnpm format and commit the result with nothing else in it. Every file changes, and none of it changes behaviour.

  3. .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 local git 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:

eslint.overrides.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:

PileWhat to doWhere to check
A dictionary word — id, docs, app, url, apiLeave itDictionary
A third-party name — React's ref, Next.js's params, shadcn/ui's cn, Linear's pageInfo, Node's cwd optionLeave it exactly as it isThird-party names
Our own shorthand — btn, lib/, helpers/, utilities/Rename itThe 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:

BeforeAfter
utilities/, helpers/, helpers.tsutils/, or a file named after what it does
ButtonProperties, propertiesButtonProps, props
MAX_, MIN_MAXIMUM_, MINIMUM_
"sm" | "md" | "lg""small" | "medium" | "large"
auth, useAuthauthentication, useAuthentication
(e) =>, (i) =>(event) =>, (index) =>

docs/, docsSite/, DOCS_URL and pnpm docs:dev already follow the standard. Leave them.

3. Rename in this order​

  1. Folders, with git mv so the history follows the files. Fix the imports at the same time.
  2. Types, so the @typescript-eslint/naming-convention errors clear.
  3. 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.
  4. 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.local and 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:

renames.txt
# 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 in animation.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 s and its links in r under 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/ except r and s gets a real type from lib/types/, never as 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 with import type straight from its file. See Types for constants.

Stage 5 — Components​

Applies setup standards 12, 13 and 14. Two branches:

  1. 5a: move the component folders, and nothing else. Use git mv so 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 in app/(homepage)/, the site frame in components/layout/, and no components/features/.
  2. 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's components/ folder.

Stage 6 — Docs​

Applies setup standard 18. Two branches:

  1. 6a: the mechanical part. Copy the doc templates and the docsSite/ viewer, move images into docs/assets/, and delete the old viewer's leftovers.
  2. 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.

  1. Copy the AGENTS.md template to the project root and replace every [placeholder]. It describes the final layout, so it goes last.
  2. Copy the eight standard skill files into .agents/skills/, and again into .claude/commands/.
  3. Set **Code standards version:** to the installed package's full version, such as 3.2.2.
  4. 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 in coding-standards.md.
  • New files follow the full standards from day one, even before the migration is finished.