Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(material/schematics): set up a system for migrating scss
* also implements the migrator for the button
- Loading branch information
1 parent
d349bef
commit 30cd65f
Showing
8 changed files
with
422 additions
and
44 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
196 changes: 196 additions & 0 deletions
196
src/material/schematics/ng-generate/mdc-migration/rules/button-styles.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; } | ||
`, | ||
); | ||
}); | ||
}); | ||
}); |
38 changes: 38 additions & 0 deletions
38
src/material/schematics/ng-generate/mdc-migration/rules/button-styles.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
12
src/material/schematics/ng-generate/mdc-migration/rules/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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()]; |
103 changes: 103 additions & 0 deletions
103
src/material/schematics/ng-generate/mdc-migration/rules/style-migrator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.