UNPKG

nx

Version:

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

250 lines (249 loc) • 12.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.isRegistryResolutionEnabled = isRegistryResolutionEnabled; exports.resetResolvePackageVersionState = resetResolvePackageVersionState; exports.resolvePackageVersionRespectingMinReleaseAge = resolvePackageVersionRespectingMinReleaseAge; const configuration_1 = require("../../config/configuration"); const is_ci_1 = require("../../utils/is-ci"); const logger_1 = require("../../utils/logger"); const output_1 = require("../../utils/output"); const catalog_1 = require("../../utils/catalog"); const package_manager_1 = require("../../utils/package-manager"); const workspace_root_1 = require("../../utils/workspace-root"); const errors_1 = require("../../utils/min-release-age/errors"); const policy_1 = require("../../utils/min-release-age/policy"); const pnpm_exclude_writer_1 = require("../../utils/min-release-age/pnpm-exclude-writer"); const resolve_1 = require("../../utils/min-release-age/resolve"); const safe_prompt_1 = require("./safe-prompt"); /** * Whether nx migrate should resolve versions via the npm registry (fast) rather * than via a real package-manager install. Precedence, highest first: * * 1. `NX_MIGRATE_USE_REGISTRY_RESOLUTION` ('true' -> enabled, 'false' -> disabled) * 2. legacy `NX_MIGRATE_SKIP_REGISTRY_FETCH` (deprecated, removed in Nx 24; * 'true' -> disabled, 'false' -> enabled) * 3. nx.json `migrate.useRegistryResolution` * 4. default: enabled */ function isRegistryResolutionEnabled(root = workspace_root_1.workspaceRoot) { warnIfLegacyRegistryEnvVarSet(); const explicit = parseBooleanFlag(process.env.NX_MIGRATE_USE_REGISTRY_RESOLUTION); if (explicit !== undefined) { return explicit; } // Legacy env is inverted: SKIP_REGISTRY_FETCH=true disables resolution. const legacy = parseBooleanFlag(process.env.NX_MIGRATE_SKIP_REGISTRY_FETCH); if (legacy !== undefined) { return !legacy; } return (0, configuration_1.readNxJson)(root)?.migrate?.useRegistryResolution ?? true; } function parseBooleanFlag(value) { return value === 'true' ? true : value === 'false' ? false : undefined; } // NX_MIGRATE_SKIP_REGISTRY_FETCH is the inverted, legacy predecessor of // NX_MIGRATE_USE_REGISTRY_RESOLUTION: deprecated in Nx 23, removed in Nx 24. // Warn once whenever it is present (even when the new var overrides it) so users // migrate off it and clean up configs where it lingers - a dormant value would // silently re-activate if the new var were later removed. function warnIfLegacyRegistryEnvVarSet() { if (legacyRegistryEnvVarDeprecationWarned || process.env.NX_MIGRATE_SKIP_REGISTRY_FETCH === undefined) { return; } legacyRegistryEnvVarDeprecationWarned = true; const bodyLines = [ 'Use NX_MIGRATE_USE_REGISTRY_RESOLUTION (or migrate.useRegistryResolution in nx.json) instead, then remove NX_MIGRATE_SKIP_REGISTRY_FETCH from your environment.', ]; if (process.env.NX_MIGRATE_USE_REGISTRY_RESOLUTION !== undefined) { bodyLines.push('It is currently being overridden by NX_MIGRATE_USE_REGISTRY_RESOLUTION.'); } output_1.output.warn({ title: 'NX_MIGRATE_SKIP_REGISTRY_FETCH is deprecated and will be removed in Nx 24.', bodyLines, }); } // The cooldown policy is constant for a migrate run; read it once. let cachedPolicyPromise; // Dedup the "resolved X instead of Y" notice across repeated specs. const reportedChangedOutcomes = new Set(); // Strict-mode approval is a once-per-run decision (mirrors pnpm prompting once). let strictApprovalGranted = false; // The deprecated NX_MIGRATE_SKIP_REGISTRY_FETCH heads-up is shown once per run. let legacyRegistryEnvVarDeprecationWarned = false; /** Test-only: clear the module-level caches between cases. */ function resetResolvePackageVersionState() { cachedPolicyPromise = undefined; reportedChangedOutcomes.clear(); strictApprovalGranted = false; legacyRegistryEnvVarDeprecationWarned = false; } function getPolicy() { cachedPolicyPromise ??= (0, policy_1.readMinReleaseAgePolicy)(); return cachedPolicyPromise; } /** * The single resolution entry point for ALL nx migrate version resolutions. It * resolves a package spec to a concrete version while honoring the effective * minimum-release-age (cooldown) policy of the user's package manager, matching * how that PM at its detected version would resolve. Falls back to a real PM * install when the policy can't be reasoned about, or when registry resolution * is opted out. */ async function resolvePackageVersionRespectingMinReleaseAge(packageName, version, options) { // A speculative lookup (e.g. populating multi-major prompt choices) sets this // to false so resolving a version the user has not chosen yet never prompts, // writes pnpm excludes, or triggers a real install. const applySideEffects = options?.applySideEffects ?? true; // A probe can never install, so the registry-resolution opt-out (which only // picks install vs. registry for the real resolution) does not apply to it: it // is always registry-based. Resolve read-only while still reproducing an active // cooldown so the probe predicts the same version a real install would accept. if (!applySideEffects) { return resolveProbe(packageName, version); } if (!isRegistryResolutionEnabled()) { return (0, package_manager_1.resolvePackageVersionUsingInstallation)(packageName, version); } const result = await getPolicy(); if (result.outcome === 'inactive') { return (0, package_manager_1.resolvePackageVersionUsingRegistry)(packageName, version); } if (result.outcome === 'ambiguous') { logger_1.logger.verbose(`Cannot determine the minimum-release-age policy (${result.reason}); falling back to a package-manager install to resolve ${packageName}@${version}.`); return (0, package_manager_1.resolvePackageVersionUsingInstallation)(packageName, version); } return resolveWithPolicy(packageName, version, result.policy, true); } /** * Read-only resolution for speculative lookups (never installs, never prompts, * never writes excludes). Reproduces an active cooldown policy off the registry * so the probe's pick matches what a real install would accept. Degrades to the * raw latest-in-range when there is no policy to reproduce (inactive) or it * cannot be reasoned about (ambiguous) - the same read-only answer the real * resolution would refine via a package-manager install. */ async function resolveProbe(packageName, version) { const result = await getPolicy(); if (result.outcome !== 'active') { return (0, package_manager_1.resolvePackageVersionUsingRegistry)(packageName, version); } return resolveWithPolicy(packageName, version, result.policy, false); } async function resolveWithPolicy(packageName, version, policy, applySideEffects) { try { const spec = (0, catalog_1.resolveCatalogReferenceIfNeeded)(packageName, version); const outcome = await (0, resolve_1.resolveCompliantVersion)(packageName, spec, policy); if (applySideEffects && outcome.version !== outcome.unconstrained) { reportChangedOutcome(packageName, spec, outcome.version, outcome.unconstrained, policy); } // An immature loose pick is returned as-is. nx does not write the // minimumReleaseAgeExclude entry here: migrate does not replace the install, // so the real `pnpm install` (>=11.1.3) auto-writes it itself. return outcome.version; } catch (e) { // A side-effect-free probe never installs/prompts/writes, so any failure - // a violation or an unknown error - just means the version is unavailable. if (!applySideEffects) { throw e; } if (e instanceof errors_1.MinReleaseAgeViolationError) { return handleViolation(packageName, e, policy); } // Unknown failure (registry hiccup, parse error): try a real install, but // surface the original error if that also fails. try { return await (0, package_manager_1.resolvePackageVersionUsingInstallation)(packageName, version); } catch { throw e; } } } function reportChangedOutcome(packageName, spec, picked, unconstrained, policy) { const key = `${packageName}@${spec}`; if (reportedChangedOutcomes.has(key)) { return; } reportedChangedOutcomes.add(key); output_1.output.log({ title: `Resolved ${packageName}@${picked} instead of ${unconstrained}: ${policy.sourceDescription}.`, }); } async function handleViolation(packageName, error, policy) { const isPnpmStrict = policy.behavior.packageManager === 'pnpm' && policy.behavior.strict && policy.behavior.writesExcludes; if (!isPnpmStrict) { throw error; } // An unknown tag yields a violation with no blocked candidates (nothing to // approve); surface the original no-matching-version error rather than // prompting over an empty list. if (error.blocked.length === 0) { throw error; } // pnpm gates its prompt on stdin (the surface enquirer reads); match that // rather than stdout, which diverges when output is piped. const canPrompt = !!process.stdin.isTTY && !(0, is_ci_1.isCI)(); if (!canPrompt) { throw error; } // pnpm's loose resolver picks the lowest in-range version (a single exact/tag // target otherwise); strict fails on exactly that pick. error.blocked is // sorted newest-first, so the resolver's pick is the last entry. Mirror the // loose path: prompt for, exclude, and return that one version. const resolved = error.blocked[error.blocked.length - 1]; const blockedLine = ` ${packageName}@${resolved.version} (published ${formatPublishAge(resolved.publishedAt)})`; // pnpm prompts once for the whole run; remember an approval for later picks. if (!strictApprovalGranted) { const { approved } = await (0, safe_prompt_1.migratePrompt)([ { name: 'approved', type: 'confirm', initial: false, message: `The following version does not meet the ${policy.sourceDescription} constraint:\n${blockedLine}\nInstall anyway and add it to minimumReleaseAgeExclude in pnpm-workspace.yaml?`, }, ]); if (!approved) { // Deny must surface as a violation: migrate.ts only rethrows // MinReleaseAgeViolationError, so a plain Error would silently fall back // to a real PM install and defeat the deny gate. Shape it like pnpm's // ERR_PNPM_MINIMUM_RELEASE_AGE_DENIED. throw new errors_1.MinReleaseAgeViolationError({ packageManager: error.packageManager, packageName: error.packageName, spec: error.spec, pmShapedDetail: 'Aborted: the immature versions were not approved.', blocked: error.blocked, remediation: [ `Add ${packageName} (optionally with a version) to minimumReleaseAgeExclude in pnpm-workspace.yaml, or re-run without minimumReleaseAgeStrict to allow these versions.`, ], }); } strictApprovalGranted = true; } (0, pnpm_exclude_writer_1.appendMinimumReleaseAgeExcludes)(workspace_root_1.workspaceRoot, [ `${packageName}@${resolved.version}`, ]); return resolved.version; } // Renders a registry ISO publish timestamp as a human age for the strict prompt // (e.g. "6 hours ago"), falling back to the raw value when it is not parseable. function formatPublishAge(publishedAt) { const elapsedMs = Date.now() - Date.parse(publishedAt); if (!Number.isFinite(elapsedMs) || elapsedMs < 0) { return publishedAt; } const minutes = Math.floor(elapsedMs / 60_000); if (minutes < 60) { return `${minutes} minute${minutes === 1 ? '' : 's'} ago`; } const hours = Math.floor(elapsedMs / 3_600_000); if (hours < 48) { return `${hours} hour${hours === 1 ? '' : 's'} ago`; } const days = Math.floor(elapsedMs / 86_400_000); return `${days} day${days === 1 ? '' : 's'} ago`; }