Skip to content

Commit bc0f07b

Browse files
committed
fix(@schematics/angular): generate services without a .service extension/type
To align with the updated style guide, Angular v20 will generate services without a `.service` file extension type for all service related files by default. Projects will automatically use this naming convention. Projects can however opt-out by setting the `type` option to `Service` for the service schematic. This can be done as a default in the `angular.json` or directly on the commandline via `--type=Service` when executing `ng generate`. As an example, `example.service.ts` will now be named `example.ts`. Additionally, the TypeScript class name will be `Example` instead of the previous `ExampleService`.
1 parent e7ae25a commit bc0f07b

File tree

10 files changed

+67
-30
lines changed

10 files changed

+67
-30
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { TestBed } from '@angular/core/testing';
2+
3+
import { <%= classify(name) %><%= classify(type) %> } from './<%= dasherize(name) %><%= type ? '.' + dasherize(type) : '' %>';
4+
5+
describe('<%= classify(name) %><%= classify(type) %>', () => {
6+
let service: <%= classify(name) %><%= classify(type) %>;
7+
8+
beforeEach(() => {
9+
TestBed.configureTestingModule({});
10+
service = TestBed.inject(<%= classify(name) %><%= classify(type) %>);
11+
});
12+
13+
it('should be created', () => {
14+
expect(service).toBeTruthy();
15+
});
16+
});

packages/schematics/angular/service/files/__name@dasherize@if-flat__/__name@dasherize__.service.ts.template packages/schematics/angular/service/files/__name@dasherize@if-flat__/__name@dasherize__.__type@dasherize__.ts.template

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
33
@Injectable({
44
providedIn: 'root'
55
})
6-
export class <%= classify(name) %>Service {
6+
export class <%= classify(name) %><%= classify(type) %> {
77

88
constructor() { }
99
}

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

-16
This file was deleted.

packages/schematics/angular/service/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ export default function (options: ServiceOptions): Rule {
1515
const flat = options.flat;
1616
options.flat = true;
1717

18+
// Schematic templates require a defined type value
19+
options.type ??= '';
20+
1821
return generateFromFiles(options, {
1922
'if-flat': (s: string) => (flat ? '' : s),
2023
});

packages/schematics/angular/service/index_spec.ts

+24-6
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,15 @@ describe('Service Schematic', () => {
4646

4747
const tree = await schematicRunner.runSchematic('service', options, appTree);
4848
const files = tree.files;
49-
expect(files).toContain('/projects/bar/src/app/foo/foo.service.spec.ts');
50-
expect(files).toContain('/projects/bar/src/app/foo/foo.service.ts');
49+
expect(files).toContain('/projects/bar/src/app/foo/foo.spec.ts');
50+
expect(files).toContain('/projects/bar/src/app/foo/foo.ts');
5151
});
5252

5353
it('service should be tree-shakeable', async () => {
5454
const options = { ...defaultOptions };
5555

5656
const tree = await schematicRunner.runSchematic('service', options, appTree);
57-
const content = tree.readContent('/projects/bar/src/app/foo/foo.service.ts');
57+
const content = tree.readContent('/projects/bar/src/app/foo/foo.ts');
5858
expect(content).toMatch(/providedIn: 'root'/);
5959
});
6060

@@ -63,15 +63,33 @@ describe('Service Schematic', () => {
6363

6464
const tree = await schematicRunner.runSchematic('service', options, appTree);
6565
const files = tree.files;
66-
expect(files).toContain('/projects/bar/src/app/foo/foo.service.ts');
67-
expect(files).not.toContain('/projects/bar/src/app/foo/foo.service.spec.ts');
66+
expect(files).toContain('/projects/bar/src/app/foo/foo.ts');
67+
expect(files).not.toContain('/projects/bar/src/app/foo/foo.spec.ts');
6868
});
6969

7070
it('should respect the sourceRoot value', async () => {
7171
const config = JSON.parse(appTree.readContent('/angular.json'));
7272
config.projects.bar.sourceRoot = 'projects/bar/custom';
7373
appTree.overwrite('/angular.json', JSON.stringify(config, null, 2));
7474
appTree = await schematicRunner.runSchematic('service', defaultOptions, appTree);
75-
expect(appTree.files).toContain('/projects/bar/custom/app/foo/foo.service.ts');
75+
expect(appTree.files).toContain('/projects/bar/custom/app/foo/foo.ts');
76+
});
77+
78+
it('should respect the type option', async () => {
79+
const options = { ...defaultOptions, type: 'Service' };
80+
const tree = await schematicRunner.runSchematic('service', options, appTree);
81+
const content = tree.readContent('/projects/bar/src/app/foo/foo.service.ts');
82+
const testContent = tree.readContent('/projects/bar/src/app/foo/foo.service.spec.ts');
83+
expect(content).toContain('export class FooService');
84+
expect(testContent).toContain("describe('FooService'");
85+
});
86+
87+
it('should allow empty string in the type option', async () => {
88+
const options = { ...defaultOptions, type: '' };
89+
const tree = await schematicRunner.runSchematic('service', options, appTree);
90+
const content = tree.readContent('/projects/bar/src/app/foo/foo.ts');
91+
const testContent = tree.readContent('/projects/bar/src/app/foo/foo.spec.ts');
92+
expect(content).toContain('export class Foo');
93+
expect(testContent).toContain("describe('Foo'");
7694
});
7795
});

