From 9beb878e2eecd32e499c8af557f22f46548248fc Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Wed, 14 Sep 2022 07:48:19 +0000 Subject: [PATCH] feat(@schematics/angular): remove Browserslist configuration files from projects The Browserslist configuration file is redundant as we set the defaults directly in @angular-devkit/build-angular. https://github.com/angular/angular-cli/blob/8da926966e9f414ceecf60b89acd475ce1b55fc5/packages/angular_devkit/build_angular/src/utils/supported-browsers.ts#L12-L19 With this commit, we remove the `.browserlistrc` configuration file from the schematics application template and through a migration in existing projects when the Browserslist query result matches the default. Users needing a finer grain support should still create a `.browserlistrc` in the root directory of the project. --- .../browser/specs/browser-support_spec.ts | 4 +- packages/schematics/angular/BUILD.bazel | 1 + .../files/.browserslistrc.template | 16 ---- .../angular/application/index_spec.ts | 11 ++- .../migrations/migration-collection.json | 8 +- .../update-15/remove-browserslist-config.ts | 90 +++++++++++++++++++ .../remove-browserslist-config_spec.ts | 84 +++++++++++++++++ 7 files changed, 190 insertions(+), 24 deletions(-) delete mode 100644 packages/schematics/angular/application/files/.browserslistrc.template create mode 100644 packages/schematics/angular/migrations/update-15/remove-browserslist-config.ts create mode 100644 packages/schematics/angular/migrations/update-15/remove-browserslist-config_spec.ts diff --git a/packages/angular_devkit/build_angular/src/builders/browser/specs/browser-support_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/specs/browser-support_spec.ts index d6649879cd32..c812c3227671 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser/specs/browser-support_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser/specs/browser-support_spec.ts @@ -21,7 +21,9 @@ describe('Browser Builder browser support', () => { afterEach(async () => host.restore().toPromise()); it('warns when IE is present in browserslist', async () => { - host.appendToFile('.browserslistrc', '\nIE 9'); + host.writeMultipleFiles({ + '.browserslistrc': '\nIE 9', + }); const logger = new logging.Logger(''); const logs: string[] = []; diff --git a/packages/schematics/angular/BUILD.bazel b/packages/schematics/angular/BUILD.bazel index d9ca542310eb..8210ad2207ab 100644 --- a/packages/schematics/angular/BUILD.bazel +++ b/packages/schematics/angular/BUILD.bazel @@ -78,6 +78,7 @@ ts_library( "//packages/angular_devkit/schematics/tasks", "//packages/schematics/angular/third_party/github.com/Microsoft/TypeScript", "@npm//@types/node", + "@npm//browserslist", "@npm//jsonc-parser", ], ) diff --git a/packages/schematics/angular/application/files/.browserslistrc.template b/packages/schematics/angular/application/files/.browserslistrc.template deleted file mode 100644 index 4f9ac26980c1..000000000000 --- a/packages/schematics/angular/application/files/.browserslistrc.template +++ /dev/null @@ -1,16 +0,0 @@ -# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. -# For additional information regarding the format and rule options, please see: -# https://github.com/browserslist/browserslist#queries - -# For the full list of supported browsers by the Angular framework, please see: -# https://angular.io/guide/browser-support - -# You can see what browsers were selected by your queries by running: -# npx browserslist - -last 1 Chrome version -last 1 Firefox version -last 2 Edge major versions -last 2 Safari major versions -last 2 iOS major versions -Firefox ESR diff --git a/packages/schematics/angular/application/index_spec.ts b/packages/schematics/angular/application/index_spec.ts index 458f106d1d8f..0e1bfc6f0665 100644 --- a/packages/schematics/angular/application/index_spec.ts +++ b/packages/schematics/angular/application/index_spec.ts @@ -547,7 +547,7 @@ describe('Application Schematic', () => { const tree = await schematicRunner .runSchematicAsync('application', options, workspaceTree) .toPromise(); - const exists = tree.exists('/projects/my-cool/.browserslistrc'); + const exists = tree.exists('/projects/my-cool/tsconfig.app.json'); expect(exists).toBeTrue(); }); @@ -556,7 +556,7 @@ describe('Application Schematic', () => { const tree = await schematicRunner .runSchematicAsync('application', options, workspaceTree) .toPromise(); - const exists = tree.exists('/projects/foo/my-cool/.browserslistrc'); + const exists = tree.exists('/projects/foo/my-cool/tsconfig.app.json'); expect(exists).toBeTrue(); }); @@ -565,7 +565,7 @@ describe('Application Schematic', () => { const tree = await schematicRunner .runSchematicAsync('application', options, workspaceTree) .toPromise(); - const exists = tree.exists('/projects/my-cool/.browserslistrc'); + const exists = tree.exists('/projects/my-cool/tsconfig.app.json'); expect(exists).toBeTrue(); }); @@ -574,7 +574,7 @@ describe('Application Schematic', () => { const tree = await schematicRunner .runSchematicAsync('application', options, workspaceTree) .toPromise(); - const exists = tree.exists('/projects/foo/my-cool/.browserslistrc'); + const exists = tree.exists('/projects/foo/my-cool/tsconfig.app.json'); expect(exists).toBeTrue(); }); @@ -584,8 +584,7 @@ describe('Application Schematic', () => { .runSchematicAsync('application', options, workspaceTree) .toPromise(); - const exists = tree.exists('/projects/foo.bar_buz/.browserslistrc'); - expect(exists).toBeTrue(); + expect(tree.exists('/projects/foo.bar_buz/tsconfig.app.json')).toBeTrue(); }); it('should support creating scoped application', async () => { diff --git a/packages/schematics/angular/migrations/migration-collection.json b/packages/schematics/angular/migrations/migration-collection.json index 63001b445889..0e5d79a4bf99 100644 --- a/packages/schematics/angular/migrations/migration-collection.json +++ b/packages/schematics/angular/migrations/migration-collection.json @@ -1,3 +1,9 @@ { - "schematics": {} + "schematics": { + "remove-browserslist-config": { + "version": "15.0.0", + "factory": "./update-15/remove-browserslist-config", + "description": "Remove Browserslist configuration files that matches the Angular CLI default configuration." + } + } } diff --git a/packages/schematics/angular/migrations/update-15/remove-browserslist-config.ts b/packages/schematics/angular/migrations/update-15/remove-browserslist-config.ts new file mode 100644 index 000000000000..0d17ecd80301 --- /dev/null +++ b/packages/schematics/angular/migrations/update-15/remove-browserslist-config.ts @@ -0,0 +1,90 @@ +/** + * @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 { Path, join } from '@angular-devkit/core'; +import { DirEntry, Rule } from '@angular-devkit/schematics'; + +const validBrowserslistConfigFilenames = new Set(['browserslist', '.browserslistrc']); + +export const DEFAULT_BROWSERS = [ + 'last 1 Chrome version', + 'last 1 Firefox version', + 'last 2 Edge major versions', + 'last 2 Safari major versions', + 'last 2 iOS major versions', + 'Firefox ESR', +]; + +function* visit(directory: DirEntry): IterableIterator { + for (const path of directory.subfiles) { + if (validBrowserslistConfigFilenames.has(path)) { + yield join(directory.path, path); + } + } + + for (const path of directory.subdirs) { + if (path === 'node_modules') { + continue; + } + + yield* visit(directory.dir(path)); + } +} + +export default function (): Rule { + return async (tree, { logger }) => { + let browserslist: typeof import('browserslist') | undefined; + + try { + browserslist = (await import('browserslist')).default; + } catch { + logger.warn('Skipping migration because the "browserslist" package could not be loaded.'); + + return; + } + + // Set the defaults to match the defaults in build-angular. + browserslist.defaults = DEFAULT_BROWSERS; + + const defaultSupportedBrowsers = new Set(browserslist(DEFAULT_BROWSERS)); + const es5Browsers = new Set(browserslist(['supports es6-module'])); + + for (const path of visit(tree.root)) { + const { defaults: browsersListConfig, ...otherConfigs } = browserslist.parseConfig( + tree.readText(path), + ); + + if (Object.keys(otherConfigs).length) { + // The config contains additional sections. + continue; + } + + const browserslistInProject = browserslist( + // Exclude from the list ES5 browsers which are not supported. + browsersListConfig.map((s) => `${s} and supports es6-module`), + { + ignoreUnknownVersions: true, + }, + ); + + if (defaultSupportedBrowsers.size !== browserslistInProject.length) { + continue; + } + + const shouldDelete = browserslistInProject.every((browser) => + defaultSupportedBrowsers.has(browser), + ); + + if (shouldDelete) { + // All browsers are the same as the default config. + // Delete file as it's redundant. + tree.delete(path); + } + } + }; +} diff --git a/packages/schematics/angular/migrations/update-15/remove-browserslist-config_spec.ts b/packages/schematics/angular/migrations/update-15/remove-browserslist-config_spec.ts new file mode 100644 index 000000000000..755aa32ad82a --- /dev/null +++ b/packages/schematics/angular/migrations/update-15/remove-browserslist-config_spec.ts @@ -0,0 +1,84 @@ +/** + * @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 { EmptyTree } from '@angular-devkit/schematics'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; +import { DEFAULT_BROWSERS } from './remove-browserslist-config'; + +describe('Migration to delete Browserslist configurations', () => { + const schematicName = 'remove-browserslist-config'; + + const schematicRunner = new SchematicTestRunner( + 'migrations', + require.resolve('../migration-collection.json'), + ); + + let tree: UnitTestTree; + + beforeEach(() => { + tree = new UnitTestTree(new EmptyTree()); + }); + + describe('given the Browserslist config matches the default', () => { + it('should delete ".browserslistrc" file', async () => { + tree.create('/src/app/.browserslistrc', DEFAULT_BROWSERS.join('\n')); + expect(tree.exists('/src/app/.browserslistrc')).toBeTrue(); + + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + expect(newTree.exists('/src/app/.browserslistrc')).toBeFalse(); + }); + + it(`should not delete "browserslist" in 'node_modules'`, async () => { + tree.create('/node_modules/browserslist', DEFAULT_BROWSERS.join('\n')); + tree.create('/node_modules/.browserslistrc', DEFAULT_BROWSERS.join('\n')); + + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + expect(newTree.exists('/node_modules/browserslist')).toBeTrue(); + expect(newTree.exists('/node_modules/.browserslistrc')).toBeTrue(); + }); + }); + + describe('given the Browserslist config does not match the default', () => { + it('should not delete "browserslist"', async () => { + tree.create('/src/app/browserslist', 'last 1 Chrome version'); + + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + expect(newTree.exists('/src/app/browserslist')).toBeTrue(); + }); + + it('should not delete ".browserslistrc"', async () => { + tree.create('/src/app/.browserslistrc', 'last 1 Chrome version'); + + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + expect(newTree.exists('/src/app/.browserslistrc')).toBeTrue(); + }); + + it('should delete ".browserslistrc" file when it only includes non supported ES5 browsers', async () => { + tree.create('/src/app/.browserslistrc', [...DEFAULT_BROWSERS, 'IE 10'].join('\n')); + expect(tree.exists('/src/app/.browserslistrc')).toBeTrue(); + + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + expect(newTree.exists('/src/app/.browserslistrc')).toBeFalse(); + }); + + it('should not delete ".browserslistrc" file when it includes additional config sections', async () => { + tree.create( + '/src/app/.browserslistrc', + ` + ${DEFAULT_BROWSERS.join('\n')} + [modern] + last 1 chrome version + `, + ); + expect(tree.exists('/src/app/.browserslistrc')).toBeTrue(); + + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + expect(newTree.exists('/src/app/.browserslistrc')).toBeTrue(); + }); + }); +});