UNPKG

nx

Version:

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

931 lines (929 loc) 57.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ReleaseGroupProcessor = exports.BUMP_TYPE_REASON_TEXT = void 0; const semver = require("semver"); const config_1 = require("../config/config"); const git_1 = require("../utils/git"); const resolve_semver_specifier_1 = require("../utils/resolve-semver-specifier"); const version_1 = require("../version"); const derive_specifier_from_conventional_commits_1 = require("./derive-specifier-from-conventional-commits"); const deriver_specifier_from_version_plans_1 = require("./deriver-specifier-from-version-plans"); const project_logger_1 = require("./project-logger"); const resolve_current_version_1 = require("./resolve-current-version"); const topological_sort_1 = require("./topological-sort"); const version_actions_1 = require("./version-actions"); exports.BUMP_TYPE_REASON_TEXT = { DEPENDENCY_WAS_BUMPED: ', because a dependency was bumped, ', USER_SPECIFIER: ', from the given specifier, ', PROMPTED_USER_SPECIFIER: ', from the prompted specifier, ', CONVENTIONAL_COMMITS: ', derived from conventional commits data, ', VERSION_PLANS: ', read from version plan {versionPlanPath}, ', DEPENDENCY_ACROSS_GROUPS_WAS_BUMPED: ', because a dependency project belonging to another release group was bumped, ', OTHER_PROJECT_IN_FIXED_GROUP_WAS_BUMPED_DUE_TO_DEPENDENCY: ', because of a dependency-only bump to another project in the same fixed release group, ', }; class ReleaseGroupProcessor { constructor(tree, projectGraph, nxReleaseConfig, releaseGroups, releaseGroupToFilteredProjects, options) { this.tree = tree; this.projectGraph = projectGraph; this.nxReleaseConfig = nxReleaseConfig; this.releaseGroups = releaseGroups; this.releaseGroupToFilteredProjects = releaseGroupToFilteredProjects; this.options = options; /** * Stores the relationships between release groups, including their dependencies * and dependents. This is used for determining processing order and propagating * version changes between related groups. */ this.groupGraph = new Map(); /** * Tracks which release groups have already been processed to avoid * processing them multiple times. Used during the group traversal. */ this.processedGroups = new Set(); /** * Keeps track of which projects have already had their versions bumped. * This is used to avoid redundant version bumping and to determine which * projects need their dependencies updated. */ this.bumpedProjects = new Set(); /** * Cache of release groups sorted in topological order to ensure dependencies * are processed before dependents. Computed once and reused throughout processing. */ this.sortedReleaseGroups = []; /** * Maps each release group to its projects sorted in topological order. * Ensures projects are processed after their dependencies within each group. */ this.sortedProjects = new Map(); /** * Track the unique afterAllProjectsVersioned functions involved in the current versioning process, * so that we can ensure they are only invoked once per versioning execution. */ this.uniqueAfterAllProjectsVersioned = new Map(); /** * Track the versionActions for each project so that we can invoke certain instance methods. */ this.projectsToVersionActions = new Map(); /** * versionData that will ultimately be returned to the nx release version handler by getVersionData() */ this.versionData = new Map(); /** * Set of all projects that are configured in the nx release config. * Used to validate dependencies and identify projects that should be updated. */ this.allProjectsConfiguredForNxRelease = new Set(); /** * Set of projects that will be processed in the current run. * This is potentially a subset of allProjectsConfiguredForNxRelease based on filters * and dependency relationships. */ this.allProjectsToProcess = new Set(); /** * Caches the current version of each project to avoid repeated disk/registry/git tag lookups. * Often used during new version calculation. Will be null if the current version resolver is set to 'none'. */ this.cachedCurrentVersions = new Map(); /** * Caches git tag information for projects that resolve their version from git tags. * This avoids performing expensive git operations multiple times for the same project. */ this.cachedLatestMatchingGitTag = new Map(); /** * Temporary storage for dependent project names while building the dependency graph. * This is used as an intermediate step before creating the full dependent projects data. */ this.tmpCachedDependentProjects = new Map(); /** * Resolve the data regarding dependent projects for each project upfront so that it remains accurate * even after updates are applied to manifests. */ this.originalDependentProjectsPerProject = new Map(); /** * In the case of fixed release groups that are configured to resolve the current version from a registry * or a git tag, it would be a waste of time and resources to resolve the current version for each individual * project, therefore we maintain a cache of the current version for each applicable fixed release group here. */ this.currentVersionsPerFixedReleaseGroup = new Map(); /** * Cache of project loggers for each project. */ this.projectLoggers = new Map(); /** * Track any version plan files that have been processed so that we can delete them after versioning is complete, * while leaving any unprocessed files in place. */ this.processedVersionPlanFiles = new Set(); /** * Certain configuration options can be overridden at the project level, and otherwise fall back to the release group level. * Many also have a specific default value if nothing is set at either level. To avoid applying this hierarchy for each project * every time such a configuration option is needed, we cache the result per project here. */ this.finalConfigsByProject = new Map(); /** * Maps each project to its release group for quick O(1) lookups. * This avoids having to scan through all release groups to find a project. */ this.projectToReleaseGroup = new Map(); /** * Maps each project to its dependents (projects that depend on it). * This is the inverse of the projectToDependencies map and enables * efficient lookup of dependent projects for propagating version changes. */ this.projectToDependents = new Map(); /** * Maps each project to its dependencies (projects it depends on). * Used for building dependency graphs and determining processing order. */ this.projectToDependencies = new Map(); /** * Caches the updateDependents setting for each project to avoid repeated * lookups and calculations. This determines if dependent projects should * be automatically updated when a dependency changes. */ this.projectToUpdateDependentsSetting = new Map(); /** * To match legacy versioning behavior in the case of semver versions with leading "v" characters, * e.g. "v1.0.0", we strip the leading "v" and use the rest of the string as the user given specifier * to ensure that it is valid. If it is a non-semver version, we just use the string as is. * * TODO: re-evaluate if this is definitely what we want... Maybe we can just delegate isValid to the * version actions implementation during prompting? */ if (options.userGivenSpecifier?.startsWith('v')) { const userGivenSpecifierWithoutLeadingV = options.userGivenSpecifier?.replace(/^v/, ''); if (semver.valid(userGivenSpecifierWithoutLeadingV)) { this.userGivenSpecifier = userGivenSpecifierWithoutLeadingV; } } else { this.userGivenSpecifier = options.userGivenSpecifier; } } /** * Initialize the processor by building the group graph and preparing for processing. * This method must be called before processGroups(). */ async init() { // Precompute project to release group mapping for O(1) lookups this.setupProjectReleaseGroupMapping(); // Setup projects to process and resolve version actions await this.setupProjectsToProcess(); // Precompute dependency relationships await this.precomputeDependencyRelationships(); // Process dependency graph to find dependents to process this.findDependentsToProcess(); // Build the group graph structure for (const group of this.releaseGroups) { this.groupGraph.set(group.name, { group, dependencies: new Set(), dependents: new Set(), }); } // Process each project within each release group for (const [, releaseGroupNode] of this.groupGraph) { for (const projectName of releaseGroupNode.group.projects) { const projectGraphNode = this.projectGraph.nodes[projectName]; // Check if the project has been filtered out of explicit versioning before continuing any further if (!this.allProjectsToProcess.has(projectName)) { continue; } const versionActions = this.getVersionActionsForProject(projectName); const finalConfigForProject = this.getFinalConfigForProject(projectName); let latestMatchingGitTag; const releaseTagPattern = releaseGroupNode.group.releaseTagPattern; // Cache the last matching git tag for relevant projects if (finalConfigForProject.currentVersionResolver === 'git-tag') { latestMatchingGitTag = await (0, git_1.getLatestGitTagForPattern)(releaseTagPattern, { projectName: projectGraphNode.name, }, { checkAllBranchesWhen: releaseGroupNode.group.releaseTagPatternCheckAllBranchesWhen, preid: this.options.preid, releaseTagPatternRequireSemver: releaseGroupNode.group.releaseTagPatternRequireSemver, releaseTagPatternStrictPreid: releaseGroupNode.group.releaseTagPatternStrictPreid, }); this.cachedLatestMatchingGitTag.set(projectName, latestMatchingGitTag); } // Cache the current version for the project const currentVersion = await (0, resolve_current_version_1.resolveCurrentVersion)(this.tree, projectGraphNode, releaseGroupNode.group, versionActions, this.projectLoggers.get(projectName), this.currentVersionsPerFixedReleaseGroup, finalConfigForProject, releaseTagPattern, latestMatchingGitTag); this.cachedCurrentVersions.set(projectName, currentVersion); } // Ensure that there is an entry in versionData for each project being processed, even if they don't end up being bumped for (const projectName of this.allProjectsToProcess) { this.versionData.set(projectName, { currentVersion: this.getCurrentCachedVersionForProject(projectName), newVersion: null, dependentProjects: this.getOriginalDependentProjects(projectName), }); } } // Build the dependency relationships between groups this.buildGroupDependencyGraph(); // Topologically sort the release groups and projects for efficient processing this.sortedReleaseGroups = this.topologicallySortReleaseGroups(); // Sort projects within each release group for (const group of this.releaseGroups) { this.sortedProjects.set(group.name, this.topologicallySortProjects(group)); } // Populate the dependent projects data await this.populateDependentProjectsData(); } /** * Setup mapping from project to release group and cache updateDependents settings */ setupProjectReleaseGroupMapping() { for (const group of this.releaseGroups) { for (const project of group.projects) { this.projectToReleaseGroup.set(project, group); // Cache updateDependents setting relevant for each project const updateDependents = group.version ?.updateDependents || 'auto'; this.projectToUpdateDependentsSetting.set(project, updateDependents); } } } /** * Determine which projects should be processed and resolve their version actions */ async setupProjectsToProcess() { // Track the projects being directly versioned let projectsToProcess = new Set(); const resolveVersionActionsForProjectCallbacks = []; // Precompute all projects in nx release config for (const [groupName, group] of Object.entries(this.nxReleaseConfig.groups)) { for (const project of group.projects) { this.allProjectsConfiguredForNxRelease.add(project); // Create a project logger for the project this.projectLoggers.set(project, new project_logger_1.ProjectLogger(project)); // If group filtering is applied and the current group is captured by the filter, add the project to the projectsToProcess if (this.options.filters.groups?.includes(groupName)) { projectsToProcess.add(project); // Otherwise, if project filtering is applied and the current project is captured by the filter, add the project to the projectsToProcess } else if (this.options.filters.projects?.includes(project)) { projectsToProcess.add(project); } const projectGraphNode = this.projectGraph.nodes[project]; /** * Try and resolve a cached ReleaseGroupWithName for the project. It may not be present * if the user filtered by group and excluded this parent group from direct versioning, * so fallback to the release group config and apply the name manually. */ let releaseGroup = this.projectToReleaseGroup.get(project); if (!releaseGroup) { releaseGroup = { ...group, name: groupName, resolvedVersionPlans: false, }; } // Resolve the final configuration for the project const finalConfigForProject = this.resolveFinalConfigForProject(releaseGroup, projectGraphNode); this.finalConfigsByProject.set(project, finalConfigForProject); /** * For our versionActions validation to accurate, we need to wait until the full allProjectsToProcess * set is populated so that all dependencies, including those across release groups, are included. * * In order to save us fully traversing the graph again to arrive at this project level, schedule a callback * to resolve the versionActions for the project only once we have all the projects to process. */ resolveVersionActionsForProjectCallbacks.push(async () => { const { versionActionsPath, versionActions, afterAllProjectsVersioned, } = await (0, version_actions_1.resolveVersionActionsForProject)(this.tree, releaseGroup, projectGraphNode, finalConfigForProject, // Will be fully populated by the time this callback is executed this.allProjectsToProcess.has(project)); if (!this.uniqueAfterAllProjectsVersioned.has(versionActionsPath)) { this.uniqueAfterAllProjectsVersioned.set(versionActionsPath, afterAllProjectsVersioned); } this.projectsToVersionActions.set(project, versionActions); }); } } // If no filters are applied, process all projects if (!this.options.filters.groups?.length && !this.options.filters.projects?.length) { projectsToProcess = this.allProjectsConfiguredForNxRelease; } // If no projects are set to be processed, throw an error. This should be impossible because the filter validation in version.ts should have already caught this if (projectsToProcess.size === 0) { throw new Error('No projects are set to be processed, please report this as a bug on https://github.com/nrwl/nx/issues'); } this.allProjectsToProcess = new Set(projectsToProcess); // Execute all the callbacks to resolve the version actions for the projects for (const cb of resolveVersionActionsForProjectCallbacks) { await cb(); } } /** * Find all dependents that should be processed due to dependency updates */ findDependentsToProcess() { const projectsToProcess = Array.from(this.allProjectsToProcess); const allTrackedDependents = new Set(); const dependentsToProcess = new Set(); // BFS traversal of dependency graph to find all transitive dependents let currentLevel = [...projectsToProcess]; while (currentLevel.length > 0) { const nextLevel = []; // Get all dependents for the current level at once const dependents = this.getAllNonImplicitDependents(currentLevel); // Process each dependent for (const dep of dependents) { // Skip if we've already seen this dependent or it's already in projectsToProcess if (allTrackedDependents.has(dep) || this.allProjectsToProcess.has(dep)) { continue; } // Track that we've seen this dependent allTrackedDependents.add(dep); // If both the dependent and its dependency have updateDependents='auto', // add the dependent to the projects to process if (this.hasAutoUpdateDependents(dep)) { // Check if any of its dependencies in the current level have auto update const hasDependencyWithAutoUpdate = currentLevel.some((proj) => this.hasAutoUpdateDependents(proj) && this.getProjectDependents(proj).has(dep)); if (hasDependencyWithAutoUpdate) { dependentsToProcess.add(dep); } } // Add to next level for traversal nextLevel.push(dep); } // Move to next level currentLevel = nextLevel; } // Add all dependents that should be processed to allProjectsToProcess dependentsToProcess.forEach((dep) => this.allProjectsToProcess.add(dep)); } buildGroupDependencyGraph() { for (const [releaseGroupName, releaseGroupNode] of this.groupGraph) { for (const projectName of releaseGroupNode.group.projects) { const projectDeps = this.getProjectDependencies(projectName); for (const dep of projectDeps) { const dependencyGroup = this.getReleaseGroupNameForProject(dep); if (dependencyGroup && dependencyGroup !== releaseGroupName) { releaseGroupNode.dependencies.add(dependencyGroup); this.groupGraph .get(dependencyGroup) .dependents.add(releaseGroupName); } } } } } async populateDependentProjectsData() { for (const [projectName, dependentProjectNames] of this .tmpCachedDependentProjects) { const dependentProjectsData = []; for (const dependentProjectName of dependentProjectNames) { const versionActions = this.getVersionActionsForProject(dependentProjectName); const { currentVersion, dependencyCollection } = await versionActions.readCurrentVersionOfDependency(this.tree, this.projectGraph, projectName); dependentProjectsData.push({ source: dependentProjectName, target: projectName, type: 'static', dependencyCollection, rawVersionSpec: currentVersion, }); } this.originalDependentProjectsPerProject.set(projectName, dependentProjectsData); } } getReleaseGroupNameForProject(projectName) { const group = this.projectToReleaseGroup.get(projectName); return group ? group.name : null; } getNextGroup() { for (const [groupName, groupNode] of this.groupGraph) { if (!this.processedGroups.has(groupName) && Array.from(groupNode.dependencies).every((dep) => this.processedGroups.has(dep))) { return groupName; } } return null; } async processGroups() { const processOrder = []; // Use the topologically sorted groups instead of getNextGroup for (const nextGroup of this.sortedReleaseGroups) { // Skip groups that have already been processed (could happen with circular dependencies) if (this.processedGroups.has(nextGroup)) { continue; } const allDependenciesProcessed = Array.from(this.groupGraph.get(nextGroup).dependencies).every((dep) => this.processedGroups.has(dep)); if (!allDependenciesProcessed) { // If we encounter a group whose dependencies aren't all processed, // it means there's a circular dependency that our topological sort broke. // We need to process any unprocessed dependencies first. for (const dep of this.groupGraph.get(nextGroup).dependencies) { if (!this.processedGroups.has(dep)) { await this.processGroup(dep); this.processedGroups.add(dep); processOrder.push(dep); } } } await this.processGroup(nextGroup); this.processedGroups.add(nextGroup); processOrder.push(nextGroup); } return processOrder; } flushAllProjectLoggers() { for (const projectLogger of this.projectLoggers.values()) { projectLogger.flush(); } } deleteProcessedVersionPlanFiles() { for (const versionPlanPath of this.processedVersionPlanFiles) { this.tree.delete(versionPlanPath); } } getVersionData() { return Object.fromEntries(this.versionData); } /** * Invoke the afterAllProjectsVersioned functions for each unique versionActions type. * This can be useful for performing actions like updating a workspace level lock file. * * Because the tree has already been flushed to disk at this point, each afterAllProjectsVersioned * function is responsible for returning the list of changed and deleted files that it affected. * * The root level `release.version.versionActionsOptions` is what is passed in here because this * is a one time action for the whole workspace. Release group and project level overrides are * not applicable. */ async afterAllProjectsVersioned(rootVersionActionsOptions) { const changedFiles = new Set(); const deletedFiles = new Set(); for (const [, afterAllProjectsVersioned] of this .uniqueAfterAllProjectsVersioned) { const { changedFiles: changedFilesForVersionActions, deletedFiles: deletedFilesForVersionActions, } = await afterAllProjectsVersioned(this.tree.root, { dryRun: this.options.dryRun, verbose: this.options.verbose, rootVersionActionsOptions, }); for (const file of changedFilesForVersionActions) { changedFiles.add(file); } for (const file of deletedFilesForVersionActions) { deletedFiles.add(file); } } return { changedFiles: Array.from(changedFiles), deletedFiles: Array.from(deletedFiles), }; } async processGroup(releaseGroupName) { const groupNode = this.groupGraph.get(releaseGroupName); const bumped = await this.bumpVersions(groupNode.group); // Flush the project loggers for the group for (const project of groupNode.group.projects) { const projectLogger = this.getProjectLoggerForProject(project); projectLogger.flush(); } if (bumped) { await this.propagateChangesToDependentGroups(releaseGroupName); } } async propagateChangesToDependentGroups(changedReleaseGroupName) { const changedGroupNode = this.groupGraph.get(changedReleaseGroupName); for (const depGroupName of changedGroupNode.dependents) { if (!this.processedGroups.has(depGroupName)) { await this.propagateChanges(depGroupName, changedReleaseGroupName); } } } async bumpVersions(releaseGroup) { if (releaseGroup.projectsRelationship === 'fixed') { return this.bumpFixedVersionGroup(releaseGroup); } else { return this.bumpIndependentVersionGroup(releaseGroup); } } async bumpFixedVersionGroup(releaseGroup) { if (releaseGroup.projects.length === 0) { return false; } let bumped = false; const firstProject = releaseGroup.projects[0]; const { newVersionInput, newVersionInputReason, newVersionInputReasonData, } = await this.determineVersionBumpForProject(releaseGroup, firstProject); if (newVersionInput === 'none') { // No direct bump for this group, but we may still need to bump if a dependency group has been bumped let bumpedByDependency = false; // Use sorted projects to check for dependencies in processed groups const sortedProjects = this.sortedProjects.get(releaseGroup.name) || []; // Iterate through each project in the release group in topological order for (const project of sortedProjects) { const dependencies = this.projectGraph.dependencies[project] || []; for (const dep of dependencies) { const depGroup = this.getReleaseGroupNameForProject(dep.target); if (depGroup && depGroup !== releaseGroup.name && this.processedGroups.has(depGroup)) { const depGroupBumpType = await this.getFixedReleaseGroupBumpType(depGroup); // If a dependency group has been bumped, determine if it should trigger a bump in this group if (depGroupBumpType !== 'none') { bumpedByDependency = true; const depBumpType = this.determineSideEffectBump(releaseGroup, depGroupBumpType); await this.bumpVersionForProject(project, depBumpType, 'DEPENDENCY_ACROSS_GROUPS_WAS_BUMPED', {}); this.bumpedProjects.add(project); // Update any dependencies in the manifest await this.updateDependenciesForProject(project); } } } } // If any project in the group was bumped due to dependency changes, we must bump all projects in the fixed group if (bumpedByDependency) { // Update all projects in topological order for (const project of sortedProjects) { if (!this.bumpedProjects.has(project)) { await this.bumpVersionForProject(project, 'patch', 'OTHER_PROJECT_IN_FIXED_GROUP_WAS_BUMPED_DUE_TO_DEPENDENCY', {}); // Ensure the bump for remaining projects this.bumpedProjects.add(project); await this.updateDependenciesForProject(project); } } } else { /** * No projects in the group are being bumped, but as it stands only the first project would have an appropriate log, * therefore add in an extra log for each additional project in the group, and we also need to make sure that the * versionData is fully populated. */ for (const project of releaseGroup.projects) { this.versionData.set(project, { currentVersion: this.getCurrentCachedVersionForProject(project), newVersion: null, dependentProjects: this.getOriginalDependentProjects(project), }); if (project === firstProject) { continue; } const projectLogger = this.getProjectLoggerForProject(project); projectLogger.buffer(`🚫 Skipping versioning for ${project} as it is a part of a fixed release group with ${firstProject} and no dependency bumps were detected`); } } return bumpedByDependency; } const { newVersion } = await this.calculateNewVersion(firstProject, newVersionInput, newVersionInputReason, newVersionInputReasonData); // Use sorted projects for processing projects in the right order const sortedProjects = this.sortedProjects.get(releaseGroup.name) || releaseGroup.projects; // First, update versions for all projects in the fixed group in topological order for (const project of sortedProjects) { const versionActions = this.getVersionActionsForProject(project); const projectLogger = this.getProjectLoggerForProject(project); const currentVersion = this.getCurrentCachedVersionForProject(project); // The first project's version was determined above, so this log is only appropriate for the remaining projects if (project !== firstProject) { projectLogger.buffer(`❓ Applied version ${newVersion} directly, because the project is a member of a fixed release group containing ${firstProject}`); } /** * Update the project's version based on the implementation details of the configured VersionActions * and display any returned log messages to the user. */ const logMessages = await versionActions.updateProjectVersion(this.tree, newVersion); for (const logMessage of logMessages) { projectLogger.buffer(logMessage); } this.bumpedProjects.add(project); bumped = true; // Populate version data for each project this.versionData.set(project, { currentVersion, newVersion, dependentProjects: this.getOriginalDependentProjects(project), }); } // Then, update dependencies for all projects in the fixed group, also in topological order if (bumped) { for (const project of sortedProjects) { await this.updateDependenciesForProject(project); } } return bumped; } async bumpIndependentVersionGroup(releaseGroup) { const releaseGroupFilteredProjects = this.releaseGroupToFilteredProjects.get(releaseGroup); let bumped = false; const projectBumpTypes = new Map(); const projectsToUpdate = new Set(); // First pass: Determine bump types for (const project of this.allProjectsToProcess) { const { newVersionInput: bumpType, newVersionInputReason: bumpTypeReason, newVersionInputReasonData: bumpTypeReasonData, } = await this.determineVersionBumpForProject(releaseGroup, project); projectBumpTypes.set(project, { bumpType, bumpTypeReason, bumpTypeReasonData, }); if (bumpType !== 'none') { projectsToUpdate.add(project); } } // Second pass: Update versions using topologically sorted projects // This ensures dependencies are processed before dependents const sortedProjects = this.sortedProjects.get(releaseGroup.name) || []; // Process projects in topological order for (const project of sortedProjects) { if (projectsToUpdate.has(project) && releaseGroupFilteredProjects.has(project)) { const { bumpType: finalBumpType, bumpTypeReason: finalBumpTypeReason, bumpTypeReasonData: finalBumpTypeReasonData, } = projectBumpTypes.get(project); if (finalBumpType !== 'none') { await this.bumpVersionForProject(project, finalBumpType, finalBumpTypeReason, finalBumpTypeReasonData); this.bumpedProjects.add(project); bumped = true; } } } // Third pass: Update dependencies also in topological order for (const project of sortedProjects) { if (projectsToUpdate.has(project) && releaseGroupFilteredProjects.has(project)) { await this.updateDependenciesForProject(project); } } return bumped; } async determineVersionBumpForProject(releaseGroup, projectName) { // User given specifier has the highest precedence if (this.userGivenSpecifier) { return { newVersionInput: this.userGivenSpecifier, newVersionInputReason: 'USER_SPECIFIER', newVersionInputReasonData: {}, }; } const projectGraphNode = this.projectGraph.nodes[projectName]; const projectLogger = this.getProjectLoggerForProject(projectName); const cachedFinalConfigForProject = this.getCachedFinalConfigForProject(projectName); if (cachedFinalConfigForProject.specifierSource === 'conventional-commits') { const currentVersion = this.getCurrentCachedVersionForProject(projectName); const bumpType = await (0, derive_specifier_from_conventional_commits_1.deriveSpecifierFromConventionalCommits)(this.nxReleaseConfig, this.projectGraph, projectLogger, releaseGroup, projectGraphNode, !!semver.prerelease(currentVersion ?? ''), this.cachedLatestMatchingGitTag.get(projectName), cachedFinalConfigForProject.fallbackCurrentVersionResolver, this.options.preid); return { newVersionInput: bumpType, newVersionInputReason: 'CONVENTIONAL_COMMITS', newVersionInputReasonData: {}, }; } // Resolve the semver relative bump via version-plans if (releaseGroup.versionPlans) { const currentVersion = this.getCurrentCachedVersionForProject(projectName); const { bumpType, versionPlanPath } = await (0, deriver_specifier_from_version_plans_1.deriveSpecifierFromVersionPlan)(projectLogger, releaseGroup, projectGraphNode, currentVersion); if (bumpType !== 'none') { this.processedVersionPlanFiles.add(versionPlanPath); } return { newVersionInput: bumpType, newVersionInputReason: 'VERSION_PLANS', newVersionInputReasonData: { versionPlanPath, }, }; } // 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 (releaseGroup.name === config_1.IMPLICIT_DEFAULT_RELEASE_GROUP) { return log; } return `${log} within release group "${releaseGroup.name}"`; }; if (cachedFinalConfigForProject.specifierSource === 'prompt') { let specifier; if (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 ${releaseGroup.projects.length} matched projects(s)`)}?`, `${maybeLogReleaseGroup(`What is the exact version for the ${releaseGroup.projects.length} matched project(s)`)}?`); } return { newVersionInput: specifier, newVersionInputReason: 'PROMPTED_USER_SPECIFIER', newVersionInputReasonData: {}, }; } throw new Error(`Unhandled version bump config, please report this as a bug on https://github.com/nrwl/nx/issues`); } getVersionActionsForProject(projectName) { const versionActions = this.projectsToVersionActions.get(projectName); if (!versionActions) { throw new Error(`No versionActions found for project ${projectName}, please report this as a bug on https://github.com/nrwl/nx/issues`); } return versionActions; } getFinalConfigForProject(projectName) { const finalConfig = this.finalConfigsByProject.get(projectName); if (!finalConfig) { throw new Error(`No final config found for project ${projectName}, please report this as a bug on https://github.com/nrwl/nx/issues`); } return finalConfig; } getProjectLoggerForProject(projectName) { const projectLogger = this.projectLoggers.get(projectName); if (!projectLogger) { throw new Error(`No project logger found for project ${projectName}, please report this as a bug on https://github.com/nrwl/nx/issues`); } return projectLogger; } getCurrentCachedVersionForProject(projectName) { return this.cachedCurrentVersions.get(projectName); } getCachedFinalConfigForProject(projectName) { const cachedFinalConfig = this.finalConfigsByProject.get(projectName); if (!cachedFinalConfig) { throw new Error(`Unexpected error: No cached config found for project ${projectName}, please report this as a bug on https://github.com/nrwl/nx/issues`); } return cachedFinalConfig; } /** * Apply project and release group precedence and default values, as well as validate the final configuration, * ready to be cached. */ resolveFinalConfigForProject(releaseGroup, projectGraphNode) { const releaseGroupVersionConfig = releaseGroup.version; const projectVersionConfig = projectGraphNode.data.release?.version; /** * specifierSource * * If the user has provided a specifier, it always takes precedence, * so the effective specifier source is 'prompt', regardless of what * the project or release group config says. */ const specifierSource = this.userGivenSpecifier ? 'prompt' : projectVersionConfig?.specifierSource ?? releaseGroupVersionConfig?.specifierSource ?? 'prompt'; /** * versionPrefix, defaults to auto */ const versionPrefix = projectVersionConfig?.versionPrefix ?? releaseGroupVersionConfig?.versionPrefix ?? 'auto'; if (versionPrefix && !version_1.validReleaseVersionPrefixes.includes(versionPrefix)) { throw new Error(`Invalid value for versionPrefix: "${versionPrefix}" Valid values are: ${version_1.validReleaseVersionPrefixes .map((s) => `"${s}"`) .join(', ')}`); } /** * currentVersionResolver, defaults to disk */ const currentVersionResolver = projectVersionConfig?.currentVersionResolver ?? releaseGroupVersionConfig?.currentVersionResolver ?? 'disk'; if (specifierSource === 'conventional-commits' && currentVersionResolver !== 'git-tag') { throw new Error(`Invalid currentVersionResolver "${currentVersionResolver}" provided for project "${projectGraphNode.name}". Must be "git-tag" when "specifierSource" is "conventional-commits"`); } /** * currentVersionResolverMetadata, defaults to {} */ const currentVersionResolverMetadata = projectVersionConfig?.currentVersionResolverMetadata ?? releaseGroupVersionConfig?.currentVersionResolverMetadata ?? {}; /** * preserveLocalDependencyProtocols * * This was false by default in legacy versioning, but is true by default now. */ const preserveLocalDependencyProtocols = projectVersionConfig?.preserveLocalDependencyProtocols ?? releaseGroupVersionConfig?.preserveLocalDependencyProtocols ?? true; /** * fallbackCurrentVersionResolver, defaults to disk when performing a first release, otherwise undefined */ const fallbackCurrentVersionResolver = projectVersionConfig?.fallbackCurrentVersionResolver ?? releaseGroupVersionConfig?.fallbackCurrentVersionResolver ?? // Always fall back to disk if this is the first release (this.options.firstRelease ? 'disk' : undefined); /** * versionActionsOptions, defaults to {} */ let versionActionsOptions = projectVersionConfig?.versionActionsOptions ?? releaseGroupVersionConfig?.versionActionsOptions ?? {}; // Apply any optional overrides that may be passed in from the programmatic API versionActionsOptions = { ...versionActionsOptions, ...(this.options.versionActionsOptionsOverrides ?? {}), }; const manifestRootsToUpdate = (projectVersionConfig?.manifestRootsToUpdate ?? releaseGroupVersionConfig?.manifestRootsToUpdate ?? []).map((manifestRoot) => { if (typeof manifestRoot === 'string') { return { path: manifestRoot, // Apply the project level preserveLocalDependencyProtocols setting that was already resolved preserveLocalDependencyProtocols, }; } return manifestRoot; }); return { specifierSource, currentVersionResolver, currentVersionResolverMetadata, fallbackCurrentVersionResolver, versionPrefix, preserveLocalDependencyProtocols, versionActionsOptions, manifestRootsToUpdate, }; } async calculateNewVersion(project, newVersionInput, // any arbitrary string, whether or not it is valid is dependent upon the version actions implementation newVersionInputReason, newVersionInputReasonData) { const currentVersion = this.getCurrentCachedVersionForProject(project); const versionActions = this.getVersionActionsForProject(project); const { newVersion, logText } = await versionActions.calculateNewVersion(currentVersion, newVersionInput, newVersionInputReason, newVersionInputReasonData, this.options.preid); const projectLogger = this.getProjectLoggerForProject(project); projectLogger.buffer(logText); return { currentVersion, newVersion }; } async updateDependenciesForProject(projectName) { if (!this.allProjectsToProcess.has(projectName)) { throw new Error(`Unable to find ${projectName} in allProjectsToProcess, please report this as a bug on https://github.com/nrwl/nx/issues`); } const versionActions = this.getVersionActionsForProject(projectName); const cachedFinalConfigForProject = this.getCachedFinalConfigForProject(projectName); const dependenciesToUpdate = {}; const dependencies = this.projectGraph.dependencies[projectName] || []; for (const dep of dependencies) { if (this.allProjectsToProcess.has(dep.target) && this.bumpedProjects.has(dep.target)) { const targetVersionData = this.versionData.get(dep.target); if (targetVersionData) { const { currentVersion: currentDependencyVersion } = await versionActions.readCurrentVersionOfDependency(this.tree, this.projectGraph, dep.target); if (!currentDependencyVersion) { continue; } let finalPrefix = ''; if (cachedFinalConfigForProject.versionPrefix === 'auto') { const prefixMatch = currentDependencyVersion?.match(/^([~^=])/); finalPrefix = prefixMatch ? prefixMatch[1] : ''; } else if (['~', '^', '='].includes(cachedFinalConfigForProject.versionPrefix)) { finalPrefix = cachedFinalConfigForProject.versionPrefix; } // Remove any existing prefix from the new version before applying the finalPrefix const cleanNewVersion = targetVersionData.newVersion.replace(/^[~^=]/, ''); dependenciesToUpdate[dep.target] = `${finalPrefix}${cleanNewVersion}`; } } } const projectLogger = this.getProjectLoggerForProject(projectName); const logMessages = await versionActions.updateProjectDependencies(this.tree, this.projectGraph, dependenciesToUpdate); for (const logMessage of logMessages) { projectLogger.buffer(logMessage); } } async bumpVersionForProject(projectName, bumpType, bumpTypeReason, bumpTypeReasonData) { const projectLogger = this.getProjectLoggerForProject(projectName); if (bumpType === 'none') { projectLogger.buffer(`⏩ Skipping bump for ${projectName} as bump type is "none"`); return; } const versionActions = this.getVersionActionsForProject(projectName); const { currentVersion, newVersion } = await this.calculateNewVersion(projectName, bumpType, bumpTypeReason, bumpTypeReasonData); /** * Update the project's version based on the implementation details of the configured VersionActions * and display any returned log messages to the user. */ const logMessages = await versionActions.updateProjectVersion(this.tree, newVersion); for (const logMessage of logMessages) { projectLogger.buffer(logMessage); } // Update version data and bumped projects this.versionData.set(projectName, { currentVersion, newVersion, dependentProjects: this.getOriginalDependentProjects(projectName), }); this.bumpedProjects.add(projectName); // Find the release group for this project const releaseGroupName = this.getReleaseGroupNameForProject(projectName); if (!releaseGroupName) { projectLogger.buffer(`⚠️ Cannot find release group for ${projectName}, skipping dependent updates`); return; } const releaseGroup = this.groupGraph.get(releaseGroupName).group; const releaseGroupVersionConfig = releaseGroup.version; // Get updateDependents from the release group level config const updateDependents = releaseGroupVersionConfig?.updateDependents || 'auto'; // Only update dependencies for dependents if the group's updateDependents is 'auto' if (updateDependents === 'auto') { const dependents = this.getNonImplicitDependentsForProject(projectName); await this.updateDependenciesForDependents(dependents); for (const dependent of dependents) { if (this.allProjectsToProcess.has(dependent) && !this.bumpedProjects.has(dependent)) { await this.bumpVersionForProject(dependent, 'patch', 'DEPENDENCY_WAS_BUMPED', {}); } } } else { const releaseGroupText = releaseGroupName !== config_1.IMPLICIT_DEFAULT_RELEASE_GROUP