Skip to content

Commit c969852

Browse files
committed
feat(@schematics/angular): add update migration to keep existing 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 or projects that do not explicitly contain these options in their workspace will use the update 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 ea1143d commit c969852

File tree

3 files changed

+197
-0
lines changed

3 files changed

+197
-0
lines changed

packages/schematics/angular/migrations/migration-collection.json

Lines changed: 5 additions & 0 deletions
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",
Lines changed: 51 additions & 0 deletions
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+
}
Lines changed: 141 additions & 0 deletions
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)