Skip to content

Commit fdc6291

Browse files
committed
feat(@schematics/angular): add update migration to keep previous style guide generation behavior
When updating to Angular v20 via `ng update`, a migration will be executed that will add schematic generation (`ng generate`) defaults to the workspace. These defaults will ensure that existing projects will continue to generate files as done in previous versions of the Angular CLI. All new projects (via `ng new`) or projects that do not explicitly contain these options in their workspace will use the updated style guide naming behavior. The option values for the `schematics` field are as follows: ``` { '@schematics/angular:component': { type: 'component' }, '@schematics/angular:directive': { type: 'directive' }, '@schematics/angular:service': { type: 'service' }, '@schematics/angular:guard': { typeSeparator: '.' }, '@schematics/angular:interceptor': { typeSeparator: '.' }, '@schematics/angular:module': { typeSeparator: '.' }, '@schematics/angular:pipe': { typeSeparator: '.' }, '@schematics/angular:resolver': { typeSeparator: '.' }, } ```
1 parent 0253bc9 commit fdc6291

File tree

3 files changed

+197
-0
lines changed

3 files changed

+197
-0
lines changed

Diff for: packages/schematics/angular/migrations/migration-collection.json

+5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
"factory": "./update-module-resolution/migration",
1616
"description": "Update 'moduleResolution' to 'bundler' in TypeScript configurations. You can read more about this, here: https://www.typescriptlang.org/tsconfig/#moduleResolution"
1717
},
18+
"previous-style-guide": {
19+
"version": "20.0.0",
20+
"factory": "./previous-style-guide/migration",
21+
"description": "Update workspace generation defaults to maintain previous style guide behavior."
22+
},
1823
"use-application-builder": {
1924
"version": "20.0.0",
2025
"factory": "./use-application-builder/migration",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import type { Rule } from '@angular-devkit/schematics';
10+
import { updateWorkspace } from '../../utility/workspace';
11+
12+
const TYPE_SCHEMATICS = ['component', 'directive', 'service'] as const;
13+
14+
const SEPARATOR_SCHEMATICS = ['guard', 'interceptor', 'module', 'pipe', 'resolver'] as const;
15+
16+
export default function (): Rule {
17+
return updateWorkspace((workspace) => {
18+
let schematicsDefaults = workspace.extensions['schematics'];
19+
20+
// Ensure "schematics" field is an object
21+
if (
22+
!schematicsDefaults ||
23+
typeof schematicsDefaults !== 'object' ||
24+
Array.isArray(schematicsDefaults)
25+
) {
26+
schematicsDefaults = workspace.extensions['schematics'] = {};
27+
}
28+
29+
// Add "type" value for each schematic to continue generating a type suffix.
30+
// New default is an empty type value.
31+
for (const schematicName of TYPE_SCHEMATICS) {
32+
const schematic = (schematicsDefaults[`@schematics/angular:${schematicName}`] ??= {});
33+
if (typeof schematic === 'object' && !Array.isArray(schematic) && !('type' in schematic)) {
34+
schematic['type'] = schematicName;
35+
}
36+
}
37+
38+
// Add "typeSeparator" value for each schematic to continue generating "." before type.
39+
// New default is an "-" type value.
40+
for (const schematicName of SEPARATOR_SCHEMATICS) {
41+
const schematic = (schematicsDefaults[`@schematics/angular:${schematicName}`] ??= {});
42+
if (
43+
typeof schematic === 'object' &&
44+
!Array.isArray(schematic) &&
45+
!('typeSeparator' in schematic)
46+
) {
47+
schematic['typeSeparator'] = '.';
48+
}
49+
}
50+
});
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { EmptyTree } from '@angular-devkit/schematics';
10+
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
11+
import { ProjectType, WorkspaceSchema } from '../../utility/workspace-models';
12+
13+
function createWorkSpaceConfig(tree: UnitTestTree, initialSchematicsValue?: unknown) {
14+
const angularConfig: WorkspaceSchema = {
15+
version: 1,
16+
projects: {
17+
app: {
18+
root: '/project/lib',
19+
sourceRoot: '/project/app/src',
20+
projectType: ProjectType.Application,
21+
prefix: 'app',
22+
architect: {},
23+
},
24+
},
25+
};
26+
27+
if (initialSchematicsValue !== undefined) {
28+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
29+
(angularConfig as any).schematics = initialSchematicsValue;
30+
}
31+
32+
tree.create('/angular.json', JSON.stringify(angularConfig, undefined, 2));
33+
}
34+
35+
describe(`Migration to update 'angular.json'.`, () => {
36+
const schematicName = 'previous-style-guide';
37+
const schematicRunner = new SchematicTestRunner(
38+
'migrations',
39+
require.resolve('../migration-collection.json'),
40+
);
41+
42+
let tree: UnitTestTree;
43+
beforeEach(() => {
44+
tree = new UnitTestTree(new EmptyTree());
45+
});
46+
47+
it(`should add defaults if no "schematics" workspace field is present`, async () => {
48+
createWorkSpaceConfig(tree);
49+
50+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
51+
const { schematics } = JSON.parse(newTree.readContent('/angular.json'));
52+
53+
expect(schematics).toEqual({
54+
'@schematics/angular:component': { type: 'component' },
55+
'@schematics/angular:directive': { type: 'directive' },
56+
'@schematics/angular:service': { type: 'service' },
57+
'@schematics/angular:guard': { typeSeparator: '.' },
58+
'@schematics/angular:interceptor': { typeSeparator: '.' },
59+
'@schematics/angular:module': { typeSeparator: '.' },
60+
'@schematics/angular:pipe': { typeSeparator: '.' },
61+
'@schematics/angular:resolver': { typeSeparator: '.' },
62+
});
63+
});
64+
65+
it(`should add defaults if empty "schematics" workspace field is present`, async () => {
66+
createWorkSpaceConfig(tree, {});
67+
68+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
69+
const { schematics } = JSON.parse(newTree.readContent('/angular.json'));
70+
71+
expect(schematics).toEqual({
72+
'@schematics/angular:component': { type: 'component' },
73+
'@schematics/angular:directive': { type: 'directive' },
74+
'@schematics/angular:service': { type: 'service' },
75+
'@schematics/angular:guard': { typeSeparator: '.' },
76+
'@schematics/angular:interceptor': { typeSeparator: '.' },
77+
'@schematics/angular:module': { typeSeparator: '.' },
78+
'@schematics/angular:pipe': { typeSeparator: '.' },
79+
'@schematics/angular:resolver': { typeSeparator: '.' },
80+
});
81+
});
82+
83+
it(`should add defaults if invalid "schematics" workspace field is present`, async () => {
84+
createWorkSpaceConfig(tree, 10);
85+
86+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
87+
const { schematics } = JSON.parse(newTree.readContent('/angular.json'));
88+
89+
expect(schematics).toEqual({
90+
'@schematics/angular:component': { type: 'component' },
91+
'@schematics/angular:directive': { type: 'directive' },
92+
'@schematics/angular:service': { type: 'service' },
93+
'@schematics/angular:guard': { typeSeparator: '.' },
94+
'@schematics/angular:interceptor': { typeSeparator: '.' },
95+
'@schematics/angular:module': { typeSeparator: '.' },
96+
'@schematics/angular:pipe': { typeSeparator: '.' },
97+
'@schematics/angular:resolver': { typeSeparator: '.' },
98+
});
99+
});
100+
101+
it(`should add defaults if existing unrelated "schematics" workspace defaults are present`, async () => {
102+
createWorkSpaceConfig(tree, {
103+
'@schematics/angular:component': { style: 'scss' },
104+
});
105+
106+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
107+
const { schematics } = JSON.parse(newTree.readContent('/angular.json'));
108+
109+
expect(schematics).toEqual({
110+
'@schematics/angular:component': { style: 'scss', type: 'component' },
111+
'@schematics/angular:directive': { type: 'directive' },
112+
'@schematics/angular:service': { type: 'service' },
113+
'@schematics/angular:guard': { typeSeparator: '.' },
114+
'@schematics/angular:interceptor': { typeSeparator: '.' },
115+
'@schematics/angular:module': { typeSeparator: '.' },
116+
'@schematics/angular:pipe': { typeSeparator: '.' },
117+
'@schematics/angular:resolver': { typeSeparator: '.' },
118+
});
119+
});
120+
121+
it(`should not overwrite defaults if existing "schematics" workspace defaults are present`, async () => {
122+
createWorkSpaceConfig(tree, {
123+
'@schematics/angular:component': { type: 'example' },
124+
'@schematics/angular:guard': { typeSeparator: '-' },
125+
});
126+
127+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
128+
const { schematics } = JSON.parse(newTree.readContent('/angular.json'));
129+
130+
expect(schematics).toEqual({
131+
'@schematics/angular:component': { type: 'example' },
132+
'@schematics/angular:directive': { type: 'directive' },
133+
'@schematics/angular:service': { type: 'service' },
134+
'@schematics/angular:guard': { typeSeparator: '-' },
135+
'@schematics/angular:interceptor': { typeSeparator: '.' },
136+
'@schematics/angular:module': { typeSeparator: '.' },
137+
'@schematics/angular:pipe': { typeSeparator: '.' },
138+
'@schematics/angular:resolver': { typeSeparator: '.' },
139+
});
140+
});
141+
});

0 commit comments

Comments
 (0)