From 21a9d1abb3284a6180c1936348d701cbca901c52 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 12 Aug 2025 19:11:57 -0400 Subject: [PATCH] feat(@schematics/angular): add migration to remove default Karma configurations Introduces a new migration that removes `karma.conf.js` files from projects if they contain only the default generated configuration. When a file is removed, the corresponding `karmaConfig` option in `angular.json` is also removed from the project's test target. The Angular CLI now provides a default Karma configuration internally, making explicit configuration files unnecessary for projects that do not require customization. This migration helps to clean up and simplify existing projects by removing these now redundant files. The migration will safely skip any `karma.conf.js` file that has been modified or contains values that cannot be reliably analyzed. --- .../migrations/karma/karma-config-analyzer.ts | 9 + .../karma/karma-config-analyzer_spec.ts | 4 +- .../migrations/karma/karma-config-comparer.ts | 41 +- .../angular/migrations/karma/migration.ts | 74 +++ .../migrations/karma/migration_spec.ts | 458 ++++++++++++++++++ .../migrations/migration-collection.json | 5 + 6 files changed, 576 insertions(+), 15 deletions(-) create mode 100644 packages/schematics/angular/migrations/karma/migration.ts create mode 100644 packages/schematics/angular/migrations/karma/migration_spec.ts diff --git a/packages/schematics/angular/migrations/karma/karma-config-analyzer.ts b/packages/schematics/angular/migrations/karma/karma-config-analyzer.ts index 0df221099568..cd9d18c12c3d 100644 --- a/packages/schematics/angular/migrations/karma/karma-config-analyzer.ts +++ b/packages/schematics/angular/migrations/karma/karma-config-analyzer.ts @@ -119,6 +119,15 @@ export function analyzeKarmaConfig(content: string): KarmaConfigAnalysis { } case ts.SyntaxKind.PropertyAccessExpression: { const propAccessExpr = node as ts.PropertyAccessExpression; + + // Handle config constants like `config.LOG_INFO` + if ( + ts.isIdentifier(propAccessExpr.expression) && + propAccessExpr.expression.text === 'config' + ) { + return `config.${propAccessExpr.name.text}`; + } + const value = extractValue(propAccessExpr.expression); if (isRequireInfo(value)) { const currentExport = value.export diff --git a/packages/schematics/angular/migrations/karma/karma-config-analyzer_spec.ts b/packages/schematics/angular/migrations/karma/karma-config-analyzer_spec.ts index 51e3dd473a6b..79af125c8690 100644 --- a/packages/schematics/angular/migrations/karma/karma-config-analyzer_spec.ts +++ b/packages/schematics/angular/migrations/karma/karma-config-analyzer_spec.ts @@ -73,8 +73,8 @@ describe('Karma Config Analyzer', () => { expect(dirInfo.isCall).toBe(true); expect(dirInfo.arguments as unknown).toEqual(['__dirname', './coverage/test-project']); - // config.LOG_INFO is a variable, so it should be flagged as unsupported - expect(hasUnsupportedValues).toBe(true); + expect(settings.get('logLevel') as unknown).toBe('config.LOG_INFO'); + expect(hasUnsupportedValues).toBe(false); }); it('should return an empty map for an empty config file', () => { diff --git a/packages/schematics/angular/migrations/karma/karma-config-comparer.ts b/packages/schematics/angular/migrations/karma/karma-config-comparer.ts index f81bf691f3c8..985a6d61236d 100644 --- a/packages/schematics/angular/migrations/karma/karma-config-comparer.ts +++ b/packages/schematics/angular/migrations/karma/karma-config-comparer.ts @@ -12,23 +12,33 @@ import { isDeepStrictEqual } from 'node:util'; import { relativePathToWorkspaceRoot } from '../../utility/paths'; import { KarmaConfigAnalysis, KarmaConfigValue, analyzeKarmaConfig } from './karma-config-analyzer'; +/** + * Represents the difference between two Karma configurations. + */ export interface KarmaConfigDiff { + /** A map of settings that were added in the project's configuration. */ added: Map; + + /** A map of settings that were removed from the project's configuration. */ removed: Map; + + /** A map of settings that were modified between the two configurations. */ modified: Map; + + /** A boolean indicating if the comparison is reliable (i.e., no unsupported values were found). */ isReliable: boolean; } /** * Generates the default Karma configuration file content as a string. - * @param relativePathToWorkspaceRoot The relative path from the project root to the workspace root. - * @param folderName The name of the project folder. + * @param relativePathToWorkspaceRoot The relative path from the Karma config file to the workspace root. + * @param projectName The name of the project. * @param needDevkitPlugin A boolean indicating if the devkit plugin is needed. * @returns The content of the default `karma.conf.js` file. */ export async function generateDefaultKarmaConfig( relativePathToWorkspaceRoot: string, - folderName: string, + projectName: string, needDevkitPlugin: boolean, ): Promise { const templatePath = path.join(__dirname, '../../config/files/karma.conf.js.template'); @@ -40,7 +50,7 @@ export async function generateDefaultKarmaConfig( /<%= relativePathToWorkspaceRoot %>/g, path.normalize(relativePathToWorkspaceRoot).replace(/\\/g, '/'), ) - .replace(/<%= folderName %>/g, folderName); + .replace(/<%= folderName %>/g, projectName); const devkitPluginRegex = /<% if \(needDevkitPlugin\) { %>(.*?)<% } %>/gs; const replacement = needDevkitPlugin ? '$1' : ''; @@ -52,8 +62,8 @@ export async function generateDefaultKarmaConfig( /** * Compares two Karma configuration analyses and returns the difference. * @param projectAnalysis The analysis of the project's configuration. - * @param defaultAnalysis The analysis of the default configuration. - * @returns A diff object representing the changes. + * @param defaultAnalysis The analysis of the default configuration to compare against. + * @returns A diff object representing the changes between the two configurations. */ export function compareKarmaConfigs( projectAnalysis: KarmaConfigAnalysis, @@ -93,8 +103,8 @@ export function compareKarmaConfigs( /** * Checks if there are any differences in the provided Karma configuration diff. - * @param diff The Karma configuration diff object. - * @returns True if there are any differences, false otherwise. + * @param diff The Karma configuration diff object to check. + * @returns True if there are any differences; false otherwise. */ export function hasDifferences(diff: KarmaConfigDiff): boolean { return diff.added.size > 0 || diff.removed.size > 0 || diff.modified.size > 0; @@ -103,33 +113,38 @@ export function hasDifferences(diff: KarmaConfigDiff): boolean { /** * Compares a project's Karma configuration with the default configuration. * @param projectConfigContent The content of the project's `karma.conf.js` file. - * @param projectRoot The root of the project's project. - * @param needDevkitPlugin A boolean indicating if the devkit plugin is needed. + * @param projectRoot The root directory of the project. + * @param needDevkitPlugin A boolean indicating if the devkit plugin is needed for the default config. + * @param karmaConfigPath The path to the Karma configuration file, used to resolve relative paths. * @returns A diff object representing the changes. */ export async function compareKarmaConfigToDefault( projectConfigContent: string, projectRoot: string, needDevkitPlugin: boolean, + karmaConfigPath?: string, ): Promise; /** * Compares a project's Karma configuration with the default configuration. * @param projectAnalysis The analysis of the project's configuration. - * @param projectRoot The root of the project's project. - * @param needDevkitPlugin A boolean indicating if the devkit plugin is needed. + * @param projectRoot The root directory of the project. + * @param needDevkitPlugin A boolean indicating if the devkit plugin is needed for the default config. + * @param karmaConfigPath The path to the Karma configuration file, used to resolve relative paths. * @returns A diff object representing the changes. */ export async function compareKarmaConfigToDefault( projectAnalysis: KarmaConfigAnalysis, projectRoot: string, needDevkitPlugin: boolean, + karmaConfigPath?: string, ): Promise; export async function compareKarmaConfigToDefault( projectConfigOrAnalysis: string | KarmaConfigAnalysis, projectRoot: string, needDevkitPlugin: boolean, + karmaConfigPath?: string, ): Promise { const projectAnalysis = typeof projectConfigOrAnalysis === 'string' @@ -137,7 +152,7 @@ export async function compareKarmaConfigToDefault( : projectConfigOrAnalysis; const defaultContent = await generateDefaultKarmaConfig( - relativePathToWorkspaceRoot(projectRoot), + relativePathToWorkspaceRoot(karmaConfigPath ? path.dirname(karmaConfigPath) : projectRoot), path.basename(projectRoot), needDevkitPlugin, ); diff --git a/packages/schematics/angular/migrations/karma/migration.ts b/packages/schematics/angular/migrations/karma/migration.ts new file mode 100644 index 000000000000..109cb5159a9c --- /dev/null +++ b/packages/schematics/angular/migrations/karma/migration.ts @@ -0,0 +1,74 @@ +/** + * @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.dev/license + */ + +import type { Rule, Tree } from '@angular-devkit/schematics'; +import { allTargetOptions, updateWorkspace } from '../../utility/workspace'; +import { Builders } from '../../utility/workspace-models'; +import { analyzeKarmaConfig } from './karma-config-analyzer'; +import { compareKarmaConfigToDefault, hasDifferences } from './karma-config-comparer'; + +function updateProjects(tree: Tree): Rule { + return updateWorkspace(async (workspace) => { + const removableKarmaConfigs = new Map(); + + for (const [, project] of workspace.projects) { + for (const [, target] of project.targets) { + let needDevkitPlugin = false; + switch (target.builder) { + case Builders.Karma: + needDevkitPlugin = true; + break; + case Builders.BuildKarma: + break; + default: + continue; + } + + for (const [, options] of allTargetOptions(target, false)) { + const karmaConfig = options['karmaConfig']; + if (typeof karmaConfig !== 'string') { + continue; + } + + let isRemovable = removableKarmaConfigs.get(karmaConfig); + if (isRemovable === undefined && tree.exists(karmaConfig)) { + const content = tree.readText(karmaConfig); + const analysis = analyzeKarmaConfig(content); + + if (analysis.hasUnsupportedValues) { + // Cannot safely determine if the file is removable. + isRemovable = false; + } else { + const diff = await compareKarmaConfigToDefault( + analysis, + project.root, + needDevkitPlugin, + karmaConfig, + ); + isRemovable = !hasDifferences(diff) && diff.isReliable; + } + + removableKarmaConfigs.set(karmaConfig, isRemovable); + + if (isRemovable) { + tree.delete(karmaConfig); + } + } + + if (isRemovable) { + delete options['karmaConfig']; + } + } + } + } + }); +} + +export default function (): Rule { + return updateProjects; +} diff --git a/packages/schematics/angular/migrations/karma/migration_spec.ts b/packages/schematics/angular/migrations/karma/migration_spec.ts new file mode 100644 index 000000000000..12a8786e97c8 --- /dev/null +++ b/packages/schematics/angular/migrations/karma/migration_spec.ts @@ -0,0 +1,458 @@ +/** + * @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.dev/license + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { EmptyTree } from '@angular-devkit/schematics'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; +import { latestVersions } from '../../utility/latest-versions'; + +const DEFAULT_KARMA_CONFIG_WITH_DEVKIT = ` +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with \`random: false\` + // or set a specific seed with \`seed: 4321\` + }, + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage/'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + browsers: ['Chrome'], + restartOnFileChange: true + }); +}; +`; + +const DEFAULT_KARMA_CONFIG_NO_DEVKIT = ` +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage') + ], + client: { + jasmine: {}, + }, + jasmineHtmlReporter: { + suppressAll: true + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage/'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + browsers: ['Chrome'], + restartOnFileChange: true + }); +}; +`; + +describe('Migration to remove default Karma configuration', () => { + const schematicRunner = new SchematicTestRunner( + 'migrations', + require.resolve('../migration-collection.json'), + ); + + let tree: UnitTestTree; + beforeEach(() => { + tree = new UnitTestTree(new EmptyTree()); + tree.create( + '/package.json', + JSON.stringify({ + devDependencies: { + '@angular-devkit/build-angular': latestVersions.DevkitBuildAngular, + }, + }), + ); + tree.create( + '/angular.json', + JSON.stringify({ + version: 1, + projects: { + app: { + root: '', + sourceRoot: 'src', + projectType: 'application', + targets: { + test: { + builder: '@angular-devkit/build-angular:karma', + options: { + karmaConfig: 'karma.conf.js', + }, + }, + }, + }, + }, + }), + ); + }); + + it('should delete default karma.conf.js and remove karmaConfig option', async () => { + tree.create('karma.conf.js', DEFAULT_KARMA_CONFIG_WITH_DEVKIT); + + const newTree = await schematicRunner.runSchematic('remove-default-karma-config', {}, tree); + const { projects } = newTree.readJson('/angular.json') as any; + expect(projects.app.targets.test.options.karmaConfig).toBeUndefined(); + expect(newTree.exists('karma.conf.js')).toBeFalse(); + }); + + it('should not delete modified karma.conf.js', async () => { + tree.create( + 'karma.conf.js', + ` +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + ], + reporters: ['progress'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + }); +}; +`, + ); + + const newTree = await schematicRunner.runSchematic('remove-default-karma-config', {}, tree); + const { projects } = newTree.readJson('/angular.json') as any; + expect(projects.app.targets.test.options.karmaConfig).toBe('karma.conf.js'); + expect(newTree.exists('karma.conf.js')).toBeTrue(); + }); + + it('should not delete karma.conf.js with unsupported values', async () => { + tree.create( + 'karma.conf.js', + ` +const myPlugin = require('my-plugin'); +module.exports = function (config) { + config.set({ + plugins: [myPlugin], + }); +}; +`, + ); + + const newTree = await schematicRunner.runSchematic('remove-default-karma-config', {}, tree); + const { projects } = newTree.readJson('/angular.json') as any; + expect(projects.app.targets.test.options.karmaConfig).toBe('karma.conf.js'); + expect(newTree.exists('karma.conf.js')).toBeTrue(); + }); + + it('should handle multiple projects referencing the same karma.conf.js', async () => { + let { projects } = tree.readJson('/angular.json') as any; + projects['app2'] = { + root: 'app2', + sourceRoot: 'app2/src', + projectType: 'application', + targets: { + test: { + builder: '@angular-devkit/build-angular:karma', + options: { + karmaConfig: 'karma.conf.js', + }, + }, + }, + }; + tree.overwrite('/angular.json', JSON.stringify({ version: 1, projects })); + tree.create('karma.conf.js', DEFAULT_KARMA_CONFIG_WITH_DEVKIT); + + const newTree = await schematicRunner.runSchematic('remove-default-karma-config', {}, tree); + projects = (newTree.readJson('/angular.json') as any).projects; + expect(projects.app.targets.test.options.karmaConfig).toBeUndefined(); + expect(projects.app2.targets.test.options.karmaConfig).toBeUndefined(); + expect(newTree.exists('karma.conf.js')).toBeFalse(); + }); + + it('should not error for a non-existent karma config file', async () => { + const { projects } = tree.readJson('/angular.json') as any; + projects.app.targets.test.options.karmaConfig = 'karma.non-existent.conf.js'; + tree.overwrite('/angular.json', JSON.stringify({ version: 1, projects })); + + const newTree = await schematicRunner.runSchematic('remove-default-karma-config', {}, tree); + const { projects: newProjects } = newTree.readJson('/angular.json') as any; + expect(newProjects.app.targets.test.options.karmaConfig).toBe('karma.non-existent.conf.js'); + }); + + it('should work for library projects', async () => { + const { projects } = tree.readJson('/angular.json') as any; + projects.app.projectType = 'library'; + tree.overwrite('/angular.json', JSON.stringify({ version: 1, projects })); + tree.create( + 'karma.conf.js', + // NOTE: The client block is slightly different in this test case. + ` +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: {}, + }, + jasmineHtmlReporter: { + suppressAll: true + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage/'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + browsers: ['Chrome'], + restartOnFileChange: true + }); +}; +`, + ); + + const newTree = await schematicRunner.runSchematic('remove-default-karma-config', {}, tree); + const { projects: newProjects } = newTree.readJson('/angular.json') as any; + expect(newProjects.app.targets.test.options.karmaConfig).toBeUndefined(); + expect(newTree.exists('karma.conf.js')).toBeFalse(); + }); + + it('should handle multiple configurations in the test target', async () => { + const { projects } = tree.readJson('/angular.json') as any; + projects.app.targets.test.configurations = { + ci: { + karmaConfig: 'karma.ci.conf.js', + }, + }; + tree.overwrite('/angular.json', JSON.stringify({ version: 1, projects })); + tree.create( + 'karma.conf.js', + // NOTE: The client block is slightly different in this test case. + ` +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: {}, + }, + jasmineHtmlReporter: { + suppressAll: true + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage/'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + browsers: ['Chrome'], + restartOnFileChange: true + }); +}; +`, + ); + tree.create( + 'karma.ci.conf.js', + ` +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + browsers: ['ChromeHeadless'], + }); +}; +`, + ); + + const newTree = await schematicRunner.runSchematic('remove-default-karma-config', {}, tree); + const { projects: newProjects } = newTree.readJson('/angular.json') as any; + expect(newProjects.app.targets.test.options.karmaConfig).toBeUndefined(); + expect(newProjects.app.targets.test.configurations.ci.karmaConfig).toBe('karma.ci.conf.js'); + expect(newTree.exists('karma.conf.js')).toBeFalse(); + expect(newTree.exists('karma.ci.conf.js')).toBeTrue(); + }); + + it('should handle karma config in a subdirectory', async () => { + const { projects } = tree.readJson('/angular.json') as any; + projects.app.targets.test.options.karmaConfig = 'src/karma.conf.js'; + tree.overwrite('/angular.json', JSON.stringify({ version: 1, projects })); + tree.create( + 'src/karma.conf.js', + ` +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: {}, + }, + jasmineHtmlReporter: { + suppressAll: true + }, + coverageReporter: { + dir: require('path').join(__dirname, '../coverage/'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + browsers: ['Chrome'], + restartOnFileChange: true + }); +}; +`, + ); + + const newTree = await schematicRunner.runSchematic('remove-default-karma-config', {}, tree); + const { projects: newProjects } = newTree.readJson('/angular.json') as any; + expect(newProjects.app.targets.test.options.karmaConfig).toBeUndefined(); + expect(newTree.exists('src/karma.conf.js')).toBeFalse(); + }); + + it('should not delete almost default karma.conf.js', async () => { + tree.create( + 'karma.conf.js', + ` +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: {}, + }, + jasmineHtmlReporter: { + suppressAll: true + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage/'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + browsers: ['Chrome'], + restartOnFileChange: true, + singleRun: true + }); +}; +`, + ); + + const newTree = await schematicRunner.runSchematic('remove-default-karma-config', {}, tree); + const { projects } = newTree.readJson('/angular.json') as any; + expect(projects.app.targets.test.options.karmaConfig).toBe('karma.conf.js'); + expect(newTree.exists('karma.conf.js')).toBeTrue(); + }); + + it('should delete default karma.conf.js when devkit is not used', async () => { + tree.overwrite( + '/package.json', + JSON.stringify({ + devDependencies: { + '@angular/build': latestVersions.AngularBuild, + }, + }), + ); + const { projects } = tree.readJson('/angular.json') as any; + projects.app.targets.test.builder = '@angular/build:karma'; + tree.overwrite('/angular.json', JSON.stringify({ version: 1, projects })); + tree.create('karma.conf.js', DEFAULT_KARMA_CONFIG_NO_DEVKIT); + + const newTree = await schematicRunner.runSchematic('remove-default-karma-config', {}, tree); + const { projects: newProjects } = newTree.readJson('/angular.json') as any; + expect(newProjects.app.targets.test.options.karmaConfig).toBeUndefined(); + expect(newTree.exists('karma.conf.js')).toBeFalse(); + }); +}); diff --git a/packages/schematics/angular/migrations/migration-collection.json b/packages/schematics/angular/migrations/migration-collection.json index 1de0910aa50a..ec0311d27d97 100644 --- a/packages/schematics/angular/migrations/migration-collection.json +++ b/packages/schematics/angular/migrations/migration-collection.json @@ -27,6 +27,11 @@ "optional": true, "recommended": true, "documentation": "tools/cli/build-system-migration" + }, + "remove-default-karma-config": { + "version": "20.2.0", + "factory": "./karma/migration", + "description": "Remove any karma configuration files that only contain the default content. The default configuration is automatically available without a specific project file." } } }