Skip to content

Commit

Permalink
feat(material/schematics): set up a system for migrating scss
Browse files Browse the repository at this point in the history
* also implements the migrator for the button
  • Loading branch information
wagnermaciel committed Jan 31, 2022
1 parent d349bef commit 30cd65f
Show file tree
Hide file tree
Showing 8 changed files with 422 additions and 44 deletions.
2 changes: 2 additions & 0 deletions src/material/schematics/ng-generate/mdc-migration/BUILD.bazel
Expand Up @@ -17,6 +17,8 @@ ts_library(
"//src/cdk/schematics",
"@npm//@angular-devkit/schematics",
"@npm//@types/node",
"@npm//postcss",
"@npm//postcss-scss",
],
)

Expand Down
12 changes: 11 additions & 1 deletion src/material/schematics/ng-generate/mdc-migration/index.ts
Expand Up @@ -10,7 +10,9 @@ import {Rule, SchematicContext, Tree} from '@angular-devkit/schematics';
import {Schema} from './schema';
import {DevkitFileSystem, UpdateProject, findStylesheetFiles} from '@angular/cdk/schematics';
import {ThemingStylesMigration} from './rules/theming-styles';
import {MIGRATORS} from './rules';
import {dirname} from 'path';
import {StyleMigrator} from './rules/style-migrator';

