diff --git a/packages/core/schematics/BUILD.bazel b/packages/core/schematics/BUILD.bazel index 05a04698b38..1202b534636 100644 --- a/packages/core/schematics/BUILD.bazel +++ b/packages/core/schematics/BUILD.bazel @@ -137,6 +137,10 @@ bundle_entrypoints = [ "incremental-hydration", "packages/core/schematics/migrations/incremental-hydration/index.js", ], + [ + "strict-safe-navigation-narrow", + "packages/core/schematics/migrations/strict-safe-navigation-narrow/index.js", + ], ] rollup.rollup( @@ -152,6 +156,7 @@ rollup.rollup( "//packages/core/schematics/migrations/change-detection-eager", "//packages/core/schematics/migrations/http-xhr-backend", "//packages/core/schematics/migrations/incremental-hydration", + "//packages/core/schematics/migrations/strict-safe-navigation-narrow", "//packages/core/schematics/migrations/strict-templates-default", "//packages/core/schematics/ng-generate/cleanup-unused-imports", "//packages/core/schematics/ng-generate/common-to-standalone-migration", diff --git a/packages/core/schematics/migrations.json b/packages/core/schematics/migrations.json index 4929b359f00..1cd0a358d51 100644 --- a/packages/core/schematics/migrations.json +++ b/packages/core/schematics/migrations.json @@ -24,6 +24,11 @@ "version": "22.0.0", "description": "Adds withNoIncrementalHydration() opt out to provideClientHydration() when incremental hydration is not enabled to retain pre-v22 behavior-.", "factory": "./bundles/incremental-hydration.cjs#migrate" + }, + "strict-safe-navigation-narrow": { + "version": "22.0.0", + "description": "Disables the 'nullishCoalescingNotNullable & optionalChainNotNullable extended diagnostics.", + "factory": "./bundles/strict-safe-navigation-narrow.cjs#migrate" } } } diff --git a/packages/core/schematics/migrations/strict-safe-navigation-narrow/BUILD.bazel b/packages/core/schematics/migrations/strict-safe-navigation-narrow/BUILD.bazel new file mode 100644 index 00000000000..19c83ab0513 --- /dev/null +++ b/packages/core/schematics/migrations/strict-safe-navigation-narrow/BUILD.bazel @@ -0,0 +1,40 @@ +load("//tools:defaults.bzl", "jasmine_test", "ts_project") + +package( + default_visibility = [ + "//packages/core/schematics:__pkg__", + "//packages/core/schematics/test:__pkg__", + ], +) + +ts_project( + name = "strict-safe-navigation-narrow", + srcs = glob( + ["**/*.ts"], + exclude = ["*.spec.ts"], + ), + deps = [ + "//:node_modules/@angular-devkit/core", + "//:node_modules/@angular-devkit/schematics", + "//:node_modules/@schematics/angular", + "//packages/core/schematics/utils", + ], +) + +ts_project( + name = "test_lib", + testonly = True, + srcs = glob(["*.spec.ts"]), + deps = [ + ":strict-safe-navigation-narrow", + "//:node_modules/@angular-devkit/core", + "//:node_modules/@angular-devkit/schematics", + "//:node_modules/@schematics/angular", + "//packages/core/schematics/utils", + ], +) + +jasmine_test( + name = "test", + data = [":test_lib"], +) diff --git a/packages/core/schematics/migrations/strict-safe-navigation-narrow/index.ts b/packages/core/schematics/migrations/strict-safe-navigation-narrow/index.ts new file mode 100644 index 00000000000..bf930593190 --- /dev/null +++ b/packages/core/schematics/migrations/strict-safe-navigation-narrow/index.ts @@ -0,0 +1,40 @@ +/** + * @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 {Rule, Tree} from '@angular-devkit/schematics'; +import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; +import {JSONFile} from '@schematics/angular/utility/json-file'; + +export function migrate(): Rule { + return async (tree: Tree) => { + const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); + const allPaths = [...new Set([...buildPaths, ...testPaths])]; + + for (const tsconfigPath of allPaths) { + const json = new JSONFile(tree, tsconfigPath); + const compilerOptions = json.get(['compilerOptions']); + + if ( + !compilerOptions || + typeof compilerOptions !== 'object' || + Object.keys(compilerOptions).length === 0 + ) { + continue; + } + + json.modify( + ['angularCompilerOptions', 'extendedDiagnostics', 'checks', 'nullishCoalescingNotNullable'], + 'suppress', + ); + json.modify( + ['angularCompilerOptions', 'extendedDiagnostics', 'checks', 'optionalChainNotNullable'], + 'suppress', + ); + } + }; +} diff --git a/packages/core/schematics/migrations/strict-safe-navigation-narrow/migration.spec.ts b/packages/core/schematics/migrations/strict-safe-navigation-narrow/migration.spec.ts new file mode 100644 index 00000000000..7218b2e024d --- /dev/null +++ b/packages/core/schematics/migrations/strict-safe-navigation-narrow/migration.spec.ts @@ -0,0 +1,141 @@ +/** + * @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 {HostTree} from '@angular-devkit/schematics'; +import {UnitTestTree} from '@angular-devkit/schematics/testing/index.js'; +import {migrate} from './index'; + +function parseConfig(tree: UnitTestTree, path: string) { + return JSON.parse(tree.readContent(path).replace(/\/\/.*$/gm, '')); +} + +describe('strict-safe-navigation-narrow migration', () => { + let tree: UnitTestTree; + + beforeEach(() => { + tree = new UnitTestTree(new HostTree()); + tree.create( + '/angular.json', + JSON.stringify({ + version: 1, + projects: { + t: { + root: '', + architect: { + build: { + options: { + tsConfig: './tsconfig.json', + }, + }, + }, + }, + }, + }), + ); + }); + + it('should not add options if compilerOptions is empty', async () => { + tree.create( + '/tsconfig.json', + JSON.stringify({ + compilerOptions: {}, + }), + ); + + const runMigration = migrate(); + await runMigration(tree, {} as any); + + const tsconfig = parseConfig(tree, '/tsconfig.json'); + expect(tsconfig.angularCompilerOptions).toBeUndefined(); + }); + + it('should not add options if compilerOptions is missing', async () => { + tree.create('/tsconfig.json', JSON.stringify({})); + + const runMigration = migrate(); + await runMigration(tree, {} as any); + + const tsconfig = parseConfig(tree, '/tsconfig.json'); + expect(tsconfig.angularCompilerOptions).toBeUndefined(); + }); + + it('should add suppress options if compilerOptions is not empty', async () => { + tree.create( + '/tsconfig.json', + JSON.stringify({ + compilerOptions: { + target: 'es2020', + }, + }), + ); + + const runMigration = migrate(); + await runMigration(tree, {} as any); + + const tsconfig = parseConfig(tree, '/tsconfig.json'); + expect( + tsconfig.angularCompilerOptions.extendedDiagnostics.checks.nullishCoalescingNotNullable, + ).toBe('suppress'); + expect( + tsconfig.angularCompilerOptions.extendedDiagnostics.checks.optionalChainNotNullable, + ).toBe('suppress'); + }); + + it('should preserve existing checks', async () => { + tree.create( + '/tsconfig.json', + JSON.stringify({ + compilerOptions: { + target: 'es2020', + }, + angularCompilerOptions: { + extendedDiagnostics: { + checks: { + somethingElse: 'warning', + }, + }, + }, + }), + ); + + const runMigration = migrate(); + await runMigration(tree, {} as any); + + const tsconfig = parseConfig(tree, '/tsconfig.json'); + expect(tsconfig.angularCompilerOptions.extendedDiagnostics.checks.somethingElse).toBe( + 'warning', + ); + expect( + tsconfig.angularCompilerOptions.extendedDiagnostics.checks.nullishCoalescingNotNullable, + ).toBe('suppress'); + expect( + tsconfig.angularCompilerOptions.extendedDiagnostics.checks.optionalChainNotNullable, + ).toBe('suppress'); + }); + + it('should handle tsconfig with comments', async () => { + tree.create( + '/tsconfig.json', + `{ + // This is a comment + "compilerOptions": { + "target": "es2020" + } + }`, + ); + + const runMigration = migrate(); + await runMigration(tree, {} as any); + + const tsconfig = parseConfig(tree, '/tsconfig.json'); + expect(tsconfig.compilerOptions.target).toBe('es2020'); + expect( + tsconfig.angularCompilerOptions.extendedDiagnostics.checks.nullishCoalescingNotNullable, + ).toBe('suppress'); + }); +});