Skip to content

Commit b238105

Browse files
committed
fix(material/core): prevent updates to v17 if project uses legacy components
These changes add a schematic that will log a fatal error and prevent the app from updating to v17 if it's using legacy components. Legacy components have been deleted in v17 so the app won't build if it updates.
1 parent 6719168 commit b238105

File tree

6 files changed

+183
-12
lines changed

6 files changed

+183
-12
lines changed

guides/v15-mdc-migration.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Migrating to MDC-based Angular Material Components
22

3-
In Angular Material v15, many of the components have been refactored to be based on the official
3+
In Angular Material v15 and later, many of the components have been refactored to be based on the official
44
[Material Design Components for Web (MDC)](https://github.com/material-components/material-components-web).
55
The components from the following imports have been refactored:
66

@@ -81,22 +81,22 @@ practices before migrating.
8181
component. Using component harnesses makes your tests easier to understand and more robust to
8282
changes in Angular Material
8383

84-
### 1. Update to Angular Material v15
84+
### 1. Update to Angular Material v16
8585

8686
Angular Material includes a schematic to help migrate applications to use the new MDC-based
87-
components. To get started, upgrade your application to Angular Material 15.
87+
components. To get started, upgrade your application to Angular Material 16.
8888

8989
```shell
90-
ng update @angular/material@15
90+
ng update @angular/material@16
9191
```
9292

9393
As part of this update, a schematic will run to automatically move your application to use the
9494
"legacy" imports containing the old component implementations. This provides a quick path to getting
95-
your application running on v15 with minimal manual changes.
95+
your application running on v16 with minimal manual changes.
9696

9797
### 2. Run the migration tool
9898

99-
After upgrading to v15, you can run the migration tool to switch from the legacy component
99+
After upgrading to v16, you can run the migration tool to switch from the legacy component
100100
implementations to the new MDC-based ones.
101101

102102
```shell

src/material/schematics/migration.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"schematics": {
44
"migration-v17": {
55
"version": "17.0.0-0",
6-
"description": "Updates the Angular Material to v17",
6+
"description": "Updates Angular Material to v17",
77
"factory": "./ng-update/index_bundled#updateToV17"
88
}
99
}

src/material/schematics/ng-update/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ ts_library(
6969
"//src/cdk/schematics",
7070
"//src/cdk/schematics/testing",
7171
"//src/material/schematics:paths",
72+
"@npm//@angular-devkit/core",
7273
"@npm//@angular-devkit/schematics",
7374
"@npm//@bazel/runfiles",
7475
"@npm//@types/jasmine",

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,24 @@ import {
1313
TargetVersion,
1414
} from '@angular/cdk/schematics';
1515

16+
import {legacyImportsError} from './migrations/legacy-imports-error';
1617
import {materialUpgradeData} from './upgrade-data';
1718
import {ThemeBaseMigration} from './migrations/theme-base-v17';
1819

1920
const materialMigrations: NullableDevkitMigration[] = [ThemeBaseMigration];
2021

2122
/** Entry point for the migration schematics with target of Angular Material v17 */
2223
export function updateToV17(): Rule {
23-
return createMigrationSchematicRule(
24-
TargetVersion.V17,
25-
materialMigrations,
26-
materialUpgradeData,
27-
onMigrationComplete,
24+
// We pass the v17 migration rule as a callback, instead of using `chain()`, because the
25+
// legacy imports error only logs an error message, it doesn't actually interrupt the migration
26+
// process and we don't want to execute migrations if there are leftover legacy imports.
27+
return legacyImportsError(
28+
createMigrationSchematicRule(
29+
TargetVersion.V17,
30+
materialMigrations,
31+
materialUpgradeData,
32+
onMigrationComplete,
33+
),
2834
);
2935
}
3036

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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.io/license
7+
*/
8+
9+
import {Rule, SchematicContext, Tree} from '@angular-devkit/schematics';
10+
import {NodePackageInstallTask} from '@angular-devkit/schematics/tasks';
11+
import * as ts from 'typescript';
12+
13+
/**
14+
* "Migration" that logs an error and prevents further migrations
15+
* from running if the project is using legacy components.
16+
* @param onSuccess Rule to run if there are no legacy imports.
17+
*/
18+
export function legacyImportsError(onSuccess: Rule): Rule {
19+
return async (tree: Tree, context: SchematicContext) => {
20+
const filesUsingLegacyImports = new Set<string>();
21+
22+
tree.visit(path => {
23+
if (!path.endsWith('.ts')) {
24+
return;
25+
}
26+
27+
const content = tree.readText(path);
28+
const sourceFile = ts.createSourceFile(path, content, ts.ScriptTarget.Latest);
29+
30+
sourceFile.forEachChild(function walk(node) {
31+
const isImportOrExport = ts.isImportDeclaration(node) || ts.isExportDeclaration(node);
32+
33+
if (
34+
isImportOrExport &&
35+
node.moduleSpecifier &&
36+
ts.isStringLiteralLike(node.moduleSpecifier) &&
37+
node.moduleSpecifier.text.startsWith('@angular/material/legacy-')
38+
) {
39+
filesUsingLegacyImports.add(path);
40+
}
41+
42+
node.forEachChild(walk);
43+
});
44+
});
45+
46+
// If there are no legacy imports left, we can continue with the migrations.
47+
if (filesUsingLegacyImports.size === 0) {
48+
return onSuccess;
49+
}
50+
51+
// At this point the project is already at v17 so we need to downgrade it back
52+
// to v16 and run `npm install` again. Ideally we would also throw an error here
53+
// to interrupt the update process, but that would interrupt `npm install` as well.
54+
if (tree.exists('package.json')) {
55+
let packageJson: Record<string, any> | null = null;
56+
57+
try {
58+
packageJson = JSON.parse(tree.readText('package.json')) as Record<string, any>;
59+
} catch {}
60+
61+
if (packageJson !== null && packageJson['dependencies']) {
62+
packageJson['dependencies']['@angular/material'] = '^16.2.0';
63+
tree.overwrite('package.json', JSON.stringify(packageJson, null, 2));
64+
context.addTask(new NodePackageInstallTask());
65+
}
66+
}
67+
68+
const errorMessage =
69+
`Cannot update to Angular Material v17, because the project is using the legacy ` +
70+
`Material components\nthat have been deleted. While Angular Material v16 is compatible with ` +
71+
`Angular v17, it is recommended\nto switch away from the legacy components as soon as possible, ` +
72+
`because they no longer receive bug fixes,\naccessibility improvements and new features.\n\n` +
73+
`Read more about migrating away from legacy components: https://material.angular.io/guide/mdc-migration\n\n` +
74+
`Files in the project using legacy Material components:\n` +
75+
Array.from(filesUsingLegacyImports, path => ' - ' + path).join('\n') +
76+
'\n';
77+
78+
context.logger.fatal(errorMessage);
79+
return;
80+
};
81+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import {createTestCaseSetup} from '@angular/cdk/schematics/testing';
2+
import {UnitTestTree} from '@angular-devkit/schematics/testing';
3+
import {logging} from '@angular-devkit/core';
4+
import {MIGRATION_PATH} from '../../paths';
5+
6+
describe('legacy imports error', () => {
7+
const PATH = 'projects/material-testing/';
8+
let runFixers: () => Promise<unknown>;
9+
let tree: UnitTestTree;
10+
let writeFile: (path: string, content: string) => void;
11+
let fatalLogs: string[];
12+
13+
beforeEach(async () => {
14+
const setup = await createTestCaseSetup('migration-v17', MIGRATION_PATH, []);
15+
runFixers = setup.runFixers;
16+
writeFile = setup.writeFile;
17+
tree = setup.appTree;
18+
fatalLogs = [];
19+
setup.runner.logger.subscribe((entry: logging.LogEntry) => {
20+
if (entry.level === 'fatal') {
21+
fatalLogs.push(entry.message);
22+
}
23+
});
24+
});
25+
26+
afterEach(() => {
27+
runFixers = tree = writeFile = fatalLogs = null!;
28+
});
29+
30+
it('should log a fatal message if the app imports a legacy import', async () => {
31+
writeFile(
32+
`${PATH}/src/app/app.module.ts`,
33+
`
34+
import {NgModule} from '@angular/core';
35+
import {MatLegacyButtonModule} from '@angular/material/legacy-button';
36+
37+
@NgModule({
38+
imports: [MatLegacyButtonModule],
39+
})
40+
export class AppModule {}
41+
`,
42+
);
43+
44+
await runFixers();
45+
46+
expect(fatalLogs.length).toBe(1);
47+
expect(fatalLogs[0]).toContain(
48+
'Cannot update to Angular Material v17, ' +
49+
'because the project is using the legacy Material components',
50+
);
51+
});
52+
53+
it('should downgrade the app to v16 if it contains legacy imports', async () => {
54+
writeFile(
55+
`${PATH}/package.json`,
56+
`{
57+
"name": "test",
58+
"version": "0.0.0",
59+
"dependencies": {
60+
"@angular/material": "^17.0.0"
61+
}
62+
}`,
63+
);
64+
65+
writeFile(
66+
`${PATH}/src/app/app.module.ts`,
67+
`
68+
import {NgModule} from '@angular/core';
69+
import {MatLegacyButtonModule} from '@angular/material/legacy-button';
70+
71+
@NgModule({
72+
imports: [MatLegacyButtonModule],
73+
})
74+
export class AppModule {}
75+
`,
76+
);
77+
78+
await runFixers();
79+
80+
const content = JSON.parse(tree.readText('/package.json')) as Record<string, any>;
81+
expect(content['dependencies']['@angular/material']).toBe('^16.2.0');
82+
});
83+
});

0 commit comments

Comments
 (0)