Skip to content

Commit

Permalink
fix(google-maps): add schematic to switch to the new clusterer name
Browse files Browse the repository at this point in the history
Since the clusterer's class and tag were renamed, we need to migrated existing users to the new name.
  • Loading branch information
crisbeto committed Oct 17, 2024
1 parent c70aae1 commit b9deeee
Show file tree
Hide file tree
Showing 8 changed files with 461 additions and 1 deletion.
7 changes: 6 additions & 1 deletion src/google-maps/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,10 @@
},
"sideEffects": false,
"schematics": "./schematics/collection.json",
"ng-update": {}
"ng-update": {
"migrations": "./schematics/migration.json",
"packageGroup": [
"@angular/google-maps"
]
}
}
1 change: 1 addition & 0 deletions src/google-maps/schematics/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@ pkg_npm(
deps = [
":schematics",
":schematics_assets",
"//src/google-maps/schematics/ng-update:ng_update_index",
],
)
9 changes: 9 additions & 0 deletions src/google-maps/schematics/migration.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"schematics": {
"migration-v19": {
"version": "19.0.0-0",
"description": "Updates the Angular Google Maps package to v19",
"factory": "./ng-update/index_bundled#updateToV19"
}
}
}
79 changes: 79 additions & 0 deletions src/google-maps/schematics/ng-update/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
load("//tools:defaults.bzl", "esbuild", "jasmine_node_test", "spec_bundle", "ts_library")

## THIS ONE IS ESM
# By default everything is ESM
# ESBUild needs ESM for bundling. Cannot reliably use CJS as input.
ts_library(
name = "ng_update_lib",
srcs = glob(
["**/*.ts"],
exclude = ["**/*.spec.ts"],
),
# Schematics can not yet run in ESM module. For now we continue to use CommonJS.
# TODO(ESM): remove this once the Angular CLI supports ESM schematics.
devmode_module = "commonjs",
deps = [
"@npm//@angular-devkit/core",
"@npm//@angular-devkit/schematics",
"@npm//@schematics/angular",
"@npm//@types/node",
"@npm//typescript",
],
)

esbuild(
name = "ng_update_index",
entry_point = ":index.ts",
external = [
"@schematics/angular",
"@angular-devkit/schematics",
"@angular-devkit/core",
"typescript",
],
# TODO: Switch to ESM when Angular CLI supports it.
format = "cjs",
output = "index_bundled.js",
platform = "node",
target = "es2015",
visibility = ["//src/google-maps/schematics:__pkg__"],
deps = [":ng_update_lib"],
)

ts_library(
name = "test_lib",
testonly = True,
srcs = glob(["**/*.spec.ts"]),
deps = [
":ng_update_lib",
"@npm//@angular-devkit/core",
"@npm//@angular-devkit/schematics",
"@npm//@bazel/runfiles",
"@npm//@types/jasmine",
"@npm//@types/node",
"@npm//@types/shelljs",
],
)

spec_bundle(
name = "spec_bundle",
external = [
"*/paths.js",
"shelljs",
"@angular-devkit/core/node",
],
platform = "cjs-legacy",
target = "es2020",
deps = [":test_lib"],
)

jasmine_node_test(
name = "test",
data = [
":ng_update_index",
"//src/google-maps/schematics:schematics_assets",
"@npm//shelljs",
],
deps = [
":spec_bundle",
],
)
141 changes: 141 additions & 0 deletions src/google-maps/schematics/ng-update/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import {Path} from '@angular-devkit/core';
import {Rule, Tree} from '@angular-devkit/schematics';
import ts from 'typescript';

/** Tag name of the clusterer component. */
const TAG_NAME = 'map-marker-clusterer';

/** Module from which the clusterer is being imported. */
const MODULE_NAME = '@angular/google-maps';

/** Old name of the clusterer class. */
const CLASS_NAME = 'MapMarkerClusterer';

/** New name of the clusterer class. */
const DEPRECATED_CLASS_NAME = 'DeprecatedMapMarkerClusterer';

