nx
Version:
1,023 lines • 134 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChangedDepInstaller = exports.isHybridMigration = exports.isPromptOnlyMigration = exports.filterDowngradedUpdates = exports.Migrator = exports.normalizeVersion = void 0;
exports.formatCommandFailure = formatCommandFailure;
exports.resolveCanonicalNxPackage = resolveCanonicalNxPackage;
exports.resolveInclude = resolveInclude;
exports.parseMigrationsOptions = parseMigrationsOptions;
exports.createFetcher = createFetcher;
exports.isNpmPeerDepsError = isNpmPeerDepsError;
exports.resolveAgenticRunId = resolveAgenticRunId;
exports.formatSkippedPromptsNextStep = formatSkippedPromptsNextStep;
exports.resolveCreateCommits = resolveCreateCommits;
exports.resolveShouldRunValidation = resolveShouldRunValidation;
exports.executeMigrations = executeMigrations;
exports.runNxOrAngularMigration = runNxOrAngularMigration;
exports.parseMigrationReturn = parseMigrationReturn;
exports.migrate = migrate;
exports.runMigration = runMigration;
exports.readMigrationCollection = readMigrationCollection;
exports.getImplementationPath = getImplementationPath;
exports.resolveMigrationForRun = resolveMigrationForRun;
exports.resolveDocumentationFileToWorkspacePath = resolveDocumentationFileToWorkspacePath;
exports.nxCliPath = nxCliPath;
const tslib_1 = require("tslib");
const pc = tslib_1.__importStar(require("picocolors"));
const child_process_1 = require("child_process");
const safe_prompt_1 = require("./safe-prompt");
const handle_import_1 = require("../../utils/handle-import");
const path_1 = require("path");
const module_1 = require("module");
const path_2 = require("../../utils/path");
const semver_1 = require("semver");
const node_url_1 = require("node:url");
const util_1 = require("util");
const tree_1 = require("../../generators/tree");
const fileutils_1 = require("../../utils/fileutils");
const tar_1 = require("../../utils/tar");
const write_formatted_json_file_1 = require("../../utils/write-formatted-json-file");
const logger_1 = require("../../utils/logger");
const git_utils_1 = require("../../utils/git-utils");
const package_json_1 = require("../../utils/package-json");
const package_manager_1 = require("../../utils/package-manager");
const errors_1 = require("../../utils/min-release-age/errors");
const resolve_package_version_1 = require("./resolve-package-version");
const handle_errors_1 = require("../../utils/handle-errors");
const connect_to_nx_cloud_1 = require("../nx-cloud/connect/connect-to-nx-cloud");
const output_1 = require("../../utils/output");
const fs_1 = require("fs");
const workspace_root_1 = require("../../utils/workspace-root");
const is_ci_1 = require("../../utils/is-ci");
const installation_directory_1 = require("../../utils/installation-directory");
const installed_nx_version_1 = require("../../utils/installed-nx-version");
const configuration_1 = require("../../config/configuration");
const child_process_2 = require("../../utils/child-process");
const client_1 = require("../../daemon/client/client");
const nx_cloud_utils_1 = require("../../utils/nx-cloud-utils");
const project_graph_1 = require("../../project-graph/project-graph");
const format_changed_files_with_prettier_if_available_1 = require("../../generators/internal-utils/format-changed-files-with-prettier-if-available");
const provenance_1 = require("../../utils/provenance");
const catalog_1 = require("../../utils/catalog");
const migrate_analytics_1 = require("./migrate-analytics");
const multi_major_1 = require("./multi-major");
const prompt_files_1 = require("./prompt-files");
const command_object_1 = require("./command-object");
const migrate_config_1 = require("./migrate-config");
const handoff_gitignore_1 = require("./agentic/handoff-gitignore");
const migrate_commits_1 = require("./migrate-commits");
const migrate_output_1 = require("./migrate-output");
const migration_shape_1 = require("./migration-shape");
Object.defineProperty(exports, "isHybridMigration", { enumerable: true, get: function () { return migration_shape_1.isHybridMigration; } });
Object.defineProperty(exports, "isPromptOnlyMigration", { enumerable: true, get: function () { return migration_shape_1.isPromptOnlyMigration; } });
const update_filters_1 = require("./update-filters");
Object.defineProperty(exports, "filterDowngradedUpdates", { enumerable: true, get: function () { return update_filters_1.filterDowngradedUpdates; } });
const version_utils_1 = require("./version-utils");
Object.defineProperty(exports, "normalizeVersion", { enumerable: true, get: function () { return version_utils_1.normalizeVersion; } });
const execAsync = (0, util_1.promisify)(child_process_1.exec);
function formatCommandFailure(command, error) {
const normalizeCommandOutput = (output) => {
if (!output) {
return undefined;
}
const normalized = typeof output === 'string' ? output.trim() : output.toString().trim();
return normalized || undefined;
};
const details = normalizeCommandOutput(error.stderr) ||
normalizeCommandOutput(error.stdout) ||
normalizeCommandOutput(error.message)
?.replace(`Command failed: ${command}`, '')
.trim();
return [`Command failed: ${command}`, ...(details ? [details] : [])].join('\n');
}
function runOrReturnExitCode(run) {
try {
run();
return 0;
}
catch (e) {
if (typeof e === 'object' &&
e !== null &&
'status' in e &&
typeof e.status === 'number') {
return e.status;
}
throw e;
}
}
function cleanSemver(version) {
return (0, semver_1.clean)(version) ?? (0, semver_1.coerce)(version);
}
function normalizeSlashes(packageName) {
return packageName.replace(/\\/g, '/');
}
class Migrator {
constructor(opts) {
this.packageUpdates = {};
this.collectedVersions = {};
this.promptAnswers = {};
if ((opts.include === 'required' || opts.include === 'optional') &&
!opts.requiredPackages) {
throw new Error(`Error: 'requiredPackages' is required when 'include' is '${opts.include}'.`);
}
this.packageJson = opts.packageJson;
this.nxInstallation = opts.nxInstallation;
this.getInstalledPackageVersion = opts.getInstalledPackageVersion;
this.fetch = opts.fetch;
this.installedPkgVersionOverrides = opts.from;
this.to = opts.to;
this.interactive = opts.interactive;
this.excludeAppliedMigrations = opts.excludeAppliedMigrations;
this.include = opts.include;
this.requiredPackages = opts.requiredPackages;
}
async fetchMigrationConfig(packageName, packageVersion) {
const migrationConfig = await this.fetch(packageName, packageVersion);
if (!migrationConfig.version) {
throw new Error(`Fetched migration metadata for ${packageName} is invalid: the target version is missing.`);
}
return migrationConfig;
}
async migrate(targetPackage, targetVersion) {
await this.buildPackageJsonUpdates(targetPackage, {
version: targetVersion,
addToPackageJson: false,
});
this.applyIncludeFilter();
const { migrations, promptContents } = await this.createMigrateJson();
return {
packageUpdates: this.packageUpdates,
migrations,
...(Object.keys(promptContents).length > 0 ? { promptContents } : {}),
minVersionWithSkippedUpdates: this.minVersionWithSkippedUpdates,
};
}
async createMigrateJson() {
const promptContents = {};
const migrations = await Promise.all(Object.keys(this.packageUpdates).map(async (packageName) => {
if (this.packageUpdates[packageName].ignoreMigrations) {
return [];
}
const currentVersion = this.getPkgVersion(packageName);
if (currentVersion === null)
return [];
const { version } = this.packageUpdates[packageName];
const { generators: migrationEntries, resolvedPromptFiles } = await this.fetchMigrationConfig(packageName, version);
if (!migrationEntries)
return [];
if (resolvedPromptFiles) {
for (const [promptPath, content] of Object.entries(resolvedPromptFiles)) {
promptContents[(0, prompt_files_1.promptContentKey)(packageName, promptPath)] = content;
}
}
return Object.entries(migrationEntries)
.filter(([, migration]) => migration.version &&
this.gt(migration.version, currentVersion) &&
this.lte(migration.version, version) &&
this.areMigrationRequirementsMet(packageName, migration))
.map(([migrationName, migration]) => ({
...migration,
package: packageName,
name: migrationName,
}));
}));
return { migrations: migrations.flat(), promptContents };
}
async buildPackageJsonUpdates(targetPackage, target) {
const packagesToCheck = await this.populatePackageJsonUpdatesAndGetPackagesToCheck(targetPackage, target);
for (const packageToCheck of packagesToCheck) {
const filteredUpdates = {};
for (const [packageUpdateKey, packageUpdate] of Object.entries(packageToCheck.updates)) {
if (this.areRequirementsMet(packageUpdate.requires) &&
!this.areIncompatiblePackagesPresent(packageUpdate.incompatibleWith) &&
(!this.interactive ||
(await this.runPackageJsonUpdatesConfirmationPrompt(packageUpdate, packageUpdateKey, packageToCheck.package)))) {
Object.entries(packageUpdate.packages).forEach(([name, update]) => {
this.validatePackageUpdateVersion(packageToCheck.package, name, update);
filteredUpdates[name] = update;
this.packageUpdates[name] = update;
});
}
}
await Promise.all(Object.entries(filteredUpdates).map(([name, update]) => this.buildPackageJsonUpdates(name, update)));
}
}
async populatePackageJsonUpdatesAndGetPackagesToCheck(targetPackage, target) {
let targetVersion = target.version;
if (this.to[targetPackage]) {
targetVersion = this.to[targetPackage];
}
if (!this.getPkgVersion(targetPackage)) {
this.addPackageUpdate(targetPackage, {
version: target.version,
addToPackageJson: target.addToPackageJson || false,
...(target.ignoreMigrations && { ignoreMigrations: true }),
});
return [];
}
let migrationConfig;
try {
migrationConfig = await this.fetchMigrationConfig(targetPackage, targetVersion);
}
catch (e) {
// A cooldown violation must keep its type so the top-level handler can
// surface its remediation; only a generic "no matching version" earns the
// --to hint.
if (!(e instanceof errors_1.MinReleaseAgeViolationError) &&
e?.message?.includes('No matching version')) {
throw new Error(`${e.message}\nRun migrate with --to="package1@version1,package2@version2"`);
}
else {
throw e;
}
}
targetVersion = migrationConfig.version;
if (this.collectedVersions[targetPackage] &&
(0, semver_1.gte)(this.collectedVersions[targetPackage], targetVersion)) {
return [];
}
this.collectedVersions[targetPackage] = targetVersion;
this.addPackageUpdate(targetPackage, {
version: migrationConfig.version,
addToPackageJson: target.addToPackageJson || false,
...(target.ignoreMigrations && { ignoreMigrations: true }),
});
const { packageJsonUpdates, packageGroupOrder } = this.getPackageJsonUpdatesFromMigrationConfig(targetPackage, targetVersion, migrationConfig, target.ignorePackageGroup);
if (!Object.keys(packageJsonUpdates).length) {
return [];
}
const shouldCheckUpdates = Object.values(packageJsonUpdates).some((packageJsonUpdate) => (this.interactive && packageJsonUpdate['x-prompt']) ||
Object.keys(packageJsonUpdate.requires ?? {}).length ||
Object.keys(packageJsonUpdate.incompatibleWith ?? {}).length);
if (shouldCheckUpdates) {
return [{ package: targetPackage, updates: packageJsonUpdates }];
}
const packageUpdatesToApply = Object.values(packageJsonUpdates).reduce((m, c) => ({ ...m, ...c.packages }), {});
return (await Promise.all(Object.entries(packageUpdatesToApply).map(([packageName, packageUpdate]) => {
this.validatePackageUpdateVersion(targetPackage, packageName, packageUpdate);
return this.populatePackageJsonUpdatesAndGetPackagesToCheck(packageName, packageUpdate);
})))
.filter((pkgs) => pkgs.length)
.flat()
.sort((pkgUpdate1, pkgUpdate2) => packageGroupOrder.indexOf(pkgUpdate1.package) -
packageGroupOrder.indexOf(pkgUpdate2.package));
}
getPackageJsonUpdatesFromMigrationConfig(packageName, targetVersion, migrationConfig, ignorePackageGroup) {
const packageGroupOrder = this.getPackageJsonUpdatesFromPackageGroup(packageName, targetVersion, migrationConfig, ignorePackageGroup);
if (!migrationConfig.packageJsonUpdates ||
!this.getPkgVersion(packageName)) {
return { packageJsonUpdates: {}, packageGroupOrder };
}
const packageJsonUpdates = this.filterPackageJsonUpdates(migrationConfig.packageJsonUpdates, packageName, targetVersion);
return { packageJsonUpdates, packageGroupOrder };
}
/**
* Mutates migrationConfig, adding package group updates into packageJsonUpdates section
*
* @param packageName Package which is being migrated
* @param targetVersion Version which is being migrated to
* @param migrationConfig Configuration which is mutated to contain package json updates
* @returns Order of package groups
*/
getPackageJsonUpdatesFromPackageGroup(packageName, targetVersion, migrationConfig, ignorePackageGroup) {
if (ignorePackageGroup) {
return [];
}
const packageGroup = packageName === '@nrwl/workspace' && (0, version_utils_1.isLegacyEra)(targetVersion)
? LEGACY_NRWL_PACKAGE_GROUP
: (migrationConfig.packageGroup ?? []);
let packageGroupOrder = [];
if (packageGroup.length) {
packageGroupOrder = packageGroup.map((packageConfig) => packageConfig.package);
migrationConfig.packageJsonUpdates ??= {};
const packages = {};
migrationConfig.packageJsonUpdates[targetVersion + '--PackageGroup'] = {
version: targetVersion,
packages,
};
for (const packageConfig of packageGroup) {
packages[packageConfig.package] = {
version: packageConfig.version === '*'
? targetVersion
: packageConfig.version,
alwaysAddToPackageJson: false,
};
if (packageConfig.version === '*' &&
this.installedPkgVersionOverrides[packageName]) {
this.installedPkgVersionOverrides[packageConfig.package] ??=
this.installedPkgVersionOverrides[packageName];
}
}
}
return packageGroupOrder;
}
filterPackageJsonUpdates(packageJsonUpdates, packageName, targetVersion) {
const filteredPackageJsonUpdates = {};
for (const [packageJsonUpdateKey, packageJsonUpdate] of Object.entries(packageJsonUpdates)) {
if (!packageJsonUpdate.packages ||
this.lt(packageJsonUpdate.version, this.getPkgVersion(packageName)) ||
this.gt(packageJsonUpdate.version, targetVersion)) {
continue;
}
const dependencies = {
...this.packageJson?.dependencies,
...this.packageJson?.devDependencies,
...this.nxInstallation?.plugins,
...(this.nxInstallation && { nx: this.nxInstallation.version }),
};
const filtered = {};
for (const [packageName, packageUpdate] of Object.entries(packageJsonUpdate.packages)) {
if (this.shouldExcludePackage(packageName)) {
continue;
}
if (this.shouldApplyPackageUpdate(packageUpdate, packageName, dependencies)) {
filtered[packageName] = {
version: packageUpdate.version,
addToPackageJson: packageUpdate.alwaysAddToPackageJson
? typeof packageUpdate.alwaysAddToPackageJson === 'string'
? packageUpdate.alwaysAddToPackageJson
: 'dependencies'
: packageUpdate.addToPackageJson || false,
...(packageUpdate.ignorePackageGroup && {
ignorePackageGroup: true,
}),
...(packageUpdate.ignoreMigrations && {
ignoreMigrations: true,
}),
};
}
}
if (Object.keys(filtered).length) {
packageJsonUpdate.packages = filtered;
filteredPackageJsonUpdates[packageJsonUpdateKey] = packageJsonUpdate;
}
}
return filteredPackageJsonUpdates;
}
shouldExcludePackage(packageName) {
if (!this.requiredPackages) {
return false;
}
if (this.include === 'required') {
return !this.requiredPackages.has(packageName);
}
return false;
}
applyIncludeFilter() {
if (this.include !== 'optional') {
return;
}
// Cascade walks through the required packages so cross-plugin optional
// deps (e.g. typescript managed by @nx/js but used by @nx/angular) get
// surfaced. Drop the required set from the final result here so only
// optional updates land in package.json.
for (const name of Object.keys(this.packageUpdates)) {
if (this.requiredPackages.has(name)) {
delete this.packageUpdates[name];
}
}
}
shouldApplyPackageUpdate(packageUpdate, packageName, dependencies) {
return ((!packageUpdate.ifPackageInstalled ||
this.getPkgVersion(packageUpdate.ifPackageInstalled)) &&
(packageUpdate.alwaysAddToPackageJson ||
packageUpdate.addToPackageJson ||
!!dependencies?.[packageName]) &&
(!this.collectedVersions[packageName] ||
this.gt(packageUpdate.version, this.collectedVersions[packageName])));
}
validatePackageUpdateVersion(sourcePackageName, packageName, packageUpdate) {
if (!packageUpdate.version) {
throw new Error(`Fetched migration metadata for ${sourcePackageName} is invalid: the target version for ${packageName} is missing.`);
}
}
addPackageUpdate(name, packageUpdate) {
if (!this.packageUpdates[name] ||
this.gt(packageUpdate.version, this.packageUpdates[name].version)) {
this.packageUpdates[name] = packageUpdate;
}
}
areRequirementsMet(requirements) {
if (!requirements || !Object.keys(requirements).length) {
return true;
}
return Object.entries(requirements).every(([pkgName, versionRange]) => {
if (this.packageUpdates[pkgName]) {
return (0, semver_1.satisfies)(cleanSemver(this.packageUpdates[pkgName].version), versionRange, { includePrerelease: true });
}
return (this.getPkgVersion(pkgName) &&
(0, semver_1.satisfies)(this.getPkgVersion(pkgName), versionRange, {
includePrerelease: true,
}));
});
}
areIncompatiblePackagesPresent(incompatibleWith) {
if (!incompatibleWith || !Object.keys(incompatibleWith).length) {
return false;
}
return Object.entries(incompatibleWith).some(([pkgName, versionRange]) => {
if (this.packageUpdates[pkgName]) {
return (0, semver_1.satisfies)(cleanSemver(this.packageUpdates[pkgName].version), versionRange, { includePrerelease: true });
}
return (this.getPkgVersion(pkgName) &&
(0, semver_1.satisfies)(this.getPkgVersion(pkgName), versionRange, {
includePrerelease: true,
}));
});
}
areMigrationRequirementsMet(packageName, migration) {
if (!this.excludeAppliedMigrations) {
return this.areRequirementsMet(migration.requires);
}
return ((this.wasMigrationSkipped(migration.requires) ||
this.isMigrationForHigherVersionThanWhatIsInstalled(packageName, migration)) &&
this.areRequirementsMet(migration.requires));
}
isMigrationForHigherVersionThanWhatIsInstalled(packageName, migration) {
const installedVersion = this.getInstalledPackageVersion(packageName);
return (migration.version &&
(!installedVersion || this.gt(migration.version, installedVersion)) &&
this.lte(migration.version, this.packageUpdates[packageName].version));
}
wasMigrationSkipped(requirements) {
// 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) ||
!(0, semver_1.satisfies)(this.getInstalledPackageVersion(pkgName), versionRange, {
includePrerelease: true,
}));
}
async runPackageJsonUpdatesConfirmationPrompt(packageUpdate, packageUpdateKey, packageName) {
if (!packageUpdate['x-prompt']) {
return Promise.resolve(true);
}
const promptKey = this.getPackageUpdatePromptKey(packageUpdate);
if (this.promptAnswers[promptKey] !== undefined) {
// a same prompt was already answered, skip
return Promise.resolve(false);
}
const promptConfig = {
name: 'shouldApply',
type: 'confirm',
message: packageUpdate['x-prompt'],
initial: true,
};
if (packageName.startsWith('@nx/')) {
// @ts-expect-error -- enquirer types aren't correct, footer does exist
promptConfig.footer = () => pc.dim(` View migration details at https://nx.dev/nx-api/${packageName.replace('@nx/', '')}#${packageUpdateKey.replace(/[-\.]/g, '')}packageupdates`);
}
return await (0, safe_prompt_1.migratePrompt)([promptConfig]).then(({ shouldApply }) => {
this.promptAnswers[promptKey] = shouldApply;
if (!shouldApply &&
(!this.minVersionWithSkippedUpdates ||
(0, semver_1.lt)(packageUpdate.version, this.minVersionWithSkippedUpdates))) {
this.minVersionWithSkippedUpdates = packageUpdate.version;
}
return shouldApply;
});
}
getPackageUpdatePromptKey(packageUpdate) {
return Object.entries(packageUpdate.packages)
.map(([name, update]) => `${name}:${JSON.stringify(update)}`)
.join('|');
}
getPkgVersion(pkg) {
return this.getInstalledPackageVersion(pkg, this.installedPkgVersionOverrides);
}
gt(v1, v2) {
return (0, semver_1.gt)((0, version_utils_1.normalizeVersion)(v1), (0, version_utils_1.normalizeVersion)(v2));
}
lt(v1, v2) {
return (0, semver_1.lt)((0, version_utils_1.normalizeVersion)(v1), (0, version_utils_1.normalizeVersion)(v2));
}
lte(v1, v2) {
return (0, semver_1.lte)((0, version_utils_1.normalizeVersion)(v1), (0, version_utils_1.normalizeVersion)(v2));
}
}
exports.Migrator = Migrator;
const LEGACY_NRWL_PACKAGE_GROUP = [
{ package: '@nrwl/workspace', version: '*' },
{ package: '@nrwl/angular', version: '*' },
{ package: '@nrwl/cypress', version: '*' },
{ package: '@nrwl/devkit', version: '*' },
{ package: '@nrwl/eslint-plugin-nx', version: '*' },
{ package: '@nrwl/express', version: '*' },
{ package: '@nrwl/jest', version: '*' },
{ package: '@nrwl/linter', version: '*' },
{ package: '@nrwl/nest', version: '*' },
{ package: '@nrwl/next', version: '*' },
{ package: '@nrwl/node', version: '*' },
{ package: '@nrwl/nx-plugin', version: '*' },
{ package: '@nrwl/react', version: '*' },
{ package: '@nrwl/storybook', version: '*' },
{ package: '@nrwl/web', version: '*' },
{ package: '@nrwl/js', version: '*' },
{ package: 'nx-cloud', version: 'latest' },
{ package: '@nrwl/react-native', version: '*' },
{ package: '@nrwl/detox', version: '*' },
{ package: '@nrwl/expo', version: '*' },
{ package: '@nrwl/tao', version: '*' },
];
function resolveRequiredPackages(targetPackage, packageGroup) {
const set = new Set([targetPackage]);
for (const { package: name } of packageGroup ?? []) {
set.add(name);
}
return set;
}
/**
* The canonical Nx package for a given target version: `@nrwl/workspace` for
* legacy (`< 14.0.0-beta.0`), `nx` otherwise. Non-semver inputs (e.g. the
* literal `'latest'` sentinel before tag resolution) resolve to modern era.
*/
function resolveCanonicalNxPackage(targetVersion) {
return (0, version_utils_1.isLegacyEra)(targetVersion) ? '@nrwl/workspace' : 'nx';
}
/**
* `@nx/workspace` is version-synced with `nx` but declares an intentionally
* narrow `packageGroup`; resolve eligibility, bounds, and the optional walk
* against `nx`'s full closure so they match what the cascade actually walks.
*/
function toNxClosurePackage(packageName) {
return packageName === '@nx/workspace' ? 'nx' : packageName;
}
async function resolveInclude(include, context, configuredInclude) {
// An explicit `--include` is validated against the target's `supportsOptionalMigrations` in
// `resolveTargetAndInclude`, so honor it directly here.
if (include) {
(0, migrate_analytics_1.setMigrateIncludeSource)('flag');
return include;
}
// Targets that don't declare `supportsOptionalMigrations` only ever run the full
// migration; there is nothing to pick between.
if (!context.targetSupportsOptionalUpdates) {
if (configuredInclude && configuredInclude !== 'all') {
output_1.output.warn({
title: `The configured nx.json migrate.include '${configuredInclude}' is not available for this migration; falling back to 'all'.`,
bodyLines: [`The target package does not support optional updates.`],
});
}
(0, migrate_analytics_1.setMigrateIncludeSource)('default');
return 'all';
}
// nx.json `migrate.include` pre-selects the answer the prompt would ask for.
if (configuredInclude) {
(0, migrate_analytics_1.setMigrateIncludeSource)('nx-json');
return configuredInclude;
}
const choices = [
{
name: 'required',
message: 'Required only (the target package and the packages it ships with)',
},
];
// `--interactive` keeps the legacy x-prompt flow, which the `optional` value
// supersedes and is incompatible with, so omit it when interactive.
if (!context.hasFrom &&
!context.hasExcludeAppliedMigrations &&
context.interactive !== true) {
choices.push({
name: 'optional',
message: 'Optional only (the dependency updates those packages recommend)',
});
}
if (!(0, safe_prompt_1.canPrompt)(context.interactive)) {
(0, migrate_analytics_1.setMigrateIncludeSource)('default');
return 'all';
}
choices.push({
name: 'all',
message: 'All (required and optional)',
});
const { include: selected } = await (0, safe_prompt_1.migratePrompt)({
type: 'select',
name: 'include',
message: 'Which packages would you like to migrate?',
choices,
});
(0, migrate_analytics_1.reportMigratePrompt)('include', selected);
(0, migrate_analytics_1.setMigrateIncludeSource)('prompt');
return selected;
}
async function versionOverrides(overrides, param) {
const res = {};
const promises = overrides.split(',').map((p) => {
const split = p.lastIndexOf('@');
if (split === -1 || split === 0) {
throw new Error(`Incorrect '${param}' section. Use --${param}="package@version"`);
}
const selectedPackage = p.substring(0, split).trim();
const selectedVersion = p.substring(split + 1).trim();
if (!selectedPackage || !selectedVersion) {
throw new Error(`Incorrect '${param}' section. Use --${param}="package@version"`);
}
return (0, version_utils_1.normalizeVersionWithTagCheck)(selectedPackage, selectedVersion).then((version) => {
res[normalizeSlashes(selectedPackage)] = version;
});
});
await Promise.all(promises);
return res;
}
async function parseTargetPackageAndVersion(args) {
if (!args) {
throw new Error(`Provide the correct package name and version. E.g., my-package@9.0.0.`);
}
if (args.indexOf('@') > -1) {
const i = args.lastIndexOf('@');
if (i === 0) {
return { targetPackage: args.trim(), targetVersion: 'latest' };
}
const targetPackage = args.substring(0, i);
const maybeVersion = args.substring(i + 1);
if (!targetPackage || !maybeVersion) {
throw new Error(`Provide the correct package name and version. E.g., my-package@9.0.0.`);
}
const targetVersion = await (0, version_utils_1.normalizeVersionWithTagCheck)(targetPackage, maybeVersion);
return { targetPackage, targetVersion };
}
if (version_utils_1.DIST_TAGS.includes(args) ||
(0, semver_1.valid)(args) ||
args.match(/^\d+(?:\.\d+)?(?:\.\d+)?$/)) {
// Passing `nx` here may seem wrong, but nx and @nrwl/workspace are synced in version.
// We could duplicate the ternary below, but its not necessary since they are equivalent
// on the registry
const targetVersion = await (0, version_utils_1.normalizeVersionWithTagCheck)('nx', args);
const isDistTag = version_utils_1.DIST_TAGS.includes(args);
const targetPackage = isDistTag
? 'nx'
: resolveCanonicalNxPackage(targetVersion);
return { targetPackage, targetVersion };
}
return { targetPackage: args, targetVersion: 'latest' };
}
async function parseMigrationsOptions(options, fetch) {
if (options.runMigrations === '') {
options.runMigrations = 'migrations.json';
}
if (options.include && options.runMigrations) {
throw new Error(`Error: '--include' cannot be combined with '--run-migrations'.`);
}
if (options.multiMajorMode && options.runMigrations) {
throw new Error(`Error: '--multi-major-mode' cannot be combined with '--run-migrations'.`);
}
if (options.runMigrations) {
return {
type: 'runMigrations',
runMigrations: options.runMigrations,
ifExists: options.ifExists,
agentic: options.agentic,
validate: options.validate,
interactive: options.interactive,
};
}
assertOptionalIncludeFlagCompatibility(options);
const [from, to] = await Promise.all([
options.from
? versionOverrides(options.from, 'from')
: Promise.resolve({}),
options.to
? await versionOverrides(options.to, 'to')
: Promise.resolve({}),
]);
// The gate reads `supportsOptionalMigrations` through this fetcher (registry-first, install
// fallback) so private registries don't fail closed. In production the caller
// shares its fetcher; standalone callers (tests) get a fresh one.
const resolvedFetch = fetch ?? createFetcher((0, package_manager_1.getPackageManagerCommand)());
const positional = options['packageAndVersion'];
const resolved = await resolveTargetAndInclude({
positional,
from,
options,
fetch: resolvedFetch,
});
const { include, installedTargetVersion } = resolved;
let { targetPackage, targetVersion } = resolved;
// Crossing more than one major can silently skip migrations: each
// major's metadata may have pruned entries from much-older versions.
const multiMajorResult = await (0, multi_major_1.maybePromptOrWarnMultiMajorMigration)({
include,
options,
targetPackage,
targetVersion,
});
targetVersion = multiMajorResult.chosen;
if (include === 'optional') {
// `include` can resolve to optional via nx.json, which bypasses the early
// CLI-only check above; re-assert against the resolved value.
assertOptionalIncludeFlagCompatibility({
include,
from: options.from,
excludeAppliedMigrations: options.excludeAppliedMigrations,
interactive: options.interactive,
});
assertOptionalTargetBounds({
targetPackage,
targetVersion,
to,
// `resolveTargetAndInclude` always resolves the installed bounds version for
// the `optional` value (or throws), so it is present here.
installedTargetVersion: installedTargetVersion,
});
}
return {
type: 'generateMigrations',
targetPackage,
targetVersion,
from,
to,
interactive: options.interactive,
excludeAppliedMigrations: options.excludeAppliedMigrations,
include,
originalTargetVersion: multiMajorResult.originalTarget,
multiMajorMode: multiMajorResult.gradual ? 'gradual' : undefined,
multiMajorChoice: multiMajorResult.decision,
};
}
function assertOptionalIncludeFlagCompatibility(options) {
if (options.include !== 'optional')
return;
if (options.from) {
throw new Error(`Error: '--include=optional' cannot be combined with '--from'.`);
}
if (options.excludeAppliedMigrations === true) {
throw new Error(`Error: '--include=optional' cannot be combined with '--exclude-applied-migrations'.`);
}
if (options.interactive === true) {
throw new Error(`Error: '--include=optional' cannot be combined with '--interactive'.`);
}
}
// Resolves the target package/version up front (the `optional` value anchors to
// the installed target; otherwise dist-tags resolve to a concrete version), then
// resolves the include value and rejects `--include` when the target doesn't support it.
// Bare invocations require an explicit target on older installs rather than
// defaulting to `latest` across a large major gap.
async function resolveTargetAndInclude(args) {
const { positional, from, options, fetch } = args;
let targetPackage;
let targetVersion;
if (positional) {
const parsed = await parseTargetPackageAndVersion(positional);
targetPackage = normalizeSlashes(parsed.targetPackage);
targetVersion = parsed.targetVersion;
}
const installed = resolveInstalledCanonical();
const installedMajor = installed && (0, semver_1.valid)(installed.version) ? (0, semver_1.major)(installed.version) : null;
// `--include=optional` anchors the target to the installed version below, so
// it never needs a target or dist-tag resolved up front.
const isExplicitOptional = options.include === 'optional';
// Bare `nx migrate` defaults to `nx@latest`. Only do so from a recent-enough
// install (v22+); an unknown or far-behind version would otherwise silently
// run a large multi-major jump, so require an explicit target there instead.
if (!positional && !isExplicitOptional) {
if (installedMajor === null || installedMajor < 22) {
throw new Error(`Provide the package and version to migrate to. E.g., \`nx migrate nx@<version>\`.`);
}
targetPackage = 'nx';
targetVersion = 'latest';
}
// Resolve dist-tags to a concrete version so the `supportsOptionalMigrations` gate and the
// downstream cascade read a real semver. Explicit dist-tags arrive already
// resolved from `parseTargetPackageAndVersion`; only bare invocations and
// bare package names (`nx migrate nx`) reach here unresolved.
if (!isExplicitOptional &&
targetPackage &&
targetVersion &&
!(0, semver_1.valid)(targetVersion)) {
try {
targetVersion = await (0, version_utils_1.normalizeVersionWithTagCheck)(targetPackage, targetVersion);
}
catch {
// Registry unavailable: keep the tag. The sentinel degrades gracefully
// downstream (multi-major and the cascade tolerate it).
}
}
// `--include` is only available for targets that opt in via `supportsOptionalMigrations`.
// required/all/prompt/nx.json read the flag at the version being migrated
// to. Skipped when the include value can't depend on it (no `--include`, no nx.json
// default, no interactive prompt) and for the explicit `optional` value, which
// anchors to the installed target and reads at that version below.
let targetSupportsOptionalUpdates = false;
// The package/version whose `supportsOptionalMigrations` flag the gate actually read,
// surfaced verbatim in the rejection message below.
let eligibilityPackage = targetPackage;
let eligibilityVersion = targetVersion;
if (!isExplicitOptional &&
targetPackage &&
(options.include ||
options.includeFromConfig ||
(0, safe_prompt_1.canPrompt)(options.interactive))) {
// Read at the canonical closure package so the gate shares the cascade's
// cached fetch (the walk normalizes `@nx/workspace` -> `nx` too).
eligibilityPackage = toNxClosurePackage(targetPackage);
targetSupportsOptionalUpdates = await fetchSupportsOptionalUpdates(fetch, eligibilityPackage, targetVersion);
}
// Recorded before the interactive prompts (include, multi-major) so runs
// abandoned at a prompt still register a start.
(0, migrate_analytics_1.reportMigrateGenerateStart)({
targetPackage: targetPackage ?? 'nx',
interactive: options.interactive,
excludeAppliedMigrations: options.excludeAppliedMigrations,
});
const include = await resolveInclude(options.include, {
hasFrom: Object.keys(from).length > 0,
hasExcludeAppliedMigrations: options.excludeAppliedMigrations === true,
interactive: options.interactive,
targetSupportsOptionalUpdates,
}, options.includeFromConfig);
let installedTargetVersion;
// The `optional` value catches up the deps the target manages for the version
// you are already on, capped at the installed version. `@nx/workspace` is
// version-synced with `nx` but declares a narrower group, so resolve the
// installed bounds against `nx`'s full closure.
if (include === 'optional') {
if (!positional) {
// Bare `--include=optional`: catch up the deps Nx manages for installed Nx.
if (!installed) {
throw new Error(`Error: '--include=optional' requires 'nx' (or '@nrwl/workspace' on Nx <14) to be installed in your workspace. Install dependencies first, then re-run.`);
}
targetPackage = installed.canonical;
installedTargetVersion = installed.version;
targetVersion = installedTargetVersion;
}
else {
const boundsPackage = toNxClosurePackage(targetPackage);
installedTargetVersion = (0, installed_nx_version_1.getInstalledVersion)(boundsPackage);
if (!installedTargetVersion) {
throw new Error(`Error: '--include=optional' requires '${boundsPackage}' to be installed in your workspace. Install dependencies first, then re-run.`);
}
// A bare package name (no semver, surfaced as the literal `'latest'`)
// anchors the catch-up walk to installed; an explicit version is kept and
// bounded against installed downstream.
if (!(0, semver_1.valid)(targetVersion)) {
targetVersion = installedTargetVersion;
}
}
// An explicit `--include=optional` is gated on the INSTALLED version's flag:
// you catch up the deps you already have, so eligibility follows the
// installed package, not the (possibly older) explicit target. Config /
// prompt-derived `optional` value was already vetted via the to-target read.
if (options.include === 'optional') {
eligibilityPackage = toNxClosurePackage(targetPackage);
eligibilityVersion = installedTargetVersion;
targetSupportsOptionalUpdates = await fetchSupportsOptionalUpdates(fetch, eligibilityPackage, installedTargetVersion);
}
}
if (options.include && !targetSupportsOptionalUpdates) {
throw new Error(`Error: '--include' requires the target package to support optional updates, but '${eligibilityPackage}@${eligibilityVersion}' does not.`);
}
return {
targetPackage: targetPackage,
targetVersion: targetVersion,
include,
installedTargetVersion,
};
}
// `--include` is opt-in per package via `supportsOptionalMigrations` in the target's
// `nx-migrations`/`ng-update` config. Read it through the shared fetcher
// (registry-first, install fallback) so registries that can't serve metadata
// via `npm view` resolve it from an install rather than failing the gate.
async function fetchSupportsOptionalUpdates(fetch, packageName, packageVersion) {
const config = await fetch(packageName, packageVersion);
return config.supportsOptionalMigrations === true;
}
// `--include=optional` upper-bound gate. The optional walk catches up from
// zero, so a target or `--to` above the installed version would surface
// optional bumps that only exist in the newer package's history. The
// required set is the target package's declared `packageGroup`; the legacy
// era falls back to the hardcoded `LEGACY_NRWL_PACKAGE_GROUP`. `installed` is
// the installed bounds version already resolved by `resolveTargetAndInclude`.
function assertOptionalTargetBounds(args) {
const { targetPackage, targetVersion, to, installedTargetVersion: installed, } = args;
const boundsPackage = toNxClosurePackage(targetPackage);
if ((0, semver_1.gt)(targetVersion, installed)) {
throw new Error(`Error: '--include=optional' cannot migrate to a version higher than what is currently installed (got '${targetPackage}@${targetVersion}', installed '${boundsPackage}@${installed}'). Either drop '--include=optional' or lower the target.`);
}
const requiredSet = (0, version_utils_1.isLegacyEra)(targetVersion)
? new Set([
boundsPackage,
...LEGACY_NRWL_PACKAGE_GROUP.map((p) => p.package),
])
: (0, installed_nx_version_1.getInstalledPackageGroup)(boundsPackage);
for (const [pkg, version] of Object.entries(to)) {
if (requiredSet.has(pkg) && (0, semver_1.gt)(version, installed)) {
throw new Error(`Error: '--include=optional' cannot migrate to a version higher than what is currently installed (got '--to ${pkg}@${version}', installed '${boundsPackage}@${installed}'). Either drop '--include=optional' or lower the '--to' value.`);
}
}
}
/**
* Pick the canonical Nx package + version for `--include=optional` when the
* user didn't supply an explicit version. Returns `'nx'` for modern era,
* falls back to `'@nrwl/workspace'` (legacy era) when only that is installed
* or when the installed `nx` itself is `<14`.
*/
function resolveInstalledCanonical() {
const installedNx = (0, installed_nx_version_1.getInstalledNxVersion)();
if (installedNx) {
return {
canonical: resolveCanonicalNxPackage(installedNx),
version: installedNx,
};
}
const installedLegacy = (0, installed_nx_version_1.getInstalledLegacyNrwlWorkspaceVersion)();
if (installedLegacy) {
return { canonical: '@nrwl/workspace', version: installedLegacy };
}
return null;
}
function createInstalledPackageVersionsResolver(root) {
const cache = {};
const nxRequires = (0, installation_directory_1.getNxRequirePaths)(root).map((path) => (0, module_1.createRequire)((0, path_1.join)(path, 'package.json')));
function getInstalledPackageVersion(packageName, overrides) {
if (overrides?.[packageName]) {
return overrides[packageName];
}
if (packageName === 'nx') {
const nxVersion = cache[packageName] ??
(() => {
for (const req of nxRequires) {
try {
const packageJsonPath = req.resolve('nx/package.json');
if (packageJsonPath.startsWith(workspace_root_1.workspaceRoot)) {
return (0, fileutils_1.readJsonFile)(packageJsonPath).version;
}
}
catch { }
}
return getInstalledPackageVersion('@nrwl/workspace', overrides);
})();
if (nxVersion) {
cache[packageName] = nxVersion;
}
return nxVersion;
}
try {
if (!cache[packageName]) {
const { packageJson, path } = (0, package_json_1.readModulePackageJson)(packageName, (0, installation_directory_1.getNxRequirePaths)(root));
// old workspaces would have the temp installation of nx in the cache,
// so the resolved package is not the one we need
if (!path.startsWith(workspace_root_1.workspaceRoot)) {
throw new Error('Resolved a package outside the workspace root.');
}
cache[packageName] = packageJson.version;
}
return cache[packageName];
}
catch {
return null;
}
}
return getInstalledPackageVersion;
}
// testing-fetch-start
function createFetcher(pmc) {
const migrationsCache = {};
const resolvedVersionCache = {};
const stats = { registryCount: 0, installCount: 0 };
function recordInstallFetch(reason) {
stats.installCount++;
stats.fallbackReason ??= reason;
}
function fetchMigrations(packageName, packageVersion, setCache) {
if (!(0, resolve_package_version_1.isRegistryResolutionEnabled)()) {
// Skip registry fetch and use installation method directly
logger_1.logger.info(`Fetching ${packageName}@${packageVersion}`);
recordInstallFetch('env-skip');
return getPackageMigrationsUsingInstall(packageName, packageVersion, pmc);
}
const cacheKey = packageName + '-' + packageVersion;
return Promise.resolve(resolvedVersionCache[cacheKey])
.then((cachedResolvedVersion) => {
if (cachedResolvedVersion) {
return cachedResolvedVersion;
}
resolvedVersionCache[cacheKey] =
(0, resolve_package_version_1.resolvePackageVersionRespectingMinReleaseAge)(packageName, packageVersion);
return resolvedVersionCache[cacheKey];
})
.then((resolvedVersion) => {
if (resolvedVersion !== packageVersion &&
migrationsCache[`${packageName}-${resolvedVersion}`]) {
return migrationsCache[`${packageName}-${resolvedVersion}`];
}
setCache(packageName, resolvedVersion);
return getPackageMigrationsUsingRegistry(packageName, resolvedVersion).then((result) => {
stats.registryCount++;
return result;
});
})
.catch((e) => {
// A cooldown violation would fail an install identically (only slower),
// so surface it instead of retrying through the package manager.
if (e instanceof errors_1.MinReleaseAgeViolationError) {
throw e;
}
logger_1.logger.verbose(`Failed to get migrations from registry for ${packageName}@${packageVersion}: ${e.message}. Falling back to install.`);
logger_1.logger.info(`Fetching ${packageName}@${packageVersion}`);
recordInstallFetch((0, migrate_analytics_1.classifyMigrateFetchFallback)(e));
return getPackageMigrationsUsingInstall(packageName, packageVersion, pmc);
});
}
const nxMigrateFetcher = (packageName, packageVersion) => {
if (migrationsCache[`${packageName}-${p