route-lazy-loading.cjs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. 'use strict';
  2. /**
  3. * @license Angular v19.2.13
  4. * (c) 2010-2025 Google LLC. https://angular.io/
  5. * License: MIT
  6. */
  7. 'use strict';
  8. var schematics = require('@angular-devkit/schematics');
  9. var fs = require('fs');
  10. var p = require('path');
  11. var compiler_host = require('./compiler_host-B1Gyeytz.cjs');
  12. var project_tsconfig_paths = require('./project_tsconfig_paths-CDVxT6Ov.cjs');
  13. var ts = require('typescript');
  14. var checker = require('./checker-5pyJrZ9G.cjs');
  15. var property_name = require('./property_name-BBwFuqMe.cjs');
  16. require('os');
  17. require('@angular-devkit/core');
  18. require('module');
  19. require('url');
  20. /**
  21. * Finds the class declaration that is being referred to by a node.
  22. * @param reference Node referring to a class declaration.
  23. * @param typeChecker
  24. */
  25. function findClassDeclaration(reference, typeChecker) {
  26. return (typeChecker
  27. .getTypeAtLocation(reference)
  28. .getSymbol()
  29. ?.declarations?.find(ts.isClassDeclaration) || null);
  30. }
  31. /*!
  32. * @license
  33. * Copyright Google LLC All Rights Reserved.
  34. *
  35. * Use of this source code is governed by an MIT-style license that can be
  36. * found in the LICENSE file at https://angular.dev/license
  37. */
  38. /**
  39. * Checks whether a component is standalone.
  40. * @param node Class being checked.
  41. * @param reflector The reflection host to use.
  42. */
  43. function isStandaloneComponent(node, reflector) {
  44. const decorators = reflector.getDecoratorsOfDeclaration(node);
  45. if (decorators === null) {
  46. return false;
  47. }
  48. const decorator = checker.findAngularDecorator(decorators, 'Component', false);
  49. if (decorator === undefined || decorator.args === null || decorator.args.length !== 1) {
  50. return false;
  51. }
  52. const arg = decorator.args[0];
  53. if (ts.isObjectLiteralExpression(arg)) {
  54. const property = property_name.findLiteralProperty(arg, 'standalone');
  55. if (property) {
  56. return property.initializer.getText() === 'true';
  57. }
  58. else {
  59. return true; // standalone is true by default in v19
  60. }
  61. }
  62. return false;
  63. }
  64. /**
  65. * Checks whether a node is variable declaration of type Routes or Route[] and comes from @angular/router
  66. * @param node Variable declaration being checked.
  67. * @param typeChecker
  68. */
  69. function isAngularRoutesArray(node, typeChecker) {
  70. if (ts.isVariableDeclaration(node)) {
  71. const type = typeChecker.getTypeAtLocation(node);
  72. if (type && typeChecker.isArrayType(type)) {
  73. // Route[] is an array type
  74. const typeArguments = typeChecker.getTypeArguments(type);
  75. const symbol = typeArguments[0]?.getSymbol();
  76. return (symbol?.name === 'Route' &&
  77. symbol?.declarations?.some((decl) => {
  78. return decl.getSourceFile().fileName.includes('@angular/router');
  79. }));
  80. }
  81. }
  82. return false;
  83. }
  84. /**
  85. * Checks whether a node is a call expression to a router module method.
  86. * Examples:
  87. * - RouterModule.forRoot(routes)
  88. * - RouterModule.forChild(routes)
  89. */
  90. function isRouterModuleCallExpression(node, typeChecker) {
  91. if (ts.isPropertyAccessExpression(node.expression)) {
  92. const propAccess = node.expression;
  93. const moduleSymbol = typeChecker.getSymbolAtLocation(propAccess.expression);
  94. return (moduleSymbol?.name === 'RouterModule' &&
  95. (propAccess.name.text === 'forRoot' || propAccess.name.text === 'forChild'));
  96. }
  97. return false;
  98. }
  99. /**
  100. * Checks whether a node is a call expression to a router method.
  101. * Example: this.router.resetConfig(routes)
  102. */
  103. function isRouterCallExpression(node, typeChecker) {
  104. if (ts.isCallExpression(node) &&
  105. ts.isPropertyAccessExpression(node.expression) &&
  106. node.expression.name.text === 'resetConfig') {
  107. const calleeExpression = node.expression.expression;
  108. const symbol = typeChecker.getSymbolAtLocation(calleeExpression);
  109. if (symbol) {
  110. const type = typeChecker.getTypeOfSymbolAtLocation(symbol, calleeExpression);
  111. // if type of router is Router, then it is a router call expression
  112. return type.aliasSymbol?.escapedName === 'Router';
  113. }
  114. }
  115. return false;
  116. }
  117. /**
  118. * Checks whether a node is a call expression to router provide function.
  119. * Example: provideRoutes(routes)
  120. */
  121. function isRouterProviderCallExpression(node, typeChecker) {
  122. if (ts.isIdentifier(node.expression)) {
  123. const moduleSymbol = typeChecker.getSymbolAtLocation(node.expression);
  124. return moduleSymbol && moduleSymbol.name === 'provideRoutes';
  125. }
  126. return false;
  127. }
  128. /**
  129. * Checks whether a node is a call expression to provideRouter function.
  130. * Example: provideRouter(routes)
  131. */
  132. function isProvideRoutesCallExpression(node, typeChecker) {
  133. if (ts.isIdentifier(node.expression)) {
  134. const moduleSymbol = typeChecker.getSymbolAtLocation(node.expression);
  135. return moduleSymbol && moduleSymbol.name === 'provideRouter';
  136. }
  137. return false;
  138. }
  139. /*!
  140. * @license
  141. * Copyright Google LLC All Rights Reserved.
  142. *
  143. * Use of this source code is governed by an MIT-style license that can be
  144. * found in the LICENSE file at https://angular.dev/license
  145. */
  146. /**
  147. * Converts all application routes that are using standalone components to be lazy loaded.
  148. * @param sourceFile File that should be migrated.
  149. * @param program
  150. */
  151. function migrateFileToLazyRoutes(sourceFile, program) {
  152. const typeChecker = program.getTypeChecker();
  153. const reflector = new checker.TypeScriptReflectionHost(typeChecker);
  154. const printer = ts.createPrinter();
  155. const tracker = new compiler_host.ChangeTracker(printer);
  156. const routeArraysToMigrate = findRoutesArrayToMigrate(sourceFile, typeChecker);
  157. if (routeArraysToMigrate.length === 0) {
  158. return { pendingChanges: [], skippedRoutes: [], migratedRoutes: [] };
  159. }
  160. const { skippedRoutes, migratedRoutes } = migrateRoutesArray(routeArraysToMigrate, typeChecker, reflector, tracker);
  161. return {
  162. pendingChanges: tracker.recordChanges().get(sourceFile) || [],
  163. skippedRoutes,
  164. migratedRoutes,
  165. };
  166. }
  167. /** Finds route object that can be migrated */
  168. function findRoutesArrayToMigrate(sourceFile, typeChecker) {
  169. const routesArrays = [];
  170. sourceFile.forEachChild(function walk(node) {
  171. if (ts.isCallExpression(node)) {
  172. if (isRouterModuleCallExpression(node, typeChecker) ||
  173. isRouterProviderCallExpression(node, typeChecker) ||
  174. isRouterCallExpression(node, typeChecker) ||
  175. isProvideRoutesCallExpression(node, typeChecker)) {
  176. const arg = node.arguments[0]; // ex: RouterModule.forRoot(routes) or provideRouter(routes)
  177. const routeFileImports = sourceFile.statements.filter(ts.isImportDeclaration);
  178. if (ts.isArrayLiteralExpression(arg) && arg.elements.length > 0) {
  179. // ex: inline routes array: RouterModule.forRoot([{ path: 'test', component: TestComponent }])
  180. routesArrays.push({
  181. routeFilePath: sourceFile.fileName,
  182. array: arg,
  183. routeFileImports,
  184. });
  185. }
  186. else if (ts.isIdentifier(arg)) {
  187. // ex: reference to routes array: RouterModule.forRoot(routes)
  188. // RouterModule.forRoot(routes), provideRouter(routes), provideRoutes(routes)
  189. const symbol = typeChecker.getSymbolAtLocation(arg);
  190. if (!symbol?.declarations)
  191. return;
  192. for (const declaration of symbol.declarations) {
  193. if (ts.isVariableDeclaration(declaration)) {
  194. const initializer = declaration.initializer;
  195. if (initializer && ts.isArrayLiteralExpression(initializer)) {
  196. // ex: const routes = [{ path: 'test', component: TestComponent }];
  197. routesArrays.push({
  198. routeFilePath: sourceFile.fileName,
  199. array: initializer,
  200. routeFileImports,
  201. });
  202. }
  203. }
  204. }
  205. }
  206. }
  207. }
  208. if (ts.isVariableDeclaration(node)) {
  209. if (isAngularRoutesArray(node, typeChecker)) {
  210. const initializer = node.initializer;
  211. if (initializer &&
  212. ts.isArrayLiteralExpression(initializer) &&
  213. initializer.elements.length > 0) {
  214. // ex: const routes: Routes = [{ path: 'test', component: TestComponent }];
  215. if (routesArrays.find((x) => x.array === initializer)) {
  216. // already exists
  217. return;
  218. }
  219. routesArrays.push({
  220. routeFilePath: sourceFile.fileName,
  221. array: initializer,
  222. routeFileImports: sourceFile.statements.filter(ts.isImportDeclaration),
  223. });
  224. }
  225. }
  226. }
  227. node.forEachChild(walk);
  228. });
  229. return routesArrays;
  230. }
  231. /** Migrate a routes object standalone components to be lazy loaded. */
  232. function migrateRoutesArray(routesArray, typeChecker, reflector, tracker) {
  233. const migratedRoutes = [];
  234. const skippedRoutes = [];
  235. const importsToRemove = [];
  236. for (const route of routesArray) {
  237. route.array.elements.forEach((element) => {
  238. if (ts.isObjectLiteralExpression(element)) {
  239. const { migratedRoutes: migrated, skippedRoutes: toBeSkipped, importsToRemove: toBeRemoved, } = migrateRoute(element, route, typeChecker, reflector, tracker);
  240. migratedRoutes.push(...migrated);
  241. skippedRoutes.push(...toBeSkipped);
  242. importsToRemove.push(...toBeRemoved);
  243. }
  244. });
  245. }
  246. for (const importToRemove of importsToRemove) {
  247. tracker.removeNode(importToRemove);
  248. }
  249. return { migratedRoutes, skippedRoutes };
  250. }
  251. /**
  252. * Migrates a single route object and returns the results of the migration
  253. * It recursively migrates the children routes if they exist
  254. */
  255. function migrateRoute(element, route, typeChecker, reflector, tracker) {
  256. const skippedRoutes = [];
  257. const migratedRoutes = [];
  258. const importsToRemove = [];
  259. const component = property_name.findLiteralProperty(element, 'component');
  260. // this can be empty string or a variable that is not a string, or not present at all
  261. const routePath = property_name.findLiteralProperty(element, 'path')?.getText() ?? '';
  262. const children = property_name.findLiteralProperty(element, 'children');
  263. // recursively migrate children routes first if they exist
  264. if (children && ts.isArrayLiteralExpression(children.initializer)) {
  265. for (const childRoute of children.initializer.elements) {
  266. if (ts.isObjectLiteralExpression(childRoute)) {
  267. const { migratedRoutes: migrated, skippedRoutes: toBeSkipped, importsToRemove: toBeRemoved, } = migrateRoute(childRoute, route, typeChecker, reflector, tracker);
  268. migratedRoutes.push(...migrated);
  269. skippedRoutes.push(...toBeSkipped);
  270. importsToRemove.push(...toBeRemoved);
  271. }
  272. }
  273. }
  274. const routeMigrationResults = { migratedRoutes, skippedRoutes, importsToRemove };
  275. if (!component) {
  276. return routeMigrationResults;
  277. }
  278. const componentDeclaration = findClassDeclaration(component, typeChecker);
  279. if (!componentDeclaration) {
  280. return routeMigrationResults;
  281. }
  282. // if component is not a standalone component, skip it
  283. if (!isStandaloneComponent(componentDeclaration, reflector)) {
  284. skippedRoutes.push({ path: routePath, file: route.routeFilePath });
  285. return routeMigrationResults;
  286. }
  287. const componentClassName = componentDeclaration.name && ts.isIdentifier(componentDeclaration.name)
  288. ? componentDeclaration.name.text
  289. : null;
  290. if (!componentClassName) {
  291. return routeMigrationResults;
  292. }
  293. // if component is in the same file as the routes array, skip it
  294. if (componentDeclaration.getSourceFile().fileName === route.routeFilePath) {
  295. return routeMigrationResults;
  296. }
  297. const componentImport = route.routeFileImports.find((importDecl) => importDecl.importClause?.getText().includes(componentClassName));
  298. // remove single and double quotes from the import path
  299. let componentImportPath = ts.isStringLiteral(componentImport?.moduleSpecifier)
  300. ? componentImport.moduleSpecifier.text
  301. : null;
  302. // if the import path is not a string literal, skip it
  303. if (!componentImportPath) {
  304. skippedRoutes.push({ path: routePath, file: route.routeFilePath });
  305. return routeMigrationResults;
  306. }
  307. const isDefaultExport = componentDeclaration.modifiers?.some((x) => x.kind === ts.SyntaxKind.DefaultKeyword) ?? false;
  308. const loadComponent = createLoadComponentPropertyAssignment(componentImportPath, componentClassName, isDefaultExport);
  309. tracker.replaceNode(component, loadComponent);
  310. // Add the import statement for the standalone component
  311. if (!importsToRemove.includes(componentImport)) {
  312. importsToRemove.push(componentImport);
  313. }
  314. migratedRoutes.push({ path: routePath, file: route.routeFilePath });
  315. // the component was migrated, so we return the results
  316. return routeMigrationResults;
  317. }
  318. /**
  319. * Generates the loadComponent property assignment for a given component.
  320. *
  321. * Example:
  322. * loadComponent: () => import('./path').then(m => m.componentName)
  323. * or
  324. * loadComponent: () => import('./path') // when isDefaultExport is true
  325. */
  326. function createLoadComponentPropertyAssignment(componentImportPath, componentDeclarationName, isDefaultExport) {
  327. return ts.factory.createPropertyAssignment('loadComponent', ts.factory.createArrowFunction(undefined, undefined, [], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), isDefaultExport
  328. ? createImportCallExpression(componentImportPath) // will generate import('./path) and will skip the then() call
  329. : ts.factory.createCallExpression(
  330. // will generate import('./path).then(m => m.componentName)
  331. ts.factory.createPropertyAccessExpression(createImportCallExpression(componentImportPath), 'then'), undefined, [createImportThenCallExpression(componentDeclarationName)])));
  332. }
  333. // import('./path)
  334. const createImportCallExpression = (componentImportPath) => ts.factory.createCallExpression(ts.factory.createIdentifier('import'), undefined, [
  335. ts.factory.createStringLiteral(componentImportPath, true),
  336. ]);
  337. // m => m.componentName
  338. const createImportThenCallExpression = (componentDeclarationName) => ts.factory.createArrowFunction(undefined, undefined, [ts.factory.createParameterDeclaration(undefined, undefined, 'm', undefined, undefined)], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('m'), componentDeclarationName));
  339. function migrate(options) {
  340. return async (tree, context) => {
  341. const { buildPaths } = await project_tsconfig_paths.getProjectTsConfigPaths(tree);
  342. const basePath = process.cwd();
  343. // TS and Schematic use paths in POSIX format even on Windows. This is needed as otherwise
  344. // string matching such as `sourceFile.fileName.startsWith(pathToMigrate)` might not work.
  345. const pathToMigrate = compiler_host.normalizePath(p.join(basePath, options.path));
  346. if (!buildPaths.length) {
  347. throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run the route lazy loading migration.');
  348. }
  349. let migratedRoutes = [];
  350. let skippedRoutes = [];
  351. for (const tsconfigPath of buildPaths) {
  352. const { migratedRoutes: migrated, skippedRoutes: skipped } = standaloneRoutesMigration(tree, tsconfigPath, basePath, pathToMigrate, options);
  353. migratedRoutes.push(...migrated);
  354. skippedRoutes.push(...skipped);
  355. }
  356. if (migratedRoutes.length === 0 && skippedRoutes.length === 0) {
  357. throw new schematics.SchematicsException(`Could not find any files to migrate under the path ${pathToMigrate}.`);
  358. }
  359. context.logger.info('🎉 Automated migration step has finished! 🎉');
  360. context.logger.info(`Number of updated routes: ${migratedRoutes.length}`);
  361. context.logger.info(`Number of skipped routes: ${skippedRoutes.length}`);
  362. if (skippedRoutes.length > 0) {
  363. context.logger.info(`Note: this migration was unable to optimize the following routes, since they use components declared in NgModules:`);
  364. for (const route of skippedRoutes) {
  365. context.logger.info(`- \`${route.path}\` path at \`${route.file}\``);
  366. }
  367. 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`);
  368. }
  369. context.logger.info('IMPORTANT! Please verify manually that your application builds and behaves as expected.');
  370. context.logger.info(`See https://angular.dev/reference/migrations/route-lazy-loading for more information.`);
  371. };
  372. }
  373. function standaloneRoutesMigration(tree, tsconfigPath, basePath, pathToMigrate, schematicOptions) {
  374. if (schematicOptions.path.startsWith('..')) {
  375. throw new schematics.SchematicsException('Cannot run route lazy loading migration outside of the current project.');
  376. }
  377. if (fs.existsSync(pathToMigrate) && !fs.statSync(pathToMigrate).isDirectory()) {
  378. throw new schematics.SchematicsException(`Migration path ${pathToMigrate} has to be a directory. Cannot run the route lazy loading migration.`);
  379. }
  380. const program = compiler_host.createMigrationProgram(tree, tsconfigPath, basePath);
  381. const sourceFiles = program
  382. .getSourceFiles()
  383. .filter((sourceFile) => sourceFile.fileName.startsWith(pathToMigrate) &&
  384. compiler_host.canMigrateFile(basePath, sourceFile, program));
  385. const migratedRoutes = [];
  386. const skippedRoutes = [];
  387. if (sourceFiles.length === 0) {
  388. return { migratedRoutes, skippedRoutes };
  389. }
  390. for (const sourceFile of sourceFiles) {
  391. const { pendingChanges, skippedRoutes: skipped, migratedRoutes: migrated, } = migrateFileToLazyRoutes(sourceFile, program);
  392. skippedRoutes.push(...skipped);
  393. migratedRoutes.push(...migrated);
  394. const update = tree.beginUpdate(p.relative(basePath, sourceFile.fileName));
  395. pendingChanges.forEach((change) => {
  396. if (change.removeLength != null) {
  397. update.remove(change.start, change.removeLength);
  398. }
  399. update.insertRight(change.start, change.text);
  400. });
  401. tree.commitUpdate(update);
  402. }
  403. return { migratedRoutes, skippedRoutes };
  404. }
  405. exports.migrate = migrate;