Skip to content

Commit 147eee4

Browse files
eneajahothePunderWoman
authored andcommitted
feat(migrations): add migration to convert standalone component routes to be lazy loaded (angular#56428)
This schematic helps developers to convert eagerly loaded component routes to lazy loaded routes PR Close angular#56428
1 parent b558f99 commit 147eee4

File tree

12 files changed

+1567
-0
lines changed

12 files changed

+1567
-0
lines changed

packages/core/schematics/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pkg_npm(
1414
"package.json",
1515
"//packages/core/schematics/ng-generate/control-flow-migration:static_files",
1616
"//packages/core/schematics/ng-generate/inject-migration:static_files",
17+
"//packages/core/schematics/ng-generate/route-lazy-loading:static_files",
1718
"//packages/core/schematics/ng-generate/standalone-migration:static_files",
1819
],
1920
validate = False,
@@ -24,6 +25,7 @@ pkg_npm(
2425
"//packages/core/schematics/migrations/invalid-two-way-bindings:bundle",
2526
"//packages/core/schematics/ng-generate/control-flow-migration:bundle",
2627
"//packages/core/schematics/ng-generate/inject-migration:bundle",
28+
"//packages/core/schematics/ng-generate/route-lazy-loading:bundle",
2729
"//packages/core/schematics/ng-generate/standalone-migration:bundle",
2830
],
2931
)

