diff --git a/packages/core/schematics/BUILD.bazel b/packages/core/schematics/BUILD.bazel index cfd2d6912580c..6f99228d7af7a 100644 --- a/packages/core/schematics/BUILD.bazel +++ b/packages/core/schematics/BUILD.bazel @@ -13,6 +13,7 @@ pkg_npm( "//packages/core/schematics/migrations/abstract-control-parent", "//packages/core/schematics/migrations/activated-route-snapshot-fragment", "//packages/core/schematics/migrations/can-activate-with-redirect-to", + "//packages/core/schematics/migrations/deep-shadow-piercing-selector", "//packages/core/schematics/migrations/dynamic-queries", "//packages/core/schematics/migrations/initial-navigation", "//packages/core/schematics/migrations/missing-injectable", diff --git a/packages/core/schematics/migrations.json b/packages/core/schematics/migrations.json index 66bcf0ec9647e..a077e0fd38618 100644 --- a/packages/core/schematics/migrations.json +++ b/packages/core/schematics/migrations.json @@ -94,6 +94,11 @@ "version": "12.0.0-next.6", "description": "`XhrFactory` has been moved from `@angular/common/http` to `@angular/common`.", "factory": "./migrations/xhr-factory/index" + }, + "migration-v12-deep-shadow-piercing-selector": { + "version": "12.0.2", + "description": "Automatically migrates shadow-piercing selector from `/deep/` to the recommanded alternative `::ng-deep`.", + "factory": "./migrations/deep-shadow-piercing-selector/index" } } } diff --git a/packages/core/schematics/migrations/deep-shadow-piercing-selector/BUILD.bazel b/packages/core/schematics/migrations/deep-shadow-piercing-selector/BUILD.bazel new file mode 100644 index 0000000000000..00f141fcd4a2a --- /dev/null +++ b/packages/core/schematics/migrations/deep-shadow-piercing-selector/BUILD.bazel @@ -0,0 +1,16 @@ +load("//tools:defaults.bzl", "ts_library") + +ts_library( + name = "deep-shadow-piercing-selector", + srcs = glob(["**/*.ts"]), + tsconfig = "//packages/core/schematics:tsconfig.json", + visibility = [ + "//packages/core/schematics:__pkg__", + "//packages/core/schematics/migrations/google3:__pkg__", + "//packages/core/schematics/test:__pkg__", + ], + deps = [ + "@npm//@angular-devkit/core", + "@npm//@angular-devkit/schematics", + ], +) diff --git a/packages/core/schematics/migrations/deep-shadow-piercing-selector/README.md b/packages/core/schematics/migrations/deep-shadow-piercing-selector/README.md new file mode 100644 index 0000000000000..4215e54ec5333 --- /dev/null +++ b/packages/core/schematics/migrations/deep-shadow-piercing-selector/README.md @@ -0,0 +1,17 @@ +## shadow-piercing selector `/deep/` to `::ng-deep` + +Automatically migrates shadow-piercing selector from `/deep/` to `::ng-deep`. + +#### Before +```css +:host /deep/ * { + cursor: pointer; +} +``` + +#### After +```css +:host ::ng-deep * { + cursor: pointer; +} +``` diff --git a/packages/core/schematics/migrations/deep-shadow-piercing-selector/index.ts b/packages/core/schematics/migrations/deep-shadow-piercing-selector/index.ts new file mode 100644 index 0000000000000..0e84ebbfeffa1 --- /dev/null +++ b/packages/core/schematics/migrations/deep-shadow-piercing-selector/index.ts @@ -0,0 +1,41 @@ +/** + * @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, join} from '@angular-devkit/core'; +import {DirEntry, Rule} from '@angular-devkit/schematics'; + +const VALID_EXTENSIONS = ['.scss', '.sass', '.css', '.styl', '.less', '.ts']; + +function* visitFiles(directory: DirEntry): IterableIterator { + for (const path of directory.subfiles) { + const extension = extname(path); + if (VALID_EXTENSIONS.includes(extension)) { + yield join(directory.path, path); + } + } + + for (const path of directory.subdirs) { + if (path === 'node_modules' || path.startsWith('.') || path === 'dist') { + continue; + } + + yield* visitFiles(directory.dir(path)); + } +} + +export default function(): Rule { + return (tree) => { + // Visit all files in an Angular workspace monorepo. + for (const file of visitFiles(tree.root)) { + const content = tree.read(file)?.toString(); + if (content?.includes('/deep/ ')) { + tree.overwrite(file, content.replace(/\/deep\/ /g, '::ng-deep ')); + } + } + }; +} diff --git a/packages/core/schematics/test/BUILD.bazel b/packages/core/schematics/test/BUILD.bazel index 135467711d158..0904e31590c2a 100644 --- a/packages/core/schematics/test/BUILD.bazel +++ b/packages/core/schematics/test/BUILD.bazel @@ -11,6 +11,7 @@ ts_library( "//packages/core/schematics/migrations/abstract-control-parent", "//packages/core/schematics/migrations/activated-route-snapshot-fragment", "//packages/core/schematics/migrations/can-activate-with-redirect-to", + "//packages/core/schematics/migrations/deep-shadow-piercing-selector", "//packages/core/schematics/migrations/dynamic-queries", "//packages/core/schematics/migrations/initial-navigation", "//packages/core/schematics/migrations/missing-injectable", diff --git a/packages/core/schematics/test/deep-shadow-piercing-selector_spec.ts b/packages/core/schematics/test/deep-shadow-piercing-selector_spec.ts new file mode 100644 index 0000000000000..912642c5a4fa0 --- /dev/null +++ b/packages/core/schematics/test/deep-shadow-piercing-selector_spec.ts @@ -0,0 +1,69 @@ +/** + * @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 {tags} from '@angular-devkit/core'; +import {EmptyTree} from '@angular-devkit/schematics'; +import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing'; + +describe('`/deep/` to `::ng-deep` migration', () => { + let tree: UnitTestTree; + const runner = new SchematicTestRunner('test', require.resolve('../migrations.json')); + + beforeEach(() => { + tree = new UnitTestTree(new EmptyTree()); + }); + + it(`should replace '/deep/' with '::ng-deep' in inline component styles`, async () => { + const fileName = '/index.ts'; + const getFileContent = (contentToReplace: string) => tags.stripIndents` + import { Component } from '@angular/core'; + + @Component({ + selector: 'app-root', + template: '', + styles: ['` + + contentToReplace + `'], + }) + export class AppComponent { } + `; + + tree.create(fileName, getFileContent(':host /deep/ * { cursor: pointer; }')); + await runMigration(); + expect(tree.readContent(fileName)) + .toBe(getFileContent(':host ::ng-deep * { cursor: pointer; }')); + }); + + for (const styleExtension of ['scss', 'sass', 'css', 'styl', 'less']) { + it(`should replace '/deep/' with '::ng-deep' in ${styleExtension} file`, async () => { + const fileName = `/index.${styleExtension}`; + tree.create(fileName, ':host /deep/ * { cursor: pointer; }'); + await runMigration(); + expect(tree.readContent(fileName)).toBe(':host ::ng-deep * { cursor: pointer; }'); + }); + } + + it(`should replace '/deep/' with '::ng-deep' when used as root selector`, async () => { + const fileName = '/index.css'; + tree.create(fileName, '/deep/ * { cursor: pointer; }'); + await runMigration(); + expect(tree.readContent(fileName)).toBe('::ng-deep * { cursor: pointer; }'); + }); + + it(`should not replace '/deep/' with '::ng-deep' in unknown file extension`, async () => { + const fileName = '/index.foo'; + const content = 'this is a not /deep/ selector'; + tree.create(fileName, content); + await runMigration(); + expect(tree.readContent(fileName)).toBe(content); + }); + + async function runMigration(): Promise { + await runner.runSchematicAsync('migration-v12-deep-shadow-piercing-selector', {}, tree) + .toPromise(); + } +});