From 93a6c6009ebd710f1fd9bf9b422db4df4c7c8721 Mon Sep 17 00:00:00 2001 From: Zach Arend Date: Thu, 14 Mar 2024 19:38:21 +0000 Subject: [PATCH] build: update schematics for v18 --- src/cdk/schematics/migration.json | 8 +- src/cdk/schematics/ng-update/index.ts | 6 +- .../ng-update/migrations/misc-template.ts | 2 +- .../schematics/update-tool/target-version.ts | 2 +- src/material/schematics/migration.json | 8 +- src/material/schematics/ng-update/index.ts | 22 +- .../migrations/legacy-imports-error.ts | 104 ------ .../migrations/theme-base-v17/index.ts | 133 ------- .../migrations/theme-base-v17/migration.ts | 321 ---------------- .../test-cases/legacy-imports-error.spec.ts | 83 ----- .../ng-update/test-cases/theme-base.spec.ts | 344 ------------------ 11 files changed, 22 insertions(+), 1011 deletions(-) delete mode 100644 src/material/schematics/ng-update/migrations/legacy-imports-error.ts delete mode 100644 src/material/schematics/ng-update/migrations/theme-base-v17/index.ts delete mode 100644 src/material/schematics/ng-update/migrations/theme-base-v17/migration.ts delete mode 100644 src/material/schematics/ng-update/test-cases/legacy-imports-error.spec.ts delete mode 100644 src/material/schematics/ng-update/test-cases/theme-base.spec.ts diff --git a/src/cdk/schematics/migration.json b/src/cdk/schematics/migration.json index 0591fc038333..5799f415ba39 100644 --- a/src/cdk/schematics/migration.json +++ b/src/cdk/schematics/migration.json @@ -1,10 +1,10 @@ { "$schema": "./node_modules/@angular-devkit/schematics/collection-schema.json", "schematics": { - "migration-v17": { - "version": "17.0.0-0", - "description": "Updates the Angular CDK to v17", - "factory": "./ng-update/index#updateToV17" + "migration-v18": { + "version": "18.0.0-0", + "description": "Updates the Angular CDK to v18", + "factory": "./ng-update/index#updateToV18" }, "ng-post-update": { "description": "Prints out results after ng-update.", diff --git a/src/cdk/schematics/ng-update/index.ts b/src/cdk/schematics/ng-update/index.ts index 2c1eb7491bcb..cb3d1a33b4d1 100644 --- a/src/cdk/schematics/ng-update/index.ts +++ b/src/cdk/schematics/ng-update/index.ts @@ -13,10 +13,10 @@ import {createMigrationSchematicRule, NullableDevkitMigration} from './devkit-mi const cdkMigrations: NullableDevkitMigration[] = []; -/** Entry point for the migration schematics with target of Angular CDK 17.0.0 */ -export function updateToV17(): Rule { +/** Entry point for the migration schematics with target of Angular CDK 18.0.0 */ +export function updateToV18(): Rule { return createMigrationSchematicRule( - TargetVersion.V17, + TargetVersion.V18, cdkMigrations, cdkUpgradeData, onMigrationComplete, diff --git a/src/cdk/schematics/ng-update/migrations/misc-template.ts b/src/cdk/schematics/ng-update/migrations/misc-template.ts index fd503250af0e..813f4e4de39b 100644 --- a/src/cdk/schematics/ng-update/migrations/misc-template.ts +++ b/src/cdk/schematics/ng-update/migrations/misc-template.ts @@ -15,7 +15,7 @@ import {UpgradeData} from '../upgrade-data'; * instances of outdated Angular CDK API that can't be migrated automatically. */ export class MiscTemplateMigration extends Migration { - // There are currently no migrations for V17 deprecations. + // There are currently no migrations for V18 deprecations. enabled = false; override visitTemplate(template: ResolvedResource): void {} diff --git a/src/cdk/schematics/update-tool/target-version.ts b/src/cdk/schematics/update-tool/target-version.ts index 73953eccc769..a6371d05e001 100644 --- a/src/cdk/schematics/update-tool/target-version.ts +++ b/src/cdk/schematics/update-tool/target-version.ts @@ -10,7 +10,7 @@ // tslint:disable-next-line:prefer-const-enum export enum TargetVersion { - V17 = 'version 17', + V18 = 'version 18', } /** diff --git a/src/material/schematics/migration.json b/src/material/schematics/migration.json index 81aa9dfe8b23..0fa2742f3b45 100644 --- a/src/material/schematics/migration.json +++ b/src/material/schematics/migration.json @@ -1,10 +1,10 @@ { "$schema": "./node_modules/@angular-devkit/schematics/collection-schema.json", "schematics": { - "migration-v17": { - "version": "17.0.0-0", - "description": "Updates Angular Material to v17", - "factory": "./ng-update/index_bundled#updateToV17" + "migration-v18": { + "version": "18.0.0-0", + "description": "Updates Angular Material to v18", + "factory": "./ng-update/index_bundled#updateToV18" } } } diff --git a/src/material/schematics/ng-update/index.ts b/src/material/schematics/ng-update/index.ts index f536c5b5473e..df166007bc94 100644 --- a/src/material/schematics/ng-update/index.ts +++ b/src/material/schematics/ng-update/index.ts @@ -13,24 +13,20 @@ import { TargetVersion, } from '@angular/cdk/schematics'; -import {legacyImportsError} from './migrations/legacy-imports-error'; import {materialUpgradeData} from './upgrade-data'; -import {ThemeBaseMigration} from './migrations/theme-base-v17'; -const materialMigrations: NullableDevkitMigration[] = [ThemeBaseMigration]; +const materialMigrations: NullableDevkitMigration[] = []; -/** Entry point for the migration schematics with target of Angular Material v17 */ -export function updateToV17(): Rule { - // We pass the v17 migration rule as a callback, instead of using `chain()`, because the +/** Entry point for the migration schematics with target of Angular Material v18 */ +export function updateToV18(): Rule { + // We pass the v18 migration rule as a callback, instead of using `chain()`, because the // legacy imports error only logs an error message, it doesn't actually interrupt the migration // process and we don't want to execute migrations if there are leftover legacy imports. - return legacyImportsError( - createMigrationSchematicRule( - TargetVersion.V17, - materialMigrations, - materialUpgradeData, - onMigrationComplete, - ), + return createMigrationSchematicRule( + TargetVersion.V18, + materialMigrations, + materialUpgradeData, + onMigrationComplete, ); } diff --git a/src/material/schematics/ng-update/migrations/legacy-imports-error.ts b/src/material/schematics/ng-update/migrations/legacy-imports-error.ts deleted file mode 100644 index c87ba6da72c2..000000000000 --- a/src/material/schematics/ng-update/migrations/legacy-imports-error.ts +++ /dev/null @@ -1,104 +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 {Rule, SchematicContext, Tree} from '@angular-devkit/schematics'; -import {NodePackageInstallTask} from '@angular-devkit/schematics/tasks'; -import * as ts from 'typescript'; - -/** String with which legacy imports start. */ -const LEGACY_IMPORTS_START = '@angular/material/legacy-'; - -/** Maximum files to print in the error message. */ -const MAX_FILES_TO_PRINT = 50; - -/** - * "Migration" that logs an error and prevents further migrations - * from running if the project is using legacy components. - * @param onSuccess Rule to run if there are no legacy imports. - */ -export function legacyImportsError(onSuccess: Rule): Rule { - return async (tree: Tree, context: SchematicContext) => { - const filesUsingLegacyImports = new Set(); - - tree.visit(path => { - if (path.includes('node_modules') || path.endsWith('.d.ts') || !path.endsWith('.ts')) { - return; - } - - const content = tree.readText(path); - - // Skip over any files that definitely cannot contain the legacy imports. - if (!content.includes(LEGACY_IMPORTS_START)) { - return; - } - - const sourceFile = ts.createSourceFile(path, content, ts.ScriptTarget.Latest); - - // Only check top-level imports/exports. - for (const statement of sourceFile.statements) { - if (!ts.isImportDeclaration(statement) && !ts.isExportDeclaration(statement)) { - continue; - } - - if ( - statement.moduleSpecifier && - ts.isStringLiteralLike(statement.moduleSpecifier) && - statement.moduleSpecifier.text.startsWith(LEGACY_IMPORTS_START) - ) { - filesUsingLegacyImports.add(path); - } - } - }); - - // If there are no legacy imports left, we can continue with the migrations. - if (filesUsingLegacyImports.size === 0) { - return onSuccess; - } - - // At this point the project is already at v17 so we need to downgrade it back - // to v16 and run `npm install` again. Ideally we would also throw an error here - // to interrupt the update process, but that would interrupt `npm install` as well. - if (tree.exists('package.json')) { - let packageJson: Record | null = null; - - try { - packageJson = JSON.parse(tree.readText('package.json')) as Record; - } catch {} - - if (packageJson !== null && packageJson['dependencies']) { - packageJson['dependencies']['@angular/material'] = '^16.2.0'; - tree.overwrite('package.json', JSON.stringify(packageJson, null, 2)); - context.addTask(new NodePackageInstallTask()); - } - } - - context.logger.fatal(formatErrorMessage(filesUsingLegacyImports)); - return; - }; -} - -function formatErrorMessage(filesUsingLegacyImports: Set): string { - const files = Array.from(filesUsingLegacyImports, path => ' - ' + path); - const filesMessage = - files.length > MAX_FILES_TO_PRINT - ? [ - ...files.slice(0, MAX_FILES_TO_PRINT), - `${files.length - MAX_FILES_TO_PRINT} more...`, - `Search your project for "${LEGACY_IMPORTS_START}" to view all usages.`, - ].join('\n') - : files.join('\n'); - - return ( - `Cannot update to Angular Material v17 because the project is using the legacy ` + - `Material components\nthat have been deleted. While Angular Material v16 is compatible with ` + - `Angular v17, it is recommended\nto switch away from the legacy components as soon as possible ` + - `because they no longer receive bug fixes,\naccessibility improvements and new features.\n\n` + - `Read more about migrating away from legacy components: https://material.angular.io/guide/mdc-migration\n\n` + - `Files in the project using legacy Material components:\n${filesMessage}\n` - ); -} diff --git a/src/material/schematics/ng-update/migrations/theme-base-v17/index.ts b/src/material/schematics/ng-update/migrations/theme-base-v17/index.ts deleted file mode 100644 index d86965919214..000000000000 --- a/src/material/schematics/ng-update/migrations/theme-base-v17/index.ts +++ /dev/null @@ -1,133 +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 {SchematicContext} from '@angular-devkit/schematics'; -import {DevkitMigration, ResolvedResource, TargetVersion} from '@angular/cdk/schematics'; -import {addThemeBaseMixins, checkThemeBaseMixins} from './migration'; - -/** Adds an @include for theme base mixins that aren't already included by the app. */ -export class ThemeBaseMigration extends DevkitMigration { - /** Number of files that have been migrated. */ - static migratedFileCount = 0; - - /** All base mixins that we have found an existing @include for. */ - static foundBaseMixins = new Set(); - - /** All base mixins that appear to be missing an @include. */ - static missingBaseMixins = new Set(); - - /** Whether to run this migration. */ - enabled = this.targetVersion === TargetVersion.V17; - - /** - * All Sass stylesheets visited. (We save a record, so we can go back through them in the - * `postAnalysis` phase). - */ - visitedSassStylesheets: ResolvedResource[] = []; - - /** - * Visit each stylesheet, noting which base mixins are accounted for (because the user is calling - * `mat.-theme()`), and which ones are missing (because the user is calling one of the - * theme-partial mixins: `mat.()`, `mat.-typography()`, - * or `mat.-density()`. - * - * We don't make any modifications at this point. Instead, the results of visiting each stylesheet - * are aggregated into a static variable which is used to determine which mixins to add in - * `postAnalysis` phase. - */ - override visitStylesheet(stylesheet: ResolvedResource): void { - if (extname(stylesheet.filePath) === '.scss') { - this.visitedSassStylesheets.push(stylesheet); - - const content = stylesheet.content; - const {found, missing} = checkThemeBaseMixins(content); - for (const mixin of found) { - ThemeBaseMigration.foundBaseMixins.add(mixin); - ThemeBaseMigration.missingBaseMixins.delete(mixin); - } - for (const mixin of missing) { - if (!ThemeBaseMigration.foundBaseMixins.has(mixin)) { - ThemeBaseMigration.missingBaseMixins.add(mixin); - } - } - } - } - - /** - * Perform the necessary updates detected while visiting the stylesheets. The - * `mat.-base()` mixins behave similarly to `mat.core()`, in that they needed to be - * included once globally. So we locate calls to `mat.core()` and add the missing mixins - * identified by earlier at these locations. - */ - override postAnalysis() { - // If we're not missing any mixins, there's nothing to migrate. - if (ThemeBaseMigration.missingBaseMixins.size === 0) { - return; - } - // If we have all-component-bases, we don't need any others and there is nothing to migrate. - if (ThemeBaseMigration.foundBaseMixins.has('all-component-bases')) { - return; - } - // If we're missing all-component-bases, we just need to add it, not the individual mixins. - if (ThemeBaseMigration.missingBaseMixins.has('all-component-bases')) { - ThemeBaseMigration.missingBaseMixins = new Set(['all-component-bases']); - } - for (const stylesheet of this.visitedSassStylesheets) { - const content = stylesheet.content; - const migratedContent = content - ? addThemeBaseMixins(content, ThemeBaseMigration.missingBaseMixins) - : content; - - if (migratedContent && migratedContent !== content) { - this.fileSystem - .edit(stylesheet.filePath) - .remove(0, stylesheet.content.length) - .insertLeft(0, migratedContent); - ThemeBaseMigration.migratedFileCount++; - } - } - if (ThemeBaseMigration.migratedFileCount === 0) { - const mixinsText = [...ThemeBaseMigration.missingBaseMixins] - .sort() - .map(m => `mat.${m}($theme)`) - .join('\n'); - this.failures.push({ - filePath: this.context.tree.root.path, - message: - `The following mixins could not be automatically added, please add them manually` + - ` if needed:\n${mixinsText}`, - }); - } - } - - /** Logs out the number of migrated files at the end of the migration. */ - static override globalPostMigration( - _tree: unknown, - _targetVersion: TargetVersion, - context: SchematicContext, - ): void { - const fileCount = ThemeBaseMigration.migratedFileCount; - const mixinCount = ThemeBaseMigration.missingBaseMixins.size; - - if (fileCount > 0 && mixinCount > 0) { - const fileCountText = fileCount === 1 ? '1 file' : `${fileCount} files`; - const mixinCountText = - mixinCount === 1 ? '1 theme base mixin' : `${mixinCount} theme base mixins`; - context.logger.info( - `Added ${mixinCountText} to ${fileCountText}.` + - ' Please search for, and address, any "TODO(v17)" comments.', - ); - } - - // Reset to avoid leaking between tests. - ThemeBaseMigration.migratedFileCount = 0; - ThemeBaseMigration.missingBaseMixins = new Set(); - ThemeBaseMigration.foundBaseMixins = new Set(); - } -} diff --git a/src/material/schematics/ng-update/migrations/theme-base-v17/migration.ts b/src/material/schematics/ng-update/migrations/theme-base-v17/migration.ts deleted file mode 100644 index 406b096620df..000000000000 --- a/src/material/schematics/ng-update/migrations/theme-base-v17/migration.ts +++ /dev/null @@ -1,321 +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 - */ - -/** Preamble to insert before the missing mixins. */ -const MISSING_MIXIN_PREAMBLE_LINES = ` -// The following mixins include base theme styles that are only needed once per application. These -// theme styles do not depend on the color, typography, or density settings in your theme. However, -// these styles may differ depending on the theme's design system. Currently all themes use the -// Material 2 design system, but in the future it may be possible to create theme based on other -// design systems, such as Material 3. -// -// Please note: you do not need to include the 'base' mixins, if you include the corresponding -// 'theme' mixin elsewhere in your Sass. The full 'theme' mixins already include the base styles. -// -// To learn more about "base" theme styles visit our theming guide: -// https://material.angular.io/guide/theming#theming-dimensions -// -// TODO(v17): Please move these @include statements to the preferred place in your Sass, and pass -// your theme to them. This will ensure the correct values for your app are included.\ -`.split('\n'); - -/** The sets of theme mixins to check for. */ -const THEME_MIXIN_SETS: { - theme: string; - color: string; - typography: string; - density: string; - base: string; -}[] = [ - { - theme: 'all-component-themes', - color: 'all-component-colors', - typography: 'all-component-typographies', - density: 'all-component-densities', - base: 'all-component-bases', - }, - ...[ - 'core', - 'card', - 'progress-bar', - 'tooltip', - 'form-field', - 'input', - 'select', - 'autocomplete', - 'dialog', - 'chips', - 'slide-toggle', - 'radio', - 'slider', - 'menu', - 'list', - 'paginator', - 'tabs', - 'checkbox', - 'button', - 'icon-button', - 'fab', - 'snack-bar', - 'table', - 'progress-spinner', - 'badge', - 'bottom-sheet', - 'button-toggle', - 'datepicker', - 'divider', - 'expansion', - 'grid-list', - 'icon', - 'sidenav', - 'stepper', - 'sort', - 'toolbar', - 'tree', - ].map(comp => ({ - theme: `${comp}-theme`, - color: `${comp}-color`, - typography: `${comp}-typography`, - density: `${comp}-density`, - base: `${comp}-base`, - })), -]; - -/** Possible pairs of comment characters in a Sass file. */ -const COMMENT_PAIRS = new Map([ - ['/*', '*/'], - ['//', '\n'], -]); - -/** Prefix for the placeholder that will be used to escape comments. */ -const COMMENT_PLACEHOLDER_START = '__<} { - const placeholders: Record = {}; - let commentCounter = 0; - let [openIndex, closeIndex] = findComment(content); - - while (openIndex > -1 && closeIndex > -1) { - const placeholder = COMMENT_PLACEHOLDER_START + commentCounter++ + COMMENT_PLACEHOLDER_END; - placeholders[placeholder] = content.slice(openIndex, closeIndex); - content = content.slice(0, openIndex) + placeholder + content.slice(closeIndex); - [openIndex, closeIndex] = findComment(content); - } - - return {content, placeholders}; -} - -/** Finds the start and end index of a comment in a file. */ -function findComment(content: string): [openIndex: number, closeIndex: number] { - // Add an extra new line at the end so that we can correctly capture single-line comments - // at the end of the file. It doesn't really matter that the end index will be out of bounds, - // because `String.prototype.slice` will clamp it to the string length. - content += '\n'; - - for (const [open, close] of COMMENT_PAIRS.entries()) { - const openIndex = content.indexOf(open); - - if (openIndex > -1) { - const closeIndex = content.indexOf(close, openIndex + 1); - return closeIndex > -1 ? [openIndex, closeIndex + close.length] : [-1, -1]; - } - } - - return [-1, -1]; -} - -/** Restores the comments that have been escaped by `escapeComments`. */ -function restoreComments(content: string, placeholders: Record): string { - Object.keys(placeholders).forEach(key => (content = content.replace(key, placeholders[key]))); - return content; -} - -/** Escapes special regex characters in a string. */ -function escapeRegExp(str: string): string { - return str.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); -} - -/** Parses out the namespace from a Sass `@use` statement. */ -function extractNamespaceFromUseStatement(fullImport: string): string { - const closeQuoteIndex = Math.max(fullImport.lastIndexOf(`"`), fullImport.lastIndexOf(`'`)); - - if (closeQuoteIndex > -1) { - const asExpression = 'as '; - const asIndex = fullImport.indexOf(asExpression, closeQuoteIndex); - - // If we found an ` as ` expression, we consider the rest of the text as the namespace. - if (asIndex > -1) { - return fullImport - .slice(asIndex + asExpression.length) - .split(';')[0] - .trim(); - } - - // Otherwise the namespace is the name of the file that is being imported. - const lastSlashIndex = fullImport.lastIndexOf('/', closeQuoteIndex); - - if (lastSlashIndex > -1) { - const fileName = fullImport - .slice(lastSlashIndex + 1, closeQuoteIndex) - // Sass allows for leading underscores to be omitted and it technically supports .scss. - .replace(/^_|(\.import)?\.scss$|\.import$/g, ''); - - // Sass ignores `/index` and infers the namespace as the next segment in the path. - if (fileName === 'index') { - const nextSlashIndex = fullImport.lastIndexOf('/', lastSlashIndex - 1); - - if (nextSlashIndex > -1) { - return fullImport.slice(nextSlashIndex + 1, lastSlashIndex); - } - } else { - return fileName; - } - } - } - - throw Error(`Could not extract namespace from import "${fullImport}".`); -} - -/** Gets the set of namespaces that the given import path is aliased to by @use. */ -function getAtUseNamespaces(content: string, path: string) { - const namespaces = new Set(); - const pattern = new RegExp(`@use +['"]~?${escapeRegExp(path)}['"].*;?\n`, 'g'); - let match: RegExpExecArray | null = null; - - while ((match = pattern.exec(content))) { - namespaces.add(extractNamespaceFromUseStatement(match[0])); - } - - return namespaces; -} - -/** Gets a list of matches representing where the given mixin is included with `@include`. */ -function getAtIncludes(content: string, namespace: string, mixin: string): RegExpMatchArray[] { - // The ending checks what comes after the mixin name. We need to check that we don't see a word - // character or `-` immediately following the mixin name, as that would change the name. Beyond - // that character we can match anything, to the end of the line. - const ending = '([^\\n\\w-][^\\n]*)?($|\\n)'; - const pattern = new RegExp( - `@include\\s+${escapeRegExp(namespace)}\\.${escapeRegExp(mixin)}${ending}`, - 'g', - ); - return [...content.matchAll(pattern)]; -} - -/** Checks whether the given mixin is included with `@include`. */ -function isMixinAtIncluded(content: string, namespace: string, mixin: string) { - return !!getAtIncludes(content, namespace, mixin).length; -} - -/** Inserts the given lines after the match point. */ -function insertLinesAfterMatch(content: string, match: RegExpMatchArray, lines: string[]): string { - const insertionPoint = match.index! + match[0].length; - return ( - content.substring(0, insertionPoint) + - lines.join('\n') + - '\n' + - content.substring(insertionPoint) - ); -} - -/** Gets the indentation at the given line in the content. */ -function getIndentation(content: string, index: number) { - let indentationStart = 0; - let indentationEnd = index; - for (let i = index; i >= 0; i--) { - if (content[i] === '\n') { - indentationStart = i + 1; - break; - } - if (!/\s/.exec(content[i])) { - indentationEnd = i; - } - } - return content.slice(indentationStart, indentationEnd); -} - -/** Gets the lines to insert to address the missing mixins. */ -function getMissingMixinLines(namespace: string, mixins: Set, indentation: string) { - return [ - ...MISSING_MIXIN_PREAMBLE_LINES, - ...[...mixins] - .sort() - .map(mixin => `@include ${namespace}.${mixin}(/* TODO(v17): pass $your-theme here */);`), - '', - ].map(line => (indentation + line).trimRight()); -} - -/** - * Checks which theme bases are found in the file via the existing included mixins, - * and which ones may be missing. - */ -export function checkThemeBaseMixins(fileContent: string): { - found: Set; - missing: Set; -} { - const found = new Set(); - const missing = new Set(); - - // Strip out comments, so they don't confuse our migration. - const {content} = escapeComments(fileContent); - const materialNamespaces = getAtUseNamespaces(content, '@angular/material'); - - // Check over all namespaces for mixins of interest. - for (const namespace of materialNamespaces) { - for (const mixins of THEME_MIXIN_SETS) { - // If they include the theme mixin, that accounts for the base theme styles. - if (isMixinAtIncluded(content, namespace, mixins.theme)) { - found.add(mixins.base); - missing.delete(mixins.base); - continue; - } - // If they haven't called the theme mixin, but do call one of the partials, - // we assume they're missing the base styles. - if (!found.has(mixins.base)) { - if ( - isMixinAtIncluded(content, namespace, mixins.color) || - isMixinAtIncluded(content, namespace, mixins.typography) || - isMixinAtIncluded(content, namespace, mixins.density) - ) { - missing.add(mixins.base); - } - } - } - } - - return {found, missing}; -} - -/** Adds the given theme base mixins, after the call to `mat.core()`. */ -export function addThemeBaseMixins(fileContent: string, mixins: Set): string { - // Strip out comments, so they don't confuse our migration. - let {content, placeholders} = escapeComments(fileContent); - const materialNamespaces = getAtUseNamespaces(content, '@angular/material'); - - for (const namespace of materialNamespaces) { - // Update the @includes in reverse order, so our changes don't mess up the indices we found. - const coreIncludes = getAtIncludes(content, namespace, 'core').reverse(); - for (const coreInclude of coreIncludes) { - if (coreInclude.index === undefined) { - throw Error(`Cannot find location of mat.core() match: ${coreInclude}`); - } - const indentation = getIndentation(content, coreInclude.index); - const lines = getMissingMixinLines(namespace, mixins, indentation); - content = insertLinesAfterMatch(content, coreInclude, lines); - } - } - - return restoreComments(content, placeholders); -} diff --git a/src/material/schematics/ng-update/test-cases/legacy-imports-error.spec.ts b/src/material/schematics/ng-update/test-cases/legacy-imports-error.spec.ts deleted file mode 100644 index 534948c147db..000000000000 --- a/src/material/schematics/ng-update/test-cases/legacy-imports-error.spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -import {createTestCaseSetup} from '@angular/cdk/schematics/testing'; -import {UnitTestTree} from '@angular-devkit/schematics/testing'; -import {logging} from '@angular-devkit/core'; -import {MIGRATION_PATH} from '../../paths'; - -describe('legacy imports error', () => { - const PATH = 'projects/material-testing/'; - let runFixers: () => Promise; - let tree: UnitTestTree; - let writeFile: (path: string, content: string) => void; - let fatalLogs: string[]; - - beforeEach(async () => { - const setup = await createTestCaseSetup('migration-v17', MIGRATION_PATH, []); - runFixers = setup.runFixers; - writeFile = setup.writeFile; - tree = setup.appTree; - fatalLogs = []; - setup.runner.logger.subscribe((entry: logging.LogEntry) => { - if (entry.level === 'fatal') { - fatalLogs.push(entry.message); - } - }); - }); - - afterEach(() => { - runFixers = tree = writeFile = fatalLogs = null!; - }); - - it('should log a fatal message if the app imports a legacy import', async () => { - writeFile( - `${PATH}/src/app/app.module.ts`, - ` - import {NgModule} from '@angular/core'; - import {MatLegacyButtonModule} from '@angular/material/legacy-button'; - - @NgModule({ - imports: [MatLegacyButtonModule], - }) - export class AppModule {} - `, - ); - - await runFixers(); - - expect(fatalLogs.length).toBe(1); - expect(fatalLogs[0]).toContain( - 'Cannot update to Angular Material v17 because the ' + - 'project is using the legacy Material components', - ); - }); - - it('should downgrade the app to v16 if it contains legacy imports', async () => { - writeFile( - `${PATH}/package.json`, - `{ - "name": "test", - "version": "0.0.0", - "dependencies": { - "@angular/material": "^17.0.0" - } - }`, - ); - - writeFile( - `${PATH}/src/app/app.module.ts`, - ` - import {NgModule} from '@angular/core'; - import {MatLegacyButtonModule} from '@angular/material/legacy-button'; - - @NgModule({ - imports: [MatLegacyButtonModule], - }) - export class AppModule {} - `, - ); - - await runFixers(); - - const content = JSON.parse(tree.readText('/package.json')) as Record; - expect(content['dependencies']['@angular/material']).toBe('^16.2.0'); - }); -}); diff --git a/src/material/schematics/ng-update/test-cases/theme-base.spec.ts b/src/material/schematics/ng-update/test-cases/theme-base.spec.ts deleted file mode 100644 index ac0b47ea9e5a..000000000000 --- a/src/material/schematics/ng-update/test-cases/theme-base.spec.ts +++ /dev/null @@ -1,344 +0,0 @@ -import {createTestCaseSetup} from '@angular/cdk/schematics/testing'; -import {MIGRATION_PATH} from '../../paths'; - -function defineTest( - description: string, - inputs: {[filename: string]: string}, - expected: {[filename: string]: string}, -) { - it(description, async () => { - const PATH = 'projects/cdk-testing/'; - const {runFixers, writeFile, appTree} = await createTestCaseSetup( - 'migration-v17', - MIGRATION_PATH, - [], - ); - - for (const filename in inputs) { - writeFile(PATH + filename, inputs[filename]); - } - - await runFixers(); - - for (const filename in expected) { - const actual = appTree.readContent(PATH + filename); - // Jasmine's expect(...).toBe(...) doesn't show us the full output. - if (actual != expected[filename]) { - fail(['\nActual:', actual, 'Expected:', expected[filename]].join('\n')); - } - } - }); -} - -describe('theme base mixins migration', () => { - defineTest( - 'should add base if color found', - { - 'global.scss': ` - @use '@angular/material' as mat; - $theme: (); - @include mat.core(); - @include mat.button-color($theme); - `, - }, - { - 'global.scss': ` - @use '@angular/material' as mat; - $theme: (); - @include mat.core(); - - // The following mixins include base theme styles that are only needed once per application. These - // theme styles do not depend on the color, typography, or density settings in your theme. However, - // these styles may differ depending on the theme's design system. Currently all themes use the - // Material 2 design system, but in the future it may be possible to create theme based on other - // design systems, such as Material 3. - // - // Please note: you do not need to include the 'base' mixins, if you include the corresponding - // 'theme' mixin elsewhere in your Sass. The full 'theme' mixins already include the base styles. - // - // To learn more about "base" theme styles visit our theming guide: - // https://material.angular.io/guide/theming#theming-dimensions - // - // TODO(v17): Please move these @include statements to the preferred place in your Sass, and pass - // your theme to them. This will ensure the correct values for your app are included. - @include mat.button-base(/* TODO(v17): pass $your-theme here */); - - @include mat.button-color($theme); - `, - }, - ); - - defineTest( - 'should add base if typography found', - { - 'global.scss': ` - @use '@angular/material'; - $theme: (); - @include material.all-component-typographies($theme); - @include material.core(); - `, - }, - { - 'global.scss': ` - @use '@angular/material'; - $theme: (); - @include material.all-component-typographies($theme); - @include material.core(); - - // The following mixins include base theme styles that are only needed once per application. These - // theme styles do not depend on the color, typography, or density settings in your theme. However, - // these styles may differ depending on the theme's design system. Currently all themes use the - // Material 2 design system, but in the future it may be possible to create theme based on other - // design systems, such as Material 3. - // - // Please note: you do not need to include the 'base' mixins, if you include the corresponding - // 'theme' mixin elsewhere in your Sass. The full 'theme' mixins already include the base styles. - // - // To learn more about "base" theme styles visit our theming guide: - // https://material.angular.io/guide/theming#theming-dimensions - // - // TODO(v17): Please move these @include statements to the preferred place in your Sass, and pass - // your theme to them. This will ensure the correct values for your app are included. - @include material.all-component-bases(/* TODO(v17): pass $your-theme here */); - - `, - }, - ); - - defineTest( - 'should add base if density found', - { - 'global.scss': ` - @use "@angular/material" as mat; - @include mat.core ; - @include mat.checkbox-density(maximum); - @include mat.card-density(-3); - `, - }, - { - 'global.scss': ` - @use "@angular/material" as mat; - @include mat.core ; - - // The following mixins include base theme styles that are only needed once per application. These - // theme styles do not depend on the color, typography, or density settings in your theme. However, - // these styles may differ depending on the theme's design system. Currently all themes use the - // Material 2 design system, but in the future it may be possible to create theme based on other - // design systems, such as Material 3. - // - // Please note: you do not need to include the 'base' mixins, if you include the corresponding - // 'theme' mixin elsewhere in your Sass. The full 'theme' mixins already include the base styles. - // - // To learn more about "base" theme styles visit our theming guide: - // https://material.angular.io/guide/theming#theming-dimensions - // - // TODO(v17): Please move these @include statements to the preferred place in your Sass, and pass - // your theme to them. This will ensure the correct values for your app are included. - @include mat.card-base(/* TODO(v17): pass $your-theme here */); - @include mat.checkbox-base(/* TODO(v17): pass $your-theme here */); - - @include mat.checkbox-density(maximum); - @include mat.card-density(-3); - `, - }, - ); - - defineTest( - 'should not add all-components-bases and individual bases', - { - 'global.scss': ` - @use '@angular/material' as mat; - $theme: (); - @include mat.core(); - @include mat.all-component-colors($theme); - @include mat.button-typography($theme); - `, - }, - { - 'global.scss': ` - @use '@angular/material' as mat; - $theme: (); - @include mat.core(); - - // The following mixins include base theme styles that are only needed once per application. These - // theme styles do not depend on the color, typography, or density settings in your theme. However, - // these styles may differ depending on the theme's design system. Currently all themes use the - // Material 2 design system, but in the future it may be possible to create theme based on other - // design systems, such as Material 3. - // - // Please note: you do not need to include the 'base' mixins, if you include the corresponding - // 'theme' mixin elsewhere in your Sass. The full 'theme' mixins already include the base styles. - // - // To learn more about "base" theme styles visit our theming guide: - // https://material.angular.io/guide/theming#theming-dimensions - // - // TODO(v17): Please move these @include statements to the preferred place in your Sass, and pass - // your theme to them. This will ensure the correct values for your app are included. - @include mat.all-component-bases(/* TODO(v17): pass $your-theme here */); - - @include mat.all-component-colors($theme); - @include mat.button-typography($theme); - `, - }, - ); - - defineTest( - 'should not add individual bases if all-component-themes is present', - { - 'global.scss': ` - @use '@angular/material' as mat; - $theme: (); - @include mat.core(); - @include mat.all-component-themes($theme); - @include mat.tabs-density($theme); - `, - }, - { - 'global.scss': ` - @use '@angular/material' as mat; - $theme: (); - @include mat.core(); - @include mat.all-component-themes($theme); - @include mat.tabs-density($theme); - `, - }, - ); - - defineTest( - 'should update all instances of mat.core', - { - 'global.scss': ` - @use '@angular/material' as mat; - .dark-theme { - $dark-theme: (); - @include mat.core(); - @include mat.slider-color($dark-theme); - } - .light-theme { - $light-theme: (); - @include mat.core(); - @include mat.slider-color($light-theme); - } - `, - }, - { - 'global.scss': ` - @use '@angular/material' as mat; - .dark-theme { - $dark-theme: (); - @include mat.core(); - - // The following mixins include base theme styles that are only needed once per application. These - // theme styles do not depend on the color, typography, or density settings in your theme. However, - // these styles may differ depending on the theme's design system. Currently all themes use the - // Material 2 design system, but in the future it may be possible to create theme based on other - // design systems, such as Material 3. - // - // Please note: you do not need to include the 'base' mixins, if you include the corresponding - // 'theme' mixin elsewhere in your Sass. The full 'theme' mixins already include the base styles. - // - // To learn more about "base" theme styles visit our theming guide: - // https://material.angular.io/guide/theming#theming-dimensions - // - // TODO(v17): Please move these @include statements to the preferred place in your Sass, and pass - // your theme to them. This will ensure the correct values for your app are included. - @include mat.slider-base(/* TODO(v17): pass $your-theme here */); - - @include mat.slider-color($dark-theme); - } - .light-theme { - $light-theme: (); - @include mat.core(); - - // The following mixins include base theme styles that are only needed once per application. These - // theme styles do not depend on the color, typography, or density settings in your theme. However, - // these styles may differ depending on the theme's design system. Currently all themes use the - // Material 2 design system, but in the future it may be possible to create theme based on other - // design systems, such as Material 3. - // - // Please note: you do not need to include the 'base' mixins, if you include the corresponding - // 'theme' mixin elsewhere in your Sass. The full 'theme' mixins already include the base styles. - // - // To learn more about "base" theme styles visit our theming guide: - // https://material.angular.io/guide/theming#theming-dimensions - // - // TODO(v17): Please move these @include statements to the preferred place in your Sass, and pass - // your theme to them. This will ensure the correct values for your app are included. - @include mat.slider-base(/* TODO(v17): pass $your-theme here */); - - @include mat.slider-color($light-theme); - } - `, - }, - ); - - defineTest( - 'should work across multiple files', - { - 'global.scss': ` - @use '@angular/material' as mat; - @use './theme'; - @use './typography'; - @include mat.core(); - @include theme.app-colors(); - @include theme.app-typography(); - `, - '_theme.scss': ` - @use '@angular/material' as mat; - $theme: (); - @mixin app-colors() { - @include mat.form-field-color($theme); - } - `, - '_typography.scss': ` - @use '@angular/material' as mat; - $typography: mat.define-typography-config(); - @mixin app-typography() { - @include mat.select-typography($typography); - } - `, - }, - { - 'global.scss': ` - @use '@angular/material' as mat; - @use './theme'; - @use './typography'; - @include mat.core(); - - // The following mixins include base theme styles that are only needed once per application. These - // theme styles do not depend on the color, typography, or density settings in your theme. However, - // these styles may differ depending on the theme's design system. Currently all themes use the - // Material 2 design system, but in the future it may be possible to create theme based on other - // design systems, such as Material 3. - // - // Please note: you do not need to include the 'base' mixins, if you include the corresponding - // 'theme' mixin elsewhere in your Sass. The full 'theme' mixins already include the base styles. - // - // To learn more about "base" theme styles visit our theming guide: - // https://material.angular.io/guide/theming#theming-dimensions - // - // TODO(v17): Please move these @include statements to the preferred place in your Sass, and pass - // your theme to them. This will ensure the correct values for your app are included. - @include mat.form-field-base(/* TODO(v17): pass $your-theme here */); - @include mat.select-base(/* TODO(v17): pass $your-theme here */); - - @include theme.app-colors(); - @include theme.app-typography(); - `, - '_theme.scss': ` - @use '@angular/material' as mat; - $theme: (); - @mixin app-colors() { - @include mat.form-field-color($theme); - } - `, - '_typography.scss': ` - @use '@angular/material' as mat; - $typography: mat.define-typography-config(); - @mixin app-typography() { - @include mat.select-typography($typography); - } - `, - }, - ); -});