diff --git a/packages/angular/migrations.json b/packages/angular/migrations.json index c3694aeeb4795..006864a21f972 100644 --- a/packages/angular/migrations.json +++ b/packages/angular/migrations.json @@ -84,6 +84,12 @@ "version": "12.3.5-beta.0", "description": "Convert targets using @nrwl/angular:webpack-browser with the buildTarget option set to use the @nrwl/angular:delegate-build executor instead.", "factory": "./src/migrations/update-12-3-0/convert-webpack-browser-build-target-to-delegate-build" + }, + "update-invalid-import-paths": { + "cli": "nx", + "version": "12.9.0", + "description": "Fixes invalid importPaths for buildable and publishable libs.", + "factory": "./src/migrations/update-12-9-0/update-invalid-import-paths" } }, "packageJsonUpdates": { diff --git a/packages/angular/src/migrations/update-12-9-0/update-invalid-import-paths.spec.ts b/packages/angular/src/migrations/update-12-9-0/update-invalid-import-paths.spec.ts new file mode 100644 index 0000000000000..e9d5c2c5ea183 --- /dev/null +++ b/packages/angular/src/migrations/update-12-9-0/update-invalid-import-paths.spec.ts @@ -0,0 +1,85 @@ +import type { Tree } from '@nrwl/devkit'; +import { updateJson, joinPathFragments, readJson } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; + +import libGenerator from '../../generators/library/library'; +import updateInvalidImportPaths from './update-invalid-import-paths'; + +describe('Migration to fix invalid import paths in affected workspaces', () => { + let tree: Tree; + + beforeEach(async () => { + tree = createTreeWithEmptyWorkspace(); + + // set up some libs + + await libGenerator(tree, { + name: 'buildable1', + buildable: true, + directory: 'dir1', + }); + + await libGenerator(tree, { + name: 'buildable2', + buildable: true, + directory: 'dir1', + }); + + await libGenerator(tree, { + name: 'publishable1', + publishable: true, + directory: 'dir1', + importPath: '@proj/publishable1', + }); + + await libGenerator(tree, { + name: 'publishable2', + publishable: true, + directory: 'dir1', + importPath: '@proj/publishable2', + }); + + // break one of each kind + updateJson( + tree, + joinPathFragments('libs/dir1/buildable1', 'package.json'), + (pkgJson) => { + pkgJson.name = '@proj/dir1-buildable1'; + return pkgJson; + } + ); + + updateJson(tree, 'tsconfig.base.json', (tsconfig) => { + const srcPath = tsconfig['@proj/publishable2']; + tsconfig['@proj/publishable2'] = undefined; + + tsconfig['@proj/dir1/publishable2'] = srcPath; + + return tsconfig; + }); + }); + + it('should fix the invalid libraries', async () => { + // ACT + await updateInvalidImportPaths(tree); + + // ASSERT + const fixedBuildable = readJson( + tree, + joinPathFragments('libs/dir1/buildable1', 'package.json') + ); + + const { compilerOptions } = readJson<{ + compilerOptions: { paths: Record }; + }>(tree, 'tsconfig.base.json'); + const { paths: tsConfigPaths } = compilerOptions; + const fixedPublishable = Boolean(tsConfigPaths['@proj/publishable2']); + const brokenPublishableShouldntExist = !Boolean( + tsConfigPaths['@proj/publishable2'] + ); + + expect(fixedBuildable.name).toEqual('@proj/dir1/buildable1'); + expect(fixedPublishable).toBeTruthy(); + expect(brokenPublishableShouldntExist).toBeFalsy(); + }); +}); diff --git a/packages/angular/src/migrations/update-12-9-0/update-invalid-import-paths.ts b/packages/angular/src/migrations/update-12-9-0/update-invalid-import-paths.ts new file mode 100644 index 0000000000000..b9b0e8a4f93cf --- /dev/null +++ b/packages/angular/src/migrations/update-12-9-0/update-invalid-import-paths.ts @@ -0,0 +1,140 @@ +import type { + NxJsonProjectConfiguration, + ProjectConfiguration, + Tree, +} from '@nrwl/devkit'; +import { + formatFiles, + getProjects, + readJson, + joinPathFragments, + updateJson, + logger, +} from '@nrwl/devkit'; + +type AffectedLib = ProjectConfiguration & NxJsonProjectConfiguration; +type InvalidLibs = { + buildableLibs: AffectedLib[]; + publishableLibs: AffectedLib[]; +}; + +export default async function (tree: Tree) { + const possibleAffectedLibs = findBuildableAndPublishableLibs(tree); + const invalidLibs = findInvalidLibs(tree, possibleAffectedLibs); + fixLibs(tree, invalidLibs); + + await formatFiles(tree); +} + +export function findBuildableAndPublishableLibs(tree: Tree): InvalidLibs { + const projects = getProjects(tree); + const buildableLibs: AffectedLib[] = []; + const publishableLibs: AffectedLib[] = []; + + for (const [name, project] of projects) { + for (const target of Object.values(project.targets || {})) { + if (target.executor === '@nrwl/angular:package') { + publishableLibs.push(project); + } else if (target.executor === '@nrwl/angular:ng-packagr-lite') { + buildableLibs.push(project); + } + } + } + + return { buildableLibs, publishableLibs }; +} + +export function findInvalidLibs(tree: Tree, libs: InvalidLibs): InvalidLibs { + const { compilerOptions } = readJson(tree, 'tsconfig.base.json'); + const { paths: tsConfigPaths } = compilerOptions; + + const invalidBuildableLibs = libs.buildableLibs.filter((lib) => + checkInvalidLib(tree, lib, tsConfigPaths) + ); + const invalidPublishableLibs = libs.publishableLibs.filter((lib) => + checkInvalidLib(tree, lib, tsConfigPaths) + ); + return { + buildableLibs: invalidBuildableLibs, + publishableLibs: invalidPublishableLibs, + }; +} + +function checkInvalidLib( + tree: Tree, + lib: AffectedLib, + tsConfigPaths: Record +) { + const { name } = readJson(tree, joinPathFragments(lib.root, 'package.json')); + return !tsConfigPaths[name]; +} + +function fixLibs(tree: Tree, { buildableLibs, publishableLibs }: InvalidLibs) { + const { compilerOptions } = readJson(tree, 'tsconfig.base.json'); + const { paths: tsConfigPaths } = compilerOptions; + + buildableLibs.map((lib) => fixBuildableLib(tree, lib, tsConfigPaths)); + publishableLibs.map((lib) => fixPublishableLib(tree, lib, tsConfigPaths)); +} + +function fixBuildableLib( + tree: Tree, + lib: AffectedLib, + tsConfigPaths: Record +) { + const srcRoot = joinPathFragments(lib.sourceRoot, 'index.ts'); + + for (const [validPackageName, tsLibSrcRoot] of Object.entries( + tsConfigPaths + )) { + if (tsLibSrcRoot[0] === srcRoot) { + updateJson( + tree, + joinPathFragments(lib.root, 'package.json'), + (pkgJson) => { + pkgJson.name = validPackageName; + + return pkgJson; + } + ); + break; + } + } +} + +function fixPublishableLib( + tree: Tree, + lib: AffectedLib, + tsConfigPaths: Record +) { + const srcRoot = joinPathFragments(lib.sourceRoot, 'index.ts'); + const { name: pkgName } = readJson( + tree, + joinPathFragments(lib.root, 'package.json') + ); + + const pkgNameParts = pkgName.split('/'); + if (Array.isArray(pkgNameParts) && pkgNameParts.length > 2) { + logger.warn( + `Your publishable package ${pkgName} is an invalid NPM Package name. Please ensure it only contains one '/'.` + ); + logger.warn( + `The affected package.json is at '${joinPathFragments( + lib.root, + 'package.json' + )}'` + ); + } + + for (const [invalidPathKey, tsLibSrcRoot] of Object.entries(tsConfigPaths)) { + if (tsLibSrcRoot[0] === srcRoot) { + updateJson(tree, 'tsconfig.base.json', (tsconfig) => { + tsconfig.compilerOptions.paths[invalidPathKey] = undefined; + tsconfig.compilerOptions.paths[pkgName] = tsLibSrcRoot; + + return tsconfig; + }); + break; + } + } +}