Skip to content

Commit

Permalink
feat(material/schematics): v15 ng-update scss migration (#25395)
Browse files Browse the repository at this point in the history
* feat(material/schematics): v15 ng-update scss migration

* fixup! feat(material/schematics): v15 ng-update scss migration

* fixup! feat(material/schematics): v15 ng-update scss migration
  • Loading branch information
wagnermaciel committed Aug 8, 2022
1 parent c7c9a25 commit 00d5f27
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 15 deletions.
2 changes: 2 additions & 0 deletions src/material/schematics/ng-update/BUILD.bazel
Expand Up @@ -20,6 +20,8 @@ ts_library(
"@npm//@angular-devkit/schematics",
"@npm//@schematics/angular",
"@npm//@types/node",
"@npm//postcss",
"@npm//postcss-scss",
"@npm//typescript",
],
)
Expand Down
@@ -0,0 +1,38 @@
/**
* @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.io/license
*/

export const COMPONENTS = [
'autocomplete',
'button',
'card',
'checkbox',
'chips',
'dialog',
'form-field',
'input',
'menu',
'option',
'optgroup',
'paginator',
'progress-bar',
'progress-spinner',
'radio',
'select',
'slide-toggle',
'snack-bar',
'table',
'tabs',
'tooltip',
];

export const MIXINS = COMPONENTS.flatMap(component => [
`${component}-theme`,
`${component}-color`,
`${component}-density`,
`${component}-typography`,
]);
Expand Up @@ -6,12 +6,68 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Migration, TargetVersion} from '@angular/cdk/schematics';
import * as ts from 'typescript';
import * as postcss from 'postcss';
import * as scss from 'postcss-scss';

import {MIXINS} from './constants';

import {Migration, ResolvedResource, TargetVersion, WorkspacePath} from '@angular/cdk/schematics';

