Nx Migration Generator
Adds an Nx Migration to an Nx Plugin. Migrations let nx migrate automatically update projects that were generated by a previous version of your plugin when you make a breaking change — renamed targets, changed vended config, or a dependency bump that requires accompanying code changes.
The generator scaffolds the migration, registers it in your plugin’s migrations.json, and wires the nx-migrations field into your plugin’s package.json (creating both if they don’t exist yet).
Generate a Migration
Section titled “Generate a Migration”- Install the Nx Console VSCode Plugin if you haven't already
- Open the Nx Console in VSCode
- Click
Generate (UI)in the "Common Nx Commands" section - Search for
@aws/nx-plugin - ts#nx-migration - Fill in the required parameters
- Click
Generate
pnpm nx g @aws/nx-plugin:ts#nx-migrationyarn nx g @aws/nx-plugin:ts#nx-migrationnpx nx g @aws/nx-plugin:ts#nx-migrationbunx nx g @aws/nx-plugin:ts#nx-migrationYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:ts#nx-migration --dry-runyarn nx g @aws/nx-plugin:ts#nx-migration --dry-runnpx nx g @aws/nx-plugin:ts#nx-migration --dry-runbunx nx g @aws/nx-plugin:ts#nx-migration --dry-runOptions
Section titled “Options”| Parameter | Type | Default | Description |
|---|---|---|---|
| project Required | string | - | The Nx Plugin project to add the migration to. We recommend using the ts#nx-plugin generator to create this. |
| name Required | string | - | Migration name, in kebab-case (e.g. rename-foo-target) |
| description | string | - | A description of what the migration does, shown by nx migrate |
| kind | deterministic | agentic | hybrid | deterministic | The kind of migration: deterministic (a codemod), agentic (an AI-applied prompt for user-owned code), or hybrid (a codemod that does the mechanical part then hands off to an agent) |
| preferInstallDependencies | boolean | true | Whether to prefer installing dependencies after the generator runs. Set to false to defer installing when batching multiple generators (an install still runs if needed so subsequent generators can compute the Nx project graph); install once at the end. |
The three kinds of migration
Section titled “The three kinds of migration”Migrations come in three forms, discriminated by which fields the migrations.json entry carries. Pass --kind to choose (default deterministic):
- Deterministic (
implementation) — a codemod with an exact before/after. Runs unattended, including in CI and non-interactive terminals. Use it alone when the change has a shape you can match reliably everywhere it appears. - Agentic (
prompt) — a markdown instruction file applied by the user’s local coding agent via Nx’s agenticnx migrateflow. Use it alone when no mechanical before/after holds, because the correct edit depends on what the user has built. When no agent runs (CI, no agent installed, consent declined), Nx surfaces the prompt as manual instructions — so write prompts as self-contained, human-actionable steps. - Hybrid (
implementation+prompt) — both halves of one change, and usually the right choice. Everything your generators vend is code the user owns and may have modified, so the codemod does as much as it can safely match and returnsagentContextdescribing what it changed and what it skipped; Nx passes that context to the pairedprompt, which directs the agent at the rest.
Generator Output
Section titled “Generator Output”The generator creates the following files under your plugin project’s source folder, depending on the chosen kind:
Directorysrc/migrations/latest/<name>/
- migration.ts Codemod implementation (deterministic and hybrid)
- migration.spec.ts Tests for your migration (deterministic and hybrid)
- prompt.md Agent/human instructions (agentic and hybrid)
- migrations.json Created or updated to register your migration
- package.json Created or updated to add an “nx-migrations” entry
A new migration lands in latest/. Grouping migrations by the release that ships them keeps their order visible at a glance as the collection grows — move a migration into a v<x.y.z>/ folder (and update its paths in migrations.json) when you assign its version.
It registers the migration in migrations.json with the fields for its kind and no version — see Versioning below. The registered key is prefixed with the folder the migration lives in (latest-<name>), so reusing a name for a later change can’t silently overwrite a migration that has already shipped.
Implementing a Migration
Section titled “Implementing a Migration”A deterministic (or hybrid) migration is a function which mutates the virtual filesystem (the Tree), returning a MigrationReturnObject. Here’s a hybrid one, which returns both:
import { type MigrationReturnObject, type Tree } from '@nx/devkit';import { formatFilesInSubtree } from '@aws/nx-plugin/sdk/utils/format';
export default async function migration( tree: Tree,): Promise<MigrationReturnObject> { const nextSteps: string[] = []; const agentContext: string[] = [];
// Read and update the files your generators produce here.
await formatFilesInSubtree(tree);
return { nextSteps, agentContext };}nextSteps— workspace-wide notes surfaced to the user afternx migrateruns. Use these to report files you skipped (see the guardrails below) or manual follow-up the migration couldn’t perform.agentContext(hybrid only) — a summary of what the codemod changed, passed to the pairedpromptwhen it runs under Nx’s agentic flow so the agent can focus on the user-owned parts.
For some example operations you can perform in your migration (generating files, modifying existing files using GritQL, reading and updating JSON), refer to the Nx Generator guide.
Guardrails
Section titled “Guardrails”Migrations run against workspaces you don’t control, so it is recommended to adhere to the following (the scaffolded skeleton bakes these in):
- Pattern-match before writing. If a target file has diverged from the shape your generators produce, skip it and report it via
nextSteps, or consider a hybrid migration, rather than clobbering the user’s changes. - Idempotent. Re-running the migration must be a no-op.
- Format what you write. Finish with
formatFilesInSubtree(tree)so the files your migration wrote are formatted correctly.
Testing Your Migration
Section titled “Testing Your Migration”The scaffolded migration.spec.ts uses createTreeUsingTsSolutionSetup() to build a virtual workspace matching the shape our preset generates. Test that the migration takes a workspace from the “before” state to the target state, skips (and reports) code it doesn’t recognise, and is idempotent:
import type { Tree } from '@nx/devkit';import { createTreeUsingTsSolutionSetup } from '@aws/nx-plugin/sdk/utils/test';import migration from './migration';
describe('my migration', () => { let tree: Tree;
beforeEach(() => { tree = createTreeUsingTsSolutionSetup(); });
it('should update the generated shape', async () => { // Set up a tree with the "before" state tree.write('project.json', JSON.stringify({ targets: { foo: {} } }));
// Run the migration await migration(tree);
// Check that the tree matches the target state const projectJson = JSON.parse(tree.read('project.json', 'utf-8')); expect(projectJson.targets).toHaveProperty('bar'); expect(projectJson.targets).not.toHaveProperty('foo'); });});Unit tests only prove the migration does what you wrote it to do. Also test it against a workspace generated by the published version of your plugin — the state your users upgrade from — and confirm the result matches what your generators produce today: inspect the diff, generate the same projects into a fresh workspace and compare, check the workspace still builds, then re-run the migration to confirm it changes nothing. Repeat with a workspace you’ve customised first, to check the migration leaves your edits alone and reports them instead.
Versioning
Section titled “Versioning”Nx runs a migration when the user’s installed plugin version is less than the migration’s version. This generator writes no version field — assign it (or stamp it from your release process) when you cut the release that ships the migration, so it runs for everyone upgrading from an earlier version.
Once you assign versions, a migrations.json registering one migration of each kind looks like this:
{ "$schema": "http://json-schema.org/schema", "name": "my-plugin", "generators": { "v2.0.0-rename-foo-target": { "version": "2.0.0", "description": "Rename the foo target to bar", "implementation": "./src/migrations/v2.0.0/rename-foo-target/migration" }, "v3.0.0-migrate-custom-handlers": { "version": "3.0.0", "description": "Update custom handlers for the new API", "prompt": "./src/migrations/v3.0.0/migrate-custom-handlers/prompt.md" }, "v4.0.0-upgrade-framework": { "version": "4.0.0", "description": "Upgrade the framework and reconcile call sites", "implementation": "./src/migrations/v4.0.0/upgrade-framework/migration", "prompt": "./src/migrations/v4.0.0/upgrade-framework/prompt.md" } }}Running Migrations
Section titled “Running Migrations”Users apply your plugin’s migrations with the two-step nx migrate flow — generate the pending migrations, install, then run them:
pnpm nx migrate my-plugin@latestpnpm nx migrate --run-migrationsyarn nx migrate my-plugin@latestyarn nx migrate --run-migrationsnpx nx migrate my-plugin@latestnpx nx migrate --run-migrationsbunx nx migrate my-plugin@latestbunx nx migrate --run-migrationsAgentic and hybrid migrations are applied by the user’s local coding agent during nx migrate --run-migrations when one is available.
For the full flow from the upgrading user’s perspective — including example output for deterministic and agentic migrations — see Upgrading Your Workspace.