standalone-migration.js 98 KB

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