diff --git a/docs/generated/cli/migrate.md b/docs/generated/cli/migrate.md index 0ca003d2114869..db495f20cc2003 100644 --- a/docs/generated/cli/migrate.md +++ b/docs/generated/cli/migrate.md @@ -35,7 +35,7 @@ Update @nrwl/workspace to "9.0.0". This will update other packages and will gene nx migrate 9.0.0 ``` -Update @nrwl/workspace and generate the list of migrations starting with version 8.0.0 of @nrwl/workspace and @nrwl/node, regardless of what installed locally: +Update @nrwl/workspace and generate the list of migrations starting with version 8.0.0 of @nrwl/workspace and @nrwl/node, regardless of what is installed locally: ```shell nx migrate @nrwl/workspace@9.0.0 --from="@nrwl/workspace@8.0.0,@nrwl/node@8.0.0" @@ -59,6 +59,12 @@ Collect package updates and migrations in interactive mode. In this mode, the us nx migrate latest --interactive ``` +Collect package updates and migrations starting with version 14.5.0 of "nx" (and Nx first-party plugins), regardless of what is installed locally, while skipping migrations that were meant to be applied on previous updates: + +```shell + nx migrate latest --from=nx@14.5.0 --skip-applied-migrations +``` + Run migrations from the provided migrations.json file. You can modify migrations.json and run this command many times: ```shell @@ -121,6 +127,14 @@ Type: `string` Execute migrations from a file (when the file isn't provided, execute migrations from migrations.json) +### skipAppliedMigrations + +Type: `boolean` + +Default: `false` + +Skip collecting migrations that were meant to be applied on previous updates. To be used with --from + ### to Type: `string` diff --git a/docs/generated/packages/nx/documents/migrate.md b/docs/generated/packages/nx/documents/migrate.md index 0ca003d2114869..db495f20cc2003 100644 --- a/docs/generated/packages/nx/documents/migrate.md +++ b/docs/generated/packages/nx/documents/migrate.md @@ -35,7 +35,7 @@ Update @nrwl/workspace to "9.0.0". This will update other packages and will gene nx migrate 9.0.0 ``` -Update @nrwl/workspace and generate the list of migrations starting with version 8.0.0 of @nrwl/workspace and @nrwl/node, regardless of what installed locally: +Update @nrwl/workspace and generate the list of migrations starting with version 8.0.0 of @nrwl/workspace and @nrwl/node, regardless of what is installed locally: ```shell nx migrate @nrwl/workspace@9.0.0 --from="@nrwl/workspace@8.0.0,@nrwl/node@8.0.0" @@ -59,6 +59,12 @@ Collect package updates and migrations in interactive mode. In this mode, the us nx migrate latest --interactive ``` +Collect package updates and migrations starting with version 14.5.0 of "nx" (and Nx first-party plugins), regardless of what is installed locally, while skipping migrations that were meant to be applied on previous updates: + +```shell + nx migrate latest --from=nx@14.5.0 --skip-applied-migrations +``` + Run migrations from the provided migrations.json file. You can modify migrations.json and run this command many times: ```shell @@ -121,6 +127,14 @@ Type: `string` Execute migrations from a file (when the file isn't provided, execute migrations from migrations.json) +### skipAppliedMigrations + +Type: `boolean` + +Default: `false` + +Skip collecting migrations that were meant to be applied on previous updates. To be used with --from + ### to Type: `string` diff --git a/packages/nx/src/command-line/examples.ts b/packages/nx/src/command-line/examples.ts index e3a26134b669a0..35e586ded959c9 100644 --- a/packages/nx/src/command-line/examples.ts +++ b/packages/nx/src/command-line/examples.ts @@ -330,7 +330,7 @@ export const examples: Record = { command: 'migrate @nrwl/workspace@9.0.0 --from="@nrwl/workspace@8.0.0,@nrwl/node@8.0.0"', description: - 'Update @nrwl/workspace and generate the list of migrations starting with version 8.0.0 of @nrwl/workspace and @nrwl/node, regardless of what installed locally', + 'Update @nrwl/workspace and generate the list of migrations starting with version 8.0.0 of @nrwl/workspace and @nrwl/node, regardless of what is installed locally', }, { command: @@ -348,6 +348,11 @@ export const examples: Record = { description: 'Collect package updates and migrations in interactive mode. In this mode, the user will be prompted whether to apply any optional package update and migration', }, + { + command: 'migrate latest --from=nx@14.5.0 --skip-applied-migrations', + description: + 'Collect package updates and migrations starting with version 14.5.0 of "nx" (and Nx first-party plugins), regardless of what is installed locally, while skipping migrations that were meant to be applied on previous updates', + }, { command: 'migrate --run-migrations=migrations.json', description: diff --git a/packages/nx/src/command-line/migrate.spec.ts b/packages/nx/src/command-line/migrate.spec.ts index cd4d2ae18c261d..2c5eb5abe653ad 100644 --- a/packages/nx/src/command-line/migrate.spec.ts +++ b/packages/nx/src/command-line/migrate.spec.ts @@ -1365,6 +1365,112 @@ describe('Migration', () => { }, }); }); + + it('should generate the correct migrations when "--only-skipped-migrations"', async () => { + const migrator = new Migrator({ + packageJson: createPackageJson({ + dependencies: { + parent: '1.0.0', + pkg1: '1.0.0', + }, + }), + getInstalledPackageVersion: (p, overrides) => overrides?.[p] ?? '1.0.0', + fetch: (p) => { + if (p === 'parent') { + return Promise.resolve({ + version: '2.0.0', + packageGroup: [{ package: 'pkg1', version: '*' }], + generators: { + // previous migration + migration1: { + version: '1.0.0', + description: 'migration1 desc', + requires: { + // didn't meet requirements and now meets requirements, should collect it + pkg1: '>=2.0.0', + }, + }, + // previous migration + migration2: { + version: '1.0.0', + description: 'migration2 desc', + requires: { + // didn't meet requirements and now doesn't meet requirements, should not collect it + pkg1: '>=3.0.0', + }, + }, + // previous migration, no requirements, should not collect it + migration3: { + version: '1.0.0', + description: 'migration3 desc', + }, + // new migration + migration4: { + version: '2.0.0', + description: 'migration4 desc', + requires: { + // meets requirements, should collect it + pkg1: '>=2.0.0', + }, + }, + // new migration + migration5: { + version: '2.0.0', + description: 'migration5 desc', + requires: { + // doesn't meet requirements, should not collect it + pkg1: '>=3.0.0', + }, + }, + // new migrationg, no requirements, should collect it + migration6: { + version: '2.0.0', + description: 'migration6 desc', + }, + }, + }); + } else if (p === 'pkg1') { + return Promise.resolve({ version: '2.0.0' }); + } else { + return Promise.resolve(null); + } + }, + from: { parent: '0.1.0' }, + to: {}, + skipAppliedMigrations: true, + }); + + const result = await migrator.updatePackageJson('parent', '2.0.0'); + + expect(result).toEqual({ + migrations: [ + { + version: '1.0.0', + name: 'migration1', + package: 'parent', + description: 'migration1 desc', + requires: { pkg1: '>=2.0.0' }, + }, + { + version: '2.0.0', + name: 'migration4', + package: 'parent', + description: 'migration4 desc', + requires: { pkg1: '>=2.0.0' }, + }, + { + version: '2.0.0', + name: 'migration6', + package: 'parent', + description: 'migration6 desc', + }, + ], + packageJson: { + parent: { version: '2.0.0', addToPackageJson: false }, + pkg1: { version: '2.0.0', addToPackageJson: false }, + }, + }); + }); }); describe('normalizeVersions', () => { diff --git a/packages/nx/src/command-line/migrate.ts b/packages/nx/src/command-line/migrate.ts index c4c4f0302a9d27..db3480e54c8103 100644 --- a/packages/nx/src/command-line/migrate.ts +++ b/packages/nx/src/command-line/migrate.ts @@ -16,6 +16,7 @@ import { import { promisify } from 'util'; import { MigrationsJson, + MigrationsJsonEntry, PackageJsonUpdateForPackage as PackageUpdate, PackageJsonUpdates, } from '../config/misc-interfaces'; @@ -31,7 +32,6 @@ import { logger } from '../utils/logger'; import { ArrayPackageGroup, NxMigrationsConfiguration, - PackageGroup, PackageJson, readModulePackageJson, readNxMigrateConfig, @@ -100,7 +100,7 @@ export interface MigratorOptions { packageJson: PackageJson; getInstalledPackageVersion: ( pkg: string, - overrides: Record + overrides?: Record ) => string; fetch: ( pkg: string, @@ -109,6 +109,7 @@ export interface MigratorOptions { from: { [pkg: string]: string }; to: { [pkg: string]: string }; interactive?: boolean; + skipAppliedMigrations?: boolean; } export class Migrator { @@ -118,6 +119,7 @@ export class Migrator { private readonly installedPkgVersionOverrides: MigratorOptions['from']; private readonly to: MigratorOptions['to']; private readonly interactive: MigratorOptions['interactive']; + private readonly skipAppliedMigrations: MigratorOptions['skipAppliedMigrations']; private readonly packageUpdates: Record = {}; private readonly collectedVersions: Record = {}; @@ -128,6 +130,7 @@ export class Migrator { this.installedPkgVersionOverrides = opts.from; this.to = opts.to; this.interactive = opts.interactive; + this.skipAppliedMigrations = opts.skipAppliedMigrations; } async migrate(targetPackage: string, targetVersion: string) { @@ -157,7 +160,7 @@ export class Migrator { migration.version && this.gt(migration.version, currentVersion) && this.lte(migration.version, version) && - this.areRequirementsMet(migration.requires) + this.areMigrationRequirementsMet(packageName, migration) ) .map(([migrationName, migration]) => ({ ...migration, @@ -218,7 +221,7 @@ export class Migrator { } if (!this.getPkgVersion(targetPackage)) { - this.addPackageJsonUpdate(targetPackage, { + this.addPackageUpdate(targetPackage, { version: target.version, addToPackageJson: target.addToPackageJson || false, }); @@ -247,6 +250,11 @@ export class Migrator { } this.collectedVersions[targetPackage] = targetVersion; + this.addPackageUpdate(targetPackage, { + version: migrationConfig.version, + addToPackageJson: target.addToPackageJson || false, + }); + const { packageJsonUpdates, packageGroupOrder } = this.getPackageJsonUpdatesFromMigrationConfig( targetPackage, @@ -254,10 +262,9 @@ export class Migrator { migrationConfig ); - this.addPackageJsonUpdate(targetPackage, { - version: migrationConfig.version, - addToPackageJson: target.addToPackageJson || false, - }); + if (!packageJsonUpdates.length) { + return []; + } const shouldCheckUpdates = packageJsonUpdates.some( (packageJsonUpdate) => @@ -424,10 +431,7 @@ export class Migrator { return filteredPackageJsonUpdates; } - private addPackageJsonUpdate( - name: string, - packageUpdate: PackageUpdate - ): void { + private addPackageUpdate(name: string, packageUpdate: PackageUpdate): void { if ( !this.packageUpdates[name] || this.gt(packageUpdate.version, this.packageUpdates[name].version) @@ -463,6 +467,55 @@ export class Migrator { ); } + private areMigrationRequirementsMet( + packageName: string, + migration: MigrationsJsonEntry + ): boolean { + if (!this.skipAppliedMigrations) { + return this.areRequirementsMet(migration.requires); + } + + return ( + (this.wasMigrationSkipped(migration.requires) || + this.isMigrationForHigherVersionThanWhatIsInstalled( + packageName, + migration + )) && + this.areRequirementsMet(migration.requires) + ); + } + + private isMigrationForHigherVersionThanWhatIsInstalled( + packageName: string, + migration: MigrationsJsonEntry + ): boolean { + const installedVersion = this.getInstalledPackageVersion(packageName); + + return ( + migration.version && + (!installedVersion || this.gt(migration.version, installedVersion)) && + this.lte(migration.version, this.packageUpdates[packageName].version) + ); + } + + private wasMigrationSkipped( + requirements: PackageJsonUpdates[string]['requires'] + ): boolean { + // no requiremets, so it ran before + if (!requirements || !Object.keys(requirements).length) { + return false; + } + + // at least a requirement was not met, it was skipped + return Object.entries(requirements).some( + ([pkgName, versionRange]) => + !this.getInstalledPackageVersion(pkgName) || + !satisfies(this.getInstalledPackageVersion(pkgName), versionRange, { + includePrerelease: true, + }) + ); + } + private async runPackageJsonUpdatesConfirmationPrompt( confirmationPrompt: string ): Promise { @@ -604,6 +657,7 @@ type GenerateMigrations = { from: { [k: string]: string }; to: { [k: string]: string }; interactive?: boolean; + skipAppliedMigrations?: boolean; }; type RunMigrations = { type: 'runMigrations'; runMigrations: string }; @@ -623,14 +677,14 @@ export function parseMigrationsOptions(options: { const { targetPackage, targetVersion } = parseTargetPackageAndVersion( options['packageAndVersion'] ); - const interactive = options.interactive; return { type: 'generateMigrations', targetPackage: normalizeSlashes(targetPackage), targetVersion, from, to, - interactive, + interactive: options.interactive, + skipAppliedMigrations: options.skipAppliedMigrations, }; } else { return { @@ -647,10 +701,10 @@ function createInstalledPackageVersionsResolver( function getInstalledPackageVersion( packageName: string, - overrides: Record + overrides?: Record ): string | null { try { - if (overrides[packageName]) { + if (overrides?.[packageName]) { return overrides[packageName]; } @@ -1051,6 +1105,7 @@ async function generateMigrationsJsonAndUpdatePackageJson( from: opts.from, to: opts.to, interactive: opts.interactive, + skipAppliedMigrations: opts.skipAppliedMigrations, }); const { migrations, packageUpdates } = await migrator.migrate( diff --git a/packages/nx/src/command-line/nx-commands.ts b/packages/nx/src/command-line/nx-commands.ts index 2aff48db00e406..9d63150d876f6e 100644 --- a/packages/nx/src/command-line/nx-commands.ts +++ b/packages/nx/src/command-line/nx-commands.ts @@ -959,12 +959,23 @@ function withMigrationOptions(yargs: yargs.Argv) { type: 'boolean', default: false, }) - .check(({ createCommits, commitPrefix }) => { + .option('skipAppliedMigrations', { + describe: + 'Skip collecting migrations that were meant to be applied on previous updates. To be used with --from', + type: 'boolean', + default: false, + }) + .check(({ createCommits, commitPrefix, from, skipAppliedMigrations }) => { if (!createCommits && commitPrefix !== defaultCommitPrefix) { throw new Error( 'Error: Providing a custom commit prefix requires --create-commits to be enabled' ); } + if (skipAppliedMigrations && !from) { + throw new Error( + 'Error: Skipping migrations that were previously applied requires --from to be set' + ); + } return true; }); }