Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(angular/icon): add schematic for migrating deprecated icons #2032

Merged
merged 11 commits into from
Oct 4, 2023
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"build:i18n": "ts-node-script ./scripts/build.ts i18n",
"generate:bazel": "ng g .:bazel",
"generate:symbols": "ng g .:extractSymbols",
"generate:icon-list": "ts-node-script ./scripts/update-icon-names.ts",
"bazel": "bazelisk",
"bazel:buildifier": "find . -type f \\( -name \"*.bzl\" -or -name WORKSPACE -or -name BUILD -or -name BUILD.bazel \\) ! -path \"*/node_modules/*\" ! -path \"*/.git/*\" | xargs buildifier -v",
"bazel:format-lint": "yarn -s bazel:buildifier --lint=warn --mode=check",
Expand All @@ -37,7 +38,7 @@
"_shortcuts": "Below are shortcuts for common commands",
"baz": "yarn -s generate:bazel",
"sym": "yarn -s generate:symbols",
"integrity": "npm-run-all --sequential baz build:schematics sym format build:i18n"
"integrity": "npm-run-all --sequential baz build:schematics sym format build:i18n generate:icon-list"
},
"repository": {
"type": "git",
Expand Down
45 changes: 45 additions & 0 deletions scripts/update-icon-names.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { writeFileSync } from 'fs';
import https from 'https';

interface IconDefinition {
name: string;
namespace?: string;
tags: string[];
scalable?: boolean;
}

interface IconAPIResponse {
icons: IconDefinition[];
}

const CDN_ICON_URL = 'https://icons.app.sbb.ch/icons/index.json';

(async function () {
const iconNames = await fetchIconsFromUrl(CDN_ICON_URL);

writeFileSync(
'./src/angular/schematics/ng-update/migrations/sbb-icon-names.json',
`${JSON.stringify(iconNames, null, 2)}\n`,
'utf8',
);
})();

function fetchIconsFromUrl(url: string): Promise<string[]> {
return new Promise((resolve) => {
https
.get(url, (resp) => {
let data = '';
resp.on('data', (chunk) => {
data += chunk;
});
resp.on('end', () => {
const response = JSON.parse(data) as IconAPIResponse;
const icons = response.icons.map((icon) => icon.name);
resolve(icons);
});
})
.on('error', (err) => {
console.error('Error: ' + err.message);
});
});
}
8 changes: 4 additions & 4 deletions src/angular/schematics/migration.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json",
"schematics": {
"migration-v15": {
"version": "15.0.0-0",
"description": "Updates SBB Angular to v15",
"factory": "./ng-update/index#updateToV15"
"migration-v17": {
"version": "17.0.0-0",
"description": "Updates SBB Angular to v17",
"factory": "./ng-update/index#updateToV17"
}
}
}
20 changes: 4 additions & 16 deletions src/angular/schematics/ng-update/index.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,19 @@
import { Rule, SchematicContext } from '@angular-devkit/schematics';
import {
cdkMigrations,
createMigrationSchematicRule,
TargetVersion,
} from '@angular/cdk/schematics';
import { createMigrationSchematicRule, TargetVersion } from '@angular/cdk/schematics';

import { ClassNamesMigration } from './migrations/class-names';
import { IconMigration } from './migrations/icon-names';
import { sbbAngularUpgradeData } from './upgrade-data';

/** Entry point for the migration schematics with target of Angular 15 */
export function updateToV15(): Rule {
patchClassNamesMigration();
export function updateToV17(): Rule {
return createMigrationSchematicRule(
TargetVersion.V17,
[],
[IconMigration],
sbbAngularUpgradeData,
onMigrationComplete,
);
}

function patchClassNamesMigration() {
const indexOfClassNamesMigration = cdkMigrations.findIndex(
(m) => m.name === 'ClassNamesMigration',
);
cdkMigrations[indexOfClassNamesMigration] = ClassNamesMigration;
}

/** Function that will be called when the migration completed. */
function onMigrationComplete(
context: SchematicContext,
Expand Down
109 changes: 109 additions & 0 deletions src/angular/schematics/ng-update/migrations/icon-names.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { Migration, ResolvedResource, UpgradeData, WorkspacePath } from '@angular/cdk/schematics';
import * as ts from 'typescript';

/**
* Migration that walks through every string literal and template and stylesheet in
* order to migrate deprecated icon names and replace them with the new ones.
*/
export class IconMigration extends Migration<UpgradeData> {
// Only enable the migration rule if there is upgrade data.
enabled = true;

// Regex to match deprecated icons.
_deprecatedIconSelector = /(kom|fpl):([\d\w-]+)/g;

// Regex to match old icon names that contained a 'product-' prefix.
// E.g.: old = 'product-tgv', new = 'tgv'; old = 'product-re-80', new 're-80',
_productRegex = /product-(\w{1,3}-?\d*)/g;

// Mapping of old icon names to new icon names.
_iconRenames: { [key: string]: string } = {
'waves-ladder-sun-small': 'waves-ladder-small',
'waves-ladder-sun-medium': 'waves-ladder-medium',
'waves-ladder-sun-large': 'waves-ladder-large',
'adouble-chevron-small-right-small': 'double-chevron-small-right-small',
'adouble-chevron-small-right-medium': 'double-chevron-small-right-medium',
'adouble-chevron-small-right-large': 'double-chevron-small-right-large',
'lcar-power-plug-small': 'car-power-plug-small',
'lcar-power-plug-medium': 'car-power-plug-medium',
'lcar-power-plug-large': 'car-power-plug-large',
};

// List of all icon names from the SBB Icon CDN.
_iconNames: string[] = [];

override visitNode(node: ts.Node): void {
const textContent = node.getText();
if (!ts.isStringLiteralLike(node)) {
return;
}

const filePath = this.fileSystem.resolve(node.getSourceFile().fileName);
this._findAllSubstringIndices(textContent, this._deprecatedIconSelector).forEach((offset) => {
const oldName = this._getDeprecatedIconName(textContent.slice(offset));
const newName = oldName ? this._renameDeprecatedIcon(oldName) : null;

if (oldName && newName && oldName !== newName) {
this._replaceDeprecatedIconName(
filePath,
node.getStart() + offset,
oldName.length,
newName,
);
}
});
}

override visitTemplate(template: ResolvedResource): void {
this._findAllSubstringIndices(template.content, this._deprecatedIconSelector).forEach(
(offset) => {
const oldName = this._getDeprecatedIconName(template.content.slice(offset));
const newName = oldName ? this._renameDeprecatedIcon(oldName) : null;

if (oldName && newName && oldName !== newName) {
this._replaceDeprecatedIconName(
template.filePath,
template.start + offset,
oldName.length,
this._renameDeprecatedIcon(oldName),
);
}
},
);
}

private _getDeprecatedIconName(content: string) {
const match = this._deprecatedIconSelector.exec(content);
return match ? match[0] : null;
}

private _renameDeprecatedIcon(name: string): string {
this._iconNames = this._iconNames.length ? this._iconNames : require('./sbb-icon-names.json');
let newName = name.replace(this._deprecatedIconSelector, '$2');
if (newName.match(this._productRegex)) {
newName = newName.replace(this._productRegex, '$1');
}
if (this._iconRenames[newName]) {
newName = this._iconRenames[newName];
}
return this._iconNames.includes(newName) ? newName : name;
}

private _replaceDeprecatedIconName(
filePath: WorkspacePath,
start: number,
length: number,
newName: string,
) {
this.fileSystem.edit(filePath).remove(start, length).insertRight(start, newName);
}

private _findAllSubstringIndices(input: string, search: RegExp): number[] {
let match: RegExpMatchArray | null = null;
const result: number[] = [];
while ((match = search.exec(input)) !== null) {
result.push(match!.index!);
}
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@
"getPackageVersionFromPackageJson": "schematics",
"getProjectName": "schematics",
"hasNgModuleProvider": "schematics",
"IconMigration": "schematics",
"inputNames": "schematics",
"isSbbAngularExportDeclaration": "schematics",
"isSbbAngularImportDeclaration": "schematics",
Expand All @@ -424,7 +425,7 @@
"SecondaryEntryPointsMigration": "schematics",
"symbolRemoval": "schematics",
"TYPOGRAPHY_CSS_PATH": "schematics",
"updateToV15": "schematics",
"updateToV17": "schematics",
"_patchTypeScriptDefaultLib": "schematics/testing",
"createFileSystemTestApp": "schematics/testing",
"createTestApp": "schematics/testing",
Expand Down
Loading
Loading