diff --git a/e2e/release/src/independent-projects.test.ts b/e2e/release/src/independent-projects.test.ts index 469d10061ef02..82b0248a5cb00 100644 --- a/e2e/release/src/independent-projects.test.ts +++ b/e2e/release/src/independent-projects.test.ts @@ -75,10 +75,6 @@ describe('nx release - independent projects', () => { pkg3 = uniq('my-pkg-3'); runCLI(`generate @nx/workspace:npm-package ${pkg3}`); - updateJson(`${pkg3}/package.json`, (json) => { - json.private = true; - return json; - }); /** * Update pkg2 to depend on pkg3. @@ -205,9 +201,6 @@ describe('nx release - independent projects', () => { + "version": "999.9.9-package.3", "scripts": { - } - + - "dependencies": { - "@proj/{project-name}": "0.0.0" @@ -424,7 +417,7 @@ describe('nx release - independent projects', () => { release: { projectsRelationship: 'independent', changelog: { - projectChangelogs: {}, // enable project changelogs with default options + projectChangelogs: true, // enable project changelogs with default options workspaceChangelog: false, // disable workspace changelog }, }, @@ -746,7 +739,25 @@ describe('nx release - independent projects', () => { > nx run {project-name}:nx-release-publish - Skipped package "@proj/{project-name}" from project "{project-name}", because it has \`"private": true\` in {project-name}/package.json + + 📦 @proj/{project-name}@999.9.9-version-git-operations-test.3 + === Tarball Contents === + + XXXB CHANGELOG.md + XXB index.js + XXXB package.json + XXB project.json + === Tarball Details === + name: @proj/{project-name} + version: 999.9.9-version-git-operations-test.3 + filename: proj-{project-name}-999.9.9-version-git-operations-test.3.tgz + package size: XXXB + unpacked size: XXXB + shasum: {SHASUM} + integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + total files: 4 + + Would publish to http://localhost:4873 with tag "latest", but [dry-run] was set @@ -837,7 +848,25 @@ describe('nx release - independent projects', () => { > nx run {project-name}:nx-release-publish - Skipped package "@proj/{project-name}" from project "{project-name}", because it has \`"private": true\` in {project-name}/package.json + + 📦 @proj/{project-name}@999.9.9-version-git-operations-test.3 + === Tarball Contents === + + XXXB CHANGELOG.md + XXB index.js + XXXB package.json + XXB project.json + === Tarball Details === + name: @proj/{project-name} + version: 999.9.9-version-git-operations-test.3 + filename: proj-{project-name}-999.9.9-version-git-operations-test.3.tgz + package size: XXXB + unpacked size: XXXB + shasum: {SHASUM} + integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + total files: 4 + + Would publish to http://localhost:4873 with tag "latest", but [dry-run] was set @@ -862,7 +891,7 @@ describe('nx release - independent projects', () => { }, }, changelog: { - projectChangelogs: {}, + projectChangelogs: true, }, }, }; @@ -920,7 +949,7 @@ describe('nx release - independent projects', () => { }, }, changelog: { - projectChangelogs: {}, + projectChangelogs: true, }, }, }; diff --git a/e2e/release/src/private-js-packages.test.ts b/e2e/release/src/private-js-packages.test.ts index e855d4958842b..e205d36b341d9 100644 --- a/e2e/release/src/private-js-packages.test.ts +++ b/e2e/release/src/private-js-packages.test.ts @@ -188,7 +188,9 @@ describe('nx release - private JS packages', () => { `); - const privatePkgPublishOutput = runCLI(`release publish -p ${privatePkg}`); + const privatePkgPublishOutput = runCLI(`release publish -p ${privatePkg}`, { + silenceError: true, + }); expect(privatePkgPublishOutput).toMatchInlineSnapshot(` > NX Your filter "{private-project-name}" matched the following projects: @@ -196,20 +198,13 @@ describe('nx release - private JS packages', () => { - {private-project-name} - > NX Running target nx-release-publish for project {private-project-name}: + > NX Based on your config, the following projects were matched for publishing but do not have the "nx-release-publish" target specified: - {private-project-name} + There are a few possible reasons for this: (1) The projects may be private (2) You may not have an appropriate plugin (such as \`@nx/js\`) installed which adds the target automatically to public projects (3) You intended to configure the target manually, or exclude those projects via config in nx.json - - > nx run {private-project-name}:nx-release-publish - - Skipped package "@proj/{private-project-name}" from project "{private-project-name}", because it has \`"private": true\` in {private-project-name}/package.json - - - - > NX Successfully ran target nx-release-publish for project {private-project-name} - + Pass --verbose to see the stacktrace. `); diff --git a/packages/nx/src/command-line/release/command-object.ts b/packages/nx/src/command-line/release/command-object.ts index aceb9e964549e..1402da9fa0275 100644 --- a/packages/nx/src/command-line/release/command-object.ts +++ b/packages/nx/src/command-line/release/command-object.ts @@ -281,10 +281,12 @@ const publishCommand: CommandModule = { description: 'A one-time password for publishing to a registry that requires 2FA', }), - handler: (args) => - import('./publish').then((m) => - m.releasePublishCLIHandler(coerceParallelOption(withOverrides(args, 2))) - ), + handler: async (args) => { + const status = await ( + await import('./publish') + ).releasePublishCLIHandler(coerceParallelOption(withOverrides(args, 2))); + process.exit(status); + }, }; function coerceParallelOption(args: any) { diff --git a/packages/nx/src/command-line/release/config/config.spec.ts b/packages/nx/src/command-line/release/config/config.spec.ts index 787066573eb08..e0c2de8ae167b 100644 --- a/packages/nx/src/command-line/release/config/config.spec.ts +++ b/packages/nx/src/command-line/release/config/config.spec.ts @@ -1967,80 +1967,6 @@ describe('createNxReleaseConfig()', () => { } `); }); - - it('should return an error if any matched projects do not have the required target specified', async () => { - const res = await createNxReleaseConfig( - { - ...projectGraph, - nodes: { - ...projectGraph.nodes, - 'project-without-target': { - name: 'project-without-target', - type: 'lib', - data: { - root: 'libs/project-without-target', - targets: {}, - } as any, - }, - }, - }, - { - groups: { - 'group-1': { - projects: '*', // using string form to ensure that is supported in addition to array form - }, - }, - }, - 'nx-release-publish' - ); - expect(res).toMatchInlineSnapshot(` - { - "error": { - "code": "PROJECTS_MISSING_TARGET", - "data": { - "projects": [ - "project-without-target", - ], - "targetName": "nx-release-publish", - }, - }, - "nxReleaseConfig": null, - } - `); - - const res2 = await createNxReleaseConfig( - { - ...projectGraph, - nodes: { - ...projectGraph.nodes, - 'another-project-without-target': { - name: 'another-project-without-target', - type: 'lib', - data: { - root: 'libs/another-project-without-target', - targets: {}, - } as any, - }, - }, - }, - {}, - 'nx-release-publish' - ); - expect(res2).toMatchInlineSnapshot(` - { - "error": { - "code": "PROJECTS_MISSING_TARGET", - "data": { - "projects": [ - "another-project-without-target", - ], - "targetName": "nx-release-publish", - }, - }, - "nxReleaseConfig": null, - } - `); - }); }); describe('user config -> mixed top level and granular git', () => { @@ -2172,80 +2098,6 @@ describe('createNxReleaseConfig()', () => { `); }); - it('should return an error if any matched projects do not have the required target specified', async () => { - const res = await createNxReleaseConfig( - { - ...projectGraph, - nodes: { - ...projectGraph.nodes, - 'project-without-target': { - name: 'project-without-target', - type: 'lib', - data: { - root: 'libs/project-without-target', - targets: {}, - } as any, - }, - }, - }, - { - groups: { - 'group-1': { - projects: '*', // using string form to ensure that is supported in addition to array form - }, - }, - }, - 'nx-release-publish' - ); - expect(res).toMatchInlineSnapshot(` - { - "error": { - "code": "PROJECTS_MISSING_TARGET", - "data": { - "projects": [ - "project-without-target", - ], - "targetName": "nx-release-publish", - }, - }, - "nxReleaseConfig": null, - } - `); - - const res2 = await createNxReleaseConfig( - { - ...projectGraph, - nodes: { - ...projectGraph.nodes, - 'another-project-without-target': { - name: 'another-project-without-target', - type: 'lib', - data: { - root: 'libs/another-project-without-target', - targets: {}, - } as any, - }, - }, - }, - {}, - 'nx-release-publish' - ); - expect(res2).toMatchInlineSnapshot(` - { - "error": { - "code": "PROJECTS_MISSING_TARGET", - "data": { - "projects": [ - "another-project-without-target", - ], - "targetName": "nx-release-publish", - }, - }, - "nxReleaseConfig": null, - } - `); - }); - it("should return an error if a group's releaseTagPattern has no {version} placeholder", async () => { const res = await createNxReleaseConfig(projectGraph, { groups: { diff --git a/packages/nx/src/command-line/release/config/config.ts b/packages/nx/src/command-line/release/config/config.ts index 8836d60a9c3a7..77c18e4f00531 100644 --- a/packages/nx/src/command-line/release/config/config.ts +++ b/packages/nx/src/command-line/release/config/config.ts @@ -14,7 +14,6 @@ import { NxJsonConfiguration } from '../../../config/nx-json'; import { output, type ProjectGraph } from '../../../devkit-exports'; import { findMatchingProjects } from '../../../utils/find-matching-projects'; -import { projectHasTarget } from '../../../utils/project-graph-utils'; import { resolveNxJsonConfigErrorMessage } from '../utils/resolve-nx-json-error-message'; type DeepRequired = Required<{ @@ -73,7 +72,6 @@ export interface CreateNxReleaseConfigError { | 'RELEASE_GROUP_MATCHES_NO_PROJECTS' | 'RELEASE_GROUP_RELEASE_TAG_PATTERN_VERSION_PLACEHOLDER_MISSING_OR_EXCESSIVE' | 'PROJECT_MATCHES_MULTIPLE_GROUPS' - | 'PROJECTS_MISSING_TARGET' | 'CONVENTIONAL_COMMITS_SHORTHAND_MIXED_WITH_OVERLAPPING_GENERATOR_OPTIONS' | 'GLOBAL_GIT_CONFIG_MIXED_WITH_GRANULAR_GIT_CONFIG'; data: Record; @@ -82,9 +80,7 @@ export interface CreateNxReleaseConfigError { // Apply default configuration to any optional user configuration and handle known errors export async function createNxReleaseConfig( projectGraph: ProjectGraph, - userConfig: NxJsonConfiguration['release'] = {}, - // Optionally ensure that all configured projects have implemented a certain target - requiredTargetName?: 'nx-release-publish' + userConfig: NxJsonConfiguration['release'] = {} ): Promise<{ error: null | CreateNxReleaseConfigError; nxReleaseConfig: NxReleaseConfig | null; @@ -353,21 +349,6 @@ export async function createNxReleaseConfig( }; } - // Ensure all matching projects have the relevant target available, if applicable - if (requiredTargetName) { - const error = ensureProjectsHaveTarget( - matchingProjects, - projectGraph, - requiredTargetName - ); - if (error) { - return { - error, - nxReleaseConfig: null, - }; - } - } - // If provided, ensure release tag pattern is valid if (releaseGroup.releaseTagPattern) { const error = ensureReleaseGroupReleaseTagPatternIsValid( @@ -526,14 +507,6 @@ export async function handleNxReleaseConfigError( }); } break; - case 'PROJECTS_MISSING_TARGET': - { - output.error({ - title: `Based on your config, the following projects were matched for release but do not have a "${error.data.targetName}" target specified. Please ensure you have an appropriate plugin such as @nx/js installed, or have configured the target manually, or exclude the projects using release groups config in nx.json:`, - bodyLines: Array.from(error.data.projects).map((name) => `- ${name}`), - }); - } - break; case 'RELEASE_GROUP_RELEASE_TAG_PATTERN_VERSION_PLACEHOLDER_MISSING_OR_EXCESSIVE': { const nxJsonMessage = await resolveNxJsonConfigErrorMessage([ @@ -610,27 +583,6 @@ function ensureArray(value: string | string[]): string[] { return Array.isArray(value) ? value : [value]; } -function ensureProjectsHaveTarget( - projects: string[], - projectGraph: ProjectGraph, - requiredTargetName: string -): null | CreateNxReleaseConfigError { - const missingTargetProjects = projects.filter( - (project) => - !projectHasTarget(projectGraph.nodes[project], requiredTargetName) - ); - if (missingTargetProjects.length) { - return { - code: 'PROJECTS_MISSING_TARGET', - data: { - targetName: requiredTargetName, - projects: missingTargetProjects, - }, - }; - } - return null; -} - function isObject(value: any): value is Record { return value && typeof value === 'object' && !Array.isArray(value); } diff --git a/packages/nx/src/command-line/release/publish.ts b/packages/nx/src/command-line/release/publish.ts index a219386f5439e..bd1b7ee635ec1 100644 --- a/packages/nx/src/command-line/release/publish.ts +++ b/packages/nx/src/command-line/release/publish.ts @@ -11,6 +11,8 @@ import { readGraphFileFromGraphArg, } from '../../utils/command-line-utils'; import { logger } from '../../utils/logger'; +import { handleErrors } from '../../utils/params'; +import { projectHasTarget } from '../../utils/project-graph-utils'; import { generateGraph } from '../graph/graph'; import { PublishOptions } from './command-object'; import { @@ -20,14 +22,17 @@ import { import { filterReleaseGroups } from './config/filter-release-groups'; export const releasePublishCLIHandler = (args: PublishOptions) => - releasePublish(args); + handleErrors(args.verbose, () => releasePublish(args, true)); /** * NOTE: This function is also exported for programmatic usage and forms part of the public API * of Nx. We intentionally do not wrap the implementation with handleErrors because users need * to have control over their own error handling when using the API. */ -export async function releasePublish(args: PublishOptions): Promise { +export async function releasePublish( + args: PublishOptions, + isCLI = false +): Promise { /** * When used via the CLI, the args object will contain a __overrides_unparsed__ property that is * important for invoking the relevant executor behind the scenes. @@ -47,8 +52,7 @@ export async function releasePublish(args: PublishOptions): Promise { // Apply default configuration to any optional user configuration const { error: configError, nxReleaseConfig } = await createNxReleaseConfig( projectGraph, - nxJson.release, - 'nx-release-publish' + nxJson.release ); if (configError) { return await handleNxReleaseConfigError(configError); @@ -86,7 +90,8 @@ export async function releasePublish(args: PublishOptions): Promise { projectGraph, nxJson, Array.from(releaseGroupToFilteredProjects.get(releaseGroup)), - shouldExcludeTaskDependencies + shouldExcludeTaskDependencies, + isCLI ); } @@ -102,7 +107,8 @@ export async function releasePublish(args: PublishOptions): Promise { projectGraph, nxJson, releaseGroup.projects, - shouldExcludeTaskDependencies + shouldExcludeTaskDependencies, + isCLI ); } @@ -120,7 +126,8 @@ async function runPublishOnProjects( projectGraph: ProjectGraph, nxJson: NxJsonConfiguration, projectNames: string[], - shouldExcludeTaskDependencies: boolean + shouldExcludeTaskDependencies: boolean, + isCLI: boolean ) { const projectsToRun: ProjectGraphProjectNode[] = projectNames.map( (projectName) => projectGraph.nodes[projectName] @@ -171,30 +178,55 @@ async function runPublishOnProjects( }, projectNames ); - } else { - /** - * Run the relevant nx-release-publish executor on each of the selected projects. - */ - const status = await runCommand( - projectsToRun, - projectGraph, - { nxJson }, - { - targets, - outputStyle: 'static', - ...(args as any), - }, - overrides, - null, - {}, - { - excludeTaskDependencies: shouldExcludeTaskDependencies, - loadDotEnvFiles: true, - } - ); + } - if (status !== 0) { - process.exit(status); + ensureAllProjectsHaveTarget(projectsToRun); + /** + * Run the relevant nx-release-publish executor on each of the selected projects. + */ + const status = await runCommand( + projectsToRun, + projectGraph, + { nxJson }, + { + targets, + outputStyle: 'static', + ...(args as any), + }, + overrides, + null, + {}, + { + excludeTaskDependencies: shouldExcludeTaskDependencies, + loadDotEnvFiles: true, } + ); + + if (status !== 0) { + // In order to not add noise to the overall CLI output, do not throw an additional error + if (isCLI) { + return status; + } + // Throw an additional error for programmatic API usage + throw new Error( + 'One or more of the selected projects could not be published' + ); } } + +function ensureAllProjectsHaveTarget(projectsToRun: ProjectGraphProjectNode[]) { + const requiredTargetName = 'nx-release-publish'; + const projectsMissingTarget = projectsToRun.filter( + (project) => !projectHasTarget(project, requiredTargetName) + ); + if (projectsMissingTarget.length === 0) { + return; + } + throw new Error( + `Based on your config, the following projects were matched for publishing but do not have the "${requiredTargetName}" target specified:\n${[ + ...projectsMissingTarget.map((p) => `- ${p.name}`), + '', + 'There are a few possible reasons for this: (1) The projects may be private (2) You may not have an appropriate plugin (such as `@nx/js`) installed which adds the target automatically to public projects (3) You intended to configure the target manually, or exclude those projects via config in nx.json', + ].join('\n')}\n` + ); +} diff --git a/packages/nx/src/command-line/release/release.ts b/packages/nx/src/command-line/release/release.ts index 17822e7409d48..4ccc282e20915 100644 --- a/packages/nx/src/command-line/release/release.ts +++ b/packages/nx/src/command-line/release/release.ts @@ -54,8 +54,7 @@ export async function release( // Apply default configuration to any optional user configuration const { error: configError, nxReleaseConfig } = await createNxReleaseConfig( projectGraph, - nxJson.release, - 'nx-release-publish' + nxJson.release ); if (configError) { return await handleNxReleaseConfigError(configError); diff --git a/packages/nx/src/command-line/release/version.ts b/packages/nx/src/command-line/release/version.ts index 99626c7e464c3..2a19d7215cffd 100644 --- a/packages/nx/src/command-line/release/version.ts +++ b/packages/nx/src/command-line/release/version.ts @@ -107,8 +107,7 @@ export async function releaseVersion( // Apply default configuration to any optional user configuration const { error: configError, nxReleaseConfig } = await createNxReleaseConfig( projectGraph, - nxJson.release, - 'nx-release-publish' + nxJson.release ); if (configError) { return await handleNxReleaseConfigError(configError); @@ -575,20 +574,48 @@ function resolveGeneratorData({ configGeneratorOptions, projects, }): GeneratorData { - const { normalizedGeneratorName, schema, implementationFactory } = - getGeneratorInformation( + try { + const { normalizedGeneratorName, schema, implementationFactory } = + getGeneratorInformation( + collectionName, + generatorName, + workspaceRoot, + projects + ); + + return { collectionName, generatorName, - workspaceRoot, - projects - ); - - return { - collectionName, - generatorName, - configGeneratorOptions, - normalizedGeneratorName, - schema, - implementationFactory, - }; + configGeneratorOptions, + normalizedGeneratorName, + schema, + implementationFactory, + }; + } catch (err) { + if (err.message.startsWith('Unable to resolve')) { + // See if it is because the plugin is not installed + try { + require.resolve(collectionName); + // is installed + throw new Error( + `Unable to resolve the generator called "${generatorName}" within the "${collectionName}" package` + ); + } catch { + /** + * Special messaging for the most common case (especially as the user is unlikely to explicitly have + * the @nx/js generator config in their nx.json so we need to be clear about what the problem is) + */ + if (collectionName === '@nx/js') { + throw new Error( + 'The @nx/js plugin is required in order to version your JavaScript packages. Please install it and try again.' + ); + } + throw new Error( + `Unable to resolve the package ${collectionName} in order to load the generator called ${generatorName}. Is the package installed?` + ); + } + } + // Unexpected error, rethrow + throw err; + } } diff --git a/packages/nx/src/config/workspaces.spec.ts b/packages/nx/src/config/workspaces.spec.ts index ae401485c4e2f..81d3d1a183171 100644 --- a/packages/nx/src/config/workspaces.spec.ts +++ b/packages/nx/src/config/workspaces.spec.ts @@ -18,20 +18,6 @@ const libConfig = (root, name?: string) => ({ }, }); -const packageLibConfig = (root, name?: string) => ({ - name: name ?? toProjectName(`${root}/some-file`), - root, - sourceRoot: root, - projectType: 'library', - targets: { - 'nx-release-publish': { - dependsOn: ['^nx-release-publish'], - executor: '@nx/js:release-publish', - options: {}, - }, - }, -}); - describe('Workspaces', () => { let fs: TempFs; beforeEach(() => { diff --git a/packages/nx/src/utils/package-json.ts b/packages/nx/src/utils/package-json.ts index a3b93bf1767cf..5f1bd57f97c2f 100644 --- a/packages/nx/src/utils/package-json.ts +++ b/packages/nx/src/utils/package-json.ts @@ -134,7 +134,8 @@ export function buildTargetFromScript( }; } -export function readTargetsFromPackageJson({ scripts, nx }: PackageJson) { +export function readTargetsFromPackageJson(packageJson: PackageJson) { + const { scripts, nx } = packageJson; const res: Record = {}; Object.keys(scripts || {}).forEach((script) => { if (!nx?.includedScripts || nx?.includedScripts.includes(script)) { @@ -142,8 +143,12 @@ export function readTargetsFromPackageJson({ scripts, nx }: PackageJson) { } }); - // Add implicit nx-release-publish target for all package.json files to allow for lightweight configuration for package based repos - if (!res['nx-release-publish']) { + /** + * Add implicit nx-release-publish target for all package.json files that are + * not marked as `"private": true` to allow for lightweight configuration for + * package based repos. + */ + if (!packageJson.private && !res['nx-release-publish']) { res['nx-release-publish'] = { dependsOn: ['^nx-release-publish'], executor: '@nx/js:release-publish', diff --git a/scripts/nx-release.ts b/scripts/nx-release.ts index 7e5da8fc34193..0a963bca24032 100755 --- a/scripts/nx-release.ts +++ b/scripts/nx-release.ts @@ -120,7 +120,7 @@ const LARGE_BUFFER = 1024 * 1000000; // If publishing locally, force all projects to not be private first if (options.local) { console.log( - chalk.dim`\n Publishing locally, so setting all resolved packages to not be private` + chalk.dim`\n Publishing locally, so setting all packages with existing nx-release-publish targets to not be private. If you have created a new private package and you want it to be published, you will need to manually configure the "nx-release-publish" target using executor "@nx/js:release-publish"` ); const projectGraph = await createProjectGraphAsync(); for (const proj of Object.values(projectGraph.nodes)) {