Skip to content

Commit

Permalink
fix(material/schematics): add explicit system variable prefix schemat…
Browse files Browse the repository at this point in the history
…ic (#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.
  • Loading branch information
crisbeto authored Nov 7, 2024
1 parent fbc4073 commit b043a35
Show file tree
Hide file tree
Showing 3 changed files with 345 additions and 1 deletion.
6 changes: 5 additions & 1 deletion src/material/schematics/ng-update/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ import {

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

const materialMigrations: NullableDevkitMigration[] = [MatCoreMigration];
const materialMigrations: NullableDevkitMigration[] = [
MatCoreMigration,
ExplicitSystemVariablePrefixMigration,
];

/** Entry point for the migration schematics with target of Angular Material v19 */
export function updateToV19(): Rule {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import {DevkitContext, Migration, ResolvedResource, UpgradeData} from '@angular/cdk/schematics';

/**
* Migration that adds `system-variables-prefix` to apps that have `use-system-variables` enabled.
*/
export class ExplicitSystemVariablePrefixMigration extends Migration<UpgradeData, DevkitContext> {
override enabled = true;

override visitStylesheet(stylesheet: ResolvedResource): void {
if (!stylesheet.filePath.endsWith('.scss')) {
return;
}

const content = this.fileSystem.read(stylesheet.filePath);
if (!content || !content.includes('@angular/material')) {
return;
}

const changes = this._getChanges(content);

if (changes.length > 0) {
const update = this.fileSystem.edit(stylesheet.filePath);

for (let i = changes.length - 1; i > -1; i--) {
update.insertRight(changes[i].start, changes[i].text);
}

this.fileSystem.commitEdits();
}
}

/** Gets the changes that should be applied to a file. */
private _getChanges(content: string) {
const key = 'use-system-variables';
const prefixKey = 'system-variables-prefix';
const changes: {start: number; text: string}[] = [];
let index = content.indexOf(key);

// Note: this migration is a bit rudimentary, because Sass doesn't expose a proper AST.
while (index > -1) {
const colonIndex = content.indexOf(':', index);
const valueEnd = colonIndex === -1 ? -1 : this._getValueEnd(content, colonIndex);

if (valueEnd === -1) {
index = content.indexOf(key, index + key.length);
continue;
}

const value = content.slice(colonIndex + 1, valueEnd + 1).trim();
if (value.startsWith('true') && !this._hasSystemPrefix(content, index, prefixKey)) {
changes.push({
start: this._getInsertIndex(content, valueEnd),
text: `${value.endsWith(',') ? '' : ','}\n ${prefixKey}: sys,`,
});
}

index = content.indexOf(key, valueEnd);
}

return changes;
}

/**
* Gets the end index of a Sass map key.
* @param content Content of the file.
* @param startIndex Index at which to start the search.
*/
private _getValueEnd(content: string, startIndex: number): number {
for (let i = startIndex + 1; i < content.length; i++) {
const char = content[i];

if (char === ',' || char === '\n' || char === ')') {
return i;
}
}

return -1;
}

/**
* Gets the index at which to insert the migrated content.
* @param content Initial file content.
* @param valueEnd Index at which the value of the system variables opt-in ends.
*/
private _getInsertIndex(content: string, valueEnd: number): number {
for (let i = valueEnd; i < content.length; i++) {
if (content[i] === '\n') {
return i;
} else if (content[i] === ')') {
return i;
}
}

return valueEnd;
}

/**
* Determines if a map that enables system variables is using system variables already.
* @param content Full file contents.
* @param keyIndex Index at which the systems variable key is defined.
* @param prefixKey Name of the key that defines the prefix.
*/
private _hasSystemPrefix(content: string, keyIndex: number, prefixKey: string): boolean {
// Note: technically this can break if there are other inline maps, but it should be rare.
const mapEnd = content.indexOf(')', keyIndex);

if (mapEnd > -1) {
for (let i = keyIndex; i > -1; i--) {
if (content[i] === '(') {
return content.slice(i, mapEnd).includes(prefixKey);
}
}
}

return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import {UnitTestTree} from '@angular-devkit/schematics/testing';
import {createTestCaseSetup} from '@angular/cdk/schematics/testing';
import {MIGRATION_PATH} from '../../paths';

const THEME_FILE_PATH = '/projects/cdk-testing/src/theme.scss';

describe('v19 explicit system variable prefix migration', () => {
let tree: UnitTestTree;
let writeFile: (filename: string, content: string) => void;
let runMigration: () => Promise<unknown>;

function stripWhitespace(content: string): string {
return content.replace(/\s/g, '');
}

beforeEach(async () => {
const testSetup = await createTestCaseSetup('migration-v19', MIGRATION_PATH, []);
tree = testSetup.appTree;
writeFile = testSetup.writeFile;
runMigration = testSetup.runFixers;
});

it('should add an explicit system variables prefix', async () => {
writeFile(
THEME_FILE_PATH,
`
@use '@angular/material' as mat;
$theme: mat.define-theme((
color: (
theme-type: 'light',
primary: mat.$azure-palette,
tertiary: mat.$red-palette,
use-system-variables: true
),
typography: (
use-system-variables: true
),
density: (
scale: -1
),
));
@include mat.all-component-themes($theme);
`,
);

await runMigration();

expect(stripWhitespace(tree.readText(THEME_FILE_PATH))).toBe(
stripWhitespace(`
@use '@angular/material' as mat;
$theme: mat.define-theme((
color: (
theme-type: 'light',
primary: mat.$azure-palette,
tertiary: mat.$red-palette,
use-system-variables: true,
system-variables-prefix: sys,
),
typography: (
use-system-variables: true,
system-variables-prefix: sys,
),
density: (
scale: -1
),
));
@include mat.all-component-themes($theme);
`),
);
});

it('should add an explicit system variables prefix if the value is using trailing commas', async () => {
writeFile(
THEME_FILE_PATH,
`
@use '@angular/material' as mat;
$theme: mat.define-theme((
color: (
theme-type: 'light',
primary: mat.$azure-palette,
tertiary: mat.$red-palette,
use-system-variables: true,
),
typography: (
use-system-variables: true,
),
density: (
scale: -1
),
));
@include mat.all-component-themes($theme);
`,
);

await runMigration();

expect(stripWhitespace(tree.readText(THEME_FILE_PATH))).toBe(
stripWhitespace(`
@use '@angular/material' as mat;
$theme: mat.define-theme((
color: (
theme-type: 'light',
primary: mat.$azure-palette,
tertiary: mat.$red-palette,
use-system-variables: true,
system-variables-prefix: sys,
),
typography: (
use-system-variables: true,
system-variables-prefix: sys,
),
density: (
scale: -1
),
));
@include mat.all-component-themes($theme);
`),
);
});

it('should not add an explicit system variables prefix if the map has one already', async () => {
writeFile(
THEME_FILE_PATH,
`
@use '@angular/material' as mat;
$theme: mat.define-theme((
color: (
theme-type: 'light',
primary: mat.$azure-palette,
tertiary: mat.$red-palette,
use-system-variables: true
),
typography: (
use-system-variables: true,
system-variables-prefix: foo
),
density: (
scale: -1
),
));
@include mat.all-component-themes($theme);
`,
);

await runMigration();

expect(stripWhitespace(tree.readText(THEME_FILE_PATH))).toBe(
stripWhitespace(`
@use '@angular/material' as mat;
$theme: mat.define-theme((
color: (
theme-type: 'light',
primary: mat.$azure-palette,
tertiary: mat.$red-palette,
use-system-variables: true,
system-variables-prefix: sys,
),
typography: (
use-system-variables: true,
system-variables-prefix: foo
),
density: (
scale: -1
),
));
@include mat.all-component-themes($theme);
`),
);
});

it('should handle a single-line map', async () => {
writeFile(
THEME_FILE_PATH,
`
@use '@angular/material' as mat;
$theme: mat.define-theme((
color: (theme-type: 'light', primary: mat.$azure-palette, use-system-variables: true),
typography: (use-system-variables: true),
density: (scale: -1),
));
@include mat.all-component-themes($theme);
`,
);

await runMigration();

expect(stripWhitespace(tree.readText(THEME_FILE_PATH))).toBe(
stripWhitespace(`
@use '@angular/material' as mat;
$theme: mat.define-theme((
color: (theme-type: 'light', primary: mat.$azure-palette, use-system-variables: true, system-variables-prefix: sys,),
typography: (use-system-variables: true, system-variables-prefix: sys,),
density: (scale: -1),
));
@include mat.all-component-themes($theme);
`),
);
});
});

0 comments on commit b043a35

Please sign in to comment.