From 4075d2b42f0e80f8f4d6149d3b76e9c500612868 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 29 May 2025 10:15:56 -0400 Subject: [PATCH] fix(@schematics/angular): remove karma config devkit package usages during application migration When performing an `ng update` to Angular v20, the application migration will detect the usage of the `karma` builder's `karmaConfig` option and attempt to remove usages of the no longer needed `@angular-devkit/build-angular` karma framework and plugin usage. While the karma framework usage will be specifically ignored when using the `@angular/build:karma` builder, the plugin usage leverages a direct `require` within the configuration file. Regardless of the ability of the builder to ignore, neither usage is needed with `@angular/build:karma` and removing the code aligns with what would be generated by `ng generate config karma`. --- .../use-application-builder/migration.ts | 38 +++++++- .../use-application-builder/migration_spec.ts | 97 +++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) diff --git a/packages/schematics/angular/migrations/use-application-builder/migration.ts b/packages/schematics/angular/migrations/use-application-builder/migration.ts index 396ba48430d2..6a59c212fa21 100644 --- a/packages/schematics/angular/migrations/use-application-builder/migration.ts +++ b/packages/schematics/angular/migrations/use-application-builder/migration.ts @@ -232,6 +232,8 @@ function updateProjects(tree: Tree, context: SchematicContext) { // Use @angular/build directly if there is no devkit package usage if (!hasAngularDevkitUsage) { + const karmaConfigFiles = new Set(); + for (const [, target] of allWorkspaceTargets(workspace)) { switch (target.builder) { case Builders.Application: @@ -245,9 +247,15 @@ function updateProjects(tree: Tree, context: SchematicContext) { break; case Builders.Karma: target.builder = '@angular/build:karma'; - // Remove "builderMode" option since the builder will always use "application" for (const [, karmaOptions] of allTargetOptions(target)) { + // Remove "builderMode" option since the builder will always use "application" delete karmaOptions['builderMode']; + + // Collect custom karma configurations for @angular-devkit/build-angular plugin removal + const karmaConfig = karmaOptions['karmaConfig']; + if (karmaConfig && typeof karmaConfig === 'string') { + karmaConfigFiles.add(karmaConfig); + } } break; case Builders.NgPackagr: @@ -292,6 +300,34 @@ function updateProjects(tree: Tree, context: SchematicContext) { }), ); } + + for (const karmaConfigFile of karmaConfigFiles) { + if (!tree.exists(karmaConfigFile)) { + continue; + } + + try { + const originalKarmaConfigText = tree.readText(karmaConfigFile); + const updatedKarmaConfigText = originalKarmaConfigText + .replaceAll(`require('@angular-devkit/build-angular/plugins/karma'),`, '') + .replaceAll(`require('@angular-devkit/build-angular/plugins/karma')`, ''); + + if (updatedKarmaConfigText.includes('@angular-devkit/build-angular/plugins')) { + throw new Error( + 'Migration does not support found usage of "@angular-devkit/build-angular".', + ); + } else { + tree.overwrite(karmaConfigFile, updatedKarmaConfigText); + } + } catch (error) { + const reason = error instanceof Error ? `Reason: ${error.message}` : ''; + context.logger.warn( + `Unable to update custom karma configuration file ("${karmaConfigFile}"). ` + + reason + + '\nReferences to the "@angular-devkit/build-angular" package within the file may need to be removed manually.', + ); + } + } } return chain(rules); diff --git a/packages/schematics/angular/migrations/use-application-builder/migration_spec.ts b/packages/schematics/angular/migrations/use-application-builder/migration_spec.ts index 3adef7d419eb..a8d50193958e 100644 --- a/packages/schematics/angular/migrations/use-application-builder/migration_spec.ts +++ b/packages/schematics/angular/migrations/use-application-builder/migration_spec.ts @@ -130,6 +130,103 @@ describe(`Migration to use the application builder`, () => { expect(builderMode).toBeUndefined(); }); + it(`should update file for 'karmaConfig' karma option (no require trailing comma)`, async () => { + addWorkspaceTarget(tree, 'test', { + 'builder': Builders.Karma, + 'options': { + 'karmaConfig': './karma.conf.js', + 'polyfills': ['zone.js', 'zone.js/testing'], + 'tsConfig': 'projects/app-a/tsconfig.spec.json', + }, + }); + 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'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ] + }); + };`, + ); + + const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); + const { + projects: { app }, + } = JSON.parse(newTree.readContent('/angular.json')); + + const { karmaConfig } = app.architect['test'].options; + expect(karmaConfig).toBe('./karma.conf.js'); + + const karmaConfigText = newTree.readText('./karma.conf.js'); + expect(karmaConfigText).not.toContain(`require('@angular-devkit/build-angular/plugins/karma')`); + }); + + it(`should update file for 'karmaConfig' karma option (require trailing comma)`, async () => { + addWorkspaceTarget(tree, 'test', { + 'builder': Builders.Karma, + 'options': { + 'karmaConfig': './karma.conf.js', + 'polyfills': ['zone.js', 'zone.js/testing'], + 'tsConfig': 'projects/app-a/tsconfig.spec.json', + }, + }); + 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'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma'), + ] + }); + };`, + ); + + const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); + const { + projects: { app }, + } = JSON.parse(newTree.readContent('/angular.json')); + + const { karmaConfig } = app.architect['test'].options; + expect(karmaConfig).toBe('./karma.conf.js'); + + const karmaConfigText = newTree.readText('./karma.conf.js'); + expect(karmaConfigText).not.toContain(`require('@angular-devkit/build-angular/plugins/karma')`); + }); + + it(`should ignore missing file for 'karmaConfig' karma option`, async () => { + addWorkspaceTarget(tree, 'test', { + 'builder': Builders.Karma, + 'options': { + 'karmaConfig': './karma.conf.js', + 'polyfills': ['zone.js', 'zone.js/testing'], + 'tsConfig': 'projects/app-a/tsconfig.spec.json', + }, + }); + + const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); + const { + projects: { app }, + } = JSON.parse(newTree.readContent('/angular.json')); + + const { karmaConfig } = app.architect['test'].options; + expect(karmaConfig).toBe('./karma.conf.js'); + }); + it('should remove tilde prefix from CSS @import specifiers', async () => { // Replace outputPath tree.create(