/** Entry point for the migration schematics with target of Angular Material v19 */
export function updateToV19(): Rule {
return tree => {
tree.visit(path => {
if (path.endsWith('.html')) {
const content = tree.readText(path);

if (content.includes('<' + TAG_NAME)) {
tree.overwrite(path, migrateHtml(content));
}
} else if (path.endsWith('.ts') && !path.endsWith('.d.ts')) {
migrateTypeScript(path, tree);
}
});
};
}

/** Migrates an HTML template from the old tag name to the new one. */
function migrateHtml(content: string): string {
return content
.replace(/<map-marker-clusterer/g, '<deprecated-map-marker-clusterer')
.replace(/<\/map-marker-clusterer/g, '</deprecated-map-marker-clusterer');
}

/** Migrates a TypeScript file from the old tag and class names to the new ones. */
function migrateTypeScript(path: Path, tree: Tree) {
const content = tree.readText(path);

// Exit early if none of the symbols we're looking for are mentioned.
if (
!content.includes('<' + TAG_NAME) &&
!content.includes(MODULE_NAME) &&
!content.includes(CLASS_NAME)
) {
return;
}

const sourceFile = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true);
const toMigrate = findTypeScriptNodesToMigrate(sourceFile);

if (toMigrate.length === 0) {
return;
}

const printer = ts.createPrinter();
const update = tree.beginUpdate(path);

for (const node of toMigrate) {
let replacement: ts.Node;

if (ts.isStringLiteralLike(node)) {
// Strings should be migrated as if they're HTML.
if (ts.isStringLiteral(node)) {
replacement = ts.factory.createStringLiteral(
migrateHtml(node.text),
node.getText()[0] === `'`,
);
} else {
replacement = ts.factory.createNoSubstitutionTemplateLiteral(migrateHtml(node.text));
}
} else {
// Imports/exports should preserve the old name, but import the clusterer using the new one.
const propertyName = ts.factory.createIdentifier(DEPRECATED_CLASS_NAME);
const name = node.name as ts.Identifier;

replacement = ts.isImportSpecifier(node)
? ts.factory.updateImportSpecifier(node, node.isTypeOnly, propertyName, name)
: ts.factory.updateExportSpecifier(node, node.isTypeOnly, propertyName, name);
}

update
.remove(node.getStart(), node.getWidth())
.insertLeft(
node.getStart(),
printer.printNode(ts.EmitHint.Unspecified, replacement, sourceFile),
);
}

tree.commitUpdate(update);
}

/** Finds the TypeScript nodes that need to be migrated from a specific file. */
function findTypeScriptNodesToMigrate(sourceFile: ts.SourceFile) {
const results: (ts.StringLiteralLike | ts.ImportSpecifier | ts.ExportSpecifier)[] = [];

sourceFile.forEachChild(function walk(node) {
// Most likely a template using the clusterer.
if (ts.isStringLiteral(node) && node.text.includes('<' + TAG_NAME)) {
results.push(node);
} else if (
// Import/export referencing the clusterer.
(ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
node.moduleSpecifier &&
ts.isStringLiteralLike(node.moduleSpecifier) &&
node.moduleSpecifier.text === MODULE_NAME
) {
const bindings = ts.isImportDeclaration(node)
? node.importClause?.namedBindings
: node.exportClause;

if (bindings && (ts.isNamedImports(bindings) || ts.isNamedExports(bindings))) {
bindings.elements.forEach(element => {
const symbolName = element.propertyName || element.name;

if (ts.isIdentifier(symbolName) && symbolName.text === CLASS_NAME) {
results.push(element);
}
});
}
} else {
node.forEachChild(walk);
}
});

// Sort the results in reverse order to make applying the updates easier.
return results.sort((a, b) => b.getStart() - a.getStart());
}
7 changes: 7 additions & 0 deletions src/google-maps/schematics/ng-update/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"module": "esnext",
"target": "es2015"
}
}
Loading

0 comments on commit b9deeee

Please sign in to comment.