standalone-migration.cjs 98 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086
  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 index = require('./index-BIvVb6in.cjs');
  10. var fs = require('fs');
  11. var p = require('path');
  12. var ts = require('typescript');
  13. var compiler_host = require('./compiler_host-B1Gyeytz.cjs');
  14. var project_tsconfig_paths = require('./project_tsconfig_paths-CDVxT6Ov.cjs');
  15. var ng_decorators = require('./ng_decorators-B5HCqr20.cjs');
  16. var nodes = require('./nodes-B16H9JUd.cjs');
  17. var imports = require('./imports-CIX-JgAN.cjs');
  18. var checker = require('./checker-5pyJrZ9G.cjs');
  19. require('os');
  20. require('@angular-devkit/core');
  21. require('module');
  22. require('url');
  23. function createProgram({ rootNames, options, host, oldProgram, }) {
  24. return new index.NgtscProgram(rootNames, options, host, oldProgram);
  25. }
  26. /** Checks whether a node is referring to a specific import specifier. */
  27. function isReferenceToImport(typeChecker, node, importSpecifier) {
  28. // If this function is called on an identifier (should be most cases), we can quickly rule out
  29. // non-matches by comparing the identifier's string and the local name of the import specifier
  30. // which saves us some calls to the type checker.
  31. if (ts.isIdentifier(node) && node.text !== importSpecifier.name.text) {
  32. return false;
  33. }
  34. const nodeSymbol = typeChecker.getTypeAtLocation(node).getSymbol();
  35. const importSymbol = typeChecker.getTypeAtLocation(importSpecifier).getSymbol();
  36. return (!!(nodeSymbol?.declarations?.[0] && importSymbol?.declarations?.[0]) &&
  37. nodeSymbol.declarations[0] === importSymbol.declarations[0]);
  38. }
  39. /*!
  40. * @license
  41. * Copyright Google LLC All Rights Reserved.
  42. *
  43. * Use of this source code is governed by an MIT-style license that can be
  44. * found in the LICENSE file at https://angular.dev/license
  45. */
  46. /** Utility class used to track a one-to-many relationship where all the items are unique. */
  47. class UniqueItemTracker {
  48. _nodes = new Map();
  49. track(key, item) {
  50. const set = this._nodes.get(key);
  51. if (set) {
  52. set.add(item);
  53. }
  54. else {
  55. this._nodes.set(key, new Set([item]));
  56. }
  57. }
  58. get(key) {
  59. return this._nodes.get(key);
  60. }
  61. getEntries() {
  62. return this._nodes.entries();
  63. }
  64. isEmpty() {
  65. return this._nodes.size === 0;
  66. }
  67. }
  68. /** Resolves references to nodes. */
  69. class ReferenceResolver {
  70. _program;
  71. _host;
  72. _rootFileNames;
  73. _basePath;
  74. _excludedFiles;
  75. _languageService;
  76. /**
  77. * If set, allows the language service to *only* read a specific file.
  78. * Used to speed up single-file lookups.
  79. */
  80. _tempOnlyFile = null;
  81. constructor(_program, _host, _rootFileNames, _basePath, _excludedFiles) {
  82. this._program = _program;
  83. this._host = _host;
  84. this._rootFileNames = _rootFileNames;
  85. this._basePath = _basePath;
  86. this._excludedFiles = _excludedFiles;
  87. }
  88. /** Finds all references to a node within the entire project. */
  89. findReferencesInProject(node) {
  90. const languageService = this._getLanguageService();
  91. const fileName = node.getSourceFile().fileName;
  92. const start = node.getStart();
  93. let referencedSymbols;
  94. // The language service can throw if it fails to read a file.
  95. // Silently continue since we're making the lookup on a best effort basis.
  96. try {
  97. referencedSymbols = languageService.findReferences(fileName, start) || [];
  98. }
  99. catch (e) {
  100. console.error('Failed reference lookup for node ' + node.getText(), e.message);
  101. referencedSymbols = [];
  102. }
  103. const results = new Map();
  104. for (const symbol of referencedSymbols) {
  105. for (const ref of symbol.references) {
  106. if (!ref.isDefinition || symbol.definition.kind === ts.ScriptElementKind.alias) {
  107. if (!results.has(ref.fileName)) {
  108. results.set(ref.fileName, []);
  109. }
  110. results
  111. .get(ref.fileName)
  112. .push([ref.textSpan.start, ref.textSpan.start + ref.textSpan.length]);
  113. }
  114. }
  115. }
  116. return results;
  117. }
  118. /** Finds all references to a node within a single file. */
  119. findSameFileReferences(node, fileName) {
  120. // Even though we're only passing in a single file into `getDocumentHighlights`, the language
  121. // service ends up traversing the entire project. Prevent it from reading any files aside from
  122. // the one we're interested in by intercepting it at the compiler host level.
  123. // This is an order of magnitude faster on a large project.
  124. this._tempOnlyFile = fileName;
  125. const nodeStart = node.getStart();
  126. const results = [];
  127. let highlights;
  128. // The language service can throw if it fails to read a file.
  129. // Silently continue since we're making the lookup on a best effort basis.
  130. try {
  131. highlights = this._getLanguageService().getDocumentHighlights(fileName, nodeStart, [
  132. fileName,
  133. ]);
  134. }
  135. catch (e) {
  136. console.error('Failed reference lookup for node ' + node.getText(), e.message);
  137. }
  138. if (highlights) {
  139. for (const file of highlights) {
  140. // We are pretty much guaranteed to only have one match from the current file since it is
  141. // the only one being passed in `getDocumentHighlight`, but we check here just in case.
  142. if (file.fileName === fileName) {
  143. for (const { textSpan: { start, length }, kind, } of file.highlightSpans) {
  144. if (kind !== ts.HighlightSpanKind.none) {
  145. results.push([start, start + length]);
  146. }
  147. }
  148. }
  149. }
  150. }
  151. // Restore full project access to the language service.
  152. this._tempOnlyFile = null;
  153. return results;
  154. }
  155. /** Used by the language service */
  156. _readFile(path) {
  157. if ((this._tempOnlyFile !== null && path !== this._tempOnlyFile) ||
  158. this._excludedFiles?.test(path)) {
  159. return '';
  160. }
  161. return this._host.readFile(path);
  162. }
  163. /** Gets a language service that can be used to perform lookups. */
  164. _getLanguageService() {
  165. if (!this._languageService) {
  166. const rootFileNames = this._rootFileNames.slice();
  167. this._program
  168. .getTsProgram()
  169. .getSourceFiles()
  170. .forEach(({ fileName }) => {
  171. if (!this._excludedFiles?.test(fileName) && !rootFileNames.includes(fileName)) {
  172. rootFileNames.push(fileName);
  173. }
  174. });
  175. this._languageService = ts.createLanguageService({
  176. getCompilationSettings: () => this._program.getTsProgram().getCompilerOptions(),
  177. getScriptFileNames: () => rootFileNames,
  178. // The files won't change so we can return the same version.
  179. getScriptVersion: () => '0',
  180. getScriptSnapshot: (path) => {
  181. const content = this._readFile(path);
  182. return content ? ts.ScriptSnapshot.fromString(content) : undefined;
  183. },
  184. getCurrentDirectory: () => this._basePath,
  185. getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
  186. readFile: (path) => this._readFile(path),
  187. fileExists: (path) => this._host.fileExists(path),
  188. }, ts.createDocumentRegistry(), ts.LanguageServiceMode.PartialSemantic);
  189. }
  190. return this._languageService;
  191. }
  192. }
  193. /** Creates a NodeLookup object from a source file. */
  194. function getNodeLookup(sourceFile) {
  195. const lookup = new Map();
  196. sourceFile.forEachChild(function walk(node) {
  197. const nodesAtStart = lookup.get(node.getStart());
  198. if (nodesAtStart) {
  199. nodesAtStart.push(node);
  200. }
  201. else {
  202. lookup.set(node.getStart(), [node]);
  203. }
  204. node.forEachChild(walk);
  205. });
  206. return lookup;
  207. }
  208. /**
  209. * Converts node offsets to the nodes they correspond to.
  210. * @param lookup Data structure used to look up nodes at particular positions.
  211. * @param offsets Offsets of the nodes.
  212. * @param results Set in which to store the results.
  213. */
  214. function offsetsToNodes(lookup, offsets, results) {
  215. for (const [start, end] of offsets) {
  216. const match = lookup.get(start)?.find((node) => node.getEnd() === end);
  217. if (match) {
  218. results.add(match);
  219. }
  220. }
  221. return results;
  222. }
  223. /**
  224. * Finds the class declaration that is being referred to by a node.
  225. * @param reference Node referring to a class declaration.
  226. * @param typeChecker
  227. */
  228. function findClassDeclaration(reference, typeChecker) {
  229. return (typeChecker
  230. .getTypeAtLocation(reference)
  231. .getSymbol()
  232. ?.declarations?.find(ts.isClassDeclaration) || null);
  233. }
  234. /** Finds a property with a specific name in an object literal expression. */
  235. function findLiteralProperty(literal, name) {
  236. return literal.properties.find((prop) => prop.name && ts.isIdentifier(prop.name) && prop.name.text === name);
  237. }
  238. /** Gets a relative path between two files that can be used inside a TypeScript import. */
  239. function getRelativeImportPath(fromFile, toFile) {
  240. let path = p.relative(p.dirname(fromFile), toFile).replace(/\.ts$/, '');
  241. // `relative` returns paths inside the same directory without `./`
  242. if (!path.startsWith('.')) {
  243. path = './' + path;
  244. }
  245. // Using the Node utilities can yield paths with forward slashes on Windows.
  246. return compiler_host.normalizePath(path);
  247. }
  248. /** Function used to remap the generated `imports` for a component to known shorter aliases. */
  249. function knownInternalAliasRemapper(imports) {
  250. return imports.map((current) => current.moduleSpecifier === '@angular/common' && current.symbolName === 'NgForOf'
  251. ? { ...current, symbolName: 'NgFor' }
  252. : current);
  253. }
  254. /**
  255. * Gets the closest node that matches a predicate, including the node that the search started from.
  256. * @param node Node from which to start the search.
  257. * @param predicate Predicate that the result needs to pass.
  258. */
  259. function closestOrSelf(node, predicate) {
  260. return predicate(node) ? node : nodes.closestNode(node, predicate);
  261. }
  262. /**
  263. * Checks whether a node is referring to a specific class declaration.
  264. * @param node Node that is being checked.
  265. * @param className Name of the class that the node might be referring to.
  266. * @param moduleName Name of the Angular module that should contain the class.
  267. * @param typeChecker
  268. */
  269. function isClassReferenceInAngularModule(node, className, moduleName, typeChecker) {
  270. const symbol = typeChecker.getTypeAtLocation(node).getSymbol();
  271. const externalName = `@angular/${moduleName}`;
  272. const internalName = `angular2/rc/packages/${moduleName}`;
  273. return !!symbol?.declarations?.some((decl) => {
  274. const closestClass = closestOrSelf(decl, ts.isClassDeclaration);
  275. const closestClassFileName = closestClass?.getSourceFile().fileName;
  276. if (!closestClass ||
  277. !closestClassFileName ||
  278. !closestClass.name ||
  279. !ts.isIdentifier(closestClass.name) ||
  280. (!closestClassFileName.includes(externalName) && !closestClassFileName.includes(internalName))) {
  281. return false;
  282. }
  283. return typeof className === 'string'
  284. ? closestClass.name.text === className
  285. : className.test(closestClass.name.text);
  286. });
  287. }
  288. /**
  289. * Finds the imports of testing libraries in a file.
  290. */
  291. function getTestingImports(sourceFile) {
  292. return {
  293. testBed: imports.getImportSpecifier(sourceFile, '@angular/core/testing', 'TestBed'),
  294. catalyst: imports.getImportSpecifier(sourceFile, /testing\/catalyst(\/(fake_)?async)?$/, 'setupModule'),
  295. };
  296. }
  297. /**
  298. * Determines if a node is a call to a testing API.
  299. * @param typeChecker Type checker to use when resolving references.
  300. * @param node Node to check.
  301. * @param testBedImport Import of TestBed within the file.
  302. * @param catalystImport Import of Catalyst within the file.
  303. */
  304. function isTestCall(typeChecker, node, testBedImport, catalystImport) {
  305. const isObjectLiteralCall = ts.isCallExpression(node) &&
  306. node.arguments.length > 0 &&
  307. // `arguments[0]` is the testing module config.
  308. ts.isObjectLiteralExpression(node.arguments[0]);
  309. const isTestBedCall = isObjectLiteralCall &&
  310. testBedImport &&
  311. ts.isPropertyAccessExpression(node.expression) &&
  312. node.expression.name.text === 'configureTestingModule' &&
  313. isReferenceToImport(typeChecker, node.expression.expression, testBedImport);
  314. const isCatalystCall = isObjectLiteralCall &&
  315. catalystImport &&
  316. ts.isIdentifier(node.expression) &&
  317. isReferenceToImport(typeChecker, node.expression, catalystImport);
  318. return !!(isTestBedCall || isCatalystCall);
  319. }
  320. /*!
  321. * @license
  322. * Copyright Google LLC All Rights Reserved.
  323. *
  324. * Use of this source code is governed by an MIT-style license that can be
  325. * found in the LICENSE file at https://angular.dev/license
  326. */
  327. /**
  328. * Converts all declarations in the specified files to standalone.
  329. * @param sourceFiles Files that should be migrated.
  330. * @param program
  331. * @param printer
  332. * @param fileImportRemapper Optional function that can be used to remap file-level imports.
  333. * @param declarationImportRemapper Optional function that can be used to remap declaration-level
  334. * imports.
  335. */
  336. function toStandalone(sourceFiles, program, printer, fileImportRemapper, declarationImportRemapper) {
  337. const templateTypeChecker = program.compiler.getTemplateTypeChecker();
  338. const typeChecker = program.getTsProgram().getTypeChecker();
  339. const modulesToMigrate = new Set();
  340. const testObjectsToMigrate = new Set();
  341. const declarations = new Set();
  342. const tracker = new compiler_host.ChangeTracker(printer, fileImportRemapper);
  343. for (const sourceFile of sourceFiles) {
  344. const modules = findNgModuleClassesToMigrate(sourceFile, typeChecker);
  345. const testObjects = findTestObjectsToMigrate(sourceFile, typeChecker);
  346. for (const module of modules) {
  347. const allModuleDeclarations = extractDeclarationsFromModule(module, templateTypeChecker);
  348. const unbootstrappedDeclarations = filterNonBootstrappedDeclarations(allModuleDeclarations, module, templateTypeChecker, typeChecker);
  349. if (unbootstrappedDeclarations.length > 0) {
  350. modulesToMigrate.add(module);
  351. unbootstrappedDeclarations.forEach((decl) => declarations.add(decl));
  352. }
  353. }
  354. testObjects.forEach((obj) => testObjectsToMigrate.add(obj));
  355. }
  356. for (const declaration of declarations) {
  357. convertNgModuleDeclarationToStandalone(declaration, declarations, tracker, templateTypeChecker, declarationImportRemapper);
  358. }
  359. for (const node of modulesToMigrate) {
  360. migrateNgModuleClass(node, declarations, tracker, typeChecker, templateTypeChecker);
  361. }
  362. migrateTestDeclarations(testObjectsToMigrate, declarations, tracker, templateTypeChecker, typeChecker);
  363. return tracker.recordChanges();
  364. }
  365. /**
  366. * Converts a single declaration defined through an NgModule to standalone.
  367. * @param decl Declaration being converted.
  368. * @param tracker Tracker used to track the file changes.
  369. * @param allDeclarations All the declarations that are being converted as a part of this migration.
  370. * @param typeChecker
  371. * @param importRemapper
  372. */
  373. function convertNgModuleDeclarationToStandalone(decl, allDeclarations, tracker, typeChecker, importRemapper) {
  374. const directiveMeta = typeChecker.getDirectiveMetadata(decl);
  375. if (directiveMeta && directiveMeta.decorator && !directiveMeta.isStandalone) {
  376. let decorator = markDecoratorAsStandalone(directiveMeta.decorator);
  377. if (directiveMeta.isComponent) {
  378. const importsToAdd = getComponentImportExpressions(decl, allDeclarations, tracker, typeChecker, importRemapper);
  379. if (importsToAdd.length > 0) {
  380. const hasTrailingComma = importsToAdd.length > 2 &&
  381. !!extractMetadataLiteral(directiveMeta.decorator)?.properties.hasTrailingComma;
  382. decorator = setPropertyOnAngularDecorator(decorator, 'imports', ts.factory.createArrayLiteralExpression(
  383. // Create a multi-line array when it has a trailing comma.
  384. ts.factory.createNodeArray(importsToAdd, hasTrailingComma), hasTrailingComma));
  385. }
  386. }
  387. tracker.replaceNode(directiveMeta.decorator, decorator);
  388. }
  389. else {
  390. const pipeMeta = typeChecker.getPipeMetadata(decl);
  391. if (pipeMeta && pipeMeta.decorator && !pipeMeta.isStandalone) {
  392. tracker.replaceNode(pipeMeta.decorator, markDecoratorAsStandalone(pipeMeta.decorator));
  393. }
  394. }
  395. }
  396. /**
  397. * Gets the expressions that should be added to a component's
  398. * `imports` array based on its template dependencies.
  399. * @param decl Component class declaration.
  400. * @param allDeclarations All the declarations that are being converted as a part of this migration.
  401. * @param tracker
  402. * @param typeChecker
  403. * @param importRemapper
  404. */
  405. function getComponentImportExpressions(decl, allDeclarations, tracker, typeChecker, importRemapper) {
  406. const templateDependencies = findTemplateDependencies(decl, typeChecker);
  407. const usedDependenciesInMigration = new Set(templateDependencies.filter((dep) => allDeclarations.has(dep.node)));
  408. const seenImports = new Set();
  409. const resolvedDependencies = [];
  410. for (const dep of templateDependencies) {
  411. const importLocation = findImportLocation(dep, decl, usedDependenciesInMigration.has(dep)
  412. ? checker.PotentialImportMode.ForceDirect
  413. : checker.PotentialImportMode.Normal, typeChecker);
  414. if (importLocation && !seenImports.has(importLocation.symbolName)) {
  415. seenImports.add(importLocation.symbolName);
  416. resolvedDependencies.push(importLocation);
  417. }
  418. }
  419. return potentialImportsToExpressions(resolvedDependencies, decl.getSourceFile(), tracker, importRemapper);
  420. }
  421. /**
  422. * Converts an array of potential imports to an array of expressions that can be
  423. * added to the `imports` array.
  424. * @param potentialImports Imports to be converted.
  425. * @param component Component class to which the imports will be added.
  426. * @param tracker
  427. * @param importRemapper
  428. */
  429. function potentialImportsToExpressions(potentialImports, toFile, tracker, importRemapper) {
  430. const processedDependencies = importRemapper
  431. ? importRemapper(potentialImports)
  432. : potentialImports;
  433. return processedDependencies.map((importLocation) => {
  434. if (importLocation.moduleSpecifier) {
  435. return tracker.addImport(toFile, importLocation.symbolName, importLocation.moduleSpecifier);
  436. }
  437. const identifier = ts.factory.createIdentifier(importLocation.symbolName);
  438. if (!importLocation.isForwardReference) {
  439. return identifier;
  440. }
  441. const forwardRefExpression = tracker.addImport(toFile, 'forwardRef', '@angular/core');
  442. const arrowFunction = ts.factory.createArrowFunction(undefined, undefined, [], undefined, undefined, identifier);
  443. return ts.factory.createCallExpression(forwardRefExpression, undefined, [arrowFunction]);
  444. });
  445. }
  446. /**
  447. * Moves all of the declarations of a class decorated with `@NgModule` to its imports.
  448. * @param node Class being migrated.
  449. * @param allDeclarations All the declarations that are being converted as a part of this migration.
  450. * @param tracker
  451. * @param typeChecker
  452. * @param templateTypeChecker
  453. */
  454. function migrateNgModuleClass(node, allDeclarations, tracker, typeChecker, templateTypeChecker) {
  455. const decorator = templateTypeChecker.getNgModuleMetadata(node)?.decorator;
  456. const metadata = decorator ? extractMetadataLiteral(decorator) : null;
  457. if (metadata) {
  458. moveDeclarationsToImports(metadata, allDeclarations, typeChecker, templateTypeChecker, tracker);
  459. }
  460. }
  461. /**
  462. * Moves all the symbol references from the `declarations` array to the `imports`
  463. * array of an `NgModule` class and removes the `declarations`.
  464. * @param literal Object literal used to configure the module that should be migrated.
  465. * @param allDeclarations All the declarations that are being converted as a part of this migration.
  466. * @param typeChecker
  467. * @param tracker
  468. */
  469. function moveDeclarationsToImports(literal, allDeclarations, typeChecker, templateTypeChecker, tracker) {
  470. const declarationsProp = findLiteralProperty(literal, 'declarations');
  471. if (!declarationsProp) {
  472. return;
  473. }
  474. const declarationsToPreserve = [];
  475. const declarationsToCopy = [];
  476. const properties = [];
  477. const importsProp = findLiteralProperty(literal, 'imports');
  478. const hasAnyArrayTrailingComma = literal.properties.some((prop) => ts.isPropertyAssignment(prop) &&
  479. ts.isArrayLiteralExpression(prop.initializer) &&
  480. prop.initializer.elements.hasTrailingComma);
  481. // Separate the declarations that we want to keep and ones we need to copy into the `imports`.
  482. if (ts.isPropertyAssignment(declarationsProp)) {
  483. // If the declarations are an array, we can analyze it to
  484. // find any classes from the current migration.
  485. if (ts.isArrayLiteralExpression(declarationsProp.initializer)) {
  486. for (const el of declarationsProp.initializer.elements) {
  487. if (ts.isIdentifier(el)) {
  488. const correspondingClass = findClassDeclaration(el, typeChecker);
  489. if (!correspondingClass ||
  490. // Check whether the declaration is either standalone already or is being converted
  491. // in this migration. We need to check if it's standalone already, in order to correct
  492. // some cases where the main app and the test files are being migrated in separate
  493. // programs.
  494. isStandaloneDeclaration(correspondingClass, allDeclarations, templateTypeChecker)) {
  495. declarationsToCopy.push(el);
  496. }
  497. else {
  498. declarationsToPreserve.push(el);
  499. }
  500. }
  501. else {
  502. declarationsToCopy.push(el);
  503. }
  504. }
  505. }
  506. else {
  507. // Otherwise create a spread that will be copied into the `imports`.
  508. declarationsToCopy.push(ts.factory.createSpreadElement(declarationsProp.initializer));
  509. }
  510. }
  511. // If there are no `imports`, create them with the declarations we want to copy.
  512. if (!importsProp && declarationsToCopy.length > 0) {
  513. properties.push(ts.factory.createPropertyAssignment('imports', ts.factory.createArrayLiteralExpression(ts.factory.createNodeArray(declarationsToCopy, hasAnyArrayTrailingComma && declarationsToCopy.length > 2))));
  514. }
  515. for (const prop of literal.properties) {
  516. if (!isNamedPropertyAssignment(prop)) {
  517. properties.push(prop);
  518. continue;
  519. }
  520. // If we have declarations to preserve, update the existing property, otherwise drop it.
  521. if (prop === declarationsProp) {
  522. if (declarationsToPreserve.length > 0) {
  523. const hasTrailingComma = ts.isArrayLiteralExpression(prop.initializer)
  524. ? prop.initializer.elements.hasTrailingComma
  525. : hasAnyArrayTrailingComma;
  526. properties.push(ts.factory.updatePropertyAssignment(prop, prop.name, ts.factory.createArrayLiteralExpression(ts.factory.createNodeArray(declarationsToPreserve, hasTrailingComma && declarationsToPreserve.length > 2))));
  527. }
  528. continue;
  529. }
  530. // If we have an `imports` array and declarations
  531. // that should be copied, we merge the two arrays.
  532. if (prop === importsProp && declarationsToCopy.length > 0) {
  533. let initializer;
  534. if (ts.isArrayLiteralExpression(prop.initializer)) {
  535. initializer = ts.factory.updateArrayLiteralExpression(prop.initializer, ts.factory.createNodeArray([...prop.initializer.elements, ...declarationsToCopy], prop.initializer.elements.hasTrailingComma));
  536. }
  537. else {
  538. initializer = ts.factory.createArrayLiteralExpression(ts.factory.createNodeArray([ts.factory.createSpreadElement(prop.initializer), ...declarationsToCopy],
  539. // Expect the declarations to be greater than 1 since
  540. // we have the pre-existing initializer already.
  541. hasAnyArrayTrailingComma && declarationsToCopy.length > 1));
  542. }
  543. properties.push(ts.factory.updatePropertyAssignment(prop, prop.name, initializer));
  544. continue;
  545. }
  546. // Retain any remaining properties.
  547. properties.push(prop);
  548. }
  549. tracker.replaceNode(literal, ts.factory.updateObjectLiteralExpression(literal, ts.factory.createNodeArray(properties, literal.properties.hasTrailingComma)), ts.EmitHint.Expression);
  550. }
  551. /** Sets a decorator node to be standalone. */
  552. function markDecoratorAsStandalone(node) {
  553. const metadata = extractMetadataLiteral(node);
  554. if (metadata === null || !ts.isCallExpression(node.expression)) {
  555. return node;
  556. }
  557. const standaloneProp = metadata.properties.find((prop) => {
  558. return isNamedPropertyAssignment(prop) && prop.name.text === 'standalone';
  559. });
  560. // In v19 standalone is the default so don't do anything if there's no `standalone`
  561. // property or it's initialized to anything other than `false`.
  562. if (!standaloneProp || standaloneProp.initializer.kind !== ts.SyntaxKind.FalseKeyword) {
  563. return node;
  564. }
  565. const newProperties = metadata.properties.filter((element) => element !== standaloneProp);
  566. // Use `createDecorator` instead of `updateDecorator`, because
  567. // the latter ends up duplicating the node's leading comment.
  568. return ts.factory.createDecorator(ts.factory.createCallExpression(node.expression.expression, node.expression.typeArguments, [
  569. ts.factory.createObjectLiteralExpression(ts.factory.createNodeArray(newProperties, metadata.properties.hasTrailingComma), newProperties.length > 1),
  570. ]));
  571. }
  572. /**
  573. * Sets a property on an Angular decorator node. If the property
  574. * already exists, its initializer will be replaced.
  575. * @param node Decorator to which to add the property.
  576. * @param name Name of the property to be added.
  577. * @param initializer Initializer for the new property.
  578. */
  579. function setPropertyOnAngularDecorator(node, name, initializer) {
  580. // Invalid decorator.
  581. if (!ts.isCallExpression(node.expression) || node.expression.arguments.length > 1) {
  582. return node;
  583. }
  584. let literalProperties;
  585. let hasTrailingComma = false;
  586. if (node.expression.arguments.length === 0) {
  587. literalProperties = [ts.factory.createPropertyAssignment(name, initializer)];
  588. }
  589. else if (ts.isObjectLiteralExpression(node.expression.arguments[0])) {
  590. const literal = node.expression.arguments[0];
  591. const existingProperty = findLiteralProperty(literal, name);
  592. hasTrailingComma = literal.properties.hasTrailingComma;
  593. if (existingProperty && ts.isPropertyAssignment(existingProperty)) {
  594. literalProperties = literal.properties.slice();
  595. literalProperties[literalProperties.indexOf(existingProperty)] =
  596. ts.factory.updatePropertyAssignment(existingProperty, existingProperty.name, initializer);
  597. }
  598. else {
  599. literalProperties = [
  600. ...literal.properties,
  601. ts.factory.createPropertyAssignment(name, initializer),
  602. ];
  603. }
  604. }
  605. else {
  606. // Unsupported case (e.g. `@Component(SOME_CONST)`). Return the original node.
  607. return node;
  608. }
  609. // Use `createDecorator` instead of `updateDecorator`, because
  610. // the latter ends up duplicating the node's leading comment.
  611. return ts.factory.createDecorator(ts.factory.createCallExpression(node.expression.expression, node.expression.typeArguments, [
  612. ts.factory.createObjectLiteralExpression(ts.factory.createNodeArray(literalProperties, hasTrailingComma), literalProperties.length > 1),
  613. ]));
  614. }
  615. /** Checks if a node is a `PropertyAssignment` with a name. */
  616. function isNamedPropertyAssignment(node) {
  617. return ts.isPropertyAssignment(node) && node.name && ts.isIdentifier(node.name);
  618. }
  619. /**
  620. * Finds the import from which to bring in a template dependency of a component.
  621. * @param target Dependency that we're searching for.
  622. * @param inContext Component in which the dependency is used.
  623. * @param importMode Mode in which to resolve the import target.
  624. * @param typeChecker
  625. */
  626. function findImportLocation(target, inContext, importMode, typeChecker) {
  627. const importLocations = typeChecker.getPotentialImportsFor(target, inContext, importMode);
  628. let firstSameFileImport = null;
  629. let firstModuleImport = null;
  630. for (const location of importLocations) {
  631. // Prefer a standalone import, if we can find one.
  632. // Otherwise fall back to the first module-based import.
  633. if (location.kind === checker.PotentialImportKind.Standalone) {
  634. return location;
  635. }
  636. if (!location.moduleSpecifier && !firstSameFileImport) {
  637. firstSameFileImport = location;
  638. }
  639. if (location.kind === checker.PotentialImportKind.NgModule &&
  640. !firstModuleImport &&
  641. // ɵ is used for some internal Angular modules that we want to skip over.
  642. !location.symbolName.startsWith('ɵ')) {
  643. firstModuleImport = location;
  644. }
  645. }
  646. return firstSameFileImport || firstModuleImport || importLocations[0] || null;
  647. }
  648. /**
  649. * Checks whether a node is an `NgModule` metadata element with at least one element.
  650. * E.g. `declarations: [Foo]` or `declarations: SOME_VAR` would match this description,
  651. * but not `declarations: []`.
  652. */
  653. function hasNgModuleMetadataElements(node) {
  654. return (ts.isPropertyAssignment(node) &&
  655. (!ts.isArrayLiteralExpression(node.initializer) || node.initializer.elements.length > 0));
  656. }
  657. /** Finds all modules whose declarations can be migrated. */
  658. function findNgModuleClassesToMigrate(sourceFile, typeChecker) {
  659. const modules = [];
  660. if (imports.getImportSpecifier(sourceFile, '@angular/core', 'NgModule')) {
  661. sourceFile.forEachChild(function walk(node) {
  662. if (ts.isClassDeclaration(node)) {
  663. const decorator = ng_decorators.getAngularDecorators(typeChecker, ts.getDecorators(node) || []).find((current) => current.name === 'NgModule');
  664. const metadata = decorator ? extractMetadataLiteral(decorator.node) : null;
  665. if (metadata) {
  666. const declarations = findLiteralProperty(metadata, 'declarations');
  667. if (declarations != null && hasNgModuleMetadataElements(declarations)) {
  668. modules.push(node);
  669. }
  670. }
  671. }
  672. node.forEachChild(walk);
  673. });
  674. }
  675. return modules;
  676. }
  677. /** Finds all testing object literals that need to be migrated. */
  678. function findTestObjectsToMigrate(sourceFile, typeChecker) {
  679. const testObjects = [];
  680. const { testBed, catalyst } = getTestingImports(sourceFile);
  681. if (testBed || catalyst) {
  682. sourceFile.forEachChild(function walk(node) {
  683. if (isTestCall(typeChecker, node, testBed, catalyst)) {
  684. const config = node.arguments[0];
  685. const declarations = findLiteralProperty(config, 'declarations');
  686. if (declarations &&
  687. ts.isPropertyAssignment(declarations) &&
  688. ts.isArrayLiteralExpression(declarations.initializer) &&
  689. declarations.initializer.elements.length > 0) {
  690. testObjects.push(config);
  691. }
  692. }
  693. node.forEachChild(walk);
  694. });
  695. }
  696. return testObjects;
  697. }
  698. /**
  699. * Finds the classes corresponding to dependencies used in a component's template.
  700. * @param decl Component in whose template we're looking for dependencies.
  701. * @param typeChecker
  702. */
  703. function findTemplateDependencies(decl, typeChecker) {
  704. const results = [];
  705. const usedDirectives = typeChecker.getUsedDirectives(decl);
  706. const usedPipes = typeChecker.getUsedPipes(decl);
  707. if (usedDirectives !== null) {
  708. for (const dir of usedDirectives) {
  709. if (ts.isClassDeclaration(dir.ref.node)) {
  710. results.push(dir.ref);
  711. }
  712. }
  713. }
  714. if (usedPipes !== null) {
  715. const potentialPipes = typeChecker.getPotentialPipes(decl);
  716. for (const pipe of potentialPipes) {
  717. if (ts.isClassDeclaration(pipe.ref.node) &&
  718. usedPipes.some((current) => pipe.name === current)) {
  719. results.push(pipe.ref);
  720. }
  721. }
  722. }
  723. return results;
  724. }
  725. /**
  726. * Removes any declarations that are a part of a module's `bootstrap`
  727. * array from an array of declarations.
  728. * @param declarations Anaalyzed declarations of the module.
  729. * @param ngModule Module whote declarations are being filtered.
  730. * @param templateTypeChecker
  731. * @param typeChecker
  732. */
  733. function filterNonBootstrappedDeclarations(declarations, ngModule, templateTypeChecker, typeChecker) {
  734. const metadata = templateTypeChecker.getNgModuleMetadata(ngModule);
  735. const metaLiteral = metadata && metadata.decorator ? extractMetadataLiteral(metadata.decorator) : null;
  736. const bootstrapProp = metaLiteral ? findLiteralProperty(metaLiteral, 'bootstrap') : null;
  737. // If there's no `bootstrap`, we can't filter.
  738. if (!bootstrapProp) {
  739. return declarations;
  740. }
  741. // If we can't analyze the `bootstrap` property, we can't safely determine which
  742. // declarations aren't bootstrapped so we assume that all of them are.
  743. if (!ts.isPropertyAssignment(bootstrapProp) ||
  744. !ts.isArrayLiteralExpression(bootstrapProp.initializer)) {
  745. return [];
  746. }
  747. const bootstrappedClasses = new Set();
  748. for (const el of bootstrapProp.initializer.elements) {
  749. const referencedClass = ts.isIdentifier(el) ? findClassDeclaration(el, typeChecker) : null;
  750. // If we can resolve an element to a class, we can filter it out,
  751. // otherwise assume that the array isn't static.
  752. if (referencedClass) {
  753. bootstrappedClasses.add(referencedClass);
  754. }
  755. else {
  756. return [];
  757. }
  758. }
  759. return declarations.filter((ref) => !bootstrappedClasses.has(ref));
  760. }
  761. /**
  762. * Extracts all classes that are referenced in a module's `declarations` array.
  763. * @param ngModule Module whose declarations are being extraced.
  764. * @param templateTypeChecker
  765. */
  766. function extractDeclarationsFromModule(ngModule, templateTypeChecker) {
  767. const metadata = templateTypeChecker.getNgModuleMetadata(ngModule);
  768. return metadata
  769. ? metadata.declarations
  770. .filter((decl) => ts.isClassDeclaration(decl.node))
  771. .map((decl) => decl.node)
  772. : [];
  773. }
  774. /**
  775. * Migrates the `declarations` from a unit test file to standalone.
  776. * @param testObjects Object literals used to configure the testing modules.
  777. * @param declarationsOutsideOfTestFiles Non-testing declarations that are part of this migration.
  778. * @param tracker
  779. * @param templateTypeChecker
  780. * @param typeChecker
  781. */
  782. function migrateTestDeclarations(testObjects, declarationsOutsideOfTestFiles, tracker, templateTypeChecker, typeChecker) {
  783. const { decorators, componentImports } = analyzeTestingModules(testObjects, typeChecker);
  784. const allDeclarations = new Set(declarationsOutsideOfTestFiles);
  785. for (const decorator of decorators) {
  786. const closestClass = nodes.closestNode(decorator.node, ts.isClassDeclaration);
  787. if (decorator.name === 'Pipe' || decorator.name === 'Directive') {
  788. tracker.replaceNode(decorator.node, markDecoratorAsStandalone(decorator.node));
  789. if (closestClass) {
  790. allDeclarations.add(closestClass);
  791. }
  792. }
  793. else if (decorator.name === 'Component') {
  794. const newDecorator = markDecoratorAsStandalone(decorator.node);
  795. const importsToAdd = componentImports.get(decorator.node);
  796. if (closestClass) {
  797. allDeclarations.add(closestClass);
  798. }
  799. if (importsToAdd && importsToAdd.size > 0) {
  800. const hasTrailingComma = importsToAdd.size > 2 &&
  801. !!extractMetadataLiteral(decorator.node)?.properties.hasTrailingComma;
  802. const importsArray = ts.factory.createNodeArray(Array.from(importsToAdd), hasTrailingComma);
  803. tracker.replaceNode(decorator.node, setPropertyOnAngularDecorator(newDecorator, 'imports', ts.factory.createArrayLiteralExpression(importsArray)));
  804. }
  805. else {
  806. tracker.replaceNode(decorator.node, newDecorator);
  807. }
  808. }
  809. }
  810. for (const obj of testObjects) {
  811. moveDeclarationsToImports(obj, allDeclarations, typeChecker, templateTypeChecker, tracker);
  812. }
  813. }
  814. /**
  815. * Analyzes a set of objects used to configure testing modules and returns the AST
  816. * nodes that need to be migrated and the imports that should be added to the imports
  817. * of any declared components.
  818. * @param testObjects Object literals that should be analyzed.
  819. */
  820. function analyzeTestingModules(testObjects, typeChecker) {
  821. const seenDeclarations = new Set();
  822. const decorators = [];
  823. const componentImports = new Map();
  824. for (const obj of testObjects) {
  825. const declarations = extractDeclarationsFromTestObject(obj, typeChecker);
  826. if (declarations.length === 0) {
  827. continue;
  828. }
  829. const importsProp = findLiteralProperty(obj, 'imports');
  830. const importElements = importsProp &&
  831. hasNgModuleMetadataElements(importsProp) &&
  832. ts.isArrayLiteralExpression(importsProp.initializer)
  833. ? importsProp.initializer.elements.filter((el) => {
  834. // Filter out calls since they may be a `ModuleWithProviders`.
  835. return (!ts.isCallExpression(el) &&
  836. // Also filter out the animations modules since they throw errors if they're imported
  837. // multiple times and it's common for apps to use the `NoopAnimationsModule` to
  838. // disable animations in screenshot tests.
  839. !isClassReferenceInAngularModule(el, /^BrowserAnimationsModule|NoopAnimationsModule$/, 'platform-browser/animations', typeChecker));
  840. })
  841. : null;
  842. for (const decl of declarations) {
  843. if (seenDeclarations.has(decl)) {
  844. continue;
  845. }
  846. const [decorator] = ng_decorators.getAngularDecorators(typeChecker, ts.getDecorators(decl) || []);
  847. if (decorator) {
  848. seenDeclarations.add(decl);
  849. decorators.push(decorator);
  850. if (decorator.name === 'Component' && importElements) {
  851. // We try to de-duplicate the imports being added to a component, because it may be
  852. // declared in different testing modules with a different set of imports.
  853. let imports = componentImports.get(decorator.node);
  854. if (!imports) {
  855. imports = new Set();
  856. componentImports.set(decorator.node, imports);
  857. }
  858. importElements.forEach((imp) => imports.add(imp));
  859. }
  860. }
  861. }
  862. }
  863. return { decorators, componentImports };
  864. }
  865. /**
  866. * Finds the class declarations that are being referred
  867. * to in the `declarations` of an object literal.
  868. * @param obj Object literal that may contain the declarations.
  869. * @param typeChecker
  870. */
  871. function extractDeclarationsFromTestObject(obj, typeChecker) {
  872. const results = [];
  873. const declarations = findLiteralProperty(obj, 'declarations');
  874. if (declarations &&
  875. hasNgModuleMetadataElements(declarations) &&
  876. ts.isArrayLiteralExpression(declarations.initializer)) {
  877. for (const element of declarations.initializer.elements) {
  878. const declaration = findClassDeclaration(element, typeChecker);
  879. // Note that we only migrate classes that are in the same file as the testing module,
  880. // because external fixture components are somewhat rare and handling them is going
  881. // to involve a lot of assumptions that are likely to be incorrect.
  882. if (declaration && declaration.getSourceFile().fileName === obj.getSourceFile().fileName) {
  883. results.push(declaration);
  884. }
  885. }
  886. }
  887. return results;
  888. }
  889. /** Extracts the metadata object literal from an Angular decorator. */
  890. function extractMetadataLiteral(decorator) {
  891. // `arguments[0]` is the metadata object literal.
  892. return ts.isCallExpression(decorator.expression) &&
  893. decorator.expression.arguments.length === 1 &&
  894. ts.isObjectLiteralExpression(decorator.expression.arguments[0])
  895. ? decorator.expression.arguments[0]
  896. : null;
  897. }
  898. /**
  899. * Checks whether a class is a standalone declaration.
  900. * @param node Class being checked.
  901. * @param declarationsInMigration Classes that are being converted to standalone in this migration.
  902. * @param templateTypeChecker
  903. */
  904. function isStandaloneDeclaration(node, declarationsInMigration, templateTypeChecker) {
  905. if (declarationsInMigration.has(node)) {
  906. return true;
  907. }
  908. const metadata = templateTypeChecker.getDirectiveMetadata(node) || templateTypeChecker.getPipeMetadata(node);
  909. return metadata != null && metadata.isStandalone;
  910. }
  911. /*!
  912. * @license
  913. * Copyright Google LLC All Rights Reserved.
  914. *
  915. * Use of this source code is governed by an MIT-style license that can be
  916. * found in the LICENSE file at https://angular.dev/license
  917. */
  918. function pruneNgModules(program, host, basePath, rootFileNames, sourceFiles, printer, importRemapper, referenceLookupExcludedFiles, declarationImportRemapper) {
  919. const filesToRemove = new Set();
  920. const tracker = new compiler_host.ChangeTracker(printer, importRemapper);
  921. const tsProgram = program.getTsProgram();
  922. const typeChecker = tsProgram.getTypeChecker();
  923. const templateTypeChecker = program.compiler.getTemplateTypeChecker();
  924. const referenceResolver = new ReferenceResolver(program, host, rootFileNames, basePath, referenceLookupExcludedFiles);
  925. const removalLocations = {
  926. arrays: new UniqueItemTracker(),
  927. imports: new UniqueItemTracker(),
  928. exports: new UniqueItemTracker(),
  929. unknown: new Set(),
  930. };
  931. const classesToRemove = new Set();
  932. const barrelExports = new UniqueItemTracker();
  933. const componentImportArrays = new UniqueItemTracker();
  934. const testArrays = new UniqueItemTracker();
  935. const nodesToRemove = new Set();
  936. sourceFiles.forEach(function walk(node) {
  937. if (ts.isClassDeclaration(node) && canRemoveClass(node, typeChecker)) {
  938. collectChangeLocations(node, removalLocations, componentImportArrays, testArrays, templateTypeChecker, referenceResolver, program);
  939. classesToRemove.add(node);
  940. }
  941. else if (ts.isExportDeclaration(node) &&
  942. !node.exportClause &&
  943. node.moduleSpecifier &&
  944. ts.isStringLiteralLike(node.moduleSpecifier) &&
  945. node.moduleSpecifier.text.startsWith('.')) {
  946. const exportedSourceFile = typeChecker
  947. .getSymbolAtLocation(node.moduleSpecifier)
  948. ?.valueDeclaration?.getSourceFile();
  949. if (exportedSourceFile) {
  950. barrelExports.track(exportedSourceFile, node);
  951. }
  952. }
  953. node.forEachChild(walk);
  954. });
  955. replaceInComponentImportsArray(componentImportArrays, classesToRemove, tracker, typeChecker, templateTypeChecker, declarationImportRemapper);
  956. replaceInTestImportsArray(testArrays, removalLocations, classesToRemove, tracker, typeChecker, templateTypeChecker, declarationImportRemapper);
  957. // We collect all the places where we need to remove references first before generating the
  958. // removal instructions since we may have to remove multiple references from one node.
  959. removeArrayReferences(removalLocations.arrays, tracker);
  960. removeImportReferences(removalLocations.imports, tracker);
  961. removeExportReferences(removalLocations.exports, tracker);
  962. addRemovalTodos(removalLocations.unknown, tracker);
  963. // Collect all the nodes to be removed before determining which files to delete since we need
  964. // to know it ahead of time when deleting barrel files that export other barrel files.
  965. (function trackNodesToRemove(nodes) {
  966. for (const node of nodes) {
  967. const sourceFile = node.getSourceFile();
  968. if (!filesToRemove.has(sourceFile) && canRemoveFile(sourceFile, nodes)) {
  969. const barrelExportsForFile = barrelExports.get(sourceFile);
  970. nodesToRemove.add(node);
  971. filesToRemove.add(sourceFile);
  972. barrelExportsForFile && trackNodesToRemove(barrelExportsForFile);
  973. }
  974. else {
  975. nodesToRemove.add(node);
  976. }
  977. }
  978. })(classesToRemove);
  979. for (const node of nodesToRemove) {
  980. const sourceFile = node.getSourceFile();
  981. if (!filesToRemove.has(sourceFile) && canRemoveFile(sourceFile, nodesToRemove)) {
  982. filesToRemove.add(sourceFile);
  983. }
  984. else {
  985. tracker.removeNode(node);
  986. }
  987. }
  988. return { pendingChanges: tracker.recordChanges(), filesToRemove };
  989. }
  990. /**
  991. * Collects all the nodes that a module needs to be removed from.
  992. * @param ngModule Module being removed.
  993. * @param removalLocations Tracks the different places from which the class should be removed.
  994. * @param componentImportArrays Set of `imports` arrays of components that need to be adjusted.
  995. * @param testImportArrays Set of `imports` arrays of tests that need to be adjusted.
  996. * @param referenceResolver
  997. * @param program
  998. */
  999. function collectChangeLocations(ngModule, removalLocations, componentImportArrays, testImportArrays, templateTypeChecker, referenceResolver, program) {
  1000. const refsByFile = referenceResolver.findReferencesInProject(ngModule.name);
  1001. const tsProgram = program.getTsProgram();
  1002. const typeChecker = tsProgram.getTypeChecker();
  1003. const nodes$1 = new Set();
  1004. for (const [fileName, refs] of refsByFile) {
  1005. const sourceFile = tsProgram.getSourceFile(fileName);
  1006. if (sourceFile) {
  1007. offsetsToNodes(getNodeLookup(sourceFile), refs, nodes$1);
  1008. }
  1009. }
  1010. for (const node of nodes$1) {
  1011. const closestArray = nodes.closestNode(node, ts.isArrayLiteralExpression);
  1012. if (closestArray) {
  1013. const closestAssignment = nodes.closestNode(closestArray, ts.isPropertyAssignment);
  1014. if (closestAssignment && isInImportsArray(closestAssignment, closestArray)) {
  1015. const closestCall = nodes.closestNode(closestAssignment, ts.isCallExpression);
  1016. if (closestCall) {
  1017. const closestDecorator = nodes.closestNode(closestCall, ts.isDecorator);
  1018. const closestClass = closestDecorator
  1019. ? nodes.closestNode(closestDecorator, ts.isClassDeclaration)
  1020. : null;
  1021. const directiveMeta = closestClass
  1022. ? templateTypeChecker.getDirectiveMetadata(closestClass)
  1023. : null;
  1024. // If the module was flagged as being removable, but it's still being used in a
  1025. // standalone component's `imports` array, it means that it was likely changed
  1026. // outside of the migration and deleting it now will be breaking. Track it
  1027. // separately so it can be handled properly.
  1028. if (directiveMeta && directiveMeta.isComponent && directiveMeta.isStandalone) {
  1029. componentImportArrays.track(closestArray, node);
  1030. continue;
  1031. }
  1032. // If the module is removable and used inside a test's `imports`,
  1033. // we track it separately so it can be replaced with its `exports`.
  1034. const { testBed, catalyst } = getTestingImports(node.getSourceFile());
  1035. if (isTestCall(typeChecker, closestCall, testBed, catalyst)) {
  1036. testImportArrays.track(closestArray, node);
  1037. continue;
  1038. }
  1039. }
  1040. }
  1041. removalLocations.arrays.track(closestArray, node);
  1042. continue;
  1043. }
  1044. const closestImport = nodes.closestNode(node, ts.isNamedImports);
  1045. if (closestImport) {
  1046. removalLocations.imports.track(closestImport, node);
  1047. continue;
  1048. }
  1049. const closestExport = nodes.closestNode(node, ts.isNamedExports);
  1050. if (closestExport) {
  1051. removalLocations.exports.track(closestExport, node);
  1052. continue;
  1053. }
  1054. removalLocations.unknown.add(node);
  1055. }
  1056. }
  1057. /**
  1058. * Replaces all the leftover modules in component `imports` arrays with their exports.
  1059. * @param componentImportArrays All the imports arrays and their nodes that represent NgModules.
  1060. * @param classesToRemove Set of classes that were marked for removal.
  1061. * @param tracker
  1062. * @param typeChecker
  1063. * @param templateTypeChecker
  1064. * @param importRemapper
  1065. */
  1066. function replaceInComponentImportsArray(componentImportArrays, classesToRemove, tracker, typeChecker, templateTypeChecker, importRemapper) {
  1067. for (const [array, toReplace] of componentImportArrays.getEntries()) {
  1068. const closestClass = nodes.closestNode(array, ts.isClassDeclaration);
  1069. if (!closestClass) {
  1070. continue;
  1071. }
  1072. const replacements = new UniqueItemTracker();
  1073. const usedImports = new Set(findTemplateDependencies(closestClass, templateTypeChecker).map((ref) => ref.node));
  1074. for (const node of toReplace) {
  1075. const moduleDecl = findClassDeclaration(node, typeChecker);
  1076. if (moduleDecl) {
  1077. const moduleMeta = templateTypeChecker.getNgModuleMetadata(moduleDecl);
  1078. if (moduleMeta) {
  1079. moduleMeta.exports.forEach((exp) => {
  1080. if (usedImports.has(exp.node)) {
  1081. replacements.track(node, exp);
  1082. }
  1083. });
  1084. }
  1085. else {
  1086. // It's unlikely not to have module metadata at this point, but just in
  1087. // case unmark the class for removal to reduce the chance of breakages.
  1088. classesToRemove.delete(moduleDecl);
  1089. }
  1090. }
  1091. }
  1092. replaceModulesInImportsArray(array, replacements, tracker, templateTypeChecker, importRemapper);
  1093. }
  1094. }
  1095. /**
  1096. * Replaces all the leftover modules in testing `imports` arrays with their exports.
  1097. * @param testImportArrays All test `imports` arrays and their nodes that represent modules.
  1098. * @param classesToRemove Classes marked for removal by the migration.
  1099. * @param tracker
  1100. * @param typeChecker
  1101. * @param templateTypeChecker
  1102. * @param importRemapper
  1103. */
  1104. function replaceInTestImportsArray(testImportArrays, removalLocations, classesToRemove, tracker, typeChecker, templateTypeChecker, importRemapper) {
  1105. for (const [array, toReplace] of testImportArrays.getEntries()) {
  1106. const replacements = new UniqueItemTracker();
  1107. for (const node of toReplace) {
  1108. const moduleDecl = findClassDeclaration(node, typeChecker);
  1109. if (moduleDecl) {
  1110. const moduleMeta = templateTypeChecker.getNgModuleMetadata(moduleDecl);
  1111. if (moduleMeta) {
  1112. // Since we don't have access to the template type checker in tests,
  1113. // we copy over all the `exports` that aren't flagged for removal.
  1114. const exports = moduleMeta.exports.filter((exp) => !classesToRemove.has(exp.node));
  1115. if (exports.length > 0) {
  1116. exports.forEach((exp) => replacements.track(node, exp));
  1117. }
  1118. else {
  1119. removalLocations.arrays.track(array, node);
  1120. }
  1121. }
  1122. else {
  1123. // It's unlikely not to have module metadata at this point, but just in
  1124. // case unmark the class for removal to reduce the chance of breakages.
  1125. classesToRemove.delete(moduleDecl);
  1126. }
  1127. }
  1128. }
  1129. replaceModulesInImportsArray(array, replacements, tracker, templateTypeChecker, importRemapper);
  1130. }
  1131. }
  1132. /**
  1133. * Replaces any leftover modules in an `imports` arrays with a set of specified exports
  1134. * @param array Imports array which is being migrated.
  1135. * @param replacements Map of NgModule references to their exports.
  1136. * @param tracker
  1137. * @param templateTypeChecker
  1138. * @param importRemapper
  1139. */
  1140. function replaceModulesInImportsArray(array, replacements, tracker, templateTypeChecker, importRemapper) {
  1141. if (replacements.isEmpty()) {
  1142. return;
  1143. }
  1144. const newElements = [];
  1145. const identifiers = new Set();
  1146. for (const element of array.elements) {
  1147. if (ts.isIdentifier(element)) {
  1148. identifiers.add(element.text);
  1149. }
  1150. }
  1151. for (const element of array.elements) {
  1152. const replacementRefs = replacements.get(element);
  1153. if (!replacementRefs) {
  1154. newElements.push(element);
  1155. continue;
  1156. }
  1157. const potentialImports = [];
  1158. for (const ref of replacementRefs) {
  1159. const importLocation = findImportLocation(ref, array, checker.PotentialImportMode.Normal, templateTypeChecker);
  1160. if (importLocation) {
  1161. potentialImports.push(importLocation);
  1162. }
  1163. }
  1164. potentialImportsToExpressions(potentialImports, array.getSourceFile(), tracker, importRemapper).forEach((expr) => {
  1165. if (!ts.isIdentifier(expr) || !identifiers.has(expr.text)) {
  1166. newElements.push(expr);
  1167. }
  1168. });
  1169. }
  1170. tracker.replaceNode(array, ts.factory.updateArrayLiteralExpression(array, newElements));
  1171. }
  1172. /**
  1173. * Removes all tracked array references.
  1174. * @param locations Locations from which to remove the references.
  1175. * @param tracker Tracker in which to register the changes.
  1176. */
  1177. function removeArrayReferences(locations, tracker) {
  1178. for (const [array, toRemove] of locations.getEntries()) {
  1179. const newElements = filterRemovedElements(array.elements, toRemove);
  1180. tracker.replaceNode(array, ts.factory.updateArrayLiteralExpression(array, ts.factory.createNodeArray(newElements, array.elements.hasTrailingComma)));
  1181. }
  1182. }
  1183. /**
  1184. * Removes all tracked import references.
  1185. * @param locations Locations from which to remove the references.
  1186. * @param tracker Tracker in which to register the changes.
  1187. */
  1188. function removeImportReferences(locations, tracker) {
  1189. for (const [namedImports, toRemove] of locations.getEntries()) {
  1190. const newElements = filterRemovedElements(namedImports.elements, toRemove);
  1191. // If no imports are left, we can try to drop the entire import.
  1192. if (newElements.length === 0) {
  1193. const importClause = nodes.closestNode(namedImports, ts.isImportClause);
  1194. // If the import clause has a name we can only drop then named imports.
  1195. // e.g. `import Foo, {ModuleToRemove} from './foo';` becomes `import Foo from './foo';`.
  1196. if (importClause && importClause.name) {
  1197. tracker.replaceNode(importClause, ts.factory.updateImportClause(importClause, importClause.isTypeOnly, importClause.name, undefined));
  1198. }
  1199. else {
  1200. // Otherwise we can drop the entire declaration.
  1201. const declaration = nodes.closestNode(namedImports, ts.isImportDeclaration);
  1202. if (declaration) {
  1203. tracker.removeNode(declaration);
  1204. }
  1205. }
  1206. }
  1207. else {
  1208. // Otherwise we just drop the imported symbols and keep the declaration intact.
  1209. tracker.replaceNode(namedImports, ts.factory.updateNamedImports(namedImports, newElements));
  1210. }
  1211. }
  1212. }
  1213. /**
  1214. * Removes all tracked export references.
  1215. * @param locations Locations from which to remove the references.
  1216. * @param tracker Tracker in which to register the changes.
  1217. */
  1218. function removeExportReferences(locations, tracker) {
  1219. for (const [namedExports, toRemove] of locations.getEntries()) {
  1220. const newElements = filterRemovedElements(namedExports.elements, toRemove);
  1221. // If no exports are left, we can drop the entire declaration.
  1222. if (newElements.length === 0) {
  1223. const declaration = nodes.closestNode(namedExports, ts.isExportDeclaration);
  1224. if (declaration) {
  1225. tracker.removeNode(declaration);
  1226. }
  1227. }
  1228. else {
  1229. // Otherwise we just drop the exported symbols and keep the declaration intact.
  1230. tracker.replaceNode(namedExports, ts.factory.updateNamedExports(namedExports, newElements));
  1231. }
  1232. }
  1233. }
  1234. /**
  1235. * Determines whether an `@NgModule` class is safe to remove. A module is safe to remove if:
  1236. * 1. It has no `declarations`.
  1237. * 2. It has no `providers`.
  1238. * 3. It has no `bootstrap` components.
  1239. * 4. It has no `ModuleWithProviders` in its `imports`.
  1240. * 5. It has no class members. Empty construstors are ignored.
  1241. * @param node Class that is being checked.
  1242. * @param typeChecker
  1243. */
  1244. function canRemoveClass(node, typeChecker) {
  1245. const decorator = findNgModuleDecorator(node, typeChecker)?.node;
  1246. // We can't remove a declaration if it's not a valid `NgModule`.
  1247. if (!decorator || !ts.isCallExpression(decorator.expression)) {
  1248. return false;
  1249. }
  1250. // Unsupported case, e.g. `@NgModule(SOME_VALUE)`.
  1251. if (decorator.expression.arguments.length > 0 &&
  1252. !ts.isObjectLiteralExpression(decorator.expression.arguments[0])) {
  1253. return false;
  1254. }
  1255. // We can't remove modules that have class members. We make an exception for an
  1256. // empty constructor which may have been generated by a tool and forgotten.
  1257. if (node.members.length > 0 && node.members.some((member) => !isEmptyConstructor(member))) {
  1258. return false;
  1259. }
  1260. // An empty `NgModule` call can be removed.
  1261. if (decorator.expression.arguments.length === 0) {
  1262. return true;
  1263. }
  1264. const literal = decorator.expression.arguments[0];
  1265. const imports = findLiteralProperty(literal, 'imports');
  1266. if (imports && isNonEmptyNgModuleProperty(imports)) {
  1267. // We can't remove the class if at least one import isn't identifier, because it may be a
  1268. // `ModuleWithProviders` which is the equivalent of having something in the `providers` array.
  1269. for (const dep of imports.initializer.elements) {
  1270. if (!ts.isIdentifier(dep)) {
  1271. return false;
  1272. }
  1273. const depDeclaration = findClassDeclaration(dep, typeChecker);
  1274. const depNgModule = depDeclaration
  1275. ? findNgModuleDecorator(depDeclaration, typeChecker)
  1276. : null;
  1277. // If any of the dependencies of the class is an `NgModule` that can't be removed, the class
  1278. // itself can't be removed either, because it may be part of a transitive dependency chain.
  1279. if (depDeclaration !== null &&
  1280. depNgModule !== null &&
  1281. !canRemoveClass(depDeclaration, typeChecker)) {
  1282. return false;
  1283. }
  1284. }
  1285. }
  1286. // We can't remove classes that have any `declarations`, `providers` or `bootstrap` elements.
  1287. // Also err on the side of caution and don't remove modules where any of the aforementioned
  1288. // properties aren't initialized to an array literal.
  1289. for (const prop of literal.properties) {
  1290. if (isNonEmptyNgModuleProperty(prop) &&
  1291. (prop.name.text === 'declarations' ||
  1292. prop.name.text === 'providers' ||
  1293. prop.name.text === 'bootstrap')) {
  1294. return false;
  1295. }
  1296. }
  1297. return true;
  1298. }
  1299. /**
  1300. * Checks whether a node is a non-empty property from an NgModule's metadata. This is defined as a
  1301. * property assignment with a static name, initialized to an array literal with more than one
  1302. * element.
  1303. * @param node Node to be checked.
  1304. */
  1305. function isNonEmptyNgModuleProperty(node) {
  1306. return (ts.isPropertyAssignment(node) &&
  1307. ts.isIdentifier(node.name) &&
  1308. ts.isArrayLiteralExpression(node.initializer) &&
  1309. node.initializer.elements.length > 0);
  1310. }
  1311. /**
  1312. * Determines if a file is safe to delete. A file is safe to delete if all it contains are
  1313. * import statements, class declarations that are about to be deleted and non-exported code.
  1314. * @param sourceFile File that is being checked.
  1315. * @param nodesToBeRemoved Nodes that are being removed as a part of the migration.
  1316. */
  1317. function canRemoveFile(sourceFile, nodesToBeRemoved) {
  1318. for (const node of sourceFile.statements) {
  1319. if (ts.isImportDeclaration(node) || nodesToBeRemoved.has(node)) {
  1320. continue;
  1321. }
  1322. if (ts.isExportDeclaration(node) ||
  1323. (ts.canHaveModifiers(node) &&
  1324. ts.getModifiers(node)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword))) {
  1325. return false;
  1326. }
  1327. }
  1328. return true;
  1329. }
  1330. /**
  1331. * Gets whether an AST node contains another AST node.
  1332. * @param parent Parent node that may contain the child.
  1333. * @param child Child node that is being checked.
  1334. */
  1335. function contains(parent, child) {
  1336. return (parent === child ||
  1337. (parent.getSourceFile().fileName === child.getSourceFile().fileName &&
  1338. child.getStart() >= parent.getStart() &&
  1339. child.getStart() <= parent.getEnd()));
  1340. }
  1341. /**
  1342. * Removes AST nodes from a node array.
  1343. * @param elements Array from which to remove the nodes.
  1344. * @param toRemove Nodes that should be removed.
  1345. */
  1346. function filterRemovedElements(elements, toRemove) {
  1347. return elements.filter((el) => {
  1348. for (const node of toRemove) {
  1349. // Check that the element contains the node, despite knowing with relative certainty that it
  1350. // does, because this allows us to unwrap some nodes. E.g. if we have `[((toRemove))]`, we
  1351. // want to remove the entire parenthesized expression, rather than just `toRemove`.
  1352. if (contains(el, node)) {
  1353. return false;
  1354. }
  1355. }
  1356. return true;
  1357. });
  1358. }
  1359. /** Returns whether a node as an empty constructor. */
  1360. function isEmptyConstructor(node) {
  1361. return (ts.isConstructorDeclaration(node) &&
  1362. node.parameters.length === 0 &&
  1363. (node.body == null || node.body.statements.length === 0));
  1364. }
  1365. /**
  1366. * Adds TODO comments to nodes that couldn't be removed manually.
  1367. * @param nodes Nodes to which to add the TODO.
  1368. * @param tracker Tracker in which to register the changes.
  1369. */
  1370. function addRemovalTodos(nodes, tracker) {
  1371. for (const node of nodes) {
  1372. // Note: the comment is inserted using string manipulation, instead of going through the AST,
  1373. // because this way we preserve more of the app's original formatting.
  1374. // Note: in theory this can duplicate comments if the module pruning runs multiple times on
  1375. // the same node. In practice it is unlikely, because the second time the node won't be picked
  1376. // up by the language service as a reference, because the class won't exist anymore.
  1377. tracker.insertText(node.getSourceFile(), node.getFullStart(), ` /* TODO(standalone-migration): clean up removed NgModule reference manually. */ `);
  1378. }
  1379. }
  1380. /** Finds the `NgModule` decorator in a class, if it exists. */
  1381. function findNgModuleDecorator(node, typeChecker) {
  1382. const decorators = ng_decorators.getAngularDecorators(typeChecker, ts.getDecorators(node) || []);
  1383. return decorators.find((decorator) => decorator.name === 'NgModule') || null;
  1384. }
  1385. /**
  1386. * Checks whether a node is used inside of an `imports` array.
  1387. * @param closestAssignment The closest property assignment to the node.
  1388. * @param closestArray The closest array to the node.
  1389. */
  1390. function isInImportsArray(closestAssignment, closestArray) {
  1391. return (closestAssignment.initializer === closestArray &&
  1392. (ts.isIdentifier(closestAssignment.name) || ts.isStringLiteralLike(closestAssignment.name)) &&
  1393. closestAssignment.name.text === 'imports');
  1394. }
  1395. /*!
  1396. * @license
  1397. * Copyright Google LLC All Rights Reserved.
  1398. *
  1399. * Use of this source code is governed by an MIT-style license that can be
  1400. * found in the LICENSE file at https://angular.dev/license
  1401. */
  1402. function toStandaloneBootstrap(program, host, basePath, rootFileNames, sourceFiles, printer, importRemapper, referenceLookupExcludedFiles, declarationImportRemapper) {
  1403. const tracker = new compiler_host.ChangeTracker(printer, importRemapper);
  1404. const typeChecker = program.getTsProgram().getTypeChecker();
  1405. const templateTypeChecker = program.compiler.getTemplateTypeChecker();
  1406. const referenceResolver = new ReferenceResolver(program, host, rootFileNames, basePath, referenceLookupExcludedFiles);
  1407. const bootstrapCalls = [];
  1408. const testObjects = new Set();
  1409. const allDeclarations = new Set();
  1410. // `bootstrapApplication` doesn't include Protractor support by default
  1411. // anymore so we have to opt the app in, if we detect it being used.
  1412. const additionalProviders = hasImport(program, rootFileNames, 'protractor')
  1413. ? new Map([['provideProtractorTestingSupport', '@angular/platform-browser']])
  1414. : null;
  1415. for (const sourceFile of sourceFiles) {
  1416. sourceFile.forEachChild(function walk(node) {
  1417. if (ts.isCallExpression(node) &&
  1418. ts.isPropertyAccessExpression(node.expression) &&
  1419. node.expression.name.text === 'bootstrapModule' &&
  1420. isClassReferenceInAngularModule(node.expression, 'PlatformRef', 'core', typeChecker)) {
  1421. const call = analyzeBootstrapCall(node, typeChecker, templateTypeChecker);
  1422. if (call) {
  1423. bootstrapCalls.push(call);
  1424. }
  1425. }
  1426. node.forEachChild(walk);
  1427. });
  1428. findTestObjectsToMigrate(sourceFile, typeChecker).forEach((obj) => testObjects.add(obj));
  1429. }
  1430. for (const call of bootstrapCalls) {
  1431. call.declarations.forEach((decl) => allDeclarations.add(decl));
  1432. migrateBootstrapCall(call, tracker, additionalProviders, referenceResolver, typeChecker, printer);
  1433. }
  1434. // The previous migrations explicitly skip over bootstrapped
  1435. // declarations so we have to migrate them now.
  1436. for (const declaration of allDeclarations) {
  1437. convertNgModuleDeclarationToStandalone(declaration, allDeclarations, tracker, templateTypeChecker, declarationImportRemapper);
  1438. }
  1439. migrateTestDeclarations(testObjects, allDeclarations, tracker, templateTypeChecker, typeChecker);
  1440. return tracker.recordChanges();
  1441. }
  1442. /**
  1443. * Extracts all of the information from a `bootstrapModule` call
  1444. * necessary to convert it to `bootstrapApplication`.
  1445. * @param call Call to be analyzed.
  1446. * @param typeChecker
  1447. * @param templateTypeChecker
  1448. */
  1449. function analyzeBootstrapCall(call, typeChecker, templateTypeChecker) {
  1450. if (call.arguments.length === 0 || !ts.isIdentifier(call.arguments[0])) {
  1451. return null;
  1452. }
  1453. const declaration = findClassDeclaration(call.arguments[0], typeChecker);
  1454. if (!declaration) {
  1455. return null;
  1456. }
  1457. const decorator = ng_decorators.getAngularDecorators(typeChecker, ts.getDecorators(declaration) || []).find((decorator) => decorator.name === 'NgModule');
  1458. if (!decorator ||
  1459. decorator.node.expression.arguments.length === 0 ||
  1460. !ts.isObjectLiteralExpression(decorator.node.expression.arguments[0])) {
  1461. return null;
  1462. }
  1463. const metadata = decorator.node.expression.arguments[0];
  1464. const bootstrapProp = findLiteralProperty(metadata, 'bootstrap');
  1465. if (!bootstrapProp ||
  1466. !ts.isPropertyAssignment(bootstrapProp) ||
  1467. !ts.isArrayLiteralExpression(bootstrapProp.initializer) ||
  1468. bootstrapProp.initializer.elements.length === 0 ||
  1469. !ts.isIdentifier(bootstrapProp.initializer.elements[0])) {
  1470. return null;
  1471. }
  1472. const component = findClassDeclaration(bootstrapProp.initializer.elements[0], typeChecker);
  1473. if (component && component.name && ts.isIdentifier(component.name)) {
  1474. return {
  1475. module: declaration,
  1476. metadata,
  1477. component: component,
  1478. call,
  1479. declarations: extractDeclarationsFromModule(declaration, templateTypeChecker),
  1480. };
  1481. }
  1482. return null;
  1483. }
  1484. /**
  1485. * Converts a `bootstrapModule` call to `bootstrapApplication`.
  1486. * @param analysis Analysis result of the call.
  1487. * @param tracker Tracker in which to register the changes.
  1488. * @param additionalFeatures Additional providers, apart from the auto-detected ones, that should
  1489. * be added to the bootstrap call.
  1490. * @param referenceResolver
  1491. * @param typeChecker
  1492. * @param printer
  1493. */
  1494. function migrateBootstrapCall(analysis, tracker, additionalProviders, referenceResolver, typeChecker, printer) {
  1495. const sourceFile = analysis.call.getSourceFile();
  1496. const moduleSourceFile = analysis.metadata.getSourceFile();
  1497. const providers = findLiteralProperty(analysis.metadata, 'providers');
  1498. const imports = findLiteralProperty(analysis.metadata, 'imports');
  1499. const nodesToCopy = new Set();
  1500. const providersInNewCall = [];
  1501. const moduleImportsInNewCall = [];
  1502. let nodeLookup = null;
  1503. // Comment out the metadata so that it'll be removed when we run the module pruning afterwards.
  1504. // If the pruning is left for some reason, the user will still have an actionable TODO.
  1505. tracker.insertText(moduleSourceFile, analysis.metadata.getStart(), '/* TODO(standalone-migration): clean up removed NgModule class manually. \n');
  1506. tracker.insertText(moduleSourceFile, analysis.metadata.getEnd(), ' */');
  1507. if (providers && ts.isPropertyAssignment(providers)) {
  1508. nodeLookup = nodeLookup || getNodeLookup(moduleSourceFile);
  1509. if (ts.isArrayLiteralExpression(providers.initializer)) {
  1510. providersInNewCall.push(...providers.initializer.elements);
  1511. }
  1512. else {
  1513. providersInNewCall.push(ts.factory.createSpreadElement(providers.initializer));
  1514. }
  1515. addNodesToCopy(sourceFile, providers, nodeLookup, tracker, nodesToCopy, referenceResolver);
  1516. }
  1517. if (imports && ts.isPropertyAssignment(imports)) {
  1518. nodeLookup = nodeLookup || getNodeLookup(moduleSourceFile);
  1519. migrateImportsForBootstrapCall(sourceFile, imports, nodeLookup, moduleImportsInNewCall, providersInNewCall, tracker, nodesToCopy, referenceResolver, typeChecker);
  1520. }
  1521. if (additionalProviders) {
  1522. additionalProviders.forEach((moduleSpecifier, name) => {
  1523. providersInNewCall.push(ts.factory.createCallExpression(tracker.addImport(sourceFile, name, moduleSpecifier), undefined, undefined));
  1524. });
  1525. }
  1526. if (nodesToCopy.size > 0) {
  1527. let text = '\n\n';
  1528. nodesToCopy.forEach((node) => {
  1529. const transformedNode = remapDynamicImports(sourceFile.fileName, node);
  1530. // Use `getText` to try an preserve the original formatting. This only works if the node
  1531. // hasn't been transformed. If it has, we have to fall back to the printer.
  1532. if (transformedNode === node) {
  1533. text += transformedNode.getText() + '\n';
  1534. }
  1535. else {
  1536. text += printer.printNode(ts.EmitHint.Unspecified, transformedNode, node.getSourceFile());
  1537. }
  1538. });
  1539. text += '\n';
  1540. tracker.insertText(sourceFile, getLastImportEnd(sourceFile), text);
  1541. }
  1542. replaceBootstrapCallExpression(analysis, providersInNewCall, moduleImportsInNewCall, tracker);
  1543. }
  1544. /**
  1545. * Replaces a `bootstrapModule` call with `bootstrapApplication`.
  1546. * @param analysis Analysis result of the `bootstrapModule` call.
  1547. * @param providers Providers that should be added to the new call.
  1548. * @param modules Modules that are being imported into the new call.
  1549. * @param tracker Object keeping track of the changes to the different files.
  1550. */
  1551. function replaceBootstrapCallExpression(analysis, providers, modules, tracker) {
  1552. const sourceFile = analysis.call.getSourceFile();
  1553. const componentPath = getRelativeImportPath(sourceFile.fileName, analysis.component.getSourceFile().fileName);
  1554. const args = [tracker.addImport(sourceFile, analysis.component.name.text, componentPath)];
  1555. const bootstrapExpression = tracker.addImport(sourceFile, 'bootstrapApplication', '@angular/platform-browser');
  1556. if (providers.length > 0 || modules.length > 0) {
  1557. const combinedProviders = [];
  1558. if (modules.length > 0) {
  1559. const importProvidersExpression = tracker.addImport(sourceFile, 'importProvidersFrom', '@angular/core');
  1560. combinedProviders.push(ts.factory.createCallExpression(importProvidersExpression, [], modules));
  1561. }
  1562. // Push the providers after `importProvidersFrom` call for better readability.
  1563. combinedProviders.push(...providers);
  1564. const providersArray = ts.factory.createNodeArray(combinedProviders, analysis.metadata.properties.hasTrailingComma && combinedProviders.length > 2);
  1565. const initializer = remapDynamicImports(sourceFile.fileName, ts.factory.createArrayLiteralExpression(providersArray, combinedProviders.length > 1));
  1566. args.push(ts.factory.createObjectLiteralExpression([ts.factory.createPropertyAssignment('providers', initializer)], true));
  1567. }
  1568. tracker.replaceNode(analysis.call, ts.factory.createCallExpression(bootstrapExpression, [], args),
  1569. // Note: it's important to pass in the source file that the nodes originated from!
  1570. // Otherwise TS won't print out literals inside of the providers that we're copying
  1571. // over from the module file.
  1572. undefined, analysis.metadata.getSourceFile());
  1573. }
  1574. /**
  1575. * Processes the `imports` of an NgModule so that they can be used in the `bootstrapApplication`
  1576. * call inside of a different file.
  1577. * @param sourceFile File to which the imports will be moved.
  1578. * @param imports Node declaring the imports.
  1579. * @param nodeLookup Map used to look up nodes based on their positions in a file.
  1580. * @param importsForNewCall Array keeping track of the imports that are being added to the new call.
  1581. * @param providersInNewCall Array keeping track of the providers in the new call.
  1582. * @param tracker Tracker in which changes to files are being stored.
  1583. * @param nodesToCopy Nodes that should be copied to the new file.
  1584. * @param referenceResolver
  1585. * @param typeChecker
  1586. */
  1587. function migrateImportsForBootstrapCall(sourceFile, imports, nodeLookup, importsForNewCall, providersInNewCall, tracker, nodesToCopy, referenceResolver, typeChecker) {
  1588. if (!ts.isArrayLiteralExpression(imports.initializer)) {
  1589. importsForNewCall.push(imports.initializer);
  1590. return;
  1591. }
  1592. for (const element of imports.initializer.elements) {
  1593. // If the reference is to a `RouterModule.forRoot` call, we can try to migrate it.
  1594. if (ts.isCallExpression(element) &&
  1595. ts.isPropertyAccessExpression(element.expression) &&
  1596. element.arguments.length > 0 &&
  1597. element.expression.name.text === 'forRoot' &&
  1598. isClassReferenceInAngularModule(element.expression.expression, 'RouterModule', 'router', typeChecker)) {
  1599. const options = element.arguments[1];
  1600. const features = options ? getRouterModuleForRootFeatures(sourceFile, options, tracker) : [];
  1601. // If the features come back as null, it means that the router
  1602. // has a configuration that can't be migrated automatically.
  1603. if (features !== null) {
  1604. providersInNewCall.push(ts.factory.createCallExpression(tracker.addImport(sourceFile, 'provideRouter', '@angular/router'), [], [element.arguments[0], ...features]));
  1605. addNodesToCopy(sourceFile, element.arguments[0], nodeLookup, tracker, nodesToCopy, referenceResolver);
  1606. if (options) {
  1607. addNodesToCopy(sourceFile, options, nodeLookup, tracker, nodesToCopy, referenceResolver);
  1608. }
  1609. continue;
  1610. }
  1611. }
  1612. if (ts.isIdentifier(element)) {
  1613. // `BrowserAnimationsModule` can be replaced with `provideAnimations`.
  1614. const animationsModule = 'platform-browser/animations';
  1615. const animationsImport = `@angular/${animationsModule}`;
  1616. if (isClassReferenceInAngularModule(element, 'BrowserAnimationsModule', animationsModule, typeChecker)) {
  1617. providersInNewCall.push(ts.factory.createCallExpression(tracker.addImport(sourceFile, 'provideAnimations', animationsImport), [], []));
  1618. continue;
  1619. }
  1620. // `NoopAnimationsModule` can be replaced with `provideNoopAnimations`.
  1621. if (isClassReferenceInAngularModule(element, 'NoopAnimationsModule', animationsModule, typeChecker)) {
  1622. providersInNewCall.push(ts.factory.createCallExpression(tracker.addImport(sourceFile, 'provideNoopAnimations', animationsImport), [], []));
  1623. continue;
  1624. }
  1625. // `HttpClientModule` can be replaced with `provideHttpClient()`.
  1626. const httpClientModule = 'common/http';
  1627. const httpClientImport = `@angular/${httpClientModule}`;
  1628. if (isClassReferenceInAngularModule(element, 'HttpClientModule', httpClientModule, typeChecker)) {
  1629. const callArgs = [
  1630. // we add `withInterceptorsFromDi()` to the call to ensure that class-based interceptors
  1631. // still work
  1632. ts.factory.createCallExpression(tracker.addImport(sourceFile, 'withInterceptorsFromDi', httpClientImport), [], []),
  1633. ];
  1634. providersInNewCall.push(ts.factory.createCallExpression(tracker.addImport(sourceFile, 'provideHttpClient', httpClientImport), [], callArgs));
  1635. continue;
  1636. }
  1637. }
  1638. const target =
  1639. // If it's a call, it'll likely be a `ModuleWithProviders`
  1640. // expression so the target is going to be call's expression.
  1641. ts.isCallExpression(element) && ts.isPropertyAccessExpression(element.expression)
  1642. ? element.expression.expression
  1643. : element;
  1644. const classDeclaration = findClassDeclaration(target, typeChecker);
  1645. const decorators = classDeclaration
  1646. ? ng_decorators.getAngularDecorators(typeChecker, ts.getDecorators(classDeclaration) || [])
  1647. : undefined;
  1648. if (!decorators ||
  1649. decorators.length === 0 ||
  1650. decorators.every(({ name }) => name !== 'Directive' && name !== 'Component' && name !== 'Pipe')) {
  1651. importsForNewCall.push(element);
  1652. addNodesToCopy(sourceFile, element, nodeLookup, tracker, nodesToCopy, referenceResolver);
  1653. }
  1654. }
  1655. }
  1656. /**
  1657. * Generates the call expressions that can be used to replace the options
  1658. * object that is passed into a `RouterModule.forRoot` call.
  1659. * @param sourceFile File that the `forRoot` call is coming from.
  1660. * @param options Node that is passed as the second argument to the `forRoot` call.
  1661. * @param tracker Tracker in which to track imports that need to be inserted.
  1662. * @returns Null if the options can't be migrated, otherwise an array of call expressions.
  1663. */
  1664. function getRouterModuleForRootFeatures(sourceFile, options, tracker) {
  1665. // Options that aren't a static object literal can't be migrated.
  1666. if (!ts.isObjectLiteralExpression(options)) {
  1667. return null;
  1668. }
  1669. const featureExpressions = [];
  1670. const configOptions = [];
  1671. const inMemoryScrollingOptions = [];
  1672. const features = new UniqueItemTracker();
  1673. for (const prop of options.properties) {
  1674. // We can't migrate options that we can't easily analyze.
  1675. if (!ts.isPropertyAssignment(prop) ||
  1676. (!ts.isIdentifier(prop.name) && !ts.isStringLiteralLike(prop.name))) {
  1677. return null;
  1678. }
  1679. switch (prop.name.text) {
  1680. // `preloadingStrategy` maps to the `withPreloading` function.
  1681. case 'preloadingStrategy':
  1682. features.track('withPreloading', prop.initializer);
  1683. break;
  1684. // `enableTracing: true` maps to the `withDebugTracing` feature.
  1685. case 'enableTracing':
  1686. if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) {
  1687. features.track('withDebugTracing', null);
  1688. }
  1689. break;
  1690. // `initialNavigation: 'enabled'` and `initialNavigation: 'enabledBlocking'` map to the
  1691. // `withEnabledBlockingInitialNavigation` feature, while `initialNavigation: 'disabled'` maps
  1692. // to the `withDisabledInitialNavigation` feature.
  1693. case 'initialNavigation':
  1694. if (!ts.isStringLiteralLike(prop.initializer)) {
  1695. return null;
  1696. }
  1697. if (prop.initializer.text === 'enabledBlocking' || prop.initializer.text === 'enabled') {
  1698. features.track('withEnabledBlockingInitialNavigation', null);
  1699. }
  1700. else if (prop.initializer.text === 'disabled') {
  1701. features.track('withDisabledInitialNavigation', null);
  1702. }
  1703. break;
  1704. // `useHash: true` maps to the `withHashLocation` feature.
  1705. case 'useHash':
  1706. if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) {
  1707. features.track('withHashLocation', null);
  1708. }
  1709. break;
  1710. // `errorHandler` maps to the `withNavigationErrorHandler` feature.
  1711. case 'errorHandler':
  1712. features.track('withNavigationErrorHandler', prop.initializer);
  1713. break;
  1714. // `anchorScrolling` and `scrollPositionRestoration` arguments have to be combined into an
  1715. // object literal that is passed into the `withInMemoryScrolling` feature.
  1716. case 'anchorScrolling':
  1717. case 'scrollPositionRestoration':
  1718. inMemoryScrollingOptions.push(prop);
  1719. break;
  1720. // All remaining properties can be passed through the `withRouterConfig` feature.
  1721. default:
  1722. configOptions.push(prop);
  1723. break;
  1724. }
  1725. }
  1726. if (inMemoryScrollingOptions.length > 0) {
  1727. features.track('withInMemoryScrolling', ts.factory.createObjectLiteralExpression(inMemoryScrollingOptions));
  1728. }
  1729. if (configOptions.length > 0) {
  1730. features.track('withRouterConfig', ts.factory.createObjectLiteralExpression(configOptions));
  1731. }
  1732. for (const [feature, featureArgs] of features.getEntries()) {
  1733. const callArgs = [];
  1734. featureArgs.forEach((arg) => {
  1735. if (arg !== null) {
  1736. callArgs.push(arg);
  1737. }
  1738. });
  1739. featureExpressions.push(ts.factory.createCallExpression(tracker.addImport(sourceFile, feature, '@angular/router'), [], callArgs));
  1740. }
  1741. return featureExpressions;
  1742. }
  1743. /**
  1744. * Finds all the nodes that are referenced inside a root node and would need to be copied into a
  1745. * new file in order for the node to compile, and tracks them.
  1746. * @param targetFile File to which the nodes will be copied.
  1747. * @param rootNode Node within which to look for references.
  1748. * @param nodeLookup Map used to look up nodes based on their positions in a file.
  1749. * @param tracker Tracker in which changes to files are stored.
  1750. * @param nodesToCopy Set that keeps track of the nodes being copied.
  1751. * @param referenceResolver
  1752. */
  1753. function addNodesToCopy(targetFile, rootNode, nodeLookup, tracker, nodesToCopy, referenceResolver) {
  1754. const refs = findAllSameFileReferences(rootNode, nodeLookup, referenceResolver);
  1755. for (const ref of refs) {
  1756. const importSpecifier = closestOrSelf(ref, ts.isImportSpecifier);
  1757. const importDeclaration = importSpecifier
  1758. ? nodes.closestNode(importSpecifier, ts.isImportDeclaration)
  1759. : null;
  1760. // If the reference is in an import, we need to add an import to the main file.
  1761. if (importDeclaration &&
  1762. importSpecifier &&
  1763. ts.isStringLiteralLike(importDeclaration.moduleSpecifier)) {
  1764. const moduleName = importDeclaration.moduleSpecifier.text.startsWith('.')
  1765. ? remapRelativeImport(targetFile.fileName, importDeclaration.moduleSpecifier)
  1766. : importDeclaration.moduleSpecifier.text;
  1767. const symbolName = importSpecifier.propertyName
  1768. ? importSpecifier.propertyName.text
  1769. : importSpecifier.name.text;
  1770. const alias = importSpecifier.propertyName ? importSpecifier.name.text : undefined;
  1771. tracker.addImport(targetFile, symbolName, moduleName, alias);
  1772. continue;
  1773. }
  1774. const variableDeclaration = closestOrSelf(ref, ts.isVariableDeclaration);
  1775. const variableStatement = variableDeclaration
  1776. ? nodes.closestNode(variableDeclaration, ts.isVariableStatement)
  1777. : null;
  1778. // If the reference is a variable, we can attempt to import it or copy it over.
  1779. if (variableDeclaration && variableStatement && ts.isIdentifier(variableDeclaration.name)) {
  1780. if (isExported(variableStatement)) {
  1781. tracker.addImport(targetFile, variableDeclaration.name.text, getRelativeImportPath(targetFile.fileName, ref.getSourceFile().fileName));
  1782. }
  1783. else {
  1784. nodesToCopy.add(variableStatement);
  1785. }
  1786. continue;
  1787. }
  1788. // Otherwise check if the reference is inside of an exportable declaration, e.g. a function.
  1789. // This code that is safe to copy over into the new file or import it, if it's exported.
  1790. const closestExportable = closestOrSelf(ref, isExportableDeclaration);
  1791. if (closestExportable) {
  1792. if (isExported(closestExportable) && closestExportable.name) {
  1793. tracker.addImport(targetFile, closestExportable.name.text, getRelativeImportPath(targetFile.fileName, ref.getSourceFile().fileName));
  1794. }
  1795. else {
  1796. nodesToCopy.add(closestExportable);
  1797. }
  1798. }
  1799. }
  1800. }
  1801. /**
  1802. * Finds all the nodes referenced within the root node in the same file.
  1803. * @param rootNode Node from which to start looking for references.
  1804. * @param nodeLookup Map used to look up nodes based on their positions in a file.
  1805. * @param referenceResolver
  1806. */
  1807. function findAllSameFileReferences(rootNode, nodeLookup, referenceResolver) {
  1808. const results = new Set();
  1809. const traversedTopLevelNodes = new Set();
  1810. const excludeStart = rootNode.getStart();
  1811. const excludeEnd = rootNode.getEnd();
  1812. (function walk(node) {
  1813. if (!isReferenceIdentifier(node)) {
  1814. node.forEachChild(walk);
  1815. return;
  1816. }
  1817. const refs = referencesToNodeWithinSameFile(node, nodeLookup, excludeStart, excludeEnd, referenceResolver);
  1818. if (refs === null) {
  1819. return;
  1820. }
  1821. for (const ref of refs) {
  1822. if (results.has(ref)) {
  1823. continue;
  1824. }
  1825. results.add(ref);
  1826. const closestTopLevel = nodes.closestNode(ref, isTopLevelStatement);
  1827. // Avoid re-traversing the same top-level nodes since we know what the result will be.
  1828. if (!closestTopLevel || traversedTopLevelNodes.has(closestTopLevel)) {
  1829. continue;
  1830. }
  1831. // Keep searching, starting from the closest top-level node. We skip import declarations,
  1832. // because we already know about them and they may put the search into an infinite loop.
  1833. if (!ts.isImportDeclaration(closestTopLevel) &&
  1834. isOutsideRange(excludeStart, excludeEnd, closestTopLevel.getStart(), closestTopLevel.getEnd())) {
  1835. traversedTopLevelNodes.add(closestTopLevel);
  1836. walk(closestTopLevel);
  1837. }
  1838. }
  1839. })(rootNode);
  1840. return results;
  1841. }
  1842. /**
  1843. * Finds all the nodes referring to a specific node within the same file.
  1844. * @param node Node whose references we're lookip for.
  1845. * @param nodeLookup Map used to look up nodes based on their positions in a file.
  1846. * @param excludeStart Start of a range that should be excluded from the results.
  1847. * @param excludeEnd End of a range that should be excluded from the results.
  1848. * @param referenceResolver
  1849. */
  1850. function referencesToNodeWithinSameFile(node, nodeLookup, excludeStart, excludeEnd, referenceResolver) {
  1851. const offsets = referenceResolver
  1852. .findSameFileReferences(node, node.getSourceFile().fileName)
  1853. .filter(([start, end]) => isOutsideRange(excludeStart, excludeEnd, start, end));
  1854. if (offsets.length > 0) {
  1855. const nodes = offsetsToNodes(nodeLookup, offsets, new Set());
  1856. if (nodes.size > 0) {
  1857. return nodes;
  1858. }
  1859. }
  1860. return null;
  1861. }
  1862. /**
  1863. * Transforms a node so that any dynamic imports with relative file paths it contains are remapped
  1864. * as if they were specified in a different file. If no transformations have occurred, the original
  1865. * node will be returned.
  1866. * @param targetFileName File name to which to remap the imports.
  1867. * @param rootNode Node being transformed.
  1868. */
  1869. function remapDynamicImports(targetFileName, rootNode) {
  1870. let hasChanged = false;
  1871. const transformer = (context) => {
  1872. return (sourceFile) => ts.visitNode(sourceFile, function walk(node) {
  1873. if (ts.isCallExpression(node) &&
  1874. node.expression.kind === ts.SyntaxKind.ImportKeyword &&
  1875. node.arguments.length > 0 &&
  1876. ts.isStringLiteralLike(node.arguments[0]) &&
  1877. node.arguments[0].text.startsWith('.')) {
  1878. hasChanged = true;
  1879. return context.factory.updateCallExpression(node, node.expression, node.typeArguments, [
  1880. context.factory.createStringLiteral(remapRelativeImport(targetFileName, node.arguments[0])),
  1881. ...node.arguments.slice(1),
  1882. ]);
  1883. }
  1884. return ts.visitEachChild(node, walk, context);
  1885. });
  1886. };
  1887. const result = ts.transform(rootNode, [transformer]).transformed[0];
  1888. return hasChanged ? result : rootNode;
  1889. }
  1890. /**
  1891. * Checks whether a node is a statement at the top level of a file.
  1892. * @param node Node to be checked.
  1893. */
  1894. function isTopLevelStatement(node) {
  1895. return node.parent != null && ts.isSourceFile(node.parent);
  1896. }
  1897. /**
  1898. * Asserts that a node is an identifier that might be referring to a symbol. This excludes
  1899. * identifiers of named nodes like property assignments.
  1900. * @param node Node to be checked.
  1901. */
  1902. function isReferenceIdentifier(node) {
  1903. return (ts.isIdentifier(node) &&
  1904. ((!ts.isPropertyAssignment(node.parent) && !ts.isParameter(node.parent)) ||
  1905. node.parent.name !== node));
  1906. }
  1907. /**
  1908. * Checks whether a range is completely outside of another range.
  1909. * @param excludeStart Start of the exclusion range.
  1910. * @param excludeEnd End of the exclusion range.
  1911. * @param start Start of the range that is being checked.
  1912. * @param end End of the range that is being checked.
  1913. */
  1914. function isOutsideRange(excludeStart, excludeEnd, start, end) {
  1915. return (start < excludeStart && end < excludeStart) || start > excludeEnd;
  1916. }
  1917. /**
  1918. * Remaps the specifier of a relative import from its original location to a new one.
  1919. * @param targetFileName Name of the file that the specifier will be moved to.
  1920. * @param specifier Specifier whose path is being remapped.
  1921. */
  1922. function remapRelativeImport(targetFileName, specifier) {
  1923. return getRelativeImportPath(targetFileName, p.join(p.dirname(specifier.getSourceFile().fileName), specifier.text));
  1924. }
  1925. /**
  1926. * Whether a node is exported.
  1927. * @param node Node to be checked.
  1928. */
  1929. function isExported(node) {
  1930. return ts.canHaveModifiers(node) && node.modifiers
  1931. ? node.modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)
  1932. : false;
  1933. }
  1934. /**
  1935. * Asserts that a node is an exportable declaration, which means that it can either be exported or
  1936. * it can be safely copied into another file.
  1937. * @param node Node to be checked.
  1938. */
  1939. function isExportableDeclaration(node) {
  1940. return (ts.isEnumDeclaration(node) ||
  1941. ts.isClassDeclaration(node) ||
  1942. ts.isFunctionDeclaration(node) ||
  1943. ts.isInterfaceDeclaration(node) ||
  1944. ts.isTypeAliasDeclaration(node));
  1945. }
  1946. /**
  1947. * Gets the index after the last import in a file. Can be used to insert new code into the file.
  1948. * @param sourceFile File in which to search for imports.
  1949. */
  1950. function getLastImportEnd(sourceFile) {
  1951. let index = 0;
  1952. for (const statement of sourceFile.statements) {
  1953. if (ts.isImportDeclaration(statement)) {
  1954. index = Math.max(index, statement.getEnd());
  1955. }
  1956. else {
  1957. break;
  1958. }
  1959. }
  1960. return index;
  1961. }
  1962. /** Checks if any of the program's files has an import of a specific module. */
  1963. function hasImport(program, rootFileNames, moduleName) {
  1964. const tsProgram = program.getTsProgram();
  1965. const deepImportStart = moduleName + '/';
  1966. for (const fileName of rootFileNames) {
  1967. const sourceFile = tsProgram.getSourceFile(fileName);
  1968. if (!sourceFile) {
  1969. continue;
  1970. }
  1971. for (const statement of sourceFile.statements) {
  1972. if (ts.isImportDeclaration(statement) &&
  1973. ts.isStringLiteralLike(statement.moduleSpecifier) &&
  1974. (statement.moduleSpecifier.text === moduleName ||
  1975. statement.moduleSpecifier.text.startsWith(deepImportStart))) {
  1976. return true;
  1977. }
  1978. }
  1979. }
  1980. return false;
  1981. }
  1982. var MigrationMode;
  1983. (function (MigrationMode) {
  1984. MigrationMode["toStandalone"] = "convert-to-standalone";
  1985. MigrationMode["pruneModules"] = "prune-ng-modules";
  1986. MigrationMode["standaloneBootstrap"] = "standalone-bootstrap";
  1987. })(MigrationMode || (MigrationMode = {}));
  1988. function migrate(options) {
  1989. return async (tree, context) => {
  1990. const { buildPaths, testPaths } = await project_tsconfig_paths.getProjectTsConfigPaths(tree);
  1991. const basePath = process.cwd();
  1992. const allPaths = [...buildPaths, ...testPaths];
  1993. // TS and Schematic use paths in POSIX format even on Windows. This is needed as otherwise
  1994. // string matching such as `sourceFile.fileName.startsWith(pathToMigrate)` might not work.
  1995. const pathToMigrate = compiler_host.normalizePath(p.join(basePath, options.path));
  1996. let migratedFiles = 0;
  1997. if (!allPaths.length) {
  1998. throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run the standalone migration.');
  1999. }
  2000. for (const tsconfigPath of allPaths) {
  2001. migratedFiles += standaloneMigration(tree, tsconfigPath, basePath, pathToMigrate, options);
  2002. }
  2003. if (migratedFiles === 0) {
  2004. throw new schematics.SchematicsException(`Could not find any files to migrate under the path ${pathToMigrate}. Cannot run the standalone migration.`);
  2005. }
  2006. context.logger.info('🎉 Automated migration step has finished! 🎉');
  2007. context.logger.info('IMPORTANT! Please verify manually that your application builds and behaves as expected.');
  2008. context.logger.info(`See https://angular.dev/reference/migrations/standalone for more information.`);
  2009. };
  2010. }
  2011. function standaloneMigration(tree, tsconfigPath, basePath, pathToMigrate, schematicOptions, oldProgram) {
  2012. if (schematicOptions.path.startsWith('..')) {
  2013. throw new schematics.SchematicsException('Cannot run standalone migration outside of the current project.');
  2014. }
  2015. const { host, options, rootNames } = compiler_host.createProgramOptions(tree, tsconfigPath, basePath, undefined, undefined, {
  2016. _enableTemplateTypeChecker: true, // Required for the template type checker to work.
  2017. compileNonExportedClasses: true, // We want to migrate non-exported classes too.
  2018. // Avoid checking libraries to speed up the migration.
  2019. skipLibCheck: true,
  2020. skipDefaultLibCheck: true,
  2021. });
  2022. const referenceLookupExcludedFiles = /node_modules|\.ngtypecheck\.ts/;
  2023. const program = createProgram({ rootNames, host, options, oldProgram });
  2024. const printer = ts.createPrinter();
  2025. if (fs.existsSync(pathToMigrate) && !fs.statSync(pathToMigrate).isDirectory()) {
  2026. throw new schematics.SchematicsException(`Migration path ${pathToMigrate} has to be a directory. Cannot run the standalone migration.`);
  2027. }
  2028. const sourceFiles = program
  2029. .getTsProgram()
  2030. .getSourceFiles()
  2031. .filter((sourceFile) => sourceFile.fileName.startsWith(pathToMigrate) &&
  2032. compiler_host.canMigrateFile(basePath, sourceFile, program.getTsProgram()));
  2033. if (sourceFiles.length === 0) {
  2034. return 0;
  2035. }
  2036. let pendingChanges;
  2037. let filesToRemove = null;
  2038. if (schematicOptions.mode === MigrationMode.pruneModules) {
  2039. const result = pruneNgModules(program, host, basePath, rootNames, sourceFiles, printer, undefined, referenceLookupExcludedFiles, knownInternalAliasRemapper);
  2040. pendingChanges = result.pendingChanges;
  2041. filesToRemove = result.filesToRemove;
  2042. }
  2043. else if (schematicOptions.mode === MigrationMode.standaloneBootstrap) {
  2044. pendingChanges = toStandaloneBootstrap(program, host, basePath, rootNames, sourceFiles, printer, undefined, referenceLookupExcludedFiles, knownInternalAliasRemapper);
  2045. }
  2046. else {
  2047. // This shouldn't happen, but default to `MigrationMode.toStandalone` just in case.
  2048. pendingChanges = toStandalone(sourceFiles, program, printer, undefined, knownInternalAliasRemapper);
  2049. }
  2050. for (const [file, changes] of pendingChanges.entries()) {
  2051. // Don't attempt to edit a file if it's going to be deleted.
  2052. if (filesToRemove?.has(file)) {
  2053. continue;
  2054. }
  2055. const update = tree.beginUpdate(p.relative(basePath, file.fileName));
  2056. changes.forEach((change) => {
  2057. if (change.removeLength != null) {
  2058. update.remove(change.start, change.removeLength);
  2059. }
  2060. update.insertRight(change.start, change.text);
  2061. });
  2062. tree.commitUpdate(update);
  2063. }
  2064. if (filesToRemove) {
  2065. for (const file of filesToRemove) {
  2066. tree.delete(p.relative(basePath, file.fileName));
  2067. }
  2068. }
  2069. // Run the module pruning after the standalone bootstrap to automatically remove the root module.
  2070. // Note that we can't run the module pruning internally without propagating the changes to disk,
  2071. // because there may be conflicting AST node changes.
  2072. if (schematicOptions.mode === MigrationMode.standaloneBootstrap) {
  2073. return (standaloneMigration(tree, tsconfigPath, basePath, pathToMigrate, { ...schematicOptions, mode: MigrationMode.pruneModules }, program) + sourceFiles.length);
  2074. }
  2075. return sourceFiles.length;
  2076. }
  2077. exports.migrate = migrate;