@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.
Files changed (30) hide show
  1. package/fesm2022/core.mjs +193 -20
  2. package/fesm2022/core.mjs.map +1 -1
  3. package/fesm2022/primitives/event-dispatch.mjs +1 -1
  4. package/fesm2022/primitives/signals.mjs +1 -1
  5. package/fesm2022/rxjs-interop.mjs +1 -1
  6. package/fesm2022/testing.mjs +4 -4
  7. package/index.d.ts +113 -4
  8. package/package.json +1 -1
  9. package/primitives/event-dispatch/index.d.ts +1 -1
  10. package/primitives/signals/index.d.ts +1 -1
  11. package/rxjs-interop/index.d.ts +1 -1
  12. package/schematics/bundles/compiler_host-ca7ba733.js +44719 -0
  13. package/schematics/bundles/control-flow-migration.js +1847 -0
  14. package/schematics/bundles/explicit-standalone-flag.js +157 -0
  15. package/schematics/bundles/imports-4ac08251.js +110 -0
  16. package/schematics/bundles/inject-migration.js +927 -0
  17. package/schematics/bundles/nodes-0e7d45ca.js +56 -0
  18. package/schematics/bundles/project_tsconfig_paths-e9ccccbf.js +90 -0
  19. package/schematics/bundles/route-lazy-loading.js +411 -0
  20. package/schematics/bundles/standalone-migration.js +22441 -0
  21. package/schematics/collection.json +7 -14
  22. package/schematics/migrations.json +4 -14
  23. package/testing/index.d.ts +1 -1
  24. package/schematics/migrations/after-render-phase/bundle.js +0 -27333
  25. package/schematics/migrations/http-providers/bundle.js +0 -27582
  26. package/schematics/migrations/invalid-two-way-bindings/bundle.js +0 -23808
  27. package/schematics/ng-generate/control-flow-migration/bundle.js +0 -28076
  28. package/schematics/ng-generate/inject-migration/bundle.js +0 -27777
  29. package/schematics/ng-generate/route-lazy-loading/bundle.js +0 -27478
  30. 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;