packages/core/schematics/collection.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@
2323
"aliases": [
2424
"inject"
2525
]
26+
},
27+
"route-lazy-loading-migration": {
28+
"description": "Updates route definitions to use lazy-loading of components instead of eagerly referencing them",
29+
"factory": "./ng-generate/route-lazy-loading/bundle",
30+
"schema": "./ng-generate/route-lazy-loading/schema.json",
31+
"aliases": ["route-lazy-loading"]
2632
}
2733
}
2834
}
35+
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
load("//tools:defaults.bzl", "esbuild", "ts_library")
2+
3+
package(
4+
default_visibility = [
5+
"//packages/core/schematics:__pkg__",
6+
"//packages/core/schematics/migrations/google3:__pkg__",
7+
"//packages/core/schematics/test:__pkg__",
8+
],
9+
)
10+
11+
filegroup(
12+
name = "static_files",
13+
srcs = ["schema.json"],
14+
)
15+
16+
ts_library(
17+
name = "route-lazy-loading",
18+
srcs = glob(["**/*.ts"]),
19+
tsconfig = "//packages/core/schematics:tsconfig.json",
20+
deps = [
21+
"//packages/compiler-cli",
22+
"//packages/compiler-cli/private",
23+
"//packages/core/schematics/utils",
24+
"@npm//@angular-devkit/schematics",
25+
"@npm//@types/node",
26+
"@npm//typescript",
27+
],
28+
)
29+
30+
esbuild(
31+
name = "bundle",
32+
entry_point = ":index.ts",
33+
external = [
34+
"@angular-devkit/*",
35+
"typescript",
36+
],
37+
format = "cjs",
38+
platform = "node",
39+
deps = [":route-lazy-loading"],
40+
)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Route lazy loading migration
2+
This schematic helps developers to convert eagerly loaded component routes to lazy loaded routes.
3+
By lazy loading components we can split the production bundle into smaller chunks,
4+
to avoid big JS bundle that includes all routes, which negatively affects initial page load of an application.
5+
6+
## How to run this migration?
7+
The migration can be run using the following command:
8+
9+
```bash
10+
ng generate @angular/core:route-lazy-loading
11+
```
12+
13+
By default, migration will go over the entire application. If you want to apply this migration to a subset of the files, you can pass the path argument as shown below:
14+
15+
```bash
16+
ng generate @angular/core:route-lazy-loading --path src/app/sub-component
17+
```
18+
19+
The value of the path parameter is a relative path within the project.
20+
21+
### How does it work?
22+
The schematic will attempt to find all the places where the application routes as defined:
23+
- `RouterModule.forRoot` and `RouterModule.forChild`
24+
- `Router.resetConfig`
25+
- `provideRouter`
26+
- `provideRoutes`
27+
- variables of type `Routes` or `Route[]` (e.g. `const routes: Routes = [{...}]`)
28+
29+
The migration will check all the components in the routes, check if they are standalone and eagerly loaded, and if so, it will convert them to lazy loaded routes.
30+
31+
**Before:**
32+
```typescript
33+
// app.module.ts
34+
import { HomeComponent } from './home/home.component';
35+
36+
@NgModule({
37+
imports: [
38+
RouterModule.forRoot([
39+
{
40+
path: 'home',
41+
component: HomeComponent, // HomeComponent is standalone and eagerly loaded
42+
},
43+
]),
44+
],
45+
})
46+
export class AppModule {}
47+
```
48+
**After:**
49+
```typescript
50+
// app.module.ts
51+
@NgModule({
52+
imports: [
53+
RouterModule.forRoot([
54+
{
55+
path: 'home',
56+
// ↓ HomeComponent is now lazy loaded
57+
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent),
58+
},
59+
]),
60+
],
61+
})
62+
export class AppModule {}
63+
```
64+
65+
> This migration will also collect information about all the components declared in NgModules
66+
and output the list of routes that use them (including corresponding location of the file).
67+
Consider making those components standalone and run this migration again.
68+
You can use an existing migration (see https://angular.dev/reference/migrations/standalone)
69+
to convert those components to standalone.
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
10+
import {existsSync, statSync} from 'fs';
11+
import {join, relative} from 'path';
12+
13+
import {normalizePath} from '../../utils/change_tracker';
14+
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
15+
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
16+
17+
import {RouteMigrationData, migrateFileToLazyRoutes} from './to-lazy-routes';
18+
19+
interface Options {
20+
path: string;
21+
}
22+
23+
export default function (options: Options): Rule {
24+
return async (tree, context) => {
25+
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
26+
const basePath = process.cwd();
27+
const allPaths = [...buildPaths, ...testPaths];
28+
// TS and Schematic use paths in POSIX format even on Windows. This is needed as otherwise
29+
// string matching such as `sourceFile.fileName.startsWith(pathToMigrate)` might not work.
30+
const pathToMigrate = normalizePath(join(basePath, options.path));
31+
32+
if (!allPaths.length) {
33+
throw new SchematicsException(
34+
'Could not find any tsconfig file. Cannot run the route lazy loading migration.',
35+
);
36+
}
37+
38+
let migratedRoutes: RouteMigrationData[] = [];
39+
let skippedRoutes: RouteMigrationData[] = [];
40+
41+
for (const tsconfigPath of allPaths) {
42+
const {migratedRoutes: migrated, skippedRoutes: skipped} = standaloneRoutesMigration(
43+
tree,
44+
tsconfigPath,
45+
basePath,
46+
pathToMigrate,
47+
options,
48+
);
49+
50+
migratedRoutes.push(...migrated);
51+
skippedRoutes.push(...skipped);
52+
}
53+
54+
if (migratedRoutes.length === 0 && skippedRoutes.length === 0) {
55+
throw new SchematicsException(
56+
`Could not find any files to migrate under the path ${pathToMigrate}.`,
57+
);
58+
}
59+
60+
context.logger.info('🎉 Automated migration step has finished! 🎉');
61+
62+
context.logger.info(`Number of updated routes: ${migratedRoutes.length}`);
63+
context.logger.info(`Number of skipped routes: ${skippedRoutes.length}`);
64+
65+
if (skippedRoutes.length > 0) {
66+
context.logger.info(
67+
`Note: this migration was unable to optimize the following routes, since they use components declared in NgModules:`,
68+
);
69+
70+
for (const route of skippedRoutes) {
71+
context.logger.info(`- \`${route.path}\` path at \`${route.file}\``);
72+
}
73+
74+
context.logger.info(
75+
`Consider making those components standalone and run this migration again. More information about standalone migration can be found at https://angular.dev/reference/migrations/standalone`,
76+
);
77+
}
78+
79+
context.logger.info(
80+
'IMPORTANT! Please verify manually that your application builds and behaves as expected.',
81+
);
82+
context.logger.info(
83+
`See https://angular.dev/reference/migrations/route-lazy-loading for more information.`,
84+
);
85+
};
86+
}
87+
88+
function standaloneRoutesMigration(
89+
tree: Tree,
90+
tsconfigPath: string,
91+
basePath: string,
92+
pathToMigrate: string,
93+
schematicOptions: Options,
94+
): {migratedRoutes: RouteMigrationData[]; skippedRoutes: RouteMigrationData[]} {
95+
if (schematicOptions.path.startsWith('..')) {
96+
throw new SchematicsException(
97+
'Cannot run route lazy loading migration outside of the current project.',
98+
);
99+
}
100+
101+
if (existsSync(pathToMigrate) && !statSync(pathToMigrate).isDirectory()) {
102+
throw new SchematicsException(
103+
`Migration path ${pathToMigrate} has to be a directory. Cannot run the route lazy loading migration.`,
104+
);
105+
}
106+
107+
const program = createMigrationProgram(tree, tsconfigPath, basePath);
108+
const sourceFiles = program
109+
.getSourceFiles()
110+
.filter(
111+
(sourceFile) =>
112+
sourceFile.fileName.startsWith(pathToMigrate) &&
113+
canMigrateFile(basePath, sourceFile, program),
114+
);
115+
116+
const migratedRoutes: RouteMigrationData[] = [];
117+
const skippedRoutes: RouteMigrationData[] = [];
118+
119+
if (sourceFiles.length === 0) {
120+
return {migratedRoutes, skippedRoutes};
121+
}
122+
123+
for (const sourceFile of sourceFiles) {
124+
const {
125+
pendingChanges,
126+
skippedRoutes: skipped,
127+
migratedRoutes: migrated,
128+
} = migrateFileToLazyRoutes(sourceFile, program);
129+
130+
skippedRoutes.push(...skipped);
131+
migratedRoutes.push(...migrated);
132+
133+
const update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
134+
135+
pendingChanges.forEach((change) => {
136+
if (change.removeLength != null) {
137+
update.remove(change.start, change.removeLength);
138+
}
139+
update.insertRight(change.start, change.text);
140+
});
141+
142+
tree.commitUpdate(update);
143+
}
144+
145+
return {migratedRoutes, skippedRoutes};
146+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema",
3+
"$id": "AngularStandaloneRoutesMigration",
4+
"title": "Angular Standalone Routes Migration Schema",
5+
"type": "object",
6+
"properties": {
7+
"path": {
8+
"type": "string",
9+
"description": "Path relative to the project root which should be migrated",
10+
"x-prompt": "Which path in your project should be migrated?",
11+
"default": "./"
12+
}
13+
}
14+
}

0 commit comments

Comments
 (0)