Skip to content

Switch theming API migration to ng update and other fixes #22628

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

Merged
merged 5 commits into from
May 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions src/cdk/schematics/ng-update/find-stylesheets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,32 @@ 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') {
visitDir(join(dirPath, fragment));
}
});
};
visitDir(baseDir);
visitDir(startDirectory);
return result;
}
6 changes: 0 additions & 6 deletions src/material/schematics/collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
}
}
28 changes: 0 additions & 28 deletions src/material/schematics/ng-generate/theming-api/index.ts

This file was deleted.

7 changes: 0 additions & 7 deletions src/material/schematics/ng-generate/theming-api/schema.json

This file was deleted.

9 changes: 0 additions & 9 deletions src/material/schematics/ng-generate/theming-api/schema.ts

This file was deleted.

2 changes: 2 additions & 0 deletions src/material/schematics/ng-update/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -36,6 +37,7 @@ const materialMigrations: NullableDevkitMigration[] = [
RippleSpeedFactorMigration,
SecondaryEntryPointsMigration,
HammerGesturesMigration,
ThemingApiMigration,
];

/** Entry point for the migration schematics with target of Angular Material v6 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,14 @@ export const removedMaterialVariables: Record<string, string> = {
'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<string, string> = {
'z-index-fab': '20',
'z-index-drawer': '100',
'ease-in-out-curve-function': 'cubic-bezier(0.35, 0, 0.25, 1)',
Expand All @@ -183,5 +191,21 @@ export const removedMaterialVariables: Record<string, string> = {
'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)',
};
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}

Expand All @@ -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.`);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<null> {
/** 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;
}
}
}
Loading