control-flow-migration.cjs 79 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919
  1. 'use strict';
  2. /**
  3. * @license Angular v19.2.13
  4. * (c) 2010-2025 Google LLC. https://angular.io/
  5. * License: MIT
  6. */
  7. 'use strict';
  8. var schematics = require('@angular-devkit/schematics');
  9. var p = require('path');
  10. var compiler_host = require('./compiler_host-B1Gyeytz.cjs');
  11. var checker = require('./checker-5pyJrZ9G.cjs');
  12. var ts = require('typescript');
  13. require('os');
  14. require('fs');
  15. require('module');
  16. require('url');
  17. function lookupIdentifiersInSourceFile(sourceFile, names) {
  18. const results = new Set();
  19. const visit = (node) => {
  20. if (ts.isIdentifier(node) && names.includes(node.text)) {
  21. results.add(node);
  22. }
  23. ts.forEachChild(node, visit);
  24. };
  25. visit(sourceFile);
  26. return results;
  27. }
  28. const ngtemplate = 'ng-template';
  29. const boundngifelse = '[ngIfElse]';
  30. const boundngifthenelse = '[ngIfThenElse]';
  31. const boundngifthen = '[ngIfThen]';
  32. const nakedngfor$1 = 'ngFor';
  33. const startMarker = '◬';
  34. const endMarker = '✢';
  35. const startI18nMarker = '⚈';
  36. const endI18nMarker = '⚉';
  37. const importRemovals = [
  38. 'NgIf',
  39. 'NgIfElse',
  40. 'NgIfThenElse',
  41. 'NgFor',
  42. 'NgForOf',
  43. 'NgForTrackBy',
  44. 'NgSwitch',
  45. 'NgSwitchCase',
  46. 'NgSwitchDefault',
  47. ];
  48. const importWithCommonRemovals = [...importRemovals, 'CommonModule'];
  49. function allFormsOf(selector) {
  50. return [selector, `*${selector}`, `[${selector}]`];
  51. }
  52. const commonModuleDirectives = new Set([
  53. ...allFormsOf('ngComponentOutlet'),
  54. ...allFormsOf('ngTemplateOutlet'),
  55. ...allFormsOf('ngClass'),
  56. ...allFormsOf('ngPlural'),
  57. ...allFormsOf('ngPluralCase'),
  58. ...allFormsOf('ngStyle'),
  59. ...allFormsOf('ngTemplateOutlet'),
  60. ...allFormsOf('ngComponentOutlet'),
  61. '[NgForOf]',
  62. '[NgForTrackBy]',
  63. '[ngIfElse]',
  64. '[ngIfThenElse]',
  65. ]);
  66. function pipeMatchRegExpFor(name) {
  67. return new RegExp(`\\|\\s*${name}`);
  68. }
  69. const commonModulePipes = [
  70. 'date',
  71. 'async',
  72. 'currency',
  73. 'number',
  74. 'i18nPlural',
  75. 'i18nSelect',
  76. 'json',
  77. 'keyvalue',
  78. 'slice',
  79. 'lowercase',
  80. 'uppercase',
  81. 'titlecase',
  82. 'percent',
  83. ].map((name) => pipeMatchRegExpFor(name));
  84. /**
  85. * Represents an element with a migratable attribute
  86. */
  87. class ElementToMigrate {
  88. el;
  89. attr;
  90. elseAttr;
  91. thenAttr;
  92. forAttrs;
  93. aliasAttrs;
  94. nestCount = 0;
  95. hasLineBreaks = false;
  96. constructor(el, attr, elseAttr = undefined, thenAttr = undefined, forAttrs = undefined, aliasAttrs = undefined) {
  97. this.el = el;
  98. this.attr = attr;
  99. this.elseAttr = elseAttr;
  100. this.thenAttr = thenAttr;
  101. this.forAttrs = forAttrs;
  102. this.aliasAttrs = aliasAttrs;
  103. }
  104. normalizeConditionString(value) {
  105. value = this.insertSemicolon(value, value.indexOf(' else '));
  106. value = this.insertSemicolon(value, value.indexOf(' then '));
  107. value = this.insertSemicolon(value, value.indexOf(' let '));
  108. return value.replace(';;', ';');
  109. }
  110. insertSemicolon(str, ix) {
  111. return ix > -1 ? `${str.slice(0, ix)};${str.slice(ix)}` : str;
  112. }
  113. getCondition() {
  114. const chunks = this.normalizeConditionString(this.attr.value).split(';');
  115. let condition = chunks[0];
  116. // checks for case of no usage of `;` in if else / if then else
  117. const elseIx = condition.indexOf(' else ');
  118. const thenIx = condition.indexOf(' then ');
  119. if (thenIx > -1) {
  120. condition = condition.slice(0, thenIx);
  121. }
  122. else if (elseIx > -1) {
  123. condition = condition.slice(0, elseIx);
  124. }
  125. let letVar = chunks.find((c) => c.search(/\s*let\s/) > -1);
  126. return condition + (letVar ? ';' + letVar : '');
  127. }
  128. getTemplateName(targetStr, secondStr) {
  129. const targetLocation = this.attr.value.indexOf(targetStr);
  130. const secondTargetLocation = secondStr ? this.attr.value.indexOf(secondStr) : undefined;
  131. let templateName = this.attr.value.slice(targetLocation + targetStr.length, secondTargetLocation);
  132. if (templateName.startsWith(':')) {
  133. templateName = templateName.slice(1).trim();
  134. }
  135. return templateName.split(';')[0].trim();
  136. }
  137. getValueEnd(offset) {
  138. return ((this.attr.valueSpan ? this.attr.valueSpan.end.offset + 1 : this.attr.keySpan.end.offset) -
  139. offset);
  140. }
  141. hasChildren() {
  142. return this.el.children.length > 0;
  143. }
  144. getChildSpan(offset) {
  145. const childStart = this.el.children[0].sourceSpan.start.offset - offset;
  146. const childEnd = this.el.children[this.el.children.length - 1].sourceSpan.end.offset - offset;
  147. return { childStart, childEnd };
  148. }
  149. shouldRemoveElseAttr() {
  150. return ((this.el.name === 'ng-template' || this.el.name === 'ng-container') &&
  151. this.elseAttr !== undefined);
  152. }
  153. getElseAttrStr() {
  154. if (this.elseAttr !== undefined) {
  155. const elseValStr = this.elseAttr.value !== '' ? `="${this.elseAttr.value}"` : '';
  156. return `${this.elseAttr.name}${elseValStr}`;
  157. }
  158. return '';
  159. }
  160. start(offset) {
  161. return this.el.sourceSpan?.start.offset - offset;
  162. }
  163. end(offset) {
  164. return this.el.sourceSpan?.end.offset - offset;
  165. }
  166. length() {
  167. return this.el.sourceSpan?.end.offset - this.el.sourceSpan?.start.offset;
  168. }
  169. }
  170. /**
  171. * Represents an ng-template inside a template being migrated to new control flow
  172. */
  173. class Template {
  174. el;
  175. name;
  176. count = 0;
  177. contents = '';
  178. children = '';
  179. i18n = null;
  180. attributes;
  181. constructor(el, name, i18n) {
  182. this.el = el;
  183. this.name = name;
  184. this.attributes = el.attrs;
  185. this.i18n = i18n;
  186. }
  187. get isNgTemplateOutlet() {
  188. return this.attributes.find((attr) => attr.name === '*ngTemplateOutlet') !== undefined;
  189. }
  190. get outletContext() {
  191. const letVar = this.attributes.find((attr) => attr.name.startsWith('let-'));
  192. return letVar ? `; context: {$implicit: ${letVar.name.split('-')[1]}}` : '';
  193. }
  194. generateTemplateOutlet() {
  195. const attr = this.attributes.find((attr) => attr.name === '*ngTemplateOutlet');
  196. const outletValue = attr?.value ?? this.name.slice(1);
  197. return `<ng-container *ngTemplateOutlet="${outletValue}${this.outletContext}"></ng-container>`;
  198. }
  199. generateContents(tmpl) {
  200. this.contents = tmpl.slice(this.el.sourceSpan.start.offset, this.el.sourceSpan.end.offset);
  201. this.children = '';
  202. if (this.el.children.length > 0) {
  203. this.children = tmpl.slice(this.el.children[0].sourceSpan.start.offset, this.el.children[this.el.children.length - 1].sourceSpan.end.offset);
  204. }
  205. }
  206. }
  207. /** Represents a file that was analyzed by the migration. */
  208. class AnalyzedFile {
  209. ranges = [];
  210. removeCommonModule = false;
  211. canRemoveImports = false;
  212. sourceFile;
  213. importRanges = [];
  214. templateRanges = [];
  215. constructor(sourceFile) {
  216. this.sourceFile = sourceFile;
  217. }
  218. /** Returns the ranges in the order in which they should be migrated. */
  219. getSortedRanges() {
  220. // templates first for checking on whether certain imports can be safely removed
  221. this.templateRanges = this.ranges
  222. .slice()
  223. .filter((x) => x.type === 'template' || x.type === 'templateUrl')
  224. .sort((aStart, bStart) => bStart.start - aStart.start);
  225. this.importRanges = this.ranges
  226. .slice()
  227. .filter((x) => x.type === 'importDecorator' || x.type === 'importDeclaration')
  228. .sort((aStart, bStart) => bStart.start - aStart.start);
  229. return [...this.templateRanges, ...this.importRanges];
  230. }
  231. /**
  232. * Adds a text range to an `AnalyzedFile`.
  233. * @param path Path of the file.
  234. * @param analyzedFiles Map keeping track of all the analyzed files.
  235. * @param range Range to be added.
  236. */
  237. static addRange(path, sourceFile, analyzedFiles, range) {
  238. let analysis = analyzedFiles.get(path);
  239. if (!analysis) {
  240. analysis = new AnalyzedFile(sourceFile);
  241. analyzedFiles.set(path, analysis);
  242. }
  243. const duplicate = analysis.ranges.find((current) => current.start === range.start && current.end === range.end);
  244. if (!duplicate) {
  245. analysis.ranges.push(range);
  246. }
  247. }
  248. /**
  249. * This verifies whether a component class is safe to remove module imports.
  250. * It is only run on .ts files.
  251. */
  252. verifyCanRemoveImports() {
  253. const importDeclaration = this.importRanges.find((r) => r.type === 'importDeclaration');
  254. const instances = lookupIdentifiersInSourceFile(this.sourceFile, importWithCommonRemovals);
  255. let foundImportDeclaration = false;
  256. let count = 0;
  257. for (let range of this.importRanges) {
  258. for (let instance of instances) {
  259. if (instance.getStart() >= range.start && instance.getEnd() <= range.end) {
  260. if (range === importDeclaration) {
  261. foundImportDeclaration = true;
  262. }
  263. count++;
  264. }
  265. }
  266. }
  267. if (instances.size !== count && importDeclaration !== undefined && foundImportDeclaration) {
  268. importDeclaration.remove = false;
  269. }
  270. }
  271. }
  272. /** Finds all non-control flow elements from common module. */
  273. class CommonCollector extends checker.RecursiveVisitor {
  274. count = 0;
  275. visitElement(el) {
  276. if (el.attrs.length > 0) {
  277. for (const attr of el.attrs) {
  278. if (this.hasDirectives(attr.name) || this.hasPipes(attr.value)) {
  279. this.count++;
  280. }
  281. }
  282. }
  283. super.visitElement(el, null);
  284. }
  285. visitBlock(ast) {
  286. for (const blockParam of ast.parameters) {
  287. if (this.hasPipes(blockParam.expression)) {
  288. this.count++;
  289. }
  290. }
  291. super.visitBlock(ast, null);
  292. }
  293. visitText(ast) {
  294. if (this.hasPipes(ast.value)) {
  295. this.count++;
  296. }
  297. }
  298. visitLetDeclaration(decl) {
  299. if (this.hasPipes(decl.value)) {
  300. this.count++;
  301. }
  302. super.visitLetDeclaration(decl, null);
  303. }
  304. hasDirectives(input) {
  305. return commonModuleDirectives.has(input);
  306. }
  307. hasPipes(input) {
  308. return commonModulePipes.some((regexp) => regexp.test(input));
  309. }
  310. }
  311. /** Finds all elements that represent i18n blocks. */
  312. class i18nCollector extends checker.RecursiveVisitor {
  313. elements = [];
  314. visitElement(el) {
  315. if (el.attrs.find((a) => a.name === 'i18n') !== undefined) {
  316. this.elements.push(el);
  317. }
  318. super.visitElement(el, null);
  319. }
  320. }
  321. /** Finds all elements with ngif structural directives. */
  322. class ElementCollector extends checker.RecursiveVisitor {
  323. _attributes;
  324. elements = [];
  325. constructor(_attributes = []) {
  326. super();
  327. this._attributes = _attributes;
  328. }
  329. visitElement(el) {
  330. if (el.attrs.length > 0) {
  331. for (const attr of el.attrs) {
  332. if (this._attributes.includes(attr.name)) {
  333. const elseAttr = el.attrs.find((x) => x.name === boundngifelse);
  334. const thenAttr = el.attrs.find((x) => x.name === boundngifthenelse || x.name === boundngifthen);
  335. const forAttrs = attr.name === nakedngfor$1 ? this.getForAttrs(el) : undefined;
  336. const aliasAttrs = this.getAliasAttrs(el);
  337. this.elements.push(new ElementToMigrate(el, attr, elseAttr, thenAttr, forAttrs, aliasAttrs));
  338. }
  339. }
  340. }
  341. super.visitElement(el, null);
  342. }
  343. getForAttrs(el) {
  344. let trackBy = '';
  345. let forOf = '';
  346. for (const attr of el.attrs) {
  347. if (attr.name === '[ngForTrackBy]') {
  348. trackBy = attr.value;
  349. }
  350. if (attr.name === '[ngForOf]') {
  351. forOf = attr.value;
  352. }
  353. }
  354. return { forOf, trackBy };
  355. }
  356. getAliasAttrs(el) {
  357. const aliases = new Map();
  358. let item = '';
  359. for (const attr of el.attrs) {
  360. if (attr.name.startsWith('let-')) {
  361. if (attr.value === '') {
  362. // item
  363. item = attr.name.replace('let-', '');
  364. }
  365. else {
  366. // alias
  367. aliases.set(attr.name.replace('let-', ''), attr.value);
  368. }
  369. }
  370. }
  371. return { item, aliases };
  372. }
  373. }
  374. /** Finds all elements with ngif structural directives. */
  375. class TemplateCollector extends checker.RecursiveVisitor {
  376. elements = [];
  377. templates = new Map();
  378. visitElement(el) {
  379. if (el.name === ngtemplate) {
  380. let i18n = null;
  381. let templateAttr = null;
  382. for (const attr of el.attrs) {
  383. if (attr.name === 'i18n') {
  384. i18n = attr;
  385. }
  386. if (attr.name.startsWith('#')) {
  387. templateAttr = attr;
  388. }
  389. }
  390. if (templateAttr !== null && !this.templates.has(templateAttr.name)) {
  391. this.templates.set(templateAttr.name, new Template(el, templateAttr.name, i18n));
  392. this.elements.push(new ElementToMigrate(el, templateAttr));
  393. }
  394. else if (templateAttr !== null) {
  395. throw new Error(`A duplicate ng-template name "${templateAttr.name}" was found. ` +
  396. `The control flow migration requires unique ng-template names within a component.`);
  397. }
  398. }
  399. super.visitElement(el, null);
  400. }
  401. }
  402. const startMarkerRegex = new RegExp(startMarker, 'gm');
  403. const endMarkerRegex = new RegExp(endMarker, 'gm');
  404. const startI18nMarkerRegex = new RegExp(startI18nMarker, 'gm');
  405. const endI18nMarkerRegex = new RegExp(endI18nMarker, 'gm');
  406. const replaceMarkerRegex = new RegExp(`${startMarker}|${endMarker}`, 'gm');
  407. /**
  408. * Analyzes a source file to find file that need to be migrated and the text ranges within them.
  409. * @param sourceFile File to be analyzed.
  410. * @param analyzedFiles Map in which to store the results.
  411. */
  412. function analyze(sourceFile, analyzedFiles) {
  413. forEachClass(sourceFile, (node) => {
  414. if (ts.isClassDeclaration(node)) {
  415. analyzeDecorators(node, sourceFile, analyzedFiles);
  416. }
  417. else {
  418. analyzeImportDeclarations(node, sourceFile, analyzedFiles);
  419. }
  420. });
  421. }
  422. function checkIfShouldChange(decl, file) {
  423. const range = file.importRanges.find((r) => r.type === 'importDeclaration');
  424. if (range === undefined || !range.remove) {
  425. return false;
  426. }
  427. // should change if you can remove the common module
  428. // if it's not safe to remove the common module
  429. // and that's the only thing there, we should do nothing.
  430. const clause = decl.getChildAt(1);
  431. return !(!file.removeCommonModule &&
  432. clause.namedBindings &&
  433. ts.isNamedImports(clause.namedBindings) &&
  434. clause.namedBindings.elements.length === 1 &&
  435. clause.namedBindings.elements[0].getText() === 'CommonModule');
  436. }
  437. function updateImportDeclaration(decl, removeCommonModule) {
  438. const clause = decl.getChildAt(1);
  439. const updatedClause = updateImportClause(clause, removeCommonModule);
  440. if (updatedClause === null) {
  441. return '';
  442. }
  443. // removeComments is set to true to prevent duplication of comments
  444. // when the import declaration is at the top of the file, but right after a comment
  445. // without this, the comment gets duplicated when the declaration is updated.
  446. // the typescript AST includes that preceding comment as part of the import declaration full text.
  447. const printer = ts.createPrinter({
  448. removeComments: true,
  449. });
  450. const updated = ts.factory.updateImportDeclaration(decl, decl.modifiers, updatedClause, decl.moduleSpecifier, undefined);
  451. return printer.printNode(ts.EmitHint.Unspecified, updated, clause.getSourceFile());
  452. }
  453. function updateImportClause(clause, removeCommonModule) {
  454. if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
  455. const removals = removeCommonModule ? importWithCommonRemovals : importRemovals;
  456. const elements = clause.namedBindings.elements.filter((el) => !removals.includes(el.getText()));
  457. if (elements.length === 0) {
  458. return null;
  459. }
  460. clause = ts.factory.updateImportClause(clause, clause.isTypeOnly, clause.name, ts.factory.createNamedImports(elements));
  461. }
  462. return clause;
  463. }
  464. function updateClassImports(propAssignment, removeCommonModule) {
  465. const printer = ts.createPrinter();
  466. const importList = propAssignment.initializer;
  467. // Can't change non-array literals.
  468. if (!ts.isArrayLiteralExpression(importList)) {
  469. return null;
  470. }
  471. const removals = removeCommonModule ? importWithCommonRemovals : importRemovals;
  472. const elements = importList.elements.filter((el) => !ts.isIdentifier(el) || !removals.includes(el.text));
  473. if (elements.length === importList.elements.length) {
  474. // nothing changed
  475. return null;
  476. }
  477. const updatedElements = ts.factory.updateArrayLiteralExpression(importList, elements);
  478. const updatedAssignment = ts.factory.updatePropertyAssignment(propAssignment, propAssignment.name, updatedElements);
  479. return printer.printNode(ts.EmitHint.Unspecified, updatedAssignment, updatedAssignment.getSourceFile());
  480. }
  481. function analyzeImportDeclarations(node, sourceFile, analyzedFiles) {
  482. if (node.getText().indexOf('@angular/common') === -1) {
  483. return;
  484. }
  485. const clause = node.getChildAt(1);
  486. if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
  487. const elements = clause.namedBindings.elements.filter((el) => importWithCommonRemovals.includes(el.getText()));
  488. if (elements.length > 0) {
  489. AnalyzedFile.addRange(sourceFile.fileName, sourceFile, analyzedFiles, {
  490. start: node.getStart(),
  491. end: node.getEnd(),
  492. node,
  493. type: 'importDeclaration',
  494. remove: true,
  495. });
  496. }
  497. }
  498. }
  499. function analyzeDecorators(node, sourceFile, analyzedFiles) {
  500. // Note: we have a utility to resolve the Angular decorators from a class declaration already.
  501. // We don't use it here, because it requires access to the type checker which makes it more
  502. // time-consuming to run internally.
  503. const decorator = ts.getDecorators(node)?.find((dec) => {
  504. return (ts.isCallExpression(dec.expression) &&
  505. ts.isIdentifier(dec.expression.expression) &&
  506. dec.expression.expression.text === 'Component');
  507. });
  508. const metadata = decorator &&
  509. decorator.expression.arguments.length > 0 &&
  510. ts.isObjectLiteralExpression(decorator.expression.arguments[0])
  511. ? decorator.expression.arguments[0]
  512. : null;
  513. if (!metadata) {
  514. return;
  515. }
  516. for (const prop of metadata.properties) {
  517. // All the properties we care about should have static
  518. // names and be initialized to a static string.
  519. if (!ts.isPropertyAssignment(prop) ||
  520. (!ts.isIdentifier(prop.name) && !ts.isStringLiteralLike(prop.name))) {
  521. continue;
  522. }
  523. switch (prop.name.text) {
  524. case 'template':
  525. // +1/-1 to exclude the opening/closing characters from the range.
  526. AnalyzedFile.addRange(sourceFile.fileName, sourceFile, analyzedFiles, {
  527. start: prop.initializer.getStart() + 1,
  528. end: prop.initializer.getEnd() - 1,
  529. node: prop,
  530. type: 'template',
  531. remove: true,
  532. });
  533. break;
  534. case 'imports':
  535. AnalyzedFile.addRange(sourceFile.fileName, sourceFile, analyzedFiles, {
  536. start: prop.name.getStart(),
  537. end: prop.initializer.getEnd(),
  538. node: prop,
  539. type: 'importDecorator',
  540. remove: true,
  541. });
  542. break;
  543. case 'templateUrl':
  544. // Leave the end as undefined which means that the range is until the end of the file.
  545. if (ts.isStringLiteralLike(prop.initializer)) {
  546. const path = p.join(p.dirname(sourceFile.fileName), prop.initializer.text);
  547. AnalyzedFile.addRange(path, sourceFile, analyzedFiles, {
  548. start: 0,
  549. node: prop,
  550. type: 'templateUrl',
  551. remove: true,
  552. });
  553. }
  554. break;
  555. }
  556. }
  557. }
  558. /**
  559. * returns the level deep a migratable element is nested
  560. */
  561. function getNestedCount(etm, aggregator) {
  562. if (aggregator.length === 0) {
  563. return 0;
  564. }
  565. if (etm.el.sourceSpan.start.offset < aggregator[aggregator.length - 1] &&
  566. etm.el.sourceSpan.end.offset !== aggregator[aggregator.length - 1]) {
  567. // element is nested
  568. aggregator.push(etm.el.sourceSpan.end.offset);
  569. return aggregator.length - 1;
  570. }
  571. else {
  572. // not nested
  573. aggregator.pop();
  574. return getNestedCount(etm, aggregator);
  575. }
  576. }
  577. /**
  578. * parses the template string into the Html AST
  579. */
  580. function parseTemplate(template) {
  581. let parsed;
  582. try {
  583. // Note: we use the HtmlParser here, instead of the `parseTemplate` function, because the
  584. // latter returns an Ivy AST, not an HTML AST. The HTML AST has the advantage of preserving
  585. // interpolated text as text nodes containing a mixture of interpolation tokens and text tokens,
  586. // rather than turning them into `BoundText` nodes like the Ivy AST does. This allows us to
  587. // easily get the text-only ranges without having to reconstruct the original text.
  588. parsed = new checker.HtmlParser().parse(template, '', {
  589. // Allows for ICUs to be parsed.
  590. tokenizeExpansionForms: true,
  591. // Explicitly disable blocks so that their characters are treated as plain text.
  592. tokenizeBlocks: true,
  593. preserveLineEndings: true,
  594. });
  595. // Don't migrate invalid templates.
  596. if (parsed.errors && parsed.errors.length > 0) {
  597. const errors = parsed.errors.map((e) => ({ type: 'parse', error: e }));
  598. return { tree: undefined, errors };
  599. }
  600. }
  601. catch (e) {
  602. return { tree: undefined, errors: [{ type: 'parse', error: e }] };
  603. }
  604. return { tree: parsed, errors: [] };
  605. }
  606. function validateMigratedTemplate(migrated, fileName) {
  607. const parsed = parseTemplate(migrated);
  608. let errors = [];
  609. if (parsed.errors.length > 0) {
  610. errors.push({
  611. type: 'parse',
  612. error: new Error(`The migration resulted in invalid HTML for ${fileName}. ` +
  613. `Please check the template for valid HTML structures and run the migration again.`),
  614. });
  615. }
  616. if (parsed.tree) {
  617. const i18nError = validateI18nStructure(parsed.tree, fileName);
  618. if (i18nError !== null) {
  619. errors.push({ type: 'i18n', error: i18nError });
  620. }
  621. }
  622. return errors;
  623. }
  624. function validateI18nStructure(parsed, fileName) {
  625. const visitor = new i18nCollector();
  626. checker.visitAll(visitor, parsed.rootNodes);
  627. const parents = visitor.elements.filter((el) => el.children.length > 0);
  628. for (const p of parents) {
  629. for (const el of visitor.elements) {
  630. if (el === p)
  631. continue;
  632. if (isChildOf(p, el)) {
  633. return new Error(`i18n Nesting error: The migration would result in invalid i18n nesting for ` +
  634. `${fileName}. Element with i18n attribute "${p.name}" would result having a child of ` +
  635. `element with i18n attribute "${el.name}". Please fix and re-run the migration.`);
  636. }
  637. }
  638. }
  639. return null;
  640. }
  641. function isChildOf(parent, el) {
  642. return (parent.sourceSpan.start.offset < el.sourceSpan.start.offset &&
  643. parent.sourceSpan.end.offset > el.sourceSpan.end.offset);
  644. }
  645. /** Possible placeholders that can be generated by `getPlaceholder`. */
  646. var PlaceholderKind;
  647. (function (PlaceholderKind) {
  648. PlaceholderKind[PlaceholderKind["Default"] = 0] = "Default";
  649. PlaceholderKind[PlaceholderKind["Alternate"] = 1] = "Alternate";
  650. })(PlaceholderKind || (PlaceholderKind = {}));
  651. /**
  652. * Wraps a string in a placeholder that makes it easier to identify during replacement operations.
  653. */
  654. function getPlaceholder(value, kind = PlaceholderKind.Default) {
  655. const name = `<<<ɵɵngControlFlowMigration_${kind}ɵɵ>>>`;
  656. return `___${name}${value}${name}___`;
  657. }
  658. /**
  659. * calculates the level of nesting of the items in the collector
  660. */
  661. function calculateNesting(visitor, hasLineBreaks) {
  662. // start from top of template
  663. // loop through each element
  664. let nestedQueue = [];
  665. for (let i = 0; i < visitor.elements.length; i++) {
  666. let currEl = visitor.elements[i];
  667. if (i === 0) {
  668. nestedQueue.push(currEl.el.sourceSpan.end.offset);
  669. currEl.hasLineBreaks = hasLineBreaks;
  670. continue;
  671. }
  672. currEl.hasLineBreaks = hasLineBreaks;
  673. currEl.nestCount = getNestedCount(currEl, nestedQueue);
  674. if (currEl.el.sourceSpan.end.offset !== nestedQueue[nestedQueue.length - 1]) {
  675. nestedQueue.push(currEl.el.sourceSpan.end.offset);
  676. }
  677. }
  678. }
  679. /**
  680. * determines if a given template string contains line breaks
  681. */
  682. function hasLineBreaks(template) {
  683. return /\r|\n/.test(template);
  684. }
  685. /**
  686. * properly adjusts template offsets based on current nesting levels
  687. */
  688. function reduceNestingOffset(el, nestLevel, offset, postOffsets) {
  689. if (el.nestCount <= nestLevel) {
  690. const count = nestLevel - el.nestCount;
  691. // reduced nesting, add postoffset
  692. for (let i = 0; i <= count; i++) {
  693. offset += postOffsets.pop() ?? 0;
  694. }
  695. }
  696. return offset;
  697. }
  698. /**
  699. * Replaces structural directive control flow instances with block control flow equivalents.
  700. * Returns null if the migration failed (e.g. there was a syntax error).
  701. */
  702. function getTemplates(template) {
  703. const parsed = parseTemplate(template);
  704. if (parsed.tree !== undefined) {
  705. const visitor = new TemplateCollector();
  706. checker.visitAll(visitor, parsed.tree.rootNodes);
  707. for (let [key, tmpl] of visitor.templates) {
  708. tmpl.count = countTemplateUsage(parsed.tree.rootNodes, key);
  709. tmpl.generateContents(template);
  710. }
  711. return visitor.templates;
  712. }
  713. return new Map();
  714. }
  715. function countTemplateUsage(nodes, templateName) {
  716. let count = 0;
  717. let isReferencedInTemplateOutlet = false;
  718. for (const node of nodes) {
  719. if (node.attrs) {
  720. for (const attr of node.attrs) {
  721. if (attr.name === '*ngTemplateOutlet' && attr.value === templateName.slice(1)) {
  722. isReferencedInTemplateOutlet = true;
  723. break;
  724. }
  725. if (attr.name.trim() === templateName) {
  726. count++;
  727. }
  728. }
  729. }
  730. if (node.children) {
  731. if (node.name === 'for') {
  732. for (const child of node.children) {
  733. if (child.value?.includes(templateName.slice(1))) {
  734. count++;
  735. }
  736. }
  737. }
  738. count += countTemplateUsage(node.children, templateName);
  739. }
  740. }
  741. return isReferencedInTemplateOutlet ? count + 2 : count;
  742. }
  743. function updateTemplates(template, templates) {
  744. const updatedTemplates = getTemplates(template);
  745. for (let [key, tmpl] of updatedTemplates) {
  746. templates.set(key, tmpl);
  747. }
  748. return templates;
  749. }
  750. function wrapIntoI18nContainer(i18nAttr, content) {
  751. const { start, middle, end } = generatei18nContainer(i18nAttr, content);
  752. return `${start}${middle}${end}`;
  753. }
  754. function generatei18nContainer(i18nAttr, middle) {
  755. const i18n = i18nAttr.value === '' ? 'i18n' : `i18n="${i18nAttr.value}"`;
  756. return { start: `<ng-container ${i18n}>`, middle, end: `</ng-container>` };
  757. }
  758. /**
  759. * Counts, replaces, and removes any necessary ng-templates post control flow migration
  760. */
  761. function processNgTemplates(template, sourceFile) {
  762. // count usage
  763. try {
  764. const templates = getTemplates(template);
  765. // swap placeholders and remove
  766. for (const [name, t] of templates) {
  767. const replaceRegex = new RegExp(getPlaceholder(name.slice(1)), 'g');
  768. const forRegex = new RegExp(getPlaceholder(name.slice(1), PlaceholderKind.Alternate), 'g');
  769. const forMatches = [...template.matchAll(forRegex)];
  770. const matches = [...forMatches, ...template.matchAll(replaceRegex)];
  771. let safeToRemove = true;
  772. if (matches.length > 0) {
  773. if (t.i18n !== null) {
  774. const container = wrapIntoI18nContainer(t.i18n, t.children);
  775. template = template.replace(replaceRegex, container);
  776. }
  777. else if (t.children.trim() === '' && t.isNgTemplateOutlet) {
  778. template = template.replace(replaceRegex, t.generateTemplateOutlet());
  779. }
  780. else if (forMatches.length > 0) {
  781. if (t.count === 2) {
  782. template = template.replace(forRegex, t.children);
  783. }
  784. else {
  785. template = template.replace(forRegex, t.generateTemplateOutlet());
  786. safeToRemove = false;
  787. }
  788. }
  789. else {
  790. template = template.replace(replaceRegex, t.children);
  791. }
  792. const dist = matches.filter((obj, index, self) => index === self.findIndex((t) => t.input === obj.input));
  793. if ((t.count === dist.length || t.count - matches.length === 1) && safeToRemove) {
  794. const refsInComponentFile = getViewChildOrViewChildrenNames(sourceFile);
  795. if (refsInComponentFile?.length > 0) {
  796. const templateRefs = getTemplateReferences(template);
  797. for (const ref of refsInComponentFile) {
  798. if (!templateRefs.includes(ref)) {
  799. template = template.replace(t.contents, `${startMarker}${endMarker}`);
  800. }
  801. }
  802. }
  803. else {
  804. template = template.replace(t.contents, `${startMarker}${endMarker}`);
  805. }
  806. }
  807. // templates may have changed structure from nested replaced templates
  808. // so we need to reprocess them before the next loop.
  809. updateTemplates(template, templates);
  810. }
  811. }
  812. // template placeholders may still exist if the ng-template name is not
  813. // present in the component. This could be because it's passed in from
  814. // another component. In that case, we need to replace any remaining
  815. // template placeholders with template outlets.
  816. template = replaceRemainingPlaceholders(template);
  817. return { migrated: template, err: undefined };
  818. }
  819. catch (err) {
  820. return { migrated: template, err: err };
  821. }
  822. }
  823. function getViewChildOrViewChildrenNames(sourceFile) {
  824. const names = [];
  825. function visit(node) {
  826. if (ts.isDecorator(node) && ts.isCallExpression(node.expression)) {
  827. const expr = node.expression;
  828. if (ts.isIdentifier(expr.expression) &&
  829. (expr.expression.text === 'ViewChild' || expr.expression.text === 'ViewChildren')) {
  830. const firstArg = expr.arguments[0];
  831. if (firstArg && ts.isStringLiteral(firstArg)) {
  832. names.push(firstArg.text);
  833. }
  834. return;
  835. }
  836. }
  837. ts.forEachChild(node, visit);
  838. }
  839. visit(sourceFile);
  840. return names;
  841. }
  842. function getTemplateReferences(template) {
  843. const parsed = parseTemplate(template);
  844. if (parsed.tree === undefined) {
  845. return [];
  846. }
  847. const references = [];
  848. function visitNodes(nodes) {
  849. for (const node of nodes) {
  850. if (node?.name === 'ng-template') {
  851. references.push(...node.attrs?.map((ref) => ref?.name?.slice(1)));
  852. }
  853. if (node.children) {
  854. visitNodes(node.children);
  855. }
  856. }
  857. }
  858. visitNodes(parsed.tree.rootNodes);
  859. return references;
  860. }
  861. function replaceRemainingPlaceholders(template) {
  862. const pattern = '.*';
  863. const placeholderPattern = getPlaceholder(pattern);
  864. const replaceRegex = new RegExp(placeholderPattern, 'g');
  865. const [placeholderStart, placeholderEnd] = placeholderPattern.split(pattern);
  866. const placeholders = [...template.matchAll(replaceRegex)];
  867. for (let ph of placeholders) {
  868. const placeholder = ph[0];
  869. const name = placeholder.slice(placeholderStart.length, placeholder.length - placeholderEnd.length);
  870. template = template.replace(placeholder, `<ng-template [ngTemplateOutlet]="${name}"></ng-template>`);
  871. }
  872. return template;
  873. }
  874. /**
  875. * determines if the CommonModule can be safely removed from imports
  876. */
  877. function canRemoveCommonModule(template) {
  878. const parsed = parseTemplate(template);
  879. let removeCommonModule = false;
  880. if (parsed.tree !== undefined) {
  881. const visitor = new CommonCollector();
  882. checker.visitAll(visitor, parsed.tree.rootNodes);
  883. removeCommonModule = visitor.count === 0;
  884. }
  885. return removeCommonModule;
  886. }
  887. /**
  888. * removes imports from template imports and import declarations
  889. */
  890. function removeImports(template, node, file) {
  891. if (template.startsWith('imports') && ts.isPropertyAssignment(node)) {
  892. const updatedImport = updateClassImports(node, file.removeCommonModule);
  893. return updatedImport ?? template;
  894. }
  895. else if (ts.isImportDeclaration(node) && checkIfShouldChange(node, file)) {
  896. return updateImportDeclaration(node, file.removeCommonModule);
  897. }
  898. return template;
  899. }
  900. /**
  901. * retrieves the original block of text in the template for length comparison during migration
  902. * processing
  903. */
  904. function getOriginals(etm, tmpl, offset) {
  905. // original opening block
  906. if (etm.el.children.length > 0) {
  907. const childStart = etm.el.children[0].sourceSpan.start.offset - offset;
  908. const childEnd = etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - offset;
  909. const start = tmpl.slice(etm.el.sourceSpan.start.offset - offset, etm.el.children[0].sourceSpan.start.offset - offset);
  910. // original closing block
  911. const end = tmpl.slice(etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - offset, etm.el.sourceSpan.end.offset - offset);
  912. const childLength = childEnd - childStart;
  913. return {
  914. start,
  915. end,
  916. childLength,
  917. children: getOriginalChildren(etm.el.children, tmpl, offset),
  918. childNodes: etm.el.children,
  919. };
  920. }
  921. // self closing or no children
  922. const start = tmpl.slice(etm.el.sourceSpan.start.offset - offset, etm.el.sourceSpan.end.offset - offset);
  923. // original closing block
  924. return { start, end: '', childLength: 0, children: [], childNodes: [] };
  925. }
  926. function getOriginalChildren(children, tmpl, offset) {
  927. return children.map((child) => {
  928. return tmpl.slice(child.sourceSpan.start.offset - offset, child.sourceSpan.end.offset - offset);
  929. });
  930. }
  931. function isI18nTemplate(etm, i18nAttr) {
  932. let attrCount = countAttributes(etm);
  933. const safeToRemove = etm.el.attrs.length === attrCount + (i18nAttr !== undefined ? 1 : 0);
  934. return etm.el.name === 'ng-template' && i18nAttr !== undefined && safeToRemove;
  935. }
  936. function isRemovableContainer(etm) {
  937. let attrCount = countAttributes(etm);
  938. const safeToRemove = etm.el.attrs.length === attrCount;
  939. return (etm.el.name === 'ng-container' || etm.el.name === 'ng-template') && safeToRemove;
  940. }
  941. function countAttributes(etm) {
  942. let attrCount = 1;
  943. if (etm.elseAttr !== undefined) {
  944. attrCount++;
  945. }
  946. if (etm.thenAttr !== undefined) {
  947. attrCount++;
  948. }
  949. attrCount += etm.aliasAttrs?.aliases.size ?? 0;
  950. attrCount += etm.aliasAttrs?.item ? 1 : 0;
  951. attrCount += etm.forAttrs?.trackBy ? 1 : 0;
  952. attrCount += etm.forAttrs?.forOf ? 1 : 0;
  953. return attrCount;
  954. }
  955. /**
  956. * builds the proper contents of what goes inside a given control flow block after migration
  957. */
  958. function getMainBlock(etm, tmpl, offset) {
  959. const i18nAttr = etm.el.attrs.find((x) => x.name === 'i18n');
  960. // removable containers are ng-templates or ng-containers that no longer need to exist
  961. // post migration
  962. if (isRemovableContainer(etm)) {
  963. let middle = '';
  964. if (etm.hasChildren()) {
  965. const { childStart, childEnd } = etm.getChildSpan(offset);
  966. middle = tmpl.slice(childStart, childEnd);
  967. }
  968. else {
  969. middle = '';
  970. }
  971. return { start: '', middle, end: '' };
  972. }
  973. else if (isI18nTemplate(etm, i18nAttr)) {
  974. // here we're removing an ng-template used for control flow and i18n and
  975. // converting it to an ng-container with i18n
  976. const { childStart, childEnd } = etm.getChildSpan(offset);
  977. return generatei18nContainer(i18nAttr, tmpl.slice(childStart, childEnd));
  978. }
  979. // the index of the start of the attribute adjusting for offset shift
  980. const attrStart = etm.attr.keySpan.start.offset - 1 - offset;
  981. // the index of the very end of the attribute value adjusted for offset shift
  982. const valEnd = etm.getValueEnd(offset);
  983. // the index of the children start and end span, if they exist. Otherwise use the value end.
  984. const { childStart, childEnd } = etm.hasChildren()
  985. ? etm.getChildSpan(offset)
  986. : { childStart: valEnd, childEnd: valEnd };
  987. // the beginning of the updated string in the main block, for example: <div some="attributes">
  988. let start = tmpl.slice(etm.start(offset), attrStart) + tmpl.slice(valEnd, childStart);
  989. // the middle is the actual contents of the element
  990. const middle = tmpl.slice(childStart, childEnd);
  991. // the end is the closing part of the element, example: </div>
  992. let end = tmpl.slice(childEnd, etm.end(offset));
  993. if (etm.shouldRemoveElseAttr()) {
  994. // this removes a bound ngIfElse attribute that's no longer needed
  995. // this could be on the start or end
  996. start = start.replace(etm.getElseAttrStr(), '');
  997. end = end.replace(etm.getElseAttrStr(), '');
  998. }
  999. return { start, middle, end };
  1000. }
  1001. function generateI18nMarkers(tmpl) {
  1002. let parsed = parseTemplate(tmpl);
  1003. if (parsed.tree !== undefined) {
  1004. const visitor = new i18nCollector();
  1005. checker.visitAll(visitor, parsed.tree.rootNodes);
  1006. for (const [ix, el] of visitor.elements.entries()) {
  1007. // we only care about elements with children and i18n tags
  1008. // elements without children have nothing to translate
  1009. // offset accounts for the addition of the 2 marker characters with each loop.
  1010. const offset = ix * 2;
  1011. if (el.children.length > 0) {
  1012. tmpl = addI18nMarkers(tmpl, el, offset);
  1013. }
  1014. }
  1015. }
  1016. return tmpl;
  1017. }
  1018. function addI18nMarkers(tmpl, el, offset) {
  1019. const startPos = el.children[0].sourceSpan.start.offset + offset;
  1020. const endPos = el.children[el.children.length - 1].sourceSpan.end.offset + offset;
  1021. return (tmpl.slice(0, startPos) +
  1022. startI18nMarker +
  1023. tmpl.slice(startPos, endPos) +
  1024. endI18nMarker +
  1025. tmpl.slice(endPos));
  1026. }
  1027. const selfClosingList = 'input|br|img|base|wbr|area|col|embed|hr|link|meta|param|source|track';
  1028. /**
  1029. * re-indents all the lines in the template properly post migration
  1030. */
  1031. function formatTemplate(tmpl, templateType) {
  1032. if (tmpl.indexOf('\n') > -1) {
  1033. tmpl = generateI18nMarkers(tmpl);
  1034. // tracks if a self closing element opened without closing yet
  1035. let openSelfClosingEl = false;
  1036. // match any type of control flow block as start of string ignoring whitespace
  1037. // @if | @switch | @case | @default | @for | } @else
  1038. const openBlockRegex = /^\s*\@(if|switch|case|default|for)|^\s*\}\s\@else/;
  1039. // regex for matching an html element opening
  1040. // <div thing="stuff" [binding]="true"> || <div thing="stuff" [binding]="true"
  1041. const openElRegex = /^\s*<([a-z0-9]+)(?![^>]*\/>)[^>]*>?/;
  1042. // regex for matching an attribute string that was left open at the endof a line
  1043. // so we can ensure we have the proper indent
  1044. // <div thing="aefaefwe
  1045. const openAttrDoubleRegex = /="([^"]|\\")*$/;
  1046. const openAttrSingleRegex = /='([^']|\\')*$/;
  1047. // regex for matching an attribute string that was closes on a separate line
  1048. // from when it was opened.
  1049. // <div thing="aefaefwe
  1050. // i18n message is here">
  1051. const closeAttrDoubleRegex = /^\s*([^><]|\\")*"/;
  1052. const closeAttrSingleRegex = /^\s*([^><]|\\')*'/;
  1053. // regex for matching a self closing html element that has no />
  1054. // <input type="button" [binding]="true">
  1055. const selfClosingRegex = new RegExp(`^\\s*<(${selfClosingList}).+\\/?>`);
  1056. // regex for matching a self closing html element that is on multi lines
  1057. // <input type="button" [binding]="true"> || <input type="button" [binding]="true"
  1058. const openSelfClosingRegex = new RegExp(`^\\s*<(${selfClosingList})(?![^>]*\\/>)[^>]*$`);
  1059. // match closing block or else block
  1060. // } | } @else
  1061. const closeBlockRegex = /^\s*\}\s*$|^\s*\}\s\@else/;
  1062. // matches closing of an html element
  1063. // </element>
  1064. const closeElRegex = /\s*<\/([a-zA-Z0-9\-_]+)\s*>/m;
  1065. // matches closing of a self closing html element when the element is on multiple lines
  1066. // [binding]="value" />
  1067. const closeMultiLineElRegex = /^\s*([a-zA-Z0-9\-_\[\]]+)?=?"?([^”<]+)?"?\s?\/>$/;
  1068. // matches closing of a self closing html element when the element is on multiple lines
  1069. // with no / in the closing: [binding]="value">
  1070. const closeSelfClosingMultiLineRegex = /^\s*([a-zA-Z0-9\-_\[\]]+)?=?"?([^”\/<]+)?"?\s?>$/;
  1071. // matches an open and close of an html element on a single line with no breaks
  1072. // <div>blah</div>
  1073. const singleLineElRegex = /\s*<([a-zA-Z0-9]+)(?![^>]*\/>)[^>]*>.*<\/([a-zA-Z0-9\-_]+)\s*>/;
  1074. const lines = tmpl.split('\n');
  1075. const formatted = [];
  1076. // the indent applied during formatting
  1077. let indent = '';
  1078. // the pre-existing indent in an inline template that we'd like to preserve
  1079. let mindent = '';
  1080. let depth = 0;
  1081. let i18nDepth = 0;
  1082. let inMigratedBlock = false;
  1083. let inI18nBlock = false;
  1084. let inAttribute = false;
  1085. let isDoubleQuotes = false;
  1086. for (let [index, line] of lines.entries()) {
  1087. depth +=
  1088. [...line.matchAll(startMarkerRegex)].length - [...line.matchAll(endMarkerRegex)].length;
  1089. inMigratedBlock = depth > 0;
  1090. i18nDepth +=
  1091. [...line.matchAll(startI18nMarkerRegex)].length -
  1092. [...line.matchAll(endI18nMarkerRegex)].length;
  1093. let lineWasMigrated = false;
  1094. if (line.match(replaceMarkerRegex)) {
  1095. line = line.replace(replaceMarkerRegex, '');
  1096. lineWasMigrated = true;
  1097. }
  1098. if (line.trim() === '' &&
  1099. index !== 0 &&
  1100. index !== lines.length - 1 &&
  1101. (inMigratedBlock || lineWasMigrated) &&
  1102. !inI18nBlock &&
  1103. !inAttribute) {
  1104. // skip blank lines except if it's the first line or last line
  1105. // this preserves leading and trailing spaces if they are already present
  1106. continue;
  1107. }
  1108. // preserves the indentation of an inline template
  1109. if (templateType === 'template' && index <= 1) {
  1110. // first real line of an inline template
  1111. const ind = line.search(/\S/);
  1112. mindent = ind > -1 ? line.slice(0, ind) : '';
  1113. }
  1114. // if a block closes, an element closes, and it's not an element on a single line or the end
  1115. // of a self closing tag
  1116. if ((closeBlockRegex.test(line) ||
  1117. (closeElRegex.test(line) &&
  1118. !singleLineElRegex.test(line) &&
  1119. !closeMultiLineElRegex.test(line))) &&
  1120. indent !== '') {
  1121. // close block, reduce indent
  1122. indent = indent.slice(2);
  1123. }
  1124. // if a line ends in an unclosed attribute, we need to note that and close it later
  1125. const isOpenDoubleAttr = openAttrDoubleRegex.test(line);
  1126. const isOpenSingleAttr = openAttrSingleRegex.test(line);
  1127. if (!inAttribute && isOpenDoubleAttr) {
  1128. inAttribute = true;
  1129. isDoubleQuotes = true;
  1130. }
  1131. else if (!inAttribute && isOpenSingleAttr) {
  1132. inAttribute = true;
  1133. isDoubleQuotes = false;
  1134. }
  1135. const newLine = inI18nBlock || inAttribute
  1136. ? line
  1137. : mindent + (line.trim() !== '' ? indent : '') + line.trim();
  1138. formatted.push(newLine);
  1139. if (!isOpenDoubleAttr &&
  1140. !isOpenSingleAttr &&
  1141. ((inAttribute && isDoubleQuotes && closeAttrDoubleRegex.test(line)) ||
  1142. (inAttribute && !isDoubleQuotes && closeAttrSingleRegex.test(line)))) {
  1143. inAttribute = false;
  1144. }
  1145. // this matches any self closing element that actually has a />
  1146. if (closeMultiLineElRegex.test(line)) {
  1147. // multi line self closing tag
  1148. indent = indent.slice(2);
  1149. if (openSelfClosingEl) {
  1150. openSelfClosingEl = false;
  1151. }
  1152. }
  1153. // this matches a self closing element that doesn't have a / in the >
  1154. if (closeSelfClosingMultiLineRegex.test(line) && openSelfClosingEl) {
  1155. openSelfClosingEl = false;
  1156. indent = indent.slice(2);
  1157. }
  1158. // this matches an open control flow block, an open HTML element, but excludes single line
  1159. // self closing tags
  1160. if ((openBlockRegex.test(line) || openElRegex.test(line)) &&
  1161. !singleLineElRegex.test(line) &&
  1162. !selfClosingRegex.test(line) &&
  1163. !openSelfClosingRegex.test(line)) {
  1164. // open block, increase indent
  1165. indent += ' ';
  1166. }
  1167. // This is a self closing element that is definitely not fully closed and is on multiple lines
  1168. if (openSelfClosingRegex.test(line)) {
  1169. openSelfClosingEl = true;
  1170. // add to the indent for the properties on it to look nice
  1171. indent += ' ';
  1172. }
  1173. inI18nBlock = i18nDepth > 0;
  1174. }
  1175. tmpl = formatted.join('\n');
  1176. }
  1177. return tmpl;
  1178. }
  1179. /** Executes a callback on each class declaration in a file. */
  1180. function forEachClass(sourceFile, callback) {
  1181. sourceFile.forEachChild(function walk(node) {
  1182. if (ts.isClassDeclaration(node) || ts.isImportDeclaration(node)) {
  1183. callback(node);
  1184. }
  1185. node.forEachChild(walk);
  1186. });
  1187. }
  1188. const boundcase = '[ngSwitchCase]';
  1189. const switchcase = '*ngSwitchCase';
  1190. const nakedcase = 'ngSwitchCase';
  1191. const switchdefault = '*ngSwitchDefault';
  1192. const nakeddefault = 'ngSwitchDefault';
  1193. const cases = [boundcase, switchcase, nakedcase, switchdefault, nakeddefault];
  1194. /**
  1195. * Replaces structural directive ngSwitch instances with new switch.
  1196. * Returns null if the migration failed (e.g. there was a syntax error).
  1197. */
  1198. function migrateCase(template) {
  1199. let errors = [];
  1200. let parsed = parseTemplate(template);
  1201. if (parsed.tree === undefined) {
  1202. return { migrated: template, errors, changed: false };
  1203. }
  1204. let result = template;
  1205. const visitor = new ElementCollector(cases);
  1206. checker.visitAll(visitor, parsed.tree.rootNodes);
  1207. calculateNesting(visitor, hasLineBreaks(template));
  1208. // this tracks the character shift from different lengths of blocks from
  1209. // the prior directives so as to adjust for nested block replacement during
  1210. // migration. Each block calculates length differences and passes that offset
  1211. // to the next migrating block to adjust character offsets properly.
  1212. let offset = 0;
  1213. let nestLevel = -1;
  1214. let postOffsets = [];
  1215. for (const el of visitor.elements) {
  1216. let migrateResult = { tmpl: result, offsets: { pre: 0, post: 0 } };
  1217. // applies the post offsets after closing
  1218. offset = reduceNestingOffset(el, nestLevel, offset, postOffsets);
  1219. if (el.attr.name === switchcase || el.attr.name === nakedcase || el.attr.name === boundcase) {
  1220. try {
  1221. migrateResult = migrateNgSwitchCase(el, result, offset);
  1222. }
  1223. catch (error) {
  1224. errors.push({ type: switchcase, error });
  1225. }
  1226. }
  1227. else if (el.attr.name === switchdefault || el.attr.name === nakeddefault) {
  1228. try {
  1229. migrateResult = migrateNgSwitchDefault(el, result, offset);
  1230. }
  1231. catch (error) {
  1232. errors.push({ type: switchdefault, error });
  1233. }
  1234. }
  1235. result = migrateResult.tmpl;
  1236. offset += migrateResult.offsets.pre;
  1237. postOffsets.push(migrateResult.offsets.post);
  1238. nestLevel = el.nestCount;
  1239. }
  1240. const changed = visitor.elements.length > 0;
  1241. return { migrated: result, errors, changed };
  1242. }
  1243. function migrateNgSwitchCase(etm, tmpl, offset) {
  1244. // includes the mandatory semicolon before as
  1245. const lbString = etm.hasLineBreaks ? '\n' : '';
  1246. const leadingSpace = etm.hasLineBreaks ? '' : ' ';
  1247. // ngSwitchCases with no values results into `case ()` which isn't valid, based off empty
  1248. // value we add quotes instead of generating empty case
  1249. const condition = etm.attr.value.length === 0 ? `''` : etm.attr.value;
  1250. const originals = getOriginals(etm, tmpl, offset);
  1251. const { start, middle, end } = getMainBlock(etm, tmpl, offset);
  1252. const startBlock = `${startMarker}${leadingSpace}@case (${condition}) {${leadingSpace}${lbString}${start}`;
  1253. const endBlock = `${end}${lbString}${leadingSpace}}${endMarker}`;
  1254. const defaultBlock = startBlock + middle + endBlock;
  1255. const updatedTmpl = tmpl.slice(0, etm.start(offset)) + defaultBlock + tmpl.slice(etm.end(offset));
  1256. // this should be the difference between the starting element up to the start of the closing
  1257. // element and the mainblock sans }
  1258. const pre = originals.start.length - startBlock.length;
  1259. const post = originals.end.length - endBlock.length;
  1260. return { tmpl: updatedTmpl, offsets: { pre, post } };
  1261. }
  1262. function migrateNgSwitchDefault(etm, tmpl, offset) {
  1263. // includes the mandatory semicolon before as
  1264. const lbString = etm.hasLineBreaks ? '\n' : '';
  1265. const leadingSpace = etm.hasLineBreaks ? '' : ' ';
  1266. const originals = getOriginals(etm, tmpl, offset);
  1267. const { start, middle, end } = getMainBlock(etm, tmpl, offset);
  1268. const startBlock = `${startMarker}${leadingSpace}@default {${leadingSpace}${lbString}${start}`;
  1269. const endBlock = `${end}${lbString}${leadingSpace}}${endMarker}`;
  1270. const defaultBlock = startBlock + middle + endBlock;
  1271. const updatedTmpl = tmpl.slice(0, etm.start(offset)) + defaultBlock + tmpl.slice(etm.end(offset));
  1272. // this should be the difference between the starting element up to the start of the closing
  1273. // element and the mainblock sans }
  1274. const pre = originals.start.length - startBlock.length;
  1275. const post = originals.end.length - endBlock.length;
  1276. return { tmpl: updatedTmpl, offsets: { pre, post } };
  1277. }
  1278. const ngfor = '*ngFor';
  1279. const nakedngfor = 'ngFor';
  1280. const fors = [ngfor, nakedngfor];
  1281. const commaSeparatedSyntax = new Map([
  1282. ['(', ')'],
  1283. ['{', '}'],
  1284. ['[', ']'],
  1285. ]);
  1286. const stringPairs = new Map([
  1287. [`"`, `"`],
  1288. [`'`, `'`],
  1289. ]);
  1290. /**
  1291. * Replaces structural directive ngFor instances with new for.
  1292. * Returns null if the migration failed (e.g. there was a syntax error).
  1293. */
  1294. function migrateFor(template) {
  1295. let errors = [];
  1296. let parsed = parseTemplate(template);
  1297. if (parsed.tree === undefined) {
  1298. return { migrated: template, errors, changed: false };
  1299. }
  1300. let result = template;
  1301. const visitor = new ElementCollector(fors);
  1302. checker.visitAll(visitor, parsed.tree.rootNodes);
  1303. calculateNesting(visitor, hasLineBreaks(template));
  1304. // this tracks the character shift from different lengths of blocks from
  1305. // the prior directives so as to adjust for nested block replacement during
  1306. // migration. Each block calculates length differences and passes that offset
  1307. // to the next migrating block to adjust character offsets properly.
  1308. let offset = 0;
  1309. let nestLevel = -1;
  1310. let postOffsets = [];
  1311. for (const el of visitor.elements) {
  1312. let migrateResult = { tmpl: result, offsets: { pre: 0, post: 0 } };
  1313. // applies the post offsets after closing
  1314. offset = reduceNestingOffset(el, nestLevel, offset, postOffsets);
  1315. try {
  1316. migrateResult = migrateNgFor(el, result, offset);
  1317. }
  1318. catch (error) {
  1319. errors.push({ type: ngfor, error });
  1320. }
  1321. result = migrateResult.tmpl;
  1322. offset += migrateResult.offsets.pre;
  1323. postOffsets.push(migrateResult.offsets.post);
  1324. nestLevel = el.nestCount;
  1325. }
  1326. const changed = visitor.elements.length > 0;
  1327. return { migrated: result, errors, changed };
  1328. }
  1329. function migrateNgFor(etm, tmpl, offset) {
  1330. if (etm.forAttrs !== undefined) {
  1331. return migrateBoundNgFor(etm, tmpl, offset);
  1332. }
  1333. return migrateStandardNgFor(etm, tmpl, offset);
  1334. }
  1335. function migrateStandardNgFor(etm, tmpl, offset) {
  1336. const aliasWithEqualRegexp = /=\s*(count|index|first|last|even|odd)/gm;
  1337. const aliasWithAsRegexp = /(count|index|first|last|even|odd)\s+as/gm;
  1338. const aliases = [];
  1339. const lbString = etm.hasLineBreaks ? '\n' : '';
  1340. const parts = getNgForParts(etm.attr.value);
  1341. const originals = getOriginals(etm, tmpl, offset);
  1342. // first portion should always be the loop definition prefixed with `let`
  1343. const condition = parts[0].replace('let ', '');
  1344. if (condition.indexOf(' as ') > -1) {
  1345. let errorMessage = `Found an aliased collection on an ngFor: "${condition}".` +
  1346. ' Collection aliasing is not supported with @for.' +
  1347. ' Refactor the code to remove the `as` alias and re-run the migration.';
  1348. throw new Error(errorMessage);
  1349. }
  1350. const loopVar = condition.split(' of ')[0];
  1351. let trackBy = loopVar;
  1352. let aliasedIndex = null;
  1353. let tmplPlaceholder = '';
  1354. for (let i = 1; i < parts.length; i++) {
  1355. const part = parts[i].trim();
  1356. if (part.startsWith('trackBy:')) {
  1357. // build trackby value
  1358. const trackByFn = part.replace('trackBy:', '').trim();
  1359. trackBy = `${trackByFn}($index, ${loopVar})`;
  1360. }
  1361. // template
  1362. if (part.startsWith('template:')) {
  1363. // use an alternate placeholder here to avoid conflicts
  1364. tmplPlaceholder = getPlaceholder(part.split(':')[1].trim(), PlaceholderKind.Alternate);
  1365. }
  1366. // aliases
  1367. // declared with `let myIndex = index`
  1368. if (part.match(aliasWithEqualRegexp)) {
  1369. // 'let myIndex = index' -> ['let myIndex', 'index']
  1370. const aliasParts = part.split('=');
  1371. const aliasedName = aliasParts[0].replace('let', '').trim();
  1372. const originalName = aliasParts[1].trim();
  1373. if (aliasedName !== '$' + originalName) {
  1374. // -> 'let myIndex = $index'
  1375. aliases.push(` let ${aliasedName} = $${originalName}`);
  1376. }
  1377. // if the aliased variable is the index, then we store it
  1378. if (originalName === 'index') {
  1379. // 'let myIndex' -> 'myIndex'
  1380. aliasedIndex = aliasedName;
  1381. }
  1382. }
  1383. // declared with `index as myIndex`
  1384. if (part.match(aliasWithAsRegexp)) {
  1385. // 'index as myIndex' -> ['index', 'myIndex']
  1386. const aliasParts = part.split(/\s+as\s+/);
  1387. const originalName = aliasParts[0].trim();
  1388. const aliasedName = aliasParts[1].trim();
  1389. if (aliasedName !== '$' + originalName) {
  1390. // -> 'let myIndex = $index'
  1391. aliases.push(` let ${aliasedName} = $${originalName}`);
  1392. }
  1393. // if the aliased variable is the index, then we store it
  1394. if (originalName === 'index') {
  1395. aliasedIndex = aliasedName;
  1396. }
  1397. }
  1398. }
  1399. // if an alias has been defined for the index, then the trackBy function must use it
  1400. if (aliasedIndex !== null && trackBy !== loopVar) {
  1401. // byId($index, user) -> byId(i, user)
  1402. trackBy = trackBy.replace('$index', aliasedIndex);
  1403. }
  1404. const aliasStr = aliases.length > 0 ? `;${aliases.join(';')}` : '';
  1405. let startBlock = `${startMarker}@for (${condition}; track ${trackBy}${aliasStr}) {${lbString}`;
  1406. let endBlock = `${lbString}}${endMarker}`;
  1407. let forBlock = '';
  1408. if (tmplPlaceholder !== '') {
  1409. startBlock = startBlock + tmplPlaceholder;
  1410. forBlock = startBlock + endBlock;
  1411. }
  1412. else {
  1413. const { start, middle, end } = getMainBlock(etm, tmpl, offset);
  1414. startBlock += start;
  1415. endBlock = end + endBlock;
  1416. forBlock = startBlock + middle + endBlock;
  1417. }
  1418. const updatedTmpl = tmpl.slice(0, etm.start(offset)) + forBlock + tmpl.slice(etm.end(offset));
  1419. const pre = originals.start.length - startBlock.length;
  1420. const post = originals.end.length - endBlock.length;
  1421. return { tmpl: updatedTmpl, offsets: { pre, post } };
  1422. }
  1423. function migrateBoundNgFor(etm, tmpl, offset) {
  1424. const forAttrs = etm.forAttrs;
  1425. const aliasAttrs = etm.aliasAttrs;
  1426. const aliasMap = aliasAttrs.aliases;
  1427. const originals = getOriginals(etm, tmpl, offset);
  1428. const condition = `${aliasAttrs.item} of ${forAttrs.forOf}`;
  1429. const aliases = [];
  1430. let aliasedIndex = '$index';
  1431. for (const [key, val] of aliasMap) {
  1432. aliases.push(` let ${key.trim()} = $${val}`);
  1433. if (val.trim() === 'index') {
  1434. aliasedIndex = key;
  1435. }
  1436. }
  1437. const aliasStr = aliases.length > 0 ? `;${aliases.join(';')}` : '';
  1438. let trackBy = aliasAttrs.item;
  1439. if (forAttrs.trackBy !== '') {
  1440. // build trackby value
  1441. trackBy = `${forAttrs.trackBy.trim()}(${aliasedIndex}, ${aliasAttrs.item})`;
  1442. }
  1443. const { start, middle, end } = getMainBlock(etm, tmpl, offset);
  1444. const startBlock = `${startMarker}@for (${condition}; track ${trackBy}${aliasStr}) {\n${start}`;
  1445. const endBlock = `${end}\n}${endMarker}`;
  1446. const forBlock = startBlock + middle + endBlock;
  1447. const updatedTmpl = tmpl.slice(0, etm.start(offset)) + forBlock + tmpl.slice(etm.end(offset));
  1448. const pre = originals.start.length - startBlock.length;
  1449. const post = originals.end.length - endBlock.length;
  1450. return { tmpl: updatedTmpl, offsets: { pre, post } };
  1451. }
  1452. function getNgForParts(expression) {
  1453. const parts = [];
  1454. const commaSeparatedStack = [];
  1455. const stringStack = [];
  1456. let current = '';
  1457. for (let i = 0; i < expression.length; i++) {
  1458. const char = expression[i];
  1459. const isInString = stringStack.length === 0;
  1460. const isInCommaSeparated = commaSeparatedStack.length === 0;
  1461. // Any semicolon is a delimiter, as well as any comma outside
  1462. // of comma-separated syntax, as long as they're outside of a string.
  1463. if (isInString &&
  1464. current.length > 0 &&
  1465. (char === ';' || (char === ',' && isInCommaSeparated))) {
  1466. parts.push(current);
  1467. current = '';
  1468. continue;
  1469. }
  1470. if (stringStack.length > 0 && stringStack[stringStack.length - 1] === char) {
  1471. stringStack.pop();
  1472. }
  1473. else if (stringPairs.has(char)) {
  1474. stringStack.push(stringPairs.get(char));
  1475. }
  1476. if (commaSeparatedSyntax.has(char)) {
  1477. commaSeparatedStack.push(commaSeparatedSyntax.get(char));
  1478. }
  1479. else if (commaSeparatedStack.length > 0 &&
  1480. commaSeparatedStack[commaSeparatedStack.length - 1] === char) {
  1481. commaSeparatedStack.pop();
  1482. }
  1483. current += char;
  1484. }
  1485. if (current.length > 0) {
  1486. parts.push(current);
  1487. }
  1488. return parts;
  1489. }
  1490. const ngif = '*ngIf';
  1491. const boundngif = '[ngIf]';
  1492. const nakedngif = 'ngIf';
  1493. const ifs = [ngif, nakedngif, boundngif];
  1494. /**
  1495. * Replaces structural directive ngif instances with new if.
  1496. * Returns null if the migration failed (e.g. there was a syntax error).
  1497. */
  1498. function migrateIf(template) {
  1499. let errors = [];
  1500. let parsed = parseTemplate(template);
  1501. if (parsed.tree === undefined) {
  1502. return { migrated: template, errors, changed: false };
  1503. }
  1504. let result = template;
  1505. const visitor = new ElementCollector(ifs);
  1506. checker.visitAll(visitor, parsed.tree.rootNodes);
  1507. calculateNesting(visitor, hasLineBreaks(template));
  1508. // this tracks the character shift from different lengths of blocks from
  1509. // the prior directives so as to adjust for nested block replacement during
  1510. // migration. Each block calculates length differences and passes that offset
  1511. // to the next migrating block to adjust character offsets properly.
  1512. let offset = 0;
  1513. let nestLevel = -1;
  1514. let postOffsets = [];
  1515. for (const el of visitor.elements) {
  1516. let migrateResult = { tmpl: result, offsets: { pre: 0, post: 0 } };
  1517. // applies the post offsets after closing
  1518. offset = reduceNestingOffset(el, nestLevel, offset, postOffsets);
  1519. try {
  1520. migrateResult = migrateNgIf(el, result, offset);
  1521. }
  1522. catch (error) {
  1523. errors.push({ type: ngif, error });
  1524. }
  1525. result = migrateResult.tmpl;
  1526. offset += migrateResult.offsets.pre;
  1527. postOffsets.push(migrateResult.offsets.post);
  1528. nestLevel = el.nestCount;
  1529. }
  1530. const changed = visitor.elements.length > 0;
  1531. return { migrated: result, errors, changed };
  1532. }
  1533. function migrateNgIf(etm, tmpl, offset) {
  1534. const matchThen = etm.attr.value.match(/[^\w\d];?\s*then/gm);
  1535. const matchElse = etm.attr.value.match(/[^\w\d];?\s*else/gm);
  1536. if (etm.thenAttr !== undefined || etm.elseAttr !== undefined) {
  1537. // bound if then / if then else
  1538. return buildBoundIfElseBlock(etm, tmpl, offset);
  1539. }
  1540. else if (matchThen && matchThen.length > 0 && matchElse && matchElse.length > 0) {
  1541. // then else
  1542. return buildStandardIfThenElseBlock(etm, tmpl, matchThen[0], matchElse[0], offset);
  1543. }
  1544. else if (matchThen && matchThen.length > 0) {
  1545. // just then
  1546. return buildStandardIfThenBlock(etm, tmpl, matchThen[0], offset);
  1547. }
  1548. else if (matchElse && matchElse.length > 0) {
  1549. // just else
  1550. return buildStandardIfElseBlock(etm, tmpl, matchElse[0], offset);
  1551. }
  1552. return buildIfBlock(etm, tmpl, offset);
  1553. }
  1554. function buildIfBlock(etm, tmpl, offset) {
  1555. const aliasAttrs = etm.aliasAttrs;
  1556. const aliases = [...aliasAttrs.aliases.keys()];
  1557. if (aliasAttrs.item) {
  1558. aliases.push(aliasAttrs.item);
  1559. }
  1560. // includes the mandatory semicolon before as
  1561. const lbString = etm.hasLineBreaks ? '\n' : '';
  1562. let condition = etm.attr.value
  1563. .replace(' as ', '; as ')
  1564. // replace 'let' with 'as' whatever spaces are between ; and 'let'
  1565. .replace(/;\s*let/g, '; as');
  1566. if (aliases.length > 1 || (aliases.length === 1 && condition.indexOf('; as') > -1)) {
  1567. // only 1 alias allowed
  1568. throw new Error('Found more than one alias on your ngIf. Remove one of them and re-run the migration.');
  1569. }
  1570. else if (aliases.length === 1) {
  1571. condition += `; as ${aliases[0]}`;
  1572. }
  1573. const originals = getOriginals(etm, tmpl, offset);
  1574. const { start, middle, end } = getMainBlock(etm, tmpl, offset);
  1575. const startBlock = `${startMarker}@if (${condition}) {${lbString}${start}`;
  1576. const endBlock = `${end}${lbString}}${endMarker}`;
  1577. const ifBlock = startBlock + middle + endBlock;
  1578. const updatedTmpl = tmpl.slice(0, etm.start(offset)) + ifBlock + tmpl.slice(etm.end(offset));
  1579. // this should be the difference between the starting element up to the start of the closing
  1580. // element and the mainblock sans }
  1581. const pre = originals.start.length - startBlock.length;
  1582. const post = originals.end.length - endBlock.length;
  1583. return { tmpl: updatedTmpl, offsets: { pre, post } };
  1584. }
  1585. function buildStandardIfElseBlock(etm, tmpl, elseString, offset) {
  1586. // includes the mandatory semicolon before as
  1587. const condition = etm
  1588. .getCondition()
  1589. .replace(' as ', '; as ')
  1590. // replace 'let' with 'as' whatever spaces are between ; and 'let'
  1591. .replace(/;\s*let/g, '; as');
  1592. const elsePlaceholder = getPlaceholder(etm.getTemplateName(elseString));
  1593. return buildIfElseBlock(etm, tmpl, condition, elsePlaceholder, offset);
  1594. }
  1595. function buildBoundIfElseBlock(etm, tmpl, offset) {
  1596. const aliasAttrs = etm.aliasAttrs;
  1597. const aliases = [...aliasAttrs.aliases.keys()];
  1598. if (aliasAttrs.item) {
  1599. aliases.push(aliasAttrs.item);
  1600. }
  1601. // includes the mandatory semicolon before as
  1602. let condition = etm.attr.value.replace(' as ', '; as ');
  1603. if (aliases.length > 1 || (aliases.length === 1 && condition.indexOf('; as') > -1)) {
  1604. // only 1 alias allowed
  1605. throw new Error('Found more than one alias on your ngIf. Remove one of them and re-run the migration.');
  1606. }
  1607. else if (aliases.length === 1) {
  1608. condition += `; as ${aliases[0]}`;
  1609. }
  1610. const elsePlaceholder = getPlaceholder(etm.elseAttr.value.trim());
  1611. if (etm.thenAttr !== undefined) {
  1612. const thenPlaceholder = getPlaceholder(etm.thenAttr.value.trim());
  1613. return buildIfThenElseBlock(etm, tmpl, condition, thenPlaceholder, elsePlaceholder, offset);
  1614. }
  1615. return buildIfElseBlock(etm, tmpl, condition, elsePlaceholder, offset);
  1616. }
  1617. function buildIfElseBlock(etm, tmpl, condition, elsePlaceholder, offset) {
  1618. const lbString = etm.hasLineBreaks ? '\n' : '';
  1619. const originals = getOriginals(etm, tmpl, offset);
  1620. const { start, middle, end } = getMainBlock(etm, tmpl, offset);
  1621. const startBlock = `${startMarker}@if (${condition}) {${lbString}${start}`;
  1622. const elseBlock = `${end}${lbString}} @else {${lbString}`;
  1623. const postBlock = elseBlock + elsePlaceholder + `${lbString}}${endMarker}`;
  1624. const ifElseBlock = startBlock + middle + postBlock;
  1625. const tmplStart = tmpl.slice(0, etm.start(offset));
  1626. const tmplEnd = tmpl.slice(etm.end(offset));
  1627. const updatedTmpl = tmplStart + ifElseBlock + tmplEnd;
  1628. const pre = originals.start.length - startBlock.length;
  1629. const post = originals.end.length - postBlock.length;
  1630. return { tmpl: updatedTmpl, offsets: { pre, post } };
  1631. }
  1632. function buildStandardIfThenElseBlock(etm, tmpl, thenString, elseString, offset) {
  1633. // includes the mandatory semicolon before as
  1634. const condition = etm
  1635. .getCondition()
  1636. .replace(' as ', '; as ')
  1637. // replace 'let' with 'as' whatever spaces are between ; and 'let'
  1638. .replace(/;\s*let/g, '; as');
  1639. const thenPlaceholder = getPlaceholder(etm.getTemplateName(thenString, elseString));
  1640. const elsePlaceholder = getPlaceholder(etm.getTemplateName(elseString));
  1641. return buildIfThenElseBlock(etm, tmpl, condition, thenPlaceholder, elsePlaceholder, offset);
  1642. }
  1643. function buildStandardIfThenBlock(etm, tmpl, thenString, offset) {
  1644. // includes the mandatory semicolon before as
  1645. const condition = etm
  1646. .getCondition()
  1647. .replace(' as ', '; as ')
  1648. // replace 'let' with 'as' whatever spaces are between ; and 'let'
  1649. .replace(/;\s*let/g, '; as');
  1650. const thenPlaceholder = getPlaceholder(etm.getTemplateName(thenString));
  1651. return buildIfThenBlock(etm, tmpl, condition, thenPlaceholder, offset);
  1652. }
  1653. function buildIfThenElseBlock(etm, tmpl, condition, thenPlaceholder, elsePlaceholder, offset) {
  1654. const lbString = etm.hasLineBreaks ? '\n' : '';
  1655. const originals = getOriginals(etm, tmpl, offset);
  1656. const startBlock = `${startMarker}@if (${condition}) {${lbString}`;
  1657. const elseBlock = `${lbString}} @else {${lbString}`;
  1658. const postBlock = thenPlaceholder + elseBlock + elsePlaceholder + `${lbString}}${endMarker}`;
  1659. const ifThenElseBlock = startBlock + postBlock;
  1660. const tmplStart = tmpl.slice(0, etm.start(offset));
  1661. const tmplEnd = tmpl.slice(etm.end(offset));
  1662. const updatedTmpl = tmplStart + ifThenElseBlock + tmplEnd;
  1663. // We ignore the contents of the element on if then else.
  1664. // If there's anything there, we need to account for the length in the offset.
  1665. const pre = originals.start.length + originals.childLength - startBlock.length;
  1666. const post = originals.end.length - postBlock.length;
  1667. return { tmpl: updatedTmpl, offsets: { pre, post } };
  1668. }
  1669. function buildIfThenBlock(etm, tmpl, condition, thenPlaceholder, offset) {
  1670. const lbString = etm.hasLineBreaks ? '\n' : '';
  1671. const originals = getOriginals(etm, tmpl, offset);
  1672. const startBlock = `${startMarker}@if (${condition}) {${lbString}`;
  1673. const postBlock = thenPlaceholder + `${lbString}}${endMarker}`;
  1674. const ifThenBlock = startBlock + postBlock;
  1675. const tmplStart = tmpl.slice(0, etm.start(offset));
  1676. const tmplEnd = tmpl.slice(etm.end(offset));
  1677. const updatedTmpl = tmplStart + ifThenBlock + tmplEnd;
  1678. // We ignore the contents of the element on if then else.
  1679. // If there's anything there, we need to account for the length in the offset.
  1680. const pre = originals.start.length + originals.childLength - startBlock.length;
  1681. const post = originals.end.length - postBlock.length;
  1682. return { tmpl: updatedTmpl, offsets: { pre, post } };
  1683. }
  1684. const ngswitch = '[ngSwitch]';
  1685. const switches = [ngswitch];
  1686. /**
  1687. * Replaces structural directive ngSwitch instances with new switch.
  1688. * Returns null if the migration failed (e.g. there was a syntax error).
  1689. */
  1690. function migrateSwitch(template) {
  1691. let errors = [];
  1692. let parsed = parseTemplate(template);
  1693. if (parsed.tree === undefined) {
  1694. return { migrated: template, errors, changed: false };
  1695. }
  1696. let result = template;
  1697. const visitor = new ElementCollector(switches);
  1698. checker.visitAll(visitor, parsed.tree.rootNodes);
  1699. calculateNesting(visitor, hasLineBreaks(template));
  1700. // this tracks the character shift from different lengths of blocks from
  1701. // the prior directives so as to adjust for nested block replacement during
  1702. // migration. Each block calculates length differences and passes that offset
  1703. // to the next migrating block to adjust character offsets properly.
  1704. let offset = 0;
  1705. let nestLevel = -1;
  1706. let postOffsets = [];
  1707. for (const el of visitor.elements) {
  1708. let migrateResult = { tmpl: result, offsets: { pre: 0, post: 0 } };
  1709. // applies the post offsets after closing
  1710. offset = reduceNestingOffset(el, nestLevel, offset, postOffsets);
  1711. if (el.attr.name === ngswitch) {
  1712. try {
  1713. migrateResult = migrateNgSwitch(el, result, offset);
  1714. }
  1715. catch (error) {
  1716. errors.push({ type: ngswitch, error });
  1717. }
  1718. }
  1719. result = migrateResult.tmpl;
  1720. offset += migrateResult.offsets.pre;
  1721. postOffsets.push(migrateResult.offsets.post);
  1722. nestLevel = el.nestCount;
  1723. }
  1724. const changed = visitor.elements.length > 0;
  1725. return { migrated: result, errors, changed };
  1726. }
  1727. function assertValidSwitchStructure(children) {
  1728. for (const child of children) {
  1729. if (child instanceof checker.Text && child.value.trim() !== '') {
  1730. throw new Error(`Text node: "${child.value}" would result in invalid migrated @switch block structure. ` +
  1731. `@switch can only have @case or @default as children.`);
  1732. }
  1733. else if (child instanceof checker.Element) {
  1734. let hasCase = false;
  1735. for (const attr of child.attrs) {
  1736. if (cases.includes(attr.name)) {
  1737. hasCase = true;
  1738. }
  1739. }
  1740. if (!hasCase) {
  1741. throw new Error(`Element node: "${child.name}" would result in invalid migrated @switch block structure. ` +
  1742. `@switch can only have @case or @default as children.`);
  1743. }
  1744. }
  1745. }
  1746. }
  1747. function migrateNgSwitch(etm, tmpl, offset) {
  1748. const lbString = etm.hasLineBreaks ? '\n' : '';
  1749. const condition = etm.attr.value;
  1750. const originals = getOriginals(etm, tmpl, offset);
  1751. assertValidSwitchStructure(originals.childNodes);
  1752. const { start, middle, end } = getMainBlock(etm, tmpl, offset);
  1753. const startBlock = `${startMarker}${start}${lbString}@switch (${condition}) {`;
  1754. const endBlock = `}${lbString}${end}${endMarker}`;
  1755. const switchBlock = startBlock + middle + endBlock;
  1756. const updatedTmpl = tmpl.slice(0, etm.start(offset)) + switchBlock + tmpl.slice(etm.end(offset));
  1757. // this should be the difference between the starting element up to the start of the closing
  1758. // element and the mainblock sans }
  1759. const pre = originals.start.length - startBlock.length;
  1760. const post = originals.end.length - endBlock.length;
  1761. return { tmpl: updatedTmpl, offsets: { pre, post } };
  1762. }
  1763. /**
  1764. * Actually migrates a given template to the new syntax
  1765. */
  1766. function migrateTemplate(template, templateType, node, file, format = true, analyzedFiles) {
  1767. let errors = [];
  1768. let migrated = template;
  1769. if (templateType === 'template' || templateType === 'templateUrl') {
  1770. const ifResult = migrateIf(template);
  1771. const forResult = migrateFor(ifResult.migrated);
  1772. const switchResult = migrateSwitch(forResult.migrated);
  1773. if (switchResult.errors.length > 0) {
  1774. return { migrated: template, errors: switchResult.errors };
  1775. }
  1776. const caseResult = migrateCase(switchResult.migrated);
  1777. const templateResult = processNgTemplates(caseResult.migrated, file.sourceFile);
  1778. if (templateResult.err !== undefined) {
  1779. return { migrated: template, errors: [{ type: 'template', error: templateResult.err }] };
  1780. }
  1781. migrated = templateResult.migrated;
  1782. const changed = ifResult.changed || forResult.changed || switchResult.changed || caseResult.changed;
  1783. if (changed) {
  1784. // determine if migrated template is a valid structure
  1785. // if it is not, fail out
  1786. const errors = validateMigratedTemplate(migrated, file.sourceFile.fileName);
  1787. if (errors.length > 0) {
  1788. return { migrated: template, errors };
  1789. }
  1790. }
  1791. if (format && changed) {
  1792. migrated = formatTemplate(migrated, templateType);
  1793. }
  1794. const markerRegex = new RegExp(`${startMarker}|${endMarker}|${startI18nMarker}|${endI18nMarker}`, 'gm');
  1795. migrated = migrated.replace(markerRegex, '');
  1796. file.removeCommonModule = canRemoveCommonModule(template);
  1797. file.canRemoveImports = true;
  1798. // when migrating an external template, we have to pass back
  1799. // whether it's safe to remove the CommonModule to the
  1800. // original component class source file
  1801. if (templateType === 'templateUrl' &&
  1802. analyzedFiles !== null &&
  1803. analyzedFiles.has(file.sourceFile.fileName)) {
  1804. const componentFile = analyzedFiles.get(file.sourceFile.fileName);
  1805. componentFile.getSortedRanges();
  1806. // we have already checked the template file to see if it is safe to remove the imports
  1807. // and common module. This check is passed off to the associated .ts file here so
  1808. // the class knows whether it's safe to remove from the template side.
  1809. componentFile.removeCommonModule = file.removeCommonModule;
  1810. componentFile.canRemoveImports = file.canRemoveImports;
  1811. // At this point, we need to verify the component class file doesn't have any other imports
  1812. // that prevent safe removal of common module. It could be that there's an associated ngmodule
  1813. // and in that case we can't safely remove the common module import.
  1814. componentFile.verifyCanRemoveImports();
  1815. }
  1816. file.verifyCanRemoveImports();
  1817. errors = [
  1818. ...ifResult.errors,
  1819. ...forResult.errors,
  1820. ...switchResult.errors,
  1821. ...caseResult.errors,
  1822. ];
  1823. }
  1824. else if (file.canRemoveImports) {
  1825. migrated = removeImports(template, node, file);
  1826. }
  1827. return { migrated, errors };
  1828. }
  1829. function migrate(options) {
  1830. return async (tree, context) => {
  1831. const basePath = process.cwd();
  1832. const pathToMigrate = compiler_host.normalizePath(p.join(basePath, options.path));
  1833. let allPaths = [];
  1834. if (pathToMigrate.trim() !== '') {
  1835. allPaths.push(pathToMigrate);
  1836. }
  1837. if (!allPaths.length) {
  1838. throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run the control flow migration.');
  1839. }
  1840. let errors = [];
  1841. for (const tsconfigPath of allPaths) {
  1842. const migrateErrors = runControlFlowMigration(tree, tsconfigPath, basePath, pathToMigrate, options);
  1843. errors = [...errors, ...migrateErrors];
  1844. }
  1845. if (errors.length > 0) {
  1846. context.logger.warn(`WARNING: ${errors.length} errors occurred during your migration:\n`);
  1847. errors.forEach((err) => {
  1848. context.logger.warn(err);
  1849. });
  1850. }
  1851. };
  1852. }
  1853. function runControlFlowMigration(tree, tsconfigPath, basePath, pathToMigrate, schematicOptions) {
  1854. if (schematicOptions.path.startsWith('..')) {
  1855. throw new schematics.SchematicsException('Cannot run control flow migration outside of the current project.');
  1856. }
  1857. const program = compiler_host.createMigrationProgram(tree, tsconfigPath, basePath);
  1858. const sourceFiles = program
  1859. .getSourceFiles()
  1860. .filter((sourceFile) => sourceFile.fileName.startsWith(pathToMigrate) &&
  1861. compiler_host.canMigrateFile(basePath, sourceFile, program));
  1862. if (sourceFiles.length === 0) {
  1863. throw new schematics.SchematicsException(`Could not find any files to migrate under the path ${pathToMigrate}. Cannot run the control flow migration.`);
  1864. }
  1865. const analysis = new Map();
  1866. const migrateErrors = new Map();
  1867. for (const sourceFile of sourceFiles) {
  1868. analyze(sourceFile, analysis);
  1869. }
  1870. // sort files with .html files first
  1871. // this ensures class files know if it's safe to remove CommonModule
  1872. const paths = sortFilePaths([...analysis.keys()]);
  1873. for (const path of paths) {
  1874. const file = analysis.get(path);
  1875. const ranges = file.getSortedRanges();
  1876. const relativePath = p.relative(basePath, path);
  1877. const content = tree.readText(relativePath);
  1878. const update = tree.beginUpdate(relativePath);
  1879. for (const { start, end, node, type } of ranges) {
  1880. const template = content.slice(start, end);
  1881. const length = (end ?? content.length) - start;
  1882. const { migrated, errors } = migrateTemplate(template, type, node, file, schematicOptions.format, analysis);
  1883. if (migrated !== null) {
  1884. update.remove(start, length);
  1885. update.insertLeft(start, migrated);
  1886. }
  1887. if (errors.length > 0) {
  1888. migrateErrors.set(path, errors);
  1889. }
  1890. }
  1891. tree.commitUpdate(update);
  1892. }
  1893. const errorList = [];
  1894. for (let [template, errors] of migrateErrors) {
  1895. errorList.push(generateErrorMessage(template, errors));
  1896. }
  1897. return errorList;
  1898. }
  1899. function sortFilePaths(names) {
  1900. names.sort((a, _) => (a.endsWith('.html') ? -1 : 0));
  1901. return names;
  1902. }
  1903. function generateErrorMessage(path, errors) {
  1904. let errorMessage = `Template "${path}" encountered ${errors.length} errors during migration:\n`;
  1905. errorMessage += errors.map((e) => ` - ${e.type}: ${e.error}\n`);
  1906. return errorMessage;
  1907. }
  1908. exports.migrate = migrate;