output-migration.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  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. var project_tsconfig_paths = require('./project_tsconfig_paths-CDVxT6Ov.js');
  10. var project_paths = require('./project_paths-CXXqWSoY.js');
  11. require('os');
  12. var ts = require('typescript');
  13. var checker = require('./checker-DP-zos5Q.js');
  14. var program = require('./program-BmLi-Vxz.js');
  15. require('path');
  16. var apply_import_manager = require('./apply_import_manager-BynuozbO.js');
  17. var index = require('./index-CPpyW--c.js');
  18. require('@angular-devkit/core');
  19. require('node:path/posix');
  20. require('fs');
  21. require('module');
  22. require('url');
  23. function isOutputDeclarationEligibleForMigration(node) {
  24. return (node.initializer !== undefined &&
  25. ts.isNewExpression(node.initializer) &&
  26. ts.isIdentifier(node.initializer.expression) &&
  27. node.initializer.expression.text === 'EventEmitter');
  28. }
  29. function isPotentialOutputCallUsage(node, name) {
  30. if (ts.isCallExpression(node) &&
  31. ts.isPropertyAccessExpression(node.expression) &&
  32. ts.isIdentifier(node.expression.name)) {
  33. return node.expression?.name.text === name;
  34. }
  35. else {
  36. return false;
  37. }
  38. }
  39. function isPotentialPipeCallUsage(node) {
  40. return isPotentialOutputCallUsage(node, 'pipe');
  41. }
  42. function isPotentialNextCallUsage(node) {
  43. return isPotentialOutputCallUsage(node, 'next');
  44. }
  45. function isPotentialCompleteCallUsage(node) {
  46. return isPotentialOutputCallUsage(node, 'complete');
  47. }
  48. function isTargetOutputDeclaration(node, checker, reflector, dtsReader) {
  49. const targetSymbol = checker.getSymbolAtLocation(node);
  50. if (targetSymbol !== undefined) {
  51. const propertyDeclaration = getTargetPropertyDeclaration(targetSymbol);
  52. if (propertyDeclaration !== null &&
  53. isOutputDeclaration(propertyDeclaration, reflector, dtsReader)) {
  54. return propertyDeclaration;
  55. }
  56. }
  57. return null;
  58. }
  59. /** Gets whether the given property is an Angular `@Output`. */
  60. function isOutputDeclaration(node, reflector, dtsReader) {
  61. // `.d.ts` file, so we check the `static ecmp` metadata on the `declare class`.
  62. if (node.getSourceFile().isDeclarationFile) {
  63. if (!ts.isIdentifier(node.name) ||
  64. !ts.isClassDeclaration(node.parent) ||
  65. node.parent.name === undefined) {
  66. return false;
  67. }
  68. const ref = new checker.Reference(node.parent);
  69. const directiveMeta = dtsReader.getDirectiveMetadata(ref);
  70. return !!directiveMeta?.outputs.getByClassPropertyName(node.name.text);
  71. }
  72. // `.ts` file, so we check for the `@Output()` decorator.
  73. return getOutputDecorator(node, reflector) !== null;
  74. }
  75. function getTargetPropertyDeclaration(targetSymbol) {
  76. const valDeclaration = targetSymbol.valueDeclaration;
  77. if (valDeclaration !== undefined && ts.isPropertyDeclaration(valDeclaration)) {
  78. return valDeclaration;
  79. }
  80. return null;
  81. }
  82. /** Returns Angular `@Output` decorator or null when a given property declaration is not an @Output */
  83. function getOutputDecorator(node, reflector) {
  84. const decorators = reflector.getDecoratorsOfDeclaration(node);
  85. const ngDecorators = decorators !== null ? checker.getAngularDecorators(decorators, ['Output'], /* isCore */ false) : [];
  86. return ngDecorators.length > 0 ? ngDecorators[0] : null;
  87. }
  88. // THINK: this utility + type is not specific to @Output, really, maybe move it to tsurge?
  89. /** Computes an unique ID for a given Angular `@Output` property. */
  90. function getUniqueIdForProperty(info, prop) {
  91. const { id } = project_paths.projectFile(prop.getSourceFile(), info);
  92. id.replace(/\.d\.ts$/, '.ts');
  93. return `${id}@@${prop.parent.name ?? 'unknown-class'}@@${prop.name.getText()}`;
  94. }
  95. function isTestRunnerImport(node) {
  96. if (ts.isImportDeclaration(node)) {
  97. const moduleSpecifier = node.moduleSpecifier.getText();
  98. return moduleSpecifier.includes('jasmine') || moduleSpecifier.includes('catalyst');
  99. }
  100. return false;
  101. }
  102. // TODO: code duplication with signals migration - sort it out
  103. /**
  104. * Gets whether the given read is used to access
  105. * the specified field.
  106. *
  107. * E.g. whether `<my-read>.toArray` is detected.
  108. */
  109. function checkNonTsReferenceAccessesField(ref, fieldName) {
  110. const readFromPath = ref.from.readAstPath.at(-1);
  111. const parentRead = ref.from.readAstPath.at(-2);
  112. if (ref.from.read !== readFromPath) {
  113. return null;
  114. }
  115. if (!(parentRead instanceof checker.PropertyRead) || parentRead.name !== fieldName) {
  116. return null;
  117. }
  118. return parentRead;
  119. }
  120. /**
  121. * Gets whether the given reference is accessed to call the
  122. * specified function on it.
  123. *
  124. * E.g. whether `<my-read>.toArray()` is detected.
  125. */
  126. function checkNonTsReferenceCallsField(ref, fieldName) {
  127. const propertyAccess = checkNonTsReferenceAccessesField(ref, fieldName);
  128. if (propertyAccess === null) {
  129. return null;
  130. }
  131. const accessIdx = ref.from.readAstPath.indexOf(propertyAccess);
  132. if (accessIdx === -1) {
  133. return null;
  134. }
  135. const potentialRead = ref.from.readAstPath[accessIdx];
  136. if (potentialRead === undefined) {
  137. return null;
  138. }
  139. return potentialRead;
  140. }
  141. const printer = ts.createPrinter();
  142. function calculateDeclarationReplacement(info, node, aliasParam) {
  143. const sf = node.getSourceFile();
  144. const payloadTypes = node.initializer !== undefined && ts.isNewExpression(node.initializer)
  145. ? node.initializer?.typeArguments
  146. : undefined;
  147. const outputCall = ts.factory.createCallExpression(ts.factory.createIdentifier('output'), payloadTypes, aliasParam !== undefined
  148. ? [
  149. ts.factory.createObjectLiteralExpression([
  150. ts.factory.createPropertyAssignment('alias', ts.factory.createStringLiteral(aliasParam, true)),
  151. ], false),
  152. ]
  153. : []);
  154. const existingModifiers = (node.modifiers ?? []).filter((modifier) => !ts.isDecorator(modifier) && modifier.kind !== ts.SyntaxKind.ReadonlyKeyword);
  155. const updatedOutputDeclaration = ts.factory.createPropertyDeclaration(
  156. // Think: this logic of dealing with modifiers is applicable to all signal-based migrations
  157. ts.factory.createNodeArray([
  158. ...existingModifiers,
  159. ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword),
  160. ]), node.name, undefined, undefined, outputCall);
  161. return prepareTextReplacementForNode(info, node, printer.printNode(ts.EmitHint.Unspecified, updatedOutputDeclaration, sf));
  162. }
  163. function calculateImportReplacements(info, sourceFiles) {
  164. const importReplacements = {};
  165. for (const sf of sourceFiles) {
  166. const importManager = new checker.ImportManager();
  167. const addOnly = [];
  168. const addRemove = [];
  169. const file = project_paths.projectFile(sf, info);
  170. importManager.addImport({
  171. requestedFile: sf,
  172. exportModuleSpecifier: '@angular/core',
  173. exportSymbolName: 'output',
  174. });
  175. apply_import_manager.applyImportManagerChanges(importManager, addOnly, [sf], info);
  176. importManager.removeImport(sf, 'Output', '@angular/core');
  177. importManager.removeImport(sf, 'EventEmitter', '@angular/core');
  178. apply_import_manager.applyImportManagerChanges(importManager, addRemove, [sf], info);
  179. importReplacements[file.id] = {
  180. add: addOnly,
  181. addAndRemove: addRemove,
  182. };
  183. }
  184. return importReplacements;
  185. }
  186. function calculateNextFnReplacement(info, node) {
  187. return prepareTextReplacementForNode(info, node, 'emit');
  188. }
  189. function calculateNextFnReplacementInTemplate(file, span) {
  190. return prepareTextReplacement(file, 'emit', span.start, span.end);
  191. }
  192. function calculateNextFnReplacementInHostBinding(file, offset, span) {
  193. return prepareTextReplacement(file, 'emit', offset + span.start, offset + span.end);
  194. }
  195. function calculateCompleteCallReplacement(info, node) {
  196. return prepareTextReplacementForNode(info, node, '', node.getFullStart());
  197. }
  198. function calculatePipeCallReplacement(info, node) {
  199. if (ts.isPropertyAccessExpression(node.expression)) {
  200. const sf = node.getSourceFile();
  201. const importManager = new checker.ImportManager();
  202. const outputToObservableIdent = importManager.addImport({
  203. requestedFile: sf,
  204. exportModuleSpecifier: '@angular/core/rxjs-interop',
  205. exportSymbolName: 'outputToObservable',
  206. });
  207. const toObsCallExp = ts.factory.createCallExpression(outputToObservableIdent, undefined, [
  208. node.expression.expression,
  209. ]);
  210. const pipePropAccessExp = ts.factory.updatePropertyAccessExpression(node.expression, toObsCallExp, node.expression.name);
  211. const pipeCallExp = ts.factory.updateCallExpression(node, pipePropAccessExp, [], node.arguments);
  212. const replacements = [
  213. prepareTextReplacementForNode(info, node, printer.printNode(ts.EmitHint.Unspecified, pipeCallExp, sf)),
  214. ];
  215. apply_import_manager.applyImportManagerChanges(importManager, replacements, [sf], info);
  216. return replacements;
  217. }
  218. else {
  219. // TODO: assert instead?
  220. throw new Error(`Unexpected call expression for .pipe - expected a property access but got "${node.getText()}"`);
  221. }
  222. }
  223. function prepareTextReplacementForNode(info, node, replacement, start) {
  224. const sf = node.getSourceFile();
  225. return new project_paths.Replacement(project_paths.projectFile(sf, info), new project_paths.TextUpdate({
  226. position: start ?? node.getStart(),
  227. end: node.getEnd(),
  228. toInsert: replacement,
  229. }));
  230. }
  231. function prepareTextReplacement(file, replacement, start, end) {
  232. return new project_paths.Replacement(file, new project_paths.TextUpdate({
  233. position: start,
  234. end: end,
  235. toInsert: replacement,
  236. }));
  237. }
  238. class OutputMigration extends project_paths.TsurgeFunnelMigration {
  239. config;
  240. constructor(config = {}) {
  241. super();
  242. this.config = config;
  243. }
  244. async analyze(info) {
  245. const { sourceFiles, program: program$1 } = info;
  246. const outputFieldReplacements = {};
  247. const problematicUsages = {};
  248. let problematicDeclarationCount = 0;
  249. const filesWithOutputDeclarations = new Set();
  250. const checker$1 = program$1.getTypeChecker();
  251. const reflector = new checker.TypeScriptReflectionHost(checker$1);
  252. const dtsReader = new program.DtsMetadataReader(checker$1, reflector);
  253. const evaluator = new program.PartialEvaluator(reflector, checker$1, null);
  254. const resourceLoader = info.ngCompiler?.['resourceManager'] ?? null;
  255. // Pre-analyze the program and get access to the template type checker.
  256. // If we are processing a non-Angular target, there is no template info.
  257. const { templateTypeChecker } = info.ngCompiler?.['ensureAnalyzed']() ?? {
  258. templateTypeChecker: null,
  259. };
  260. const knownFields = {
  261. // Note: We don't support cross-target migration of `Partial<T>` usages.
  262. // This is an acceptable limitation for performance reasons.
  263. shouldTrackClassReference: () => false,
  264. attemptRetrieveDescriptorFromSymbol: (s) => {
  265. const propDeclaration = getTargetPropertyDeclaration(s);
  266. if (propDeclaration !== null) {
  267. const classFieldID = getUniqueIdForProperty(info, propDeclaration);
  268. if (classFieldID !== null) {
  269. return {
  270. node: propDeclaration,
  271. key: classFieldID,
  272. };
  273. }
  274. }
  275. return null;
  276. },
  277. };
  278. let isTestFile = false;
  279. const outputMigrationVisitor = (node) => {
  280. // detect output declarations
  281. if (ts.isPropertyDeclaration(node)) {
  282. const outputDecorator = getOutputDecorator(node, reflector);
  283. if (outputDecorator !== null) {
  284. if (isOutputDeclarationEligibleForMigration(node)) {
  285. const outputDef = {
  286. id: getUniqueIdForProperty(info, node),
  287. aliasParam: outputDecorator.args?.at(0),
  288. };
  289. const outputFile = project_paths.projectFile(node.getSourceFile(), info);
  290. if (this.config.shouldMigrate === undefined ||
  291. this.config.shouldMigrate({
  292. key: outputDef.id,
  293. node: node,
  294. }, outputFile)) {
  295. const aliasParam = outputDef.aliasParam;
  296. const aliasOptionValue = aliasParam ? evaluator.evaluate(aliasParam) : undefined;
  297. if (aliasOptionValue == undefined || typeof aliasOptionValue === 'string') {
  298. filesWithOutputDeclarations.add(node.getSourceFile());
  299. addOutputReplacement(outputFieldReplacements, outputDef.id, outputFile, calculateDeclarationReplacement(info, node, aliasOptionValue?.toString()));
  300. }
  301. else {
  302. problematicUsages[outputDef.id] = true;
  303. problematicDeclarationCount++;
  304. }
  305. }
  306. }
  307. else {
  308. problematicDeclarationCount++;
  309. }
  310. }
  311. }
  312. // detect .next usages that should be migrated to .emit
  313. if (isPotentialNextCallUsage(node) && ts.isPropertyAccessExpression(node.expression)) {
  314. const propertyDeclaration = isTargetOutputDeclaration(node.expression.expression, checker$1, reflector, dtsReader);
  315. if (propertyDeclaration !== null) {
  316. const id = getUniqueIdForProperty(info, propertyDeclaration);
  317. const outputFile = project_paths.projectFile(node.getSourceFile(), info);
  318. addOutputReplacement(outputFieldReplacements, id, outputFile, calculateNextFnReplacement(info, node.expression.name));
  319. }
  320. }
  321. // detect .complete usages that should be removed
  322. if (isPotentialCompleteCallUsage(node) && ts.isPropertyAccessExpression(node.expression)) {
  323. const propertyDeclaration = isTargetOutputDeclaration(node.expression.expression, checker$1, reflector, dtsReader);
  324. if (propertyDeclaration !== null) {
  325. const id = getUniqueIdForProperty(info, propertyDeclaration);
  326. const outputFile = project_paths.projectFile(node.getSourceFile(), info);
  327. if (ts.isExpressionStatement(node.parent)) {
  328. addOutputReplacement(outputFieldReplacements, id, outputFile, calculateCompleteCallReplacement(info, node.parent));
  329. }
  330. else {
  331. problematicUsages[id] = true;
  332. }
  333. }
  334. }
  335. // detect imports of test runners
  336. if (isTestRunnerImport(node)) {
  337. isTestFile = true;
  338. }
  339. // detect unsafe access of the output property
  340. if (isPotentialPipeCallUsage(node) && ts.isPropertyAccessExpression(node.expression)) {
  341. const propertyDeclaration = isTargetOutputDeclaration(node.expression.expression, checker$1, reflector, dtsReader);
  342. if (propertyDeclaration !== null) {
  343. const id = getUniqueIdForProperty(info, propertyDeclaration);
  344. if (isTestFile) {
  345. const outputFile = project_paths.projectFile(node.getSourceFile(), info);
  346. addOutputReplacement(outputFieldReplacements, id, outputFile, ...calculatePipeCallReplacement(info, node));
  347. }
  348. else {
  349. problematicUsages[id] = true;
  350. }
  351. }
  352. }
  353. ts.forEachChild(node, outputMigrationVisitor);
  354. };
  355. // calculate output migration replacements
  356. for (const sf of sourceFiles) {
  357. isTestFile = false;
  358. ts.forEachChild(sf, outputMigrationVisitor);
  359. }
  360. // take care of the references in templates and host bindings
  361. const referenceResult = { references: [] };
  362. const { visitor: templateHostRefVisitor } = index.createFindAllSourceFileReferencesVisitor(info, checker$1, reflector, resourceLoader, evaluator, templateTypeChecker, knownFields, null, // TODO: capture known output names as an optimization
  363. referenceResult);
  364. // calculate template / host binding replacements
  365. for (const sf of sourceFiles) {
  366. ts.forEachChild(sf, templateHostRefVisitor);
  367. }
  368. for (const ref of referenceResult.references) {
  369. // detect .next usages that should be migrated to .emit in template and host binding expressions
  370. if (ref.kind === index.ReferenceKind.InTemplate) {
  371. const callExpr = checkNonTsReferenceCallsField(ref, 'next');
  372. // TODO: here and below for host bindings, we should ideally filter in the global meta stage
  373. // (instead of using the `outputFieldReplacements` map)
  374. // as technically, the call expression could refer to an output
  375. // from a whole different compilation unit (e.g. tsconfig.json).
  376. if (callExpr !== null && outputFieldReplacements[ref.target.key] !== undefined) {
  377. addOutputReplacement(outputFieldReplacements, ref.target.key, ref.from.templateFile, calculateNextFnReplacementInTemplate(ref.from.templateFile, callExpr.nameSpan));
  378. }
  379. }
  380. else if (ref.kind === index.ReferenceKind.InHostBinding) {
  381. const callExpr = checkNonTsReferenceCallsField(ref, 'next');
  382. if (callExpr !== null && outputFieldReplacements[ref.target.key] !== undefined) {
  383. addOutputReplacement(outputFieldReplacements, ref.target.key, ref.from.file, calculateNextFnReplacementInHostBinding(ref.from.file, ref.from.hostPropertyNode.getStart() + 1, callExpr.nameSpan));
  384. }
  385. }
  386. }
  387. // calculate import replacements but do so only for files that have output declarations
  388. const importReplacements = calculateImportReplacements(info, filesWithOutputDeclarations);
  389. return project_paths.confirmAsSerializable({
  390. problematicDeclarationCount,
  391. outputFields: outputFieldReplacements,
  392. importReplacements,
  393. problematicUsages,
  394. });
  395. }
  396. async combine(unitA, unitB) {
  397. const outputFields = {};
  398. const importReplacements = {};
  399. const problematicUsages = {};
  400. let problematicDeclarationCount = 0;
  401. for (const unit of [unitA, unitB]) {
  402. for (const declIdStr of Object.keys(unit.outputFields)) {
  403. const declId = declIdStr;
  404. // THINK: detect clash? Should we have an utility to merge data based on unique IDs?
  405. outputFields[declId] = unit.outputFields[declId];
  406. }
  407. for (const fileIDStr of Object.keys(unit.importReplacements)) {
  408. const fileID = fileIDStr;
  409. importReplacements[fileID] = unit.importReplacements[fileID];
  410. }
  411. problematicDeclarationCount += unit.problematicDeclarationCount;
  412. }
  413. for (const unit of [unitA, unitB]) {
  414. for (const declIdStr of Object.keys(unit.problematicUsages)) {
  415. const declId = declIdStr;
  416. problematicUsages[declId] = unit.problematicUsages[declId];
  417. }
  418. }
  419. return project_paths.confirmAsSerializable({
  420. problematicDeclarationCount,
  421. outputFields,
  422. importReplacements,
  423. problematicUsages,
  424. });
  425. }
  426. async globalMeta(combinedData) {
  427. const globalMeta = {
  428. importReplacements: combinedData.importReplacements,
  429. outputFields: combinedData.outputFields,
  430. problematicDeclarationCount: combinedData.problematicDeclarationCount,
  431. problematicUsages: {},
  432. };
  433. for (const keyStr of Object.keys(combinedData.problematicUsages)) {
  434. const key = keyStr;
  435. // it might happen that a problematic usage is detected but we didn't see the declaration - skipping those
  436. if (globalMeta.outputFields[key] !== undefined) {
  437. globalMeta.problematicUsages[key] = true;
  438. }
  439. }
  440. // Noop here as we don't have any form of special global metadata.
  441. return project_paths.confirmAsSerializable(combinedData);
  442. }
  443. async stats(globalMetadata) {
  444. const detectedOutputs = new Set(Object.keys(globalMetadata.outputFields)).size +
  445. globalMetadata.problematicDeclarationCount;
  446. const problematicOutputs = new Set(Object.keys(globalMetadata.problematicUsages)).size +
  447. globalMetadata.problematicDeclarationCount;
  448. const successRate = detectedOutputs > 0 ? (detectedOutputs - problematicOutputs) / detectedOutputs : 1;
  449. return {
  450. counters: {
  451. detectedOutputs,
  452. problematicOutputs,
  453. successRate,
  454. },
  455. };
  456. }
  457. async migrate(globalData) {
  458. const migratedFiles = new Set();
  459. const problematicFiles = new Set();
  460. const replacements = [];
  461. for (const declIdStr of Object.keys(globalData.outputFields)) {
  462. const declId = declIdStr;
  463. const outputField = globalData.outputFields[declId];
  464. if (!globalData.problematicUsages[declId]) {
  465. replacements.push(...outputField.replacements);
  466. migratedFiles.add(outputField.file.id);
  467. }
  468. else {
  469. problematicFiles.add(outputField.file.id);
  470. }
  471. }
  472. for (const fileIDStr of Object.keys(globalData.importReplacements)) {
  473. const fileID = fileIDStr;
  474. if (migratedFiles.has(fileID)) {
  475. const importReplacements = globalData.importReplacements[fileID];
  476. if (problematicFiles.has(fileID)) {
  477. replacements.push(...importReplacements.add);
  478. }
  479. else {
  480. replacements.push(...importReplacements.addAndRemove);
  481. }
  482. }
  483. }
  484. return { replacements };
  485. }
  486. }
  487. function addOutputReplacement(outputFieldReplacements, outputId, file, ...replacements) {
  488. let existingReplacements = outputFieldReplacements[outputId];
  489. if (existingReplacements === undefined) {
  490. outputFieldReplacements[outputId] = existingReplacements = {
  491. file: file,
  492. replacements: [],
  493. };
  494. }
  495. existingReplacements.replacements.push(...replacements);
  496. }
  497. function migrate(options) {
  498. return async (tree, context) => {
  499. const { buildPaths, testPaths } = await project_tsconfig_paths.getProjectTsConfigPaths(tree);
  500. if (!buildPaths.length && !testPaths.length) {
  501. throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run output migration.');
  502. }
  503. const fs = new project_paths.DevkitMigrationFilesystem(tree);
  504. checker.setFileSystem(fs);
  505. const migration = new OutputMigration({
  506. shouldMigrate: (_, file) => {
  507. return (file.rootRelativePath.startsWith(fs.normalize(options.path)) &&
  508. !/(^|\/)node_modules\//.test(file.rootRelativePath));
  509. },
  510. });
  511. const analysisPath = fs.resolve(options.analysisDir);
  512. const unitResults = [];
  513. const programInfos = [...buildPaths, ...testPaths].map((tsconfigPath) => {
  514. context.logger.info(`Preparing analysis for: ${tsconfigPath}..`);
  515. const baseInfo = migration.createProgram(tsconfigPath, fs);
  516. const info = migration.prepareProgram(baseInfo);
  517. // Support restricting the analysis to subfolders for larger projects.
  518. if (analysisPath !== '/') {
  519. info.sourceFiles = info.sourceFiles.filter((sf) => sf.fileName.startsWith(analysisPath));
  520. info.fullProgramSourceFiles = info.fullProgramSourceFiles.filter((sf) => sf.fileName.startsWith(analysisPath));
  521. }
  522. return { info, tsconfigPath };
  523. });
  524. // Analyze phase. Treat all projects as compilation units as
  525. // this allows us to support references between those.
  526. for (const { info, tsconfigPath } of programInfos) {
  527. context.logger.info(`Scanning for outputs: ${tsconfigPath}..`);
  528. unitResults.push(await migration.analyze(info));
  529. }
  530. context.logger.info(``);
  531. context.logger.info(`Processing analysis data between targets..`);
  532. context.logger.info(``);
  533. const combined = await project_paths.synchronouslyCombineUnitData(migration, unitResults);
  534. if (combined === null) {
  535. context.logger.error('Migration failed unexpectedly with no analysis data');
  536. return;
  537. }
  538. const globalMeta = await migration.globalMeta(combined);
  539. const replacementsPerFile = new Map();
  540. for (const { info, tsconfigPath } of programInfos) {
  541. context.logger.info(`Migrating: ${tsconfigPath}..`);
  542. const { replacements } = await migration.migrate(globalMeta);
  543. const changesPerFile = project_paths.groupReplacementsByFile(replacements);
  544. for (const [file, changes] of changesPerFile) {
  545. if (!replacementsPerFile.has(file)) {
  546. replacementsPerFile.set(file, changes);
  547. }
  548. }
  549. }
  550. context.logger.info(`Applying changes..`);
  551. for (const [file, changes] of replacementsPerFile) {
  552. const recorder = tree.beginUpdate(file);
  553. for (const c of changes) {
  554. recorder
  555. .remove(c.data.position, c.data.end - c.data.position)
  556. .insertLeft(c.data.position, c.data.toInsert);
  557. }
  558. tree.commitUpdate(recorder);
  559. }
  560. const { counters: { detectedOutputs, problematicOutputs, successRate }, } = await migration.stats(globalMeta);
  561. const migratedOutputs = detectedOutputs - problematicOutputs;
  562. const successRatePercent = (successRate * 100).toFixed(2);
  563. context.logger.info('');
  564. context.logger.info(`Successfully migrated to outputs as functions 🎉`);
  565. context.logger.info(` -> Migrated ${migratedOutputs} out of ${detectedOutputs} detected outputs (${successRatePercent} %).`);
  566. };
  567. }
  568. exports.migrate = migrate;