Skip to content

Commit 8d715fa

Browse files
committed
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`.
1 parent b96dd54 commit 8d715fa

File tree

8 files changed

+61
-65
lines changed

8 files changed

+61
-65
lines changed

packages/schematics/angular/directive/files/__name@dasherize@if-flat__/__name@dasherize__.directive.spec.ts.template

-8
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { <%= classify(name) %><%= classify(type) %> } from './<%= dasherize(name) %><%= type ? '.' + dasherize(type) : '' %>';
2+
3+
describe('<%= classify(name) %><%= classify(type) %>', () => {
4+
it('should create an instance', () => {
5+
const directive = new <%= classify(name) %><%= classify(type) %>();
6+
expect(directive).toBeTruthy();
7+
});
8+
});
+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Directive } from '@angular/core';
44
selector: '[<%= selector %>]'<% if(!standalone) {%>,
55
standalone: false<%}%>
66
})
7-
export class <%= classify(name) %>Directive {
7+
export class <%= classify(name) %><%= classify(type) %> {
88

99
constructor() { }
1010

packages/schematics/angular/directive/index.ts

+6-25
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,10 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {
10-
Rule,
11-
SchematicsException,
12-
Tree,
13-
apply,
14-
applyTemplates,
15-
chain,
16-
filter,
17-
mergeWith,
18-
move,
19-
noop,
20-
strings,
21-
url,
22-
} from '@angular-devkit/schematics';
9+
import { Rule, SchematicsException, Tree, chain, strings } from '@angular-devkit/schematics';
2310
import { addDeclarationToNgModule } from '../utility/add-declaration-to-ng-module';
2411
import { findModuleFromOptions } from '../utility/find-module';
12+
import { generateFromFiles } from '../utility/generate-from-files';
2513
import { parseName } from '../utility/parse-name';
2614
import { validateClassName, validateHtmlSelector } from '../utility/validation';
2715
import { buildDefaultPath, getWorkspace } from '../utility/workspace';
@@ -52,6 +40,9 @@ export default function (options: DirectiveOptions): Rule {
5240

5341
options.module = findModuleFromOptions(host, options);
5442

43+
// Schematic templates require a defined type value
44+
options.type ??= '';
45+
5546
const parsedPath = parseName(options.path, options.name);
5647
options.name = parsedPath.name;
5748
options.path = parsedPath.path;
@@ -60,23 +51,13 @@ export default function (options: DirectiveOptions): Rule {
6051
validateHtmlSelector(options.selector);
6152
validateClassName(strings.classify(options.name));
6253

63-
const templateSource = apply(url('./files'), [
64-
options.skipTests ? filter((path) => !path.endsWith('.spec.ts.template')) : noop(),
65-
applyTemplates({
66-
...strings,
67-
'if-flat': (s: string) => (options.flat ? '' : s),
68-
...options,
69-
}),
70-
move(parsedPath.path),
71-
]);
72-
7354
return chain([
7455
addDeclarationToNgModule({
7556
type: 'directive',
7657

7758
...options,
7859
}),
79-
mergeWith(templateSource),
60+
generateFromFiles(options),
8061
]);
8162
};
8263
}

packages/schematics/angular/directive/index_spec.ts

+37-19
Original file line numberDiff line numberDiff line change
@@ -50,47 +50,47 @@ describe('Directive Schematic', () => {
5050

5151
const tree = await schematicRunner.runSchematic('directive', options, appTree);
5252
const files = tree.files;
53-
expect(files).toContain('/projects/bar/src/app/foo/foo.directive.spec.ts');
54-
expect(files).toContain('/projects/bar/src/app/foo/foo.directive.ts');
53+
expect(files).toContain('/projects/bar/src/app/foo/foo.spec.ts');
54+
expect(files).toContain('/projects/bar/src/app/foo/foo.ts');
5555
});
5656

5757
it('should converts dash-cased-name to a camelCasedSelector', async () => {
5858
const options = { ...defaultOptions, name: 'my-dir' };
5959

6060
const tree = await schematicRunner.runSchematic('directive', options, appTree);
61-
const content = tree.readContent('/projects/bar/src/app/my-dir.directive.ts');
61+
const content = tree.readContent('/projects/bar/src/app/my-dir.ts');
6262
expect(content).toMatch(/selector: '\[appMyDir\]'/);
6363
});
6464

6565
it('should create the right selector with a path in the name', async () => {
6666
const options = { ...defaultOptions, name: 'sub/test' };
6767
appTree = await schematicRunner.runSchematic('directive', options, appTree);
6868

69-
const content = appTree.readContent('/projects/bar/src/app/sub/test.directive.ts');
69+
const content = appTree.readContent('/projects/bar/src/app/sub/test.ts');
7070
expect(content).toMatch(/selector: '\[appTest\]'/);
7171
});
7272

7373
it('should use the prefix', async () => {
7474
const options = { ...defaultOptions, prefix: 'pre' };
7575
const tree = await schematicRunner.runSchematic('directive', options, appTree);
7676

77-
const content = tree.readContent('/projects/bar/src/app/foo.directive.ts');
77+
const content = tree.readContent('/projects/bar/src/app/foo.ts');
7878
expect(content).toMatch(/selector: '\[preFoo\]'/);
7979
});
8080

8181
it('should use the default project prefix if none is passed', async () => {
8282
const options = { ...defaultOptions, prefix: undefined };
8383
const tree = await schematicRunner.runSchematic('directive', options, appTree);
8484

85-
const content = tree.readContent('/projects/bar/src/app/foo.directive.ts');
85+
const content = tree.readContent('/projects/bar/src/app/foo.ts');
8686
expect(content).toMatch(/selector: '\[appFoo\]'/);
8787
});
8888

8989
it('should use the supplied prefix if it is ""', async () => {
9090
const options = { ...defaultOptions, prefix: '' };
9191
const tree = await schematicRunner.runSchematic('directive', options, appTree);
9292

93-
const content = tree.readContent('/projects/bar/src/app/foo.directive.ts');
93+
const content = tree.readContent('/projects/bar/src/app/foo.ts');
9494
expect(content).toMatch(/selector: '\[foo\]'/);
9595
});
9696

@@ -99,16 +99,16 @@ describe('Directive Schematic', () => {
9999

100100
const tree = await schematicRunner.runSchematic('directive', options, appTree);
101101
const files = tree.files;
102-
expect(files).toContain('/projects/bar/src/app/foo.directive.ts');
103-
expect(files).not.toContain('/projects/bar/src/app/foo.directive.spec.ts');
102+
expect(files).toContain('/projects/bar/src/app/foo.ts');
103+
expect(files).not.toContain('/projects/bar/src/app/foo.spec.ts');
104104
});
105105

106106
it('should create a standalone directive', async () => {
107107
const options = { ...defaultOptions, standalone: true };
108108
const tree = await schematicRunner.runSchematic('directive', options, appTree);
109-
const directiveContent = tree.readContent('/projects/bar/src/app/foo.directive.ts');
109+
const directiveContent = tree.readContent('/projects/bar/src/app/foo.ts');
110110
expect(directiveContent).not.toContain('standalone');
111-
expect(directiveContent).toContain('class FooDirective');
111+
expect(directiveContent).toContain('class Foo');
112112
});
113113

114114
it('should error when class name contains invalid characters', async () => {
@@ -119,6 +119,24 @@ describe('Directive Schematic', () => {
119119
).toBeRejectedWithError('Class name "404" is invalid.');
120120
});
121121

122+
it('should respect the type option', async () => {
123+
const options = { ...defaultOptions, type: 'Directive' };
124+
const tree = await schematicRunner.runSchematic('directive', options, appTree);
125+
const content = tree.readContent('/projects/bar/src/app/foo.directive.ts');
126+
const testContent = tree.readContent('/projects/bar/src/app/foo.directive.spec.ts');
127+
expect(content).toContain('export class FooDirective');
128+
expect(testContent).toContain("describe('FooDirective'");
129+
});
130+
131+
it('should allow empty string in the type option', async () => {
132+
const options = { ...defaultOptions, type: '' };
133+
const tree = await schematicRunner.runSchematic('directive', options, appTree);
134+
const content = tree.readContent('/projects/bar/src/app/foo.ts');
135+
const testContent = tree.readContent('/projects/bar/src/app/foo.spec.ts');
136+
expect(content).toContain('export class Foo');
137+
expect(testContent).toContain("describe('Foo'");
138+
});
139+
122140
describe('standalone=false', () => {
123141
const defaultNonStandaloneOptions: DirectiveOptions = {
124142
...defaultOptions,
@@ -139,11 +157,11 @@ describe('Directive Schematic', () => {
139157

140158
const tree = await schematicRunner.runSchematic('directive', options, appTree);
141159
const files = tree.files;
142-
expect(files).toContain('/projects/baz/src/app/foo.directive.spec.ts');
143-
expect(files).toContain('/projects/baz/src/app/foo.directive.ts');
160+
expect(files).toContain('/projects/baz/src/app/foo.spec.ts');
161+
expect(files).toContain('/projects/baz/src/app/foo.ts');
144162
const moduleContent = tree.readContent('/projects/baz/src/app/app.module.ts');
145-
expect(moduleContent).toMatch(/import.*Foo.*from '.\/foo.directive'/);
146-
expect(moduleContent).toMatch(/declarations:\s*\[[^\]]+?,\r?\n\s+FooDirective\r?\n/m);
163+
expect(moduleContent).toMatch(/import.*Foo.*from '.\/foo'/);
164+
expect(moduleContent).toMatch(/declarations:\s*\[[^\]]+?,\r?\n\s+Foo\r?\n/m);
147165
});
148166

149167
it('should respect the sourceRoot value', async () => {
@@ -167,7 +185,7 @@ describe('Directive Schematic', () => {
167185
appTree,
168186
);
169187

170-
expect(appTree.files).toContain('/projects/baz/custom/app/foo.directive.ts');
188+
expect(appTree.files).toContain('/projects/baz/custom/app/foo.ts');
171189
});
172190

173191
it('should find the closest module', async () => {
@@ -188,15 +206,15 @@ describe('Directive Schematic', () => {
188206

189207
const tree = await schematicRunner.runSchematic('directive', options, appTree);
190208
const fooModuleContent = tree.readContent(fooModule);
191-
expect(fooModuleContent).toMatch(/import { FooDirective } from '.\/foo.directive'/);
209+
expect(fooModuleContent).toMatch(/import { Foo } from '.\/foo'/);
192210
});
193211

194212
it('should export the directive', async () => {
195213
const options = { ...defaultNonStandaloneOptions, export: true };
196214

197215
const tree = await schematicRunner.runSchematic('directive', options, appTree);
198216
const appModuleContent = tree.readContent('/projects/baz/src/app/app.module.ts');
199-
expect(appModuleContent).toMatch(/exports: \[\n(\s*) {2}FooDirective\n\1\]/);
217+
expect(appModuleContent).toMatch(/exports: \[\n(\s*) {2}Foo\n\1\]/);
200218
});
201219

202220
it('should import into a specified module', async () => {
@@ -205,7 +223,7 @@ describe('Directive Schematic', () => {
205223
const tree = await schematicRunner.runSchematic('directive', options, appTree);
206224
const appModule = tree.readContent('/projects/baz/src/app/app.module.ts');
207225

208-
expect(appModule).toMatch(/import { FooDirective } from '.\/foo.directive'/);
226+
expect(appModule).toMatch(/import { Foo } from '.\/foo'/);
209227
});
210228

211229
it('should fail if specified module does not exist', async () => {

packages/schematics/angular/directive/schema.json

+4
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@
8080
"type": "boolean",
8181
"default": false,
8282
"description": "Automatically export the directive from the specified NgModule, making it accessible to other modules in the application."
83+
},
84+
"type": {
85+
"type": "string",
86+
"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`."
8387
}
8488
},
8589
"required": ["name", "project"]

