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