Skip to content

Commit db740bd

Browse files
committed
fix(google-maps): add schematic to switch to the new clusterer name
Since the clusterer's class and tag were renamed, we need to migrated existing users to the new name.
1 parent 0c79fe1 commit db740bd

File tree

7 files changed

+456
-1
lines changed

7 files changed

+456
-1
lines changed

src/google-maps/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,10 @@
2727
},
2828
"sideEffects": false,
2929
"schematics": "./schematics/collection.json",
30-
"ng-update": {}
30+
"ng-update": {
31+
"migrations": "./schematics/migration.json",
32+
"packageGroup": [
33+
"@angular/google-maps"
34+
]
35+
}
3136
}

src/google-maps/schematics/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,6 @@ pkg_npm(
3030
deps = [
3131
":schematics",
3232
":schematics_assets",
33+
"//src/google-maps/schematics/ng-update:ng_update_index",
3334
],
3435
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"schematics": {
3+
"migration-v19": {
4+
"version": "19.0.0-0",
5+
"description": "Updates the Angular Google Maps package to v19",
6+
"factory": "./ng-update/index_bundled#updateToV19"
7+
}
8+
}
9+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
load("//tools:defaults.bzl", "esbuild", "jasmine_node_test", "spec_bundle", "ts_library")
2+
3+
## THIS ONE IS ESM
4+
# By default everything is ESM
5+
# ESBUild needs ESM for bundling. Cannot reliably use CJS as input.
6+
ts_library(
7+
name = "ng_update_lib",
8+
srcs = glob(
9+
["**/*.ts"],
10+
exclude = ["**/*.spec.ts"],
11+
),
12+
# Schematics can not yet run in ESM module. For now we continue to use CommonJS.
13+
# TODO(ESM): remove this once the Angular CLI supports ESM schematics.
14+
devmode_module = "commonjs",
15+
deps = [
16+
"@npm//@angular-devkit/core",
17+
"@npm//@angular-devkit/schematics",
18+
"@npm//@schematics/angular",
19+
"@npm//@types/node",
20+
"@npm//typescript",
21+
],
22+
)
23+
24+
esbuild(
25+
name = "ng_update_index",
26+
entry_point = ":index.ts",
27+
external = [
28+
"@schematics/angular",
29+
"@angular-devkit/schematics",
30+
"@angular-devkit/core",
31+
"typescript",
32+
],
33+
# TODO: Switch to ESM when Angular CLI supports it.
34+
format = "cjs",
35+
output = "index_bundled.js",
36+
platform = "node",
37+
target = "es2015",
38+
visibility = ["//src/google-maps/schematics:__pkg__"],
39+
deps = [":ng_update_lib"],
40+
)
41+
42+
ts_library(
43+
name = "test_lib",
44+
testonly = True,
45+
srcs = glob(["**/*.spec.ts"]),
46+
deps = [
47+
":ng_update_lib",
48+
"@npm//@angular-devkit/core",
49+
"@npm//@angular-devkit/schematics",
50+
"@npm//@bazel/runfiles",
51+
"@npm//@types/jasmine",
52+
"@npm//@types/node",
53+
"@npm//@types/shelljs",
54+
],
55+
)
56+
57+
spec_bundle(
58+
name = "spec_bundle",
59+
external = [
60+
"*/paths.js",
61+
"shelljs",
62+
"@angular-devkit/core/node",
63+
],
64+
platform = "cjs-legacy",
65+
target = "es2020",
66+
deps = [":test_lib"],
67+
)
68+
69+
jasmine_node_test(
70+
name = "test",
71+
data = [
72+
":ng_update_index",
73+
"//src/google-maps/schematics:schematics_assets",
74+
"@npm//shelljs",
75+
],
76+
deps = [
77+
":spec_bundle",
78+
],
79+
)
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
"compilerOptions": {
4+
"module": "esnext",
5+
"target": "es2015"
6+
}
7+
}

0 commit comments

Comments
 (0)