fix(migrations): skip tsconfig files of non-Angular projects

Only touch tsconfig files for targets that use an Angular builder, including community ones like Nx, so non-Angular projects in mixed workspaces are left alone.

Fixes #69837

(cherry picked from commit f72600eadd)
This commit is contained in:
lazerg
2026-07-20 20:54:02 +05:00
committed by Matthew Beck
parent d457cc9d2e
commit d85288d75f
6 changed files with 165 additions and 2 deletions
@@ -12,7 +12,9 @@ import {JSONFile} from '@schematics/angular/utility/json-file';
export function migrate(): Rule {
return async (tree: Tree) => {
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree, {
angularBuildersOnly: true,
});
const allPaths = [...new Set([...buildPaths, ...testPaths])];
for (const tsconfigPath of allPaths) {
@@ -28,6 +28,7 @@ describe('strict-safe-navigation-narrow migration', () => {
root: '',
architect: {
build: {
builder: '@angular/build:application',
options: {
tsConfig: './tsconfig.json',
},
@@ -138,4 +139,44 @@ describe('strict-safe-navigation-narrow migration', () => {
tsconfig.angularCompilerOptions.extendedDiagnostics.checks.nullishCoalescingNotNullable,
).toBe('suppress');
});
it('should not migrate tsconfig files of projects that do not use an Angular builder', async () => {
tree.overwrite(
'/angular.json',
JSON.stringify({
version: 1,
projects: {
'angular-app': {
root: '',
architect: {
build: {
builder: '@angular/build:application',
options: {tsConfig: './tsconfig.app.json'},
},
},
},
'node-lib': {
root: '',
architect: {
build: {
builder: '@nx/js:tsc',
options: {tsConfig: './tsconfig.lib.json'},
},
},
},
},
}),
);
tree.create('/tsconfig.app.json', JSON.stringify({compilerOptions: {target: 'es2020'}}));
tree.create('/tsconfig.lib.json', JSON.stringify({compilerOptions: {target: 'es2020'}}));
const runMigration = migrate();
await runMigration(tree, {} as any);
const appTsconfig = parseConfig(tree, '/tsconfig.app.json');
expect(
appTsconfig.angularCompilerOptions.extendedDiagnostics.checks.nullishCoalescingNotNullable,
).toBe('suppress');
expect(parseConfig(tree, '/tsconfig.lib.json').angularCompilerOptions).toBeUndefined();
});
});
@@ -44,7 +44,9 @@ function getResolvedAngularCompilerOptions(tree: any, tsconfigPath: string): Rec
*/
export function migrate(): Rule {
return async (tree) => {
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree, {
angularBuildersOnly: true,
});
const allPaths = [...new Set([...buildPaths, ...testPaths])];
for (const tsconfigPath of allPaths) {
@@ -28,6 +28,7 @@ describe('strict-templates-default migration', () => {
root: '',
architect: {
build: {
builder: '@angular/build:application',
options: {
tsConfig: './tsconfig.json',
},
@@ -197,4 +198,41 @@ describe('strict-templates-default migration', () => {
expect(baseTsconfig.angularCompilerOptions).toBeDefined();
expect(appTsconfig.angularCompilerOptions).toBeUndefined();
});
it('should not migrate tsconfig files of projects that do not use an Angular builder', async () => {
tree.overwrite(
'/angular.json',
JSON.stringify({
version: 1,
projects: {
'angular-app': {
root: '',
architect: {
build: {
builder: '@angular/build:application',
options: {tsConfig: './tsconfig.app.json'},
},
},
},
'node-lib': {
root: '',
architect: {
build: {
builder: '@nx/js:tsc',
options: {tsConfig: './tsconfig.lib.json'},
},
},
},
},
}),
);
tree.create('/tsconfig.app.json', JSON.stringify({compilerOptions: {target: 'es2020'}}));
tree.create('/tsconfig.lib.json', JSON.stringify({compilerOptions: {target: 'es2020'}}));
await migrate()(tree, {} as any);
const appTsconfig = parseConfig(tree, '/tsconfig.app.json');
expect(appTsconfig.angularCompilerOptions.strictTemplates).toBe(false);
expect(parseConfig(tree, '/tsconfig.lib.json').angularCompilerOptions).toBeUndefined();
});
});
@@ -100,4 +100,59 @@ describe('project tsconfig paths', () => {
expect((await getProjectTsConfigPaths(testTree)).buildPaths).toEqual(['tsconfig.json']);
});
it('should only return tsconfig paths of Angular builders when `angularBuildersOnly` is set', async () => {
testTree.create('/tsconfig.app.json', '');
testTree.create('/tsconfig.nx.json', '');
testTree.create('/tsconfig.lib.json', '');
testTree.create('/tsconfig.ngx-build-plus.json', '');
testTree.create(
'/angular.json',
JSON.stringify({
version: 1,
projects: {
angular_app: {
root: '',
architect: {
build: {
builder: '@angular/build:application',
options: {tsConfig: './tsconfig.app.json'},
},
},
},
nx_angular_app: {
root: '',
architect: {
build: {
builder: '@nx/angular:webpack-browser',
options: {tsConfig: './tsconfig.nx.json'},
},
},
},
node_lib: {
root: '',
architect: {
build: {builder: '@nx/js:tsc', options: {tsConfig: './tsconfig.lib.json'}},
},
},
ngx_build_plus_app: {
root: '',
architect: {
build: {
builder: 'ngx-build-plus:browser',
options: {tsConfig: './tsconfig.ngx-build-plus.json'},
},
},
},
},
}),
);
const {buildPaths} = await getProjectTsConfigPaths(testTree, {angularBuildersOnly: true});
expect(buildPaths).toEqual([
'tsconfig.app.json',
'tsconfig.nx.json',
'tsconfig.ngx-build-plus.json',
]);
});
});
@@ -12,9 +12,14 @@ import {Tree} from '@angular-devkit/schematics';
/**
* Gets all tsconfig paths from a CLI project by reading the workspace configuration
* and looking for common tsconfig locations.
*
* When `angularBuildersOnly` is set, only targets that use an Angular builder are considered.
* This avoids picking up tsconfig files of non-Angular projects in a mixed workspace (e.g. an Nx
* monorepo), which should not be touched by Angular migrations.
*/
export async function getProjectTsConfigPaths(
tree: Tree,
{angularBuildersOnly = false}: {angularBuildersOnly?: boolean} = {},
): Promise<{buildPaths: string[]; testPaths: string[]}> {
// Start with some tsconfig paths that are generally used within CLI projects. Note
// that we are not interested in IDE-specific tsconfig files (e.g. /tsconfig.json)
@@ -24,6 +29,10 @@ export async function getProjectTsConfigPaths(
const workspace = await getWorkspace(tree);
for (const [, project] of workspace.projects) {
for (const [name, target] of project.targets) {
if (angularBuildersOnly && !isAngularBuilder(target.builder)) {
continue;
}
for (const [, options] of allTargetOptions(target)) {
const tsConfig = options['tsConfig'];
// Filter out tsconfig files that don't exist in the CLI project.
@@ -46,6 +55,22 @@ export async function getProjectTsConfigPaths(
};
}
/** Prefixes of Angular builders, including common community builders (e.g. Nx). */
const angularBuilderPrefixes = [
'@angular-devkit/build-angular:',
'@angular/build:',
'@nx/angular:',
'@angular-builders/',
'ngx-build-plus:',
];
/** Whether the given builder is a recognized Angular builder. */
function isAngularBuilder(builder: string | undefined): boolean {
return (
builder !== undefined && angularBuilderPrefixes.some((prefix) => builder.startsWith(prefix))
);
}
/** Get options for all configurations for the passed builder target. */
function* allTargetOptions(
target: workspaces.TargetDefinition,