packages/core/schematics/ng-generate/standalone-migration/README.md
ng generate schematic that helps users to convert an application to standalone components,
directives and pipes. The migration can be run with ng generate @angular/core:standalone and it
has the following options:
mode - Configures the mode that migration should run in. The different modes are clarified
further down in this document.path - Relative path within the project that the migration should apply to. Can be used to
migrate specific sub-directories individually. Defaults to the project root.The standalone migration involves multiple distinct operations, and as such has to be run multiple
times. Authors should verify that the app still works between each of the steps. If the application
is large, it can be easier to use the path option to migrate specific sub-sections of the app
individually.
Note: The schematic often needs to generate new code or copy existing code to different places. This means that likely the formatting won't match your app anymore and there may be some lint failures. The application should compile, but it's expected that the author will fix up any formatting and linting failures.
An example migration could look as follows:
ng generate @angular/core:standalone.ng generate @angular/core:standalone.ng generate @angular/core:standalone.The migration is made up the following modes that are intended to be run in the order they are listed in:
In this mode, the migration will find all of the components, directives and pipes, and convert them
to standalone by removing standalone: false and adding any dependencies to the imports array.
Note: NgModules which bootstrap a component are explicitly ignored in this step, because they
are likely to be root modules and they would have to be bootstrapped using bootstrapApplication
instead of bootstrapModule. Their declarations will be converted automatically as a part of the
"Switch to standalone bootstrapping API" step.
Before:
// app.module.ts
@NgModule({
imports: [CommonModule],
declarations: [MyComp, MyDir, MyPipe],
})
export class AppModule {}
// my-comp.ts
@Component({
selector: 'my-comp',
template: '<div my-dir *ngIf="showGreeting">{{ "Hello" | myPipe }}</div>',
standalone: false,
})
export class MyComp {
public showGreeting = true;
}
// my-dir.ts
@Directive({selector: '[my-dir]', standalone: false})
export class MyDir {}
// my-pipe.ts
@Pipe({name: 'myPipe', pure: true, standalone: false})
export class MyPipe {}
After:
// app.module.ts
@NgModule({
imports: [CommonModule, MyComp, MyDir, MyPipe],
})
export class AppModule {}
// my-comp.ts
@Component({
selector: 'my-comp',
template: '<div my-dir *ngIf="showGreeting">{{ "Hello" | myPipe }}</div>',
imports: [NgIf, MyDir, MyPipe],
})
export class MyComp {
public showGreeting = true;
}
// my-dir.ts
@Directive({selector: '[my-dir]'})
export class MyDir {}
// my-pipe.ts
@Pipe({name: 'myPipe', pure: true})
export class MyPipe {}
After converting all declarations to standalone, a lot of NgModules won't be necessary anymore!
This step identifies such modules and deletes them, including as many references to them, as
possible. If a module reference can't be deleted automatically, the migration will leave a TODO
comment saying TODO(standalone-migration): clean up removed NgModule reference manually so that
the author can delete it themselves.
A module is considered "safe to remove" if it:
declarations.providers.bootstrap components.imports that reference a ModuleWithProviders symbol or a module that can't be removed.Before:
// importer.module.ts
@NgModule({
imports: [FooComp, BarPipe],
exports: [FooComp, BarPipe],
})
export class ImporterModule {}
// configurer.module.ts
import {ImporterModule} from './importer.module';
console.log(ImporterModule);
@NgModule({
imports: [ImporterModule],
exports: [ImporterModule],
providers: [{provide: FOO, useValue: 123}],
})
export class ConfigurerModule {}
// index.ts
export {ImporterModule, ConfigurerModule} from './modules/index';
After:
// importer.module.ts
// Deleted!
// configurer.module.ts
console.log(
/* TODO(standalone-migration): clean up removed NgModule reference manually */ ImporterModule,
);
@NgModule({
imports: [],
exports: [],
providers: [{provide: FOO, useValue: 123}],
})
export class ConfigurerModule {}
// index.ts
export {ConfigurerModule} from './modules/index';
Converts any usages of the old bootstrapModule API to the new bootstrapApplication. To do this
in a safe way, the migration has to make the following changes to the application's code:
bootstrapApplication call to replace the bootstrapModule one.declarations of the module that is being bootstrapped to standalone. These
modules were skipped explicitly in the first step of the migration.providers from the bootstrapped module into the providers option of
bootstrapApplication.imports array of the rootModule to the providers option of
bootstrapApplication and wrap them in an importsProvidersFrom function call.RouterModule.forRoot will become provideRouter.If the migration detects that the providers or imports of the root module are referencing code
outside of the class declaration, it will attempt to carry over as much of it as it can to the new
location. If some of that code is exported, it will be imported in the new location, otherwise it
will be copied over.
Before:
// ./app/app.module.ts
import {NgModule, InjectionToken} from '@angular/core';
import {RouterModule} from '@angular/router';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {AppComponent} from './app.component.ts';
import {SharedModule} from './shared.module';
import {ImportedInterface} from './some-interface';
import {CONFIG} from './config';
interface NonImportedInterface {
foo: any;
baz: ImportedInterface;
}
const token = new InjectionToken<NonImportedInterface>('token');
export class ExportedConfigClass {}
@NgModule({
imports: [
SharedModule,
BrowserAnimationsModule,
RouterModule.forRoot(
[
{
path: 'shop',
loadComponent: () => import('./shop/shop.component').then((m) => m.ShopComponent),
},
],
{
initialNavigation: 'enabledBlocking',
},
),
],
declarations: [AppComponent],
bootstrap: [AppComponent],
providers: [
{provide: token, useValue: {foo: true, bar: {baz: false}}},
{provide: CONFIG, useClass: ExportedConfigClass},
],
})
export class AppModule {}
// ./app/app.component.ts
@Component({selector: 'app', template: 'hello', standalone: false})
export class AppComponent {}
// ./main.ts
import {platformBrowser} from '@angular/platform-browser';
import {AppModule} from './app/app.module';
platformBrowser()
.bootstrapModule(AppModule)
.catch((e) => console.error(e));
After:
// ./app/app.module.ts
import {NgModule, InjectionToken} from '@angular/core';
import {RouterModule} from '@angular/router';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {AppComponent} from './app.component.ts';
import {SharedModule} from '../shared/shared.module';
import {ImportedInterface} from './some-interface';
import {CONFIG} from './config';
interface NonImportedInterface {
foo: any;
bar: ImportedInterface;
}
const token = new InjectionToken<NonImportedInterface>('token');
export class ExportedConfigClass {}
// ./app/app.component.ts
@Component({selector: 'app', template: 'hello'})
export class AppComponent {}
// ./main.ts
import {platformBrowser, bootstrapApplication} from '@angular/platform-browser';
import {InjectionToken, importProvidersFrom} from '@angular/core';
import {withEnabledBlockingInitialNavigation, provideRouter} from '@angular/router';
import {provideAnimations} from '@angular/platform-browser/animations';
import {AppModule, ExportedConfigClass} from './app/app.module';
import {AppComponent} from './app/app.component';
import {CONFIG} from './app/config';
import {SharedModule} from './shared/shared.module';
import {ImportedInterface} from './app/some-interface';
interface NonImportedInterface {
foo: any;
bar: ImportedInterface;
}
const token = new InjectionToken<NonImportedInterface>('token');
bootstrapApplication(AppComponent, {
providers: [
importProvidersFrom(SharedModule),
{provide: token, useValue: {foo: true, bar: {baz: false}}},
{provide: CONFIG, useClass: ExportedConfigClass},
provideAnimations(),
provideRouter(
[
{
path: 'shop',
loadComponent: () => import('./app/shop/shop.component').then((m) => m.ShopComponent),
},
],
withEnabledBlockingInitialNavigation(),
),
],
}).catch((e) => console.error(e));