|
| 1 | +/** |
| 2 | + * @license |
| 3 | + * Copyright Google LLC All Rights Reserved. |
| 4 | + * |
| 5 | + * Use of this source code is governed by an MIT-style license that can be |
| 6 | + * found in the LICENSE file at https://angular.dev/license |
| 7 | + */ |
| 8 | + |
| 9 | +import {Path} from '@angular-devkit/core'; |
| 10 | +import {Rule, Tree} from '@angular-devkit/schematics'; |
| 11 | +import ts from 'typescript'; |
| 12 | + |
| 13 | +/** Tag name of the clusterer component. */ |
| 14 | +const TAG_NAME = 'map-marker-clusterer'; |
| 15 | + |
| 16 | +/** Module from which the clusterer is being imported. */ |
| 17 | +const MODULE_NAME = '@angular/google-maps'; |
| 18 | + |
| 19 | +/** Old name of the clusterer class. */ |
| 20 | +const CLASS_NAME = 'MapMarkerClusterer'; |
| 21 | + |
| 22 | +/** New name of the clusterer class. */ |
| 23 | +const DEPRECATED_CLASS_NAME = 'DeprecatedMapMarkerClusterer'; |
| 24 | + |
| 25 | +/** Entry point for the migration schematics with target of Angular Material v19 */ |
| 26 | +export function updateToV19(): Rule { |
| 27 | + return tree => { |
| 28 | + tree.visit(path => { |
| 29 | + if (path.endsWith('.html')) { |
| 30 | + const content = tree.readText(path); |
| 31 | + |
| 32 | + if (content.includes('<' + TAG_NAME)) { |
| 33 | + tree.overwrite(path, migrateHtml(content)); |
| 34 | + } |
| 35 | + } else if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { |
| 36 | + migrateTypeScript(path, tree); |
| 37 | + } |
| 38 | + }); |
| 39 | + }; |
| 40 | +} |
| 41 | + |
| 42 | +/** Migrates an HTML template from the old tag name to the new one. */ |
| 43 | +function migrateHtml(content: string): string { |
| 44 | + return content |
| 45 | + .replace(/<map-marker-clusterer/g, '<deprecated-map-marker-clusterer') |
| 46 | + .replace(/<\/map-marker-clusterer/g, '</deprecated-map-marker-clusterer'); |
| 47 | +} |
| 48 | + |
| 49 | +/** Migrates a TypeScript file from the old tag and class names to the new ones. */ |
| 50 | +function migrateTypeScript(path: Path, tree: Tree) { |
| 51 | + const content = tree.readText(path); |
| 52 | + |
| 53 | + // Exit early if none of the symbols we're looking for are mentioned. |
| 54 | + if ( |
| 55 | + !content.includes('<' + TAG_NAME) && |
| 56 | + !content.includes(MODULE_NAME) && |
| 57 | + !content.includes(CLASS_NAME) |
| 58 | + ) { |
| 59 | + return; |
| 60 | + } |
| 61 | + |
| 62 | + const sourceFile = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true); |
| 63 | + const toMigrate = findTypeScriptNodesToMigrate(sourceFile); |
| 64 | + |
| 65 | + if (toMigrate.length === 0) { |
| 66 | + return; |
| 67 | + } |
| 68 | + |
| 69 | + const printer = ts.createPrinter(); |
| 70 | + const update = tree.beginUpdate(path); |
| 71 | + |
| 72 | + for (const node of toMigrate) { |
| 73 | + let replacement: ts.Node; |
| 74 | + |
| 75 | + if (ts.isStringLiteralLike(node)) { |
| 76 | + // Strings should be migrated as if they're HTML. |
| 77 | + if (ts.isStringLiteral(node)) { |
| 78 | + replacement = ts.factory.createStringLiteral( |
| 79 | + migrateHtml(node.text), |
| 80 | + node.getText()[0] === `'`, |
| 81 | + ); |
| 82 | + } else { |
| 83 | + replacement = ts.factory.createNoSubstitutionTemplateLiteral(migrateHtml(node.text)); |
| 84 | + } |
| 85 | + } else { |
| 86 | + // Imports/exports should preserve the old name, but import the clusterer using the new one. |
| 87 | + const propertyName = ts.factory.createIdentifier(DEPRECATED_CLASS_NAME); |
| 88 | + const name = node.name as ts.Identifier; |
| 89 | + |
| 90 | + replacement = ts.isImportSpecifier(node) |
| 91 | + ? ts.factory.updateImportSpecifier(node, node.isTypeOnly, propertyName, name) |
| 92 | + : ts.factory.updateExportSpecifier(node, node.isTypeOnly, propertyName, name); |
| 93 | + } |
| 94 | + |
| 95 | + update |
| 96 | + .remove(node.getStart(), node.getWidth()) |
| 97 | + .insertLeft( |
| 98 | + node.getStart(), |
| 99 | + printer.printNode(ts.EmitHint.Unspecified, replacement, sourceFile), |
| 100 | + ); |
| 101 | + } |
| 102 | + |
| 103 | + tree.commitUpdate(update); |
| 104 | +} |
| 105 | + |
| 106 | +/** Finds the TypeScript nodes that need to be migrated from a specific file. */ |
| 107 | +function findTypeScriptNodesToMigrate(sourceFile: ts.SourceFile) { |
| 108 | + const results: (ts.StringLiteralLike | ts.ImportSpecifier | ts.ExportSpecifier)[] = []; |
| 109 | + |
| 110 | + sourceFile.forEachChild(function walk(node) { |
| 111 | + // Most likely a template using the clusterer. |
| 112 | + if (ts.isStringLiteral(node) && node.text.includes('<' + TAG_NAME)) { |
| 113 | + results.push(node); |
| 114 | + } else if ( |
| 115 | + // Import/export referencing the clusterer. |
| 116 | + (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && |
| 117 | + node.moduleSpecifier && |
| 118 | + ts.isStringLiteralLike(node.moduleSpecifier) && |
| 119 | + node.moduleSpecifier.text === MODULE_NAME |
| 120 | + ) { |
| 121 | + const bindings = ts.isImportDeclaration(node) |
| 122 | + ? node.importClause?.namedBindings |
| 123 | + : node.exportClause; |
| 124 | + |
| 125 | + if (bindings && (ts.isNamedImports(bindings) || ts.isNamedExports(bindings))) { |
| 126 | + bindings.elements.forEach(element => { |
| 127 | + const symbolName = element.propertyName || element.name; |
| 128 | + |
| 129 | + if (ts.isIdentifier(symbolName) && symbolName.text === CLASS_NAME) { |
| 130 | + results.push(element); |
| 131 | + } |
| 132 | + }); |
| 133 | + } |
| 134 | + } else { |
| 135 | + node.forEachChild(walk); |
| 136 | + } |
| 137 | + }); |
| 138 | + |
| 139 | + // Sort the results in reverse order to make applying the updates easier. |
| 140 | + return results.sort((a, b) => b.getStart() - a.getStart()); |
| 141 | +} |
0 commit comments