12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919 |
- 'use strict';
- /**
- * @license Angular v19.2.13
- * (c) 2010-2025 Google LLC. https://angular.io/
- * License: MIT
- */
- 'use strict';
- var schematics = require('@angular-devkit/schematics');
- var p = require('path');
- var compiler_host = require('./compiler_host-B1Gyeytz.cjs');
- var checker = require('./checker-5pyJrZ9G.cjs');
- var ts = require('typescript');
- require('os');
- require('fs');
- require('module');
- require('url');
- function lookupIdentifiersInSourceFile(sourceFile, names) {
- const results = new Set();
- const visit = (node) => {
- if (ts.isIdentifier(node) && names.includes(node.text)) {
- results.add(node);
- }
- ts.forEachChild(node, visit);
- };
- visit(sourceFile);
- return results;
- }
- const ngtemplate = 'ng-template';
- const boundngifelse = '[ngIfElse]';
- const boundngifthenelse = '[ngIfThenElse]';
- const boundngifthen = '[ngIfThen]';
- const nakedngfor$1 = 'ngFor';
- const startMarker = '◬';
- const endMarker = '✢';
- const startI18nMarker = '⚈';
- const endI18nMarker = '⚉';
- const importRemovals = [
- 'NgIf',
- 'NgIfElse',
- 'NgIfThenElse',
- 'NgFor',
- 'NgForOf',
- 'NgForTrackBy',
- 'NgSwitch',
- 'NgSwitchCase',
- 'NgSwitchDefault',
- ];
- const importWithCommonRemovals = [...importRemovals, 'CommonModule'];
- function allFormsOf(selector) {
- return [selector, `*${selector}`, `[${selector}]`];
- }
- const commonModuleDirectives = new Set([
- ...allFormsOf('ngComponentOutlet'),
- ...allFormsOf('ngTemplateOutlet'),
- ...allFormsOf('ngClass'),
- ...allFormsOf('ngPlural'),
- ...allFormsOf('ngPluralCase'),
- ...allFormsOf('ngStyle'),
- ...allFormsOf('ngTemplateOutlet'),
- ...allFormsOf('ngComponentOutlet'),
- '[NgForOf]',
- '[NgForTrackBy]',
- '[ngIfElse]',
- '[ngIfThenElse]',
- ]);
- function pipeMatchRegExpFor(name) {
- return new RegExp(`\\|\\s*${name}`);
- }
- const commonModulePipes = [
- 'date',
- 'async',
- 'currency',
- 'number',
- 'i18nPlural',
- 'i18nSelect',
- 'json',
- 'keyvalue',
- 'slice',
- 'lowercase',
- 'uppercase',
- 'titlecase',
- 'percent',
- ].map((name) => pipeMatchRegExpFor(name));
- /**
- * Represents an element with a migratable attribute
- */
- class ElementToMigrate {
- el;
- attr;
- elseAttr;
- thenAttr;
- forAttrs;
- aliasAttrs;
- nestCount = 0;
- hasLineBreaks = false;
- constructor(el, attr, elseAttr = undefined, thenAttr = undefined, forAttrs = undefined, aliasAttrs = undefined) {
- this.el = el;
- this.attr = attr;
- this.elseAttr = elseAttr;
- this.thenAttr = thenAttr;
- this.forAttrs = forAttrs;
- this.aliasAttrs = aliasAttrs;
- }
- normalizeConditionString(value) {
- value = this.insertSemicolon(value, value.indexOf(' else '));
- value = this.insertSemicolon(value, value.indexOf(' then '));
- value = this.insertSemicolon(value, value.indexOf(' let '));
- return value.replace(';;', ';');
- }
- insertSemicolon(str, ix) {
- return ix > -1 ? `${str.slice(0, ix)};${str.slice(ix)}` : str;
- }
- getCondition() {
- const chunks = this.normalizeConditionString(this.attr.value).split(';');
- let condition = chunks[0];
- // checks for case of no usage of `;` in if else / if then else
- const elseIx = condition.indexOf(' else ');
- const thenIx = condition.indexOf(' then ');
- if (thenIx > -1) {
- condition = condition.slice(0, thenIx);
- }
- else if (elseIx > -1) {
- condition = condition.slice(0, elseIx);
- }
- let letVar = chunks.find((c) => c.search(/\s*let\s/) > -1);
- return condition + (letVar ? ';' + letVar : '');
- }
- getTemplateName(targetStr, secondStr) {
- const targetLocation = this.attr.value.indexOf(targetStr);
- const secondTargetLocation = secondStr ? this.attr.value.indexOf(secondStr) : undefined;
- let templateName = this.attr.value.slice(targetLocation + targetStr.length, secondTargetLocation);
- if (templateName.startsWith(':')) {
- templateName = templateName.slice(1).trim();
- }
- return templateName.split(';')[0].trim();
- }
- getValueEnd(offset) {
- return ((this.attr.valueSpan ? this.attr.valueSpan.end.offset + 1 : this.attr.keySpan.end.offset) -
- offset);
- }
- hasChildren() {
- return this.el.children.length > 0;
- }
- getChildSpan(offset) {
- const childStart = this.el.children[0].sourceSpan.start.offset - offset;
- const childEnd = this.el.children[this.el.children.length - 1].sourceSpan.end.offset - offset;
- return { childStart, childEnd };
- }
- shouldRemoveElseAttr() {
- return ((this.el.name === 'ng-template' || this.el.name === 'ng-container') &&
- this.elseAttr !== undefined);
- }
- getElseAttrStr() {
- if (this.elseAttr !== undefined) {
- const elseValStr = this.elseAttr.value !== '' ? `="${this.elseAttr.value}"` : '';
- return `${this.elseAttr.name}${elseValStr}`;
- }
- return '';
- }
- start(offset) {
- return this.el.sourceSpan?.start.offset - offset;
- }
- end(offset) {
- return this.el.sourceSpan?.end.offset - offset;
- }
- length() {
- return this.el.sourceSpan?.end.offset - this.el.sourceSpan?.start.offset;
- }
- }
- /**
- * Represents an ng-template inside a template being migrated to new control flow
- */
- class Template {
- el;
- name;
- count = 0;
- contents = '';
- children = '';
- i18n = null;
- attributes;
- constructor(el, name, i18n) {
- this.el = el;
- this.name = name;
- this.attributes = el.attrs;
- this.i18n = i18n;
- }
- get isNgTemplateOutlet() {
- return this.attributes.find((attr) => attr.name === '*ngTemplateOutlet') !== undefined;
- }
- get outletContext() {
- const letVar = this.attributes.find((attr) => attr.name.startsWith('let-'));
- return letVar ? `; context: {$implicit: ${letVar.name.split('-')[1]}}` : '';
- }
- generateTemplateOutlet() {
- const attr = this.attributes.find((attr) => attr.name === '*ngTemplateOutlet');
- const outletValue = attr?.value ?? this.name.slice(1);
- return `<ng-container *ngTemplateOutlet="${outletValue}${this.outletContext}"></ng-container>`;
- }
- generateContents(tmpl) {
- this.contents = tmpl.slice(this.el.sourceSpan.start.offset, this.el.sourceSpan.end.offset);
- this.children = '';
- if (this.el.children.length > 0) {
- this.children = tmpl.slice(this.el.children[0].sourceSpan.start.offset, this.el.children[this.el.children.length - 1].sourceSpan.end.offset);
- }
- }
- }
- /** Represents a file that was analyzed by the migration. */
- class AnalyzedFile {
- ranges = [];
- removeCommonModule = false;
- canRemoveImports = false;
- sourceFile;
- importRanges = [];
- templateRanges = [];
- constructor(sourceFile) {
- this.sourceFile = sourceFile;
- }
- /** Returns the ranges in the order in which they should be migrated. */
- getSortedRanges() {
- // templates first for checking on whether certain imports can be safely removed
- this.templateRanges = this.ranges
- .slice()
- .filter((x) => x.type === 'template' || x.type === 'templateUrl')
- .sort((aStart, bStart) => bStart.start - aStart.start);
- this.importRanges = this.ranges
- .slice()
- .filter((x) => x.type === 'importDecorator' || x.type === 'importDeclaration')
- .sort((aStart, bStart) => bStart.start - aStart.start);
- return [...this.templateRanges, ...this.importRanges];
- }
- /**
- * Adds a text range to an `AnalyzedFile`.
- * @param path Path of the file.
- * @param analyzedFiles Map keeping track of all the analyzed files.
- * @param range Range to be added.
- */
- static addRange(path, sourceFile, analyzedFiles, range) {
- let analysis = analyzedFiles.get(path);
- if (!analysis) {
- analysis = new AnalyzedFile(sourceFile);
- analyzedFiles.set(path, analysis);
- }
- const duplicate = analysis.ranges.find((current) => current.start === range.start && current.end === range.end);
- if (!duplicate) {
- analysis.ranges.push(range);
- }
- }
- /**
- * This verifies whether a component class is safe to remove module imports.
- * It is only run on .ts files.
- */
- verifyCanRemoveImports() {
- const importDeclaration = this.importRanges.find((r) => r.type === 'importDeclaration');
- const instances = lookupIdentifiersInSourceFile(this.sourceFile, importWithCommonRemovals);
- let foundImportDeclaration = false;
- let count = 0;
- for (let range of this.importRanges) {
- for (let instance of instances) {
- if (instance.getStart() >= range.start && instance.getEnd() <= range.end) {
- if (range === importDeclaration) {
- foundImportDeclaration = true;
- }
- count++;
- }
- }
- }
- if (instances.size !== count && importDeclaration !== undefined && foundImportDeclaration) {
- importDeclaration.remove = false;
- }
- }
- }
- /** Finds all non-control flow elements from common module. */
- class CommonCollector extends checker.RecursiveVisitor {
- count = 0;
- visitElement(el) {
- if (el.attrs.length > 0) {
- for (const attr of el.attrs) {
- if (this.hasDirectives(attr.name) || this.hasPipes(attr.value)) {
- this.count++;
- }
- }
- }
- super.visitElement(el, null);
- }
- visitBlock(ast) {
- for (const blockParam of ast.parameters) {
- if (this.hasPipes(blockParam.expression)) {
- this.count++;
- }
- }
- super.visitBlock(ast, null);
- }
- visitText(ast) {
- if (this.hasPipes(ast.value)) {
- this.count++;
- }
- }
- visitLetDeclaration(decl) {
- if (this.hasPipes(decl.value)) {
- this.count++;
- }
- super.visitLetDeclaration(decl, null);
- }
- hasDirectives(input) {
- return commonModuleDirectives.has(input);
- }
- hasPipes(input) {
- return commonModulePipes.some((regexp) => regexp.test(input));
- }
- }
- /** Finds all elements that represent i18n blocks. */
- class i18nCollector extends checker.RecursiveVisitor {
- elements = [];
- visitElement(el) {
- if (el.attrs.find((a) => a.name === 'i18n') !== undefined) {
- this.elements.push(el);
- }
- super.visitElement(el, null);
- }
- }
- /** Finds all elements with ngif structural directives. */
- class ElementCollector extends checker.RecursiveVisitor {
- _attributes;
- elements = [];
- constructor(_attributes = []) {
- super();
- this._attributes = _attributes;
- }
- visitElement(el) {
- if (el.attrs.length > 0) {
- for (const attr of el.attrs) {
- if (this._attributes.includes(attr.name)) {
- const elseAttr = el.attrs.find((x) => x.name === boundngifelse);
- const thenAttr = el.attrs.find((x) => x.name === boundngifthenelse || x.name === boundngifthen);
- const forAttrs = attr.name === nakedngfor$1 ? this.getForAttrs(el) : undefined;
- const aliasAttrs = this.getAliasAttrs(el);
- this.elements.push(new ElementToMigrate(el, attr, elseAttr, thenAttr, forAttrs, aliasAttrs));
- }
- }
- }
- super.visitElement(el, null);
- }
- getForAttrs(el) {
- let trackBy = '';
- let forOf = '';
- for (const attr of el.attrs) {
- if (attr.name === '[ngForTrackBy]') {
- trackBy = attr.value;
- }
- if (attr.name === '[ngForOf]') {
- forOf = attr.value;
- }
- }
- return { forOf, trackBy };
- }
- getAliasAttrs(el) {
- const aliases = new Map();
- let item = '';
- for (const attr of el.attrs) {
- if (attr.name.startsWith('let-')) {
- if (attr.value === '') {
- // item
- item = attr.name.replace('let-', '');
- }
- else {
- // alias
- aliases.set(attr.name.replace('let-', ''), attr.value);
- }
- }
- }
- return { item, aliases };
- }
- }
- /** Finds all elements with ngif structural directives. */
- class TemplateCollector extends checker.RecursiveVisitor {
- elements = [];
- templates = new Map();
- visitElement(el) {
- if (el.name === ngtemplate) {
- let i18n = null;
- let templateAttr = null;
- for (const attr of el.attrs) {
- if (attr.name === 'i18n') {
- i18n = attr;
- }
- if (attr.name.startsWith('#')) {
- templateAttr = attr;
- }
- }
- if (templateAttr !== null && !this.templates.has(templateAttr.name)) {
- this.templates.set(templateAttr.name, new Template(el, templateAttr.name, i18n));
- this.elements.push(new ElementToMigrate(el, templateAttr));
- }
- else if (templateAttr !== null) {
- throw new Error(`A duplicate ng-template name "${templateAttr.name}" was found. ` +
- `The control flow migration requires unique ng-template names within a component.`);
- }
- }
- super.visitElement(el, null);
- }
- }
- const startMarkerRegex = new RegExp(startMarker, 'gm');
- const endMarkerRegex = new RegExp(endMarker, 'gm');
- const startI18nMarkerRegex = new RegExp(startI18nMarker, 'gm');
- const endI18nMarkerRegex = new RegExp(endI18nMarker, 'gm');
- const replaceMarkerRegex = new RegExp(`${startMarker}|${endMarker}`, 'gm');
- /**
- * Analyzes a source file to find file that need to be migrated and the text ranges within them.
- * @param sourceFile File to be analyzed.
- * @param analyzedFiles Map in which to store the results.
- */
- function analyze(sourceFile, analyzedFiles) {
- forEachClass(sourceFile, (node) => {
- if (ts.isClassDeclaration(node)) {
- analyzeDecorators(node, sourceFile, analyzedFiles);
- }
- else {
- analyzeImportDeclarations(node, sourceFile, analyzedFiles);
- }
- });
- }
- function checkIfShouldChange(decl, file) {
- const range = file.importRanges.find((r) => r.type === 'importDeclaration');
- if (range === undefined || !range.remove) {
- return false;
- }
- // should change if you can remove the common module
- // if it's not safe to remove the common module
- // and that's the only thing there, we should do nothing.
- const clause = decl.getChildAt(1);
- return !(!file.removeCommonModule &&
- clause.namedBindings &&
- ts.isNamedImports(clause.namedBindings) &&
- clause.namedBindings.elements.length === 1 &&
- clause.namedBindings.elements[0].getText() === 'CommonModule');
- }
- function updateImportDeclaration(decl, removeCommonModule) {
- const clause = decl.getChildAt(1);
- const updatedClause = updateImportClause(clause, removeCommonModule);
- if (updatedClause === null) {
- return '';
- }
- // removeComments is set to true to prevent duplication of comments
- // when the import declaration is at the top of the file, but right after a comment
- // without this, the comment gets duplicated when the declaration is updated.
- // the typescript AST includes that preceding comment as part of the import declaration full text.
- const printer = ts.createPrinter({
- removeComments: true,
- });
- const updated = ts.factory.updateImportDeclaration(decl, decl.modifiers, updatedClause, decl.moduleSpecifier, undefined);
- return printer.printNode(ts.EmitHint.Unspecified, updated, clause.getSourceFile());
- }
- function updateImportClause(clause, removeCommonModule) {
- if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
- const removals = removeCommonModule ? importWithCommonRemovals : importRemovals;
- const elements = clause.namedBindings.elements.filter((el) => !removals.includes(el.getText()));
- if (elements.length === 0) {
- return null;
- }
- clause = ts.factory.updateImportClause(clause, clause.isTypeOnly, clause.name, ts.factory.createNamedImports(elements));
- }
- return clause;
- }
- function updateClassImports(propAssignment, removeCommonModule) {
- const printer = ts.createPrinter();
- const importList = propAssignment.initializer;
- // Can't change non-array literals.
- if (!ts.isArrayLiteralExpression(importList)) {
- return null;
- }
- const removals = removeCommonModule ? importWithCommonRemovals : importRemovals;
- const elements = importList.elements.filter((el) => !ts.isIdentifier(el) || !removals.includes(el.text));
- if (elements.length === importList.elements.length) {
- // nothing changed
- return null;
- }
- const updatedElements = ts.factory.updateArrayLiteralExpression(importList, elements);
- const updatedAssignment = ts.factory.updatePropertyAssignment(propAssignment, propAssignment.name, updatedElements);
- return printer.printNode(ts.EmitHint.Unspecified, updatedAssignment, updatedAssignment.getSourceFile());
- }
- function analyzeImportDeclarations(node, sourceFile, analyzedFiles) {
- if (node.getText().indexOf('@angular/common') === -1) {
- return;
- }
- const clause = node.getChildAt(1);
- if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
- const elements = clause.namedBindings.elements.filter((el) => importWithCommonRemovals.includes(el.getText()));
- if (elements.length > 0) {
- AnalyzedFile.addRange(sourceFile.fileName, sourceFile, analyzedFiles, {
- start: node.getStart(),
- end: node.getEnd(),
- node,
- type: 'importDeclaration',
- remove: true,
- });
- }
- }
- }
- function analyzeDecorators(node, sourceFile, analyzedFiles) {
- // Note: we have a utility to resolve the Angular decorators from a class declaration already.
- // We don't use it here, because it requires access to the type checker which makes it more
- // time-consuming to run internally.
- const decorator = ts.getDecorators(node)?.find((dec) => {
- return (ts.isCallExpression(dec.expression) &&
- ts.isIdentifier(dec.expression.expression) &&
- dec.expression.expression.text === 'Component');
- });
- const metadata = decorator &&
- decorator.expression.arguments.length > 0 &&
- ts.isObjectLiteralExpression(decorator.expression.arguments[0])
- ? decorator.expression.arguments[0]
- : null;
- if (!metadata) {
- return;
- }
- for (const prop of metadata.properties) {
- // All the properties we care about should have static
- // names and be initialized to a static string.
- if (!ts.isPropertyAssignment(prop) ||
- (!ts.isIdentifier(prop.name) && !ts.isStringLiteralLike(prop.name))) {
- continue;
- }
- switch (prop.name.text) {
- case 'template':
- // +1/-1 to exclude the opening/closing characters from the range.
- AnalyzedFile.addRange(sourceFile.fileName, sourceFile, analyzedFiles, {
- start: prop.initializer.getStart() + 1,
- end: prop.initializer.getEnd() - 1,
- node: prop,
- type: 'template',
- remove: true,
- });
- break;
- case 'imports':
- AnalyzedFile.addRange(sourceFile.fileName, sourceFile, analyzedFiles, {
- start: prop.name.getStart(),
- end: prop.initializer.getEnd(),
- node: prop,
- type: 'importDecorator',
- remove: true,
- });
- break;
- case 'templateUrl':
- // Leave the end as undefined which means that the range is until the end of the file.
- if (ts.isStringLiteralLike(prop.initializer)) {
- const path = p.join(p.dirname(sourceFile.fileName), prop.initializer.text);
- AnalyzedFile.addRange(path, sourceFile, analyzedFiles, {
- start: 0,
- node: prop,
- type: 'templateUrl',
- remove: true,
- });
- }
- break;
- }
- }
- }
- /**
- * returns the level deep a migratable element is nested
- */
- function getNestedCount(etm, aggregator) {
- if (aggregator.length === 0) {
- return 0;
- }
- if (etm.el.sourceSpan.start.offset < aggregator[aggregator.length - 1] &&
- etm.el.sourceSpan.end.offset !== aggregator[aggregator.length - 1]) {
- // element is nested
- aggregator.push(etm.el.sourceSpan.end.offset);
- return aggregator.length - 1;
- }
- else {
- // not nested
- aggregator.pop();
- return getNestedCount(etm, aggregator);
- }
- }
- /**
- * parses the template string into the Html AST
- */
- function parseTemplate(template) {
- let parsed;
- try {
- // Note: we use the HtmlParser here, instead of the `parseTemplate` function, because the
- // latter returns an Ivy AST, not an HTML AST. The HTML AST has the advantage of preserving
- // interpolated text as text nodes containing a mixture of interpolation tokens and text tokens,
- // rather than turning them into `BoundText` nodes like the Ivy AST does. This allows us to
- // easily get the text-only ranges without having to reconstruct the original text.
- parsed = new checker.HtmlParser().parse(template, '', {
- // Allows for ICUs to be parsed.
- tokenizeExpansionForms: true,
- // Explicitly disable blocks so that their characters are treated as plain text.
- tokenizeBlocks: true,
- preserveLineEndings: true,
- });
- // Don't migrate invalid templates.
- if (parsed.errors && parsed.errors.length > 0) {
- const errors = parsed.errors.map((e) => ({ type: 'parse', error: e }));
- return { tree: undefined, errors };
- }
- }
- catch (e) {
- return { tree: undefined, errors: [{ type: 'parse', error: e }] };
- }
- return { tree: parsed, errors: [] };
- }
- function validateMigratedTemplate(migrated, fileName) {
- const parsed = parseTemplate(migrated);
- let errors = [];
- if (parsed.errors.length > 0) {
- errors.push({
- type: 'parse',
- error: new Error(`The migration resulted in invalid HTML for ${fileName}. ` +
- `Please check the template for valid HTML structures and run the migration again.`),
- });
- }
- if (parsed.tree) {
- const i18nError = validateI18nStructure(parsed.tree, fileName);
- if (i18nError !== null) {
- errors.push({ type: 'i18n', error: i18nError });
- }
- }
- return errors;
- }
- function validateI18nStructure(parsed, fileName) {
- const visitor = new i18nCollector();
- checker.visitAll(visitor, parsed.rootNodes);
- const parents = visitor.elements.filter((el) => el.children.length > 0);
- for (const p of parents) {
- for (const el of visitor.elements) {
- if (el === p)
- continue;
- if (isChildOf(p, el)) {
- return new Error(`i18n Nesting error: The migration would result in invalid i18n nesting for ` +
- `${fileName}. Element with i18n attribute "${p.name}" would result having a child of ` +
- `element with i18n attribute "${el.name}". Please fix and re-run the migration.`);
- }
- }
- }
- return null;
- }
- function isChildOf(parent, el) {
- return (parent.sourceSpan.start.offset < el.sourceSpan.start.offset &&
- parent.sourceSpan.end.offset > el.sourceSpan.end.offset);
- }
- /** Possible placeholders that can be generated by `getPlaceholder`. */
- var PlaceholderKind;
- (function (PlaceholderKind) {
- PlaceholderKind[PlaceholderKind["Default"] = 0] = "Default";
- PlaceholderKind[PlaceholderKind["Alternate"] = 1] = "Alternate";
- })(PlaceholderKind || (PlaceholderKind = {}));
- /**
- * Wraps a string in a placeholder that makes it easier to identify during replacement operations.
- */
- function getPlaceholder(value, kind = PlaceholderKind.Default) {
- const name = `<<<ɵɵngControlFlowMigration_${kind}ɵɵ>>>`;
- return `___${name}${value}${name}___`;
- }
- /**
- * calculates the level of nesting of the items in the collector
- */
- function calculateNesting(visitor, hasLineBreaks) {
- // start from top of template
- // loop through each element
- let nestedQueue = [];
- for (let i = 0; i < visitor.elements.length; i++) {
- let currEl = visitor.elements[i];
- if (i === 0) {
- nestedQueue.push(currEl.el.sourceSpan.end.offset);
- currEl.hasLineBreaks = hasLineBreaks;
- continue;
- }
- currEl.hasLineBreaks = hasLineBreaks;
- currEl.nestCount = getNestedCount(currEl, nestedQueue);
- if (currEl.el.sourceSpan.end.offset !== nestedQueue[nestedQueue.length - 1]) {
- nestedQueue.push(currEl.el.sourceSpan.end.offset);
- }
- }
- }
- /**
- * determines if a given template string contains line breaks
- */
- function hasLineBreaks(template) {
- return /\r|\n/.test(template);
- }
- /**
- * properly adjusts template offsets based on current nesting levels
- */
- function reduceNestingOffset(el, nestLevel, offset, postOffsets) {
- if (el.nestCount <= nestLevel) {
- const count = nestLevel - el.nestCount;
- // reduced nesting, add postoffset
- for (let i = 0; i <= count; i++) {
- offset += postOffsets.pop() ?? 0;
- }
- }
- return offset;
- }
- /**
- * Replaces structural directive control flow instances with block control flow equivalents.
- * Returns null if the migration failed (e.g. there was a syntax error).
- */
- function getTemplates(template) {
- const parsed = parseTemplate(template);
- if (parsed.tree !== undefined) {
- const visitor = new TemplateCollector();
- checker.visitAll(visitor, parsed.tree.rootNodes);
- for (let [key, tmpl] of visitor.templates) {
- tmpl.count = countTemplateUsage(parsed.tree.rootNodes, key);
- tmpl.generateContents(template);
- }
- return visitor.templates;
- }
- return new Map();
- }
- function countTemplateUsage(nodes, templateName) {
- let count = 0;
- let isReferencedInTemplateOutlet = false;
- for (const node of nodes) {
- if (node.attrs) {
- for (const attr of node.attrs) {
- if (attr.name === '*ngTemplateOutlet' && attr.value === templateName.slice(1)) {
- isReferencedInTemplateOutlet = true;
- break;
- }
- if (attr.name.trim() === templateName) {
- count++;
- }
- }
- }
- if (node.children) {
- if (node.name === 'for') {
- for (const child of node.children) {
- if (child.value?.includes(templateName.slice(1))) {
- count++;
- }
- }
- }
- count += countTemplateUsage(node.children, templateName);
- }
- }
- return isReferencedInTemplateOutlet ? count + 2 : count;
- }
- function updateTemplates(template, templates) {
- const updatedTemplates = getTemplates(template);
- for (let [key, tmpl] of updatedTemplates) {
- templates.set(key, tmpl);
- }
- return templates;
- }
- function wrapIntoI18nContainer(i18nAttr, content) {
- const { start, middle, end } = generatei18nContainer(i18nAttr, content);
- return `${start}${middle}${end}`;
- }
- function generatei18nContainer(i18nAttr, middle) {
- const i18n = i18nAttr.value === '' ? 'i18n' : `i18n="${i18nAttr.value}"`;
- return { start: `<ng-container ${i18n}>`, middle, end: `</ng-container>` };
- }
- /**
- * Counts, replaces, and removes any necessary ng-templates post control flow migration
- */
- function processNgTemplates(template, sourceFile) {
- // count usage
- try {
- const templates = getTemplates(template);
- // swap placeholders and remove
- for (const [name, t] of templates) {
- const replaceRegex = new RegExp(getPlaceholder(name.slice(1)), 'g');
- const forRegex = new RegExp(getPlaceholder(name.slice(1), PlaceholderKind.Alternate), 'g');
- const forMatches = [...template.matchAll(forRegex)];
- const matches = [...forMatches, ...template.matchAll(replaceRegex)];
- let safeToRemove = true;
- if (matches.length > 0) {
- if (t.i18n !== null) {
- const container = wrapIntoI18nContainer(t.i18n, t.children);
- template = template.replace(replaceRegex, container);
- }
- else if (t.children.trim() === '' && t.isNgTemplateOutlet) {
- template = template.replace(replaceRegex, t.generateTemplateOutlet());
- }
- else if (forMatches.length > 0) {
- if (t.count === 2) {
- template = template.replace(forRegex, t.children);
- }
- else {
- template = template.replace(forRegex, t.generateTemplateOutlet());
- safeToRemove = false;
- }
- }
- else {
- template = template.replace(replaceRegex, t.children);
- }
- const dist = matches.filter((obj, index, self) => index === self.findIndex((t) => t.input === obj.input));
- if ((t.count === dist.length || t.count - matches.length === 1) && safeToRemove) {
- const refsInComponentFile = getViewChildOrViewChildrenNames(sourceFile);
- if (refsInComponentFile?.length > 0) {
- const templateRefs = getTemplateReferences(template);
- for (const ref of refsInComponentFile) {
- if (!templateRefs.includes(ref)) {
- template = template.replace(t.contents, `${startMarker}${endMarker}`);
- }
- }
- }
- else {
- template = template.replace(t.contents, `${startMarker}${endMarker}`);
- }
- }
- // templates may have changed structure from nested replaced templates
- // so we need to reprocess them before the next loop.
- updateTemplates(template, templates);
- }
- }
- // template placeholders may still exist if the ng-template name is not
- // present in the component. This could be because it's passed in from
- // another component. In that case, we need to replace any remaining
- // template placeholders with template outlets.
- template = replaceRemainingPlaceholders(template);
- return { migrated: template, err: undefined };
- }
- catch (err) {
- return { migrated: template, err: err };
- }
- }
- function getViewChildOrViewChildrenNames(sourceFile) {
- const names = [];
- function visit(node) {
- if (ts.isDecorator(node) && ts.isCallExpression(node.expression)) {
- const expr = node.expression;
- if (ts.isIdentifier(expr.expression) &&
- (expr.expression.text === 'ViewChild' || expr.expression.text === 'ViewChildren')) {
- const firstArg = expr.arguments[0];
- if (firstArg && ts.isStringLiteral(firstArg)) {
- names.push(firstArg.text);
- }
- return;
- }
- }
- ts.forEachChild(node, visit);
- }
- visit(sourceFile);
- return names;
- }
- function getTemplateReferences(template) {
- const parsed = parseTemplate(template);
- if (parsed.tree === undefined) {
- return [];
- }
- const references = [];
- function visitNodes(nodes) {
- for (const node of nodes) {
- if (node?.name === 'ng-template') {
- references.push(...node.attrs?.map((ref) => ref?.name?.slice(1)));
- }
- if (node.children) {
- visitNodes(node.children);
- }
- }
- }
- visitNodes(parsed.tree.rootNodes);
- return references;
- }
- function replaceRemainingPlaceholders(template) {
- const pattern = '.*';
- const placeholderPattern = getPlaceholder(pattern);
- const replaceRegex = new RegExp(placeholderPattern, 'g');
- const [placeholderStart, placeholderEnd] = placeholderPattern.split(pattern);
- const placeholders = [...template.matchAll(replaceRegex)];
- for (let ph of placeholders) {
- const placeholder = ph[0];
- const name = placeholder.slice(placeholderStart.length, placeholder.length - placeholderEnd.length);
- template = template.replace(placeholder, `<ng-template [ngTemplateOutlet]="${name}"></ng-template>`);
- }
- return template;
- }
- /**
- * determines if the CommonModule can be safely removed from imports
- */
- function canRemoveCommonModule(template) {
- const parsed = parseTemplate(template);
- let removeCommonModule = false;
- if (parsed.tree !== undefined) {
- const visitor = new CommonCollector();
- checker.visitAll(visitor, parsed.tree.rootNodes);
- removeCommonModule = visitor.count === 0;
- }
- return removeCommonModule;
- }
- /**
- * removes imports from template imports and import declarations
- */
- function removeImports(template, node, file) {
- if (template.startsWith('imports') && ts.isPropertyAssignment(node)) {
- const updatedImport = updateClassImports(node, file.removeCommonModule);
- return updatedImport ?? template;
- }
- else if (ts.isImportDeclaration(node) && checkIfShouldChange(node, file)) {
- return updateImportDeclaration(node, file.removeCommonModule);
- }
- return template;
- }
- /**
- * retrieves the original block of text in the template for length comparison during migration
- * processing
- */
- function getOriginals(etm, tmpl, offset) {
- // original opening block
- if (etm.el.children.length > 0) {
- const childStart = etm.el.children[0].sourceSpan.start.offset - offset;
- const childEnd = etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - offset;
- const start = tmpl.slice(etm.el.sourceSpan.start.offset - offset, etm.el.children[0].sourceSpan.start.offset - offset);
- // original closing block
- const end = tmpl.slice(etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - offset, etm.el.sourceSpan.end.offset - offset);
- const childLength = childEnd - childStart;
- return {
- start,
- end,
- childLength,
- children: getOriginalChildren(etm.el.children, tmpl, offset),
- childNodes: etm.el.children,
- };
- }
- // self closing or no children
- const start = tmpl.slice(etm.el.sourceSpan.start.offset - offset, etm.el.sourceSpan.end.offset - offset);
- // original closing block
- return { start, end: '', childLength: 0, children: [], childNodes: [] };
- }
- function getOriginalChildren(children, tmpl, offset) {
- return children.map((child) => {
- return tmpl.slice(child.sourceSpan.start.offset - offset, child.sourceSpan.end.offset - offset);
- });
- }
- function isI18nTemplate(etm, i18nAttr) {
- let attrCount = countAttributes(etm);
- const safeToRemove = etm.el.attrs.length === attrCount + (i18nAttr !== undefined ? 1 : 0);
- return etm.el.name === 'ng-template' && i18nAttr !== undefined && safeToRemove;
- }
- function isRemovableContainer(etm) {
- let attrCount = countAttributes(etm);
- const safeToRemove = etm.el.attrs.length === attrCount;
- return (etm.el.name === 'ng-container' || etm.el.name === 'ng-template') && safeToRemove;
- }
- function countAttributes(etm) {
- let attrCount = 1;
- if (etm.elseAttr !== undefined) {
- attrCount++;
- }
- if (etm.thenAttr !== undefined) {
- attrCount++;
- }
- attrCount += etm.aliasAttrs?.aliases.size ?? 0;
- attrCount += etm.aliasAttrs?.item ? 1 : 0;
- attrCount += etm.forAttrs?.trackBy ? 1 : 0;
- attrCount += etm.forAttrs?.forOf ? 1 : 0;
- return attrCount;
- }
- /**
- * builds the proper contents of what goes inside a given control flow block after migration
- */
- function getMainBlock(etm, tmpl, offset) {
- const i18nAttr = etm.el.attrs.find((x) => x.name === 'i18n');
- // removable containers are ng-templates or ng-containers that no longer need to exist
- // post migration
- if (isRemovableContainer(etm)) {
- let middle = '';
- if (etm.hasChildren()) {
- const { childStart, childEnd } = etm.getChildSpan(offset);
- middle = tmpl.slice(childStart, childEnd);
- }
- else {
- middle = '';
- }
- return { start: '', middle, end: '' };
- }
- else if (isI18nTemplate(etm, i18nAttr)) {
- // here we're removing an ng-template used for control flow and i18n and
- // converting it to an ng-container with i18n
- const { childStart, childEnd } = etm.getChildSpan(offset);
- return generatei18nContainer(i18nAttr, tmpl.slice(childStart, childEnd));
- }
- // the index of the start of the attribute adjusting for offset shift
- const attrStart = etm.attr.keySpan.start.offset - 1 - offset;
- // the index of the very end of the attribute value adjusted for offset shift
- const valEnd = etm.getValueEnd(offset);
- // the index of the children start and end span, if they exist. Otherwise use the value end.
- const { childStart, childEnd } = etm.hasChildren()
- ? etm.getChildSpan(offset)
- : { childStart: valEnd, childEnd: valEnd };
- // the beginning of the updated string in the main block, for example: <div some="attributes">
- let start = tmpl.slice(etm.start(offset), attrStart) + tmpl.slice(valEnd, childStart);
- // the middle is the actual contents of the element
- const middle = tmpl.slice(childStart, childEnd);
- // the end is the closing part of the element, example: </div>
- let end = tmpl.slice(childEnd, etm.end(offset));
- if (etm.shouldRemoveElseAttr()) {
- // this removes a bound ngIfElse attribute that's no longer needed
- // this could be on the start or end
- start = start.replace(etm.getElseAttrStr(), '');
- end = end.replace(etm.getElseAttrStr(), '');
- }
- return { start, middle, end };
- }
- function generateI18nMarkers(tmpl) {
- let parsed = parseTemplate(tmpl);
- if (parsed.tree !== undefined) {
- const visitor = new i18nCollector();
- checker.visitAll(visitor, parsed.tree.rootNodes);
- for (const [ix, el] of visitor.elements.entries()) {
- // we only care about elements with children and i18n tags
- // elements without children have nothing to translate
- // offset accounts for the addition of the 2 marker characters with each loop.
- const offset = ix * 2;
- if (el.children.length > 0) {
- tmpl = addI18nMarkers(tmpl, el, offset);
- }
- }
- }
- return tmpl;
- }
- function addI18nMarkers(tmpl, el, offset) {
- const startPos = el.children[0].sourceSpan.start.offset + offset;
- const endPos = el.children[el.children.length - 1].sourceSpan.end.offset + offset;
- return (tmpl.slice(0, startPos) +
- startI18nMarker +
- tmpl.slice(startPos, endPos) +
- endI18nMarker +
- tmpl.slice(endPos));
- }
- const selfClosingList = 'input|br|img|base|wbr|area|col|embed|hr|link|meta|param|source|track';
- /**
- * re-indents all the lines in the template properly post migration
- */
- function formatTemplate(tmpl, templateType) {
- if (tmpl.indexOf('\n') > -1) {
- tmpl = generateI18nMarkers(tmpl);
- // tracks if a self closing element opened without closing yet
- let openSelfClosingEl = false;
- // match any type of control flow block as start of string ignoring whitespace
- // @if | @switch | @case | @default | @for | } @else
- const openBlockRegex = /^\s*\@(if|switch|case|default|for)|^\s*\}\s\@else/;
- // regex for matching an html element opening
- // <div thing="stuff" [binding]="true"> || <div thing="stuff" [binding]="true"
- const openElRegex = /^\s*<([a-z0-9]+)(?![^>]*\/>)[^>]*>?/;
- // regex for matching an attribute string that was left open at the endof a line
- // so we can ensure we have the proper indent
- // <div thing="aefaefwe
- const openAttrDoubleRegex = /="([^"]|\\")*$/;
- const openAttrSingleRegex = /='([^']|\\')*$/;
- // regex for matching an attribute string that was closes on a separate line
- // from when it was opened.
- // <div thing="aefaefwe
- // i18n message is here">
- const closeAttrDoubleRegex = /^\s*([^><]|\\")*"/;
- const closeAttrSingleRegex = /^\s*([^><]|\\')*'/;
- // regex for matching a self closing html element that has no />
- // <input type="button" [binding]="true">
- const selfClosingRegex = new RegExp(`^\\s*<(${selfClosingList}).+\\/?>`);
- // regex for matching a self closing html element that is on multi lines
- // <input type="button" [binding]="true"> || <input type="button" [binding]="true"
- const openSelfClosingRegex = new RegExp(`^\\s*<(${selfClosingList})(?![^>]*\\/>)[^>]*$`);
- // match closing block or else block
- // } | } @else
- const closeBlockRegex = /^\s*\}\s*$|^\s*\}\s\@else/;
- // matches closing of an html element
- // </element>
- const closeElRegex = /\s*<\/([a-zA-Z0-9\-_]+)\s*>/m;
- // matches closing of a self closing html element when the element is on multiple lines
- // [binding]="value" />
- const closeMultiLineElRegex = /^\s*([a-zA-Z0-9\-_\[\]]+)?=?"?([^”<]+)?"?\s?\/>$/;
- // matches closing of a self closing html element when the element is on multiple lines
- // with no / in the closing: [binding]="value">
- const closeSelfClosingMultiLineRegex = /^\s*([a-zA-Z0-9\-_\[\]]+)?=?"?([^”\/<]+)?"?\s?>$/;
- // matches an open and close of an html element on a single line with no breaks
- // <div>blah</div>
- const singleLineElRegex = /\s*<([a-zA-Z0-9]+)(?![^>]*\/>)[^>]*>.*<\/([a-zA-Z0-9\-_]+)\s*>/;
- const lines = tmpl.split('\n');
- const formatted = [];
- // the indent applied during formatting
- let indent = '';
- // the pre-existing indent in an inline template that we'd like to preserve
- let mindent = '';
- let depth = 0;
- let i18nDepth = 0;
- let inMigratedBlock = false;
- let inI18nBlock = false;
- let inAttribute = false;
- let isDoubleQuotes = false;
- for (let [index, line] of lines.entries()) {
- depth +=
- [...line.matchAll(startMarkerRegex)].length - [...line.matchAll(endMarkerRegex)].length;
- inMigratedBlock = depth > 0;
- i18nDepth +=
- [...line.matchAll(startI18nMarkerRegex)].length -
- [...line.matchAll(endI18nMarkerRegex)].length;
- let lineWasMigrated = false;
- if (line.match(replaceMarkerRegex)) {
- line = line.replace(replaceMarkerRegex, '');
- lineWasMigrated = true;
- }
- if (line.trim() === '' &&
- index !== 0 &&
- index !== lines.length - 1 &&
- (inMigratedBlock || lineWasMigrated) &&
- !inI18nBlock &&
- !inAttribute) {
- // skip blank lines except if it's the first line or last line
- // this preserves leading and trailing spaces if they are already present
- continue;
- }
- // preserves the indentation of an inline template
- if (templateType === 'template' && index <= 1) {
- // first real line of an inline template
- const ind = line.search(/\S/);
- mindent = ind > -1 ? line.slice(0, ind) : '';
- }
- // if a block closes, an element closes, and it's not an element on a single line or the end
- // of a self closing tag
- if ((closeBlockRegex.test(line) ||
- (closeElRegex.test(line) &&
- !singleLineElRegex.test(line) &&
- !closeMultiLineElRegex.test(line))) &&
- indent !== '') {
- // close block, reduce indent
- indent = indent.slice(2);
- }
- // if a line ends in an unclosed attribute, we need to note that and close it later
- const isOpenDoubleAttr = openAttrDoubleRegex.test(line);
- const isOpenSingleAttr = openAttrSingleRegex.test(line);
- if (!inAttribute && isOpenDoubleAttr) {
- inAttribute = true;
- isDoubleQuotes = true;
- }
- else if (!inAttribute && isOpenSingleAttr) {
- inAttribute = true;
- isDoubleQuotes = false;
- }
- const newLine = inI18nBlock || inAttribute
- ? line
- : mindent + (line.trim() !== '' ? indent : '') + line.trim();
- formatted.push(newLine);
- if (!isOpenDoubleAttr &&
- !isOpenSingleAttr &&
- ((inAttribute && isDoubleQuotes && closeAttrDoubleRegex.test(line)) ||
- (inAttribute && !isDoubleQuotes && closeAttrSingleRegex.test(line)))) {
- inAttribute = false;
- }
- // this matches any self closing element that actually has a />
- if (closeMultiLineElRegex.test(line)) {
- // multi line self closing tag
- indent = indent.slice(2);
- if (openSelfClosingEl) {
- openSelfClosingEl = false;
- }
- }
- // this matches a self closing element that doesn't have a / in the >
- if (closeSelfClosingMultiLineRegex.test(line) && openSelfClosingEl) {
- openSelfClosingEl = false;
- indent = indent.slice(2);
- }
- // this matches an open control flow block, an open HTML element, but excludes single line
- // self closing tags
- if ((openBlockRegex.test(line) || openElRegex.test(line)) &&
- !singleLineElRegex.test(line) &&
- !selfClosingRegex.test(line) &&
- !openSelfClosingRegex.test(line)) {
- // open block, increase indent
- indent += ' ';
- }
- // This is a self closing element that is definitely not fully closed and is on multiple lines
- if (openSelfClosingRegex.test(line)) {
- openSelfClosingEl = true;
- // add to the indent for the properties on it to look nice
- indent += ' ';
- }
- inI18nBlock = i18nDepth > 0;
- }
- tmpl = formatted.join('\n');
- }
- return tmpl;
- }
- /** Executes a callback on each class declaration in a file. */
- function forEachClass(sourceFile, callback) {
- sourceFile.forEachChild(function walk(node) {
- if (ts.isClassDeclaration(node) || ts.isImportDeclaration(node)) {
- callback(node);
- }
- node.forEachChild(walk);
- });
- }
- const boundcase = '[ngSwitchCase]';
- const switchcase = '*ngSwitchCase';
- const nakedcase = 'ngSwitchCase';
- const switchdefault = '*ngSwitchDefault';
- const nakeddefault = 'ngSwitchDefault';
- const cases = [boundcase, switchcase, nakedcase, switchdefault, nakeddefault];
- /**
- * Replaces structural directive ngSwitch instances with new switch.
- * Returns null if the migration failed (e.g. there was a syntax error).
- */
- function migrateCase(template) {
- let errors = [];
- let parsed = parseTemplate(template);
- if (parsed.tree === undefined) {
- return { migrated: template, errors, changed: false };
- }
- let result = template;
- const visitor = new ElementCollector(cases);
- checker.visitAll(visitor, parsed.tree.rootNodes);
- calculateNesting(visitor, hasLineBreaks(template));
- // this tracks the character shift from different lengths of blocks from
- // the prior directives so as to adjust for nested block replacement during
- // migration. Each block calculates length differences and passes that offset
- // to the next migrating block to adjust character offsets properly.
- let offset = 0;
- let nestLevel = -1;
- let postOffsets = [];
- for (const el of visitor.elements) {
- let migrateResult = { tmpl: result, offsets: { pre: 0, post: 0 } };
- // applies the post offsets after closing
- offset = reduceNestingOffset(el, nestLevel, offset, postOffsets);
- if (el.attr.name === switchcase || el.attr.name === nakedcase || el.attr.name === boundcase) {
- try {
- migrateResult = migrateNgSwitchCase(el, result, offset);
- }
- catch (error) {
- errors.push({ type: switchcase, error });
- }
- }
- else if (el.attr.name === switchdefault || el.attr.name === nakeddefault) {
- try {
- migrateResult = migrateNgSwitchDefault(el, result, offset);
- }
- catch (error) {
- errors.push({ type: switchdefault, error });
- }
- }
- result = migrateResult.tmpl;
- offset += migrateResult.offsets.pre;
- postOffsets.push(migrateResult.offsets.post);
- nestLevel = el.nestCount;
- }
- const changed = visitor.elements.length > 0;
- return { migrated: result, errors, changed };
- }
- function migrateNgSwitchCase(etm, tmpl, offset) {
- // includes the mandatory semicolon before as
- const lbString = etm.hasLineBreaks ? '\n' : '';
- const leadingSpace = etm.hasLineBreaks ? '' : ' ';
- // ngSwitchCases with no values results into `case ()` which isn't valid, based off empty
- // value we add quotes instead of generating empty case
- const condition = etm.attr.value.length === 0 ? `''` : etm.attr.value;
- const originals = getOriginals(etm, tmpl, offset);
- const { start, middle, end } = getMainBlock(etm, tmpl, offset);
- const startBlock = `${startMarker}${leadingSpace}@case (${condition}) {${leadingSpace}${lbString}${start}`;
- const endBlock = `${end}${lbString}${leadingSpace}}${endMarker}`;
- const defaultBlock = startBlock + middle + endBlock;
- const updatedTmpl = tmpl.slice(0, etm.start(offset)) + defaultBlock + tmpl.slice(etm.end(offset));
- // this should be the difference between the starting element up to the start of the closing
- // element and the mainblock sans }
- const pre = originals.start.length - startBlock.length;
- const post = originals.end.length - endBlock.length;
- return { tmpl: updatedTmpl, offsets: { pre, post } };
- }
- function migrateNgSwitchDefault(etm, tmpl, offset) {
- // includes the mandatory semicolon before as
- const lbString = etm.hasLineBreaks ? '\n' : '';
- const leadingSpace = etm.hasLineBreaks ? '' : ' ';
- const originals = getOriginals(etm, tmpl, offset);
- const { start, middle, end } = getMainBlock(etm, tmpl, offset);
- const startBlock = `${startMarker}${leadingSpace}@default {${leadingSpace}${lbString}${start}`;
- const endBlock = `${end}${lbString}${leadingSpace}}${endMarker}`;
- const defaultBlock = startBlock + middle + endBlock;
- const updatedTmpl = tmpl.slice(0, etm.start(offset)) + defaultBlock + tmpl.slice(etm.end(offset));
- // this should be the difference between the starting element up to the start of the closing
- // element and the mainblock sans }
- const pre = originals.start.length - startBlock.length;
- const post = originals.end.length - endBlock.length;
- return { tmpl: updatedTmpl, offsets: { pre, post } };
- }
- const ngfor = '*ngFor';
- const nakedngfor = 'ngFor';
- const fors = [ngfor, nakedngfor];
- const commaSeparatedSyntax = new Map([
- ['(', ')'],
- ['{', '}'],
- ['[', ']'],
- ]);
- const stringPairs = new Map([
- [`"`, `"`],
- [`'`, `'`],
- ]);
- /**
- * Replaces structural directive ngFor instances with new for.
- * Returns null if the migration failed (e.g. there was a syntax error).
- */
- function migrateFor(template) {
- let errors = [];
- let parsed = parseTemplate(template);
- if (parsed.tree === undefined) {
- return { migrated: template, errors, changed: false };
- }
- let result = template;
- const visitor = new ElementCollector(fors);
- checker.visitAll(visitor, parsed.tree.rootNodes);
- calculateNesting(visitor, hasLineBreaks(template));
- // this tracks the character shift from different lengths of blocks from
- // the prior directives so as to adjust for nested block replacement during
- // migration. Each block calculates length differences and passes that offset
- // to the next migrating block to adjust character offsets properly.
- let offset = 0;
- let nestLevel = -1;
- let postOffsets = [];
- for (const el of visitor.elements) {
- let migrateResult = { tmpl: result, offsets: { pre: 0, post: 0 } };
- // applies the post offsets after closing
- offset = reduceNestingOffset(el, nestLevel, offset, postOffsets);
- try {
- migrateResult = migrateNgFor(el, result, offset);
- }
- catch (error) {
- errors.push({ type: ngfor, error });
- }
- result = migrateResult.tmpl;
- offset += migrateResult.offsets.pre;
- postOffsets.push(migrateResult.offsets.post);
- nestLevel = el.nestCount;
- }
- const changed = visitor.elements.length > 0;
- return { migrated: result, errors, changed };
- }
- function migrateNgFor(etm, tmpl, offset) {
- if (etm.forAttrs !== undefined) {
- return migrateBoundNgFor(etm, tmpl, offset);
- }
- return migrateStandardNgFor(etm, tmpl, offset);
- }
- function migrateStandardNgFor(etm, tmpl, offset) {
- const aliasWithEqualRegexp = /=\s*(count|index|first|last|even|odd)/gm;
- const aliasWithAsRegexp = /(count|index|first|last|even|odd)\s+as/gm;
- const aliases = [];
- const lbString = etm.hasLineBreaks ? '\n' : '';
- const parts = getNgForParts(etm.attr.value);
- const originals = getOriginals(etm, tmpl, offset);
- // first portion should always be the loop definition prefixed with `let`
- const condition = parts[0].replace('let ', '');
- if (condition.indexOf(' as ') > -1) {
- let errorMessage = `Found an aliased collection on an ngFor: "${condition}".` +
- ' Collection aliasing is not supported with @for.' +
- ' Refactor the code to remove the `as` alias and re-run the migration.';
- throw new Error(errorMessage);
- }
- const loopVar = condition.split(' of ')[0];
- let trackBy = loopVar;
- let aliasedIndex = null;
- let tmplPlaceholder = '';
- for (let i = 1; i < parts.length; i++) {
- const part = parts[i].trim();
- if (part.startsWith('trackBy:')) {
- // build trackby value
- const trackByFn = part.replace('trackBy:', '').trim();
- trackBy = `${trackByFn}($index, ${loopVar})`;
- }
- // template
- if (part.startsWith('template:')) {
- // use an alternate placeholder here to avoid conflicts
- tmplPlaceholder = getPlaceholder(part.split(':')[1].trim(), PlaceholderKind.Alternate);
- }
- // aliases
- // declared with `let myIndex = index`
- if (part.match(aliasWithEqualRegexp)) {
- // 'let myIndex = index' -> ['let myIndex', 'index']
- const aliasParts = part.split('=');
- const aliasedName = aliasParts[0].replace('let', '').trim();
- const originalName = aliasParts[1].trim();
- if (aliasedName !== '$' + originalName) {
- // -> 'let myIndex = $index'
- aliases.push(` let ${aliasedName} = $${originalName}`);
- }
- // if the aliased variable is the index, then we store it
- if (originalName === 'index') {
- // 'let myIndex' -> 'myIndex'
- aliasedIndex = aliasedName;
- }
- }
- // declared with `index as myIndex`
- if (part.match(aliasWithAsRegexp)) {
- // 'index as myIndex' -> ['index', 'myIndex']
- const aliasParts = part.split(/\s+as\s+/);
- const originalName = aliasParts[0].trim();
- const aliasedName = aliasParts[1].trim();
- if (aliasedName !== '$' + originalName) {
- // -> 'let myIndex = $index'
- aliases.push(` let ${aliasedName} = $${originalName}`);
- }
- // if the aliased variable is the index, then we store it
- if (originalName === 'index') {
- aliasedIndex = aliasedName;
- }
- }
- }
- // if an alias has been defined for the index, then the trackBy function must use it
- if (aliasedIndex !== null && trackBy !== loopVar) {
- // byId($index, user) -> byId(i, user)
- trackBy = trackBy.replace('$index', aliasedIndex);
- }
- const aliasStr = aliases.length > 0 ? `;${aliases.join(';')}` : '';
- let startBlock = `${startMarker}@for (${condition}; track ${trackBy}${aliasStr}) {${lbString}`;
- let endBlock = `${lbString}}${endMarker}`;
- let forBlock = '';
- if (tmplPlaceholder !== '') {
- startBlock = startBlock + tmplPlaceholder;
- forBlock = startBlock + endBlock;
- }
- else {
- const { start, middle, end } = getMainBlock(etm, tmpl, offset);
- startBlock += start;
- endBlock = end + endBlock;
- forBlock = startBlock + middle + endBlock;
- }
- const updatedTmpl = tmpl.slice(0, etm.start(offset)) + forBlock + tmpl.slice(etm.end(offset));
- const pre = originals.start.length - startBlock.length;
- const post = originals.end.length - endBlock.length;
- return { tmpl: updatedTmpl, offsets: { pre, post } };
- }
- function migrateBoundNgFor(etm, tmpl, offset) {
- const forAttrs = etm.forAttrs;
- const aliasAttrs = etm.aliasAttrs;
- const aliasMap = aliasAttrs.aliases;
- const originals = getOriginals(etm, tmpl, offset);
- const condition = `${aliasAttrs.item} of ${forAttrs.forOf}`;
- const aliases = [];
- let aliasedIndex = '$index';
- for (const [key, val] of aliasMap) {
- aliases.push(` let ${key.trim()} = $${val}`);
- if (val.trim() === 'index') {
- aliasedIndex = key;
- }
- }
- const aliasStr = aliases.length > 0 ? `;${aliases.join(';')}` : '';
- let trackBy = aliasAttrs.item;
- if (forAttrs.trackBy !== '') {
- // build trackby value
- trackBy = `${forAttrs.trackBy.trim()}(${aliasedIndex}, ${aliasAttrs.item})`;
- }
- const { start, middle, end } = getMainBlock(etm, tmpl, offset);
- const startBlock = `${startMarker}@for (${condition}; track ${trackBy}${aliasStr}) {\n${start}`;
- const endBlock = `${end}\n}${endMarker}`;
- const forBlock = startBlock + middle + endBlock;
- const updatedTmpl = tmpl.slice(0, etm.start(offset)) + forBlock + tmpl.slice(etm.end(offset));
- const pre = originals.start.length - startBlock.length;
- const post = originals.end.length - endBlock.length;
- return { tmpl: updatedTmpl, offsets: { pre, post } };
- }
- function getNgForParts(expression) {
- const parts = [];
- const commaSeparatedStack = [];
- const stringStack = [];
- let current = '';
- for (let i = 0; i < expression.length; i++) {
- const char = expression[i];
- const isInString = stringStack.length === 0;
- const isInCommaSeparated = commaSeparatedStack.length === 0;
- // Any semicolon is a delimiter, as well as any comma outside
- // of comma-separated syntax, as long as they're outside of a string.
- if (isInString &&
- current.length > 0 &&
- (char === ';' || (char === ',' && isInCommaSeparated))) {
- parts.push(current);
- current = '';
- continue;
- }
- if (stringStack.length > 0 && stringStack[stringStack.length - 1] === char) {
- stringStack.pop();
- }
- else if (stringPairs.has(char)) {
- stringStack.push(stringPairs.get(char));
- }
- if (commaSeparatedSyntax.has(char)) {
- commaSeparatedStack.push(commaSeparatedSyntax.get(char));
- }
- else if (commaSeparatedStack.length > 0 &&
- commaSeparatedStack[commaSeparatedStack.length - 1] === char) {
- commaSeparatedStack.pop();
- }
- current += char;
- }
- if (current.length > 0) {
- parts.push(current);
- }
- return parts;
- }
- const ngif = '*ngIf';
- const boundngif = '[ngIf]';
- const nakedngif = 'ngIf';
- const ifs = [ngif, nakedngif, boundngif];
- /**
- * Replaces structural directive ngif instances with new if.
- * Returns null if the migration failed (e.g. there was a syntax error).
- */
- function migrateIf(template) {
- let errors = [];
- let parsed = parseTemplate(template);
- if (parsed.tree === undefined) {
- return { migrated: template, errors, changed: false };
- }
- let result = template;
- const visitor = new ElementCollector(ifs);
- checker.visitAll(visitor, parsed.tree.rootNodes);
- calculateNesting(visitor, hasLineBreaks(template));
- // this tracks the character shift from different lengths of blocks from
- // the prior directives so as to adjust for nested block replacement during
- // migration. Each block calculates length differences and passes that offset
- // to the next migrating block to adjust character offsets properly.
- let offset = 0;
- let nestLevel = -1;
- let postOffsets = [];
- for (const el of visitor.elements) {
- let migrateResult = { tmpl: result, offsets: { pre: 0, post: 0 } };
- // applies the post offsets after closing
- offset = reduceNestingOffset(el, nestLevel, offset, postOffsets);
- try {
- migrateResult = migrateNgIf(el, result, offset);
- }
- catch (error) {
- errors.push({ type: ngif, error });
- }
- result = migrateResult.tmpl;
- offset += migrateResult.offsets.pre;
- postOffsets.push(migrateResult.offsets.post);
- nestLevel = el.nestCount;
- }
- const changed = visitor.elements.length > 0;
- return { migrated: result, errors, changed };
- }
- function migrateNgIf(etm, tmpl, offset) {
- const matchThen = etm.attr.value.match(/[^\w\d];?\s*then/gm);
- const matchElse = etm.attr.value.match(/[^\w\d];?\s*else/gm);
- if (etm.thenAttr !== undefined || etm.elseAttr !== undefined) {
- // bound if then / if then else
- return buildBoundIfElseBlock(etm, tmpl, offset);
- }
- else if (matchThen && matchThen.length > 0 && matchElse && matchElse.length > 0) {
- // then else
- return buildStandardIfThenElseBlock(etm, tmpl, matchThen[0], matchElse[0], offset);
- }
- else if (matchThen && matchThen.length > 0) {
- // just then
- return buildStandardIfThenBlock(etm, tmpl, matchThen[0], offset);
- }
- else if (matchElse && matchElse.length > 0) {
- // just else
- return buildStandardIfElseBlock(etm, tmpl, matchElse[0], offset);
- }
- return buildIfBlock(etm, tmpl, offset);
- }
- function buildIfBlock(etm, tmpl, offset) {
- const aliasAttrs = etm.aliasAttrs;
- const aliases = [...aliasAttrs.aliases.keys()];
- if (aliasAttrs.item) {
- aliases.push(aliasAttrs.item);
- }
- // includes the mandatory semicolon before as
- const lbString = etm.hasLineBreaks ? '\n' : '';
- let condition = etm.attr.value
- .replace(' as ', '; as ')
- // replace 'let' with 'as' whatever spaces are between ; and 'let'
- .replace(/;\s*let/g, '; as');
- if (aliases.length > 1 || (aliases.length === 1 && condition.indexOf('; as') > -1)) {
- // only 1 alias allowed
- throw new Error('Found more than one alias on your ngIf. Remove one of them and re-run the migration.');
- }
- else if (aliases.length === 1) {
- condition += `; as ${aliases[0]}`;
- }
- const originals = getOriginals(etm, tmpl, offset);
- const { start, middle, end } = getMainBlock(etm, tmpl, offset);
- const startBlock = `${startMarker}@if (${condition}) {${lbString}${start}`;
- const endBlock = `${end}${lbString}}${endMarker}`;
- const ifBlock = startBlock + middle + endBlock;
- const updatedTmpl = tmpl.slice(0, etm.start(offset)) + ifBlock + tmpl.slice(etm.end(offset));
- // this should be the difference between the starting element up to the start of the closing
- // element and the mainblock sans }
- const pre = originals.start.length - startBlock.length;
- const post = originals.end.length - endBlock.length;
- return { tmpl: updatedTmpl, offsets: { pre, post } };
- }
- function buildStandardIfElseBlock(etm, tmpl, elseString, offset) {
- // includes the mandatory semicolon before as
- const condition = etm
- .getCondition()
- .replace(' as ', '; as ')
- // replace 'let' with 'as' whatever spaces are between ; and 'let'
- .replace(/;\s*let/g, '; as');
- const elsePlaceholder = getPlaceholder(etm.getTemplateName(elseString));
- return buildIfElseBlock(etm, tmpl, condition, elsePlaceholder, offset);
- }
- function buildBoundIfElseBlock(etm, tmpl, offset) {
- const aliasAttrs = etm.aliasAttrs;
- const aliases = [...aliasAttrs.aliases.keys()];
- if (aliasAttrs.item) {
- aliases.push(aliasAttrs.item);
- }
- // includes the mandatory semicolon before as
- let condition = etm.attr.value.replace(' as ', '; as ');
- if (aliases.length > 1 || (aliases.length === 1 && condition.indexOf('; as') > -1)) {
- // only 1 alias allowed
- throw new Error('Found more than one alias on your ngIf. Remove one of them and re-run the migration.');
- }
- else if (aliases.length === 1) {
- condition += `; as ${aliases[0]}`;
- }
- const elsePlaceholder = getPlaceholder(etm.elseAttr.value.trim());
- if (etm.thenAttr !== undefined) {
- const thenPlaceholder = getPlaceholder(etm.thenAttr.value.trim());
- return buildIfThenElseBlock(etm, tmpl, condition, thenPlaceholder, elsePlaceholder, offset);
- }
- return buildIfElseBlock(etm, tmpl, condition, elsePlaceholder, offset);
- }
- function buildIfElseBlock(etm, tmpl, condition, elsePlaceholder, offset) {
- const lbString = etm.hasLineBreaks ? '\n' : '';
- const originals = getOriginals(etm, tmpl, offset);
- const { start, middle, end } = getMainBlock(etm, tmpl, offset);
- const startBlock = `${startMarker}@if (${condition}) {${lbString}${start}`;
- const elseBlock = `${end}${lbString}} @else {${lbString}`;
- const postBlock = elseBlock + elsePlaceholder + `${lbString}}${endMarker}`;
- const ifElseBlock = startBlock + middle + postBlock;
- const tmplStart = tmpl.slice(0, etm.start(offset));
- const tmplEnd = tmpl.slice(etm.end(offset));
- const updatedTmpl = tmplStart + ifElseBlock + tmplEnd;
- const pre = originals.start.length - startBlock.length;
- const post = originals.end.length - postBlock.length;
- return { tmpl: updatedTmpl, offsets: { pre, post } };
- }
- function buildStandardIfThenElseBlock(etm, tmpl, thenString, elseString, offset) {
- // includes the mandatory semicolon before as
- const condition = etm
- .getCondition()
- .replace(' as ', '; as ')
- // replace 'let' with 'as' whatever spaces are between ; and 'let'
- .replace(/;\s*let/g, '; as');
- const thenPlaceholder = getPlaceholder(etm.getTemplateName(thenString, elseString));
- const elsePlaceholder = getPlaceholder(etm.getTemplateName(elseString));
- return buildIfThenElseBlock(etm, tmpl, condition, thenPlaceholder, elsePlaceholder, offset);
- }
- function buildStandardIfThenBlock(etm, tmpl, thenString, offset) {
- // includes the mandatory semicolon before as
- const condition = etm
- .getCondition()
- .replace(' as ', '; as ')
- // replace 'let' with 'as' whatever spaces are between ; and 'let'
- .replace(/;\s*let/g, '; as');
- const thenPlaceholder = getPlaceholder(etm.getTemplateName(thenString));
- return buildIfThenBlock(etm, tmpl, condition, thenPlaceholder, offset);
- }
- function buildIfThenElseBlock(etm, tmpl, condition, thenPlaceholder, elsePlaceholder, offset) {
- const lbString = etm.hasLineBreaks ? '\n' : '';
- const originals = getOriginals(etm, tmpl, offset);
- const startBlock = `${startMarker}@if (${condition}) {${lbString}`;
- const elseBlock = `${lbString}} @else {${lbString}`;
- const postBlock = thenPlaceholder + elseBlock + elsePlaceholder + `${lbString}}${endMarker}`;
- const ifThenElseBlock = startBlock + postBlock;
- const tmplStart = tmpl.slice(0, etm.start(offset));
- const tmplEnd = tmpl.slice(etm.end(offset));
- const updatedTmpl = tmplStart + ifThenElseBlock + tmplEnd;
- // We ignore the contents of the element on if then else.
- // If there's anything there, we need to account for the length in the offset.
- const pre = originals.start.length + originals.childLength - startBlock.length;
- const post = originals.end.length - postBlock.length;
- return { tmpl: updatedTmpl, offsets: { pre, post } };
- }
- function buildIfThenBlock(etm, tmpl, condition, thenPlaceholder, offset) {
- const lbString = etm.hasLineBreaks ? '\n' : '';
- const originals = getOriginals(etm, tmpl, offset);
- const startBlock = `${startMarker}@if (${condition}) {${lbString}`;
- const postBlock = thenPlaceholder + `${lbString}}${endMarker}`;
- const ifThenBlock = startBlock + postBlock;
- const tmplStart = tmpl.slice(0, etm.start(offset));
- const tmplEnd = tmpl.slice(etm.end(offset));
- const updatedTmpl = tmplStart + ifThenBlock + tmplEnd;
- // We ignore the contents of the element on if then else.
- // If there's anything there, we need to account for the length in the offset.
- const pre = originals.start.length + originals.childLength - startBlock.length;
- const post = originals.end.length - postBlock.length;
- return { tmpl: updatedTmpl, offsets: { pre, post } };
- }
- const ngswitch = '[ngSwitch]';
- const switches = [ngswitch];
- /**
- * Replaces structural directive ngSwitch instances with new switch.
- * Returns null if the migration failed (e.g. there was a syntax error).
- */
- function migrateSwitch(template) {
- let errors = [];
- let parsed = parseTemplate(template);
- if (parsed.tree === undefined) {
- return { migrated: template, errors, changed: false };
- }
- let result = template;
- const visitor = new ElementCollector(switches);
- checker.visitAll(visitor, parsed.tree.rootNodes);
- calculateNesting(visitor, hasLineBreaks(template));
- // this tracks the character shift from different lengths of blocks from
- // the prior directives so as to adjust for nested block replacement during
- // migration. Each block calculates length differences and passes that offset
- // to the next migrating block to adjust character offsets properly.
- let offset = 0;
- let nestLevel = -1;
- let postOffsets = [];
- for (const el of visitor.elements) {
- let migrateResult = { tmpl: result, offsets: { pre: 0, post: 0 } };
- // applies the post offsets after closing
- offset = reduceNestingOffset(el, nestLevel, offset, postOffsets);
- if (el.attr.name === ngswitch) {
- try {
- migrateResult = migrateNgSwitch(el, result, offset);
- }
- catch (error) {
- errors.push({ type: ngswitch, error });
- }
- }
- result = migrateResult.tmpl;
- offset += migrateResult.offsets.pre;
- postOffsets.push(migrateResult.offsets.post);
- nestLevel = el.nestCount;
- }
- const changed = visitor.elements.length > 0;
- return { migrated: result, errors, changed };
- }
- function assertValidSwitchStructure(children) {
- for (const child of children) {
- if (child instanceof checker.Text && child.value.trim() !== '') {
- throw new Error(`Text node: "${child.value}" would result in invalid migrated @switch block structure. ` +
- `@switch can only have @case or @default as children.`);
- }
- else if (child instanceof checker.Element) {
- let hasCase = false;
- for (const attr of child.attrs) {
- if (cases.includes(attr.name)) {
- hasCase = true;
- }
- }
- if (!hasCase) {
- throw new Error(`Element node: "${child.name}" would result in invalid migrated @switch block structure. ` +
- `@switch can only have @case or @default as children.`);
- }
- }
- }
- }
- function migrateNgSwitch(etm, tmpl, offset) {
- const lbString = etm.hasLineBreaks ? '\n' : '';
- const condition = etm.attr.value;
- const originals = getOriginals(etm, tmpl, offset);
- assertValidSwitchStructure(originals.childNodes);
- const { start, middle, end } = getMainBlock(etm, tmpl, offset);
- const startBlock = `${startMarker}${start}${lbString}@switch (${condition}) {`;
- const endBlock = `}${lbString}${end}${endMarker}`;
- const switchBlock = startBlock + middle + endBlock;
- const updatedTmpl = tmpl.slice(0, etm.start(offset)) + switchBlock + tmpl.slice(etm.end(offset));
- // this should be the difference between the starting element up to the start of the closing
- // element and the mainblock sans }
- const pre = originals.start.length - startBlock.length;
- const post = originals.end.length - endBlock.length;
- return { tmpl: updatedTmpl, offsets: { pre, post } };
- }
- /**
- * Actually migrates a given template to the new syntax
- */
- function migrateTemplate(template, templateType, node, file, format = true, analyzedFiles) {
- let errors = [];
- let migrated = template;
- if (templateType === 'template' || templateType === 'templateUrl') {
- const ifResult = migrateIf(template);
- const forResult = migrateFor(ifResult.migrated);
- const switchResult = migrateSwitch(forResult.migrated);
- if (switchResult.errors.length > 0) {
- return { migrated: template, errors: switchResult.errors };
- }
- const caseResult = migrateCase(switchResult.migrated);
- const templateResult = processNgTemplates(caseResult.migrated, file.sourceFile);
- if (templateResult.err !== undefined) {
- return { migrated: template, errors: [{ type: 'template', error: templateResult.err }] };
- }
- migrated = templateResult.migrated;
- const changed = ifResult.changed || forResult.changed || switchResult.changed || caseResult.changed;
- if (changed) {
- // determine if migrated template is a valid structure
- // if it is not, fail out
- const errors = validateMigratedTemplate(migrated, file.sourceFile.fileName);
- if (errors.length > 0) {
- return { migrated: template, errors };
- }
- }
- if (format && changed) {
- migrated = formatTemplate(migrated, templateType);
- }
- const markerRegex = new RegExp(`${startMarker}|${endMarker}|${startI18nMarker}|${endI18nMarker}`, 'gm');
- migrated = migrated.replace(markerRegex, '');
- file.removeCommonModule = canRemoveCommonModule(template);
- file.canRemoveImports = true;
- // when migrating an external template, we have to pass back
- // whether it's safe to remove the CommonModule to the
- // original component class source file
- if (templateType === 'templateUrl' &&
- analyzedFiles !== null &&
- analyzedFiles.has(file.sourceFile.fileName)) {
- const componentFile = analyzedFiles.get(file.sourceFile.fileName);
- componentFile.getSortedRanges();
- // we have already checked the template file to see if it is safe to remove the imports
- // and common module. This check is passed off to the associated .ts file here so
- // the class knows whether it's safe to remove from the template side.
- componentFile.removeCommonModule = file.removeCommonModule;
- componentFile.canRemoveImports = file.canRemoveImports;
- // At this point, we need to verify the component class file doesn't have any other imports
- // that prevent safe removal of common module. It could be that there's an associated ngmodule
- // and in that case we can't safely remove the common module import.
- componentFile.verifyCanRemoveImports();
- }
- file.verifyCanRemoveImports();
- errors = [
- ...ifResult.errors,
- ...forResult.errors,
- ...switchResult.errors,
- ...caseResult.errors,
- ];
- }
- else if (file.canRemoveImports) {
- migrated = removeImports(template, node, file);
- }
- return { migrated, errors };
- }
- function migrate(options) {
- return async (tree, context) => {
- const basePath = process.cwd();
- const pathToMigrate = compiler_host.normalizePath(p.join(basePath, options.path));
- let allPaths = [];
- if (pathToMigrate.trim() !== '') {
- allPaths.push(pathToMigrate);
- }
- if (!allPaths.length) {
- throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run the control flow migration.');
- }
- let errors = [];
- for (const tsconfigPath of allPaths) {
- const migrateErrors = runControlFlowMigration(tree, tsconfigPath, basePath, pathToMigrate, options);
- errors = [...errors, ...migrateErrors];
- }
- if (errors.length > 0) {
- context.logger.warn(`WARNING: ${errors.length} errors occurred during your migration:\n`);
- errors.forEach((err) => {
- context.logger.warn(err);
- });
- }
- };
- }
- function runControlFlowMigration(tree, tsconfigPath, basePath, pathToMigrate, schematicOptions) {
- if (schematicOptions.path.startsWith('..')) {
- throw new schematics.SchematicsException('Cannot run control flow migration outside of the current project.');
- }
- const program = compiler_host.createMigrationProgram(tree, tsconfigPath, basePath);
- const sourceFiles = program
- .getSourceFiles()
- .filter((sourceFile) => sourceFile.fileName.startsWith(pathToMigrate) &&
- compiler_host.canMigrateFile(basePath, sourceFile, program));
- if (sourceFiles.length === 0) {
- throw new schematics.SchematicsException(`Could not find any files to migrate under the path ${pathToMigrate}. Cannot run the control flow migration.`);
- }
- const analysis = new Map();
- const migrateErrors = new Map();
- for (const sourceFile of sourceFiles) {
- analyze(sourceFile, analysis);
- }
- // sort files with .html files first
- // this ensures class files know if it's safe to remove CommonModule
- const paths = sortFilePaths([...analysis.keys()]);
- for (const path of paths) {
- const file = analysis.get(path);
- const ranges = file.getSortedRanges();
- const relativePath = p.relative(basePath, path);
- const content = tree.readText(relativePath);
- const update = tree.beginUpdate(relativePath);
- for (const { start, end, node, type } of ranges) {
- const template = content.slice(start, end);
- const length = (end ?? content.length) - start;
- const { migrated, errors } = migrateTemplate(template, type, node, file, schematicOptions.format, analysis);
- if (migrated !== null) {
- update.remove(start, length);
- update.insertLeft(start, migrated);
- }
- if (errors.length > 0) {
- migrateErrors.set(path, errors);
- }
- }
- tree.commitUpdate(update);
- }
- const errorList = [];
- for (let [template, errors] of migrateErrors) {
- errorList.push(generateErrorMessage(template, errors));
- }
- return errorList;
- }
- function sortFilePaths(names) {
- names.sort((a, _) => (a.endsWith('.html') ? -1 : 0));
- return names;
- }
- function generateErrorMessage(path, errors) {
- let errorMessage = `Template "${path}" encountered ${errors.length} errors during migration:\n`;
- errorMessage += errors.map((e) => ` - ${e.type}: ${e.error}\n`);
- return errorMessage;
- }
- exports.migrate = migrate;
|