UNPKG

@nx/js

Version:

The JS plugin for Nx contains executors and generators that provide the best experience for developing JavaScript and TypeScript projects.

762 lines (759 loc) 51.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.releaseVersionGenerator = releaseVersionGenerator; const devkit_1 = require("@nx/devkit"); const chalk = require("chalk"); const enquirer_1 = require("enquirer"); const node_child_process_1 = require("node:child_process"); const promises_1 = require("node:fs/promises"); const node_path_1 = require("node:path"); const config_1 = require("nx/src/command-line/release/config/config"); const git_1 = require("nx/src/command-line/release/utils/git"); const resolve_semver_specifier_1 = require("nx/src/command-line/release/utils/resolve-semver-specifier"); const semver_1 = require("nx/src/command-line/release/utils/semver"); const version_legacy_1 = require("nx/src/command-line/release/version-legacy"); const utils_1 = require("nx/src/tasks-runner/utils"); const ora = require("ora"); const semver_2 = require("semver"); const update_lock_file_1 = require("../../release/utils/update-lock-file"); const is_locally_linked_package_version_1 = require("../../utils/is-locally-linked-package-version"); const npm_config_1 = require("../../utils/npm-config"); const resolve_local_package_dependencies_1 = require("./utils/resolve-local-package-dependencies"); const sort_projects_topologically_1 = require("./utils/sort-projects-topologically"); function resolvePreIdSpecifier(currentSpecifier, preid) { if (!currentSpecifier.startsWith('pre') && preid) { return `pre${currentSpecifier}`; } return currentSpecifier; } async function releaseVersionGenerator(tree, options) { let logger; try { const versionData = {}; // If the user provided a specifier, validate that it is valid semver or a relative semver keyword if (options.specifier) { if (!(0, semver_1.isValidSemverSpecifier)(options.specifier)) { throw new Error(`The given version specifier "${options.specifier}" is not valid. You provide an exact version or a valid semver keyword such as "major", "minor", "patch", etc.`); } // The node semver library classes a leading `v` as valid, but we want to ensure it is not present in the final version options.specifier = options.specifier.replace(/^v/, ''); } if (options.versionPrefix && version_legacy_1.validReleaseVersionPrefixes.indexOf(options.versionPrefix) === -1) { throw new Error(`Invalid value for version.generatorOptions.versionPrefix: "${options.versionPrefix}" Valid values are: ${version_legacy_1.validReleaseVersionPrefixes .map((s) => `"${s}"`) .join(', ')}`); } if (options.firstRelease) { // always use disk as a fallback for the first release options.fallbackCurrentVersionResolver = 'disk'; } // Set default for updateDependents const updateDependents = options.updateDependents ?? 'auto'; const updateDependentsBump = resolvePreIdSpecifier('patch', options.preid); // Sort the projects topologically if update dependents is enabled const projects = updateDependents === 'never' || options.releaseGroup.projectsRelationship !== 'independent' ? options.projects : (0, sort_projects_topologically_1.sortProjectsTopologically)(options.projectGraph, options.projects); const projectToDependencyBumps = new Map(); const resolvePackageRoot = createResolvePackageRoot(options.packageRoot); // Resolve any custom package roots for each project upfront as they will need to be reused during dependency resolution const projectNameToPackageRootMap = new Map(); for (const project of projects) { projectNameToPackageRootMap.set(project.name, resolvePackageRoot(project)); } let currentVersion = undefined; let currentVersionResolvedFromFallback = false; // only used for options.currentVersionResolver === 'git-tag', but // must be declared here in order to reuse it for additional projects let latestMatchingGitTag = undefined; // if specifier is undefined, then we haven't resolved it yet // if specifier is null, then it has been resolved and no changes are necessary let specifier = options.specifier ? options.specifier : undefined; const deleteVersionPlanCallbacks = []; // If the user has set the logUnchangedProjects option to false, we will not print any logs for projects that have no changes. const logUnchangedProjects = options.logUnchangedProjects ?? true; for (const project of projects) { const projectName = project.name; const packageRoot = projectNameToPackageRootMap.get(projectName); if (!packageRoot) { throw new Error(`The project "${projectName}" does not have a packageRoot available. Please report this issue on https://github.com/nrwl/nx`); } const packageJsonPath = (0, devkit_1.joinPathFragments)(packageRoot, 'package.json'); if (!tree.exists(packageJsonPath)) { throw new Error(`The project "${projectName}" does not have a package.json available at ${packageJsonPath}. To fix this you will either need to add a package.json file at that location, or configure "release" within your nx.json to exclude "${projectName}" from the current release group, or amend the packageRoot configuration to point to where the package.json should be.`); } const color = getColor(projectName); logger = new ProjectLogger(projectName, color); const packageJson = (0, devkit_1.readJson)(tree, packageJsonPath); logger.buffer(`🔍 Reading data for package "${packageJson.name}" from ${packageJsonPath}`); const { name: packageName, version: currentVersionFromDisk } = packageJson; switch (options.currentVersionResolver) { case 'registry': { const metadata = options.currentVersionResolverMetadata; const registryArg = typeof metadata?.registry === 'string' ? metadata.registry : undefined; const tagArg = typeof metadata?.tag === 'string' ? metadata.tag : undefined; const warnFn = (message) => { console.log(chalk.keyword('orange')(message)); }; const { registry, tag, registryConfigKey } = await (0, npm_config_1.parseRegistryOptions)(devkit_1.workspaceRoot, { packageRoot: (0, node_path_1.join)(devkit_1.workspaceRoot, packageRoot), packageJson, }, { registry: registryArg, tag: tagArg, }, warnFn); /** * If the currentVersionResolver is set to registry, and the projects are not independent, we only want to make the request once for the whole batch of projects. * For independent projects, we need to make a request for each project individually as they will most likely have different versions. */ if (!currentVersion || options.releaseGroup.projectsRelationship === 'independent') { const spinner = ora(`${Array.from(new Array(projectName.length + 3)).join(' ')}Resolving the current version for tag "${tag}" on ${registry}`); spinner.color = color.spinnerColor; spinner.start(); try { // Must be non-blocking async to allow spinner to render currentVersion = await new Promise((resolve, reject) => { (0, node_child_process_1.exec)(`npm view ${packageName} version --"${registryConfigKey}=${registry}" --tag=${tag}`, { windowsHide: false, }, (error, stdout, stderr) => { if (error) { return reject(error); } if (stderr) { return reject(stderr); } return resolve(stdout.trim()); }); }); spinner.stop(); logger.buffer(`📄 Resolved the current version as ${currentVersion} for tag "${tag}" from registry ${registry}`); } catch (e) { spinner.stop(); if (options.fallbackCurrentVersionResolver === 'disk') { if (!currentVersionFromDisk && (options.specifierSource === 'conventional-commits' || options.specifierSource === 'version-plans')) { currentVersion = await handleNoAvailableDiskFallback({ logger, projectName, packageJsonPath, specifierSource: options.specifierSource, currentVersionSourceMessage: `from the registry ${registry}`, resolutionSuggestion: `you should publish an initial version to the registry`, }); } else { logger.buffer(`📄 Unable to resolve the current version from the registry ${registry}. Falling back to the version on disk of ${currentVersionFromDisk}`); currentVersion = currentVersionFromDisk; currentVersionResolvedFromFallback = true; } } else { throw new Error(`Unable to resolve the current version from the registry ${registry}. Please ensure that the package exists in the registry in order to use the "registry" currentVersionResolver. Alternatively, you can use the --first-release option or set "release.version.generatorOptions.fallbackCurrentVersionResolver" to "disk" in order to fallback to the version on disk when the registry lookup fails.`); } } } else { if (currentVersionResolvedFromFallback) { logger.buffer(`📄 Using the current version ${currentVersion} already resolved from disk fallback.`); } else { logger.buffer(`📄 Using the current version ${currentVersion} already resolved from the registry ${registry}`); } } break; } case 'disk': currentVersion = currentVersionFromDisk; if (!currentVersion) { throw new Error(`Unable to determine the current version for project "${project.name}" from ${packageJsonPath}, please ensure that the "version" field is set within the file`); } logger.buffer(`📄 Resolved the current version as ${currentVersion} from ${packageJsonPath}`); break; case 'git-tag': { if (!currentVersion || // We always need to independently resolve the current version from git tag per project if the projects are independent options.releaseGroup.projectsRelationship === 'independent') { const releaseTagPattern = options.releaseGroup.releaseTagPattern; latestMatchingGitTag = await (0, git_1.getLatestGitTagForPattern)(releaseTagPattern, { projectName: project.name, }, options.releaseGroup.releaseTagPatternCheckAllBranchesWhen); if (!latestMatchingGitTag) { if (options.fallbackCurrentVersionResolver === 'disk') { if (!currentVersionFromDisk && (options.specifierSource === 'conventional-commits' || options.specifierSource === 'version-plans')) { currentVersion = await handleNoAvailableDiskFallback({ logger, projectName, packageJsonPath, specifierSource: options.specifierSource, currentVersionSourceMessage: `from git tag using pattern "${releaseTagPattern}"`, resolutionSuggestion: `you should set an initial git tag on a relevant commit`, }); } else { logger.buffer(`📄 Unable to resolve the current version from git tag using pattern "${releaseTagPattern}". Falling back to the version on disk of ${currentVersionFromDisk}`); currentVersion = currentVersionFromDisk; currentVersionResolvedFromFallback = true; } } else { throw new Error(`No git tags matching pattern "${releaseTagPattern}" for project "${project.name}" were found. You will need to create an initial matching tag to use as a base for determining the next version. Alternatively, you can use the --first-release option or set "release.version.generatorOptions.fallbackCurrentVersionResolver" to "disk" in order to fallback to the version on disk when no matching git tags are found.`); } } else { currentVersion = latestMatchingGitTag.extractedVersion; logger.buffer(`📄 Resolved the current version as ${currentVersion} from git tag "${latestMatchingGitTag.tag}".`); } } else { if (currentVersionResolvedFromFallback) { logger.buffer(`📄 Using the current version ${currentVersion} already resolved from disk fallback.`); } else { logger.buffer( // In this code path we know that latestMatchingGitTag is defined, because we are not relying on the fallbackCurrentVersionResolver, so we can safely use the non-null assertion operator `📄 Using the current version ${currentVersion} already resolved from git tag "${latestMatchingGitTag.tag}".`); } } break; } default: throw new Error(`Invalid value for options.currentVersionResolver: ${options.currentVersionResolver}`); } if (options.specifier) { logger.buffer(`📄 Using the provided version specifier "${options.specifier}".`); // The user is forcibly overriding whatever specifierSource they had otherwise set by imperatively providing a specifier options.specifierSource = 'prompt'; } /** * If we are versioning independently then we always need to determine the specifier for each project individually, except * for the case where the user has provided an explicit specifier on the command. * * Otherwise, if versioning the projects together we only need to perform this logic if the specifier is still unset from * previous iterations of the loop. * * NOTE: In the case that we have previously determined via conventional commits that no changes are necessary, the specifier * will be explicitly set to `null`, so that is why we only check for `undefined` explicitly here. */ if (specifier === undefined || (options.releaseGroup.projectsRelationship === 'independent' && !options.specifier)) { const specifierSource = options.specifierSource; switch (specifierSource) { case 'conventional-commits': { if (options.currentVersionResolver !== 'git-tag') { throw new Error(`Invalid currentVersionResolver "${options.currentVersionResolver}" provided for release group "${options.releaseGroup.name}". Must be "git-tag" when "specifierSource" is "conventional-commits"`); } const affectedProjects = options.releaseGroup.projectsRelationship === 'independent' ? [projectName] : projects.map((p) => p.name); // latestMatchingGitTag will be undefined if the current version was resolved from the disk fallback. // In this case, we want to use the first commit as the ref to be consistent with the changelog command. const previousVersionRef = latestMatchingGitTag ? latestMatchingGitTag.tag : options.fallbackCurrentVersionResolver === 'disk' ? await (0, git_1.getFirstGitCommit)() : undefined; if (!previousVersionRef) { // This should never happen since the checks above should catch if the current version couldn't be resolved throw new Error(`Unable to determine previous version ref for the projects ${affectedProjects.join(', ')}. This is likely a bug in Nx.`); } specifier = await (0, resolve_semver_specifier_1.resolveSemverSpecifierFromConventionalCommits)(previousVersionRef, options.projectGraph, affectedProjects, options.conventionalCommitsConfig); if (!specifier) { if (updateDependents !== 'never' && options.releaseGroup.projectsRelationship === 'independent' && projectToDependencyBumps.has(projectName)) { // No applicable changes to the project directly by the user, but one or more dependencies have been bumped and updateDependents is enabled specifier = updateDependentsBump; logger.buffer(`📄 Resolved the specifier as "${specifier}" because "release.version.generatorOptions.updateDependents" is enabled`); break; } logger.buffer(`🚫 No changes were detected using git history and the conventional commits standard.`); break; } // Always assume that if the current version is a prerelease, then the next version should be a prerelease. // Users must manually graduate from a prerelease to a release by providing an explicit specifier. if ((0, semver_2.prerelease)(currentVersion ?? '')) { specifier = 'prerelease'; logger.buffer(`📄 Resolved the specifier as "${specifier}" since the current version is a prerelease.`); } else { let extraText = ''; const prereleaseSpecifier = resolvePreIdSpecifier(specifier, options.preid); if (prereleaseSpecifier !== specifier) { specifier = prereleaseSpecifier; extraText = `, combined with your given preid "${options.preid}"`; } logger.buffer(`📄 Resolved the specifier as "${specifier}" using git history and the conventional commits standard${extraText}.`); } break; } case 'prompt': { // Only add the release group name to the log if it is one set by the user, otherwise it is useless noise const maybeLogReleaseGroup = (log) => { if (options.releaseGroup.name === config_1.IMPLICIT_DEFAULT_RELEASE_GROUP) { return log; } return `${log} within release group "${options.releaseGroup.name}"`; }; if (options.releaseGroup.projectsRelationship === 'independent') { specifier = await (0, resolve_semver_specifier_1.resolveSemverSpecifierFromPrompt)(`${maybeLogReleaseGroup(`What kind of change is this for project "${projectName}"`)}?`, `${maybeLogReleaseGroup(`What is the exact version for project "${projectName}"`)}?`); } else { specifier = await (0, resolve_semver_specifier_1.resolveSemverSpecifierFromPrompt)(`${maybeLogReleaseGroup(`What kind of change is this for the ${projects.length} matched projects(s)`)}?`, `${maybeLogReleaseGroup(`What is the exact version for the ${projects.length} matched project(s)`)}?`); } break; } case 'version-plans': { if (!options.releaseGroup.versionPlans) { if (options.releaseGroup.name === config_1.IMPLICIT_DEFAULT_RELEASE_GROUP) { throw new Error(`Invalid specifierSource "version-plans" provided. To enable version plans, set the "release.versionPlans" configuration option to "true" in nx.json.`); } else { throw new Error(`Invalid specifierSource "version-plans" provided. To enable version plans for release group "${options.releaseGroup.name}", set the "versionPlans" configuration option to "true" within the release group configuration in nx.json.`); } } if (options.releaseGroup.projectsRelationship === 'independent') { specifier = options.releaseGroup .resolvedVersionPlans.reduce((spec, plan) => { if (!spec) { return plan.projectVersionBumps[projectName]; } if (plan.projectVersionBumps[projectName]) { const prevNewVersion = (0, semver_2.inc)(currentVersion, spec); const nextNewVersion = (0, semver_2.inc)(currentVersion, plan.projectVersionBumps[projectName]); return (0, semver_2.gt)(nextNewVersion, prevNewVersion) ? plan.projectVersionBumps[projectName] : spec; } return spec; }, null); } else { specifier = options.releaseGroup.resolvedVersionPlans.reduce((spec, plan) => { if (!spec) { return plan.groupVersionBump; } const prevNewVersion = (0, semver_2.inc)(currentVersion, spec); const nextNewVersion = (0, semver_2.inc)(currentVersion, plan.groupVersionBump); return (0, semver_2.gt)(nextNewVersion, prevNewVersion) ? plan.groupVersionBump : spec; }, null); } if (!specifier) { if (updateDependents !== 'never' && options.releaseGroup.projectsRelationship === 'independent' && projectToDependencyBumps.has(projectName)) { // No applicable changes to the project directly by the user, but one or more dependencies have been bumped and updateDependents is enabled specifier = updateDependentsBump; logger.buffer(`📄 Resolved the specifier as "${specifier}" because "release.version.generatorOptions.updateDependents" is enabled`); } else { specifier = null; logger.buffer(`🚫 No changes were detected within version plans.`); } } else { logger.buffer(`📄 Resolved the specifier as "${specifier}" using version plans.`); } if (options.deleteVersionPlans) { (options.releaseGroup.resolvedVersionPlans || []).forEach((p) => { deleteVersionPlanCallbacks.push(async (dryRun) => { if (!dryRun) { await (0, promises_1.rm)(p.absolutePath, { recursive: true, force: true }); // the relative path is easier to digest, so use that for // git operations and logging return [p.relativePath]; } else { return []; } }); }); } break; } default: throw new Error(`Invalid specifierSource "${specifierSource}" provided. Must be one of "prompt", "conventional-commits" or "version-plans".`); } } // Resolve any local package dependencies for this project (before applying the new version or updating the versionData) const localPackageDependencies = (0, resolve_local_package_dependencies_1.resolveLocalPackageDependencies)(tree, options.projectGraph, projects, projectNameToPackageRootMap, resolvePackageRoot, // includeAll when the release group is independent, as we may be filtering to a specific subset of projects, but we still want to update their dependents options.releaseGroup.projectsRelationship === 'independent'); // list of projects that depend on the current package const allDependentProjects = Object.values(localPackageDependencies) .flat() .filter((localPackageDependency) => { return localPackageDependency.target === projectName; }); const includeTransitiveDependents = updateDependents !== 'never' && options.releaseGroup.projectsRelationship === 'independent'; const transitiveLocalPackageDependents = []; if (includeTransitiveDependents) { for (const directDependent of allDependentProjects) { // Look through localPackageDependencies to find any which have a target on the current dependent for (const localPackageDependency of Object.values(localPackageDependencies).flat()) { if (localPackageDependency.target === directDependent.source) { transitiveLocalPackageDependents.push(localPackageDependency); } } } } const dependentProjectsInCurrentBatch = []; const dependentProjectsOutsideCurrentBatch = []; // Track circular dependencies using value of project1:project2 const circularDependencies = new Set(); const projectsDependOnCurrentProject = localPackageDependencies[projectName]?.map((localPackageDependencies) => localPackageDependencies.target) ?? []; for (const dependentProject of allDependentProjects) { // Track circular dependencies (add both directions for easy look up) if (projectsDependOnCurrentProject.includes(dependentProject.source)) { circularDependencies.add(`${dependentProject.source}:${dependentProject.target}`); circularDependencies.add(`${dependentProject.target}:${dependentProject.source}`); } let isInCurrentBatch = options.projects.some((project) => project.name === dependentProject.source); // For version-plans, we don't just need to consider the current batch of projects, but also the ones that are actually being updated as part of the plan file(s) if (isInCurrentBatch && options.specifierSource === 'version-plans') { isInCurrentBatch = (options.releaseGroup.resolvedVersionPlans || []).some((plan) => { if ('projectVersionBumps' in plan) { return plan.projectVersionBumps[dependentProject.source]; } return true; }); } if (!isInCurrentBatch) { dependentProjectsOutsideCurrentBatch.push(dependentProject); } else { dependentProjectsInCurrentBatch.push(dependentProject); } } // If not always updating dependents (when they don't already appear in the batch itself), print a warning to the user about what is being skipped and how to change it if (updateDependents === 'never' || options.releaseGroup.projectsRelationship !== 'independent') { if (dependentProjectsOutsideCurrentBatch.length > 0) { let logMsg = `⚠️ Warning, the following packages depend on "${project.name}"`; const reason = options.specifierSource === 'version-plans' ? 'because they are not referenced in any version plans' : 'via --projects'; if (options.releaseGroup.name === config_1.IMPLICIT_DEFAULT_RELEASE_GROUP) { logMsg += ` but have been filtered out ${reason}, and therefore will not be updated:`; } else { logMsg += ` but are either not part of the current release group "${options.releaseGroup.name}", or have been filtered out ${reason}, and therefore will not be updated:`; } const indent = Array.from(new Array(projectName.length + 4)) .map(() => ' ') .join(''); logMsg += `\n${dependentProjectsOutsideCurrentBatch .map((dependentProject) => `${indent}- ${dependentProject.source}`) .join('\n')}`; logMsg += `\n${indent}=> You can adjust this behavior by removing the usage of \`version.generatorOptions.updateDependents\` with "never"`; logger.buffer(logMsg); } } if (!currentVersion) { throw new Error(`The current version for project "${project.name}" could not be resolved. Please report this on https://github.com/nrwl/nx`); } versionData[projectName] = { currentVersion, newVersion: null, // will stay as null in the final result in the case that no changes are detected dependentProjects: updateDependents === 'auto' && options.releaseGroup.projectsRelationship === 'independent' ? allDependentProjects : dependentProjectsInCurrentBatch, }; if (!specifier) { logger.buffer(`🚫 Skipping versioning "${packageJson.name}" as no changes were detected.`); // Print the buffered logs for this unchanged project, as long as the user has not explicitly disabled this behavior if (logUnchangedProjects) { logger.flush(); } continue; } const newVersion = (0, version_legacy_1.deriveNewSemverVersion)(currentVersion, specifier, options.preid); versionData[projectName].newVersion = newVersion; (0, devkit_1.writeJson)(tree, packageJsonPath, { ...packageJson, version: newVersion, }); logger.buffer(`✍️ New version ${newVersion} written to ${packageJsonPath}`); if (allDependentProjects.length > 0) { const totalProjectsToUpdate = updateDependents === 'auto' && options.releaseGroup.projectsRelationship === 'independent' ? allDependentProjects.length + // Only count transitive dependents that aren't already direct dependents transitiveLocalPackageDependents.filter((transitive) => !allDependentProjects.some((direct) => direct.source === transitive.source)).length - // There are two entries per circular dep circularDependencies.size / 2 : dependentProjectsInCurrentBatch.length; if (totalProjectsToUpdate > 0) { logger.buffer(`✍️ Applying new version ${newVersion} to ${totalProjectsToUpdate} ${totalProjectsToUpdate > 1 ? 'packages which depend' : 'package which depends'} on ${project.name}`); } } const updateDependentProjectAndAddToVersionData = ({ dependentProject, dependencyPackageName, newDependencyVersion, forceVersionBump, }) => { const updatedFilePath = (0, devkit_1.joinPathFragments)(projectNameToPackageRootMap.get(dependentProject.source), 'package.json'); (0, devkit_1.updateJson)(tree, updatedFilePath, (json) => { // Auto (i.e.infer existing) by default let versionPrefix = options.versionPrefix ?? 'auto'; const currentDependencyVersion = json[dependentProject.dependencyCollection][dependencyPackageName]; // Depending on the package manager, locally linked packages could reference packages with `"private": true` and no version field at all const currentPackageVersion = json.version ?? null; if (!currentPackageVersion && (0, is_locally_linked_package_version_1.isLocallyLinkedPackageVersion)(currentDependencyVersion)) { if (forceVersionBump) { // Look up any dependent projects from the transitiveLocalPackageDependents list const transitiveDependentProjects = transitiveLocalPackageDependents.filter((localPackageDependency) => localPackageDependency.target === dependentProject.source); versionData[dependentProject.source] = { currentVersion: currentPackageVersion, newVersion: currentDependencyVersion, dependentProjects: transitiveDependentProjects, }; } return json; } // For auto, we infer the prefix based on the current version of the dependent if (versionPrefix === 'auto') { versionPrefix = ''; // we don't want to end up printing auto if (currentDependencyVersion) { const prefixMatch = currentDependencyVersion.match(/^[~^]/); if (prefixMatch) { versionPrefix = prefixMatch[0]; } else { versionPrefix = ''; } } } // Apply the new version of the dependency to the dependent (if not preserving locally linked package protocols) const shouldUpdateDependency = !((0, is_locally_linked_package_version_1.isLocallyLinkedPackageVersion)(currentDependencyVersion) && options.preserveLocalDependencyProtocols); if (shouldUpdateDependency) { const newDepVersion = `${versionPrefix}${newDependencyVersion}`; json[dependentProject.dependencyCollection][dependencyPackageName] = newDepVersion; } // Bump the dependent's version if applicable and record it in the version data if (forceVersionBump) { const newPackageVersion = (0, version_legacy_1.deriveNewSemverVersion)(currentPackageVersion, forceVersionBump, options.preid); json.version = newPackageVersion; // Look up any dependent projects from the transitiveLocalPackageDependents list const transitiveDependentProjects = transitiveLocalPackageDependents.filter((localPackageDependency) => localPackageDependency.target === dependentProject.source); versionData[dependentProject.source] = { currentVersion: currentPackageVersion, newVersion: newPackageVersion, dependentProjects: transitiveDependentProjects, }; } return json; }); }; for (const dependentProject of dependentProjectsInCurrentBatch) { if (projectToDependencyBumps.has(dependentProject.source)) { const dependencyBumps = projectToDependencyBumps.get(dependentProject.source); dependencyBumps.add(projectName); } else { projectToDependencyBumps.set(dependentProject.source, new Set([projectName])); } updateDependentProjectAndAddToVersionData({ dependentProject, dependencyPackageName: packageName, newDependencyVersion: newVersion, // We don't force bump because we know they will come later in the topologically sorted projects loop and may have their own version update logic to take into account forceVersionBump: false, }); } if (updateDependents === 'auto' && options.releaseGroup.projectsRelationship === 'independent') { for (const dependentProject of dependentProjectsOutsideCurrentBatch) { if (options.specifierSource === 'version-plans' && !projectToDependencyBumps.has(dependentProject.source)) { projectToDependencyBumps.set(dependentProject.source, new Set([projectName])); } updateDependentProjectAndAddToVersionData({ dependentProject, dependencyPackageName: packageName, newDependencyVersion: newVersion, // For these additional dependents, we need to update their package.json version as well because we know they will not come later in the topologically sorted projects loop // (Unless using version plans and the dependent is not filtered out by --projects) forceVersionBump: options.specifierSource === 'version-plans' && projects.find((p) => p.name === dependentProject.source) ? false : updateDependentsBump, }); } } for (const transitiveDependentProject of transitiveLocalPackageDependents) { const isAlreadyDirectDependent = allDependentProjects.some((dep) => dep.source === transitiveDependentProject.source); if (isAlreadyDirectDependent) { // Don't continue directly in this scenario - we still need to update the dependency version // but we don't want to bump the project's own version as it will end up being double patched const dependencyProjectName = transitiveDependentProject.target; const dependencyPackageRoot = projectNameToPackageRootMap.get(dependencyProjectName); if (!dependencyPackageRoot) { throw new Error(`The project "${dependencyProjectName}" does not have a packageRoot available. Please report this issue on https://github.com/nrwl/nx`); } const dependencyPackageJsonPath = (0, devkit_1.joinPathFragments)(dependencyPackageRoot, 'package.json'); const dependencyPackageJson = (0, devkit_1.readJson)(tree, dependencyPackageJsonPath); updateDependentProjectAndAddToVersionData({ dependentProject: transitiveDependentProject, dependencyPackageName: dependencyPackageJson.name, newDependencyVersion: dependencyPackageJson.version, forceVersionBump: false, // Never bump version for direct dependents }); continue; } // Check if the transitive dependent originates from a circular dependency const isFromCircularDependency = circularDependencies.has(`${transitiveDependentProject.source}:${transitiveDependentProject.target}`); const dependencyProjectName = transitiveDependentProject.target; const dependencyPackageRoot = projectNameToPackageRootMap.get(dependencyProjectName); if (!dependencyPackageRoot) { throw new Error(`The project "${dependencyProjectName}" does not have a packageRoot available. Please report this issue on https://github.com/nrwl/nx`); } const dependencyPackageJsonPath = (0, devkit_1.joinPathFragments)(dependencyPackageRoot, 'package.json'); const dependencyPackageJson = (0, devkit_1.readJson)(tree, dependencyPackageJsonPath); updateDependentProjectAndAddToVersionData({ dependentProject: transitiveDependentProject, dependencyPackageName: dependencyPackageJson.name, newDependencyVersion: dependencyPackageJson.version, /** * For these additional dependents, we need to update their package.json version as well because we know they will not come later in the topologically sorted projects loop. * The one exception being if the dependent is part of a circular dependency, in which case we don't want to force a version bump as this would come in addition to the one * already applied. */ forceVersionBump: isFromCircularDependency ? false : updateDependentsBump, }); } // Print the logs that have been buffered for this project logger.flush(); } /** * Ensure that formatting is applied so that version bump diffs are as minimal as possible * within the context of the user's workspace. */ await (0, devkit_1.formatFiles)(tree); // Return the version data so that it can be leveraged by the overall version command return { data: versionData, callback: async (tree, { generatorOptions, ...opts }) => { const changedFiles = []; const deletedFiles = []; for (const cb of deleteVersionPlanCallbacks) { deletedFiles.push(...(await cb(opts.dryRun))); } const cwd = tree.root; changedFiles.push(...(await (0, update_lock_file_1.updateLockFile)(cwd, { ...opts, useLegacyVersioning: true, options: generatorOptions, }))); return { changedFiles, deletedFiles }; }, }; } catch (e) { // Flush any pending logs before printing the error to make troubleshooting easier logger?.flush(); if (process.env.NX_VERBOSE_LOGGING === 'true') { devkit_1.output.error({ title: e.message, }); // Dump the full stack trace in verbose mode console.error(e); } else { devkit_1.output.error({ title: e.message, }); } process.exit(1); } } exports.default = releaseVersionGenerator; function createResolvePackageRoot(customPackageRoot) { return (projectNode) => { // Default to the project root if no custom packageRoot if (!customPackageRoot) { return projectNode.data.root; } if (projectNode.data.root === '.') { // This is a temporary workaround to fix NXC-574 until NXC-573 is resolved. // This has been fixed in "versioning v2" return projectNode.data.root; } return (0, utils_1.interpolate)(customPackageRoot, { workspaceRoot: '', projectRoot: projectNode.data.root, projectName: projectNode.name, }); }; } const colors = [ { instance: chalk.green, spinnerColor: 'green' }, { instance: chalk.greenBright, spinnerColor: 'green' }, { instance: chalk.red, spinnerColor: 'red' }, { instance: chalk.redBright, spinnerColor: 'red' }, { instance: chalk.cyan, spinnerColor: 'cyan' }, { instance: chalk.cyanBright, spinnerColor: 'cyan' }, { instance: chalk.yellow, spinnerColor: 'yellow' }, { instance: chalk.yellowBright, spinnerColor: 'yellow' }, { instance: chalk.magenta, spinnerColor: 'magenta' }, { instance: chalk.magentaBright, spinnerColor: 'magenta' }, ]; function getColor(projectName) { let code = 0; for (let i = 0; i < projectName.length; ++i) { code += projectName.charCodeAt(i); } const colorIndex = code % colors.length; return colors[colorIndex]; } class ProjectLogger { constructor(projectName, color) { this.projectName = projectName; this.color = color; this.logs = []; } buffer(msg) { this.logs.push(msg); } flush() { devkit_1.output.logSingleLine(`Running release version for project: ${this.color.instance.bold(this.projectName)}`); this.logs.forEach((msg) => { console.log(this.color.instance.bold(this.projectName) + ' ' + msg); }); } } /** * Allow users to be unblocked when locally running releases for the very first time with certain combinations that require an initial * version in order to function (e.g. a relative semver bump derived via conventional commits or version plans) by providing an interactive * prompt to let them opt into using 0.0.0 as the implied current version. */ async function handleNoAvailableDiskFallback({ logger, projectName, packageJsonPath, specifierSource, currentVersionSourceMessage, resolutionSuggestion, }) { const unresolvableCurrentVersionError = new Error(`Unable to resolve the current version ${currentVersionSourceMessage} and there is no version on disk to fall back to. This is invalid with ${specifierSource} because the new version is determined by relatively bumping the current version. To resolve this, ${resolutionSuggestion}, or set an appropriate value for "version" in ${packageJsonPath}`); if (process.env.CI === 'true') { // We can't prompt in CI, so error immediately throw unresolvableCurrentVersionError; } try { const reply = await (0, enquirer_1.prompt)([ {