packages/schematics/angular/service/schema.json

+4
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@
3939
"type": "boolean",
4040
"description": "Skip the generation of a unit test file `spec.ts` for the service.",
4141
"default": false
42+
},
43+
"type": {
44+
"type": "string",
45+
"description": "Append a custom type to the service's filename. For example, if you set the type to `service`, the file will be named `my-service.service.ts`."
4246
}
4347
},
4448
"required": ["name", "project"]

packages/schematics/angular/utility/generate-from-files.ts

+13
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
*/
88

99
import {
10+
FileOperator,
1011
Rule,
1112
Tree,
1213
apply,
1314
applyTemplates,
1415
chain,
1516
filter,
17+
forEach,
1618
mergeWith,
1719
move,
1820
noop,
@@ -31,6 +33,7 @@ export interface GenerateFromFilesOptions {
3133
project: string;
3234
skipTests?: boolean;
3335
templateFilesDirectory?: string;
36+
type?: string;
3437
}
3538

3639
export function generateFromFiles(
@@ -56,6 +59,16 @@ export function generateFromFiles(
5659
...options,
5760
...extraTemplateValues,
5861
}),
62+
!options.type
63+
? forEach(((file) => {
64+
return file.path.includes('..')
65+
? {
66+
content: file.content,
67+
path: file.path.replace('..', '.'),
68+
}
69+
: file;
70+
}) as FileOperator)
71+
: noop(),
5972
move(parsedPath.path + (options.flat ? '' : '/' + strings.dasherize(options.name))),
6073
]);
6174

tests/legacy-cli/e2e/tests/build/library/setup.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export async function libraryConsumptionSetup(): Promise<void> {
1717
export class MyLibComponent {}`,
1818
'./src/app/app.ts': `
1919
import { Component } from '@angular/core';
20-
import { MyLibService, MyLibComponent } from 'my-lib';
20+
import { MyLibComponent } from 'my-lib';
2121
2222
@Component({
2323
standalone: true,
@@ -28,8 +28,7 @@ export async function libraryConsumptionSetup(): Promise<void> {
2828
export class App {
2929
title = 'test-project';
3030
31-
constructor(myLibService: MyLibService) {
32-
console.log(myLibService);
31+
constructor() {
3332
}
3433
}
3534
`,

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ export default function () {
99
return (
1010
ng('generate', 'service', 'test-service')
1111
.then(() => expectFileToExist(serviceDir))
12-
.then(() => expectFileToExist(join(serviceDir, 'test-service.service.ts')))
13-
.then(() => expectFileToExist(join(serviceDir, 'test-service.service.spec.ts')))
12+
.then(() => expectFileToExist(join(serviceDir, 'test-service.ts')))
13+
.then(() => expectFileToExist(join(serviceDir, 'test-service.spec.ts')))
1414

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

tests/legacy-cli/e2e/tests/misc/es2015-nometa.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { prependToFile, replaceInFile } from '../../utils/fs';
22
import { ng } from '../../utils/process';
33

44
export default async function () {
5-
await ng('generate', 'service', 'user');
5+
await ng('generate', 'service', 'user-service');
66

77
// Update the application to use the new service
8-
await prependToFile('src/app/app.ts', "import { UserService } from './user.service';");
8+
await prependToFile('src/app/app.ts', "import { UserService } from './user-service';");
99

1010
await replaceInFile(
1111
'src/app/app.ts',

0 commit comments

Comments
 (0)