UNPKG

nx

Version:

The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.

1,023 lines • 134 kB
"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