Skip to content

Commit 482009b

Browse files
committed
fix(material/schematics): add explicit system variable prefix schematic (#29980)
In v19 we're changing the default system variables prefix from `sys` to `mat-sys`. These changes add a schematic that will update existing apps to keep the `sys` prefix. (cherry picked from commit b043a35)
1 parent 7a3b634 commit 482009b

File tree

3 files changed

+345
-1
lines changed

3 files changed

+345
-1
lines changed

src/material/schematics/ng-update/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@ import {
1515

1616
import {materialUpgradeData} from './upgrade-data';
1717
import {MatCoreMigration} from './migrations/mat-core-removal';
18+
import {ExplicitSystemVariablePrefixMigration} from './migrations/explicit-system-variable-prefix';
1819

19-
const materialMigrations: NullableDevkitMigration[] = [MatCoreMigration];
20+
const materialMigrations: NullableDevkitMigration[] = [
21+
MatCoreMigration,
22+
ExplicitSystemVariablePrefixMigration,
23+
];
2024

2125
/** Entry point for the migration schematics with target of Angular Material v19 */
2226
export function updateToV19(): Rule {
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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 {DevkitContext, Migration, ResolvedResource, UpgradeData} from '@angular/cdk/schematics';
10+
11+
/**
12+
* Migration that adds `system-variables-prefix` to apps that have `use-system-variables` enabled.
13+
*/
14+
export class ExplicitSystemVariablePrefixMigration extends Migration<UpgradeData, DevkitContext> {
15+
override enabled = true;
16+
17+
override visitStylesheet(stylesheet: ResolvedResource): void {
18+
if (!stylesheet.filePath.endsWith('.scss')) {
19+
return;
20+
}
21+
22+
const content = this.fileSystem.read(stylesheet.filePath);
23+
if (!content || !content.includes('@angular/material')) {
24+
return;
25+
}
26+
27+
const changes = this._getChanges(content);
28+
29+
if (changes.length > 0) {
30+
const update = this.fileSystem.edit(stylesheet.filePath);
31+
32+
for (let i = changes.length - 1; i > -1; i--) {
33+
update.insertRight(changes[i].start, changes[i].text);
34+
}
35+
36+
this.fileSystem.commitEdits();
37+
}
38+
}
39+
40+
/** Gets the changes that should be applied to a file. */
41+
private _getChanges(content: string) {
42+
const key = 'use-system-variables';
43+
const prefixKey = 'system-variables-prefix';
44+
const changes: {start: number; text: string}[] = [];
45+
let index = content.indexOf(key);
46+
47+
// Note: this migration is a bit rudimentary, because Sass doesn't expose a proper AST.
48+
while (index > -1) {
49+
const colonIndex = content.indexOf(':', index);
50+
const valueEnd = colonIndex === -1 ? -1 : this._getValueEnd(content, colonIndex);
51+
52+
if (valueEnd === -1) {
53+
index = content.indexOf(key, index + key.length);
54+
continue;
55+
}
56+
57+
const value = content.slice(colonIndex + 1, valueEnd + 1).trim();
58+
if (value.startsWith('true') && !this._hasSystemPrefix(content, index, prefixKey)) {
59+
changes.push({
60+
start: this._getInsertIndex(content, valueEnd),
61+
text: `${value.endsWith(',') ? '' : ','}\n ${prefixKey}: sys,`,
62+
});
63+
}
64+
65+
index = content.indexOf(key, valueEnd);
66+
}
67+
68+
return changes;
69+
}
70+
71+
/**
72+
* Gets the end index of a Sass map key.
73+
* @param content Content of the file.
74+
* @param startIndex Index at which to start the search.
75+
*/
76+
private _getValueEnd(content: string, startIndex: number): number {
77+
for (let i = startIndex + 1; i < content.length; i++) {
78+
const char = content[i];
79+
80+
if (char === ',' || char === '\n' || char === ')') {
81+
return i;
82+
}
83+
}
84+
85+
return -1;
86+
}
87+
88+
/**
89+
* Gets the index at which to insert the migrated content.
90+
* @param content Initial file content.
91+
* @param valueEnd Index at which the value of the system variables opt-in ends.
92+
*/
93+
private _getInsertIndex(content: string, valueEnd: number): number {
94+
for (let i = valueEnd; i < content.length; i++) {
95+
if (content[i] === '\n') {
96+
return i;
97+
} else if (content[i] === ')') {
98+
return i;
99+
}
100+
}
101+
102+
return valueEnd;
103+
}
104+
105+
/**
106+
* Determines if a map that enables system variables is using system variables already.
107+
* @param content Full file contents.
108+
* @param keyIndex Index at which the systems variable key is defined.
109+
* @param prefixKey Name of the key that defines the prefix.
110+
*/
111+
private _hasSystemPrefix(content: string, keyIndex: number, prefixKey: string): boolean {
112+
// Note: technically this can break if there are other inline maps, but it should be rare.
113+
const mapEnd = content.indexOf(')', keyIndex);
114+
115+
if (mapEnd > -1) {
116+
for (let i = keyIndex; i > -1; i--) {
117+
if (content[i] === '(') {
118+
return content.slice(i, mapEnd).includes(prefixKey);
119+
}
120+
}
121+
}
122+
123+
return false;
124+
}
125+
}
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import {UnitTestTree} from '@angular-devkit/schematics/testing';
2+
import {createTestCaseSetup} from '@angular/cdk/schematics/testing';
3+
import {MIGRATION_PATH} from '../../paths';
4+
5+
const THEME_FILE_PATH = '/projects/cdk-testing/src/theme.scss';
6+
7+
describe('v19 explicit system variable prefix migration', () => {
8+
let tree: UnitTestTree;
9+
let writeFile: (filename: string, content: string) => void;
10+
let runMigration: () => Promise<unknown>;
11+
12+
function stripWhitespace(content: string): string {
13+
return content.replace(/\s/g, '');
14+
}
15+
16+
beforeEach(async () => {
17+
const testSetup = await createTestCaseSetup('migration-v19', MIGRATION_PATH, []);
18+
tree = testSetup.appTree;
19+
writeFile = testSetup.writeFile;
20+
runMigration = testSetup.runFixers;
21+
});
22+
23+
it('should add an explicit system variables prefix', async () => {
24+
writeFile(
25+
THEME_FILE_PATH,
26+
`
27+
@use '@angular/material' as mat;
28+
29+
$theme: mat.define-theme((
30+
color: (
31+
theme-type: 'light',
32+
primary: mat.$azure-palette,
33+
tertiary: mat.$red-palette,
34+
use-system-variables: true
35+
),
36+
typography: (
37+
use-system-variables: true
38+
),
39+
density: (
40+
scale: -1
41+
),
42+
));
43+
44+
@include mat.all-component-themes($theme);
45+
`,
46+
);
47+
48+
await runMigration();
49+
50+
expect(stripWhitespace(tree.readText(THEME_FILE_PATH))).toBe(
51+
stripWhitespace(`
52+
@use '@angular/material' as mat;
53+
54+
$theme: mat.define-theme((
55+
color: (
56+
theme-type: 'light',
57+
primary: mat.$azure-palette,
58+
tertiary: mat.$red-palette,
59+
use-system-variables: true,
60+
system-variables-prefix: sys,
61+
),
62+
typography: (
63+
use-system-variables: true,
64+
system-variables-prefix: sys,
65+
),
66+
density: (
67+
scale: -1
68+
),
69+
));
70+
71+
@include mat.all-component-themes($theme);
72+
`),
73+
);
74+
});
75+
76+
it('should add an explicit system variables prefix if the value is using trailing commas', async () => {
77+
writeFile(
78+
THEME_FILE_PATH,
79+
`
80+
@use '@angular/material' as mat;
81+
82+
$theme: mat.define-theme((
83+
color: (
84+
theme-type: 'light',
85+
primary: mat.$azure-palette,
86+
tertiary: mat.$red-palette,
87+
use-system-variables: true,
88+
),
89+
typography: (
90+
use-system-variables: true,
91+
),
92+
density: (
93+
scale: -1
94+
),
95+
));
96+
97+
@include mat.all-component-themes($theme);
98+
`,
99+
);
100+
101+
await runMigration();
102+
103+
expect(stripWhitespace(tree.readText(THEME_FILE_PATH))).toBe(
104+
stripWhitespace(`
105+
@use '@angular/material' as mat;
106+
107+
$theme: mat.define-theme((
108+
color: (
109+
theme-type: 'light',
110+
primary: mat.$azure-palette,
111+
tertiary: mat.$red-palette,
112+
use-system-variables: true,
113+
system-variables-prefix: sys,
114+
),
115+
typography: (
116+
use-system-variables: true,
117+
system-variables-prefix: sys,
118+
),
119+
density: (
120+
scale: -1
121+
),
122+
));
123+
124+
@include mat.all-component-themes($theme);
125+
`),
126+
);
127+
});
128+
129+
it('should not add an explicit system variables prefix if the map has one already', async () => {
130+
writeFile(
131+
THEME_FILE_PATH,
132+
`
133+
@use '@angular/material' as mat;
134+
135+
$theme: mat.define-theme((
136+
color: (
137+
theme-type: 'light',
138+
primary: mat.$azure-palette,
139+
tertiary: mat.$red-palette,
140+
use-system-variables: true
141+
),
142+
typography: (
143+
use-system-variables: true,
144+
system-variables-prefix: foo
145+
),
146+
density: (
147+
scale: -1
148+
),
149+
));
150+
151+
@include mat.all-component-themes($theme);
152+
`,
153+
);
154+
155+
await runMigration();
156+
157+
expect(stripWhitespace(tree.readText(THEME_FILE_PATH))).toBe(
158+
stripWhitespace(`
159+
@use '@angular/material' as mat;
160+
161+
$theme: mat.define-theme((
162+
color: (
163+
theme-type: 'light',
164+
primary: mat.$azure-palette,
165+
tertiary: mat.$red-palette,
166+
use-system-variables: true,
167+
system-variables-prefix: sys,
168+
),
169+
typography: (
170+
use-system-variables: true,
171+
system-variables-prefix: foo
172+
),
173+
density: (
174+
scale: -1
175+
),
176+
));
177+
178+
@include mat.all-component-themes($theme);
179+
`),
180+
);
181+
});
182+
183+
it('should handle a single-line map', async () => {
184+
writeFile(
185+
THEME_FILE_PATH,
186+
`
187+
@use '@angular/material' as mat;
188+
189+
$theme: mat.define-theme((
190+
color: (theme-type: 'light', primary: mat.$azure-palette, use-system-variables: true),
191+
typography: (use-system-variables: true),
192+
density: (scale: -1),
193+
));
194+
195+
@include mat.all-component-themes($theme);
196+
`,
197+
);
198+
199+
await runMigration();
200+
201+
expect(stripWhitespace(tree.readText(THEME_FILE_PATH))).toBe(
202+
stripWhitespace(`
203+
@use '@angular/material' as mat;
204+
205+
$theme: mat.define-theme((
206+
color: (theme-type: 'light', primary: mat.$azure-palette, use-system-variables: true, system-variables-prefix: sys,),
207+
typography: (use-system-variables: true, system-variables-prefix: sys,),
208+
density: (scale: -1),
209+
));
210+
211+
@include mat.all-component-themes($theme);
212+
`),
213+
);
214+
});
215+
});

0 commit comments

Comments
 (0)