Skip to content

Commit ed8f701

Browse files
crisbetothePunderWoman
authored andcommitted
docs(core): add readme for standalone migration (#48848)
Updates the readme for the standalone migration to describe the process and the different transformations that are happening. PR Close #48848
1 parent 345e737 commit ed8f701

File tree

1 file changed

+331
-1
lines changed
  • packages/core/schematics/ng-generate/standalone-migration

1 file changed

+331
-1
lines changed
Lines changed: 331 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,332 @@
11
# 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

Comments
 (0)