-
Notifications
You must be signed in to change notification settings - Fork 6.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(material/core): prevent updates to v17 if project uses legacy com…
…ponents (#28024) 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.
- Loading branch information
Showing
6 changed files
with
200 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
98 changes: 98 additions & 0 deletions
98
src/material/schematics/ng-update/migrations/legacy-imports-error.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
/** | ||
* @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.io/license | ||
*/ | ||
|
||
import {Rule, SchematicContext, Tree} from '@angular-devkit/schematics'; | ||
import {NodePackageInstallTask} from '@angular-devkit/schematics/tasks'; | ||
import * as ts from 'typescript'; | ||
|
||
/** String with which legacy imports start. */ | ||
const LEGACY_IMPORTS_START = '@angular/material/legacy-'; | ||
|
||
/** Maximum files to print in the error message. */ | ||
const MAX_FILES_TO_PRINT = 50; | ||
|
||
/** | ||
* "Migration" that logs an error and prevents further migrations | ||
* from running if the project is using legacy components. | ||
* @param onSuccess Rule to run if there are no legacy imports. | ||
*/ | ||
export function legacyImportsError(onSuccess: Rule): Rule { | ||
return async (tree: Tree, context: SchematicContext) => { | ||
const filesUsingLegacyImports = new Set<string>(); | ||
|
||
tree.visit(path => { | ||
if (!path.endsWith('.ts')) { | ||
return; | ||
} | ||
|
||
const content = tree.readText(path); | ||
const sourceFile = ts.createSourceFile(path, content, ts.ScriptTarget.Latest); | ||
|
||
sourceFile.forEachChild(function walk(node) { | ||
const isImportOrExport = ts.isImportDeclaration(node) || ts.isExportDeclaration(node); | ||
|
||
if ( | ||
isImportOrExport && | ||
node.moduleSpecifier && | ||
ts.isStringLiteralLike(node.moduleSpecifier) && | ||
node.moduleSpecifier.text.startsWith(LEGACY_IMPORTS_START) | ||
) { | ||
filesUsingLegacyImports.add(path); | ||
} | ||
|
||
node.forEachChild(walk); | ||
}); | ||
}); | ||
|
||
// If there are no legacy imports left, we can continue with the migrations. | ||
if (filesUsingLegacyImports.size === 0) { | ||
return onSuccess; | ||
} | ||
|
||
// At this point the project is already at v17 so we need to downgrade it back | ||
// to v16 and run `npm install` again. Ideally we would also throw an error here | ||
// to interrupt the update process, but that would interrupt `npm install` as well. | ||
if (tree.exists('package.json')) { | ||
let packageJson: Record<string, any> | null = null; | ||
|
||
try { | ||
packageJson = JSON.parse(tree.readText('package.json')) as Record<string, any>; | ||
} catch {} | ||
|
||
if (packageJson !== null && packageJson['dependencies']) { | ||
packageJson['dependencies']['@angular/material'] = '^16.2.0'; | ||
tree.overwrite('package.json', JSON.stringify(packageJson, null, 2)); | ||
context.addTask(new NodePackageInstallTask()); | ||
} | ||
} | ||
|
||
context.logger.fatal(formatErrorMessage(filesUsingLegacyImports)); | ||
return; | ||
}; | ||
} | ||
|
||
function formatErrorMessage(filesUsingLegacyImports: Set<string>): string { | ||
const files = Array.from(filesUsingLegacyImports, path => ' - ' + path); | ||
const filesMessage = | ||
files.length > MAX_FILES_TO_PRINT | ||
? [ | ||
...files.slice(0, MAX_FILES_TO_PRINT), | ||
`${files.length - MAX_FILES_TO_PRINT} more...`, | ||
`Search your project for "${LEGACY_IMPORTS_START}" to view all usages.`, | ||
].join('\n') | ||
: files.join('\n'); | ||
|
||
return ( | ||
`Cannot update to Angular Material v17 because the project is using the legacy ` + | ||
`Material components\nthat have been deleted. While Angular Material v16 is compatible with ` + | ||
`Angular v17, it is recommended\nto switch away from the legacy components as soon as possible ` + | ||
`because they no longer receive bug fixes,\naccessibility improvements and new features.\n\n` + | ||
`Read more about migrating away from legacy components: https://material.angular.io/guide/mdc-migration\n\n` + | ||
`Files in the project using legacy Material components:\n${filesMessage}\n` | ||
); | ||
} |
83 changes: 83 additions & 0 deletions
83
src/material/schematics/ng-update/test-cases/legacy-imports-error.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import {createTestCaseSetup} from '@angular/cdk/schematics/testing'; | ||
import {UnitTestTree} from '@angular-devkit/schematics/testing'; | ||
import {logging} from '@angular-devkit/core'; | ||
import {MIGRATION_PATH} from '../../paths'; | ||
|
||
describe('legacy imports error', () => { | ||
const PATH = 'projects/material-testing/'; | ||
let runFixers: () => Promise<unknown>; | ||
let tree: UnitTestTree; | ||
let writeFile: (path: string, content: string) => void; | ||
let fatalLogs: string[]; | ||
|
||
beforeEach(async () => { | ||
const setup = await createTestCaseSetup('migration-v17', MIGRATION_PATH, []); | ||
runFixers = setup.runFixers; | ||
writeFile = setup.writeFile; | ||
tree = setup.appTree; | ||
fatalLogs = []; | ||
setup.runner.logger.subscribe((entry: logging.LogEntry) => { | ||
if (entry.level === 'fatal') { | ||
fatalLogs.push(entry.message); | ||
} | ||
}); | ||
}); | ||
|
||
afterEach(() => { | ||
runFixers = tree = writeFile = fatalLogs = null!; | ||
}); | ||
|
||
it('should log a fatal message if the app imports a legacy import', async () => { | ||
writeFile( | ||
`${PATH}/src/app/app.module.ts`, | ||
` | ||
import {NgModule} from '@angular/core'; | ||
import {MatLegacyButtonModule} from '@angular/material/legacy-button'; | ||
@NgModule({ | ||
imports: [MatLegacyButtonModule], | ||
}) | ||
export class AppModule {} | ||
`, | ||
); | ||
|
||
await runFixers(); | ||
|
||
expect(fatalLogs.length).toBe(1); | ||
expect(fatalLogs[0]).toContain( | ||
'Cannot update to Angular Material v17, ' + | ||
'because the project is using the legacy Material components', | ||
); | ||
}); | ||
|
||
it('should downgrade the app to v16 if it contains legacy imports', async () => { | ||
writeFile( | ||
`${PATH}/package.json`, | ||
`{ | ||
"name": "test", | ||
"version": "0.0.0", | ||
"dependencies": { | ||
"@angular/material": "^17.0.0" | ||
} | ||
}`, | ||
); | ||
|
||
writeFile( | ||
`${PATH}/src/app/app.module.ts`, | ||
` | ||
import {NgModule} from '@angular/core'; | ||
import {MatLegacyButtonModule} from '@angular/material/legacy-button'; | ||
@NgModule({ | ||
imports: [MatLegacyButtonModule], | ||
}) | ||
export class AppModule {} | ||
`, | ||
); | ||
|
||
await runFixers(); | ||
|
||
const content = JSON.parse(tree.readText('/package.json')) as Record<string, any>; | ||
expect(content['dependencies']['@angular/material']).toBe('^16.2.0'); | ||
}); | ||
}); |