As an Angular library author, how do you ship a migration schematic so `ng update` rewrites consumers' code for your breaking change?
answer
- package.json ng-update.migrations
- a separate migration collection
- each entry carries a version
- optional and recommended flags
- idempotent Tree edits, tested
basics
~20 sPoint ng-update.migrations in the library's package.json at a migration collection whose entries each carry a version, a factory and a description. When a consumer updates across that version, ng update runs the rule, which edits their code through the Tree.
solid answer
~40 sA migration is an ordinary schematic registered in a dedicated collection, for example `migrations/migration-collection.json`, which the library's `package.json` names under `ng-update.migrations`. Each entry has a `version` (the release that introduces the change), a `factory`, a `description`, and optionally `optional: true` and `recommended: true`. `ng update` runs the entries whose `version` falls after the installed version and up to the target version. Optional ones are offered in a prompt, pre-checked when recommended. The rule itself visits the consumer's files through the `Tree`. It finds usages of the old API by parsing TypeScript rather than with regexes, rewrites them with `UpdateRecorder`, logs what it could not fix, and is idempotent. It is tested with `SchematicTestRunner` against before-and-after fixtures.
code
json · 16 lines{
"schematics": {
"rename-table-config": {
"version": "4.0.0",
"factory": "./rename-table-config/index#migrate",
"description": "Rename provideTableConfig() to provideDataTable()."
},
"adopt-virtual-scroll": {
"version": "4.1.0",
"factory": "./adopt-virtual-scroll/index#migrate",
"description": "Switch long tables to virtual scrolling.",
"optional": true,
"recommended": true
}
}
}go deeper
Know that libraries can ship code migrations which ng update runs for you.
Know the wiring: ng-update.migrations in package.json, a migration collection, and a version on each entry.
Author safe migrations: AST-based edits through UpdateRecorder, idempotence, honest warnings, optional versus required, and SchematicTestRunner tests.
Set a library team's breaking-change policy: which changes get automated migrations, how long they are kept, and how migration coverage gates a release.
## Why ship a migration at all When a library renames an export, changes a function's signature or moves a configuration option, every consumer has to edit their code. A **migration schematic** does those edits for them during `ng update`, the same mechanism Angular itself and `@schematics/angular` use for their own breaking changes. For a widely used internal or public library, it turns a breaking change from a support burden into a routine update. ## The wiring Three pieces connect a release to its migrations: 1. **`package.json`** of the published library: - `"ng-update": { "migrations": "./migrations/migration-collection.json" }`; - optionally `packageGroup`, the list of sibling packages that should be updated together (this is how Angular's framework packages move as a set). 2. **The migration collection**, a `collection.json`-format file whose `schematics` entries each have: - `version`: the library version that needs this migration, for example `"4.0.0"`; - `factory` and `description`, as in any collection; - optionally `optional: true` (not run automatically; offered in a prompt) and `recommended: true` (pre-selected in that prompt). 3. **The rule factory** each entry points at, compiled to JS and shipped in the package. The CLI's own collection is a live model: `@schematics/angular`'s `migration-collection.json` lists entries at `version` `22.0.0`. One example is `use-application-builder`, marked `optional` and `recommended`, with a `documentation` link. ## Which migrations run The version selection is `ng update`'s job, but an author has to know the rule to pick the right `version`. The CLI runs an entry when its `version` is **greater than the version being updated from and no greater than the version being updated to**. So a consumer going from 3.4.0 to 4.1.0 gets every migration versioned 4.0.0 and 4.1.0, but not one marked 3.4.0, which they already passed. Required migrations run in version order. Optional ones are listed with a checkbox prompt in an interactive terminal, or printed with the `ng update <pkg> --name <migration>` command that runs them later. ## Writing the rule | Concern | Practice | |---|---| | Finding usages | Walk the tree (`tree.getDir('/').visit(...)` or `tree.visit`), skip `node_modules`, parse `.ts` files with the TypeScript compiler API rather than regexes | | Editing | `tree.beginUpdate(path)`, `remove`/`insertRight` at AST positions, then `tree.commitUpdate(recorder)` | | Idempotence | Re-running must be a no-op: check for the new form before inserting it | | Honesty | Log with `context.logger.warn` every usage it could not migrate, with file and line, so the user can finish by hand | | Config files | Use `@schematics/angular/utility`'s `updateWorkspace` for `angular.json` changes rather than string edits | | Dependencies | `addDependency` for a newly required peer; installs are scheduled after the write | Test each migration with `SchematicTestRunner`. Seed a tree with a fixture holding the old API, run the migration by name, and assert the new source. Also assert that a second run changes nothing, and that files it must not touch are untouched. A minimal test looks like this: ```ts import { HostTree } from '@angular-devkit/schematics'; import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; const runner = new SchematicTestRunner('migrations', require.resolve('../migration-collection.json')); it('renames the provider and is idempotent', async () => { const input = new UnitTestTree(new HostTree()); input.create('/src/app/app.config.ts', 'providers: [provideTableConfig({})]'); const once = await runner.runSchematic('rename-table-config', {}, input); expect(once.readContent('/src/app/app.config.ts')).toContain('provideDataTable('); const twice = await runner.runSchematic('rename-table-config', {}, once); expect(twice.readContent('/src/app/app.config.ts')).toBe(once.readContent('/src/app/app.config.ts')); }); ``` ## Release discipline - **One migration per breaking change.** Small, named migrations are easier to review, to mark optional and to re-run individually. - **Version it to the release that breaks.** A wrong `version` means the migration never runs for the people who need it, or runs for people who already migrated. - **Never rely on it alone.** Document the change in the changelog too; some code, such as dynamic usage or templates built from strings, cannot be migrated automatically. - **Keep old migrations.** Consumers skipping several releases in one `ng update` still need the earlier entries, because the range covers all of them. ## What this question is not about The mechanics of `ng update` itself belong to the update command: resolving versions, one major per step, `--migrate-only` with `--from`/`--to`, `--create-commits`. So do the specific edits Angular's own framework migrations make. An interviewer asking this question wants to hear that migrations are ordinary schematics with a version gate, and that authoring them well means safe, idempotent, tested Tree edits.
- A consumer updates your library from 3.2.0 to 5.0.0 in one step. Which of your migrations run?Every entry whose `version` is greater than 3.2.0 and at most 5.0.0, so the 4.x migrations as well as the 5.0.0 ones, required ones in version order. (The one-major-per-step rule applies to `@angular/*` packages, not to your library.) That is why old migrations must stay in the collection instead of being deleted after a release.
- When should a migration be marked `optional`?When it adopts a recommended new pattern rather than fixing code that would otherwise break, like the CLI's own move to the application builder. Optional migrations are offered in a prompt, pre-checked if `recommended`, and can be run later by name. Breaking-change fixes should stay required.
saying these in an interview costs you the question
- Believes ng update runs a library's migrations regardless of their version field
- Deletes last major's migrations once the new major is published
- Rewrites consumer code with broad regexes and no idempotence check
- Thinks a migration schematic needs a different API from a generator
- Relies on the migration alone and skips the changelog entry