From c2a1ad2a8c66a512e379f598209cc24ec46222ff Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 19 Mar 2025 14:16:25 -0400 Subject: [PATCH] fix(@schematics/angular): generate directives without a .directive extension/type To align with the updated style guide, Angular v20 will generate services without a `.directive` file extension type for all directive related files by default. Projects will automatically use this naming convention. Projects can however opt-out by setting the `type` option to `Directive` for the directive schematic. This can be done as a default in the `angular.json` or directly on the commandline via `--type=Directive` when executing `ng generate`. As an example, `example.directive.ts` will now be named `example.ts`. Additionally, the TypeScript class name will be `Example` instead of the previous `ExampleDirective`. --- ...ame@dasherize__.directive.spec.ts.template | 8 --- ...rize__.__type@dasherize__.spec.ts.template | 8 +++ ...asherize__.__type@dasherize__.ts.template} | 2 +- .../schematics/angular/directive/index.ts | 31 ++-------- .../angular/directive/index_spec.ts | 56 ++++++++++++------- .../schematics/angular/directive/schema.json | 4 ++ .../generate/directive/directive-basic.ts | 4 +- .../generate/directive/directive-prefix.ts | 13 +---- 8 files changed, 61 insertions(+), 65 deletions(-) delete mode 100644 packages/schematics/angular/directive/files/__name@dasherize@if-flat__/__name@dasherize__.directive.spec.ts.template create mode 100644 packages/schematics/angular/directive/files/__name@dasherize__.__type@dasherize__.spec.ts.template rename packages/schematics/angular/directive/files/{__name@dasherize@if-flat__/__name@dasherize__.directive.ts.template => __name@dasherize__.__type@dasherize__.ts.template} (73%) diff --git a/packages/schematics/angular/directive/files/__name@dasherize@if-flat__/__name@dasherize__.directive.spec.ts.template b/packages/schematics/angular/directive/files/__name@dasherize@if-flat__/__name@dasherize__.directive.spec.ts.template deleted file mode 100644 index d8e001680f09..000000000000 --- a/packages/schematics/angular/directive/files/__name@dasherize@if-flat__/__name@dasherize__.directive.spec.ts.template +++ /dev/null @@ -1,8 +0,0 @@ -import { <%= classify(name) %>Directive } from './<%= dasherize(name) %>.directive'; - -describe('<%= classify(name) %>Directive', () => { - it('should create an instance', () => { - const directive = new <%= classify(name) %>Directive(); - expect(directive).toBeTruthy(); - }); -}); diff --git a/packages/schematics/angular/directive/files/__name@dasherize__.__type@dasherize__.spec.ts.template b/packages/schematics/angular/directive/files/__name@dasherize__.__type@dasherize__.spec.ts.template new file mode 100644 index 000000000000..59bddc63660a --- /dev/null +++ b/packages/schematics/angular/directive/files/__name@dasherize__.__type@dasherize__.spec.ts.template @@ -0,0 +1,8 @@ +import { <%= classify(name) %><%= classify(type) %> } from './<%= dasherize(name) %><%= type ? '.' + dasherize(type) : '' %>'; + +describe('<%= classify(name) %><%= classify(type) %>', () => { + it('should create an instance', () => { + const directive = new <%= classify(name) %><%= classify(type) %>(); + expect(directive).toBeTruthy(); + }); +}); diff --git a/packages/schematics/angular/directive/files/__name@dasherize@if-flat__/__name@dasherize__.directive.ts.template b/packages/schematics/angular/directive/files/__name@dasherize__.__type@dasherize__.ts.template similarity index 73% rename from packages/schematics/angular/directive/files/__name@dasherize@if-flat__/__name@dasherize__.directive.ts.template rename to packages/schematics/angular/directive/files/__name@dasherize__.__type@dasherize__.ts.template index a63130fdf334..4e55f9d19e6b 100644 --- a/packages/schematics/angular/directive/files/__name@dasherize@if-flat__/__name@dasherize__.directive.ts.template +++ b/packages/schematics/angular/directive/files/__name@dasherize__.__type@dasherize__.ts.template @@ -4,7 +4,7 @@ import { Directive } from '@angular/core'; selector: '[<%= selector %>]'<% if(!standalone) {%>, standalone: false<%}%> }) -export class <%= classify(name) %>Directive { +export class <%= classify(name) %><%= classify(type) %> { constructor() { } diff --git a/packages/schematics/angular/directive/index.ts b/packages/schematics/angular/directive/index.ts index 97e6a9deaa18..0ef5e0c9ff8e 100644 --- a/packages/schematics/angular/directive/index.ts +++ b/packages/schematics/angular/directive/index.ts @@ -6,22 +6,10 @@ * found in the LICENSE file at https://angular.dev/license */ -import { - Rule, - SchematicsException, - Tree, - apply, - applyTemplates, - chain, - filter, - mergeWith, - move, - noop, - strings, - url, -} from '@angular-devkit/schematics'; +import { Rule, SchematicsException, Tree, chain, strings } from '@angular-devkit/schematics'; import { addDeclarationToNgModule } from '../utility/add-declaration-to-ng-module'; import { findModuleFromOptions } from '../utility/find-module'; +import { generateFromFiles } from '../utility/generate-from-files'; import { parseName } from '../utility/parse-name'; import { validateClassName, validateHtmlSelector } from '../utility/validation'; import { buildDefaultPath, getWorkspace } from '../utility/workspace'; @@ -52,6 +40,9 @@ export default function (options: DirectiveOptions): Rule { options.module = findModuleFromOptions(host, options); + // Schematic templates require a defined type value + options.type ??= ''; + const parsedPath = parseName(options.path, options.name); options.name = parsedPath.name; options.path = parsedPath.path; @@ -60,23 +51,13 @@ export default function (options: DirectiveOptions): Rule { validateHtmlSelector(options.selector); validateClassName(strings.classify(options.name)); - const templateSource = apply(url('./files'), [ - options.skipTests ? filter((path) => !path.endsWith('.spec.ts.template')) : noop(), - applyTemplates({ - ...strings, - 'if-flat': (s: string) => (options.flat ? '' : s), - ...options, - }), - move(parsedPath.path), - ]); - return chain([ addDeclarationToNgModule({ type: 'directive', ...options, }), - mergeWith(templateSource), + generateFromFiles(options), ]); }; } diff --git a/packages/schematics/angular/directive/index_spec.ts b/packages/schematics/angular/directive/index_spec.ts index 1fbdad0ef713..affb582fbb67 100644 --- a/packages/schematics/angular/directive/index_spec.ts +++ b/packages/schematics/angular/directive/index_spec.ts @@ -50,15 +50,15 @@ describe('Directive Schematic', () => { const tree = await schematicRunner.runSchematic('directive', options, appTree); const files = tree.files; - expect(files).toContain('/projects/bar/src/app/foo/foo.directive.spec.ts'); - expect(files).toContain('/projects/bar/src/app/foo/foo.directive.ts'); + expect(files).toContain('/projects/bar/src/app/foo/foo.spec.ts'); + expect(files).toContain('/projects/bar/src/app/foo/foo.ts'); }); it('should converts dash-cased-name to a camelCasedSelector', async () => { const options = { ...defaultOptions, name: 'my-dir' }; const tree = await schematicRunner.runSchematic('directive', options, appTree); - const content = tree.readContent('/projects/bar/src/app/my-dir.directive.ts'); + const content = tree.readContent('/projects/bar/src/app/my-dir.ts'); expect(content).toMatch(/selector: '\[appMyDir\]'/); }); @@ -66,7 +66,7 @@ describe('Directive Schematic', () => { const options = { ...defaultOptions, name: 'sub/test' }; appTree = await schematicRunner.runSchematic('directive', options, appTree); - const content = appTree.readContent('/projects/bar/src/app/sub/test.directive.ts'); + const content = appTree.readContent('/projects/bar/src/app/sub/test.ts'); expect(content).toMatch(/selector: '\[appTest\]'/); }); @@ -74,7 +74,7 @@ describe('Directive Schematic', () => { const options = { ...defaultOptions, prefix: 'pre' }; const tree = await schematicRunner.runSchematic('directive', options, appTree); - const content = tree.readContent('/projects/bar/src/app/foo.directive.ts'); + const content = tree.readContent('/projects/bar/src/app/foo.ts'); expect(content).toMatch(/selector: '\[preFoo\]'/); }); @@ -82,7 +82,7 @@ describe('Directive Schematic', () => { const options = { ...defaultOptions, prefix: undefined }; const tree = await schematicRunner.runSchematic('directive', options, appTree); - const content = tree.readContent('/projects/bar/src/app/foo.directive.ts'); + const content = tree.readContent('/projects/bar/src/app/foo.ts'); expect(content).toMatch(/selector: '\[appFoo\]'/); }); @@ -90,7 +90,7 @@ describe('Directive Schematic', () => { const options = { ...defaultOptions, prefix: '' }; const tree = await schematicRunner.runSchematic('directive', options, appTree); - const content = tree.readContent('/projects/bar/src/app/foo.directive.ts'); + const content = tree.readContent('/projects/bar/src/app/foo.ts'); expect(content).toMatch(/selector: '\[foo\]'/); }); @@ -99,16 +99,16 @@ describe('Directive Schematic', () => { const tree = await schematicRunner.runSchematic('directive', options, appTree); const files = tree.files; - expect(files).toContain('/projects/bar/src/app/foo.directive.ts'); - expect(files).not.toContain('/projects/bar/src/app/foo.directive.spec.ts'); + expect(files).toContain('/projects/bar/src/app/foo.ts'); + expect(files).not.toContain('/projects/bar/src/app/foo.spec.ts'); }); it('should create a standalone directive', async () => { const options = { ...defaultOptions, standalone: true }; const tree = await schematicRunner.runSchematic('directive', options, appTree); - const directiveContent = tree.readContent('/projects/bar/src/app/foo.directive.ts'); + const directiveContent = tree.readContent('/projects/bar/src/app/foo.ts'); expect(directiveContent).not.toContain('standalone'); - expect(directiveContent).toContain('class FooDirective'); + expect(directiveContent).toContain('class Foo'); }); it('should error when class name contains invalid characters', async () => { @@ -119,6 +119,24 @@ describe('Directive Schematic', () => { ).toBeRejectedWithError('Class name "404" is invalid.'); }); + it('should respect the type option', async () => { + const options = { ...defaultOptions, type: 'Directive' }; + const tree = await schematicRunner.runSchematic('directive', options, appTree); + const content = tree.readContent('/projects/bar/src/app/foo.directive.ts'); + const testContent = tree.readContent('/projects/bar/src/app/foo.directive.spec.ts'); + expect(content).toContain('export class FooDirective'); + expect(testContent).toContain("describe('FooDirective'"); + }); + + it('should allow empty string in the type option', async () => { + const options = { ...defaultOptions, type: '' }; + const tree = await schematicRunner.runSchematic('directive', options, appTree); + const content = tree.readContent('/projects/bar/src/app/foo.ts'); + const testContent = tree.readContent('/projects/bar/src/app/foo.spec.ts'); + expect(content).toContain('export class Foo'); + expect(testContent).toContain("describe('Foo'"); + }); + describe('standalone=false', () => { const defaultNonStandaloneOptions: DirectiveOptions = { ...defaultOptions, @@ -139,11 +157,11 @@ describe('Directive Schematic', () => { const tree = await schematicRunner.runSchematic('directive', options, appTree); const files = tree.files; - expect(files).toContain('/projects/baz/src/app/foo.directive.spec.ts'); - expect(files).toContain('/projects/baz/src/app/foo.directive.ts'); + expect(files).toContain('/projects/baz/src/app/foo.spec.ts'); + expect(files).toContain('/projects/baz/src/app/foo.ts'); const moduleContent = tree.readContent('/projects/baz/src/app/app.module.ts'); - expect(moduleContent).toMatch(/import.*Foo.*from '.\/foo.directive'/); - expect(moduleContent).toMatch(/declarations:\s*\[[^\]]+?,\r?\n\s+FooDirective\r?\n/m); + expect(moduleContent).toMatch(/import.*Foo.*from '.\/foo'/); + expect(moduleContent).toMatch(/declarations:\s*\[[^\]]+?,\r?\n\s+Foo\r?\n/m); }); it('should respect the sourceRoot value', async () => { @@ -167,7 +185,7 @@ describe('Directive Schematic', () => { appTree, ); - expect(appTree.files).toContain('/projects/baz/custom/app/foo.directive.ts'); + expect(appTree.files).toContain('/projects/baz/custom/app/foo.ts'); }); it('should find the closest module', async () => { @@ -188,7 +206,7 @@ describe('Directive Schematic', () => { const tree = await schematicRunner.runSchematic('directive', options, appTree); const fooModuleContent = tree.readContent(fooModule); - expect(fooModuleContent).toMatch(/import { FooDirective } from '.\/foo.directive'/); + expect(fooModuleContent).toMatch(/import { Foo } from '.\/foo'/); }); it('should export the directive', async () => { @@ -196,7 +214,7 @@ describe('Directive Schematic', () => { const tree = await schematicRunner.runSchematic('directive', options, appTree); const appModuleContent = tree.readContent('/projects/baz/src/app/app.module.ts'); - expect(appModuleContent).toMatch(/exports: \[\n(\s*) {2}FooDirective\n\1\]/); + expect(appModuleContent).toMatch(/exports: \[\n(\s*) {2}Foo\n\1\]/); }); it('should import into a specified module', async () => { @@ -205,7 +223,7 @@ describe('Directive Schematic', () => { const tree = await schematicRunner.runSchematic('directive', options, appTree); const appModule = tree.readContent('/projects/baz/src/app/app.module.ts'); - expect(appModule).toMatch(/import { FooDirective } from '.\/foo.directive'/); + expect(appModule).toMatch(/import { Foo } from '.\/foo'/); }); it('should fail if specified module does not exist', async () => { diff --git a/packages/schematics/angular/directive/schema.json b/packages/schematics/angular/directive/schema.json index 753c520774ee..4a4041604fb0 100644 --- a/packages/schematics/angular/directive/schema.json +++ b/packages/schematics/angular/directive/schema.json @@ -80,6 +80,10 @@ "type": "boolean", "default": false, "description": "Automatically export the directive from the specified NgModule, making it accessible to other modules in the application." + }, + "type": { + "type": "string", + "description": "Append a custom type to the directive's filename. For example, if you set the type to `directive`, the file will be named `example.directive.ts`." } }, "required": ["name", "project"] diff --git a/tests/legacy-cli/e2e/tests/generate/directive/directive-basic.ts b/tests/legacy-cli/e2e/tests/generate/directive/directive-basic.ts index 3706743c392b..9ad00dfa22a3 100644 --- a/tests/legacy-cli/e2e/tests/generate/directive/directive-basic.ts +++ b/tests/legacy-cli/e2e/tests/generate/directive/directive-basic.ts @@ -6,8 +6,8 @@ export default function () { const directiveDir = join('src', 'app'); return ( ng('generate', 'directive', 'test-directive') - .then(() => expectFileToExist(join(directiveDir, 'test-directive.directive.ts'))) - .then(() => expectFileToExist(join(directiveDir, 'test-directive.directive.spec.ts'))) + .then(() => expectFileToExist(join(directiveDir, 'test-directive.ts'))) + .then(() => expectFileToExist(join(directiveDir, 'test-directive.spec.ts'))) // Try to run the unit tests. .then(() => ng('test', '--watch=false')) diff --git a/tests/legacy-cli/e2e/tests/generate/directive/directive-prefix.ts b/tests/legacy-cli/e2e/tests/generate/directive/directive-prefix.ts index ce2d6da3e4f6..b0a95ce399cb 100644 --- a/tests/legacy-cli/e2e/tests/generate/directive/directive-prefix.ts +++ b/tests/legacy-cli/e2e/tests/generate/directive/directive-prefix.ts @@ -16,9 +16,7 @@ export default function () { }), ) .then(() => ng('generate', 'directive', 'test2-directive')) - .then(() => - expectFileToMatch(join(directiveDir, 'test2-directive.directive.ts'), /selector: '\[preW/), - ) + .then(() => expectFileToMatch(join(directiveDir, 'test2-directive.ts'), /selector: '\[preW/)) .then(() => ng('generate', 'application', 'app-two', '--skip-install')) .then(() => useCIDefaults('app-two')) .then(() => useCIChrome('app-two', './projects/app-two')) @@ -33,17 +31,12 @@ export default function () { .then(() => ng('generate', 'directive', '--skip-import', 'test3-directive')) .then(() => process.chdir('../..')) .then(() => - expectFileToMatch( - join('projects', 'app-two', 'test3-directive.directive.ts'), - /selector: '\[preW/, - ), + expectFileToMatch(join('projects', 'app-two', 'test3-directive.ts'), /selector: '\[preW/), ) .then(() => process.chdir('src/app')) .then(() => ng('generate', 'directive', 'test-directive')) .then(() => process.chdir('../..')) - .then(() => - expectFileToMatch(join(directiveDir, 'test-directive.directive.ts'), /selector: '\[preP/), - ) + .then(() => expectFileToMatch(join(directiveDir, 'test-directive.ts'), /selector: '\[preP/)) // Try to run the unit tests. .then(() => ng('test', '--watch=false'))