How does Angular's standalone migration convert an NgModule-based app, and why must its three modes run in order?
answer
- one schematic, three passes
- declarations first, modules second
- bootstrap last
- static analysis has blind spots
basics
~20 sAngular's standalone migration runs in three passes: convert components, directives and pipes to standalone, prune NgModules that became empty, then switch bootstrapModule to bootstrapApplication. Each pass relies on the previous one, and each needs a build check between runs.
solid answer
~40 s`ng generate @angular/core:standalone` has a `mode` option with three values, run in order with a build in between. **`convert-to-standalone`** removes `standalone: false` and adds each declaration's dependencies to its own `imports`, but skips NgModules that bootstrap a component. **`prune-ng-modules`** deletes modules that are now safe to remove: no `declarations`, `providers`, `bootstrap` components, class members, or imports of `ModuleWithProviders` or unremovable modules; references it cannot delete get a `TODO(standalone-migration)` comment. **`standalone-bootstrap`** replaces `bootstrapModule` with `bootstrapApplication`, copying root providers and imports where it can. The order matters because pruning only works once declarations are standalone, and bootstrapping is converted last. It needs v15.2+, a compiling project and a clean Git branch, and it can get test `imports` wrong because tests are not AOT-compiled.
code
bash · 6 linesng generate @angular/core:standalone --mode=convert-to-standalone
# build and test, commit
ng generate @angular/core:standalone --mode=prune-ng-modules
# build and test, commit
ng generate @angular/core:standalone --mode=standalone-bootstrap
# build and test, commitgo deeper
Remember that the standalone migration has three modes: convert declarations, prune modules, switch bootstrapping, in that order.
Explain what each mode changes, what makes a module safe to prune, and why the root module waits for the bootstrap step.
Plan for the leftovers: modules with providers, TODO comments, test imports that are wrong because tests are not AOT-compiled.
Decide how far to take the conversion in a large codebase and whether remaining provider-heavy modules are worth rewriting.
Standalone components, directives and pipes declare their own dependencies instead of receiving them through an `NgModule`. Since v19 they are the default: a declaration is standalone unless it says `standalone: false`. Apps written before that are full of `NgModule`s, and the **standalone migration** is the official tool for converting them. ## Prerequisites Before running it, the project must: 1. be on **Angular 15.2.0 or later**; 2. **build without compilation errors**, because the schematic relies on static analysis; 3. be on a **clean Git branch** with all work saved, so each step can be reviewed or reverted. ## The three modes The schematic is `ng generate @angular/core:standalone` (full name `standalone-migration`). Its `mode` option takes one of three values, and the docs require running them in this order, verifying the build between each: | Order | `mode` | What it does | | :-- | :-- | :-- | | 1 | `convert-to-standalone` | Removes `standalone: false` from components, directives and pipes and adds what each needs to its own `imports` array | | 2 | `prune-ng-modules` | Deletes NgModules that are now safe to remove and as many references to them as possible | | 3 | `standalone-bootstrap` | Replaces `bootstrapModule` with `bootstrapApplication`, removes `standalone: false` from the root component and deletes the root module | A `path` option limits a run to part of the project. ### Step 1 in detail A declaration moves out of its module's `declarations` and becomes an entry in the module's `imports` (so other modules still get it through `exports`), and its template dependencies are added to the component's own `imports`, for example `imports: [NgIf]`. Modules that **bootstrap** a component are skipped here, because they are likely root modules handled in step 3. ### Step 2 in detail A module is considered **safe to remove** only if it has: - no `declarations`; - no `providers`; - no `bootstrap` components; - no `imports` that reference a `ModuleWithProviders` symbol or a module that cannot itself be removed; - no class members (an empty constructor is ignored). Where a reference cannot be deleted automatically, the migration leaves `/* TODO(standalone-migration): clean up removed NgModule reference manually */`. Modules with `providers` survive, which is why feature modules that configure services often remain. ### Step 3 in detail The root module's `providers` and `imports` are copied, as far as possible, into the `bootstrapApplication` call, and the root module is deleted. ## Why the order matters - **Pruning depends on conversion.** A module still declaring components is never "safe to remove", so running step 2 first removes almost nothing. - **Bootstrap is the riskiest change.** It touches app start-up and root providers, so it comes last, when the rest of the app already works standalone. - **Each step changes what the next one sees.** Building and testing between runs keeps a failure attributable to one step. ## Known limitations - **Unit tests are not AOT-compiled**, so `imports` the schematic adds to components declared in tests may not be entirely correct. - **Custom wrappers are invisible.** A helper that wraps `TestBed.configureTestingModule` hides the components it declares from the schematic. - **Code that cannot be statically analysed**, such as dynamically built metadata, may be skipped. - **Files not included by any `tsconfig`** are ignored. - **Formatting** of generated code may not match your style rules; run the formatter and linter afterwards. ## After the migration - Remove remaining `NgModule`s by hand where the TODO comments point. - Run the unit tests and fix failures, usually missing test imports. - Consider the follow-up schematics: `cleanup-unused-imports` removes unused standalone imports, and `common-to-standalone` replaces `CommonModule` with individual directive and pipe imports. The design of a mixed app, with standalone components and remaining NgModules working together, is its own topic; the migration's job is to get you there with as little hand editing as possible.
- Why do some NgModules survive Angular's prune-ng-modules step?A module is removed only if it has no declarations, no providers, no bootstrap components, no class members, and no imports of a ModuleWithProviders or of a module that cannot be removed. Feature modules that register providers therefore stay, and references the tool cannot delete get a TODO(standalone-migration) comment.
- Why can the standalone migration get component imports in unit tests wrong?The schematic relies on the compiler's static analysis, and unit tests are not ahead-of-time compiled, so the imports it adds to components declared in tests may be incomplete. A custom wrapper around TestBed.configureTestingModule also hides declarations from it.
It is like dissolving a set of shared toolboxes: first every worker gets their own kit with exactly the tools they use, then the empty boxes are thrown away, and only at the end do you change how the workshop opens in the morning.
saying these in an interview costs you the question
- One run of the standalone schematic converts the whole app in a single pass.
- The prune step deletes every NgModule, including ones with providers.
- The migration works on a project that does not currently compile.
- Running the bootstrap mode first is fine since the order does not matter.
- Test files are migrated as reliably as application code.