export class LegacyComponentsMigration extends Migration<null> {
enabled = this.targetVersion === TargetVersion.V15;

override visitStylesheet(stylesheet: ResolvedResource): void {
let namespace: string | undefined = undefined;
const processor = new postcss.Processor([
{
postcssPlugin: 'legacy-components-v15-plugin',
AtRule: {
use: node => {
namespace = namespace ?? this._parseSassNamespace(node);
},
include: node => this._handleAtInclude(node, stylesheet.filePath, namespace),
},
},
]);
processor.process(stylesheet.content, {syntax: scss}).sync();
}

/** Returns the namespace of the given at-rule if it is importing from @angular/material. */
private _parseSassNamespace(node: postcss.AtRule): string | undefined {
if (node.params.startsWith('@angular/material', 1)) {
return node.params.split(/\s+/).pop();
}
return;
}

/** Handles updating the at-include rules of legacy component mixins. */
private _handleAtInclude(
node: postcss.AtRule,
filePath: WorkspacePath,
namespace?: string,
): void {
if (!namespace || !node.source?.start) {
return;
}
if (this._isLegacyMixin(node, namespace)) {
this._replaceAt(filePath, node.source.start.offset, {
old: `${namespace}.`,
new: `${namespace}.legacy-`,
});
}
}

/** Returns true if the given at-include rule is a use of a legacy component mixin. */
private _isLegacyMixin(node: postcss.AtRule, namespace: string): boolean {
for (let i = 0; i < MIXINS.length; i++) {
if (node.params.startsWith(`${namespace}.${MIXINS[i]}`)) {
return true;
}
}
return false;
}

override visitNode(node: ts.Node): void {
if (ts.isImportDeclaration(node)) {
this._handleImportDeclaration(node);
Expand Down Expand Up @@ -40,7 +96,7 @@ export class LegacyComponentsMigration extends Migration<null> {
const newExport = n.propertyName
? `MatLegacy${suffix}`
: `MatLegacy${suffix}: Mat${suffix}`;
this._replaceAt(name, {old: oldExport, new: newExport});
this._tsReplaceAt(name, {old: oldExport, new: newExport});
}
}
}
Expand All @@ -49,7 +105,7 @@ export class LegacyComponentsMigration extends Migration<null> {
private _handleImportDeclaration(node: ts.ImportDeclaration): void {
const moduleSpecifier = node.moduleSpecifier as ts.StringLiteral;
if (moduleSpecifier.text.startsWith('@angular/material/')) {
this._replaceAt(node, {old: '@angular/material/', new: '@angular/material/legacy-'});
this._tsReplaceAt(node, {old: '@angular/material/', new: '@angular/material/legacy-'});

if (node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) {
this._handleNamedImportBindings(node.importClause.namedBindings);
Expand All @@ -61,7 +117,7 @@ export class LegacyComponentsMigration extends Migration<null> {
private _handleImportExpression(node: ts.CallExpression): void {
const moduleSpecifier = node.arguments[0] as ts.StringLiteral;
if (moduleSpecifier.text.startsWith('@angular/material/')) {
this._replaceAt(node, {old: '@angular/material/', new: '@angular/material/legacy-'});
this._tsReplaceAt(node, {old: '@angular/material/', new: '@angular/material/legacy-'});
}
}

Expand All @@ -75,7 +131,7 @@ export class LegacyComponentsMigration extends Migration<null> {
const newExport = n.propertyName
? `MatLegacy${suffix}`
: `MatLegacy${suffix} as Mat${suffix}`;
this._replaceAt(name, {old: oldExport, new: newExport});
this._tsReplaceAt(name, {old: oldExport, new: newExport});
}
}

Expand Down Expand Up @@ -108,10 +164,19 @@ export class LegacyComponentsMigration extends Migration<null> {
);
}

/** Updates the source file of the given node with the given replacements. */
private _replaceAt(node: ts.Node, str: {old: string; new: string}): void {
/** Updates the source file of the given ts node with the given replacements. */
private _tsReplaceAt(node: ts.Node, str: {old: string; new: string}): void {
const filePath = this.fileSystem.resolve(node.getSourceFile().fileName);
const index = this.fileSystem.read(filePath)!.indexOf(str.old, node.pos);
this._replaceAt(filePath, node.pos, str);
}

/** Updates the source file with the given replacements. */
private _replaceAt(
filePath: WorkspacePath,
offset: number,
str: {old: string; new: string},
): void {
const index = this.fileSystem.read(filePath)!.indexOf(str.old, offset);
this.fileSystem.edit(filePath).remove(index, str.old.length).insertRight(index, str.new);
}
}
@@ -1,6 +1,7 @@
import {UnitTestTree} from '@angular-devkit/schematics/testing';
import {createTestCaseSetup} from '@angular/cdk/schematics/testing';
import {join} from 'path';
import {COMPONENTS} from '../../migrations/legacy-components-v15/constants';
import {MIGRATION_PATH} from '../../../paths';

const PROJECT_ROOT_DIR = '/projects/cdk-testing';
Expand All @@ -13,8 +14,14 @@ describe('v15 legacy components migration', () => {
/** Writes an single line file. */
let writeLine: (path: string, line: string) => void;

/** Reads a file. */
let readFile: (path: string) => string;
/** Writes multiple lines to a file. */
let writeLines: (path: string, lines: string[]) => void;

/** Reads a single line file. */
let readLine: (path: string) => string;

/** Reads multiple lines from a file. */
let readLines: (path: string) => string[];

/** Runs the v15 migration on the test application. */
let runMigration: () => Promise<{logOutput: string}>;
Expand All @@ -23,15 +30,17 @@ describe('v15 legacy components migration', () => {
const testSetup = await createTestCaseSetup('migration-v15', MIGRATION_PATH, []);
tree = testSetup.appTree;
runMigration = testSetup.runFixers;
readFile = (path: string) => tree.readContent(path);
readLine = (path: string) => tree.readContent(path);
readLines = (path: string) => tree.readContent(path).split('\n');
writeLine = (path: string, lines: string) => testSetup.writeFile(path, lines);
writeLines = (path: string, lines: string[]) => testSetup.writeFile(path, lines.join('\n'));
});

describe('typescript migrations', () => {
async function runTypeScriptMigrationTest(ctx: string, opts: {old: string; new: string}) {
writeLine(TS_FILE_PATH, opts.old);
await runMigration();
expect(readFile(TS_FILE_PATH)).withContext(ctx).toEqual(opts.new);
expect(readLine(TS_FILE_PATH)).withContext(ctx).toEqual(opts.new);
}

it('updates import declarations', async () => {
Expand Down Expand Up @@ -74,10 +83,65 @@ describe('v15 legacy components migration', () => {
});

describe('style migrations', () => {
it('should do nothing yet', async () => {
writeLine(THEME_FILE_PATH, ' ');
async function runSassMigrationTest(ctx: string, opts: {old: string[]; new: string[]}) {
writeLines(THEME_FILE_PATH, opts.old);
await runMigration();
expect(readFile(THEME_FILE_PATH)).toEqual(' ');
expect(readLines(THEME_FILE_PATH)).withContext(ctx).toEqual(opts.new);
}

it('updates all mixins', async () => {
const oldFile: string[] = [`@use '@angular/material' as mat;`];
const newFile: string[] = [`@use '@angular/material' as mat;`];
for (let i = 0; i < COMPONENTS.length; i++) {
oldFile.push(
...[
`@include mat.${COMPONENTS[i]}-theme($theme);`,
`@include mat.${COMPONENTS[i]}-color($theme);`,
`@include mat.${COMPONENTS[i]}-density($theme);`,
`@include mat.${COMPONENTS[i]}-typography($theme);`,
],
);
newFile.push(
...[
`@include mat.legacy-${COMPONENTS[i]}-theme($theme);`,
`@include mat.legacy-${COMPONENTS[i]}-color($theme);`,
`@include mat.legacy-${COMPONENTS[i]}-density($theme);`,
`@include mat.legacy-${COMPONENTS[i]}-typography($theme);`,
],
);
}
await runSassMigrationTest('all components', {
old: oldFile,
new: newFile,
});
await runSassMigrationTest('w/ unique namespaces', {
old: [`@use '@angular/material' as material;`, `@include material.button-theme($theme);`],
new: [
`@use '@angular/material' as material;`,
`@include material.legacy-button-theme($theme);`,
],
});
await runSassMigrationTest('w/ unique whitespace', {
old: [
` @use '@angular/material' as material ; `,
` @include material.button-theme( $theme ) ; `,
],
new: [
` @use '@angular/material' as material ; `,
` @include material.legacy-button-theme( $theme ) ; `,
],
});
});

it('does not update non-mdc component mixins', async () => {
await runSassMigrationTest('datepicker', {
old: [`@use '@angular/material' as mat;`, `@include mat.datepicker-theme($theme);`],
new: [`@use '@angular/material' as mat;`, `@include mat.datepicker-theme($theme);`],
});
await runSassMigrationTest('button-toggle', {
old: [`@use '@angular/material' as mat;`, `@include mat.button-toggle-theme($theme);`],
new: [`@use '@angular/material' as mat;`, `@include mat.button-toggle-theme($theme);`],
});
});
});
});

0 comments on commit 00d5f27

Please sign in to comment.