@nx/js
Version:
762 lines (759 loc) • 51.2 kB
JavaScript
"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)([
{