diff --git a/packages/js/src/generators/release-version/release-version.spec.ts b/packages/js/src/generators/release-version/release-version.spec.ts index d8b2f8ac427b6..3f63c86a7ad57 100644 --- a/packages/js/src/generators/release-version/release-version.spec.ts +++ b/packages/js/src/generators/release-version/release-version.spec.ts @@ -1,4 +1,4 @@ -let originalExit = process.exit; +const originalExit = process.exit; let stubProcessExit = false; const processExitSpy = jest @@ -12,10 +12,10 @@ const processExitSpy = jest import { ProjectGraph, Tree, output, readJson } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import * as enquirer from 'enquirer'; import { ReleaseGroupWithName } from 'nx/src/command-line/release/config/filter-release-groups'; import { releaseVersionGenerator } from './release-version'; import { createWorkspaceWithPackageDependencies } from './test-utils/create-workspace-with-package-dependencies'; -import * as enquirer from 'enquirer'; jest.mock('enquirer'); @@ -27,7 +27,7 @@ describe('release-version', () => { let projectGraph: ProjectGraph; beforeEach(() => { - // @ts-ignore + // @ts-expect-error read-only property process.exit = processExitSpy; tree = createTreeWithEmptyWorkspace(); @@ -95,12 +95,14 @@ describe('release-version', () => { "dependentProjects": [ { "dependencyCollection": "dependencies", + "rawVersionSpec": "0.0.1", "source": "project-with-dependency-on-my-pkg", "target": "my-lib", "type": "static", }, { "dependencyCollection": "devDependencies", + "rawVersionSpec": "0.0.1", "source": "project-with-devDependency-on-my-pkg", "target": "my-lib", "type": "static", @@ -314,7 +316,7 @@ To fix this you will either need to add a package.json file at that location, or describe('independent release group', () => { describe('specifierSource: prompt', () => { it(`should appropriately prompt for each project independently and apply the version updates across all package.json files`, async () => { - // @ts-ignore + // @ts-expect-error read-only property enquirer.prompt = jest .fn() // First project will be minor @@ -920,13 +922,13 @@ To fix this you will either need to add a package.json file at that location, or specifier: 'major', currentVersionResolver: 'disk', releaseGroup: createReleaseGroup('fixed'), - versionPrefix: '$', + versionPrefix: '$' as any, }); expect(outputSpy).toHaveBeenCalledWith({ title: `Invalid value for version.generatorOptions.versionPrefix: "$" -Valid values are: "auto", "", "~", "^"`, +Valid values are: "auto", "", "~", "^", "="`, }); outputSpy.mockRestore(); diff --git a/packages/js/src/generators/release-version/release-version.ts b/packages/js/src/generators/release-version/release-version.ts index 23a17728cebb6..27132fdc66e49 100644 --- a/packages/js/src/generators/release-version/release-version.ts +++ b/packages/js/src/generators/release-version/release-version.ts @@ -10,7 +10,8 @@ import { writeJson, } from '@nx/devkit'; import * as chalk from 'chalk'; -import { exec } from 'child_process'; +import { exec } from 'node:child_process'; +import { relative } from 'node:path'; import { IMPLICIT_DEFAULT_RELEASE_GROUP } from 'nx/src/command-line/release/config/config'; import { getFirstGitCommit, @@ -27,10 +28,8 @@ import { deriveNewSemverVersion, validReleaseVersionPrefixes, } from 'nx/src/command-line/release/version'; - import { interpolate } from 'nx/src/tasks-runner/utils'; import * as ora from 'ora'; -import { relative } from 'path'; import { prerelease } from 'semver'; import { ReleaseVersionGeneratorSchema } from './schema'; import { resolveLocalPackageDependencies } from './utils/resolve-local-package-dependencies'; @@ -76,20 +75,6 @@ Valid values are: ${validReleaseVersionPrefixes const projects = options.projects; - const createResolvePackageRoot = - (customPackageRoot?: string) => - (projectNode: ProjectGraphProjectNode): string => { - // Default to the project root if no custom packageRoot - if (!customPackageRoot) { - return projectNode.data.root; - } - return interpolate(customPackageRoot, { - workspaceRoot: '', - projectRoot: projectNode.data.root, - projectName: projectNode.name, - }); - }; - const resolvePackageRoot = createResolvePackageRoot(options.packageRoot); // Resolve any custom package roots for each project upfront as they will need to be reused during dependency resolution @@ -101,20 +86,31 @@ Valid values are: ${validReleaseVersionPrefixes ); } - let currentVersion: string; - let currentVersionResolvedFromFallback: boolean = false; + let currentVersion: string | undefined = 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: { tag: string; extractedVersion: string }; + let latestMatchingGitTag: + | { tag: string; extractedVersion: string } + | null + | undefined = 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; + let specifier: string | null | undefined = options.specifier + ? options.specifier + : undefined; 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 = joinPathFragments(packageRoot, 'package.json'); const workspaceRelativePackageJsonPath = relative( workspaceRoot, @@ -268,7 +264,10 @@ To fix this you will either need to add a package.json file at that location, or ); } else { log( - `📄 Using the current version ${currentVersion} already resolved from git tag "${latestMatchingGitTag.tag}".` + // 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 + }".` ); } } @@ -301,7 +300,7 @@ To fix this you will either need to add a package.json file at that location, or ) { const specifierSource = options.specifierSource; switch (specifierSource) { - case 'conventional-commits': + 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"` @@ -347,7 +346,7 @@ To fix this you will either need to add a package.json file at that location, or // // 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 (prerelease(currentVersion)) { + if (prerelease(currentVersion ?? '')) { specifier = 'prerelease'; log( `📄 Resolved the specifier as "${specifier}" since the current version is a prerelease.` @@ -358,6 +357,7 @@ To fix this you will either need to add a package.json file at that location, or ); } 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: string): string => { @@ -413,9 +413,16 @@ To fix this you will either need to add a package.json file at that location, or return localPackageDependency.target === project.name; }); + 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, dependentProjects, + // @ts-ignore: The types will be updated in a future version of Nx newVersion: null, // will stay as null in the final result in the case that no changes are detected }; @@ -455,12 +462,17 @@ To fix this you will either need to add a package.json file at that location, or } for (const dependentProject of dependentProjects) { + const dependentPackageRoot = projectNameToPackageRootMap.get( + dependentProject.source + ); + if (!dependentPackageRoot) { + throw new Error( + `The dependent project "${dependentProject.source}" does not have a packageRoot available. Please report this issue on https://github.com/nrwl/nx` + ); + } updateJson( tree, - joinPathFragments( - projectNameToPackageRootMap.get(dependentProject.source), - 'package.json' - ), + joinPathFragments(dependentPackageRoot, 'package.json'), (json) => { // Auto (i.e.infer existing) by default let versionPrefix = options.versionPrefix ?? 'auto'; @@ -490,7 +502,7 @@ To fix this you will either need to add a package.json file at that location, or } /** - * Ensure that formatting is applied so that version bump diffs are as mimimal as possible + * Ensure that formatting is applied so that version bump diffs are as minimal as possible * within the context of the user's workspace. */ await formatFiles(tree); @@ -504,7 +516,7 @@ To fix this you will either need to add a package.json file at that location, or return updatedFiles; }, }; - } catch (e) { + } catch (e: any) { if (process.env.NX_VERBOSE_LOGGING === 'true') { output.error({ title: e.message, @@ -522,6 +534,20 @@ To fix this you will either need to add a package.json file at that location, or export default releaseVersionGenerator; +function createResolvePackageRoot(customPackageRoot?: string) { + return (projectNode: ProjectGraphProjectNode): string => { + // Default to the project root if no custom packageRoot + if (!customPackageRoot) { + return projectNode.data.root; + } + return interpolate(customPackageRoot, { + workspaceRoot: '', + projectRoot: projectNode.data.root, + projectName: projectNode.name, + }); + }; +} + const colors = [ { instance: chalk.green, spinnerColor: 'green' }, { instance: chalk.greenBright, spinnerColor: 'green' }, diff --git a/packages/js/src/generators/release-version/utils/resolve-local-package-dependencies.spec.ts b/packages/js/src/generators/release-version/utils/resolve-local-package-dependencies.spec.ts index 9fe6120a92d6e..97401e2a69fa8 100644 --- a/packages/js/src/generators/release-version/utils/resolve-local-package-dependencies.spec.ts +++ b/packages/js/src/generators/release-version/utils/resolve-local-package-dependencies.spec.ts @@ -97,18 +97,21 @@ describe('resolveLocalPackageDependencies()', () => { "projectA": [ { "dependencyCollection": "dependencies", + "rawVersionSpec": "1.0.0", "source": "projectA", "target": "projectB", "type": "static", }, { "dependencyCollection": "devDependencies", + "rawVersionSpec": "1.0.0", "source": "projectA", "target": "projectC", "type": "static", }, { "dependencyCollection": "optionalDependencies", + "rawVersionSpec": "1.0.0", "source": "projectA", "target": "projectD", "type": "static", @@ -220,24 +223,28 @@ describe('resolveLocalPackageDependencies()', () => { "projectA": [ { "dependencyCollection": "dependencies", + "rawVersionSpec": "file:../projectB", "source": "projectA", "target": "projectB", "type": "static", }, { "dependencyCollection": "devDependencies", + "rawVersionSpec": "workspace:*", "source": "projectA", "target": "projectC", "type": "static", }, { "dependencyCollection": "optionalDependencies", + "rawVersionSpec": "workspace:../projectD", "source": "projectA", "target": "projectD", "type": "static", }, { "dependencyCollection": "dependencies", + "rawVersionSpec": "link:../projectE", "source": "projectA", "target": "projectE", "type": "static", @@ -246,6 +253,7 @@ describe('resolveLocalPackageDependencies()', () => { "projectB": [ { "dependencyCollection": "dependencies", + "rawVersionSpec": "workspace:1.0.0", "source": "projectB", "target": "projectC", "type": "static", @@ -305,6 +313,7 @@ describe('resolveLocalPackageDependencies()', () => { "projectA": [ { "dependencyCollection": "dependencies", + "rawVersionSpec": "1.0.0", "source": "projectA", "target": "projectB", "type": "static", @@ -394,18 +403,21 @@ describe('resolveLocalPackageDependencies()', () => { "projectA": [ { "dependencyCollection": "dependencies", + "rawVersionSpec": "1.0.0", "source": "projectA", "target": "projectB", "type": "static", }, { "dependencyCollection": "dependencies", + "rawVersionSpec": "1.0.0", "source": "projectA", "target": "projectC", "type": "static", }, { "dependencyCollection": "dependencies", + "rawVersionSpec": "file:../../../packages/projectD", "source": "projectA", "target": "projectD", "type": "static", diff --git a/packages/js/src/generators/release-version/utils/resolve-local-package-dependencies.ts b/packages/js/src/generators/release-version/utils/resolve-local-package-dependencies.ts index a56f0259d8b45..a2a576bcc93d5 100644 --- a/packages/js/src/generators/release-version/utils/resolve-local-package-dependencies.ts +++ b/packages/js/src/generators/release-version/utils/resolve-local-package-dependencies.ts @@ -13,6 +13,12 @@ import { Package } from './package'; import { resolveVersionSpec } from './resolve-version-spec'; interface LocalPackageDependency extends ProjectGraphDependency { + /** + * The rawVersionSpec contains the value of the version spec as it was defined in the package.json + * of the dependent project. This can be useful in cases where the version spec is a range, path or + * workspace reference, and it needs to be be reverted to that original value as part of the release. + */ + rawVersionSpec: string; dependencyCollection: | 'dependencies' | 'devDependencies' @@ -104,6 +110,7 @@ export function resolveLocalPackageDependencies( { ...dep, dependencyCollection: sourceNpmDependency.collection, + rawVersionSpec: sourceNpmDependency.spec, }, ]; } diff --git a/packages/nx/src/command-line/release/utils/shared.ts b/packages/nx/src/command-line/release/utils/shared.ts index 9f152d27de161..5993ede7ea9c9 100644 --- a/packages/nx/src/command-line/release/utils/shared.ts +++ b/packages/nx/src/command-line/release/utils/shared.ts @@ -27,7 +27,11 @@ export type ReleaseVersionGeneratorResult = { export type VersionData = Record< string, { - newVersion: string; + /** + * newVersion will be null in the case that no changes are detected for the project, + * e.g. when using conventional commits + */ + newVersion: string | null; currentVersion: string; dependentProjects: any[]; // TODO: investigate generic type for this once more ecosystems are explored } diff --git a/packages/nx/src/command-line/release/version.ts b/packages/nx/src/command-line/release/version.ts index d0fcc53ce5a3a..4f70fff6d63f4 100644 --- a/packages/nx/src/command-line/release/version.ts +++ b/packages/nx/src/command-line/release/version.ts @@ -50,7 +50,7 @@ export type { VersionData, } from './utils/shared'; -export const validReleaseVersionPrefixes = ['auto', '', '~', '^']; +export const validReleaseVersionPrefixes = ['auto', '', '~', '^', '='] as const; export interface ReleaseVersionGeneratorSchema { // The projects being versioned in the current execution