feat(core): respect packageGroup in nx migrate (#9667)

* cleanup(core): refactor migrate.ts

* feat(core): respect packageGroup in nx migrate

ISSUES CLOSED: #4575

* fix build

* chore(repo): trigger CI

* chore(core): fix review comments, without packageGroup extension

* chore(core): revert `withTempNpmDirectory` changes

* chore(repo): trigger ci

* chore(core): fix unit tests

* chore(core): remove view and pack from pmc

* chore(repo): kick off CI

* chore(core): add tests for recursive packageGroup

* chore(core): add another test for cyclic packageGroup

* chore(repo): kickoff CI

Co-authored-by: Craigory Coppola <craigorycoppola@gmail.com>
This commit is contained in:
Giora Guttsait 2022-04-08 22:57:24 +03:00 committed by GitHub
parent 0e68c616fe
commit 32b49b6f7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 711 additions and 493 deletions

View File

@ -15,7 +15,7 @@ import { packagesWeCareAbout } from 'nx/src/command-line/report';
describe('Cli', () => { describe('Cli', () => {
beforeEach(() => newProject()); beforeEach(() => newProject());
it('vvvshould execute long running tasks', async () => { it('should execute long running tasks', async () => {
const myapp = uniq('myapp'); const myapp = uniq('myapp');
runCLI(`generate @nrwl/web:app ${myapp}`); runCLI(`generate @nrwl/web:app ${myapp}`);
updateProjectConfig(myapp, (c) => { updateProjectConfig(myapp, (c) => {
@ -156,7 +156,7 @@ describe('list', () => {
describe('migrate', () => { describe('migrate', () => {
beforeEach(() => newProject()); beforeEach(() => newProject());
it('clear-cacheshould run migrations', () => { it('should run migrations', () => {
updateFile( updateFile(
`./node_modules/migrate-parent-package/package.json`, `./node_modules/migrate-parent-package/package.json`,
JSON.stringify({ JSON.stringify({

View File

@ -32,13 +32,13 @@ export async function determineMigration(
): Promise<MigrationDefinition> { ): Promise<MigrationDefinition> {
const angularVersion = getInstalledAngularVersion(); const angularVersion = getInstalledAngularVersion();
const majorAngularVersion = major(angularVersion); const majorAngularVersion = major(angularVersion);
latestWorkspaceVersionWithMigration = resolvePackageVersion( latestWorkspaceVersionWithMigration = await resolvePackageVersion(
'@nrwl/angular', '@nrwl/angular',
latestWorkspaceRangeVersionWithMigration latestWorkspaceRangeVersionWithMigration
); );
if (version) { if (version) {
const normalizedVersion = normalizeVersion(version); const normalizedVersion = await normalizeVersion(version);
if (lte(normalizedVersion, latestWorkspaceVersionWithMigration)) { if (lte(normalizedVersion, latestWorkspaceVersionWithMigration)) {
// specified version should use @nrwl/workspace:ng-add // specified version should use @nrwl/workspace:ng-add
return { packageName: '@nrwl/workspace', version: normalizedVersion }; return { packageName: '@nrwl/workspace', version: normalizedVersion };
@ -66,7 +66,8 @@ export async function determineMigration(
); );
} }
const latestNxCompatibleVersion = getNxVersionBasedOnInstalledAngularVersion( const latestNxCompatibleVersion =
await getNxVersionBasedOnInstalledAngularVersion(
angularVersion, angularVersion,
majorAngularVersion majorAngularVersion
); );
@ -105,7 +106,8 @@ async function findAndSuggestVersionToUse(
majorAngularVersion: number, majorAngularVersion: number,
userSpecifiedVersion: string userSpecifiedVersion: string
): Promise<MigrationDefinition> { ): Promise<MigrationDefinition> {
const latestNxCompatibleVersion = getNxVersionBasedOnInstalledAngularVersion( const latestNxCompatibleVersion =
await getNxVersionBasedOnInstalledAngularVersion(
angularVersion, angularVersion,
majorAngularVersion majorAngularVersion
); );
@ -134,10 +136,10 @@ async function findAndSuggestVersionToUse(
process.exit(1); process.exit(1);
} }
function getNxVersionBasedOnInstalledAngularVersion( async function getNxVersionBasedOnInstalledAngularVersion(
angularVersion: string, angularVersion: string,
majorAngularVersion: number majorAngularVersion: number
): string { ): Promise<string> {
if (lt(angularVersion, '13.0.0')) { if (lt(angularVersion, '13.0.0')) {
// the @nrwl/angular:ng-add generator is only available for versions supporting // the @nrwl/angular:ng-add generator is only available for versions supporting
// Angular >= 13.0.0, fall back to @nrwl/workspace:ng-add // 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 // 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<boolean> { async function promptForVersion(version: string): Promise<boolean> {
@ -177,13 +179,13 @@ function getInstalledAngularVersion(): string {
return readJsonFile(packageJsonPath).version; return readJsonFile(packageJsonPath).version;
} }
function normalizeVersion(version: string): string { async function normalizeVersion(version: string): Promise<string> {
if ( if (
version.startsWith('^') || version.startsWith('^') ||
version.startsWith('~') || version.startsWith('~') ||
version.split('.').length < 3 version.split('.').length < 3
) { ) {
return resolvePackageVersion('@nrwl/angular', version); return await resolvePackageVersion('@nrwl/angular', version);
} }
return version; return version;

View File

@ -55,13 +55,13 @@ export function installDependencies(
}); });
} }
export function resolvePackageVersion( export async function resolvePackageVersion(
packageName: string, packageName: string,
version: string version: string
): string { ): Promise<string> {
try { try {
return resolvePackageVersionUsingRegistry(packageName, version); return await resolvePackageVersionUsingRegistry(packageName, version);
} catch { } catch {
return resolvePackageVersionUsingInstallation(packageName, version); return await resolvePackageVersionUsingInstallation(packageName, version);
} }
} }

View File

@ -1,32 +1,36 @@
import { PackageJson } from '../utils/package-json';
import { Migrator, normalizeVersion, parseMigrationsOptions } from './migrate'; import { Migrator, normalizeVersion, parseMigrationsOptions } from './migrate';
const createPackageJson = (
overrides: Partial<PackageJson> = {}
): PackageJson => ({
name: 'some-workspace',
version: '0.0.0',
...overrides,
});
describe('Migration', () => { describe('Migration', () => {
describe('packageJson patch', () => { describe('packageJson patch', () => {
it('should throw an error when the target package is not available', async () => { it('should throw an error when the target package is not available', async () => {
const migrator = new Migrator({ const migrator = new Migrator({
packageJson: {}, packageJson: createPackageJson(),
versions: () => '1.0', versions: () => '1.0',
fetch: (_p, _v) => { fetch: (_p, _v) => {
throw new Error('cannot fetch'); throw new Error('cannot fetch');
}, },
from: {},
to: {}, to: {},
}); });
try { await expect(
await migrator.updatePackageJson('mypackage', 'myversion'); migrator.updatePackageJson('mypackage', 'myversion')
throw new Error('fail'); ).rejects.toThrowError(/cannot fetch/);
} catch (e) {
expect(e.message).toEqual(`cannot fetch`);
}
}); });
it('should return a patch to the new version', async () => { it('should return a patch to the new version', async () => {
const migrator = new Migrator({ const migrator = new Migrator({
packageJson: {}, packageJson: createPackageJson(),
versions: () => '1.0.0', versions: () => '1.0.0',
fetch: (_p, _v) => Promise.resolve({ version: '2.0.0' }), fetch: (_p, _v) => Promise.resolve({ version: '2.0.0' }),
from: {},
to: {}, to: {},
}); });
@ -40,7 +44,7 @@ describe('Migration', () => {
it('should collect the information recursively from upserts', async () => { it('should collect the information recursively from upserts', async () => {
const migrator = new Migrator({ const migrator = new Migrator({
packageJson: { dependencies: { child: '1.0.0' } }, packageJson: createPackageJson({ dependencies: { child: '1.0.0' } }),
versions: () => '1.0.0', versions: () => '1.0.0',
fetch: (p, _v) => { fetch: (p, _v) => {
if (p === 'parent') { if (p === 'parent') {
@ -68,7 +72,6 @@ describe('Migration', () => {
return Promise.resolve(null); return Promise.resolve(null);
} }
}, },
from: {},
to: {}, to: {},
}); });
@ -84,7 +87,7 @@ describe('Migration', () => {
it('should support the deprecated "alwaysAddToPackageJson" option', async () => { it('should support the deprecated "alwaysAddToPackageJson" option', async () => {
const migrator = new Migrator({ const migrator = new Migrator({
packageJson: { dependencies: { child1: '1.0.0' } }, packageJson: createPackageJson({ dependencies: { child1: '1.0.0' } }),
versions: () => '1.0.0', versions: () => '1.0.0',
fetch: (p, _v) => { fetch: (p, _v) => {
if (p === 'mypackage') { if (p === 'mypackage') {
@ -108,7 +111,6 @@ describe('Migration', () => {
return Promise.resolve(null); return Promise.resolve(null);
} }
}, },
from: {},
to: {}, to: {},
}); });
@ -124,7 +126,7 @@ describe('Migration', () => {
it('should stop recursive calls when exact version', async () => { it('should stop recursive calls when exact version', async () => {
const migrator = new Migrator({ const migrator = new Migrator({
packageJson: { dependencies: { child: '1.0.0' } }, packageJson: createPackageJson({ dependencies: { child: '1.0.0' } }),
versions: () => '1.0.0', versions: () => '1.0.0',
fetch: (p, _v) => { fetch: (p, _v) => {
if (p === 'parent') { if (p === 'parent') {
@ -157,7 +159,6 @@ describe('Migration', () => {
return Promise.resolve(null); return Promise.resolve(null);
} }
}, },
from: {},
to: {}, to: {},
}); });
@ -172,13 +173,13 @@ describe('Migration', () => {
it('should set the version of a dependency to the newest', async () => { it('should set the version of a dependency to the newest', async () => {
const migrator = new Migrator({ const migrator = new Migrator({
packageJson: { packageJson: createPackageJson({
dependencies: { dependencies: {
child1: '1.0.0', child1: '1.0.0',
child2: '1.0.0', child2: '1.0.0',
grandchild: '1.0.0', grandchild: '1.0.0',
}, },
}, }),
versions: () => '1.0.0', versions: () => '1.0.0',
fetch: (p, _v) => { fetch: (p, _v) => {
if (p === 'parent') { if (p === 'parent') {
@ -225,7 +226,6 @@ describe('Migration', () => {
return Promise.resolve({ version: '4.0.0' }); return Promise.resolve({ version: '4.0.0' });
} }
}, },
from: {},
to: {}, to: {},
}); });
@ -242,7 +242,9 @@ describe('Migration', () => {
it('should skip the versions <= currently installed', async () => { it('should skip the versions <= currently installed', async () => {
const migrator = new Migrator({ 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', versions: () => '1.0.0',
fetch: (p, _v) => { fetch: (p, _v) => {
if (p === 'parent') { if (p === 'parent') {
@ -275,7 +277,6 @@ describe('Migration', () => {
return Promise.resolve({ version: '2.0.0' }); return Promise.resolve({ version: '2.0.0' });
} }
}, },
from: {},
to: {}, to: {},
}); });
@ -290,7 +291,9 @@ describe('Migration', () => {
it('should conditionally process packages if they are installed', async () => { it('should conditionally process packages if they are installed', async () => {
const migrator = new Migrator({ 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), versions: (p) => (p !== 'not-installed' ? '1.0.0' : null),
fetch: (p, _v) => { fetch: (p, _v) => {
if (p === 'parent') { if (p === 'parent') {
@ -318,7 +321,6 @@ describe('Migration', () => {
return Promise.resolve(null); return Promise.resolve(null);
} }
}, },
from: {},
to: {}, to: {},
}); });
@ -331,78 +333,155 @@ describe('Migration', () => {
}); });
}); });
// this is temporary. if nx gets used by other projects, it('should migrate related libraries using packageGroup', async () => {
// we will extract the special casing
it('should special case @nrwl/workspace', async () => {
const migrator = new Migrator({ const migrator = new Migrator({
packageJson: { packageJson: {
name: 'some-workspace',
version: '0.0.0',
devDependencies: { devDependencies: {
'@nrwl/workspace': '0.9.0', '@my-company/nx-workspace': '0.9.0',
'@nrwl/cli': '0.9.0', '@my-company/lib-1': '0.9.0',
'@nrwl/angular': '0.9.0', '@my-company/lib-2': '0.9.0',
'@nrwl/cypress': '0.9.0', '@my-company/lib-3': '0.9.0',
'@nrwl/devkit': '0.9.0', '@my-company/lib-3-child': '0.9.0',
'@nrwl/eslint-plugin-nx': '0.9.0', '@my-company/lib-4': '0.9.0',
'@nrwl/express': '0.9.0', '@my-company/lib-5': '0.9.0',
'@nrwl/jest': '0.9.0', '@my-company/lib-6': '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',
}, },
}, },
versions: () => '1.0.0', versions: () => '1.0.0',
fetch: (_p, _v) => Promise.resolve({ version: '2.0.0' }), fetch: async (pkg, version) => {
from: {}, 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: {}, to: {},
}); });
expect( expect(
await migrator.updatePackageJson('@nrwl/workspace', '2.0.0') await migrator.updatePackageJson('@my-company/nx-workspace', '2.0.0')
).toEqual({ ).toStrictEqual({
migrations: [], migrations: [],
packageJson: { packageJson: {
'@nrwl/workspace': { version: '2.0.0', addToPackageJson: false }, '@my-company/nx-workspace': {
'@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': {
version: '2.0.0', version: '2.0.0',
addToPackageJson: false, addToPackageJson: false,
}, },
'@nrwl/express': { version: '2.0.0', addToPackageJson: false }, '@my-company/lib-1': { version: '2.0.0', addToPackageJson: false },
'@nrwl/jest': { version: '2.0.0', addToPackageJson: false }, '@my-company/lib-2': { version: '2.0.0', addToPackageJson: false },
'@nrwl/js': { version: '2.0.0', addToPackageJson: false }, '@my-company/lib-3': { version: '2.0.0', addToPackageJson: false },
'@nrwl/linter': { version: '2.0.0', addToPackageJson: false }, '@my-company/lib-3-child': {
'@nrwl/nest': { version: '2.0.0', addToPackageJson: false }, version: '2.0.0',
'@nrwl/next': { version: '2.0.0', addToPackageJson: false }, addToPackageJson: false,
'@nrwl/node': { version: '2.0.0', addToPackageJson: false }, },
'@nrwl/nx-cloud': { version: '2.0.0', addToPackageJson: false }, '@my-company/lib-4': { version: '2.0.1', 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 }, 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 () => { it('should not throw when packages are missing', async () => {
const migrator = new Migrator({ const migrator = new Migrator({
packageJson: {}, packageJson: createPackageJson(),
versions: (p) => (p === '@nrwl/nest' ? null : '1.0.0'), versions: (p) => (p === '@nrwl/nest' ? null : '1.0.0'),
fetch: (_p, _v) => fetch: (_p, _v) =>
Promise.resolve({ Promise.resolve({
version: '2.0.0', version: '2.0.0',
packageJsonUpdates: { one: { version: '2.0.0', packages: {} } }, packageJsonUpdates: { one: { version: '2.0.0', packages: {} } },
}), }),
from: {},
to: {}, to: {},
}); });
await migrator.updatePackageJson('@nrwl/workspace', '2.0.0'); await migrator.updatePackageJson('@nrwl/workspace', '2.0.0');
@ -410,7 +489,7 @@ describe('Migration', () => {
it('should only fetch packages that are installed', async () => { it('should only fetch packages that are installed', async () => {
const migrator = new Migrator({ const migrator = new Migrator({
packageJson: {}, packageJson: createPackageJson(),
versions: (p) => (p === '@nrwl/nest' ? null : '1.0.0'), versions: (p) => (p === '@nrwl/nest' ? null : '1.0.0'),
fetch: (p, _v) => { fetch: (p, _v) => {
if (p === '@nrwl/nest') { if (p === '@nrwl/nest') {
@ -421,7 +500,6 @@ describe('Migration', () => {
packageJsonUpdates: { one: { version: '2.0.0', packages: {} } }, packageJsonUpdates: { one: { version: '2.0.0', packages: {} } },
}); });
}, },
from: {},
to: {}, to: {},
}); });
await migrator.updatePackageJson('@nrwl/workspace', '2.0.0'); 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 () => { it('should only fetch packages that are top-level deps', async () => {
const migrator = new Migrator({ 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', versions: () => '1.0.0',
fetch: (p, _v) => { fetch: (p, _v) => {
if (p === 'parent') { if (p === 'parent') {
@ -455,7 +535,6 @@ describe('Migration', () => {
throw new Error('Boom'); throw new Error('Boom');
} }
}, },
from: {},
to: {}, to: {},
}); });
@ -466,7 +545,9 @@ describe('Migration', () => {
describe('migrations', () => { describe('migrations', () => {
it('should create a list of migrations to run', async () => { it('should create a list of migrations to run', async () => {
const migrator = new Migrator({ 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) => { versions: (p) => {
if (p === 'parent') return '1.0.0'; if (p === 'parent') return '1.0.0';
if (p === 'child') return '1.0.0'; if (p === 'child') return '1.0.0';
@ -518,7 +599,6 @@ describe('Migration', () => {
return Promise.resolve(null); return Promise.resolve(null);
} }
}, },
from: {},
to: {}, to: {},
}); });
expect(await migrator.updatePackageJson('parent', '2.0.0')).toEqual({ 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 () => { it('should not generate migrations for non top-level packages', async () => {
const migrator = new Migrator({ const migrator = new Migrator({
packageJson: { dependencies: { child: '1.0.0' } }, packageJson: createPackageJson({ dependencies: { child: '1.0.0' } }),
versions: (p) => { versions: (p) => {
if (p === 'parent') return '1.0.0'; if (p === 'parent') return '1.0.0';
if (p === 'child') return '1.0.0'; if (p === 'child') return '1.0.0';
@ -599,7 +679,6 @@ describe('Migration', () => {
return Promise.resolve(null); return Promise.resolve(null);
} }
}, },
from: {},
to: {}, to: {},
}); });

View File

@ -1,8 +1,8 @@
import { execSync } from 'child_process'; import { exec, execSync } from 'child_process';
import { copyFileSync, removeSync } from 'fs-extra'; import { remove } from 'fs-extra';
import { dirname, join } from 'path'; import { dirname, join } from 'path';
import { gt, lte } from 'semver'; import { gt, lte } from 'semver';
import { dirSync } from 'tmp'; import { promisify } from 'util';
import { NxJsonConfiguration } from '../config/nx-json'; import { NxJsonConfiguration } from '../config/nx-json';
import { flushChanges, FsTree } from '../config/tree'; import { flushChanges, FsTree } from '../config/tree';
import { import {
@ -12,92 +12,111 @@ import {
writeJsonFile, writeJsonFile,
} from '../utils/fileutils'; } from '../utils/fileutils';
import { logger } from '../utils/logger'; import { logger } from '../utils/logger';
import { NxMigrationsConfiguration, PackageJson } from '../utils/package-json';
import { import {
checkForNPMRC, createTempNpmDirectory,
detectPackageManager,
getPackageManagerCommand, getPackageManagerCommand,
packageRegistryPack,
packageRegistryView,
resolvePackageVersionUsingRegistry, resolvePackageVersionUsingRegistry,
} from '../utils/package-manager'; } from '../utils/package-manager';
import { handleErrors } from '../utils/params'; import { handleErrors } from '../utils/params';
type Dependencies = 'dependencies' | 'devDependencies'; export type Dependencies = 'dependencies' | 'devDependencies';
export type MigrationsJson = { export interface PackageJsonUpdateForPackage {
version: string; version: string;
collection?: string; ifPackageInstalled?: string;
generators?: { alwaysAddToPackageJson?: boolean | Dependencies;
[name: string]: { version: string; description?: string; cli?: string }; addToPackageJson?: boolean | Dependencies;
}; }
packageJsonUpdates?: {
export type PackageJsonUpdates = {
[name: string]: { [name: string]: {
version: string; version: string;
packages: { packages: {
[p: string]: { [packageName: string]: PackageJsonUpdateForPackage;
version: string;
ifPackageInstalled?: string;
alwaysAddToPackageJson?: boolean;
addToPackageJson?: Dependencies;
};
};
}; };
}; };
}; };
export function normalizeVersion(version: string) { export interface GeneratorMigration {
const [v, t] = version.split('-'); version: string;
const [major, minor, patch] = v.split('.'); description?: string;
const newV = `${major || 0}.${minor || 0}.${patch || 0}`; cli?: string;
const newVersion = t ? `${newV}-${t}` : newV; implementation?: string;
factory?: string;
try {
gt(newVersion, '0.0.0');
return newVersion;
} catch (e) {
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';
}
}
}
}
} }
function slash(packageName) { 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 [semver, prereleaseTag] = version.split('-');
const [major, minor, patch] = semver.split('.');
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 {
if (gt(variation, '0.0.0')) {
return variation;
}
} catch {}
}
return '0.0.0';
}
function normalizeSlashes(packageName: string): string {
return packageName.replace(/\\/g, '/'); return packageName.replace(/\\/g, '/');
} }
export class Migrator { export interface MigratorOptions {
private readonly packageJson: any; packageJson: PackageJson;
private readonly versions: (p: string) => string; versions: (pkg: string) => string;
private readonly fetch: (p: string, v: string) => Promise<MigrationsJson>; fetch: (
private readonly from: { [p: string]: string }; pkg: string,
private readonly to: { [p: string]: string }; version: string
) => Promise<ResolvedMigrationConfiguration>;
to: { [pkg: string]: string };
}
constructor(opts: { export class Migrator {
packageJson: any; private readonly packageJson: MigratorOptions['packageJson'];
versions: (p: string) => string; private readonly versions: MigratorOptions['versions'];
fetch: (p: string, v: string) => Promise<MigrationsJson>; private readonly fetch: MigratorOptions['fetch'];
from: { [p: string]: string }; private readonly to: MigratorOptions['to'];
to: { [p: string]: string };
}) { constructor(opts: MigratorOptions) {
this.packageJson = opts.packageJson; this.packageJson = opts.packageJson;
this.versions = opts.versions; this.versions = opts.versions;
this.fetch = opts.fetch; this.fetch = opts.fetch;
this.from = opts.from;
this.to = opts.to; this.to = opts.to;
} }
@ -107,47 +126,47 @@ export class Migrator {
{ version: targetVersion, addToPackageJson: false }, { version: targetVersion, addToPackageJson: false },
{} {}
); );
const migrations = await this._createMigrateJson(packageJson); const migrations = await this._createMigrateJson(packageJson);
return { packageJson, migrations }; return { packageJson, migrations };
} }
private async _createMigrateJson(versions: { private async _createMigrateJson(
[k: string]: { version: string; addToPackageJson: Dependencies | false }; versions: Record<string, PackageJsonUpdateForPackage>
}) { ) {
const migrations = await Promise.all( const migrations = await Promise.all(
Object.keys(versions).map(async (c) => { Object.keys(versions).map(async (packageName) => {
const currentVersion = this.versions(c); const currentVersion = this.versions(packageName);
if (currentVersion === null) return []; if (currentVersion === null) return [];
const target = versions[c]; const { version } = versions[packageName];
const migrationsJson = await this.fetch(c, target.version); const { generators } = await this.fetch(packageName, version);
const generators = migrationsJson.generators;
if (!generators) return []; if (!generators) return [];
return Object.keys(generators)
return Object.entries(generators)
.filter( .filter(
(r) => ([, migration]) =>
generators[r].version && migration.version &&
this.gt(generators[r].version, currentVersion) && this.gt(migration.version, currentVersion) &&
this.lte(generators[r].version, target.version) this.lte(migration.version, version)
) )
.map((r) => ({ .map(([migrationName, migration]) => ({
...migrationsJson.generators[r], ...migration,
package: c, package: packageName,
name: r, name: migrationName,
})); }));
}) })
); );
return migrations.reduce((m, c) => [...m, ...c], []); return migrations.flat();
} }
private async _updatePackageJson( private async _updatePackageJson(
targetPackage: string, targetPackage: string,
target: { version: string; addToPackageJson: Dependencies | false }, target: PackageJsonUpdateForPackage,
collectedVersions: { collectedVersions: Record<string, PackageJsonUpdateForPackage>
[k: string]: { version: string; addToPackageJson: Dependencies | false }; ): Promise<Record<string, PackageJsonUpdateForPackage>> {
}
) {
let targetVersion = target.version; let targetVersion = target.version;
if (this.to[targetPackage]) { if (this.to[targetPackage]) {
targetVersion = this.to[targetPackage]; targetVersion = this.to[targetPackage];
@ -158,16 +177,16 @@ export class Migrator {
[targetPackage]: { [targetPackage]: {
version: target.version, version: target.version,
addToPackageJson: target.addToPackageJson || false, addToPackageJson: target.addToPackageJson || false,
}, } as PackageJsonUpdateForPackage,
}; };
} }
let migrationsJson; let migrationsJson: ResolvedMigrationConfiguration;
try { try {
migrationsJson = await this.fetch(targetPackage, targetVersion); migrationsJson = await this.fetch(targetPackage, targetVersion);
targetVersion = migrationsJson.version; targetVersion = migrationsJson.version;
} catch (e) { } catch (e) {
if (e.message.indexOf('No matching version') > -1) { if (e?.message?.includes('No matching version')) {
throw new Error( throw new Error(
`${e.message}\nRun migrate with --to="package1@version1,package2@version2"` `${e.message}\nRun migrate with --to="package1@version1,package2@version2"`
); );
@ -175,129 +194,120 @@ export class Migrator {
throw e; throw e;
} }
} }
const packages = this.collapsePackages( const packages = this.collapsePackages(
targetPackage, targetPackage,
targetVersion, targetVersion,
migrationsJson migrationsJson
); );
const childCalls = await Promise.all( const childPackageMigrations = await Promise.all(
Object.keys(packages) Object.keys(packages)
.filter((r) => { .filter((packageName) => {
return ( return (
!collectedVersions[r] || !collectedVersions[packageName] ||
this.gt(packages[r].version, collectedVersions[r].version) this.gt(
packages[packageName].version,
collectedVersions[packageName].version
)
); );
}) })
.map((u) => .map((packageName) =>
this._updatePackageJson(u, packages[u], { this._updatePackageJson(packageName, packages[packageName], {
...collectedVersions, ...collectedVersions,
[targetPackage]: target, [targetPackage]: target,
}) })
) )
); );
return childCalls.reduce(
(m, c) => { return childPackageMigrations.reduce(
Object.keys(c).forEach((r) => { (migrations, childMigrations) => {
if (!m[r] || this.gt(c[r].version, m[r].version)) { for (const migrationName of Object.keys(childMigrations)) {
m[r] = c[r]; if (
!migrations[migrationName] ||
this.gt(
childMigrations[migrationName].version,
migrations[migrationName].version
)
) {
migrations[migrationName] = childMigrations[migrationName];
} }
}); }
return m; return migrations;
}, },
{ {
[targetPackage]: { [targetPackage]: {
version: migrationsJson.version, version: migrationsJson.version,
addToPackageJson: target.addToPackageJson || false, addToPackageJson: target.addToPackageJson || false,
}, },
} } as Record<string, PackageJsonUpdateForPackage>
); );
} }
private collapsePackages( private collapsePackages(
packageName: string, packageName: string,
targetVersion: string, targetVersion: string,
m: MigrationsJson | null migration: ResolvedMigrationConfiguration
) { ): Record<string, PackageJsonUpdateForPackage> {
// this should be used to know what version to include // this should be used to know what version to include
// we should use from everywhere we use versions // we should use from everywhere we use versions
if (packageName === '@nrwl/workspace') { if (migration.packageGroup) {
if (!m.packageJsonUpdates) m.packageJsonUpdates = {}; migration.packageJsonUpdates ??= {};
m.packageJsonUpdates[`${targetVersion}-defaultPackages`] = { migration.packageJsonUpdates[`${targetVersion}-defaultPackages`] = {
version: targetVersion, version: targetVersion,
packages: [ packages: migration.packageGroup.reduce((acc, packageConfig) => {
'nx', const { package: pkg, version } =
'@nrwl/angular', typeof packageConfig === 'string'
'@nrwl/cypress', ? { package: packageConfig, version: targetVersion }
'@nrwl/devkit', : packageConfig;
'@nrwl/eslint-plugin-nx',
'@nrwl/express', return {
'@nrwl/jest', ...acc,
'@nrwl/js', [pkg]: {
'@nrwl/cli', version,
'@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,
alwaysAddToPackageJson: false, alwaysAddToPackageJson: false,
}, } as PackageJsonUpdateForPackage,
}), };
{} }, {}),
),
}; };
} }
if (!m.packageJsonUpdates || !this.versions(packageName)) return {};
return Object.keys(m.packageJsonUpdates) if (!migration.packageJsonUpdates || !this.versions(packageName)) return {};
.filter((r) => {
return Object.values(migration.packageJsonUpdates)
.filter(({ version, packages }) => {
return ( return (
this.gt( packages &&
m.packageJsonUpdates[r].version, this.gt(version, this.versions(packageName)) &&
this.versions(packageName) this.lte(version, targetVersion)
) && this.lte(m.packageJsonUpdates[r].version, targetVersion)
); );
}) })
.map((r) => m.packageJsonUpdates[r].packages) .map(({ packages }) => {
.map((packages) => {
if (!packages) return {};
return Object.keys(packages)
.filter((pkg) => {
const { dependencies, devDependencies } = this.packageJson; const { dependencies, devDependencies } = this.packageJson;
return Object.entries(packages)
.filter(([packageName, packageUpdate]) => {
return ( return (
(!packages[pkg].ifPackageInstalled || (!packageUpdate.ifPackageInstalled ||
this.versions(packages[pkg].ifPackageInstalled)) && this.versions(packageUpdate.ifPackageInstalled)) &&
(packages[pkg].alwaysAddToPackageJson || (packageUpdate.alwaysAddToPackageJson ||
packages[pkg].addToPackageJson || packageUpdate.addToPackageJson ||
!!dependencies?.[pkg] || !!dependencies?.[packageName] ||
!!devDependencies?.[pkg]) !!devDependencies?.[packageName])
); );
}) })
.reduce( .reduce(
(m, c) => ({ (acc, [packageName, packageUpdate]) => ({
...m, ...acc,
[c]: { [packageName]: {
version: packages[c].version, version: packageUpdate.version,
addToPackageJson: packages[c].alwaysAddToPackageJson addToPackageJson: packageUpdate.alwaysAddToPackageJson
? 'dependencies' ? 'dependencies'
: packages[c].addToPackageJson || false, : packageUpdate.addToPackageJson || false,
}, },
}), }),
{} {} as Record<string, PackageJsonUpdateForPackage>
); );
}) })
.reduce((m, c) => ({ ...m, ...c }), {}); .reduce((m, c) => ({ ...m, ...c }), {});
@ -333,7 +343,8 @@ function versionOverrides(overrides: string, param: string) {
`Incorrect '${param}' section. Use --${param}="package@version"` `Incorrect '${param}' section. Use --${param}="package@version"`
); );
} }
res[slash(selectedPackage)] = normalizeVersionWithTagCheck(selectedVersion); res[normalizeSlashes(selectedPackage)] =
normalizeVersionWithTagCheck(selectedVersion);
}); });
return res; return res;
} }
@ -388,6 +399,7 @@ type GenerateMigrations = {
from: { [k: string]: string }; from: { [k: string]: string };
to: { [k: string]: string }; to: { [k: string]: string };
}; };
type RunMigrations = { type: 'runMigrations'; runMigrations: string }; type RunMigrations = { type: 'runMigrations'; runMigrations: string };
export function parseMigrationsOptions(options: { export function parseMigrationsOptions(options: {
@ -407,7 +419,7 @@ export function parseMigrationsOptions(options: {
); );
return { return {
type: 'generateMigrations', type: 'generateMigrations',
targetPackage: slash(targetPackage), targetPackage: normalizeSlashes(targetPackage),
targetVersion, targetVersion,
from, from,
to, to,
@ -420,16 +432,23 @@ export function parseMigrationsOptions(options: {
} }
} }
function versions(root: string, from: { [p: string]: string }) { function versions(root: string, from: Record<string, string>) {
const cache: Record<string, string> = {};
return (packageName: string) => { return (packageName: string) => {
try { try {
if (from[packageName]) { if (from[packageName]) {
return from[packageName]; return from[packageName];
} }
if (!cache[packageName]) {
const packageJsonPath = require.resolve(`${packageName}/package.json`, { const packageJsonPath = require.resolve(`${packageName}/package.json`, {
paths: [root], paths: [root],
}); });
return readJsonFile(packageJsonPath).version; cache[packageName] = readJsonFile(packageJsonPath).version;
}
return cache[packageName];
} catch { } catch {
return null; return null;
} }
@ -438,20 +457,21 @@ function versions(root: string, from: { [p: string]: string }) {
// testing-fetch-start // testing-fetch-start
function createFetcher() { function createFetcher() {
const cache = {}; const cache: Record<string, ResolvedMigrationConfiguration> = {};
return async function f(
return async function nxMigrateFetcher(
packageName: string, packageName: string,
packageVersion: string packageVersion: string
): Promise<MigrationsJson> { ): Promise<ResolvedMigrationConfiguration> {
if (cache[`${packageName}-${packageVersion}`]) { if (cache[`${packageName}-${packageVersion}`]) {
return cache[`${packageName}-${packageVersion}`]; return cache[`${packageName}-${packageVersion}`];
} }
let resolvedVersion: string; let resolvedVersion: string = packageVersion;
let migrations: any; let resolvedMigrationConfiguration: ResolvedMigrationConfiguration;
try { try {
resolvedVersion = resolvePackageVersionUsingRegistry( resolvedVersion = await resolvePackageVersionUsingRegistry(
packageName, packageName,
packageVersion packageVersion
); );
@ -461,210 +481,224 @@ function createFetcher() {
} }
logger.info(`Fetching ${packageName}@${packageVersion}`); logger.info(`Fetching ${packageName}@${packageVersion}`);
migrations = await getPackageMigrations(packageName, resolvedVersion);
resolvedMigrationConfiguration = await getPackageMigrationsUsingRegistry(
packageName,
resolvedVersion
);
} catch { } catch {
logger.info(`Fetching ${packageName}@${packageVersion}`); logger.info(`Fetching ${packageName}@${packageVersion}`);
const result = await installPackageAndGetVersionAngMigrations(
resolvedMigrationConfiguration = await getPackageMigrationsUsingInstall(
packageName, packageName,
packageVersion packageVersion
); );
resolvedVersion = result.resolvedVersion;
migrations = result.migrations; resolvedVersion = resolvedMigrationConfiguration.version;
} }
if (migrations) { resolvedMigrationConfiguration = {
cache[`${packageName}-${packageVersion}`] = cache[ ...resolvedMigrationConfiguration,
`${packageName}-${resolvedVersion}` generators:
] = { resolvedMigrationConfiguration.generators ??
version: resolvedVersion, resolvedMigrationConfiguration.schematics,
generators: migrations.generators ?? migrations.schematics,
packageJsonUpdates: migrations.packageJsonUpdates,
}; };
} else {
cache[`${packageName}-${packageVersion}`] = cache[
`${packageName}-${resolvedVersion}`
] = {
version: resolvedVersion,
};
}
return cache[`${packageName}-${packageVersion}`]; cache[`${packageName}-${packageVersion}`] = cache[
`${packageName}-${resolvedVersion}`
] = resolvedMigrationConfiguration;
return resolvedMigrationConfiguration;
}; };
} }
// testing-fetch-end // testing-fetch-end
async function getPackageMigrations( async function getPackageMigrationsUsingRegistry(
packageName: string, packageName: string,
packageVersion: string packageVersion: string
) { ): Promise<ResolvedMigrationConfiguration> {
try {
// check if there are migrations in the packages by looking at the // check if there are migrations in the packages by looking at the
// registry directly // registry directly
const migrationsPath = getPackageMigrationsPathFromRegistry( const migrationsConfig = await getPackageMigrationsConfigFromRegistry(
packageName, packageName,
packageVersion packageVersion
); );
if (!migrationsPath) {
return null; if (!migrationsConfig.migrations) {
return {
version: packageVersion,
packageGroup: migrationsConfig.packageGroup,
};
} }
// try to obtain the migrations from the registry directly // try to obtain the migrations from the registry directly
return await getPackageMigrationsUsingRegistry( return await downloadPackageMigrationsFromRegistry(
packageName, packageName,
packageVersion, packageVersion,
migrationsPath migrationsConfig
); );
} catch {
// fall back to installing the package
const { migrations } = await installPackageAndGetVersionAngMigrations(
packageName,
packageVersion
);
return migrations;
}
} }
function getPackageMigrationsPathFromRegistry( function resolveNxMigrationConfig(json: Partial<PackageJson>) {
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, packageName: string,
packageVersion: string packageVersion: string
): string | null { ): Promise<NxMigrationsConfiguration> {
let pm = detectPackageManager(); const result = await packageRegistryView(
if (pm === 'yarn') { packageName,
pm = 'npm'; packageVersion,
} 'nx-migrations ng-update --json'
const result = execSync( );
`${pm} view ${packageName}@${packageVersion} nx-migrations ng-update --json`,
{
stdio: [],
}
)
.toString()
.trim();
if (!result) { if (!result) {
return null; return null;
} }
const json = JSON.parse(result); return resolveNxMigrationConfig(JSON.parse(result));
let migrationsFilePath = json['nx-migrations'] ?? json['ng-update'] ?? json;
if (typeof json === 'object') {
migrationsFilePath = migrationsFilePath.migrations;
}
return migrationsFilePath;
} }
async function getPackageMigrationsUsingRegistry( async function downloadPackageMigrationsFromRegistry(
packageName: string, packageName: string,
packageVersion: string, packageVersion: string,
migrationsFilePath: string { migrations: migrationsFilePath, packageGroup }: NxMigrationsConfiguration
) { ): Promise<ResolvedMigrationConfiguration> {
const dir = dirSync().name; const dir = createTempNpmDirectory();
createNPMRC(dir);
let pm = detectPackageManager(); let result: ResolvedMigrationConfiguration;
if (pm === 'yarn') {
pm = 'npm';
}
const tarballPath = execSync(`${pm} pack ${packageName}@${packageVersion}`, {
cwd: dir,
stdio: [],
})
.toString()
.trim();
let migrations = null;
migrationsFilePath = join('package', migrationsFilePath);
const migrationDestinationPath = join(dir, migrationsFilePath);
try { try {
await extractFileFromTarball( const { tarballPath } = await packageRegistryPack(
join(dir, tarballPath), dir,
migrationsFilePath, packageName,
migrationDestinationPath packageVersion
); );
migrations = readJsonFile(migrationDestinationPath); const migrations = await extractFileFromTarball(
join(dir, tarballPath),
join('package', migrationsFilePath),
join(dir, migrationsFilePath)
).then((path) => readJsonFile<MigrationsJson>(path));
result = { ...migrations, packageGroup, version: packageVersion };
} catch { } catch {
throw new Error( throw new Error(
`Failed to find migrations file "${migrationsFilePath}" in package "${packageName}@${packageVersion}".` `Failed to find migrations file "${migrationsFilePath}" in package "${packageName}@${packageVersion}".`
); );
} } finally {
try { try {
removeSync(dir); await remove(dir);
} catch { } catch {
// It's okay if this fails, the OS will clean it up eventually // 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, packageName: string,
packageVersion: string packageVersion: string
) { ): Promise<ResolvedMigrationConfiguration> {
const dir = dirSync().name; const dir = createTempNpmDirectory();
createNPMRC(dir);
let result: ResolvedMigrationConfiguration;
try {
const pmc = getPackageManagerCommand(); const pmc = getPackageManagerCommand();
execSync(`${pmc.add} ${packageName}@${packageVersion}`, {
stdio: [], await execAsync(`${pmc.add} ${packageName}@${packageVersion}`, {
cwd: dir, cwd: dir,
}); });
const packageJsonPath = require.resolve(`${packageName}/package.json`, { const {
paths: [dir], migrations: migrationsFilePath,
}); packageGroup,
const { version: resolvedVersion } = readJsonFile(packageJsonPath); packageJson,
} = readPackageMigrationConfig(packageName, dir);
const migrationsFilePath = packageToMigrationsFilePath(packageName, dir); let migrations: MigrationsJson = undefined;
let migrations = null;
if (migrationsFilePath) { if (migrationsFilePath) {
migrations = readJsonFile(migrationsFilePath); migrations = readJsonFile<MigrationsJson>(migrationsFilePath);
} }
result = { ...migrations, packageGroup, version: packageJson.version };
} finally {
try { try {
removeSync(dir); await remove(dir);
} catch { } catch {
// It's okay if this fails, the OS will clean it up eventually // It's okay if this fails, the OS will clean it up eventually
} }
return { migrations, resolvedVersion };
}
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`);
} }
return result;
} }
function packageToMigrationsFilePath(packageName: string, dir: string) { interface PackageMigrationConfig extends NxMigrationsConfiguration {
packageJson: PackageJson;
}
function readPackageMigrationConfig(
packageName: string,
dir: string
): PackageMigrationConfig {
const packageJsonPath = require.resolve(`${packageName}/package.json`, { const packageJsonPath = require.resolve(`${packageName}/package.json`, {
paths: [dir], paths: [dir],
}); });
const json = readJsonFile(packageJsonPath);
let migrationsFile = json['nx-migrations'] || json['ng-update'];
// migrationsFile is an object const json = readJsonFile<PackageJson>(packageJsonPath);
if (migrationsFile && migrationsFile.migrations) { const migrationConfigOrFile = json['nx-migrations'] || json['ng-update'];
migrationsFile = migrationsFile.migrations;
if (!migrationConfigOrFile) {
return { packageJson: json, migrations: null, packageGroup: [] };
} }
const migrationsConfig =
typeof migrationConfigOrFile === 'string'
? {
migrations: migrationConfigOrFile,
packageGroup: [],
}
: migrationConfigOrFile;
try { try {
if (migrationsFile && typeof migrationsFile === 'string') { const migrationFile = require.resolve(migrationsConfig.migrations, {
return require.resolve(migrationsFile, {
paths: [dirname(packageJsonPath)], paths: [dirname(packageJsonPath)],
}); });
} else {
return null; return {
} packageJson: json,
migrations: migrationFile,
packageGroup: migrationsConfig.packageGroup,
};
} catch { } catch {
return null; return {
packageJson: json,
migrations: null,
packageGroup: migrationsConfig.packageGroup,
};
} }
} }
@ -680,28 +714,30 @@ function createMigrationsFile(
function updatePackageJson( function updatePackageJson(
root: string, root: string,
updatedPackages: { updatedPackages: Record<string, PackageJsonUpdateForPackage>
[p: string]: { version: string; addToPackageJson: Dependencies | false };
}
) { ) {
const packageJsonPath = join(root, 'package.json'); const packageJsonPath = join(root, 'package.json');
const parseOptions: JsonReadOptions = {}; const parseOptions: JsonReadOptions = {};
const json = readJsonFile(packageJsonPath, parseOptions); const json = readJsonFile(packageJsonPath, parseOptions);
Object.keys(updatedPackages).forEach((p) => { 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]) {
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; json.devDependencies[p] = updatedPackages[p].version;
return;
} }
if (json.dependencies?.[p]) {
json.dependencies[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, { writeJsonFile(packageJsonPath, json, {
appendNewLine: parseOptions.endsWithNewline, appendNewLine: parseOptions.endsWithNewline,
}); });
@ -720,18 +756,21 @@ async function generateMigrationsJsonAndUpdatePackageJson(
try { try {
logger.info(`Fetching meta data about packages.`); logger.info(`Fetching meta data about packages.`);
logger.info(`It may take a few minutes.`); logger.info(`It may take a few minutes.`);
const originalPackageJson = readJsonFile(join(root, 'package.json')); const originalPackageJson = readJsonFile(join(root, 'package.json'));
const migrator = new Migrator({ const migrator = new Migrator({
packageJson: originalPackageJson, packageJson: originalPackageJson,
versions: versions(root, opts.from), versions: versions(root, opts.from),
fetch: createFetcher(), fetch: createFetcher(),
from: opts.from,
to: opts.to, to: opts.to,
}); });
const { migrations, packageJson } = await migrator.updatePackageJson( const { migrations, packageJson } = await migrator.updatePackageJson(
opts.targetPackage, opts.targetPackage,
opts.targetVersion opts.targetVersion
); );
updatePackageJson(root, packageJson); updatePackageJson(root, packageJson);
if (migrations.length > 0) { if (migrations.length > 0) {
@ -752,13 +791,14 @@ async function generateMigrationsJsonAndUpdatePackageJson(
`- Make sure package.json changes make sense and then run '${pmc.install}'` `- Make sure package.json changes make sense and then run '${pmc.install}'`
); );
if (migrations.length > 0) { 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`); logger.info(`- To learn more go to https://nx.dev/using-nx/updating-nx`);
if (showConnectToCloudMessage()) { if (showConnectToCloudMessage()) {
const cmd = pmc.run('nx', 'connect-to-nx-cloud');
logger.info( 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) { } catch (e) {
@ -807,7 +847,7 @@ async function runMigrations(
cli?: 'nx' | 'angular'; cli?: 'nx' | 'angular';
}[] = readJsonFile(join(root, opts.runMigrations)).migrations; }[] = readJsonFile(join(root, opts.runMigrations)).migrations;
for (let m of migrations) { for (const m of migrations) {
logger.info(`Running migration ${m.name}`); logger.info(`Running migration ${m.name}`);
if (m.cli === 'nx') { if (m.cli === 'nx') {
await runNxMigration(root, m.package, m.name); await runNxMigration(root, m.package, m.name);
@ -826,12 +866,16 @@ async function runMigrations(
} }
async function runNxMigration(root: string, packageName: string, name: string) { async function runNxMigration(root: string, packageName: string, name: string) {
const collectionPath = packageToMigrationsFilePath(packageName, root); const collectionPath = readPackageMigrationConfig(
const collection = readJsonFile(collectionPath); packageName,
root
).migrations;
const collection = readJsonFile<MigrationsJson>(collectionPath);
const g = collection.generators || collection.schematics; const g = collection.generators || collection.schematics;
const implRelativePath = g[name].implementation || g[name].factory; const implRelativePath = g[name].implementation || g[name].factory;
let implPath; let implPath: string;
try { try {
implPath = require.resolve(implRelativePath, { implPath = require.resolve(implRelativePath, {

View File

@ -119,7 +119,7 @@ export async function extractFileFromTarball(
file: string, file: string,
destinationFilePath: string destinationFilePath: string
) { ) {
return new Promise<void>((resolve, reject) => { return new Promise<string>((resolve, reject) => {
ensureDirSync(dirname(destinationFilePath)); ensureDirSync(dirname(destinationFilePath));
var tarExtractStream = tar.extract(); var tarExtractStream = tar.extract();
const destinationFileStream = createWriteStream(destinationFilePath); const destinationFileStream = createWriteStream(destinationFilePath);
@ -130,7 +130,7 @@ export async function extractFileFromTarball(
stream.pipe(destinationFileStream); stream.pipe(destinationFileStream);
stream.on('end', () => { stream.on('end', () => {
isFileExtracted = true; isFileExtracted = true;
resolve(); resolve(destinationFilePath);
}); });
} }

View File

@ -9,9 +9,15 @@ export interface NxProjectPackageJsonConfiguration {
targets?: Record<string, PackageJsonTargetConfiguration>; targets?: Record<string, PackageJsonTargetConfiguration>;
} }
export interface NxMigrationsConfiguration {
migrations?: string;
packageGroup?: (string | { package: string; version: string })[];
}
export interface PackageJson { export interface PackageJson {
// Generic Package.Json Configuration // Generic Package.Json Configuration
name: string; name: string;
version: string;
scripts?: Record<string, string>; scripts?: Record<string, string>;
dependencies?: Record<string, string>; dependencies?: Record<string, string>;
devDependencies?: Record<string, string>; devDependencies?: Record<string, string>;
@ -30,7 +36,8 @@ export interface PackageJson {
schematics?: string; schematics?: string;
builders?: string; builders?: string;
executors?: string; executors?: string;
'nx-migrations'?: string; 'nx-migrations'?: string | NxMigrationsConfiguration;
'ng-update'?: string | NxMigrationsConfiguration;
} }
export function buildTargetFromScript( export function buildTargetFromScript(

View File

@ -1,8 +1,13 @@
import { execSync } from 'child_process'; import { exec, execSync } from 'child_process';
import { copyFileSync, existsSync, unlinkSync } from 'fs'; import { copyFileSync, existsSync } from 'fs';
import { remove } from 'fs-extra';
import { dirname, join } from 'path'; import { dirname, join } from 'path';
import { dirSync } from 'tmp'; import { dirSync } from 'tmp';
import { promisify } from 'util';
import { readJsonFile, writeJsonFile } from './fileutils'; import { readJsonFile, writeJsonFile } from './fileutils';
import { PackageJson } from './package-json';
const execAsync = promisify(exec);
export type PackageManager = 'yarn' | 'pnpm' | 'npm'; export type PackageManager = 'yarn' | 'pnpm' | 'npm';
@ -114,25 +119,37 @@ export function checkForNPMRC(
return existsSync(path) ? path : null; 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 * 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). * NPM registry (when using Yarn it will fall back to NPM to fetch the info).
*/ */
export function resolvePackageVersionUsingRegistry( export async function resolvePackageVersionUsingRegistry(
packageName: string, packageName: string,
version: string version: string
): string { ): Promise<string> {
let pm = detectPackageManager();
if (pm === 'yarn') {
pm = 'npm';
}
try { try {
const result = execSync(`${pm} view ${packageName}@${version} version`, { const result = await packageRegistryView(packageName, version, 'version');
stdio: [],
})
.toString()
.trim();
if (!result) { if (!result) {
throw new Error(`Unable to resolve version ${packageName}@${version}.`); 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 * installing it in a temporary directory and fetching the version from the
* package.json. * package.json.
*/ */
export function resolvePackageVersionUsingInstallation( export async function resolvePackageVersionUsingInstallation(
packageName: string, packageName: string,
version: string version: string
): string { ): Promise<string> {
const dir = dirSync().name; const dir = createTempNpmDirectory();
const npmrc = checkForNPMRC();
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(); const pmc = getPackageManagerCommand();
execSync(`${pmc.add} ${packageName}@${version}`, { stdio: [], cwd: dir }); await execAsync(`${pmc.add} ${packageName}@${version}`, { cwd: dir });
const packageJsonPath = require.resolve(`${packageName}/package.json`, { const packageJsonPath = require.resolve(`${packageName}/package.json`, {
paths: [dir], paths: [dir],
}); });
const { version: resolvedVersion } = readJsonFile(packageJsonPath);
return readJsonFile<PackageJson>(packageJsonPath).version;
} finally {
try { try {
unlinkSync(dir); await remove(dir);
} catch { } catch {
// It's okay if this fails, the OS will clean it up eventually // It's okay if this fails, the OS will clean it up eventually
} }
}
return resolvedVersion; }
export async function packageRegistryView(
pkg: string,
version: string,
args: string
): Promise<string> {
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 { stdout } = await execAsync(`${pm} view ${pkg}@${version} ${args}`);
return stdout.toString().trim();
}
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';
}
const { stdout } = await execAsync(`${pm} pack ${pkg}@${version}`, { cwd });
const tarballPath = stdout.trim();
return { tarballPath };
} }

View File

@ -107,6 +107,7 @@ describe('project graph utils', () => {
describe('mergeNpmScriptsWithTargets', () => { describe('mergeNpmScriptsWithTargets', () => {
const packageJson: PackageJson = { const packageJson: PackageJson = {
name: 'my-app', name: 'my-app',
version: '0.0.0',
scripts: { scripts: {
build: 'echo 1', build: 'echo 1',
}, },

View File

@ -47,7 +47,12 @@
"@nrwl/nx-plugin", "@nrwl/nx-plugin",
"@nrwl/react", "@nrwl/react",
"@nrwl/storybook", "@nrwl/storybook",
"@nrwl/web" "@nrwl/web",
"@nrwl/js",
"@nrwl/cli",
"@nrwl/nx-cloud",
"@nrwl/react-native",
"@nrwl/detox"
] ]
}, },
"peerDependencies": { "peerDependencies": {
@ -82,6 +87,32 @@
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
"nx-migrations": { "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"
}
]
} }
} }