@angular/core 19.0.0-next.2 → 19.0.0-next.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/core.mjs +193 -20
- package/fesm2022/core.mjs.map +1 -1
- package/fesm2022/primitives/event-dispatch.mjs +1 -1
- package/fesm2022/primitives/signals.mjs +1 -1
- package/fesm2022/rxjs-interop.mjs +1 -1
- package/fesm2022/testing.mjs +4 -4
- package/index.d.ts +113 -4
- package/package.json +1 -1
- package/primitives/event-dispatch/index.d.ts +1 -1
- package/primitives/signals/index.d.ts +1 -1
- package/rxjs-interop/index.d.ts +1 -1
- package/schematics/bundles/compiler_host-ca7ba733.js +44719 -0
- package/schematics/bundles/control-flow-migration.js +1847 -0
- package/schematics/bundles/explicit-standalone-flag.js +157 -0
- package/schematics/bundles/imports-4ac08251.js +110 -0
- package/schematics/bundles/inject-migration.js +927 -0
- package/schematics/bundles/nodes-0e7d45ca.js +56 -0
- package/schematics/bundles/project_tsconfig_paths-e9ccccbf.js +90 -0
- package/schematics/bundles/route-lazy-loading.js +411 -0
- package/schematics/bundles/standalone-migration.js +22441 -0
- package/schematics/collection.json +7 -14
- package/schematics/migrations.json +4 -14
- package/testing/index.d.ts +1 -1
- package/schematics/migrations/after-render-phase/bundle.js +0 -27333
- package/schematics/migrations/http-providers/bundle.js +0 -27582
- package/schematics/migrations/invalid-two-way-bindings/bundle.js +0 -23808
- package/schematics/ng-generate/control-flow-migration/bundle.js +0 -28076
- package/schematics/ng-generate/inject-migration/bundle.js +0 -27777
- package/schematics/ng-generate/route-lazy-loading/bundle.js +0 -27478
- package/schematics/ng-generate/standalone-migration/bundle.js +0 -52161
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* @license Angular v19.0.0-next.4
|
|
4
|
+
* (c) 2010-2024 Google LLC. https://angular.io/
|
|
5
|
+
* License: MIT
|
|
6
|
+
*/
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
var ts = require('typescript');
|
|
10
|
+
var imports = require('./imports-4ac08251.js');
|
|
11
|
+
|
|
12
|
+
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
13
|
+
|
|
14
|
+
var ts__default = /*#__PURE__*/_interopDefaultLegacy(ts);
|
|
15
|
+
|
|
16
|
+
function getCallDecoratorImport(typeChecker, decorator) {
|
|
17
|
+
// Note that this does not cover the edge case where decorators are called from
|
|
18
|
+
// a namespace import: e.g. "@core.Component()". This is not handled by Ngtsc either.
|
|
19
|
+
if (!ts__default["default"].isCallExpression(decorator.expression) ||
|
|
20
|
+
!ts__default["default"].isIdentifier(decorator.expression.expression)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const identifier = decorator.expression.expression;
|
|
24
|
+
return imports.getImportOfIdentifier(typeChecker, identifier);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Gets all decorators which are imported from an Angular package (e.g. "@angular/core")
|
|
29
|
+
* from a list of decorators.
|
|
30
|
+
*/
|
|
31
|
+
function getAngularDecorators(typeChecker, decorators) {
|
|
32
|
+
return decorators
|
|
33
|
+
.map((node) => ({ node, importData: getCallDecoratorImport(typeChecker, node) }))
|
|
34
|
+
.filter(({ importData }) => importData && importData.importModule.startsWith('@angular/'))
|
|
35
|
+
.map(({ node, importData }) => ({
|
|
36
|
+
node: node,
|
|
37
|
+
name: importData.name,
|
|
38
|
+
moduleName: importData.importModule,
|
|
39
|
+
importNode: importData.node,
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Find the closest parent node of a particular kind. */
|
|
44
|
+
function closestNode(node, predicate) {
|
|
45
|
+
let current = node.parent;
|
|
46
|
+
while (current && !ts__default["default"].isSourceFile(current)) {
|
|
47
|
+
if (predicate(current)) {
|
|
48
|
+
return current;
|
|
49
|
+
}
|
|
50
|
+
current = current.parent;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
exports.closestNode = closestNode;
|
|
56
|
+
exports.getAngularDecorators = getAngularDecorators;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* @license Angular v19.0.0-next.4
|
|
4
|
+
* (c) 2010-2024 Google LLC. https://angular.io/
|
|
5
|
+
* License: MIT
|
|
6
|
+
*/
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
var core = require('@angular-devkit/core');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Gets all tsconfig paths from a CLI project by reading the workspace configuration
|
|
13
|
+
* and looking for common tsconfig locations.
|
|
14
|
+
*/
|
|
15
|
+
async function getProjectTsConfigPaths(tree) {
|
|
16
|
+
// Start with some tsconfig paths that are generally used within CLI projects. Note
|
|
17
|
+
// that we are not interested in IDE-specific tsconfig files (e.g. /tsconfig.json)
|
|
18
|
+
const buildPaths = new Set();
|
|
19
|
+
const testPaths = new Set();
|
|
20
|
+
const workspace = await getWorkspace(tree);
|
|
21
|
+
for (const [, project] of workspace.projects) {
|
|
22
|
+
for (const [name, target] of project.targets) {
|
|
23
|
+
if (name !== 'build' && name !== 'test') {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
for (const [, options] of allTargetOptions(target)) {
|
|
27
|
+
const tsConfig = options['tsConfig'];
|
|
28
|
+
// Filter out tsconfig files that don't exist in the CLI project.
|
|
29
|
+
if (typeof tsConfig !== 'string' || !tree.exists(tsConfig)) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (name === 'build') {
|
|
33
|
+
buildPaths.add(core.normalize(tsConfig));
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
testPaths.add(core.normalize(tsConfig));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
buildPaths: [...buildPaths],
|
|
43
|
+
testPaths: [...testPaths],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Get options for all configurations for the passed builder target. */
|
|
47
|
+
function* allTargetOptions(target) {
|
|
48
|
+
if (target.options) {
|
|
49
|
+
yield [undefined, target.options];
|
|
50
|
+
}
|
|
51
|
+
if (!target.configurations) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
for (const [name, options] of Object.entries(target.configurations)) {
|
|
55
|
+
if (options) {
|
|
56
|
+
yield [name, options];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function createHost(tree) {
|
|
61
|
+
return {
|
|
62
|
+
async readFile(path) {
|
|
63
|
+
const data = tree.read(path);
|
|
64
|
+
if (!data) {
|
|
65
|
+
throw new Error('File not found.');
|
|
66
|
+
}
|
|
67
|
+
return core.virtualFs.fileBufferToString(data);
|
|
68
|
+
},
|
|
69
|
+
async writeFile(path, data) {
|
|
70
|
+
return tree.overwrite(path, data);
|
|
71
|
+
},
|
|
72
|
+
async isDirectory(path) {
|
|
73
|
+
// Approximate a directory check.
|
|
74
|
+
// We don't need to consider empty directories and hence this is a good enough approach.
|
|
75
|
+
// This is also per documentation, see:
|
|
76
|
+
// https://angular.dev/tools/cli/schematics-for-libraries#get-the-project-configuration
|
|
77
|
+
return !tree.exists(path) && tree.getDir(path).subfiles.length > 0;
|
|
78
|
+
},
|
|
79
|
+
async isFile(path) {
|
|
80
|
+
return tree.exists(path);
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async function getWorkspace(tree) {
|
|
85
|
+
const host = createHost(tree);
|
|
86
|
+
const { workspace } = await core.workspaces.readWorkspace('/', host);
|
|
87
|
+
return workspace;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
exports.getProjectTsConfigPaths = getProjectTsConfigPaths;
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* @license Angular v19.0.0-next.4
|
|
4
|
+
* (c) 2010-2024 Google LLC. https://angular.io/
|
|
5
|
+
* License: MIT
|
|
6
|
+
*/
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
10
|
+
|
|
11
|
+
var schematics = require('@angular-devkit/schematics');
|
|
12
|
+
var fs = require('fs');
|
|
13
|
+
var p = require('path');
|
|
14
|
+
var compiler_host = require('./compiler_host-ca7ba733.js');
|
|
15
|
+
var project_tsconfig_paths = require('./project_tsconfig_paths-e9ccccbf.js');
|
|
16
|
+
var ts = require('typescript');
|
|
17
|
+
require('os');
|
|
18
|
+
require('module');
|
|
19
|
+
require('url');
|
|
20
|
+
require('@angular-devkit/core');
|
|
21
|
+
|
|
22
|
+
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
23
|
+
|
|
24
|
+
var ts__default = /*#__PURE__*/_interopDefaultLegacy(ts);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Finds the class declaration that is being referred to by a node.
|
|
28
|
+
* @param reference Node referring to a class declaration.
|
|
29
|
+
* @param typeChecker
|
|
30
|
+
*/
|
|
31
|
+
function findClassDeclaration(reference, typeChecker) {
|
|
32
|
+
return (typeChecker
|
|
33
|
+
.getTypeAtLocation(reference)
|
|
34
|
+
.getSymbol()
|
|
35
|
+
?.declarations?.find(ts__default["default"].isClassDeclaration) || null);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Finds a property with a specific name in an object literal expression. */
|
|
39
|
+
function findLiteralProperty(literal, name) {
|
|
40
|
+
return literal.properties.find((prop) => prop.name && ts__default["default"].isIdentifier(prop.name) && prop.name.text === name);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/*!
|
|
44
|
+
* @license
|
|
45
|
+
* Copyright Google LLC All Rights Reserved.
|
|
46
|
+
*
|
|
47
|
+
* Use of this source code is governed by an MIT-style license that can be
|
|
48
|
+
* found in the LICENSE file at https://angular.dev/license
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* Checks whether a component is standalone.
|
|
52
|
+
* @param node Class being checked.
|
|
53
|
+
*/
|
|
54
|
+
function isStandaloneComponent(node) {
|
|
55
|
+
const decorator = node.modifiers?.find((m) => m.kind === ts__default["default"].SyntaxKind.Decorator);
|
|
56
|
+
if (!decorator) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
if (ts__default["default"].isCallExpression(decorator.expression)) {
|
|
60
|
+
const arg = decorator.expression.arguments[0];
|
|
61
|
+
if (ts__default["default"].isObjectLiteralExpression(arg)) {
|
|
62
|
+
const property = findLiteralProperty(arg, 'standalone');
|
|
63
|
+
return property ? property.initializer.getText() === 'true' : false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Checks whether a node is variable declaration of type Routes or Route[] and comes from @angular/router
|
|
70
|
+
* @param node Variable declaration being checked.
|
|
71
|
+
* @param typeChecker
|
|
72
|
+
*/
|
|
73
|
+
function isAngularRoutesArray(node, typeChecker) {
|
|
74
|
+
if (ts__default["default"].isVariableDeclaration(node)) {
|
|
75
|
+
const type = typeChecker.getTypeAtLocation(node);
|
|
76
|
+
if (type && typeChecker.isArrayType(type)) {
|
|
77
|
+
// Route[] is an array type
|
|
78
|
+
const typeArguments = typeChecker.getTypeArguments(type);
|
|
79
|
+
const symbol = typeArguments[0]?.getSymbol();
|
|
80
|
+
return (symbol?.name === 'Route' &&
|
|
81
|
+
symbol?.declarations?.some((decl) => {
|
|
82
|
+
return decl.getSourceFile().fileName.includes('@angular/router');
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Checks whether a node is a call expression to a router module method.
|
|
90
|
+
* Examples:
|
|
91
|
+
* - RouterModule.forRoot(routes)
|
|
92
|
+
* - RouterModule.forChild(routes)
|
|
93
|
+
*/
|
|
94
|
+
function isRouterModuleCallExpression(node, typeChecker) {
|
|
95
|
+
if (ts__default["default"].isPropertyAccessExpression(node.expression)) {
|
|
96
|
+
const propAccess = node.expression;
|
|
97
|
+
const moduleSymbol = typeChecker.getSymbolAtLocation(propAccess.expression);
|
|
98
|
+
return (moduleSymbol?.name === 'RouterModule' &&
|
|
99
|
+
(propAccess.name.text === 'forRoot' || propAccess.name.text === 'forChild'));
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Checks whether a node is a call expression to a router method.
|
|
105
|
+
* Example: this.router.resetConfig(routes)
|
|
106
|
+
*/
|
|
107
|
+
function isRouterCallExpression(node, typeChecker) {
|
|
108
|
+
if (ts__default["default"].isCallExpression(node) &&
|
|
109
|
+
ts__default["default"].isPropertyAccessExpression(node.expression) &&
|
|
110
|
+
node.expression.name.text === 'resetConfig') {
|
|
111
|
+
const calleeExpression = node.expression.expression;
|
|
112
|
+
const symbol = typeChecker.getSymbolAtLocation(calleeExpression);
|
|
113
|
+
if (symbol) {
|
|
114
|
+
const type = typeChecker.getTypeOfSymbolAtLocation(symbol, calleeExpression);
|
|
115
|
+
// if type of router is Router, then it is a router call expression
|
|
116
|
+
return type.aliasSymbol?.escapedName === 'Router';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Checks whether a node is a call expression to router provide function.
|
|
123
|
+
* Example: provideRoutes(routes)
|
|
124
|
+
*/
|
|
125
|
+
function isRouterProviderCallExpression(node, typeChecker) {
|
|
126
|
+
if (ts__default["default"].isIdentifier(node.expression)) {
|
|
127
|
+
const moduleSymbol = typeChecker.getSymbolAtLocation(node.expression);
|
|
128
|
+
return moduleSymbol && moduleSymbol.name === 'provideRoutes';
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Checks whether a node is a call expression to provideRouter function.
|
|
134
|
+
* Example: provideRouter(routes)
|
|
135
|
+
*/
|
|
136
|
+
function isProvideRoutesCallExpression(node, typeChecker) {
|
|
137
|
+
if (ts__default["default"].isIdentifier(node.expression)) {
|
|
138
|
+
const moduleSymbol = typeChecker.getSymbolAtLocation(node.expression);
|
|
139
|
+
return moduleSymbol && moduleSymbol.name === 'provideRouter';
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/*!
|
|
145
|
+
* @license
|
|
146
|
+
* Copyright Google LLC All Rights Reserved.
|
|
147
|
+
*
|
|
148
|
+
* Use of this source code is governed by an MIT-style license that can be
|
|
149
|
+
* found in the LICENSE file at https://angular.dev/license
|
|
150
|
+
*/
|
|
151
|
+
/**
|
|
152
|
+
* Converts all application routes that are using standalone components to be lazy loaded.
|
|
153
|
+
* @param sourceFile File that should be migrated.
|
|
154
|
+
* @param program
|
|
155
|
+
*/
|
|
156
|
+
function migrateFileToLazyRoutes(sourceFile, program) {
|
|
157
|
+
const typeChecker = program.getTypeChecker();
|
|
158
|
+
const printer = ts__default["default"].createPrinter();
|
|
159
|
+
const tracker = new compiler_host.ChangeTracker(printer);
|
|
160
|
+
const routeArraysToMigrate = findRoutesArrayToMigrate(sourceFile, typeChecker);
|
|
161
|
+
if (routeArraysToMigrate.length === 0) {
|
|
162
|
+
return { pendingChanges: [], skippedRoutes: [], migratedRoutes: [] };
|
|
163
|
+
}
|
|
164
|
+
const { skippedRoutes, migratedRoutes } = migrateRoutesArray(routeArraysToMigrate, typeChecker, tracker);
|
|
165
|
+
return {
|
|
166
|
+
pendingChanges: tracker.recordChanges().get(sourceFile) || [],
|
|
167
|
+
skippedRoutes,
|
|
168
|
+
migratedRoutes,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/** Finds route object that can be migrated */
|
|
172
|
+
function findRoutesArrayToMigrate(sourceFile, typeChecker) {
|
|
173
|
+
const routesArrays = [];
|
|
174
|
+
sourceFile.forEachChild(function walk(node) {
|
|
175
|
+
if (ts__default["default"].isCallExpression(node)) {
|
|
176
|
+
if (isRouterModuleCallExpression(node, typeChecker) ||
|
|
177
|
+
isRouterProviderCallExpression(node, typeChecker) ||
|
|
178
|
+
isRouterCallExpression(node, typeChecker) ||
|
|
179
|
+
isProvideRoutesCallExpression(node, typeChecker)) {
|
|
180
|
+
const arg = node.arguments[0]; // ex: RouterModule.forRoot(routes) or provideRouter(routes)
|
|
181
|
+
const routeFileImports = sourceFile.statements.filter(ts__default["default"].isImportDeclaration);
|
|
182
|
+
if (ts__default["default"].isArrayLiteralExpression(arg) && arg.elements.length > 0) {
|
|
183
|
+
// ex: inline routes array: RouterModule.forRoot([{ path: 'test', component: TestComponent }])
|
|
184
|
+
routesArrays.push({
|
|
185
|
+
routeFilePath: sourceFile.fileName,
|
|
186
|
+
array: arg,
|
|
187
|
+
routeFileImports,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
else if (ts__default["default"].isIdentifier(arg)) {
|
|
191
|
+
// ex: reference to routes array: RouterModule.forRoot(routes)
|
|
192
|
+
// RouterModule.forRoot(routes), provideRouter(routes), provideRoutes(routes)
|
|
193
|
+
const symbol = typeChecker.getSymbolAtLocation(arg);
|
|
194
|
+
if (!symbol?.declarations)
|
|
195
|
+
return;
|
|
196
|
+
for (const declaration of symbol.declarations) {
|
|
197
|
+
if (ts__default["default"].isVariableDeclaration(declaration)) {
|
|
198
|
+
const initializer = declaration.initializer;
|
|
199
|
+
if (initializer && ts__default["default"].isArrayLiteralExpression(initializer)) {
|
|
200
|
+
// ex: const routes = [{ path: 'test', component: TestComponent }];
|
|
201
|
+
routesArrays.push({
|
|
202
|
+
routeFilePath: sourceFile.fileName,
|
|
203
|
+
array: initializer,
|
|
204
|
+
routeFileImports,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (ts__default["default"].isVariableDeclaration(node)) {
|
|
213
|
+
if (isAngularRoutesArray(node, typeChecker)) {
|
|
214
|
+
const initializer = node.initializer;
|
|
215
|
+
if (initializer &&
|
|
216
|
+
ts__default["default"].isArrayLiteralExpression(initializer) &&
|
|
217
|
+
initializer.elements.length > 0) {
|
|
218
|
+
// ex: const routes: Routes = [{ path: 'test', component: TestComponent }];
|
|
219
|
+
if (routesArrays.find((x) => x.array === initializer)) {
|
|
220
|
+
// already exists
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
routesArrays.push({
|
|
224
|
+
routeFilePath: sourceFile.fileName,
|
|
225
|
+
array: initializer,
|
|
226
|
+
routeFileImports: sourceFile.statements.filter(ts__default["default"].isImportDeclaration),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
node.forEachChild(walk);
|
|
232
|
+
});
|
|
233
|
+
return routesArrays;
|
|
234
|
+
}
|
|
235
|
+
/** Migrate a routes object standalone components to be lazy loaded. */
|
|
236
|
+
function migrateRoutesArray(routesArray, typeChecker, tracker) {
|
|
237
|
+
const migratedRoutes = [];
|
|
238
|
+
const skippedRoutes = [];
|
|
239
|
+
const importsToRemove = [];
|
|
240
|
+
for (const route of routesArray) {
|
|
241
|
+
route.array.elements.forEach((element) => {
|
|
242
|
+
if (ts__default["default"].isObjectLiteralExpression(element)) {
|
|
243
|
+
const { migratedRoutes: migrated, skippedRoutes: toBeSkipped, importsToRemove: toBeRemoved, } = migrateRoute(element, route, typeChecker, tracker);
|
|
244
|
+
migratedRoutes.push(...migrated);
|
|
245
|
+
skippedRoutes.push(...toBeSkipped);
|
|
246
|
+
importsToRemove.push(...toBeRemoved);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
for (const importToRemove of importsToRemove) {
|
|
251
|
+
tracker.removeNode(importToRemove);
|
|
252
|
+
}
|
|
253
|
+
return { migratedRoutes, skippedRoutes };
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Migrates a single route object and returns the results of the migration
|
|
257
|
+
* It recursively migrates the children routes if they exist
|
|
258
|
+
*/
|
|
259
|
+
function migrateRoute(element, route, typeChecker, tracker) {
|
|
260
|
+
const skippedRoutes = [];
|
|
261
|
+
const migratedRoutes = [];
|
|
262
|
+
const importsToRemove = [];
|
|
263
|
+
const component = findLiteralProperty(element, 'component');
|
|
264
|
+
// this can be empty string or a variable that is not a string, or not present at all
|
|
265
|
+
const routePath = findLiteralProperty(element, 'path')?.getText() ?? '';
|
|
266
|
+
const children = findLiteralProperty(element, 'children');
|
|
267
|
+
// recursively migrate children routes first if they exist
|
|
268
|
+
if (children && ts__default["default"].isArrayLiteralExpression(children.initializer)) {
|
|
269
|
+
for (const childRoute of children.initializer.elements) {
|
|
270
|
+
if (ts__default["default"].isObjectLiteralExpression(childRoute)) {
|
|
271
|
+
const { migratedRoutes: migrated, skippedRoutes: toBeSkipped, importsToRemove: toBeRemoved, } = migrateRoute(childRoute, route, typeChecker, tracker);
|
|
272
|
+
migratedRoutes.push(...migrated);
|
|
273
|
+
skippedRoutes.push(...toBeSkipped);
|
|
274
|
+
importsToRemove.push(...toBeRemoved);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const routeMigrationResults = { migratedRoutes, skippedRoutes, importsToRemove };
|
|
279
|
+
if (!component) {
|
|
280
|
+
return routeMigrationResults;
|
|
281
|
+
}
|
|
282
|
+
const componentDeclaration = findClassDeclaration(component, typeChecker);
|
|
283
|
+
if (!componentDeclaration) {
|
|
284
|
+
return routeMigrationResults;
|
|
285
|
+
}
|
|
286
|
+
// if component is not a standalone component, skip it
|
|
287
|
+
if (!isStandaloneComponent(componentDeclaration)) {
|
|
288
|
+
skippedRoutes.push({ path: routePath, file: route.routeFilePath });
|
|
289
|
+
return routeMigrationResults;
|
|
290
|
+
}
|
|
291
|
+
const componentClassName = componentDeclaration.name && ts__default["default"].isIdentifier(componentDeclaration.name)
|
|
292
|
+
? componentDeclaration.name.text
|
|
293
|
+
: null;
|
|
294
|
+
if (!componentClassName) {
|
|
295
|
+
return routeMigrationResults;
|
|
296
|
+
}
|
|
297
|
+
// if component is in the same file as the routes array, skip it
|
|
298
|
+
if (componentDeclaration.getSourceFile().fileName === route.routeFilePath) {
|
|
299
|
+
return routeMigrationResults;
|
|
300
|
+
}
|
|
301
|
+
const componentImport = route.routeFileImports.find((importDecl) => importDecl.importClause?.getText().includes(componentClassName));
|
|
302
|
+
// remove single and double quotes from the import path
|
|
303
|
+
let componentImportPath = ts__default["default"].isStringLiteral(componentImport?.moduleSpecifier)
|
|
304
|
+
? componentImport.moduleSpecifier.text
|
|
305
|
+
: null;
|
|
306
|
+
// if the import path is not a string literal, skip it
|
|
307
|
+
if (!componentImportPath) {
|
|
308
|
+
skippedRoutes.push({ path: routePath, file: route.routeFilePath });
|
|
309
|
+
return routeMigrationResults;
|
|
310
|
+
}
|
|
311
|
+
const isDefaultExport = componentDeclaration.modifiers?.some((x) => x.kind === ts__default["default"].SyntaxKind.DefaultKeyword) ?? false;
|
|
312
|
+
const loadComponent = createLoadComponentPropertyAssignment(componentImportPath, componentClassName, isDefaultExport);
|
|
313
|
+
tracker.replaceNode(component, loadComponent);
|
|
314
|
+
// Add the import statement for the standalone component
|
|
315
|
+
if (!importsToRemove.includes(componentImport)) {
|
|
316
|
+
importsToRemove.push(componentImport);
|
|
317
|
+
}
|
|
318
|
+
migratedRoutes.push({ path: routePath, file: route.routeFilePath });
|
|
319
|
+
// the component was migrated, so we return the results
|
|
320
|
+
return routeMigrationResults;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Generates the loadComponent property assignment for a given component.
|
|
324
|
+
*
|
|
325
|
+
* Example:
|
|
326
|
+
* loadComponent: () => import('./path').then(m => m.componentName)
|
|
327
|
+
* or
|
|
328
|
+
* loadComponent: () => import('./path') // when isDefaultExport is true
|
|
329
|
+
*/
|
|
330
|
+
function createLoadComponentPropertyAssignment(componentImportPath, componentDeclarationName, isDefaultExport) {
|
|
331
|
+
return ts__default["default"].factory.createPropertyAssignment('loadComponent', ts__default["default"].factory.createArrowFunction(undefined, undefined, [], undefined, ts__default["default"].factory.createToken(ts__default["default"].SyntaxKind.EqualsGreaterThanToken), isDefaultExport
|
|
332
|
+
? createImportCallExpression(componentImportPath) // will generate import('./path) and will skip the then() call
|
|
333
|
+
: ts__default["default"].factory.createCallExpression(
|
|
334
|
+
// will generate import('./path).then(m => m.componentName)
|
|
335
|
+
ts__default["default"].factory.createPropertyAccessExpression(createImportCallExpression(componentImportPath), 'then'), undefined, [createImportThenCallExpression(componentDeclarationName)])));
|
|
336
|
+
}
|
|
337
|
+
// import('./path)
|
|
338
|
+
const createImportCallExpression = (componentImportPath) => ts__default["default"].factory.createCallExpression(ts__default["default"].factory.createIdentifier('import'), undefined, [
|
|
339
|
+
ts__default["default"].factory.createStringLiteral(componentImportPath, true),
|
|
340
|
+
]);
|
|
341
|
+
// m => m.componentName
|
|
342
|
+
const createImportThenCallExpression = (componentDeclarationName) => ts__default["default"].factory.createArrowFunction(undefined, undefined, [ts__default["default"].factory.createParameterDeclaration(undefined, undefined, 'm', undefined, undefined)], undefined, ts__default["default"].factory.createToken(ts__default["default"].SyntaxKind.EqualsGreaterThanToken), ts__default["default"].factory.createPropertyAccessExpression(ts__default["default"].factory.createIdentifier('m'), componentDeclarationName));
|
|
343
|
+
|
|
344
|
+
function migrate(options) {
|
|
345
|
+
return async (tree, context) => {
|
|
346
|
+
const { buildPaths } = await project_tsconfig_paths.getProjectTsConfigPaths(tree);
|
|
347
|
+
const basePath = process.cwd();
|
|
348
|
+
// TS and Schematic use paths in POSIX format even on Windows. This is needed as otherwise
|
|
349
|
+
// string matching such as `sourceFile.fileName.startsWith(pathToMigrate)` might not work.
|
|
350
|
+
const pathToMigrate = compiler_host.normalizePath(p.join(basePath, options.path));
|
|
351
|
+
if (!buildPaths.length) {
|
|
352
|
+
throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run the route lazy loading migration.');
|
|
353
|
+
}
|
|
354
|
+
let migratedRoutes = [];
|
|
355
|
+
let skippedRoutes = [];
|
|
356
|
+
for (const tsconfigPath of buildPaths) {
|
|
357
|
+
const { migratedRoutes: migrated, skippedRoutes: skipped } = standaloneRoutesMigration(tree, tsconfigPath, basePath, pathToMigrate, options);
|
|
358
|
+
migratedRoutes.push(...migrated);
|
|
359
|
+
skippedRoutes.push(...skipped);
|
|
360
|
+
}
|
|
361
|
+
if (migratedRoutes.length === 0 && skippedRoutes.length === 0) {
|
|
362
|
+
throw new schematics.SchematicsException(`Could not find any files to migrate under the path ${pathToMigrate}.`);
|
|
363
|
+
}
|
|
364
|
+
context.logger.info('🎉 Automated migration step has finished! 🎉');
|
|
365
|
+
context.logger.info(`Number of updated routes: ${migratedRoutes.length}`);
|
|
366
|
+
context.logger.info(`Number of skipped routes: ${skippedRoutes.length}`);
|
|
367
|
+
if (skippedRoutes.length > 0) {
|
|
368
|
+
context.logger.info(`Note: this migration was unable to optimize the following routes, since they use components declared in NgModules:`);
|
|
369
|
+
for (const route of skippedRoutes) {
|
|
370
|
+
context.logger.info(`- \`${route.path}\` path at \`${route.file}\``);
|
|
371
|
+
}
|
|
372
|
+
context.logger.info(`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`);
|
|
373
|
+
}
|
|
374
|
+
context.logger.info('IMPORTANT! Please verify manually that your application builds and behaves as expected.');
|
|
375
|
+
context.logger.info(`See https://angular.dev/reference/migrations/route-lazy-loading for more information.`);
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function standaloneRoutesMigration(tree, tsconfigPath, basePath, pathToMigrate, schematicOptions) {
|
|
379
|
+
if (schematicOptions.path.startsWith('..')) {
|
|
380
|
+
throw new schematics.SchematicsException('Cannot run route lazy loading migration outside of the current project.');
|
|
381
|
+
}
|
|
382
|
+
if (fs.existsSync(pathToMigrate) && !fs.statSync(pathToMigrate).isDirectory()) {
|
|
383
|
+
throw new schematics.SchematicsException(`Migration path ${pathToMigrate} has to be a directory. Cannot run the route lazy loading migration.`);
|
|
384
|
+
}
|
|
385
|
+
const program = compiler_host.createMigrationProgram(tree, tsconfigPath, basePath);
|
|
386
|
+
const sourceFiles = program
|
|
387
|
+
.getSourceFiles()
|
|
388
|
+
.filter((sourceFile) => sourceFile.fileName.startsWith(pathToMigrate) &&
|
|
389
|
+
compiler_host.canMigrateFile(basePath, sourceFile, program));
|
|
390
|
+
const migratedRoutes = [];
|
|
391
|
+
const skippedRoutes = [];
|
|
392
|
+
if (sourceFiles.length === 0) {
|
|
393
|
+
return { migratedRoutes, skippedRoutes };
|
|
394
|
+
}
|
|
395
|
+
for (const sourceFile of sourceFiles) {
|
|
396
|
+
const { pendingChanges, skippedRoutes: skipped, migratedRoutes: migrated, } = migrateFileToLazyRoutes(sourceFile, program);
|
|
397
|
+
skippedRoutes.push(...skipped);
|
|
398
|
+
migratedRoutes.push(...migrated);
|
|
399
|
+
const update = tree.beginUpdate(p.relative(basePath, sourceFile.fileName));
|
|
400
|
+
pendingChanges.forEach((change) => {
|
|
401
|
+
if (change.removeLength != null) {
|
|
402
|
+
update.remove(change.start, change.removeLength);
|
|
403
|
+
}
|
|
404
|
+
update.insertRight(change.start, change.text);
|
|
405
|
+
});
|
|
406
|
+
tree.commitUpdate(update);
|
|
407
|
+
}
|
|
408
|
+
return { migratedRoutes, skippedRoutes };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
exports.migrate = migrate;
|