tests/legacy-cli/e2e/tests/generate/directive/directive-basic.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ export default function () {
66
const directiveDir = join('src', 'app');
77
return (
88
ng('generate', 'directive', 'test-directive')
9-
.then(() => expectFileToExist(join(directiveDir, 'test-directive.directive.ts')))
10-
.then(() => expectFileToExist(join(directiveDir, 'test-directive.directive.spec.ts')))
9+
.then(() => expectFileToExist(join(directiveDir, 'test-directive.ts')))
10+
.then(() => expectFileToExist(join(directiveDir, 'test-directive.spec.ts')))
1111

1212
// Try to run the unit tests.
1313
.then(() => ng('test', '--watch=false'))

tests/legacy-cli/e2e/tests/generate/directive/directive-prefix.ts

+3-10
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@ export default function () {
1616
}),
1717
)
1818
.then(() => ng('generate', 'directive', 'test2-directive'))
19-
.then(() =>
20-
expectFileToMatch(join(directiveDir, 'test2-directive.directive.ts'), /selector: '\[preW/),
21-
)
19+
.then(() => expectFileToMatch(join(directiveDir, 'test2-directive.ts'), /selector: '\[preW/))
2220
.then(() => ng('generate', 'application', 'app-two', '--skip-install'))
2321
.then(() => useCIDefaults('app-two'))
2422
.then(() => useCIChrome('app-two', './projects/app-two'))
@@ -33,17 +31,12 @@ export default function () {
3331
.then(() => ng('generate', 'directive', '--skip-import', 'test3-directive'))
3432
.then(() => process.chdir('../..'))
3533
.then(() =>
36-
expectFileToMatch(
37-
join('projects', 'app-two', 'test3-directive.directive.ts'),
38-
/selector: '\[preW/,
39-
),
34+
expectFileToMatch(join('projects', 'app-two', 'test3-directive.ts'), /selector: '\[preW/),
4035
)
4136
.then(() => process.chdir('src/app'))
4237
.then(() => ng('generate', 'directive', 'test-directive'))
4338
.then(() => process.chdir('../..'))
44-
.then(() =>
45-
expectFileToMatch(join(directiveDir, 'test-directive.directive.ts'), /selector: '\[preP/),
46-
)
39+
.then(() => expectFileToMatch(join(directiveDir, 'test-directive.ts'), /selector: '\[preP/))
4740

4841
// Try to run the unit tests.
4942
.then(() => ng('test', '--watch=false'))

0 commit comments

Comments
 (0)