diff --git a/src/cdk/schematics/ng-update/find-stylesheets.ts b/src/cdk/schematics/ng-update/find-stylesheets.ts index 52c7e840c7d7..371e984b8d74 100644 --- a/src/cdk/schematics/ng-update/find-stylesheets.ts +++ b/src/cdk/schematics/ng-update/find-stylesheets.ts @@ -12,12 +12,25 @@ import {Tree} from '@angular-devkit/schematics'; /** Regular expression that matches stylesheet paths */ const STYLESHEET_REGEX = /.*\.(css|scss)/; -/** Finds stylesheets in the given directory from within the specified tree. */ -export function findStylesheetFiles(tree: Tree, baseDir: string): string[] { +/** + * Finds stylesheets in the given directory from within the specified tree. + * @param tree Devkit tree where stylesheet files can be found in. + * @param startDirectory Optional start directory where stylesheets should be searched in. + * This can be useful if only stylesheets within a given folder are relevant (to avoid + * unnecessary iterations). + */ +export function findStylesheetFiles(tree: Tree, startDirectory: string = '/'): string[] { const result: string[] = []; const visitDir = dirPath => { const {subfiles, subdirs} = tree.getDir(dirPath); - result.push(...subfiles.filter(f => STYLESHEET_REGEX.test(f))); + + subfiles.forEach(fileName => { + if (STYLESHEET_REGEX.test(fileName)) { + result.push(join(dirPath, fileName)); + } + }); + + // Visit directories within the current directory to find other stylesheets. subdirs.forEach(fragment => { // Do not visit directories or files inside node modules or `dist/` folders. if (fragment !== 'node_modules' && fragment !== 'dist') { @@ -25,6 +38,6 @@ export function findStylesheetFiles(tree: Tree, baseDir: string): string[] { } }); }; - visitDir(baseDir); + visitDir(startDirectory); return result; } diff --git a/src/material/schematics/collection.json b/src/material/schematics/collection.json index 7e7c546fef6f..0004348db9cf 100644 --- a/src/material/schematics/collection.json +++ b/src/material/schematics/collection.json @@ -42,12 +42,6 @@ "factory": "./ng-generate/address-form/index", "schema": "./ng-generate/address-form/schema.json", "aliases": ["address-form", "material-address-form", "material-addressForm"] - }, - "themingApi": { - "description": "Switch the project to the new @use-based Material theming API", - "factory": "./ng-generate/theming-api/index", - "schema": "./ng-generate/theming-api/schema.json", - "aliases": ["theming-api", "sass-api"] } } } diff --git a/src/material/schematics/ng-generate/theming-api/index.ts b/src/material/schematics/ng-generate/theming-api/index.ts deleted file mode 100644 index 73cb0ba50018..000000000000 --- a/src/material/schematics/ng-generate/theming-api/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @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 {extname} from '@angular-devkit/core'; -import {Rule, Tree} from '@angular-devkit/schematics'; -import {Schema} from './schema'; -import {migrateFileContent} from './migration'; - -export default function(_options: Schema): Rule { - return (tree: Tree) => { - tree.visit((path, entry) => { - if (extname(path) === '.scss' && path.indexOf('node_modules') === -1) { - const content = entry?.content.toString(); - const migratedContent = content ? migrateFileContent(content, - '~@angular/material/', '~@angular/cdk/', '~@angular/material', '~@angular/cdk') : content; - - if (migratedContent && migratedContent !== content) { - tree.overwrite(path, migratedContent); - } - } - }); - }; -} diff --git a/src/material/schematics/ng-generate/theming-api/schema.json b/src/material/schematics/ng-generate/theming-api/schema.json deleted file mode 100644 index e6ef39bd5bea..000000000000 --- a/src/material/schematics/ng-generate/theming-api/schema.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "SchematicsMaterialThemingApi", - "title": "Material Theming API migration", - "type": "object", - "properties": {} -} diff --git a/src/material/schematics/ng-generate/theming-api/schema.ts b/src/material/schematics/ng-generate/theming-api/schema.ts deleted file mode 100644 index 423608e83b67..000000000000 --- a/src/material/schematics/ng-generate/theming-api/schema.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @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 interface Schema {} diff --git a/src/material/schematics/ng-update/index.ts b/src/material/schematics/ng-update/index.ts index 7f0eac808259..41e47f2de066 100644 --- a/src/material/schematics/ng-update/index.ts +++ b/src/material/schematics/ng-update/index.ts @@ -24,6 +24,7 @@ import { import { SecondaryEntryPointsMigration } from './migrations/package-imports-v8/secondary-entry-points-migration'; +import {ThemingApiMigration} from './migrations/theming-api-v12/theming-api-migration'; import {materialUpgradeData} from './upgrade-data'; @@ -36,6 +37,7 @@ const materialMigrations: NullableDevkitMigration[] = [ RippleSpeedFactorMigration, SecondaryEntryPointsMigration, HammerGesturesMigration, + ThemingApiMigration, ]; /** Entry point for the migration schematics with target of Angular Material v6 */ diff --git a/src/material/schematics/ng-generate/theming-api/config.ts b/src/material/schematics/ng-update/migrations/theming-api-v12/config.ts similarity index 88% rename from src/material/schematics/ng-generate/theming-api/config.ts rename to src/material/schematics/ng-update/migrations/theming-api-v12/config.ts index aa4fc415313a..01d364e43fbc 100644 --- a/src/material/schematics/ng-generate/theming-api/config.ts +++ b/src/material/schematics/ng-update/migrations/theming-api-v12/config.ts @@ -169,6 +169,14 @@ export const removedMaterialVariables: Record = { 'mat-tree-node-height': '48px', 'mat-tree-node-minimum-height': '24px', 'mat-tree-node-maximum-height': '48px', +}; + +/** + * Material variables **without a `mat-` prefix** that have been removed from the public API + * and which should be replaced with their values. These should be migrated only when there's a + * Material import, because their names could conflict with other variables in the user's app. + */ +export const unprefixedRemovedVariables: Record = { 'z-index-fab': '20', 'z-index-drawer': '100', 'ease-in-out-curve-function': 'cubic-bezier(0.35, 0, 0.25, 1)', @@ -183,5 +191,21 @@ export const removedMaterialVariables: Record = { 'swift-ease-in-out': 'all 500ms cubic-bezier(0.35, 0, 0.25, 1)', 'swift-linear-duration': '80ms', 'swift-linear-timing-function': 'linear', - 'swift-linear': 'all 80ms linear' + 'swift-linear': 'all 80ms linear', + 'black-87-opacity': 'rgba(black, 0.87)', + 'white-87-opacity': 'rgba(white, 0.87)', + 'black-12-opacity': 'rgba(black, 0.12)', + 'white-12-opacity': 'rgba(white, 0.12)', + 'black-6-opacity': 'rgba(black, 0.06)', + 'white-6-opacity': 'rgba(white, 0.06)', + 'dark-primary-text': 'rgba(black, 0.87)', + 'dark-secondary-text': 'rgba(black, 0.54)', + 'dark-disabled-text': 'rgba(black, 0.38)', + 'dark-dividers': 'rgba(black, 0.12)', + 'dark-focused': 'rgba(black, 0.12)', + 'light-primary-text': 'white', + 'light-secondary-text': 'rgba(white, 0.7)', + 'light-disabled-text': 'rgba(white, 0.5)', + 'light-dividers': 'rgba(white, 0.12)', + 'light-focused': 'rgba(white, 0.12)', }; diff --git a/src/material/schematics/ng-generate/theming-api/migration.ts b/src/material/schematics/ng-update/migrations/theming-api-v12/migration.ts similarity index 83% rename from src/material/schematics/ng-generate/theming-api/migration.ts rename to src/material/schematics/ng-update/migrations/theming-api-v12/migration.ts index 38a971d5ba7c..3fe4a94c8605 100644 --- a/src/material/schematics/ng-generate/theming-api/migration.ts +++ b/src/material/schematics/ng-update/migrations/theming-api-v12/migration.ts @@ -12,9 +12,16 @@ import { materialVariables, cdkMixins, cdkVariables, - removedMaterialVariables + removedMaterialVariables, + unprefixedRemovedVariables } from './config'; +/** The result of a search for imports and namespaces in a file. */ +interface DetectImportResult { + imports: string[]; + namespaces: string[]; +} + /** * Migrates the content of a file to the new theming API. Note that this migration is using plain * string manipulation, rather than the AST from PostCSS and the schematics string manipulation @@ -39,14 +46,15 @@ export function migrateFileContent(content: string, // Try to migrate the symbols even if there are no imports. This is used // to cover the case where the Components symbols were used transitively. - content = migrateMaterialSymbols(content, newMaterialImportPath, materialResults.namespaces); - content = migrateCdkSymbols(content, newCdkImportPath, cdkResults.namespaces); + content = migrateMaterialSymbols(content, newMaterialImportPath, materialResults); + content = migrateCdkSymbols(content, newCdkImportPath, cdkResults); content = replaceRemovedVariables(content, removedMaterialVariables); // We can assume that the migration has taken care of any Components symbols that were // imported transitively so we can always drop the old imports. We also assume that imports // to the new entry points have been added already. if (materialResults.imports.length) { + content = replaceRemovedVariables(content, unprefixedRemovedVariables); content = removeStrings(content, materialResults.imports); } @@ -62,7 +70,7 @@ export function migrateFileContent(content: string, * @param content File content in which to look for imports. * @param prefix Prefix that the imports should start with. */ -function detectImports(content: string, prefix: string): {imports: string[], namespaces: string[]} { +function detectImports(content: string, prefix: string): DetectImportResult { if (prefix[prefix.length - 1] !== '/') { // Some of the logic further down makes assumptions about the import depth. throw Error(`Prefix "${prefix}" has to end in a slash.`); @@ -94,47 +102,49 @@ function detectImports(content: string, prefix: string): {imports: string[], nam } /** Migrates the Material symbls in a file. */ -function migrateMaterialSymbols(content: string, importPath: string, namespaces: string[]): string { +function migrateMaterialSymbols(content: string, importPath: string, + detectedImports: DetectImportResult): string { const initialContent = content; const namespace = 'mat'; // Migrate the mixins. - content = renameSymbols(content, materialMixins, namespaces, mixinKeyFormatter, + content = renameSymbols(content, materialMixins, detectedImports.namespaces, mixinKeyFormatter, getMixinValueFormatter(namespace)); // Migrate the functions. - content = renameSymbols(content, materialFunctions, namespaces, functionKeyFormatter, - getFunctionValueFormatter(namespace)); + content = renameSymbols(content, materialFunctions, detectedImports.namespaces, + functionKeyFormatter, getFunctionValueFormatter(namespace)); // Migrate the variables. - content = renameSymbols(content, materialVariables, namespaces, variableKeyFormatter, - getVariableValueFormatter(namespace)); + content = renameSymbols(content, materialVariables, detectedImports.namespaces, + variableKeyFormatter, getVariableValueFormatter(namespace)); if (content !== initialContent) { // Add an import to the new API only if any of the APIs were being used. - content = insertUseStatement(content, importPath, namespace); + content = insertUseStatement(content, importPath, detectedImports.imports, namespace); } return content; } /** Migrates the CDK symbols in a file. */ -function migrateCdkSymbols(content: string, importPath: string, namespaces: string[]): string { +function migrateCdkSymbols(content: string, importPath: string, + detectedImports: DetectImportResult): string { const initialContent = content; const namespace = 'cdk'; // Migrate the mixins. - content = renameSymbols(content, cdkMixins, namespaces, mixinKeyFormatter, + content = renameSymbols(content, cdkMixins, detectedImports.namespaces, mixinKeyFormatter, getMixinValueFormatter(namespace)); // Migrate the variables. - content = renameSymbols(content, cdkVariables, namespaces, variableKeyFormatter, + content = renameSymbols(content, cdkVariables, detectedImports.namespaces, variableKeyFormatter, getVariableValueFormatter(namespace)); // Previously the CDK symbols were exposed through `material/theming`, but now we have a // dedicated entrypoint for the CDK. Only add an import for it if any of the symbols are used. if (content !== initialContent) { - content = insertUseStatement(content, importPath, namespace); + content = insertUseStatement(content, importPath, detectedImports.imports, namespace); } return content; @@ -173,11 +183,21 @@ function renameSymbols(content: string, } /** Inserts an `@use` statement in a string. */ -function insertUseStatement(content: string, importPath: string, namespace: string): string { +function insertUseStatement(content: string, importPath: string, importsToIgnore: string[], + namespace: string): string { + // We want to find the first import that isn't in the list of ignored imports or find nothing, + // because the imports being replaced might be the only ones in the file and they can be further + // down. An easy way to do this is to replace the imports with a random character and run + // `indexOf` on the result. This isn't the most efficient way of doing it, but it's more compact + // and it allows us to easily deal with things like comment nodes. + const contentToSearch = importsToIgnore.reduce((accumulator, current) => + accumulator.replace(current, '◬'.repeat(current.length)), content); + // Sass has a limitation that all `@use` declarations have to come before `@import` so we have // to find the first import and insert before it. Technically we can get away with always // inserting at 0, but the file may start with something like a license header. - const newImportIndex = Math.max(0, content.indexOf('@import ')); + const newImportIndex = Math.max(0, contentToSearch.indexOf('@import ')); + return content.slice(0, newImportIndex) + `@use '${importPath}' as ${namespace};\n` + content.slice(newImportIndex); } diff --git a/src/material/schematics/ng-update/migrations/theming-api-v12/theming-api-migration.ts b/src/material/schematics/ng-update/migrations/theming-api-v12/theming-api-migration.ts new file mode 100644 index 000000000000..ec98653c2ec0 --- /dev/null +++ b/src/material/schematics/ng-update/migrations/theming-api-v12/theming-api-migration.ts @@ -0,0 +1,46 @@ +/** + * @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 {extname} from '@angular-devkit/core'; +import {SchematicContext} from '@angular-devkit/schematics'; +import {DevkitMigration, ResolvedResource, TargetVersion} from '@angular/cdk/schematics'; +import {migrateFileContent} from './migration'; + +/** Migration that switches all Sass files using Material theming APIs to `@use`. */ +export class ThemingApiMigration extends DevkitMigration { + /** Number of files that have been migrated. */ + static migratedFileCount = 0; + + enabled = this.targetVersion === TargetVersion.V12; + + visitStylesheet(stylesheet: ResolvedResource): void { + if (extname(stylesheet.filePath) === '.scss') { + const content = stylesheet.content; + const migratedContent = content ? migrateFileContent(content, + '~@angular/material/', '~@angular/cdk/', '~@angular/material', '~@angular/cdk') : content; + + if (migratedContent && migratedContent !== content) { + this.fileSystem.edit(stylesheet.filePath) + .remove(0, stylesheet.content.length) + .insertLeft(0, migratedContent); + ThemingApiMigration.migratedFileCount++; + } + } + } + + /** Logs out the number of migrated files at the end of the migration. */ + static globalPostMigration(_tree: unknown, context: SchematicContext): void { + const count = ThemingApiMigration.migratedFileCount; + + if (count > 0) { + context.logger.info(`Migrated ${count === 1 ? `1 file` : `${count} files`} to the ` + + `new Angular Material theming API.`); + ThemingApiMigration.migratedFileCount = 0; + } + } +} diff --git a/src/material/schematics/ng-generate/theming-api/index.spec.ts b/src/material/schematics/ng-update/test-cases/v12/misc/theming-api-v12.spec.ts similarity index 72% rename from src/material/schematics/ng-generate/theming-api/index.spec.ts rename to src/material/schematics/ng-update/test-cases/v12/misc/theming-api-v12.spec.ts index 8514da7de586..9f6a63cedab1 100644 --- a/src/material/schematics/ng-generate/theming-api/index.spec.ts +++ b/src/material/schematics/ng-update/test-cases/v12/misc/theming-api-v12.spec.ts @@ -1,19 +1,34 @@ -import {SchematicTestRunner} from '@angular-devkit/schematics/testing'; -import {createTestApp, getFileContent} from '@angular/cdk/schematics/testing'; -import {COLLECTION_PATH} from '../../paths'; -import {Schema} from './schema'; +import {UnitTestTree} from '@angular-devkit/schematics/testing'; +import {createTestCaseSetup} from '@angular/cdk/schematics/testing'; +import {join} from 'path'; +import {MIGRATION_PATH} from '../../../../paths'; + +describe('v12 theming API migration', () => { + const PROJECT_PATH = '/projects/cdk-testing'; + const THEME_PATH = join(PROJECT_PATH, 'src/theme.scss'); + let tree: UnitTestTree; + let _writeFile: (filePath: string, text: string) => void; + let runMigration: () => Promise<{logOutput: string}>; + + beforeEach(async () => { + const testSetup = await createTestCaseSetup('migration-v12', MIGRATION_PATH, []); + tree = testSetup.appTree; + runMigration = testSetup.runFixers; + _writeFile = testSetup.writeFile; + }); -describe('Material theming API schematic', () => { - const options: Schema = {}; - let runner: SchematicTestRunner; + /** Writes an array of lines as a single file. */ + function writeLines(path: string, lines: string[]): void { + _writeFile(path, lines.join('\n')); + } - beforeEach(() => { - runner = new SchematicTestRunner('schematics', COLLECTION_PATH); - }); + /** Reads a file and split it into an array where each item is a new line. */ + function splitFile(path: string): string[] { + return tree.readContent(path).split('\n'); + } it('should migrate a theme based on the theming API', async () => { - const app = await createTestApp(runner); - app.create('/theme.scss', [ + writeLines(THEME_PATH, [ `@import '~@angular/material/theming';`, `@include mat-core();`, @@ -43,10 +58,11 @@ describe('Material theming API schematic', () => { `.unicorn-dark-theme {`, `@include angular-material-color($dark-theme);`, `}` - ].join('\n')); + ]); - const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise(); - expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([ + await runMigration(); + + expect(splitFile(THEME_PATH)).toEqual([ `@use '~@angular/material' as mat;`, `@include mat.core();`, @@ -80,8 +96,7 @@ describe('Material theming API schematic', () => { }); it('should migrate files using CDK APIs through the theming import', async () => { - const app = await createTestApp(runner); - app.create('/theme.scss', [ + writeLines(THEME_PATH, [ `@import '~@angular/material/theming';`, ``, `@include cdk-overlay();`, @@ -96,10 +111,11 @@ describe('Material theming API schematic', () => { `outline: solid 1px;`, `}`, `}` - ].join('\n')); + ]); + + await runMigration(); - const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise(); - expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([ + expect(splitFile(THEME_PATH)).toEqual([ `@use '~@angular/cdk' as cdk;`, ``, `@include cdk.overlay();`, @@ -117,8 +133,7 @@ describe('Material theming API schematic', () => { }); it('should migrate files using both Material and CDK APIs', async () => { - const app = await createTestApp(runner); - app.create('/theme.scss', [ + writeLines(THEME_PATH, [ `@import './foo'`, `@import '~@angular/material/theming';`, ``, @@ -139,10 +154,11 @@ describe('Material theming API schematic', () => { `.my-dialog {`, `z-index: $cdk-z-index-overlay-container + 1;`, `}`, - ].join('\n')); + ]); + + await runMigration(); - const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise(); - expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([ + expect(splitFile(THEME_PATH)).toEqual([ `@use '~@angular/material' as mat;`, `@use '~@angular/cdk' as cdk;`, `@import './foo'`, @@ -168,43 +184,43 @@ describe('Material theming API schematic', () => { }); it('should detect imports using double quotes', async () => { - const app = await createTestApp(runner); - app.create('/theme.scss', [ + writeLines(THEME_PATH, [ `@import "~@angular/material/theming";`, `@include mat-core();`, - ].join('\n')); + ]); - const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise(); - expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([ + await runMigration(); + + expect(splitFile(THEME_PATH)).toEqual([ `@use '~@angular/material' as mat;`, `@include mat.core();`, ]); }); it('should migrate mixins that are invoked without parentheses', async () => { - const app = await createTestApp(runner); - app.create('/theme.scss', [ + writeLines(THEME_PATH, [ `@import '~@angular/material/theming';`, `@include mat-base-typography;`, - ].join('\n')); + ]); - const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise(); - expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([ + await runMigration(); + + expect(splitFile(THEME_PATH)).toEqual([ `@use '~@angular/material' as mat;`, `@include mat.typography-hierarchy;`, ]); }); it('should migrate files that import the Material APIs transitively', async () => { - const app = await createTestApp(runner); - app.create('/theme.scss', [ + writeLines(THEME_PATH, [ `@import 're-exports-material-symbols';`, `@include mat-core();`, `@include mat-button-theme();`, - ].join('\n')); + ]); + + await runMigration(); - const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise(); - expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([ + expect(splitFile(THEME_PATH)).toEqual([ `@use '~@angular/material' as mat;`, `@import 're-exports-material-symbols';`, `@include mat.core();`, @@ -213,30 +229,30 @@ describe('Material theming API schematic', () => { }); it('should allow an arbitrary number of spaces after @include and @import', async () => { - const app = await createTestApp(runner); - app.create('/theme.scss', [ + writeLines(THEME_PATH, [ `@import '~@angular/material/theming';`, `@include mat-core;`, - ].join('\n')); + ]); + + await runMigration(); - const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise(); - expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([ + expect(splitFile(THEME_PATH)).toEqual([ `@use '~@angular/material' as mat;`, `@include mat.core;`, ]); }); it('should insert the new @use statement above other @import statements', async () => { - const app = await createTestApp(runner); - app.create('/theme.scss', [ + writeLines(THEME_PATH, [ `@import './foo'`, `@import "~@angular/material/theming";`, `@import './bar'`, `@include mat-core();`, - ].join('\n')); + ]); - const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise(); - expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([ + await runMigration(); + + expect(splitFile(THEME_PATH)).toEqual([ `@use '~@angular/material' as mat;`, `@import './foo'`, `@import './bar'`, @@ -245,16 +261,16 @@ describe('Material theming API schematic', () => { }); it('should account for other @use statements when inserting the new Material @use', async () => { - const app = await createTestApp(runner); - app.create('/theme.scss', [ + writeLines(THEME_PATH, [ `@use './foo'`, `@import './bar'`, `@import "~@angular/material/theming";`, `@include mat-core();`, - ].join('\n')); + ]); + + await runMigration(); - const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise(); - expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([ + expect(splitFile(THEME_PATH)).toEqual([ `@use './foo'`, `@use '~@angular/material' as mat;`, `@import './bar'`, @@ -263,16 +279,16 @@ describe('Material theming API schematic', () => { }); it('should account for file headers placed aboved the @import statements', async () => { - const app = await createTestApp(runner); - app.create('/theme.scss', [ + writeLines(THEME_PATH, [ `/** This is a license. */`, `@import './foo'`, `@import '~@angular/material/theming';`, `@include mat-core();`, - ].join('\n')); + ]); + + await runMigration(); - const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise(); - expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([ + expect(splitFile(THEME_PATH)).toEqual([ `/** This is a license. */`, `@use '~@angular/material' as mat;`, `@import './foo'`, @@ -281,25 +297,28 @@ describe('Material theming API schematic', () => { }); it('should migrate multiple files within the same project', async () => { - const app = await createTestApp(runner); - app.create('/theme.scss', [ + const componentPath = join(PROJECT_PATH, 'components/dialog.scss'); + + writeLines(THEME_PATH, [ `@import '~@angular/material/theming';`, `@include angular-material-theme();`, - ].join('\n')); + ]); - app.create('/components/dialog.scss', [ + writeLines(componentPath, [ `@import '~@angular/material/theming';`, `.my-dialog {`, `z-index: $cdk-z-index-overlay-container + 1;`, `}`, - ].join('\n')); + ]); - const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise(); - expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([ + await runMigration(); + + expect(splitFile(THEME_PATH)).toEqual([ `@use '~@angular/material' as mat;`, `@include mat.all-component-themes();`, ]); - expect(getFileContent(tree, '/components/dialog.scss').split('\n')).toEqual([ + + expect(splitFile(componentPath)).toEqual([ `@use '~@angular/cdk' as cdk;`, `.my-dialog {`, `z-index: cdk.$overlay-container-z-index + 1;`, @@ -308,17 +327,17 @@ describe('Material theming API schematic', () => { }); it('should handle variables whose names overlap', async () => { - const app = await createTestApp(runner); - app.create('/theme.scss', [ + writeLines(THEME_PATH, [ `@import '~@angular/material/theming';`, `$one: $mat-blue-grey;`, `$two: $mat-blue;`, '$three: $mat-blue', '$four: $mat-blue-gray', - ].join('\n')); + ]); - const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise(); - expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([ + await runMigration(); + + expect(splitFile(THEME_PATH)).toEqual([ `@use '~@angular/material' as mat;`, `$one: mat.$blue-grey-palette;`, `$two: mat.$blue-palette;`, @@ -328,8 +347,7 @@ describe('Material theming API schematic', () => { }); it('should migrate individual component themes', async () => { - const app = await createTestApp(runner); - app.create('/theme.scss', [ + writeLines(THEME_PATH, [ `@import '~@angular/material/theming';`, `@include mat-core();`, @@ -348,10 +366,11 @@ describe('Material theming API schematic', () => { `@include mat-expansion-panel-theme($candy-app-theme);`, `@include mat-datepicker-theme($candy-app-theme);`, `@include mat-option-theme($candy-app-theme);`, - ].join('\n')); + ]); + + await runMigration(); - const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise(); - expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([ + expect(splitFile(THEME_PATH)).toEqual([ `@use '~@angular/material' as mat;`, `@include mat.core();`, @@ -375,8 +394,7 @@ describe('Material theming API schematic', () => { }); it('should migrate deep imports', async () => { - const app = await createTestApp(runner); - app.create('/theme.scss', [ + writeLines(THEME_PATH, [ `@import '~@angular/material/core/theming/palette';`, `@import '~@angular/material/core/theming/theming';`, `@import '~@angular/material/button/button-theme';`, @@ -400,12 +418,13 @@ describe('Material theming API schematic', () => { `@include mat-table-theme($candy-app-theme);`, `@include mat-datepicker-theme($candy-app-theme);`, `@include mat-option-theme($candy-app-theme);`, - ].join('\n')); + ]); - const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise(); - expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([ - `@use '~@angular/material' as mat;`, + await runMigration(); + + expect(splitFile(THEME_PATH)).toEqual([ `@use '~@angular/cdk' as cdk;`, + `@use '~@angular/material' as mat;`, `@include cdk.overlay();`, @@ -426,8 +445,7 @@ describe('Material theming API schematic', () => { }); it('should migrate usages of @use, with and without namespaces', async () => { - const app = await createTestApp(runner); - app.create('/theme.scss', [ + writeLines(THEME_PATH, [ `@use '~@angular/material/core/theming/palette' as palette;`, `@use '~@angular/material/core/theming/theming';`, `@use '~@angular/material/button/button-theme' as button;`, @@ -452,10 +470,11 @@ describe('Material theming API schematic', () => { `@include table.mat-table-theme($candy-app-theme);`, `@include datepicker.mat-datepicker-theme($candy-app-theme);`, `@include mat-option-theme($candy-app-theme);`, - ].join('\n')); + ]); - const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise(); - expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([ + await runMigration(); + + expect(splitFile(THEME_PATH)).toEqual([ `@use '~@angular/material' as mat;`, `@use '~@angular/cdk' as cdk;`, @@ -478,8 +497,7 @@ describe('Material theming API schematic', () => { }); it('should handle edge case inferred Sass import namespaces', async () => { - const app = await createTestApp(runner); - app.create('/theme.scss', [ + writeLines(THEME_PATH, [ `@use '~@angular/material/core/index';`, `@use '~@angular/material/button/_button-theme';`, `@use '~@angular/material/table/table-theme.import';`, @@ -489,10 +507,11 @@ describe('Material theming API schematic', () => { `@include button-theme.mat-button-theme();`, `@include table-theme.mat-table-theme();`, `@include datepicker-theme.mat-datepicker-theme();`, - ].join('\n')); + ]); - const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise(); - expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([ + await runMigration(); + + expect(splitFile(THEME_PATH)).toEqual([ `@use '~@angular/material' as mat;`, `@include mat.core();`, @@ -503,17 +522,17 @@ describe('Material theming API schematic', () => { }); it('should drop the old import path even if the file is not using any symbols', async () => { - const app = await createTestApp(runner); - app.create('/theme.scss', [ + writeLines(THEME_PATH, [ `@import '~@angular/material/theming';`, ``, `.my-dialog {`, `color: red;`, `}`, - ].join('\n')); + ]); + + await runMigration(); - const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise(); - expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([ + expect(splitFile(THEME_PATH)).toEqual([ `.my-dialog {`, `color: red;`, `}`, @@ -521,8 +540,7 @@ describe('Material theming API schematic', () => { }); it('should replace removed variables with their values', async () => { - const app = await createTestApp(runner); - app.create('/theme.scss', [ + writeLines(THEME_PATH, [ `@import '~@angular/material/theming';`, ``, `@include mat-button-toggle-theme();`, @@ -538,10 +556,11 @@ describe('Material theming API schematic', () => { `height: $mat-button-toggle-standard-minimum-height;`, `}`, `}` - ].join('\n')); + ]); + + await runMigration(); - const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise(); - expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([ + expect(splitFile(THEME_PATH)).toEqual([ `@use '~@angular/material' as mat;`, ``, `@include mat.button-toggle-theme();`, @@ -561,8 +580,7 @@ describe('Material theming API schematic', () => { }); it('should not replace assignments to removed variables', async () => { - const app = await createTestApp(runner); - app.create('/theme.scss', [ + writeLines(THEME_PATH, [ `@import '~@angular/material/theming';`, ``, `$mat-button-toggle-standard-height: 50px;`, @@ -571,10 +589,11 @@ describe('Material theming API schematic', () => { `$mat-toggle-size: 11px;`, ``, `@include mat-button-toggle-theme();`, - ].join('\n')); + ]); + + await runMigration(); - const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise(); - expect(getFileContent(tree, '/theme.scss').split('\n')).toEqual([ + expect(splitFile(THEME_PATH)).toEqual([ `@use '~@angular/material' as mat;`, ``, `$mat-button-toggle-standard-height: 50px;`, @@ -587,16 +606,16 @@ describe('Material theming API schematic', () => { }); it('should not migrate files in the node_modules', async () => { - const app = await createTestApp(runner); - app.create('/node_modules/theme.scss', [ + writeLines('/node_modules/theme.scss', [ `@import '~@angular/material/theming';`, ``, `@include mat-button-toggle-theme();`, ``, - ].join('\n')); + ]); - const tree = await runner.runSchematicAsync('theming-api', options, app).toPromise(); - expect(getFileContent(tree, '/node_modules/theme.scss').split('\n')).toEqual([ + await runMigration(); + + expect(splitFile('/node_modules/theme.scss')).toEqual([ `@import '~@angular/material/theming';`, ``, `@include mat-button-toggle-theme();`, @@ -604,4 +623,74 @@ describe('Material theming API schematic', () => { ]); }); + it('should only migrate unprefixed variables if there is a theming import', async () => { + const otherTheme = join(PROJECT_PATH, 'other-theme.scss'); + + writeLines(THEME_PATH, [ + `@import '~@angular/material/theming';`, + ``, + `.my-button {`, + `z-index: $z-index-fab;`, + `}` + ]); + + writeLines(otherTheme, [ + `@import 're-exports-material-symbols';`, + ``, + `.my-drawer {`, + `z-index: $z-index-drawer;`, + `}` + ]); + + await runMigration(); + + expect(splitFile(THEME_PATH)).toEqual([ + `.my-button {`, + `z-index: 20;`, + `}` + ]); + + expect(splitFile(otherTheme)).toEqual([ + `@import 're-exports-material-symbols';`, + ``, + `.my-drawer {`, + `z-index: $z-index-drawer;`, + `}` + ]); + }); + + it('should insert the @use statement at the top of the file, if the theming import is ' + + 'the only import in the file and there is other content before it', async () => { + writeLines(THEME_PATH, [ + `:host {`, + `display: block;`, + `width: 100%;`, + `}`, + ``, + `@import '~@angular/material/theming';`, + ``, + `.button {`, + `@include mat-elevation(4);`, + `padding: 8px;`, + `}`, + ]); + + await runMigration(); + + expect(splitFile(THEME_PATH)).toEqual([ + `@use '~@angular/material' as mat;`, + `:host {`, + `display: block;`, + `width: 100%;`, + `}`, + ``, + ``, + `.button {`, + `@include mat.elevation(4);`, + `padding: 8px;`, + `}`, + ]); + }); + + });