/** Groups of components that must be migrated together. */
const migrationGroups = [
Expand Down Expand Up @@ -59,6 +61,14 @@ export default function (options: Schema): Rule {
console.log('Migrating:', [...componentsToMigrate]);
console.log('Directory:', migrationDir);

const migrators: StyleMigrator[] = [];
const components = [...componentsToMigrate];
for (let i = 0; i < MIGRATORS.length; i++) {
if (components.includes(MIGRATORS[i].component)) {
migrators.push(MIGRATORS[i]);
}
}

return (tree: Tree, context: SchematicContext) => {
const fileSystem = new DevkitFileSystem(tree);
const program = UpdateProject.createProgramFromTsconfig(
Expand All @@ -71,7 +81,7 @@ export default function (options: Schema): Rule {
const {hasFailures} = project.migrate(
[ThemingStylesMigration],
null,
null,
migrators,
additionalStylesheetPaths,
);

Expand Down
@@ -0,0 +1,196 @@
import {createTestApp, patchDevkitTreeToExposeTypeScript} from '@angular/cdk/schematics/testing';
import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing';
import {Schema} from '../schema';
import {runfiles} from '@bazel/runfiles';

describe('button styles', () => {
let runner: SchematicTestRunner;
let cliAppTree: UnitTestTree;
const tsconfig = '/projects/material/tsconfig.app.json';
const themeFile = '/projects/material/src/theme.scss';

async function runMigrationTest(oldFileContent: string, newFileContent: string) {
cliAppTree.create(themeFile, oldFileContent);
const tree = await migrate({tsconfig, components: ['button']});
expect(tree.readContent(themeFile)).toBe(newFileContent);
}

beforeEach(async () => {
runner = new SchematicTestRunner(
'@angular/material',
runfiles.resolveWorkspaceRelative('src/material/schematics/collection.json'),
);
cliAppTree = patchDevkitTreeToExposeTypeScript(await createTestApp(runner));
});

async function migrate(options: Schema): Promise<UnitTestTree> {
return await runner.runSchematicAsync('mdcMigration', options, cliAppTree).toPromise();
}

describe('mixin migrations', () => {
it('should replace the old theme with the new ones', async () => {
await runMigrationTest(
`
@use '@angular/material' as mat;
$theme: ();
@include mat.button-theme($theme);
`,
`
@use '@angular/material' as mat;
$theme: ();
@include mat.mdc-button-theme($theme);
@include mat.mdc-button-typography($theme);
@include mat.mdc-fab-theme($theme);
@include mat.mdc-fab-typography($theme);
@include mat.mdc-icon-theme($theme);
@include mat.mdc-icon-typography($theme);
`,
);
});

it('should use the correct namespace', async () => {
await runMigrationTest(
`
@use '@angular/material' as arbitrary;
$theme: ();
@include arbitrary.button-theme($theme);
`,
`
@use '@angular/material' as arbitrary;
$theme: ();
@include arbitrary.mdc-button-theme($theme);
@include arbitrary.mdc-button-typography($theme);
@include arbitrary.mdc-fab-theme($theme);
@include arbitrary.mdc-fab-typography($theme);
@include arbitrary.mdc-icon-theme($theme);
@include arbitrary.mdc-icon-typography($theme);
`,
);
});

it('should handle updating multiple themes', async () => {
await runMigrationTest(
`
@use '@angular/material' as mat;
$light-theme: ();
$dark-theme: ();
@include mat.button-theme($light-theme);
@include mat.button-theme($dark-theme);
`,
`
@use '@angular/material' as mat;
$light-theme: ();
$dark-theme: ();
@include mat.mdc-button-theme($light-theme);
@include mat.mdc-button-typography($light-theme);
@include mat.mdc-fab-theme($light-theme);
@include mat.mdc-fab-typography($light-theme);
@include mat.mdc-icon-theme($light-theme);
@include mat.mdc-icon-typography($light-theme);
@include mat.mdc-button-theme($dark-theme);
@include mat.mdc-button-typography($dark-theme);
@include mat.mdc-fab-theme($dark-theme);
@include mat.mdc-fab-typography($dark-theme);
@include mat.mdc-icon-theme($dark-theme);
@include mat.mdc-icon-typography($dark-theme);
`,
);
});

it('should preserve whitespace', async () => {
await runMigrationTest(
`
@use '@angular/material' as mat;
$theme: ();
@include mat.button-theme($theme);
`,
`
@use '@angular/material' as mat;
$theme: ();
@include mat.mdc-button-theme($theme);
@include mat.mdc-button-typography($theme);
@include mat.mdc-fab-theme($theme);
@include mat.mdc-fab-typography($theme);
@include mat.mdc-icon-theme($theme);
@include mat.mdc-icon-typography($theme);
`,
);
});
});

describe('selector migrations', () => {
it('should update the legacy mat-button classname', async () => {
await runMigrationTest(
`
.mat-button {
padding: 50px;
}
`,
`
.mat-mdc-button {
padding: 50px;
}
`,
);
});

it('should update multiple legacy classnames', async () => {
await runMigrationTest(
`
.mat-button {
padding: 50px;
}
.mat-button-base {
padding: 25px;
}
`,
`
.mat-mdc-button {
padding: 50px;
}
.mat-mdc-button-base {
padding: 25px;
}
`,
);
});

it('should update a legacy classname w/ multiple selectors', async () => {
await runMigrationTest(
`
.some-class.mat-button, .another-class {
padding: 50px;
}
`,
`
.some-class.mat-mdc-button, .another-class {
padding: 50px;
}
`,
);
});

it('should preserve the whitespace of multiple selectors', async () => {
await runMigrationTest(
`
.some-class,
.mat-button,
.another-class { padding: 50px; }
`,
`
.some-class,
.mat-mdc-button,
.another-class { padding: 50px; }
`,
);
});
});
});
@@ -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
*/

import {ClassNameChange, StyleMigrator} from './style-migrator';

export class ButtonStylesMigrator extends StyleMigrator {
component = 'button';

mixinChanges = [
{
old: 'button-theme',
new: [
'mdc-button-theme',
'mdc-button-typography',
'mdc-fab-theme',
'mdc-fab-typography',
'mdc-icon-theme',
'mdc-icon-typography',
],
},
];

classChanges: ClassNameChange[] = [
{old: '.mat-button-base', new: '.mat-mdc-button-base'},
{old: '.mat-button', new: '.mat-mdc-button'},
{old: '.mat-raised-button', new: '.mat-mdc-raised-button'},
{old: '.mat-icon-button', new: '.mat-mdc-icon-button'},
{old: '.mat-fab', new: '.mat-mdc-fab'},
{old: '.mat-mini-fab', new: '.mat-mdc-mini-fab'},
{old: '.mat-stroked-button', new: '.mat-mdc-outlined-button'},
{old: '.mat-flat-button', new: '.mat-mdc-flat-button'},
];
}
12 changes: 12 additions & 0 deletions src/material/schematics/ng-generate/mdc-migration/rules/index.ts
@@ -0,0 +1,12 @@
/**
* @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
*/

import {ButtonStylesMigrator} from './button-styles';
import {StyleMigrator} from './style-migrator';

export const MIGRATORS: StyleMigrator[] = [new ButtonStylesMigrator()];
@@ -0,0 +1,103 @@
/**
* @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
*/

import * as postcss from 'postcss';

/** The changes to a class names. */
export interface ClassNameChange {
/** The legacy class name. */
old: string;
/** The new class name. */
new: string;
}

/** The changes to an scss mixin. */
export interface MixinChange {
/** The name of the legacy scss mixin. */
old: string;

/** The name(s) of the new scss mixin(s). */
new: string[];
}
export abstract class StyleMigrator {
/** The name of the component that this migration handles. */
abstract component: string;

/** The old and new class names of this component. */
abstract classChanges: ClassNameChange[];

/** The old mixins and their replacements. */
abstract mixinChanges: MixinChange[];

/**
* Returns whether the given at-include at-rule is a use of a legacy mixin for this component.
*
* @param namespace the namespace being used for angular/material.
* @param atRule a postcss at-include at-rule.
* @returns `true` if the given at-rule is a use of a legacy mixin for this component.
*/
isLegacyMixin(namespace: string, atRule: postcss.AtRule): boolean {
return this.mixinChanges.some(change => atRule.params.includes(`${namespace}.${change.old}`));
}

/**
* Replaces a legacy mixin for this component with the new mixin(s).
*
* @param namespace the namespace being used for angular/material.
* @param atRule an at-include at-rule of a legacy mixin for this component.
*/
replaceMixin(namespace: string, atRule: postcss.AtRule): void {
const change = this.mixinChanges.find(c => {
return atRule.params.includes(`${namespace}.${c.old}`);
})!;

// Cloning & inserting the first node before changing the
// indentation preserves the indentation of the first node (e.g. 3 newlines).
atRule.cloneBefore({
params: atRule.params.replace(change.old, change.new[0]),
});

// We change the indentation before inserting all of the other nodes
// because the additional @includes should only be separated by a single newline.
const indentation = atRule.raws.before?.split('\n').pop();
atRule.raws.before = '\n' + indentation;

// Note: It may be more efficient to create an array of clones and then insert
// them all at once. If we are having performance issues, we should revisit this.
for (let i = 1; i < change.new.length; i++) {
atRule.cloneBefore({
params: atRule.params.replace(change.old, change.new[i]),
});
}
atRule.remove();
}

/**
* Returns whether the given postcss rule uses a legacy selector of this component.
*
* @param rule a postcss rule.
* @returns `true` if the given Rule uses a legacy selector of this component.
*/
isLegacySelector(rule: postcss.Rule): boolean {
return this.classChanges.some(change => rule.selector.includes(change.old));
}

/**
* Replaces a legacy selector of this component with the new one.
*
* @param rule a postcss rule.
*/
replaceLegacySelector(rule: postcss.Rule): void {
for (let i = 0; i < this.classChanges.length; i++) {
const change = this.classChanges[i];
if (rule.selector.includes(change.old)) {
rule.selector = rule.selector.replace(change.old, change.new);
}
}
}
}

0 comments on commit 30cd65f

Please sign in to comment.