diff --git a/e2e/nx-misc/src/misc.test.ts b/e2e/nx-misc/src/misc.test.ts index 32a7fa9d43227..84bb36be3b9a4 100644 --- a/e2e/nx-misc/src/misc.test.ts +++ b/e2e/nx-misc/src/misc.test.ts @@ -1,5 +1,6 @@ import { cleanupProject, + getPublishedVersion, isNotWindows, newProject, readFile, @@ -12,7 +13,6 @@ import { updateFile, } from '@nrwl/e2e/utils'; import { renameSync } from 'fs'; -import { packagesWeCareAbout } from 'nx/src/command-line/report'; import * as path from 'path'; describe('Nx Commands', () => { @@ -44,9 +44,12 @@ describe('Nx Commands', () => { it(`should report package versions`, async () => { const reportOutput = runCLI('report'); - packagesWeCareAbout.forEach((p) => { - expect(reportOutput).toContain(p); - }); + expect(reportOutput).toEqual( + expect.stringMatching( + new RegExp(`\@nrwl\/workspace.*:.*${getPublishedVersion()}`) + ) + ); + expect(reportOutput).toContain('@nrwl/workspace'); }, 120000); it(`should list plugins`, async () => { diff --git a/packages/nx/src/command-line/migrate.spec.ts b/packages/nx/src/command-line/migrate.spec.ts index f1afad85d4534..33907fb1f5be7 100644 --- a/packages/nx/src/command-line/migrate.spec.ts +++ b/packages/nx/src/command-line/migrate.spec.ts @@ -1,6 +1,11 @@ import * as enquirer from 'enquirer'; import { PackageJson } from '../utils/package-json'; -import { Migrator, normalizeVersion, parseMigrationsOptions } from './migrate'; +import { + Migrator, + normalizeVersion, + parseMigrationsOptions, + ResolvedMigrationConfiguration, +} from './migrate'; const createPackageJson = ( overrides: Partial = {} @@ -366,9 +371,9 @@ describe('Migration', () => { return { version: '2.0.0', packageGroup: [ - '@my-company/lib-1', - '@my-company/lib-2', - '@my-company/lib-3', + { package: '@my-company/lib-1', version: '*' }, + { package: '@my-company/lib-2', version: '*' }, + { package: '@my-company/lib-3', version: '*' }, { package: '@my-company/lib-4', version: 'latest' }, ], }; @@ -376,13 +381,17 @@ describe('Migration', () => { if (pkg === '@my-company/lib-6') { return { version: '2.0.0', - packageGroup: ['@my-company/nx-workspace'], + packageGroup: [ + { version: '*', package: '@my-company/nx-workspace' }, + ], }; } if (pkg === '@my-company/lib-3') { return { version: '2.0.0', - packageGroup: ['@my-company/lib-3-child'], + packageGroup: [ + { version: '*', package: '@my-company/lib-3-child' }, + ], }; } if (version === 'latest') { @@ -440,19 +449,26 @@ describe('Migration', () => { if (pkg === '@my-company/nx-workspace' && version === '3.0.0') { return { version: '3.0.0', - packageGroup: ['@my-company/lib-1', '@my-company/lib-2'], + packageGroup: [ + { package: '@my-company/lib-1', version: '*' }, + { package: '@my-company/lib-2', version: '*' }, + ], }; } if (pkg === '@my-company/lib-1' && version === 'latest') { return { version: '3.0.0', - packageGroup: ['@my-company/nx-workspace'], + packageGroup: [ + { package: '@my-company/nx-workspace', version: '*' }, + ], }; } if (pkg === '@my-company/lib-1' && version === '3.0.0') { return { version: '3.0.0', - packageGroup: ['@my-company/nx-workspace'], + packageGroup: [ + { package: '@my-company/nx-workspace', version: '*' }, + ], }; } if (pkg === '@my-company/lib-2' && version === '3.0.0') { @@ -827,7 +843,7 @@ describe('Migration', () => { }, }), getInstalledPackageVersion: () => '1.0.0', - fetch: (p) => { + fetch: (p): Promise => { if (p === 'mypackage') { return Promise.resolve({ version: '2.0.0', @@ -837,7 +853,10 @@ describe('Migration', () => { packages: { child1: { version: '3.0.0' } }, }, }, - packageGroup: ['pkg1', 'pkg2'], + packageGroup: [ + { package: 'pkg1', version: '*' }, + { package: 'pkg2', version: '*' }, + ], }); } else if (p === 'pkg1') { // add a delay to showcase the dependent requirement will wait for it @@ -1008,7 +1027,7 @@ describe('Migration', () => { if (p === 'child') return '1.0.0'; return null; }, - fetch: (p, _v) => { + fetch: (p: string): Promise => { if (p === 'parent') { return Promise.resolve({ version: '2.0.0', @@ -1089,7 +1108,7 @@ describe('Migration', () => { if (p === 'newChild') return '1.0.0'; // installed as a transitive dep, not a top-level dep return null; }, - fetch: (p, _v) => { + fetch: (p: string): Promise => { if (p === 'parent') { return Promise.resolve({ version: '2.0.0', diff --git a/packages/nx/src/command-line/migrate.ts b/packages/nx/src/command-line/migrate.ts index 19020f962fccf..1760eca87a9bd 100644 --- a/packages/nx/src/command-line/migrate.ts +++ b/packages/nx/src/command-line/migrate.ts @@ -29,6 +29,7 @@ import { } from '../utils/fileutils'; import { logger } from '../utils/logger'; import { + ArrayPackageGroup, NxMigrationsConfiguration, PackageGroup, PackageJson, @@ -50,7 +51,7 @@ import { messages, recordStat } from '../utils/ab-testing'; import { nxVersion } from '../utils/versions'; export interface ResolvedMigrationConfiguration extends MigrationsJson { - packageGroup?: NxMigrationsConfiguration['packageGroup']; + packageGroup?: ArrayPackageGroup; } const execAsync = promisify(exec); @@ -302,24 +303,12 @@ export class Migrator { packageJsonUpdates: PackageJsonUpdates[string][]; packageGroupOrder: string[]; } { - const packageGroup = this.normalizePackageGroup( - packageName, - targetVersion, - migrationConfig.packageGroup - ); - - let packageGroupOrder: string[] = []; - if (packageGroup.length) { - packageGroupOrder = packageGroup.map( - (packageConfig) => packageConfig.package - ); - - setPackageGroupAsPackageJsonUpdate( - packageGroup, + const packageGroupOrder: string[] = + this.getPackageJsonUpdatesFromPackageGroup( + packageName, targetVersion, migrationConfig ); - } if ( !migrationConfig.packageJsonUpdates || @@ -337,6 +326,56 @@ export class Migrator { return { packageJsonUpdates, packageGroupOrder }; } + /** + * Mutates migrationConfig, adding package group updates into packageJsonUpdates section + * + * @param packageName Package which is being migrated + * @param targetVersion Version which is being migrated to + * @param migrationConfig Configuration which is mutated to contain package json updates + * @returns Order of package groups + */ + private getPackageJsonUpdatesFromPackageGroup( + packageName: string, + targetVersion: string, + migrationConfig: ResolvedMigrationConfiguration + ) { + const packageGroup: ArrayPackageGroup = + packageName === '@nrwl/workspace' && lt(targetVersion, '14.0.0-beta.0') + ? LEGACY_NRWL_PACKAGE_GROUP + : migrationConfig.packageGroup ?? []; + + let packageGroupOrder: string[] = []; + if (packageGroup.length) { + packageGroupOrder = packageGroup.map( + (packageConfig) => packageConfig.package + ); + + migrationConfig.packageJsonUpdates ??= {}; + const packages: Record = {}; + migrationConfig.packageJsonUpdates[targetVersion + '--PackageGroup'] = { + version: targetVersion, + packages, + }; + for (const packageConfig of packageGroup) { + packages[packageConfig.package] = { + version: + packageConfig.version === '*' + ? targetVersion + : packageConfig.version, + alwaysAddToPackageJson: false, + }; + if ( + packageConfig.version === '*' && + this.installedPkgVersionOverrides[packageName] + ) { + this.installedPkgVersionOverrides[packageConfig.package] ??= + this.installedPkgVersionOverrides[packageName]; + } + } + } + return packageGroupOrder; + } + private filterPackageJsonUpdates( packageJsonUpdates: PackageJsonUpdates, packageName: string, @@ -450,73 +489,6 @@ export class Migrator { ); } - private normalizePackageGroup( - packageName: string, - targetVersion: string, - packageGroup: PackageGroup - ): { package: string; version: string }[] { - // Support Migrating to older versions of Nx - // Use the packageGroup of the latest version of Nx instead of the one from the target version which could be older. - if ( - packageName === '@nrwl/workspace' && - lt(targetVersion, '14.0.0-beta.0') - ) { - packageGroup = { - '@nrwl/workspace': '*', - '@nrwl/angular': '*', - '@nrwl/cypress': '*', - '@nrwl/devkit': '*', - '@nrwl/eslint-plugin-nx': '*', - '@nrwl/express': '*', - '@nrwl/jest': '*', - '@nrwl/linter': '*', - '@nrwl/nest': '*', - '@nrwl/next': '*', - '@nrwl/node': '*', - '@nrwl/nx-plugin': '*', - '@nrwl/react': '*', - '@nrwl/storybook': '*', - '@nrwl/web': '*', - '@nrwl/js': '*', - '@nrwl/cli': '*', - '@nrwl/nx-cloud': 'latest', - '@nrwl/react-native': '*', - '@nrwl/detox': '*', - '@nrwl/expo': '*', - }; - } - - if (!packageGroup) { - return []; - } - - if (!Array.isArray(packageGroup)) { - return Object.entries(packageGroup).map(([pkg, version]) => { - if (this.installedPkgVersionOverrides[packageName] && version === '*') { - this.installedPkgVersionOverrides[pkg] ??= - this.installedPkgVersionOverrides[packageName]; - } - return { package: pkg, version }; - }); - } - - return packageGroup.map((packageConfig) => { - if (this.installedPkgVersionOverrides[packageName]) { - if (typeof packageConfig === 'string') { - this.installedPkgVersionOverrides[packageConfig] ??= - this.installedPkgVersionOverrides[packageName]; - } else if (packageConfig.version === '*') { - this.installedPkgVersionOverrides[packageConfig.package] ??= - this.installedPkgVersionOverrides[packageName]; - } - } - - return typeof packageConfig === 'string' - ? { package: packageConfig, version: targetVersion } - : packageConfig; - }); - } - private gt(v1: string, v2: string) { return gt(normalizeVersion(v1), normalizeVersion(v2)); } @@ -526,23 +498,29 @@ export class Migrator { } } -function setPackageGroupAsPackageJsonUpdate( - packageGroup: { package: string; version: string }[], - targetVersion: string, - migrationConfig: ResolvedMigrationConfiguration -) { - migrationConfig.packageJsonUpdates ??= {}; - migrationConfig.packageJsonUpdates[targetVersion + '--PackageGroup'] = { - version: targetVersion, - packages: packageGroup.reduce((acc, packageConfig) => { - acc[packageConfig.package] = { - version: packageConfig.version, - alwaysAddToPackageJson: false, - }; - return acc; - }, {}), - }; -} +const LEGACY_NRWL_PACKAGE_GROUP: ArrayPackageGroup = [ + { package: '@nrwl/workspace', version: '*' }, + { package: '@nrwl/angular', version: '*' }, + { package: '@nrwl/cypress', version: '*' }, + { package: '@nrwl/devkit', version: '*' }, + { package: '@nrwl/eslint-plugin-nx', version: '*' }, + { package: '@nrwl/express', version: '*' }, + { package: '@nrwl/jest', version: '*' }, + { package: '@nrwl/linter', version: '*' }, + { package: '@nrwl/nest', version: '*' }, + { package: '@nrwl/next', version: '*' }, + { package: '@nrwl/node', version: '*' }, + { package: '@nrwl/nx-plugin', version: '*' }, + { package: '@nrwl/react', version: '*' }, + { package: '@nrwl/storybook', version: '*' }, + { package: '@nrwl/web', version: '*' }, + { package: '@nrwl/js', version: '*' }, + { package: '@nrwl/cli', version: '*' }, + { package: '@nrwl/nx-cloud', version: 'latest' }, + { package: '@nrwl/react-native', version: '*' }, + { package: '@nrwl/detox', version: '*' }, + { package: '@nrwl/expo', version: '*' }, +]; function normalizeVersionWithTagCheck(version: string) { if (version === 'latest' || version === 'next') return version; @@ -810,7 +788,7 @@ async function getPackageMigrationsUsingRegistry( async function getPackageMigrationsConfigFromRegistry( packageName: string, packageVersion: string -): Promise { +) { const result = await packageRegistryView( packageName, packageVersion, @@ -827,7 +805,10 @@ async function getPackageMigrationsConfigFromRegistry( async function downloadPackageMigrationsFromRegistry( packageName: string, packageVersion: string, - { migrations: migrationsFilePath, packageGroup }: NxMigrationsConfiguration + { + migrations: migrationsFilePath, + packageGroup, + }: NxMigrationsConfiguration & { packageGroup?: ArrayPackageGroup } ): Promise { const { dir, cleanup } = createTempNpmDirectory(); @@ -894,6 +875,7 @@ async function getPackageMigrationsUsingInstall( interface PackageMigrationConfig extends NxMigrationsConfiguration { packageJson: PackageJson; + packageGroup: ArrayPackageGroup; } function readPackageMigrationConfig( @@ -905,35 +887,27 @@ function readPackageMigrationConfig( [dir] ); - const migrationConfigOrFile = json['nx-migrations'] || json['ng-update']; + const config = readNxMigrateConfig(json); - if (!migrationConfigOrFile) { + if (!config) { return { packageJson: json, migrations: null, packageGroup: [] }; } - const migrationsConfig = - typeof migrationConfigOrFile === 'string' - ? { - migrations: migrationConfigOrFile, - packageGroup: [], - } - : migrationConfigOrFile; - try { - const migrationFile = require.resolve(migrationsConfig.migrations, { + const migrationFile = require.resolve(config.migrations, { paths: [dirname(packageJsonPath)], }); return { packageJson: json, migrations: migrationFile, - packageGroup: migrationsConfig.packageGroup, + packageGroup: config.packageGroup, }; } catch { return { packageJson: json, migrations: null, - packageGroup: migrationsConfig.packageGroup, + packageGroup: config.packageGroup, }; } } diff --git a/packages/nx/src/command-line/report.spec.ts b/packages/nx/src/command-line/report.spec.ts index 31705f48cf1e1..a1d1c4e5df2f8 100644 --- a/packages/nx/src/command-line/report.spec.ts +++ b/packages/nx/src/command-line/report.spec.ts @@ -1,5 +1,11 @@ -// import * as devkit from '@nrwl/devkit'; import * as fileUtils from '../utils/fileutils'; +import * as packageJsonUtils from '../utils/package-json'; +import { + findInstalledCommunityPlugins, + findInstalledPackagesWeCareAbout, + findMisalignedPackagesForPackage, + packagesWeCareAbout, +} from './report'; jest.mock('nx/src/utils/workspace-root', () => ({ workspaceRoot: '', @@ -10,141 +16,276 @@ jest.mock('../utils/fileutils', () => ({ resolve: (file) => `node_modules/${file}`, })); -describe('reenable spec', () => { - it('empty', () => { - expect(1).toEqual(1); +describe('report', () => { + describe('findInstalledCommunityPlugins', () => { + afterEach(() => jest.resetAllMocks()); + + it('should read angular-devkit plugins', () => { + jest.spyOn(fileUtils, 'readJsonFile').mockImplementation((path) => { + console.log(path); + if (path === 'package.json') { + return { + dependencies: { + 'plugin-one': '1.0.0', + }, + devDependencies: { + 'plugin-two': '2.0.0', + }, + }; + } + }); + jest.spyOn(packageJsonUtils, 'readModulePackageJson').mockImplementation( + provideMockPackages({ + 'plugin-one': { + 'ng-update': {}, + version: '1.0.0', + }, + 'plugin-two': { + schematics: '', + version: '2.0.0', + }, + }) + ); + const plugins = findInstalledCommunityPlugins(); + expect(plugins).toEqual([ + expect.objectContaining({ name: 'plugin-one', version: '1.0.0' }), + expect.objectContaining({ name: 'plugin-two', version: '2.0.0' }), + ]); + }); + + it('should exclude misc @angluar packages', () => { + jest.spyOn(fileUtils, 'readJsonFile').mockImplementation((path) => { + if (path === 'package.json') { + return { + dependencies: { + '@angular/cdk': '1.0.0', + }, + devDependencies: { + 'plugin-two': '2.0.0', + }, + }; + } + }); + jest.spyOn(packageJsonUtils, 'readModulePackageJson').mockImplementation( + provideMockPackages({ + 'plugin-two': { + schematics: '', + version: '2.0.0', + }, + }) + ); + const plugins = findInstalledCommunityPlugins(); + expect(plugins).toEqual([ + expect.objectContaining({ package: 'plugin-two', version: '2.0.0' }), + ]); + }); + + it('should read nx devkit plugins', () => { + jest.spyOn(fileUtils, 'readJsonFile').mockImplementation((path) => { + if (path === 'package.json') { + return { + dependencies: { + 'plugin-one': '1.0.0', + }, + devDependencies: { + 'plugin-two': '2.0.0', + }, + }; + } + }); + jest.spyOn(packageJsonUtils, 'readModulePackageJson').mockImplementation( + provideMockPackages({ + 'plugin-one': { + 'nx-migrations': {}, + version: '1.0.0', + }, + 'plugin-two': { + generators: '', + version: '2.0.0', + }, + }) + ); + const plugins = findInstalledCommunityPlugins(); + expect(plugins).toEqual([ + expect.objectContaining({ package: 'plugin-one', version: '1.0.0' }), + expect.objectContaining({ package: 'plugin-two', version: '2.0.0' }), + ]); + }); + + it('should not include non-plugins', () => { + jest.spyOn(fileUtils, 'readJsonFile').mockImplementation((path) => { + if (path === 'package.json') { + return { + dependencies: { + 'plugin-one': '1.0.0', + }, + devDependencies: { + 'plugin-two': '2.0.0', + 'other-package': '1.44.0', + }, + }; + } + }); + jest.spyOn(packageJsonUtils, 'readModulePackageJson').mockImplementation( + provideMockPackages({ + 'plugin-one': { + 'nx-migrations': {}, + version: '1.0.0', + }, + 'plugin-two': { + generators: '', + version: '2.0.0', + }, + 'other-package': { + version: '1.44.0', + }, + }) + ); + const plugins = findInstalledCommunityPlugins().map((x) => x.package); + expect(plugins).not.toContain('other-package'); + }); + }); + + describe('findInstalledPackagesWeCareAbout', () => { + it('should not list packages that are not installed', () => { + const installed: [string, packageJsonUtils.PackageJson][] = + packagesWeCareAbout.map((x) => [ + x, + { + name: x, + version: '1.0.0', + }, + ]); + const uninstalled: [string, packageJsonUtils.PackageJson][] = [ + installed.pop(), + installed.pop(), + installed.pop(), + installed.pop(), + ].map((x) => [x[0], null]); + + jest + .spyOn(packageJsonUtils, 'readModulePackageJson') + .mockImplementation( + provideMockPackages(Object.fromEntries(installed.concat(uninstalled))) + ); + + const result = findInstalledPackagesWeCareAbout().map((x) => x.package); + for (const [pkg] of uninstalled) { + expect(result).not.toContain(pkg); + } + for (const [pkg] of installed) { + expect(result).toContain(pkg); + } + }); + }); + + describe('findMisalignedPackagesForPackage', () => { + it('should identify misaligned packages for array specified package groups', () => { + jest.spyOn(packageJsonUtils, 'readModulePackageJson').mockImplementation( + provideMockPackages({ + 'plugin-one': { + 'ng-update': {}, + version: '1.0.0', + }, + 'plugin-two': { + schematics: '', + version: '2.0.0', + }, + }) + ); + const results = findMisalignedPackagesForPackage({ + name: 'my-package', + version: '1.0.0', + 'nx-migrations': { + packageGroup: ['plugin-one', 'plugin-two'], + }, + }); + expect(results.misalignedPackages).toEqual([ + { + name: 'plugin-two', + version: '2.0.0', + }, + ]); + expect(results.migrateTarget).toEqual('my-package@2.0.0'); + }); + + it('should identify misaligned packages for expanded array specified package groups', () => { + jest.spyOn(packageJsonUtils, 'readModulePackageJson').mockImplementation( + provideMockPackages({ + 'plugin-one': { + 'ng-update': {}, + version: '0.5.0', + }, + 'plugin-two': { + schematics: '', + version: '2.0.0', + }, + }) + ); + const results = findMisalignedPackagesForPackage({ + name: 'my-package', + version: '1.0.0', + 'nx-migrations': { + packageGroup: [ + { package: 'plugin-one', version: '*' }, + { package: 'plugin-two', version: 'latest' }, + ], + }, + }); + expect(results.misalignedPackages).toEqual([ + { + name: 'plugin-one', + version: '0.5.0', + }, + ]); + expect(results.migrateTarget).toEqual('my-package@1.0.0'); + }); + + it('should identify misaligned packages for object specified package groups', () => { + jest.spyOn(packageJsonUtils, 'readModulePackageJson').mockImplementation( + provideMockPackages({ + 'plugin-one': { + 'ng-update': {}, + version: '0.5.0', + }, + 'plugin-two': { + schematics: '', + version: '2.0.0', + }, + }) + ); + const results = findMisalignedPackagesForPackage({ + name: 'my-package', + version: '1.0.0', + 'nx-migrations': { + packageGroup: { + 'plugin-one': '*', + 'plugin-two': 'latest', + }, + }, + }); + expect(results.misalignedPackages).toEqual([ + { + name: 'plugin-one', + version: '0.5.0', + }, + ]); + expect(results.migrateTarget).toEqual('my-package@1.0.0'); + }); }); }); -// describe('report', () => { -// describe('findInstalledCommunityPlugins', () => { -// afterEach(() => jest.resetAllMocks()); -// -// it('should read angular-devkit plugins', () => { -// jest.spyOn(devkit, 'readJsonFile').mockImplementation((path) => { -// console.log(path); -// if (path === 'package.json') { -// return { -// dependencies: { -// 'plugin-one': '1.0.0', -// }, -// devDependencies: { -// 'plugin-two': '2.0.0', -// }, -// }; -// } else if ( -// path.includes(join('node_modules', 'plugin-one', 'package.json')) -// ) { -// return { -// 'ng-update': {}, -// version: '1.0.0', -// }; -// } else if ( -// path.includes(join('node_modules', 'plugin-two', 'package.json')) -// ) { -// return { -// schematics: {}, -// version: '2.0.0', -// }; -// } -// }); -// const plugins = findInstalledCommunityPlugins(); -// expect(plugins).toEqual([ -// { package: 'plugin-one', version: '1.0.0' }, -// { package: 'plugin-two', version: '2.0.0' }, -// ]); -// }); -// -// it('should exclude misc @angluar packages', () => { -// jest.spyOn(devkit, 'readJsonFile').mockImplementation((path) => { -// if (path === 'package.json') { -// return { -// dependencies: { -// '@angular/cdk': '1.0.0', -// }, -// devDependencies: { -// 'plugin-two': '2.0.0', -// }, -// }; -// } else if ( -// path.includes(join('node_modules', 'plugin-two', 'package.json')) -// ) { -// return { -// schematics: {}, -// version: '1.0.0', -// }; -// } -// }); -// const plugins = findInstalledCommunityPlugins(); -// expect(plugins).toEqual([{ package: 'plugin-two', version: '1.0.0' }]); -// }); -// -// it('should read nx devkit plugins', () => { -// jest.spyOn(devkit, 'readJsonFile').mockImplementation((path) => { -// if (path === 'package.json') { -// return { -// dependencies: { -// 'plugin-one': '1.0.0', -// }, -// devDependencies: { -// 'plugin-two': '2.0.0', -// }, -// }; -// } else if ( -// path.includes(join('node_modules', 'plugin-one', 'package.json')) -// ) { -// return { -// 'nx-migrations': {}, -// version: '1.0.0', -// }; -// } else if ( -// path.includes(join('node_modules', 'plugin-two', 'package.json')) -// ) { -// return { -// generators: {}, -// version: '2.0.0', -// }; -// } -// }); -// const plugins = findInstalledCommunityPlugins(); -// expect(plugins).toEqual([ -// { package: 'plugin-one', version: '1.0.0' }, -// { package: 'plugin-two', version: '2.0.0' }, -// ]); -// }); -// -// it('should not include non-plugins', () => { -// jest.spyOn(devkit, 'readJsonFile').mockImplementation((path) => { -// if (path === 'package.json') { -// return { -// dependencies: { -// 'plugin-one': '1.0.0', -// }, -// devDependencies: { -// 'plugin-two': '2.0.0', -// 'other-package': '1.44.0', -// }, -// }; -// } else if ( -// path.includes(join('node_modules', 'plugin-one', 'package.json')) -// ) { -// return { -// 'nx-migrations': {}, -// }; -// } else if ( -// path.includes(join('node_modules', 'plugin-two', 'package.json')) -// ) { -// return { -// generators: {}, -// }; -// } else { -// return { -// version: '', -// }; -// } -// }); -// const plugins = findInstalledCommunityPlugins().map((x) => x.package); -// expect(plugins).not.toContain('other-package'); -// }); -// }); -// }); +function provideMockPackages( + packages: Record> +): (m: string) => ReturnType { + return (m) => { + if (m in packages) { + return { + path: `node_modules/${m}/package.json`, + packageJson: { name: m, ...packages[m] }, + }; + } else { + throw new Error(`Attempted to read unmocked package ${m}`); + } + }; +} diff --git a/packages/nx/src/command-line/report.ts b/packages/nx/src/command-line/report.ts index 659daff576e31..98844b60472f8 100644 --- a/packages/nx/src/command-line/report.ts +++ b/packages/nx/src/command-line/report.ts @@ -5,42 +5,32 @@ import { join } from 'path'; import { detectPackageManager, getPackageManagerVersion, + PackageManager, } from '../utils/package-manager'; import { readJsonFile } from '../utils/fileutils'; -import { PackageJson, readModulePackageJson } from '../utils/package-json'; +import { + PackageJson, + readModulePackageJson, + readNxMigrateConfig, +} from '../utils/package-json'; import { getLocalWorkspacePlugins } from '../utils/plugins/local-plugins'; import { createProjectGraphAsync, readProjectsConfigurationFromProjectGraph, } from '../project-graph/project-graph'; +import { gt, valid } from 'semver'; + +const nxPackageJson = readJsonFile( + join(__dirname, '../../package.json') +); export const packagesWeCareAbout = [ 'nx', - '@nrwl/angular', - '@nrwl/cypress', - '@nrwl/detox', - '@nrwl/devkit', - '@nrwl/esbuild', - '@nrwl/eslint-plugin-nx', - '@nrwl/expo', - '@nrwl/express', - '@nrwl/jest', - '@nrwl/js', - '@nrwl/linter', - '@nrwl/nest', - '@nrwl/next', - '@nrwl/node', - '@nrwl/nx-cloud', - '@nrwl/nx-plugin', - '@nrwl/react', - '@nrwl/react-native', - '@nrwl/rollup', - '@nrwl/schematics', - '@nrwl/storybook', - '@nrwl/web', - '@nrwl/webpack', - '@nrwl/workspace', - '@nrwl/vite', + 'lerna', + ...nxPackageJson['nx-migrations'].packageGroup.map((x) => + typeof x === 'string' ? x : x.package + ), + '@nrwl/schematics', // manually added since we don't publish it anymore. 'typescript', ]; @@ -51,6 +41,7 @@ export const patternsWeIgnoreInCommunityReport: Array = [ '@nestjs/schematics', ]; +const LINE_SEPARATOR = '---------------------------------------'; /** * Reports relevant version numbers for adding to an Nx issue report * @@ -60,8 +51,15 @@ export const patternsWeIgnoreInCommunityReport: Array = [ * */ export async function reportHandler() { - const pm = detectPackageManager(); - const pmVersion = getPackageManagerVersion(pm); + const { + pm, + pmVersion, + localPlugins, + communityPlugins, + packageVersionsWeCareAbout, + outOfSyncPackageGroup, + projectGraphError, + } = await getReportData(); const bodyLines = [ `Node : ${process.versions.node}`, @@ -70,33 +68,55 @@ export async function reportHandler() { ``, ]; - packagesWeCareAbout.forEach((p) => { - bodyLines.push(`${chalk.green(p)} : ${chalk.bold(readPackageVersion(p))}`); + let padding = + Math.max(...packageVersionsWeCareAbout.map((x) => x.package.length)) + 1; + packageVersionsWeCareAbout.forEach((p) => { + bodyLines.push( + `${chalk.green(p.package.padEnd(padding))} : ${chalk.bold(p.version)}` + ); }); - bodyLines.push('---------------------------------------'); + if (communityPlugins.length) { + bodyLines.push(LINE_SEPARATOR); + padding = Math.max(...communityPlugins.map((x) => x.package.length)) + 1; + bodyLines.push('Community plugins:'); + communityPlugins.forEach((p) => { + bodyLines.push( + `${chalk.green(p.package.padEnd(padding))}: ${chalk.bold(p.version)}` + ); + }); + } + + if (localPlugins.length) { + bodyLines.push(LINE_SEPARATOR); - try { - const projectGraph = await createProjectGraphAsync({ exitOnError: true }); bodyLines.push('Local workspace plugins:'); - const plugins = getLocalWorkspacePlugins( - readProjectsConfigurationFromProjectGraph(projectGraph) - ).keys(); - for (const plugin of plugins) { + + for (const plugin of localPlugins) { bodyLines.push(`\t ${chalk.green(plugin)}`); } - bodyLines.push(...plugins); - } catch { - bodyLines.push('Unable to construct project graph'); } - bodyLines.push('---------------------------------------'); + if (outOfSyncPackageGroup) { + bodyLines.push(LINE_SEPARATOR); + bodyLines.push( + `The following packages should match the installed version of ${outOfSyncPackageGroup.basePackage}` + ); + for (const pkg of outOfSyncPackageGroup.misalignedPackages) { + bodyLines.push(` - ${pkg.name}@${pkg.version}`); + } + bodyLines.push(''); + bodyLines.push( + `To fix this, run \`nx migrate ${outOfSyncPackageGroup.migrateTarget}\`` + ); + } - const communityPlugins = findInstalledCommunityPlugins(); - bodyLines.push('Community plugins:'); - communityPlugins.forEach((p) => { - bodyLines.push(`\t ${chalk.green(p.package)}: ${chalk.bold(p.version)}`); - }); + if (projectGraphError) { + bodyLines.push(LINE_SEPARATOR); + bodyLines.push('⚠️ Unable to construct project graph.'); + bodyLines.push(projectGraphError.message); + bodyLines.push(projectGraphError.stack); + } output.log({ title: 'Report complete - copy this into the issue template', @@ -104,7 +124,71 @@ export async function reportHandler() { }); } -export function readPackageJson(p: string): PackageJson | null { +export interface ReportData { + pm: PackageManager; + pmVersion: string; + localPlugins: string[]; + communityPlugins: (PackageJson & { + package: string; + })[]; + packageVersionsWeCareAbout: { + package: string; + version: string; + }[]; + outOfSyncPackageGroup?: { + basePackage: string; + misalignedPackages: { + name: string; + version: string; + }[]; + migrateTarget: string; + }; + projectGraphError?: Error | null; +} + +export async function getReportData(): Promise { + const pm = detectPackageManager(); + const pmVersion = getPackageManagerVersion(pm); + + const localPlugins = await findLocalPlugins(); + const communityPlugins = findInstalledCommunityPlugins(); + + let projectGraphError: Error | null = null; + try { + await createProjectGraphAsync(); + } catch (e) { + projectGraphError = e; + } + + const packageVersionsWeCareAbout = findInstalledPackagesWeCareAbout(); + + const outOfSyncPackageGroup = findMisalignedPackagesForPackage(nxPackageJson); + + return { + pm, + pmVersion, + localPlugins, + communityPlugins, + packageVersionsWeCareAbout, + outOfSyncPackageGroup, + projectGraphError, + }; +} + +async function findLocalPlugins() { + try { + const projectGraph = await createProjectGraphAsync({ exitOnError: true }); + return Array.from( + getLocalWorkspacePlugins( + readProjectsConfigurationFromProjectGraph(projectGraph) + ).keys() + ); + } catch { + return []; + } +} + +function readPackageJson(p: string): PackageJson | null { try { return readModulePackageJson(p).packageJson; } catch { @@ -112,14 +196,58 @@ export function readPackageJson(p: string): PackageJson | null { } } -export function readPackageVersion(p: string): string { - return readPackageJson(p)?.version || 'Not Found'; +function readPackageVersion(p: string): string | null { + return readPackageJson(p)?.version; +} + +interface OutOfSyncPackageGroup { + basePackage: string; + misalignedPackages: { + name: string; + version: string; + }[]; + migrateTarget: string; } -export function findInstalledCommunityPlugins(): { +export function findMisalignedPackagesForPackage( + base: PackageJson +): undefined | OutOfSyncPackageGroup { + const misalignedPackages: { name: string; version: string }[] = []; + + let migrateTarget = base.version; + + const { packageGroup } = readNxMigrateConfig(base); + + for (const entry of packageGroup ?? []) { + const { package: packageName, version } = entry; + // should be aligned + if (version === '*') { + const installedVersion = readPackageVersion(packageName); + + if (installedVersion && installedVersion !== base.version) { + if (valid(installedVersion) && gt(installedVersion, migrateTarget)) { + migrateTarget = installedVersion; + } + misalignedPackages.push({ + name: packageName, + version: installedVersion, + }); + } + } + } + + return misalignedPackages.length + ? { + basePackage: base.name, + misalignedPackages, + migrateTarget: `${base.name}@${migrateTarget}`, + } + : undefined; +} + +export function findInstalledCommunityPlugins(): (PackageJson & { package: string; - version: string; -}[] { +})[] { const { dependencies, devDependencies } = readJsonFile( join(workspaceRoot, 'package.json') ); @@ -152,7 +280,7 @@ export function findInstalledCommunityPlugins(): { 'executors', ].some((field) => field in depPackageJson) ) { - arr.push({ package: nextDep, version: depPackageJson.version }); + arr.push({ package: nextDep, ...depPackageJson }); return arr; } else { return arr; @@ -165,3 +293,12 @@ export function findInstalledCommunityPlugins(): { [] ); } +export function findInstalledPackagesWeCareAbout() { + return packagesWeCareAbout.reduce((acc, next) => { + const v = readPackageVersion(next); + if (v) { + acc.push({ package: next, version: v }); + } + return acc; + }, [] as { package: string; version: string }[]); +} diff --git a/packages/nx/src/utils/package-json.ts b/packages/nx/src/utils/package-json.ts index 7be7c8364cc8a..51b50fec0c34b 100644 --- a/packages/nx/src/utils/package-json.ts +++ b/packages/nx/src/utils/package-json.ts @@ -20,9 +20,11 @@ export interface NxProjectPackageJsonConfiguration { includedScripts?: string[]; } -export type PackageGroup = +export type ArrayPackageGroup = { package: string; version: string }[]; +export type MixedPackageGroup = | (string | { package: string; version: string })[] | Record; +export type PackageGroup = MixedPackageGroup | ArrayPackageGroup; export interface NxMigrationsConfiguration { migrations?: string; @@ -73,12 +75,25 @@ export interface PackageJson { 'ng-update'?: string | NxMigrationsConfiguration; } +export function normalizePackageGroup( + packageGroup: PackageGroup +): ArrayPackageGroup { + return Array.isArray(packageGroup) + ? packageGroup.map((x) => + typeof x === 'string' ? { package: x, version: '*' } : x + ) + : Object.entries(packageGroup).map(([pkg, version]) => ({ + package: pkg, + version, + })); +} + export function readNxMigrateConfig( json: Partial -): NxMigrationsConfiguration { +): NxMigrationsConfiguration & { packageGroup?: ArrayPackageGroup } { const parseNxMigrationsConfig = ( fromJson?: string | NxMigrationsConfiguration - ): NxMigrationsConfiguration => { + ): NxMigrationsConfiguration & { packageGroup?: ArrayPackageGroup } => { if (!fromJson) { return {}; } @@ -88,7 +103,9 @@ export function readNxMigrateConfig( return { ...(fromJson.migrations ? { migrations: fromJson.migrations } : {}), - ...(fromJson.packageGroup ? { packageGroup: fromJson.packageGroup } : {}), + ...(fromJson.packageGroup + ? { packageGroup: normalizePackageGroup(fromJson.packageGroup) } + : {}), }; };