|
1 | 1 | # Standalone migration |
2 | | -TODO |
| 2 | +`ng generate` schematic that helps users to convert an application to `standalone` components, |
| 3 | +directives and pipes. The migration can be run with `ng generate @angular/core:standalone` and it |
| 4 | +has the following options: |
| 5 | + |
| 6 | +* `mode` - Configures the mode that migration should run in. The different modes are clarified |
| 7 | +further down in this document. |
| 8 | +* `path` - Relative path within the project that the migration should apply to. Can be used to |
| 9 | +migrate specific subdirectories individually. Defaults to the project root. |
| 10 | + |
| 11 | +## Migration flow |
| 12 | +The standalone migration involves multiple distinct operations, and as such has to be run multiple |
| 13 | +times. Authors should verify that the app still works between each of the steps. If the application |
| 14 | +is large, it can be easier to use the `path` option to migrate specific subsections of the app |
| 15 | +individually. |
| 16 | + |
| 17 | +**Note:** The schematic often needs to generate new code or copy existing code to different places. |
| 18 | +This means that likely the formatting won't match your app anymore and there may be some lint |
| 19 | +failures. The application should compile, but it's expected that the author will fix up any |
| 20 | +formatting and linting failures. |
| 21 | + |
| 22 | +An example migration could look as follows: |
| 23 | +1. `ng generate @angular/core:standalone`. |
| 24 | +2. Select the "Convert all components, directives and pipes to standalone" option. |
| 25 | +3. Verify that the app works and commit the changes. |
| 26 | +4. `ng generate @angular/core:standalone`. |
| 27 | +5. Select the "Remove unnecessary NgModule classes" option. |
| 28 | +6. Verify that the app works and commit the changes. |
| 29 | +7. `ng generate @angular/core:standalone`. |
| 30 | +8. Select the "Bootstrap the application using standalone APIs" option. |
| 31 | +9. Verify that the app works and commit the changes. |
| 32 | +10. Run your linting and formatting checks, and fix any failures. Commit the result. |
| 33 | + |
| 34 | +## Migration modes |
| 35 | +The migration is made up the following modes that are intended to be run in the order they are |
| 36 | +listed in: |
| 37 | +1. Convert declarations to standalone. |
| 38 | +2. Remove unnecessary NgModules. |
| 39 | +3. Switch to standalone bootstrapping API. |
| 40 | + |
| 41 | +### Convert declarations to standalone |
| 42 | +In this mode, the migration will find all of the components, directives and pipes, and convert them |
| 43 | +to standalone by setting `standalone: true` and adding any dependencies to the `imports` array. |
| 44 | + |
| 45 | +**Note:** NgModules which bootstrap a component are explicitly ignored in this step, because they |
| 46 | +are likely to be root modules and they would have to be bootstrapped using `bootstrapApplication` |
| 47 | +instead of `bootstrapModule`. Their declarations will be converted automatically as a part of the |
| 48 | +"Switch to standalone bootstrapping API" step. |
| 49 | + |
| 50 | +**Before:** |
| 51 | +```typescript |
| 52 | +// app.module.ts |
| 53 | +@NgModule({ |
| 54 | + imports: [CommonModule], |
| 55 | + declarations: [MyComp, MyDir, MyPipe] |
| 56 | +}) |
| 57 | +export class AppModule {} |
| 58 | +``` |
| 59 | + |
| 60 | +```typescript |
| 61 | +// my-comp.ts |
| 62 | +@Component({ |
| 63 | + selector: 'my-comp', |
| 64 | + template: '<div my-dir *ngIf="showGreeting">{{ "Hello" | myPipe }}</div>', |
| 65 | +}) |
| 66 | +export class MyComp {} |
| 67 | +``` |
| 68 | + |
| 69 | +```typescript |
| 70 | +// my-dir.ts |
| 71 | +@Directive({selector: '[my-dir]'}) |
| 72 | +export class MyDir {} |
| 73 | +``` |
| 74 | + |
| 75 | +```typescript |
| 76 | +// my-pipe.ts |
| 77 | +@Pipe({name: 'myPipe', pure: true}) |
| 78 | +export class MyPipe {} |
| 79 | +``` |
| 80 | + |
| 81 | +**After:** |
| 82 | +```typescript |
| 83 | +// app.module.ts |
| 84 | +@NgModule({ |
| 85 | + imports: [CommonModule, MyComp, MyDir, MyPipe] |
| 86 | +}) |
| 87 | +export class AppModule {} |
| 88 | +``` |
| 89 | + |
| 90 | +```typescript |
| 91 | +// my-comp.ts |
| 92 | +@Component({ |
| 93 | + selector: 'my-comp', |
| 94 | + template: '<div my-dir *ngIf="showGreeting">{{ "Hello" | myPipe }}</div>', |
| 95 | + standalone: true, |
| 96 | + imports: [NgIf, MyDir, MyPipe] |
| 97 | +}) |
| 98 | +export class MyComp {} |
| 99 | +``` |
| 100 | + |
| 101 | +```typescript |
| 102 | +// my-dir.ts |
| 103 | +@Directive({selector: '[my-dir]', standalone: true}) |
| 104 | +export class MyDir {} |
| 105 | +``` |
| 106 | + |
| 107 | +```typescript |
| 108 | +// my-pipe.ts |
| 109 | +@Pipe({name: 'myPipe', pure: true, standalone: true}) |
| 110 | +export class MyPipe {} |
| 111 | +``` |
| 112 | + |
| 113 | +### Remove unnecessary NgModules |
| 114 | +After converting all declarations to standalone, a lot of NgModules won't be necessary anymore! |
| 115 | +This step identifies such modules and deletes them, including as many references to them, as |
| 116 | +possible. If a module reference can't be deleted automatically, the migration will leave a TODO |
| 117 | +comment saying `TODO(standalone-migration): clean up removed NgModule reference manually` so that |
| 118 | +the author can delete it themselves. |
| 119 | + |
| 120 | +A module is considered "safe to remove" if it: |
| 121 | +* Has no `declarations`. |
| 122 | +* Has no `providers`. |
| 123 | +* Has no `bootstrap` components. |
| 124 | +* Has no `imports` that reference a `ModuleWithProviders` symbol. |
| 125 | +* Has no class members. Empty construstors are ignored. |
| 126 | + |
| 127 | +**Before:** |
| 128 | +```typescript |
| 129 | +// declarer.module.ts |
| 130 | + |
| 131 | +@NgModule({ |
| 132 | + declarations: [FooComp, BarPipe], |
| 133 | + exports: [FooComp, BarPipe] |
| 134 | +}) |
| 135 | +export class DeclarerModule {} |
| 136 | +``` |
| 137 | + |
| 138 | +```typescript |
| 139 | +// configurer.module.ts |
| 140 | +import {DeclarerModule} from './declarer.module'; |
| 141 | + |
| 142 | +console.log(DeclarerModule); |
| 143 | + |
| 144 | +@NgModule({ |
| 145 | + imports: [DeclarerModule], |
| 146 | + exports: [DeclarerModule], |
| 147 | + providers: [{provide: FOO, useValue: 123}] |
| 148 | +}) |
| 149 | +export class ConfigurerModule {} |
| 150 | +``` |
| 151 | + |
| 152 | +```typescript |
| 153 | +// index.ts |
| 154 | +export {DeclarerModule, ConfigurerModule} from './modules/index'; |
| 155 | +``` |
| 156 | + |
| 157 | +**After:** |
| 158 | +```typescript |
| 159 | +// declarer.module.ts |
| 160 | +// Deleted! |
| 161 | +``` |
| 162 | + |
| 163 | +```typescript |
| 164 | +// configurer.module.ts |
| 165 | +console.log(/* TODO(standalone-migration): clean up removed NgModule reference manually */ DeclarerModule); |
| 166 | + |
| 167 | +@NgModule({ |
| 168 | + imports: [], |
| 169 | + exports: [], |
| 170 | + providers: [{provide: FOO, useValue: 123}] |
| 171 | +}) |
| 172 | +export class ConfigurerModule {} |
| 173 | +``` |
| 174 | + |
| 175 | +```typescript |
| 176 | +// index.ts |
| 177 | +export {DeclarerModule} from './modules/index'; |
| 178 | +``` |
| 179 | + |
| 180 | +### Switch to standalone bootstrapping API |
| 181 | +Converts any usages of the old `bootstrapModule` API to the new `bootstrapApplication`. To do this |
| 182 | +in a safe way, the migration has to make the following changes to the application's code: |
| 183 | +1. Generate the `bootstrapApplication` call to replace the `bootstrapModule` one. |
| 184 | +2. Convert the `declarations` of the module that is being bootstrapped to `standalone`. These |
| 185 | +modules were skipped explicitly in the first step of the migration. |
| 186 | +3. Copy any `providers` from the bootstrapped module into the `providers` option of |
| 187 | +`bootstrapApplication`. |
| 188 | +4. Copy any classes from the `imports` array of the rootModule to the `providers` option of |
| 189 | +`bootstrapApplication` and wrap them in an `importsProvidersFrom` function call. |
| 190 | +5. Adjust any dynamic import paths so that they're correct when they're copied over. |
| 191 | +6. If an API with a standalone equivalent is detected, it may be converted automatically as well. |
| 192 | +E.g. `RouterModule.forRoot` will become `provideRouter`. |
| 193 | +7. Comment out the module metadata of the root class and leave a TODO to remove it. This can also |
| 194 | +be done automatically by running the "Remove unnecessary NgModules" step again. |
| 195 | + |
| 196 | +If the migration detects that the `providers` or `imports` of the root module are referencing code |
| 197 | +outside of the class declaration, it will attempt to carry over as much of it as it can to the new |
| 198 | +location. If some of that code is exported, it will be imported in the new location, otherwise it |
| 199 | +will be copied over. |
| 200 | + |
| 201 | +**Before:** |
| 202 | +```typescript |
| 203 | +// ./app/app.module.ts |
| 204 | +import {NgModule, InjectionToken} from '@angular/core'; |
| 205 | +import {RouterModule} from '@angular/router'; |
| 206 | +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; |
| 207 | +import {AppComponent} from './app.component.ts'; |
| 208 | +import {SharedModule} from './shared.module'; |
| 209 | +import {ImportedInterface} from './some-interface'; |
| 210 | +import {CONFIG} from './config'; |
| 211 | + |
| 212 | +interface NonImportedInterface { |
| 213 | + foo: any; |
| 214 | + baz: ImportedInterface; |
| 215 | +} |
| 216 | + |
| 217 | +const token = new InjectionToken<NonImportedInterface>('token'); |
| 218 | + |
| 219 | +export class ExportedConfigClass {} |
| 220 | + |
| 221 | +@NgModule({ |
| 222 | + imports: [ |
| 223 | + SharedModule, |
| 224 | + BrowserAnimationsModule, |
| 225 | + RouterModule.forRoot([{ |
| 226 | + path: 'shop', |
| 227 | + loadComponent: () => import('./shop/shop.component').then(m => m.ShopComponent) |
| 228 | + }]) |
| 229 | + ], |
| 230 | + declarations: [AppComponent], |
| 231 | + bootstrap: [AppComponent], |
| 232 | + providers: [ |
| 233 | + {provide: token, useValue: {foo: true, bar: {baz: false}}}, |
| 234 | + {provide: CONFIG, useClass: ExportedConfigClass} |
| 235 | + ] |
| 236 | +}) |
| 237 | +export class AppModule {} |
| 238 | +``` |
| 239 | + |
| 240 | +```typescript |
| 241 | +// ./app/app.component.ts |
| 242 | +@Component({selector: 'app', template: 'hello'}) |
| 243 | +export class AppComponent {} |
| 244 | +``` |
| 245 | + |
| 246 | +```typescript |
| 247 | +// ./main.ts |
| 248 | +import {platformBrowser} from '@angular/platform-browser'; |
| 249 | +import {AppModule} from './app/app.module'; |
| 250 | + |
| 251 | +platformBrowser().bootstrapModule(AppModule).catch(e => console.error(e)); |
| 252 | +``` |
| 253 | + |
| 254 | +**After:** |
| 255 | +```typescript |
| 256 | +// ./app/app.module.ts |
| 257 | +import {NgModule, InjectionToken} from '@angular/core'; |
| 258 | +import {RouterModule} from '@angular/router'; |
| 259 | +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; |
| 260 | +import {AppComponent} from './app.component.ts'; |
| 261 | +import {SharedModule} from '../shared/shared.module'; |
| 262 | +import {ImportedInterface} from './some-interface'; |
| 263 | +import {CONFIG} from './config'; |
| 264 | + |
| 265 | +interface NonImportedInterface { |
| 266 | + foo: any; |
| 267 | + bar: ImportedInterface; |
| 268 | +} |
| 269 | + |
| 270 | +const token = new InjectionToken<NonImportedInterface>('token'); |
| 271 | + |
| 272 | +export class ExportedConfigClass {} |
| 273 | + |
| 274 | +@NgModule(/* |
| 275 | +TODO(standalone-migration): clean up removed NgModule class manually or run the "Remove unnecessary NgModule classes" step of the migration again. |
| 276 | +{ |
| 277 | + imports: [ |
| 278 | + SharedModule, |
| 279 | + BrowserAnimationsModule, |
| 280 | + RouterModule.forRoot([{ |
| 281 | + path: 'shop', |
| 282 | + loadComponent: () => import('./shop/shop.component').then(m => m.ShopComponent) |
| 283 | + }]) |
| 284 | + ], |
| 285 | + declarations: [AppComponent], |
| 286 | + bootstrap: [AppComponent], |
| 287 | + providers: [ |
| 288 | + {provide: token, useValue: {foo: true, bar: {baz: false}}}, |
| 289 | + {provide: CONFIG, useClass: ExportedConfigClass} |
| 290 | + ] |
| 291 | +}*/) |
| 292 | +export class AppModule {} |
| 293 | +``` |
| 294 | + |
| 295 | +```typescript |
| 296 | +// ./app/app.component.ts |
| 297 | +@Component({selector: 'app', template: 'hello', standalone: true}) |
| 298 | +export class AppComponent {} |
| 299 | +``` |
| 300 | + |
| 301 | +```typescript |
| 302 | +// ./main.ts |
| 303 | +import {platformBrowser, bootstrapApplication} from '@angular/platform-browser'; |
| 304 | +import {InjectionToken, importProvidersFrom} from '@angular/core'; |
| 305 | +import {provideRouter} from '@angular/router'; |
| 306 | +import {provideAnimations} from '@angular/platform-browser/animations'; |
| 307 | +import {AppModule, ExportedConfigClass} from './app/app.module'; |
| 308 | +import {AppComponent} from './app/app.component'; |
| 309 | +import {CONFIG} from './app/config'; |
| 310 | +import {SharedModule} from './shared/shared.module'; |
| 311 | +import {ImportedInterface} from './app/some-interface'; |
| 312 | + |
| 313 | +interface NonImportedInterface { |
| 314 | + foo: any; |
| 315 | + bar: ImportedInterface; |
| 316 | +} |
| 317 | + |
| 318 | +const token = new InjectionToken<NonImportedInterface>('token'); |
| 319 | + |
| 320 | +bootstrapApplication(AppComponent, { |
| 321 | + providers: [ |
| 322 | + importProvidersFrom(SharedModule), |
| 323 | + {provide: token, useValue: {foo: true, bar: {baz: false}}}, |
| 324 | + {provide: CONFIG, useClass: ExportedConfigClass}, |
| 325 | + provideAnimations(), |
| 326 | + provideRouter([{ |
| 327 | + path: 'shop', |
| 328 | + loadComponent: () => import('./app/shop/shop.component').then(m => m.ShopComponent) |
| 329 | + }]) |
| 330 | + ] |
| 331 | +}).catch(e => console.error(e)); |
| 332 | +``` |
0 commit comments