output-migration.cjs 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. 'use strict';
  2. /**
  3. * @license Angular v19.2.13
  4. * (c) 2010-2025 Google LLC. https://angular.io/
  5. * License: MIT
  6. */
  7. 'use strict';
  8. var ts = require('typescript');
  9. require('os');
  10. var checker = require('./checker-5pyJrZ9G.cjs');
  11. var index$1 = require('./index-BIvVb6in.cjs');
  12. require('path');
  13. var project_paths = require('./project_paths-CyWVEsbT.cjs');
  14. var apply_import_manager = require('./apply_import_manager-QQDfWa1Z.cjs');
  15. var index = require('./index-BPhQoCcF.cjs');
  16. require('@angular-devkit/core');
  17. require('node:path/posix');
  18. require('fs');
  19. require('module');
  20. require('url');
  21. require('@angular-devkit/schematics');
  22. require('./project_tsconfig_paths-CDVxT6Ov.cjs');
  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. let payloadTypes;
  145. if (node.initializer && ts.isNewExpression(node.initializer) && node.initializer.typeArguments) {
  146. payloadTypes = node.initializer.typeArguments;
  147. }
  148. else if (node.type && ts.isTypeReferenceNode(node.type) && node.type.typeArguments) {
  149. payloadTypes = ts.factory.createNodeArray(node.type.typeArguments);
  150. }
  151. const outputCall = ts.factory.createCallExpression(ts.factory.createIdentifier('output'), payloadTypes, aliasParam !== undefined
  152. ? [
  153. ts.factory.createObjectLiteralExpression([
  154. ts.factory.createPropertyAssignment('alias', ts.factory.createStringLiteral(aliasParam, true)),
  155. ], false),
  156. ]
  157. : []);
  158. const existingModifiers = (node.modifiers ?? []).filter((modifier) => !ts.isDecorator(modifier) && modifier.kind !== ts.SyntaxKind.ReadonlyKeyword);
  159. const updatedOutputDeclaration = ts.factory.createPropertyDeclaration(
  160. // Think: this logic of dealing with modifiers is applicable to all signal-based migrations
  161. ts.factory.createNodeArray([
  162. ...existingModifiers,
  163. ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword),
  164. ]), node.name, undefined, undefined, outputCall);
  165. return prepareTextReplacementForNode(info, node, printer.printNode(ts.EmitHint.Unspecified, updatedOutputDeclaration, sf));
  166. }
  167. function calculateImportReplacements(info, sourceFiles) {
  168. const importReplacements = {};
  169. for (const sf of sourceFiles) {
  170. const importManager = new checker.ImportManager();
  171. const addOnly = [];
  172. const addRemove = [];
  173. const file = project_paths.projectFile(sf, info);
  174. importManager.addImport({
  175. requestedFile: sf,
  176. exportModuleSpecifier: '@angular/core',
  177. exportSymbolName: 'output',
  178. });
  179. apply_import_manager.applyImportManagerChanges(importManager, addOnly, [sf], info);
  180. importManager.removeImport(sf, 'Output', '@angular/core');
  181. importManager.removeImport(sf, 'EventEmitter', '@angular/core');
  182. apply_import_manager.applyImportManagerChanges(importManager, addRemove, [sf], info);
  183. importReplacements[file.id] = {
  184. add: addOnly,
  185. addAndRemove: addRemove,
  186. };
  187. }
  188. return importReplacements;
  189. }
  190. function calculateNextFnReplacement(info, node) {
  191. return prepareTextReplacementForNode(info, node, 'emit');
  192. }
  193. function calculateNextFnReplacementInTemplate(file, span) {
  194. return prepareTextReplacement(file, 'emit', span.start, span.end);
  195. }
  196. function calculateNextFnReplacementInHostBinding(file, offset, span) {
  197. return prepareTextReplacement(file, 'emit', offset + span.start, offset + span.end);
  198. }
  199. function calculateCompleteCallReplacement(info, node) {
  200. return prepareTextReplacementForNode(info, node, '', node.getFullStart());
  201. }
  202. function calculatePipeCallReplacement(info, node) {
  203. if (ts.isPropertyAccessExpression(node.expression)) {
  204. const sf = node.getSourceFile();
  205. const importManager = new checker.ImportManager();
  206. const outputToObservableIdent = importManager.addImport({
  207. requestedFile: sf,
  208. exportModuleSpecifier: '@angular/core/rxjs-interop',
  209. exportSymbolName: 'outputToObservable',
  210. });
  211. const toObsCallExp = ts.factory.createCallExpression(outputToObservableIdent, undefined, [
  212. node.expression.expression,
  213. ]);
  214. const pipePropAccessExp = ts.factory.updatePropertyAccessExpression(node.expression, toObsCallExp, node.expression.name);
  215. const pipeCallExp = ts.factory.updateCallExpression(node, pipePropAccessExp, [], node.arguments);
  216. const replacements = [
  217. prepareTextReplacementForNode(info, node, printer.printNode(ts.EmitHint.Unspecified, pipeCallExp, sf)),
  218. ];
  219. apply_import_manager.applyImportManagerChanges(importManager, replacements, [sf], info);
  220. return replacements;
  221. }
  222. else {
  223. // TODO: assert instead?
  224. throw new Error(`Unexpected call expression for .pipe - expected a property access but got "${node.getText()}"`);
  225. }
  226. }
  227. function prepareTextReplacementForNode(info, node, replacement, start) {
  228. const sf = node.getSourceFile();
  229. return new project_paths.Replacement(project_paths.projectFile(sf, info), new project_paths.TextUpdate({
  230. position: start ?? node.getStart(),
  231. end: node.getEnd(),
  232. toInsert: replacement,
  233. }));
  234. }
  235. function prepareTextReplacement(file, replacement, start, end) {
  236. return new project_paths.Replacement(file, new project_paths.TextUpdate({
  237. position: start,
  238. end: end,
  239. toInsert: replacement,
  240. }));
  241. }
  242. class OutputMigration extends project_paths.TsurgeFunnelMigration {
  243. config;
  244. constructor(config = {}) {
  245. super();
  246. this.config = config;
  247. }
  248. async analyze(info) {
  249. const { sourceFiles, program } = info;
  250. const outputFieldReplacements = {};
  251. const problematicUsages = {};
  252. let problematicDeclarationCount = 0;
  253. const filesWithOutputDeclarations = new Set();
  254. const checker$1 = program.getTypeChecker();
  255. const reflector = new checker.TypeScriptReflectionHost(checker$1);
  256. const dtsReader = new index$1.DtsMetadataReader(checker$1, reflector);
  257. const evaluator = new index$1.PartialEvaluator(reflector, checker$1, null);
  258. const resourceLoader = info.ngCompiler?.['resourceManager'] ?? null;
  259. // Pre-analyze the program and get access to the template type checker.
  260. // If we are processing a non-Angular target, there is no template info.
  261. const { templateTypeChecker } = info.ngCompiler?.['ensureAnalyzed']() ?? {
  262. templateTypeChecker: null,
  263. };
  264. const knownFields = {
  265. // Note: We don't support cross-target migration of `Partial<T>` usages.
  266. // This is an acceptable limitation for performance reasons.
  267. shouldTrackClassReference: () => false,
  268. attemptRetrieveDescriptorFromSymbol: (s) => {
  269. const propDeclaration = getTargetPropertyDeclaration(s);
  270. if (propDeclaration !== null) {
  271. const classFieldID = getUniqueIdForProperty(info, propDeclaration);
  272. if (classFieldID !== null) {
  273. return {
  274. node: propDeclaration,
  275. key: classFieldID,
  276. };
  277. }
  278. }
  279. return null;
  280. },
  281. };
  282. let isTestFile = false;
  283. const outputMigrationVisitor = (node) => {
  284. // detect output declarations
  285. if (ts.isPropertyDeclaration(node)) {
  286. const outputDecorator = getOutputDecorator(node, reflector);
  287. if (outputDecorator !== null) {
  288. if (isOutputDeclarationEligibleForMigration(node)) {
  289. const outputDef = {
  290. id: getUniqueIdForProperty(info, node),
  291. aliasParam: outputDecorator.args?.at(0),
  292. };
  293. const outputFile = project_paths.projectFile(node.getSourceFile(), info);
  294. if (this.config.shouldMigrate === undefined ||
  295. this.config.shouldMigrate({
  296. key: outputDef.id,
  297. node: node,
  298. }, outputFile)) {
  299. const aliasParam = outputDef.aliasParam;
  300. const aliasOptionValue = aliasParam ? evaluator.evaluate(aliasParam) : undefined;
  301. if (aliasOptionValue == undefined || typeof aliasOptionValue === 'string') {
  302. filesWithOutputDeclarations.add(node.getSourceFile());
  303. addOutputReplacement(outputFieldReplacements, outputDef.id, outputFile, calculateDeclarationReplacement(info, node, aliasOptionValue?.toString()));
  304. }
  305. else {
  306. problematicUsages[outputDef.id] = true;
  307. problematicDeclarationCount++;
  308. }
  309. }
  310. }
  311. else {
  312. problematicDeclarationCount++;
  313. }
  314. }
  315. }
  316. // detect .next usages that should be migrated to .emit
  317. if (isPotentialNextCallUsage(node) && ts.isPropertyAccessExpression(node.expression)) {
  318. const propertyDeclaration = isTargetOutputDeclaration(node.expression.expression, checker$1, reflector, dtsReader);
  319. if (propertyDeclaration !== null) {
  320. const id = getUniqueIdForProperty(info, propertyDeclaration);
  321. const outputFile = project_paths.projectFile(node.getSourceFile(), info);
  322. addOutputReplacement(outputFieldReplacements, id, outputFile, calculateNextFnReplacement(info, node.expression.name));
  323. }
  324. }
  325. // detect .complete usages that should be removed
  326. if (isPotentialCompleteCallUsage(node) && ts.isPropertyAccessExpression(node.expression)) {
  327. const propertyDeclaration = isTargetOutputDeclaration(node.expression.expression, checker$1, reflector, dtsReader);
  328. if (propertyDeclaration !== null) {
  329. const id = getUniqueIdForProperty(info, propertyDeclaration);
  330. const outputFile = project_paths.projectFile(node.getSourceFile(), info);
  331. if (ts.isExpressionStatement(node.parent)) {
  332. addOutputReplacement(outputFieldReplacements, id, outputFile, calculateCompleteCallReplacement(info, node.parent));
  333. }
  334. else {
  335. problematicUsages[id] = true;
  336. }
  337. }
  338. }
  339. addCommentForEmptyEmit(node, info, checker$1, reflector, dtsReader, outputFieldReplacements);
  340. // detect imports of test runners
  341. if (isTestRunnerImport(node)) {
  342. isTestFile = true;
  343. }
  344. // detect unsafe access of the output property
  345. if (isPotentialPipeCallUsage(node) && ts.isPropertyAccessExpression(node.expression)) {
  346. const propertyDeclaration = isTargetOutputDeclaration(node.expression.expression, checker$1, reflector, dtsReader);
  347. if (propertyDeclaration !== null) {
  348. const id = getUniqueIdForProperty(info, propertyDeclaration);
  349. if (isTestFile) {
  350. const outputFile = project_paths.projectFile(node.getSourceFile(), info);
  351. addOutputReplacement(outputFieldReplacements, id, outputFile, ...calculatePipeCallReplacement(info, node));
  352. }
  353. else {
  354. problematicUsages[id] = true;
  355. }
  356. }
  357. }
  358. ts.forEachChild(node, outputMigrationVisitor);
  359. };
  360. // calculate output migration replacements
  361. for (const sf of sourceFiles) {
  362. isTestFile = false;
  363. ts.forEachChild(sf, outputMigrationVisitor);
  364. }
  365. // take care of the references in templates and host bindings
  366. const referenceResult = { references: [] };
  367. const { visitor: templateHostRefVisitor } = index.createFindAllSourceFileReferencesVisitor(info, checker$1, reflector, resourceLoader, evaluator, templateTypeChecker, knownFields, null, // TODO: capture known output names as an optimization
  368. referenceResult);
  369. // calculate template / host binding replacements
  370. for (const sf of sourceFiles) {
  371. ts.forEachChild(sf, templateHostRefVisitor);
  372. }
  373. for (const ref of referenceResult.references) {
  374. // detect .next usages that should be migrated to .emit in template and host binding expressions
  375. if (ref.kind === index.ReferenceKind.InTemplate) {
  376. const callExpr = checkNonTsReferenceCallsField(ref, 'next');
  377. // TODO: here and below for host bindings, we should ideally filter in the global meta stage
  378. // (instead of using the `outputFieldReplacements` map)
  379. // as technically, the call expression could refer to an output
  380. // from a whole different compilation unit (e.g. tsconfig.json).
  381. if (callExpr !== null && outputFieldReplacements[ref.target.key] !== undefined) {
  382. addOutputReplacement(outputFieldReplacements, ref.target.key, ref.from.templateFile, calculateNextFnReplacementInTemplate(ref.from.templateFile, callExpr.nameSpan));
  383. }
  384. }
  385. else if (ref.kind === index.ReferenceKind.InHostBinding) {
  386. const callExpr = checkNonTsReferenceCallsField(ref, 'next');
  387. if (callExpr !== null && outputFieldReplacements[ref.target.key] !== undefined) {
  388. addOutputReplacement(outputFieldReplacements, ref.target.key, ref.from.file, calculateNextFnReplacementInHostBinding(ref.from.file, ref.from.hostPropertyNode.getStart() + 1, callExpr.nameSpan));
  389. }
  390. }
  391. }
  392. // calculate import replacements but do so only for files that have output declarations
  393. const importReplacements = calculateImportReplacements(info, filesWithOutputDeclarations);
  394. return project_paths.confirmAsSerializable({
  395. problematicDeclarationCount,
  396. outputFields: outputFieldReplacements,
  397. importReplacements,
  398. problematicUsages,
  399. });
  400. }
  401. async combine(unitA, unitB) {
  402. const outputFields = {};
  403. const importReplacements = {};
  404. const problematicUsages = {};
  405. let problematicDeclarationCount = 0;
  406. for (const unit of [unitA, unitB]) {
  407. for (const declIdStr of Object.keys(unit.outputFields)) {
  408. const declId = declIdStr;
  409. // THINK: detect clash? Should we have an utility to merge data based on unique IDs?
  410. outputFields[declId] = unit.outputFields[declId];
  411. }
  412. for (const fileIDStr of Object.keys(unit.importReplacements)) {
  413. const fileID = fileIDStr;
  414. importReplacements[fileID] = unit.importReplacements[fileID];
  415. }
  416. problematicDeclarationCount += unit.problematicDeclarationCount;
  417. }
  418. for (const unit of [unitA, unitB]) {
  419. for (const declIdStr of Object.keys(unit.problematicUsages)) {
  420. const declId = declIdStr;
  421. problematicUsages[declId] = unit.problematicUsages[declId];
  422. }
  423. }
  424. return project_paths.confirmAsSerializable({
  425. problematicDeclarationCount,
  426. outputFields,
  427. importReplacements,
  428. problematicUsages,
  429. });
  430. }
  431. async globalMeta(combinedData) {
  432. const globalMeta = {
  433. importReplacements: combinedData.importReplacements,
  434. outputFields: combinedData.outputFields,
  435. problematicDeclarationCount: combinedData.problematicDeclarationCount,
  436. problematicUsages: {},
  437. };
  438. for (const keyStr of Object.keys(combinedData.problematicUsages)) {
  439. const key = keyStr;
  440. // it might happen that a problematic usage is detected but we didn't see the declaration - skipping those
  441. if (globalMeta.outputFields[key] !== undefined) {
  442. globalMeta.problematicUsages[key] = true;
  443. }
  444. }
  445. // Noop here as we don't have any form of special global metadata.
  446. return project_paths.confirmAsSerializable(combinedData);
  447. }
  448. async stats(globalMetadata) {
  449. const detectedOutputs = new Set(Object.keys(globalMetadata.outputFields)).size +
  450. globalMetadata.problematicDeclarationCount;
  451. const problematicOutputs = new Set(Object.keys(globalMetadata.problematicUsages)).size +
  452. globalMetadata.problematicDeclarationCount;
  453. const successRate = detectedOutputs > 0 ? (detectedOutputs - problematicOutputs) / detectedOutputs : 1;
  454. return {
  455. counters: {
  456. detectedOutputs,
  457. problematicOutputs,
  458. successRate,
  459. },
  460. };
  461. }
  462. async migrate(globalData) {
  463. const migratedFiles = new Set();
  464. const problematicFiles = new Set();
  465. const replacements = [];
  466. for (const declIdStr of Object.keys(globalData.outputFields)) {
  467. const declId = declIdStr;
  468. const outputField = globalData.outputFields[declId];
  469. if (!globalData.problematicUsages[declId]) {
  470. replacements.push(...outputField.replacements);
  471. migratedFiles.add(outputField.file.id);
  472. }
  473. else {
  474. problematicFiles.add(outputField.file.id);
  475. }
  476. }
  477. for (const fileIDStr of Object.keys(globalData.importReplacements)) {
  478. const fileID = fileIDStr;
  479. if (migratedFiles.has(fileID)) {
  480. const importReplacements = globalData.importReplacements[fileID];
  481. if (problematicFiles.has(fileID)) {
  482. replacements.push(...importReplacements.add);
  483. }
  484. else {
  485. replacements.push(...importReplacements.addAndRemove);
  486. }
  487. }
  488. }
  489. return { replacements };
  490. }
  491. }
  492. function addOutputReplacement(outputFieldReplacements, outputId, file, ...replacements) {
  493. let existingReplacements = outputFieldReplacements[outputId];
  494. if (existingReplacements === undefined) {
  495. outputFieldReplacements[outputId] = existingReplacements = {
  496. file: file,
  497. replacements: [],
  498. };
  499. }
  500. existingReplacements.replacements.push(...replacements);
  501. }
  502. function addCommentForEmptyEmit(node, info, checker, reflector, dtsReader, outputFieldReplacements) {
  503. if (!isEmptyEmitCall(node))
  504. return;
  505. const propertyAccess = getPropertyAccess(node);
  506. if (!propertyAccess)
  507. return;
  508. const symbol = checker.getSymbolAtLocation(propertyAccess.name);
  509. if (!symbol || !symbol.declarations?.length)
  510. return;
  511. const propertyDeclaration = isTargetOutputDeclaration(propertyAccess, checker, reflector, dtsReader);
  512. if (!propertyDeclaration)
  513. return;
  514. const eventEmitterType = getEventEmitterArgumentType(propertyDeclaration);
  515. if (!eventEmitterType)
  516. return;
  517. const id = getUniqueIdForProperty(info, propertyDeclaration);
  518. const file = project_paths.projectFile(node.getSourceFile(), info);
  519. const formatter = getFormatterText(node);
  520. const todoReplacement = new project_paths.TextUpdate({
  521. toInsert: `${formatter.indent}// TODO: The 'emit' function requires a mandatory ${eventEmitterType} argument\n`,
  522. end: formatter.lineStartPos,
  523. position: formatter.lineStartPos,
  524. });
  525. addOutputReplacement(outputFieldReplacements, id, file, new project_paths.Replacement(file, todoReplacement));
  526. }
  527. function isEmptyEmitCall(node) {
  528. return (ts.isCallExpression(node) &&
  529. ts.isPropertyAccessExpression(node.expression) &&
  530. node.expression.name.text === 'emit' &&
  531. node.arguments.length === 0);
  532. }
  533. function getPropertyAccess(node) {
  534. const propertyAccessExpression = node.expression.expression;
  535. return ts.isPropertyAccessExpression(propertyAccessExpression) ? propertyAccessExpression : null;
  536. }
  537. function getEventEmitterArgumentType(propertyDeclaration) {
  538. const initializer = propertyDeclaration.initializer;
  539. if (!initializer || !ts.isNewExpression(initializer))
  540. return null;
  541. const isEventEmitter = ts.isIdentifier(initializer.expression) && initializer.expression.getText() === 'EventEmitter';
  542. if (!isEventEmitter)
  543. return null;
  544. const [typeArg] = initializer.typeArguments ?? [];
  545. return typeArg ? typeArg.getText() : null;
  546. }
  547. function getFormatterText(node) {
  548. const sourceFile = node.getSourceFile();
  549. const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
  550. const lineStartPos = sourceFile.getPositionOfLineAndCharacter(line, 0);
  551. const indent = sourceFile.text.slice(lineStartPos, node.getStart());
  552. return { indent, lineStartPos };
  553. }
  554. function migrate(options) {
  555. return async (tree, context) => {
  556. await project_paths.runMigrationInDevkit({
  557. tree,
  558. getMigration: (fs) => new OutputMigration({
  559. shouldMigrate: (_, file) => {
  560. return (file.rootRelativePath.startsWith(fs.normalize(options.path)) &&
  561. !/(^|\/)node_modules\//.test(file.rootRelativePath));
  562. },
  563. }),
  564. beforeProgramCreation: (tsconfigPath, stage) => {
  565. if (stage === project_paths.MigrationStage.Analysis) {
  566. context.logger.info(`Preparing analysis for: ${tsconfigPath}...`);
  567. }
  568. else {
  569. context.logger.info(`Running migration for: ${tsconfigPath}...`);
  570. }
  571. },
  572. afterProgramCreation: (info, fs) => {
  573. const analysisPath = fs.resolve(options.analysisDir);
  574. // Support restricting the analysis to subfolders for larger projects.
  575. if (analysisPath !== '/') {
  576. info.sourceFiles = info.sourceFiles.filter((sf) => sf.fileName.startsWith(analysisPath));
  577. info.fullProgramSourceFiles = info.fullProgramSourceFiles.filter((sf) => sf.fileName.startsWith(analysisPath));
  578. }
  579. },
  580. beforeUnitAnalysis: (tsconfigPath) => {
  581. context.logger.info(`Scanning for outputs: ${tsconfigPath}...`);
  582. },
  583. afterAllAnalyzed: () => {
  584. context.logger.info(``);
  585. context.logger.info(`Processing analysis data between targets...`);
  586. context.logger.info(``);
  587. },
  588. afterAnalysisFailure: () => {
  589. context.logger.error('Migration failed unexpectedly with no analysis data');
  590. },
  591. whenDone: ({ counters }) => {
  592. const { detectedOutputs, problematicOutputs, successRate } = counters;
  593. const migratedOutputs = detectedOutputs - problematicOutputs;
  594. const successRatePercent = (successRate * 100).toFixed(2);
  595. context.logger.info('');
  596. context.logger.info(`Successfully migrated to outputs as functions 🎉`);
  597. context.logger.info(` -> Migrated ${migratedOutputs} out of ${detectedOutputs} detected outputs (${successRatePercent} %).`);
  598. },
  599. });
  600. };
  601. }
  602. exports.migrate = migrate;