diff --git a/e2e/cli/src/cli.test.ts b/e2e/cli/src/cli.test.ts index f673ecff9296f..d442d97e530c1 100644 --- a/e2e/cli/src/cli.test.ts +++ b/e2e/cli/src/cli.test.ts @@ -15,7 +15,7 @@ import { packagesWeCareAbout } from 'nx/src/command-line/report'; describe('Cli', () => { beforeEach(() => newProject()); - it('vvvshould execute long running tasks', async () => { + it('should execute long running tasks', async () => { const myapp = uniq('myapp'); runCLI(`generate @nrwl/web:app ${myapp}`); updateProjectConfig(myapp, (c) => { @@ -156,7 +156,7 @@ describe('list', () => { describe('migrate', () => { beforeEach(() => newProject()); - it('clear-cacheshould run migrations', () => { + it('should run migrations', () => { updateFile( `./node_modules/migrate-parent-package/package.json`, JSON.stringify({ diff --git a/packages/make-angular-cli-faster/src/utilities/migration.ts b/packages/make-angular-cli-faster/src/utilities/migration.ts index bc704bda6c357..723050952000a 100644 --- a/packages/make-angular-cli-faster/src/utilities/migration.ts +++ b/packages/make-angular-cli-faster/src/utilities/migration.ts @@ -32,13 +32,13 @@ export async function determineMigration( ): Promise { const angularVersion = getInstalledAngularVersion(); const majorAngularVersion = major(angularVersion); - latestWorkspaceVersionWithMigration = resolvePackageVersion( + latestWorkspaceVersionWithMigration = await resolvePackageVersion( '@nrwl/angular', latestWorkspaceRangeVersionWithMigration ); if (version) { - const normalizedVersion = normalizeVersion(version); + const normalizedVersion = await normalizeVersion(version); if (lte(normalizedVersion, latestWorkspaceVersionWithMigration)) { // specified version should use @nrwl/workspace:ng-add return { packageName: '@nrwl/workspace', version: normalizedVersion }; @@ -66,10 +66,11 @@ export async function determineMigration( ); } - const latestNxCompatibleVersion = getNxVersionBasedOnInstalledAngularVersion( - angularVersion, - majorAngularVersion - ); + const latestNxCompatibleVersion = + await getNxVersionBasedOnInstalledAngularVersion( + angularVersion, + majorAngularVersion + ); // should use @nrwl/workspace:ng-add if the version is less than the // latest workspace version that has the migration, otherwise use @@ -105,10 +106,11 @@ async function findAndSuggestVersionToUse( majorAngularVersion: number, userSpecifiedVersion: string ): Promise { - const latestNxCompatibleVersion = getNxVersionBasedOnInstalledAngularVersion( - angularVersion, - majorAngularVersion - ); + const latestNxCompatibleVersion = + await getNxVersionBasedOnInstalledAngularVersion( + angularVersion, + majorAngularVersion + ); const useSuggestedVersion = await promptForVersion(latestNxCompatibleVersion); if (useSuggestedVersion) { // should use @nrwl/workspace:ng-add if the version is less than the @@ -134,10 +136,10 @@ async function findAndSuggestVersionToUse( process.exit(1); } -function getNxVersionBasedOnInstalledAngularVersion( +async function getNxVersionBasedOnInstalledAngularVersion( angularVersion: string, majorAngularVersion: number -): string { +): Promise { if (lt(angularVersion, '13.0.0')) { // the @nrwl/angular:ng-add generator is only available for versions supporting // Angular >= 13.0.0, fall back to @nrwl/workspace:ng-add @@ -154,7 +156,7 @@ function getNxVersionBasedOnInstalledAngularVersion( } // use latest, only the last version in the map should not contain a max - return resolvePackageVersion('@nrwl/angular', 'latest'); + return await resolvePackageVersion('@nrwl/angular', 'latest'); } async function promptForVersion(version: string): Promise { @@ -177,13 +179,13 @@ function getInstalledAngularVersion(): string { return readJsonFile(packageJsonPath).version; } -function normalizeVersion(version: string): string { +async function normalizeVersion(version: string): Promise { if ( version.startsWith('^') || version.startsWith('~') || version.split('.').length < 3 ) { - return resolvePackageVersion('@nrwl/angular', version); + return await resolvePackageVersion('@nrwl/angular', version); } return version; diff --git a/packages/make-angular-cli-faster/src/utilities/package-manager.ts b/packages/make-angular-cli-faster/src/utilities/package-manager.ts index f178d154fb29f..057688cf2efea 100644 --- a/packages/make-angular-cli-faster/src/utilities/package-manager.ts +++ b/packages/make-angular-cli-faster/src/utilities/package-manager.ts @@ -55,13 +55,13 @@ export function installDependencies( }); } -export function resolvePackageVersion( +export async function resolvePackageVersion( packageName: string, version: string -): string { +): Promise { try { - return resolvePackageVersionUsingRegistry(packageName, version); + return await resolvePackageVersionUsingRegistry(packageName, version); } catch { - return resolvePackageVersionUsingInstallation(packageName, version); + return await resolvePackageVersionUsingInstallation(packageName, version); } } diff --git a/packages/nx/src/command-line/migrate.spec.ts b/packages/nx/src/command-line/migrate.spec.ts index a1affb34173c9..47669050dbc15 100644 --- a/packages/nx/src/command-line/migrate.spec.ts +++ b/packages/nx/src/command-line/migrate.spec.ts @@ -1,32 +1,36 @@ +import { PackageJson } from '../utils/package-json'; import { Migrator, normalizeVersion, parseMigrationsOptions } from './migrate'; +const createPackageJson = ( + overrides: Partial = {} +): PackageJson => ({ + name: 'some-workspace', + version: '0.0.0', + ...overrides, +}); + describe('Migration', () => { describe('packageJson patch', () => { it('should throw an error when the target package is not available', async () => { const migrator = new Migrator({ - packageJson: {}, + packageJson: createPackageJson(), versions: () => '1.0', fetch: (_p, _v) => { throw new Error('cannot fetch'); }, - from: {}, to: {}, }); - try { - await migrator.updatePackageJson('mypackage', 'myversion'); - throw new Error('fail'); - } catch (e) { - expect(e.message).toEqual(`cannot fetch`); - } + await expect( + migrator.updatePackageJson('mypackage', 'myversion') + ).rejects.toThrowError(/cannot fetch/); }); it('should return a patch to the new version', async () => { const migrator = new Migrator({ - packageJson: {}, + packageJson: createPackageJson(), versions: () => '1.0.0', fetch: (_p, _v) => Promise.resolve({ version: '2.0.0' }), - from: {}, to: {}, }); @@ -40,7 +44,7 @@ describe('Migration', () => { it('should collect the information recursively from upserts', async () => { const migrator = new Migrator({ - packageJson: { dependencies: { child: '1.0.0' } }, + packageJson: createPackageJson({ dependencies: { child: '1.0.0' } }), versions: () => '1.0.0', fetch: (p, _v) => { if (p === 'parent') { @@ -68,7 +72,6 @@ describe('Migration', () => { return Promise.resolve(null); } }, - from: {}, to: {}, }); @@ -84,7 +87,7 @@ describe('Migration', () => { it('should support the deprecated "alwaysAddToPackageJson" option', async () => { const migrator = new Migrator({ - packageJson: { dependencies: { child1: '1.0.0' } }, + packageJson: createPackageJson({ dependencies: { child1: '1.0.0' } }), versions: () => '1.0.0', fetch: (p, _v) => { if (p === 'mypackage') { @@ -108,7 +111,6 @@ describe('Migration', () => { return Promise.resolve(null); } }, - from: {}, to: {}, }); @@ -124,7 +126,7 @@ describe('Migration', () => { it('should stop recursive calls when exact version', async () => { const migrator = new Migrator({ - packageJson: { dependencies: { child: '1.0.0' } }, + packageJson: createPackageJson({ dependencies: { child: '1.0.0' } }), versions: () => '1.0.0', fetch: (p, _v) => { if (p === 'parent') { @@ -157,7 +159,6 @@ describe('Migration', () => { return Promise.resolve(null); } }, - from: {}, to: {}, }); @@ -172,13 +173,13 @@ describe('Migration', () => { it('should set the version of a dependency to the newest', async () => { const migrator = new Migrator({ - packageJson: { + packageJson: createPackageJson({ dependencies: { child1: '1.0.0', child2: '1.0.0', grandchild: '1.0.0', }, - }, + }), versions: () => '1.0.0', fetch: (p, _v) => { if (p === 'parent') { @@ -225,7 +226,6 @@ describe('Migration', () => { return Promise.resolve({ version: '4.0.0' }); } }, - from: {}, to: {}, }); @@ -242,7 +242,9 @@ describe('Migration', () => { it('should skip the versions <= currently installed', async () => { const migrator = new Migrator({ - packageJson: { dependencies: { child: '1.0.0', grandchild: '2.0.0' } }, + packageJson: createPackageJson({ + dependencies: { child: '1.0.0', grandchild: '2.0.0' }, + }), versions: () => '1.0.0', fetch: (p, _v) => { if (p === 'parent') { @@ -275,7 +277,6 @@ describe('Migration', () => { return Promise.resolve({ version: '2.0.0' }); } }, - from: {}, to: {}, }); @@ -290,7 +291,9 @@ describe('Migration', () => { it('should conditionally process packages if they are installed', async () => { const migrator = new Migrator({ - packageJson: { dependencies: { child1: '1.0.0', child2: '1.0.0' } }, + packageJson: createPackageJson({ + dependencies: { child1: '1.0.0', child2: '1.0.0' }, + }), versions: (p) => (p !== 'not-installed' ? '1.0.0' : null), fetch: (p, _v) => { if (p === 'parent') { @@ -318,7 +321,6 @@ describe('Migration', () => { return Promise.resolve(null); } }, - from: {}, to: {}, }); @@ -331,78 +333,155 @@ describe('Migration', () => { }); }); - // this is temporary. if nx gets used by other projects, - // we will extract the special casing - it('should special case @nrwl/workspace', async () => { + it('should migrate related libraries using packageGroup', async () => { const migrator = new Migrator({ packageJson: { + name: 'some-workspace', + version: '0.0.0', + devDependencies: { - '@nrwl/workspace': '0.9.0', - '@nrwl/cli': '0.9.0', - '@nrwl/angular': '0.9.0', - '@nrwl/cypress': '0.9.0', - '@nrwl/devkit': '0.9.0', - '@nrwl/eslint-plugin-nx': '0.9.0', - '@nrwl/express': '0.9.0', - '@nrwl/jest': '0.9.0', - '@nrwl/js': '0.9.0', - '@nrwl/linter': '0.9.0', - '@nrwl/nest': '0.9.0', - '@nrwl/next': '0.9.0', - '@nrwl/node': '0.9.0', - '@nrwl/nx-cloud': '0.9.0', - '@nrwl/nx-plugin': '0.9.0', - '@nrwl/react': '0.9.0', - '@nrwl/storybook': '0.9.0', - '@nrwl/web': '0.9.0', + '@my-company/nx-workspace': '0.9.0', + '@my-company/lib-1': '0.9.0', + '@my-company/lib-2': '0.9.0', + '@my-company/lib-3': '0.9.0', + '@my-company/lib-3-child': '0.9.0', + '@my-company/lib-4': '0.9.0', + '@my-company/lib-5': '0.9.0', + '@my-company/lib-6': '0.9.0', }, }, versions: () => '1.0.0', - fetch: (_p, _v) => Promise.resolve({ version: '2.0.0' }), - from: {}, + fetch: async (pkg, version) => { + if (pkg === '@my-company/nx-workspace') { + return { + version: '2.0.0', + packageGroup: [ + '@my-company/lib-1', + '@my-company/lib-2', + '@my-company/lib-3', + { package: '@my-company/lib-4', version: 'latest' }, + ], + }; + } + if (pkg === '@my-company/lib-6') { + return { + version: '2.0.0', + packageGroup: ['@my-company/nx-workspace'], + }; + } + if (pkg === '@my-company/lib-3') { + return { + version: '2.0.0', + packageGroup: ['@my-company/lib-3-child'], + }; + } + if (version === 'latest') { + return { version: '2.0.1' }; + } + return { version: '2.0.0' }; + }, to: {}, }); expect( - await migrator.updatePackageJson('@nrwl/workspace', '2.0.0') - ).toEqual({ + await migrator.updatePackageJson('@my-company/nx-workspace', '2.0.0') + ).toStrictEqual({ migrations: [], packageJson: { - '@nrwl/workspace': { version: '2.0.0', addToPackageJson: false }, - '@nrwl/angular': { version: '2.0.0', addToPackageJson: false }, - '@nrwl/cypress': { version: '2.0.0', addToPackageJson: false }, - '@nrwl/devkit': { addToPackageJson: false, version: '2.0.0' }, - '@nrwl/eslint-plugin-nx': { + '@my-company/nx-workspace': { + version: '2.0.0', + addToPackageJson: false, + }, + '@my-company/lib-1': { version: '2.0.0', addToPackageJson: false }, + '@my-company/lib-2': { version: '2.0.0', addToPackageJson: false }, + '@my-company/lib-3': { version: '2.0.0', addToPackageJson: false }, + '@my-company/lib-3-child': { version: '2.0.0', addToPackageJson: false, }, - '@nrwl/express': { version: '2.0.0', addToPackageJson: false }, - '@nrwl/jest': { version: '2.0.0', addToPackageJson: false }, - '@nrwl/js': { version: '2.0.0', addToPackageJson: false }, - '@nrwl/linter': { version: '2.0.0', addToPackageJson: false }, - '@nrwl/nest': { version: '2.0.0', addToPackageJson: false }, - '@nrwl/next': { version: '2.0.0', addToPackageJson: false }, - '@nrwl/node': { version: '2.0.0', addToPackageJson: false }, - '@nrwl/nx-cloud': { version: '2.0.0', addToPackageJson: false }, - '@nrwl/nx-plugin': { version: '2.0.0', addToPackageJson: false }, - '@nrwl/react': { version: '2.0.0', addToPackageJson: false }, - '@nrwl/storybook': { version: '2.0.0', addToPackageJson: false }, - '@nrwl/web': { version: '2.0.0', addToPackageJson: false }, - '@nrwl/cli': { version: '2.0.0', addToPackageJson: false }, + '@my-company/lib-4': { version: '2.0.1', addToPackageJson: false }, + }, + }); + }); + + it('should properly handle cyclic dependency in nested packageGroup', async () => { + const migrator = new Migrator({ + packageJson: { + name: 'some-workspace', + version: '0.0.0', + + devDependencies: { + '@my-company/nx-workspace': '0.9.0', + '@my-company/lib-1': '0.9.0', + '@my-company/lib-2': '0.9.0', + }, + }, + versions: () => '1.0.0', + fetch: async (pkg, version) => { + if (pkg === '@my-company/nx-workspace' && version === '2.0.0') { + return { + version: '2.0.0', + packageGroup: [ + { package: '@my-company/lib-1', version: 'latest' }, + ], + }; + } + if (pkg === '@my-company/nx-workspace' && version === '3.0.0') { + return { + version: '3.0.0', + packageGroup: ['@my-company/lib-1', '@my-company/lib-2'], + }; + } + if (pkg === '@my-company/lib-1' && version === 'latest') { + return { + version: '3.0.0', + packageGroup: ['@my-company/nx-workspace'], + }; + } + if (pkg === '@my-company/lib-1' && version === '3.0.0') { + return { + version: '3.0.0', + packageGroup: ['@my-company/nx-workspace'], + }; + } + if (pkg === '@my-company/lib-2' && version === '3.0.0') { + return { + version: '3.0.0', + packageGroup: [ + // this should be ignored because it's a smaller version + { package: '@my-company/nx-workspace', version: '2.99.0' }, + ], + }; + } + throw new Error(`Should not call fetch for ${pkg}@${version}`); + }, + to: {}, + }); + + expect( + await migrator.updatePackageJson('@my-company/nx-workspace', '2.0.0') + ).toStrictEqual({ + migrations: [], + packageJson: { + '@my-company/nx-workspace': { + version: '3.0.0', + addToPackageJson: false, + }, + '@my-company/lib-1': { version: '3.0.0', addToPackageJson: false }, + '@my-company/lib-2': { version: '3.0.0', addToPackageJson: false }, }, }); }); it('should not throw when packages are missing', async () => { const migrator = new Migrator({ - packageJson: {}, + packageJson: createPackageJson(), versions: (p) => (p === '@nrwl/nest' ? null : '1.0.0'), fetch: (_p, _v) => Promise.resolve({ version: '2.0.0', packageJsonUpdates: { one: { version: '2.0.0', packages: {} } }, }), - from: {}, to: {}, }); await migrator.updatePackageJson('@nrwl/workspace', '2.0.0'); @@ -410,7 +489,7 @@ describe('Migration', () => { it('should only fetch packages that are installed', async () => { const migrator = new Migrator({ - packageJson: {}, + packageJson: createPackageJson(), versions: (p) => (p === '@nrwl/nest' ? null : '1.0.0'), fetch: (p, _v) => { if (p === '@nrwl/nest') { @@ -421,7 +500,6 @@ describe('Migration', () => { packageJsonUpdates: { one: { version: '2.0.0', packages: {} } }, }); }, - from: {}, to: {}, }); await migrator.updatePackageJson('@nrwl/workspace', '2.0.0'); @@ -429,7 +507,9 @@ describe('Migration', () => { it('should only fetch packages that are top-level deps', async () => { const migrator = new Migrator({ - packageJson: { devDependencies: { parent: '1.0.0', child1: '1.0.0' } }, + packageJson: createPackageJson({ + devDependencies: { parent: '1.0.0', child1: '1.0.0' }, + }), versions: () => '1.0.0', fetch: (p, _v) => { if (p === 'parent') { @@ -455,7 +535,6 @@ describe('Migration', () => { throw new Error('Boom'); } }, - from: {}, to: {}, }); @@ -466,7 +545,9 @@ describe('Migration', () => { describe('migrations', () => { it('should create a list of migrations to run', async () => { const migrator = new Migrator({ - packageJson: { dependencies: { child: '1.0.0', newChild: '1.0.0' } }, + packageJson: createPackageJson({ + dependencies: { child: '1.0.0', newChild: '1.0.0' }, + }), versions: (p) => { if (p === 'parent') return '1.0.0'; if (p === 'child') return '1.0.0'; @@ -518,7 +599,6 @@ describe('Migration', () => { return Promise.resolve(null); } }, - from: {}, to: {}, }); expect(await migrator.updatePackageJson('parent', '2.0.0')).toEqual({ @@ -546,7 +626,7 @@ describe('Migration', () => { it('should not generate migrations for non top-level packages', async () => { const migrator = new Migrator({ - packageJson: { dependencies: { child: '1.0.0' } }, + packageJson: createPackageJson({ dependencies: { child: '1.0.0' } }), versions: (p) => { if (p === 'parent') return '1.0.0'; if (p === 'child') return '1.0.0'; @@ -599,7 +679,6 @@ describe('Migration', () => { return Promise.resolve(null); } }, - from: {}, to: {}, }); diff --git a/packages/nx/src/command-line/migrate.ts b/packages/nx/src/command-line/migrate.ts index 04fc5329f5e6a..980b5d1a8db68 100644 --- a/packages/nx/src/command-line/migrate.ts +++ b/packages/nx/src/command-line/migrate.ts @@ -1,8 +1,8 @@ -import { execSync } from 'child_process'; -import { copyFileSync, removeSync } from 'fs-extra'; +import { exec, execSync } from 'child_process'; +import { remove } from 'fs-extra'; import { dirname, join } from 'path'; import { gt, lte } from 'semver'; -import { dirSync } from 'tmp'; +import { promisify } from 'util'; import { NxJsonConfiguration } from '../config/nx-json'; import { flushChanges, FsTree } from '../config/tree'; import { @@ -12,92 +12,111 @@ import { writeJsonFile, } from '../utils/fileutils'; import { logger } from '../utils/logger'; +import { NxMigrationsConfiguration, PackageJson } from '../utils/package-json'; import { - checkForNPMRC, - detectPackageManager, + createTempNpmDirectory, getPackageManagerCommand, + packageRegistryPack, + packageRegistryView, resolvePackageVersionUsingRegistry, } from '../utils/package-manager'; import { handleErrors } from '../utils/params'; -type Dependencies = 'dependencies' | 'devDependencies'; +export type Dependencies = 'dependencies' | 'devDependencies'; -export type MigrationsJson = { +export interface PackageJsonUpdateForPackage { version: string; - collection?: string; - generators?: { - [name: string]: { version: string; description?: string; cli?: string }; - }; - packageJsonUpdates?: { - [name: string]: { - version: string; - packages: { - [p: string]: { - version: string; - ifPackageInstalled?: string; - alwaysAddToPackageJson?: boolean; - addToPackageJson?: Dependencies; - }; - }; + ifPackageInstalled?: string; + alwaysAddToPackageJson?: boolean | Dependencies; + addToPackageJson?: boolean | Dependencies; +} + +export type PackageJsonUpdates = { + [name: string]: { + version: string; + packages: { + [packageName: string]: PackageJsonUpdateForPackage; }; }; }; +export interface GeneratorMigration { + version: string; + description?: string; + cli?: string; + implementation?: string; + factory?: string; +} + +export interface MigrationsJson { + version: string; + collection?: string; + generators?: { [name: string]: GeneratorMigration }; + schematics?: { [name: string]: GeneratorMigration }; + packageJsonUpdates?: PackageJsonUpdates; +} + +export interface ResolvedMigrationConfiguration extends MigrationsJson { + packageGroup?: NxMigrationsConfiguration['packageGroup']; +} + +const execAsync = promisify(exec); + export function normalizeVersion(version: string) { - const [v, t] = version.split('-'); - const [major, minor, patch] = v.split('.'); - const newV = `${major || 0}.${minor || 0}.${patch || 0}`; - const newVersion = t ? `${newV}-${t}` : newV; + const [semver, prereleaseTag] = version.split('-'); + const [major, minor, patch] = semver.split('.'); - try { - gt(newVersion, '0.0.0'); - return newVersion; - } catch (e) { + const newSemver = `${major || 0}.${minor || 0}.${patch || 0}`; + + const newVersion = prereleaseTag + ? `${newSemver}-${prereleaseTag}` + : newSemver; + + const withoutPatch = `${major || 0}.${minor || 0}.0`; + const withoutPatchAndMinor = `${major || 0}.0.0`; + + const variationsToCheck = [ + newVersion, + newSemver, + withoutPatch, + withoutPatchAndMinor, + ]; + + for (const variation of variationsToCheck) { try { - gt(newV, '0.0.0'); - return newV; - } catch (e) { - const withoutPatch = `${major || 0}.${minor || 0}.0`; - try { - if (gt(withoutPatch, '0.0.0')) { - return withoutPatch; - } - } catch (e) { - const withoutPatchAndMinor = `${major || 0}.0.0`; - try { - if (gt(withoutPatchAndMinor, '0.0.0')) { - return withoutPatchAndMinor; - } - } catch (e) { - return '0.0.0'; - } + if (gt(variation, '0.0.0')) { + return variation; } - } + } catch {} } + + return '0.0.0'; } -function slash(packageName) { +function normalizeSlashes(packageName: string): string { return packageName.replace(/\\/g, '/'); } +export interface MigratorOptions { + packageJson: PackageJson; + versions: (pkg: string) => string; + fetch: ( + pkg: string, + version: string + ) => Promise; + to: { [pkg: string]: string }; +} + export class Migrator { - private readonly packageJson: any; - private readonly versions: (p: string) => string; - private readonly fetch: (p: string, v: string) => Promise; - private readonly from: { [p: string]: string }; - private readonly to: { [p: string]: string }; - - constructor(opts: { - packageJson: any; - versions: (p: string) => string; - fetch: (p: string, v: string) => Promise; - from: { [p: string]: string }; - to: { [p: string]: string }; - }) { + private readonly packageJson: MigratorOptions['packageJson']; + private readonly versions: MigratorOptions['versions']; + private readonly fetch: MigratorOptions['fetch']; + private readonly to: MigratorOptions['to']; + + constructor(opts: MigratorOptions) { this.packageJson = opts.packageJson; this.versions = opts.versions; this.fetch = opts.fetch; - this.from = opts.from; this.to = opts.to; } @@ -107,47 +126,47 @@ export class Migrator { { version: targetVersion, addToPackageJson: false }, {} ); + const migrations = await this._createMigrateJson(packageJson); return { packageJson, migrations }; } - private async _createMigrateJson(versions: { - [k: string]: { version: string; addToPackageJson: Dependencies | false }; - }) { + private async _createMigrateJson( + versions: Record + ) { const migrations = await Promise.all( - Object.keys(versions).map(async (c) => { - const currentVersion = this.versions(c); + Object.keys(versions).map(async (packageName) => { + const currentVersion = this.versions(packageName); if (currentVersion === null) return []; - const target = versions[c]; - const migrationsJson = await this.fetch(c, target.version); - const generators = migrationsJson.generators; + const { version } = versions[packageName]; + const { generators } = await this.fetch(packageName, version); + if (!generators) return []; - return Object.keys(generators) + + return Object.entries(generators) .filter( - (r) => - generators[r].version && - this.gt(generators[r].version, currentVersion) && - this.lte(generators[r].version, target.version) + ([, migration]) => + migration.version && + this.gt(migration.version, currentVersion) && + this.lte(migration.version, version) ) - .map((r) => ({ - ...migrationsJson.generators[r], - package: c, - name: r, + .map(([migrationName, migration]) => ({ + ...migration, + package: packageName, + name: migrationName, })); }) ); - return migrations.reduce((m, c) => [...m, ...c], []); + return migrations.flat(); } private async _updatePackageJson( targetPackage: string, - target: { version: string; addToPackageJson: Dependencies | false }, - collectedVersions: { - [k: string]: { version: string; addToPackageJson: Dependencies | false }; - } - ) { + target: PackageJsonUpdateForPackage, + collectedVersions: Record + ): Promise> { let targetVersion = target.version; if (this.to[targetPackage]) { targetVersion = this.to[targetPackage]; @@ -158,16 +177,16 @@ export class Migrator { [targetPackage]: { version: target.version, addToPackageJson: target.addToPackageJson || false, - }, + } as PackageJsonUpdateForPackage, }; } - let migrationsJson; + let migrationsJson: ResolvedMigrationConfiguration; try { migrationsJson = await this.fetch(targetPackage, targetVersion); targetVersion = migrationsJson.version; } catch (e) { - if (e.message.indexOf('No matching version') > -1) { + if (e?.message?.includes('No matching version')) { throw new Error( `${e.message}\nRun migrate with --to="package1@version1,package2@version2"` ); @@ -175,129 +194,120 @@ export class Migrator { throw e; } } + const packages = this.collapsePackages( targetPackage, targetVersion, migrationsJson ); - const childCalls = await Promise.all( + const childPackageMigrations = await Promise.all( Object.keys(packages) - .filter((r) => { + .filter((packageName) => { return ( - !collectedVersions[r] || - this.gt(packages[r].version, collectedVersions[r].version) + !collectedVersions[packageName] || + this.gt( + packages[packageName].version, + collectedVersions[packageName].version + ) ); }) - .map((u) => - this._updatePackageJson(u, packages[u], { + .map((packageName) => + this._updatePackageJson(packageName, packages[packageName], { ...collectedVersions, [targetPackage]: target, }) ) ); - return childCalls.reduce( - (m, c) => { - Object.keys(c).forEach((r) => { - if (!m[r] || this.gt(c[r].version, m[r].version)) { - m[r] = c[r]; + + return childPackageMigrations.reduce( + (migrations, childMigrations) => { + for (const migrationName of Object.keys(childMigrations)) { + if ( + !migrations[migrationName] || + this.gt( + childMigrations[migrationName].version, + migrations[migrationName].version + ) + ) { + migrations[migrationName] = childMigrations[migrationName]; } - }); - return m; + } + return migrations; }, { [targetPackage]: { version: migrationsJson.version, addToPackageJson: target.addToPackageJson || false, }, - } + } as Record ); } private collapsePackages( packageName: string, targetVersion: string, - m: MigrationsJson | null - ) { + migration: ResolvedMigrationConfiguration + ): Record { // this should be used to know what version to include // we should use from everywhere we use versions - if (packageName === '@nrwl/workspace') { - if (!m.packageJsonUpdates) m.packageJsonUpdates = {}; - m.packageJsonUpdates[`${targetVersion}-defaultPackages`] = { + if (migration.packageGroup) { + migration.packageJsonUpdates ??= {}; + migration.packageJsonUpdates[`${targetVersion}-defaultPackages`] = { version: targetVersion, - packages: [ - 'nx', - '@nrwl/angular', - '@nrwl/cypress', - '@nrwl/devkit', - '@nrwl/eslint-plugin-nx', - '@nrwl/express', - '@nrwl/jest', - '@nrwl/js', - '@nrwl/cli', - '@nrwl/linter', - '@nrwl/nest', - '@nrwl/next', - '@nrwl/node', - '@nrwl/nx-cloud', - '@nrwl/nx-plugin', - '@nrwl/react', - '@nrwl/storybook', - '@nrwl/web', - '@nrwl/react-native', - '@nrwl/detox', - ].reduce( - (m, c) => ({ - ...m, - [c]: { - version: c === '@nrwl/nx-cloud' ? 'latest' : targetVersion, + packages: migration.packageGroup.reduce((acc, packageConfig) => { + const { package: pkg, version } = + typeof packageConfig === 'string' + ? { package: packageConfig, version: targetVersion } + : packageConfig; + + return { + ...acc, + [pkg]: { + version, alwaysAddToPackageJson: false, - }, - }), - {} - ), + } as PackageJsonUpdateForPackage, + }; + }, {}), }; } - if (!m.packageJsonUpdates || !this.versions(packageName)) return {}; - return Object.keys(m.packageJsonUpdates) - .filter((r) => { + if (!migration.packageJsonUpdates || !this.versions(packageName)) return {}; + + return Object.values(migration.packageJsonUpdates) + .filter(({ version, packages }) => { return ( - this.gt( - m.packageJsonUpdates[r].version, - this.versions(packageName) - ) && this.lte(m.packageJsonUpdates[r].version, targetVersion) + packages && + this.gt(version, this.versions(packageName)) && + this.lte(version, targetVersion) ); }) - .map((r) => m.packageJsonUpdates[r].packages) - .map((packages) => { - if (!packages) return {}; - - return Object.keys(packages) - .filter((pkg) => { - const { dependencies, devDependencies } = this.packageJson; + .map(({ packages }) => { + const { dependencies, devDependencies } = this.packageJson; + return Object.entries(packages) + .filter(([packageName, packageUpdate]) => { return ( - (!packages[pkg].ifPackageInstalled || - this.versions(packages[pkg].ifPackageInstalled)) && - (packages[pkg].alwaysAddToPackageJson || - packages[pkg].addToPackageJson || - !!dependencies?.[pkg] || - !!devDependencies?.[pkg]) + (!packageUpdate.ifPackageInstalled || + this.versions(packageUpdate.ifPackageInstalled)) && + (packageUpdate.alwaysAddToPackageJson || + packageUpdate.addToPackageJson || + !!dependencies?.[packageName] || + !!devDependencies?.[packageName]) ); }) .reduce( - (m, c) => ({ - ...m, - [c]: { - version: packages[c].version, - addToPackageJson: packages[c].alwaysAddToPackageJson + (acc, [packageName, packageUpdate]) => ({ + ...acc, + [packageName]: { + version: packageUpdate.version, + addToPackageJson: packageUpdate.alwaysAddToPackageJson ? 'dependencies' - : packages[c].addToPackageJson || false, + : packageUpdate.addToPackageJson || false, }, }), - {} + {} as Record ); }) .reduce((m, c) => ({ ...m, ...c }), {}); @@ -333,7 +343,8 @@ function versionOverrides(overrides: string, param: string) { `Incorrect '${param}' section. Use --${param}="package@version"` ); } - res[slash(selectedPackage)] = normalizeVersionWithTagCheck(selectedVersion); + res[normalizeSlashes(selectedPackage)] = + normalizeVersionWithTagCheck(selectedVersion); }); return res; } @@ -388,6 +399,7 @@ type GenerateMigrations = { from: { [k: string]: string }; to: { [k: string]: string }; }; + type RunMigrations = { type: 'runMigrations'; runMigrations: string }; export function parseMigrationsOptions(options: { @@ -407,7 +419,7 @@ export function parseMigrationsOptions(options: { ); return { type: 'generateMigrations', - targetPackage: slash(targetPackage), + targetPackage: normalizeSlashes(targetPackage), targetVersion, from, to, @@ -420,16 +432,23 @@ export function parseMigrationsOptions(options: { } } -function versions(root: string, from: { [p: string]: string }) { +function versions(root: string, from: Record) { + const cache: Record = {}; + return (packageName: string) => { try { if (from[packageName]) { return from[packageName]; } - const packageJsonPath = require.resolve(`${packageName}/package.json`, { - paths: [root], - }); - return readJsonFile(packageJsonPath).version; + + if (!cache[packageName]) { + const packageJsonPath = require.resolve(`${packageName}/package.json`, { + paths: [root], + }); + cache[packageName] = readJsonFile(packageJsonPath).version; + } + + return cache[packageName]; } catch { return null; } @@ -438,20 +457,21 @@ function versions(root: string, from: { [p: string]: string }) { // testing-fetch-start function createFetcher() { - const cache = {}; - return async function f( + const cache: Record = {}; + + return async function nxMigrateFetcher( packageName: string, packageVersion: string - ): Promise { + ): Promise { if (cache[`${packageName}-${packageVersion}`]) { return cache[`${packageName}-${packageVersion}`]; } - let resolvedVersion: string; - let migrations: any; + let resolvedVersion: string = packageVersion; + let resolvedMigrationConfiguration: ResolvedMigrationConfiguration; try { - resolvedVersion = resolvePackageVersionUsingRegistry( + resolvedVersion = await resolvePackageVersionUsingRegistry( packageName, packageVersion ); @@ -461,210 +481,224 @@ function createFetcher() { } logger.info(`Fetching ${packageName}@${packageVersion}`); - migrations = await getPackageMigrations(packageName, resolvedVersion); + + resolvedMigrationConfiguration = await getPackageMigrationsUsingRegistry( + packageName, + resolvedVersion + ); } catch { logger.info(`Fetching ${packageName}@${packageVersion}`); - const result = await installPackageAndGetVersionAngMigrations( + + resolvedMigrationConfiguration = await getPackageMigrationsUsingInstall( packageName, packageVersion ); - resolvedVersion = result.resolvedVersion; - migrations = result.migrations; - } - if (migrations) { - cache[`${packageName}-${packageVersion}`] = cache[ - `${packageName}-${resolvedVersion}` - ] = { - version: resolvedVersion, - generators: migrations.generators ?? migrations.schematics, - packageJsonUpdates: migrations.packageJsonUpdates, - }; - } else { - cache[`${packageName}-${packageVersion}`] = cache[ - `${packageName}-${resolvedVersion}` - ] = { - version: resolvedVersion, - }; + resolvedVersion = resolvedMigrationConfiguration.version; } - return cache[`${packageName}-${packageVersion}`]; + resolvedMigrationConfiguration = { + ...resolvedMigrationConfiguration, + generators: + resolvedMigrationConfiguration.generators ?? + resolvedMigrationConfiguration.schematics, + }; + + cache[`${packageName}-${packageVersion}`] = cache[ + `${packageName}-${resolvedVersion}` + ] = resolvedMigrationConfiguration; + + return resolvedMigrationConfiguration; }; } - // testing-fetch-end -async function getPackageMigrations( +async function getPackageMigrationsUsingRegistry( packageName: string, packageVersion: string -) { - try { - // check if there are migrations in the packages by looking at the - // registry directly - const migrationsPath = getPackageMigrationsPathFromRegistry( - packageName, - packageVersion - ); - if (!migrationsPath) { - return null; - } +): Promise { + // check if there are migrations in the packages by looking at the + // registry directly + const migrationsConfig = await getPackageMigrationsConfigFromRegistry( + packageName, + packageVersion + ); - // try to obtain the migrations from the registry directly - return await getPackageMigrationsUsingRegistry( - packageName, - packageVersion, - migrationsPath - ); - } catch { - // fall back to installing the package - const { migrations } = await installPackageAndGetVersionAngMigrations( - packageName, - packageVersion - ); - return migrations; + if (!migrationsConfig.migrations) { + return { + version: packageVersion, + packageGroup: migrationsConfig.packageGroup, + }; } + + // try to obtain the migrations from the registry directly + return await downloadPackageMigrationsFromRegistry( + packageName, + packageVersion, + migrationsConfig + ); } -function getPackageMigrationsPathFromRegistry( +function resolveNxMigrationConfig(json: Partial) { + const parseNxMigrationsConfig = ( + fromJson: string | NxMigrationsConfiguration + ): NxMigrationsConfiguration => { + if (typeof fromJson === 'string') { + return { migrations: fromJson, packageGroup: [] }; + } + + return { + ...(fromJson.migrations ? { migrations: fromJson.migrations } : {}), + ...(fromJson.packageGroup ? { packageGroup: fromJson.packageGroup } : {}), + }; + }; + + const config: NxMigrationsConfiguration = { + ...parseNxMigrationsConfig(json['ng-update']), + ...parseNxMigrationsConfig(json['nx-migrations']), + // In case there's a `migrations` field in `package.json` + ...parseNxMigrationsConfig(json as any), + }; + + return config; +} + +async function getPackageMigrationsConfigFromRegistry( packageName: string, packageVersion: string -): string | null { - let pm = detectPackageManager(); - if (pm === 'yarn') { - pm = 'npm'; - } - const result = execSync( - `${pm} view ${packageName}@${packageVersion} nx-migrations ng-update --json`, - { - stdio: [], - } - ) - .toString() - .trim(); +): Promise { + const result = await packageRegistryView( + packageName, + packageVersion, + 'nx-migrations ng-update --json' + ); if (!result) { return null; } - const json = JSON.parse(result); - let migrationsFilePath = json['nx-migrations'] ?? json['ng-update'] ?? json; - if (typeof json === 'object') { - migrationsFilePath = migrationsFilePath.migrations; - } - - return migrationsFilePath; + return resolveNxMigrationConfig(JSON.parse(result)); } -async function getPackageMigrationsUsingRegistry( +async function downloadPackageMigrationsFromRegistry( packageName: string, packageVersion: string, - migrationsFilePath: string -) { - const dir = dirSync().name; - createNPMRC(dir); + { migrations: migrationsFilePath, packageGroup }: NxMigrationsConfiguration +): Promise { + const dir = createTempNpmDirectory(); - let pm = detectPackageManager(); - if (pm === 'yarn') { - pm = 'npm'; - } - - const tarballPath = execSync(`${pm} pack ${packageName}@${packageVersion}`, { - cwd: dir, - stdio: [], - }) - .toString() - .trim(); + let result: ResolvedMigrationConfiguration; - let migrations = null; - migrationsFilePath = join('package', migrationsFilePath); - const migrationDestinationPath = join(dir, migrationsFilePath); try { - await extractFileFromTarball( - join(dir, tarballPath), - migrationsFilePath, - migrationDestinationPath + const { tarballPath } = await packageRegistryPack( + dir, + packageName, + packageVersion ); - migrations = readJsonFile(migrationDestinationPath); + const migrations = await extractFileFromTarball( + join(dir, tarballPath), + join('package', migrationsFilePath), + join(dir, migrationsFilePath) + ).then((path) => readJsonFile(path)); + + result = { ...migrations, packageGroup, version: packageVersion }; } catch { throw new Error( `Failed to find migrations file "${migrationsFilePath}" in package "${packageName}@${packageVersion}".` ); + } finally { + try { + await remove(dir); + } catch { + // It's okay if this fails, the OS will clean it up eventually + } } - try { - removeSync(dir); - } catch { - // It's okay if this fails, the OS will clean it up eventually - } - - return migrations; + return result; } -async function installPackageAndGetVersionAngMigrations( +async function getPackageMigrationsUsingInstall( packageName: string, packageVersion: string -) { - const dir = dirSync().name; - createNPMRC(dir); +): Promise { + const dir = createTempNpmDirectory(); - const pmc = getPackageManagerCommand(); - execSync(`${pmc.add} ${packageName}@${packageVersion}`, { - stdio: [], - cwd: dir, - }); + let result: ResolvedMigrationConfiguration; - const packageJsonPath = require.resolve(`${packageName}/package.json`, { - paths: [dir], - }); - const { version: resolvedVersion } = readJsonFile(packageJsonPath); + try { + const pmc = getPackageManagerCommand(); - const migrationsFilePath = packageToMigrationsFilePath(packageName, dir); - let migrations = null; - if (migrationsFilePath) { - migrations = readJsonFile(migrationsFilePath); - } + await execAsync(`${pmc.add} ${packageName}@${packageVersion}`, { + cwd: dir, + }); - try { - removeSync(dir); - } catch { - // It's okay if this fails, the OS will clean it up eventually + const { + migrations: migrationsFilePath, + packageGroup, + packageJson, + } = readPackageMigrationConfig(packageName, dir); + + let migrations: MigrationsJson = undefined; + if (migrationsFilePath) { + migrations = readJsonFile(migrationsFilePath); + } + + result = { ...migrations, packageGroup, version: packageJson.version }; + } finally { + try { + await remove(dir); + } catch { + // It's okay if this fails, the OS will clean it up eventually + } } - return { migrations, resolvedVersion }; + return result; } -function createNPMRC(dir: string): void { - // A package.json is needed for pnpm pack and for .npmrc to resolve - writeJsonFile(`${dir}/package.json`, {}); - const npmrc = checkForNPMRC(); - if (npmrc) { - // Copy npmrc if it exists, so that npm still follows it. - copyFileSync(npmrc, `${dir}/.npmrc`); - } +interface PackageMigrationConfig extends NxMigrationsConfiguration { + packageJson: PackageJson; } -function packageToMigrationsFilePath(packageName: string, dir: string) { +function readPackageMigrationConfig( + packageName: string, + dir: string +): PackageMigrationConfig { const packageJsonPath = require.resolve(`${packageName}/package.json`, { paths: [dir], }); - const json = readJsonFile(packageJsonPath); - let migrationsFile = json['nx-migrations'] || json['ng-update']; - // migrationsFile is an object - if (migrationsFile && migrationsFile.migrations) { - migrationsFile = migrationsFile.migrations; + const json = readJsonFile(packageJsonPath); + const migrationConfigOrFile = json['nx-migrations'] || json['ng-update']; + + if (!migrationConfigOrFile) { + return { packageJson: json, migrations: null, packageGroup: [] }; } + + const migrationsConfig = + typeof migrationConfigOrFile === 'string' + ? { + migrations: migrationConfigOrFile, + packageGroup: [], + } + : migrationConfigOrFile; + try { - if (migrationsFile && typeof migrationsFile === 'string') { - return require.resolve(migrationsFile, { - paths: [dirname(packageJsonPath)], - }); - } else { - return null; - } + const migrationFile = require.resolve(migrationsConfig.migrations, { + paths: [dirname(packageJsonPath)], + }); + + return { + packageJson: json, + migrations: migrationFile, + packageGroup: migrationsConfig.packageGroup, + }; } catch { - return null; + return { + packageJson: json, + migrations: null, + packageGroup: migrationsConfig.packageGroup, + }; } } @@ -680,28 +714,30 @@ function createMigrationsFile( function updatePackageJson( root: string, - updatedPackages: { - [p: string]: { version: string; addToPackageJson: Dependencies | false }; - } + updatedPackages: Record ) { const packageJsonPath = join(root, 'package.json'); const parseOptions: JsonReadOptions = {}; const json = readJsonFile(packageJsonPath, parseOptions); + Object.keys(updatedPackages).forEach((p) => { - if (json.devDependencies && json.devDependencies[p]) { + if (json.devDependencies?.[p]) { json.devDependencies[p] = updatedPackages[p].version; - } else if (json.dependencies && json.dependencies[p]) { + return; + } + + if (json.dependencies?.[p]) { json.dependencies[p] = updatedPackages[p].version; - } else if (updatedPackages[p].addToPackageJson) { - if (updatedPackages[p].addToPackageJson === 'dependencies') { - if (!json.dependencies) json.dependencies = {}; - json.dependencies[p] = updatedPackages[p].version; - } else if (updatedPackages[p].addToPackageJson === 'devDependencies') { - if (!json.devDependencies) json.devDependencies = {}; - json.devDependencies[p] = updatedPackages[p].version; - } + return; + } + + const dependencyType = updatedPackages[p].addToPackageJson; + if (typeof dependencyType === 'string') { + json[dependencyType] ??= {}; + json[dependencyType][p] = updatedPackages[p].version; } }); + writeJsonFile(packageJsonPath, json, { appendNewLine: parseOptions.endsWithNewline, }); @@ -720,18 +756,21 @@ async function generateMigrationsJsonAndUpdatePackageJson( try { logger.info(`Fetching meta data about packages.`); logger.info(`It may take a few minutes.`); + const originalPackageJson = readJsonFile(join(root, 'package.json')); + const migrator = new Migrator({ packageJson: originalPackageJson, versions: versions(root, opts.from), fetch: createFetcher(), - from: opts.from, to: opts.to, }); + const { migrations, packageJson } = await migrator.updatePackageJson( opts.targetPackage, opts.targetVersion ); + updatePackageJson(root, packageJson); if (migrations.length > 0) { @@ -752,13 +791,14 @@ async function generateMigrationsJsonAndUpdatePackageJson( `- Make sure package.json changes make sense and then run '${pmc.install}'` ); if (migrations.length > 0) { - logger.info(`- Run 'nx migrate --run-migrations'`); + logger.info(`- Run '${pmc.run('nx', 'migrate --run-migrations')}'`); } logger.info(`- To learn more go to https://nx.dev/using-nx/updating-nx`); if (showConnectToCloudMessage()) { + const cmd = pmc.run('nx', 'connect-to-nx-cloud'); logger.info( - `- You may run "nx connect-to-nx-cloud" to get faster builds, GitHub integration, and more. Check out https://nx.app` + `- You may run '${cmd}' to get faster builds, GitHub integration, and more. Check out https://nx.app` ); } } catch (e) { @@ -807,7 +847,7 @@ async function runMigrations( cli?: 'nx' | 'angular'; }[] = readJsonFile(join(root, opts.runMigrations)).migrations; - for (let m of migrations) { + for (const m of migrations) { logger.info(`Running migration ${m.name}`); if (m.cli === 'nx') { await runNxMigration(root, m.package, m.name); @@ -826,12 +866,16 @@ async function runMigrations( } async function runNxMigration(root: string, packageName: string, name: string) { - const collectionPath = packageToMigrationsFilePath(packageName, root); - const collection = readJsonFile(collectionPath); + const collectionPath = readPackageMigrationConfig( + packageName, + root + ).migrations; + + const collection = readJsonFile(collectionPath); const g = collection.generators || collection.schematics; const implRelativePath = g[name].implementation || g[name].factory; - let implPath; + let implPath: string; try { implPath = require.resolve(implRelativePath, { diff --git a/packages/nx/src/utils/fileutils.ts b/packages/nx/src/utils/fileutils.ts index dc61fff67d405..06a4c1059efc4 100644 --- a/packages/nx/src/utils/fileutils.ts +++ b/packages/nx/src/utils/fileutils.ts @@ -119,7 +119,7 @@ export async function extractFileFromTarball( file: string, destinationFilePath: string ) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { ensureDirSync(dirname(destinationFilePath)); var tarExtractStream = tar.extract(); const destinationFileStream = createWriteStream(destinationFilePath); @@ -130,7 +130,7 @@ export async function extractFileFromTarball( stream.pipe(destinationFileStream); stream.on('end', () => { isFileExtracted = true; - resolve(); + resolve(destinationFilePath); }); } diff --git a/packages/nx/src/utils/package-json.ts b/packages/nx/src/utils/package-json.ts index b4f3cb2678208..08abb7d646289 100644 --- a/packages/nx/src/utils/package-json.ts +++ b/packages/nx/src/utils/package-json.ts @@ -9,9 +9,15 @@ export interface NxProjectPackageJsonConfiguration { targets?: Record; } +export interface NxMigrationsConfiguration { + migrations?: string; + packageGroup?: (string | { package: string; version: string })[]; +} + export interface PackageJson { // Generic Package.Json Configuration name: string; + version: string; scripts?: Record; dependencies?: Record; devDependencies?: Record; @@ -25,7 +31,8 @@ export interface PackageJson { schematics?: string; builders?: string; executors?: string; - 'nx-migrations'?: string; + 'nx-migrations'?: string | NxMigrationsConfiguration; + 'ng-update'?: string | NxMigrationsConfiguration; } export function buildTargetFromScript( diff --git a/packages/nx/src/utils/package-manager.ts b/packages/nx/src/utils/package-manager.ts index e9be1f71a4596..7f5fe6ff323d7 100644 --- a/packages/nx/src/utils/package-manager.ts +++ b/packages/nx/src/utils/package-manager.ts @@ -1,8 +1,13 @@ -import { execSync } from 'child_process'; -import { copyFileSync, existsSync, unlinkSync } from 'fs'; +import { exec, execSync } from 'child_process'; +import { copyFileSync, existsSync } from 'fs'; +import { remove } from 'fs-extra'; import { dirname, join } from 'path'; import { dirSync } from 'tmp'; +import { promisify } from 'util'; import { readJsonFile, writeJsonFile } from './fileutils'; +import { PackageJson } from './package-json'; + +const execAsync = promisify(exec); export type PackageManager = 'yarn' | 'pnpm' | 'npm'; @@ -114,25 +119,37 @@ export function checkForNPMRC( return existsSync(path) ? path : null; } +/** + * Creates a temporary directory where you can run package manager commands safely. + * + * For cases where you'd want to install packages that require an `.npmrc` set up, + * this function looks up for the nearest `.npmrc` (if exists) and copies it over to the + * temp directory. + */ +export function createTempNpmDirectory(): string { + const dir = dirSync().name; + + // A package.json is needed for pnpm pack and for .npmrc to resolve + writeJsonFile(`${dir}/package.json`, {}); + const npmrc = checkForNPMRC(); + if (npmrc) { + // Copy npmrc if it exists, so that npm still follows it. + copyFileSync(npmrc, `${dir}/.npmrc`); + } + + return dir; +} + /** * Returns the resolved version for a given package and version tag using the * NPM registry (when using Yarn it will fall back to NPM to fetch the info). */ -export function resolvePackageVersionUsingRegistry( +export async function resolvePackageVersionUsingRegistry( packageName: string, version: string -): string { - let pm = detectPackageManager(); - if (pm === 'yarn') { - pm = 'npm'; - } - +): Promise { try { - const result = execSync(`${pm} view ${packageName}@${version} version`, { - stdio: [], - }) - .toString() - .trim(); + const result = await packageRegistryView(packageName, version, 'version'); if (!result) { throw new Error(`Unable to resolve version ${packageName}@${version}.`); @@ -157,32 +174,69 @@ export function resolvePackageVersionUsingRegistry( * installing it in a temporary directory and fetching the version from the * package.json. */ -export function resolvePackageVersionUsingInstallation( +export async function resolvePackageVersionUsingInstallation( packageName: string, version: string -): string { - const dir = dirSync().name; - const npmrc = checkForNPMRC(); +): Promise { + const dir = createTempNpmDirectory(); - writeJsonFile(`${dir}/package.json`, {}); - if (npmrc) { - // Copy npmrc if it exists, so that npm still follows it. - copyFileSync(npmrc, `${dir}/.npmrc`); + try { + const pmc = getPackageManagerCommand(); + await execAsync(`${pmc.add} ${packageName}@${version}`, { cwd: dir }); + + const packageJsonPath = require.resolve(`${packageName}/package.json`, { + paths: [dir], + }); + + return readJsonFile(packageJsonPath).version; + } finally { + try { + await remove(dir); + } catch { + // It's okay if this fails, the OS will clean it up eventually + } } +} - const pmc = getPackageManagerCommand(); - execSync(`${pmc.add} ${packageName}@${version}`, { stdio: [], cwd: dir }); +export async function packageRegistryView( + pkg: string, + version: string, + args: string +): Promise { + let pm = detectPackageManager(); + if (pm === 'yarn') { + /** + * yarn has `yarn info` but it behaves differently than (p)npm, + * which makes it's usage unreliable + * + * @see https://github.com/nrwl/nx/pull/9667#discussion_r842553994 + */ + pm = 'npm'; + } - const packageJsonPath = require.resolve(`${packageName}/package.json`, { - paths: [dir], - }); - const { version: resolvedVersion } = readJsonFile(packageJsonPath); + const { stdout } = await execAsync(`${pm} view ${pkg}@${version} ${args}`); + return stdout.toString().trim(); +} - try { - unlinkSync(dir); - } catch { - // It's okay if this fails, the OS will clean it up eventually +export async function packageRegistryPack( + cwd: string, + pkg: string, + version: string +): Promise<{ tarballPath: string }> { + let pm = detectPackageManager(); + if (pm === 'yarn') { + /** + * `(p)npm pack` will download a tarball of the specified version, + * whereas `yarn` pack creates a tarball of the active workspace, so it + * does not work for getting the content of a library. + * + * @see https://github.com/nrwl/nx/pull/9667#discussion_r842553994 + */ + pm = 'npm'; } - return resolvedVersion; + const { stdout } = await execAsync(`${pm} pack ${pkg}@${version}`, { cwd }); + + const tarballPath = stdout.trim(); + return { tarballPath }; } diff --git a/packages/nx/src/utils/project-graph-utils.spec.ts b/packages/nx/src/utils/project-graph-utils.spec.ts index c268429ca888c..577215811cc97 100644 --- a/packages/nx/src/utils/project-graph-utils.spec.ts +++ b/packages/nx/src/utils/project-graph-utils.spec.ts @@ -107,6 +107,7 @@ describe('project graph utils', () => { describe('mergeNpmScriptsWithTargets', () => { const packageJson: PackageJson = { name: 'my-app', + version: '0.0.0', scripts: { build: 'echo 1', }, diff --git a/packages/workspace/package.json b/packages/workspace/package.json index 4acefe78c3065..d29e5aafce465 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -47,7 +47,12 @@ "@nrwl/nx-plugin", "@nrwl/react", "@nrwl/storybook", - "@nrwl/web" + "@nrwl/web", + "@nrwl/js", + "@nrwl/cli", + "@nrwl/nx-cloud", + "@nrwl/react-native", + "@nrwl/detox" ] }, "peerDependencies": { @@ -82,6 +87,32 @@ "tslib": "^2.3.0" }, "nx-migrations": { - "migrations": "./migrations.json" + "migrations": "./migrations.json", + "packageGroup": [ + "@nrwl/workspace", + "@nrwl/angular", + "nx", + "@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/react-native", + "@nrwl/detox", + { + "package": "@nrwl/nx-cloud", + "version": "latest" + } + ] } }