diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json index 2c3510534de0c..b623ad03f8258 100644 --- a/docs/generated/manifests/menus.json +++ b/docs/generated/manifests/menus.json @@ -2323,6 +2323,14 @@ "isExternal": false, "children": [], "disableCollapsible": false + }, + { + "name": "Update Your Local Registry Setup to use Nx Release", + "path": "/recipes/nx-release/update-local-registry-setup", + "id": "update-local-registry-setup", + "isExternal": false, + "children": [], + "disableCollapsible": false } ], "disableCollapsible": false @@ -4147,6 +4155,14 @@ "isExternal": false, "children": [], "disableCollapsible": false + }, + { + "name": "Update Your Local Registry Setup to use Nx Release", + "path": "/recipes/nx-release/update-local-registry-setup", + "id": "update-local-registry-setup", + "isExternal": false, + "children": [], + "disableCollapsible": false } ], "disableCollapsible": false @@ -4199,6 +4215,14 @@ "children": [], "disableCollapsible": false }, + { + "name": "Update Your Local Registry Setup to use Nx Release", + "path": "/recipes/nx-release/update-local-registry-setup", + "id": "update-local-registry-setup", + "isExternal": false, + "children": [], + "disableCollapsible": false + }, { "name": "Other", "path": "/recipes/other", diff --git a/docs/generated/manifests/nx.json b/docs/generated/manifests/nx.json index ccce722b176ca..dda34002074c8 100644 --- a/docs/generated/manifests/nx.json +++ b/docs/generated/manifests/nx.json @@ -3177,6 +3177,17 @@ "isExternal": false, "path": "/recipes/nx-release/publish-rust-crates", "tags": ["nx-release"] + }, + { + "id": "update-local-registry-setup", + "name": "Update Your Local Registry Setup to use Nx Release", + "description": "", + "mediaImage": "", + "file": "shared/recipes/nx-release/update-local-registry-setup", + "itemList": [], + "isExternal": false, + "path": "/recipes/nx-release/update-local-registry-setup", + "tags": ["nx-release"] } ], "isExternal": false, @@ -5678,6 +5689,17 @@ "isExternal": false, "path": "/recipes/nx-release/publish-rust-crates", "tags": ["nx-release"] + }, + { + "id": "update-local-registry-setup", + "name": "Update Your Local Registry Setup to use Nx Release", + "description": "", + "mediaImage": "", + "file": "shared/recipes/nx-release/update-local-registry-setup", + "itemList": [], + "isExternal": false, + "path": "/recipes/nx-release/update-local-registry-setup", + "tags": ["nx-release"] } ], "isExternal": false, @@ -5750,6 +5772,17 @@ "path": "/recipes/nx-release/publish-rust-crates", "tags": ["nx-release"] }, + "/recipes/nx-release/update-local-registry-setup": { + "id": "update-local-registry-setup", + "name": "Update Your Local Registry Setup to use Nx Release", + "description": "", + "mediaImage": "", + "file": "shared/recipes/nx-release/update-local-registry-setup", + "itemList": [], + "isExternal": false, + "path": "/recipes/nx-release/update-local-registry-setup", + "tags": ["nx-release"] + }, "/recipes/other": { "id": "other", "name": "Other", diff --git a/docs/generated/manifests/tags.json b/docs/generated/manifests/tags.json index ffd6010310d3f..069375794b384 100644 --- a/docs/generated/manifests/tags.json +++ b/docs/generated/manifests/tags.json @@ -1029,6 +1029,13 @@ "id": "publish-rust-crates", "name": "Publish Rust Crates", "path": "/recipes/nx-release/publish-rust-crates" + }, + { + "description": "", + "file": "shared/recipes/nx-release/update-local-registry-setup", + "id": "update-local-registry-setup", + "name": "Update Your Local Registry Setup to use Nx Release", + "path": "/recipes/nx-release/update-local-registry-setup" } ], "database": [ diff --git a/docs/generated/packages/js/generators/library.json b/docs/generated/packages/js/generators/library.json index 68dfd176d6208..779da183bc076 100644 --- a/docs/generated/packages/js/generators/library.json +++ b/docs/generated/packages/js/generators/library.json @@ -89,7 +89,7 @@ "publishable": { "type": "boolean", "default": false, - "description": "Generate a publishable library.", + "description": "Configure the library ready for use with `nx release` (https://nx.dev/core-features/manage-releases).", "x-priority": "important" }, "importPath": { diff --git a/docs/generated/packages/js/generators/release-version.json b/docs/generated/packages/js/generators/release-version.json index 16f235a9cc234..f58617582f713 100644 --- a/docs/generated/packages/js/generators/release-version.json +++ b/docs/generated/packages/js/generators/release-version.json @@ -50,6 +50,18 @@ "type": "object", "description": "Additional metadata to pass to the current version resolver.", "default": {} + }, + "skipLockFileUpdate": { + "type": "boolean", + "description": "Whether to skip updating the lock file after updating the version." + }, + "installArgs": { + "type": "string", + "description": "Additional arguments to pass to the package manager when updating the lock file with an install command." + }, + "installIgnoreScripts": { + "type": "boolean", + "description": "Whether to ignore install lifecycle scripts when updating the lock file with an install command." } }, "required": ["projects", "projectGraph", "releaseGroup"], diff --git a/docs/map.json b/docs/map.json index d79ab93052639..be4a8da1313b4 100644 --- a/docs/map.json +++ b/docs/map.json @@ -1149,6 +1149,12 @@ "id": "publish-rust-crates", "tags": ["nx-release"], "file": "shared/recipes/nx-release/publish-rust-crates" + }, + { + "name": "Update Your Local Registry Setup to use Nx Release", + "id": "update-local-registry-setup", + "tags": ["nx-release"], + "file": "shared/recipes/nx-release/update-local-registry-setup" } ] }, diff --git a/docs/shared/recipes/nx-release/update-local-registry-setup.md b/docs/shared/recipes/nx-release/update-local-registry-setup.md new file mode 100644 index 0000000000000..338052e5cb40c --- /dev/null +++ b/docs/shared/recipes/nx-release/update-local-registry-setup.md @@ -0,0 +1,79 @@ +# Update Your Local Registry Setup to use Nx Release + +Nx will create a `tools/start-local-registry.ts` script for starting a local registry and publishing packages to it in preparation for running end to end tests. If you have an existing `tools/start-local-registry.ts` script from a previous version of Nx, you should update it to use Nx Release to publish packages to the local registry. This will ensure that newly generated libraries are published appropriately when running end to end tests. + +## The Previous Version + +The previous version of the `tools/start-local-registry.ts` script used publish targets on each project to publish the packages to the local registry. This is no longer necessary with Nx Release. You can identify the previous version by the `nx run-many` command that publishes the packages: + +```typescript +/** + * This script starts a local registry for e2e testing purposes. + * It is meant to be called in jest's globalSetup. + */ +import { startLocalRegistry } from '@nx/js/plugins/jest/local-registry'; +import { execFileSync } from 'child_process'; + +export default async () => { + // local registry target to run + const localRegistryTarget = '@demo-plugin-1800/source:local-registry'; + // storage folder for the local registry + const storage = './tmp/local-registry/storage'; + + global.stopLocalRegistry = await startLocalRegistry({ + localRegistryTarget, + storage, + verbose: false, + }); + const nx = require.resolve('nx'); + execFileSync( + nx, + ['run-many', '--targets', 'publish', '--ver', '0.0.0-e2e', '--tag', 'e2e'], + { env: process.env, stdio: 'inherit' } + ); +}; +``` + +If your script looks like this, you should update it. + +## The Updated Version + +The updated version of the `tools/start-local-registry.ts` script uses Nx Release to publish the packages to the local registry. This is done by running `releaseVersion` and `releasePublish` functions from `nx/release`. Your updated script should look like this: + +```typescript +/** + * This script starts a local registry for e2e testing purposes. + * It is meant to be called in jest's globalSetup. + */ +import { startLocalRegistry } from '@nx/js/plugins/jest/local-registry'; +import { execFileSync } from 'child_process'; +import { releasePublish, releaseVersion } from 'nx/release'; + +export default async () => { + // local registry target to run + const localRegistryTarget = '@demo-plugin-1800/source:local-registry'; + // storage folder for the local registry + const storage = './tmp/local-registry/storage'; + + global.stopLocalRegistry = await startLocalRegistry({ + localRegistryTarget, + storage, + verbose: false, + }); + + await releaseVersion({ + specifier: '0.0.0-e2e', + stageChanges: false, + gitCommit: false, + gitTag: false, + firstRelease: true, + generatorOptionsOverrides: { + skipLockFileUpdate: true, + }, + }); + await releasePublish({ + tag: 'e2e', + firstRelease: true, + }); +}; +``` diff --git a/docs/shared/reference/sitemap.md b/docs/shared/reference/sitemap.md index f384b2ed38223..9a0df7f33a624 100644 --- a/docs/shared/reference/sitemap.md +++ b/docs/shared/reference/sitemap.md @@ -183,6 +183,7 @@ - [Publish in CI/CD](/recipes/nx-release/publish-in-ci-cd) - [Automate GitHub Releases](/recipes/nx-release/automate-github-releases) - [Publish Rust Crates](/recipes/nx-release/publish-rust-crates) + - [Update Your Local Registry Setup to use Nx Release](/recipes/nx-release/update-local-registry-setup) - [Other](/recipes/other) - [Rescope Packages from @nrwl to @nx](/recipes/other/rescope) - [Showcase](/showcase) diff --git a/e2e/esbuild/src/esbuild.test.ts b/e2e/esbuild/src/esbuild.test.ts index 70989bbf85ffc..5ed6b47f3e740 100644 --- a/e2e/esbuild/src/esbuild.test.ts +++ b/e2e/esbuild/src/esbuild.test.ts @@ -10,12 +10,10 @@ import { readJson, runCLI, runCommand, - runCommandUntil, tmpProjPath, uniq, updateFile, updateJson, - waitUntil, } from '@nx/e2e/utils'; import { join } from 'path'; @@ -44,6 +42,7 @@ describe('EsBuild Plugin', () => { expect(packageJson).toEqual({ name: `@proj/${myPkg}`, version: '0.0.1', + private: true, type: 'commonjs', main: './index.cjs', dependencies: {}, diff --git a/e2e/eslint/src/linter.test.ts b/e2e/eslint/src/linter.test.ts index 9d7d9405ee752..5654460696ef9 100644 --- a/e2e/eslint/src/linter.test.ts +++ b/e2e/eslint/src/linter.test.ts @@ -497,6 +497,7 @@ describe('Linter', () => { }, "main": "./src/index.js", "name": "@proj/${mylib}", + "private": true, "type": "commonjs", "typings": "./src/index.d.ts", "version": "0.0.1", diff --git a/e2e/plugin/src/nx-plugin.test.ts b/e2e/plugin/src/nx-plugin.test.ts index 6b17ca7500487..51a258c564415 100644 --- a/e2e/plugin/src/nx-plugin.test.ts +++ b/e2e/plugin/src/nx-plugin.test.ts @@ -16,11 +16,11 @@ import { } from '@nx/e2e/utils'; import type { PackageJson } from 'nx/src/utils/package-json'; +import { join } from 'path'; import { ASYNC_GENERATOR_EXECUTOR_CONTENTS, NX_PLUGIN_V2_CONTENTS, } from './nx-plugin.fixtures'; -import { join } from 'path'; describe('Nx Plugin', () => { let workspaceName: string; @@ -50,6 +50,7 @@ describe('Nx Plugin', () => { expect(project).toMatchObject({ tags: [], }); + runCLI(`e2e ${plugin}-e2e`); }, 90000); diff --git a/e2e/release/src/pre-version-command.test.ts b/e2e/release/src/pre-version-command.test.ts new file mode 100644 index 0000000000000..cada8beda6ae8 --- /dev/null +++ b/e2e/release/src/pre-version-command.test.ts @@ -0,0 +1,117 @@ +import { + cleanupProject, + newProject, + runCLI, + uniq, + updateJson, +} from '@nx/e2e/utils'; + +expect.addSnapshotSerializer({ + serialize(str: string) { + return ( + str + // Remove all output unique to specific projects to ensure deterministic snapshots + .replaceAll(/my-pkg-\d+/g, '{project-name}') + .replaceAll( + /integrity:\s*.*/g, + 'integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' + ) + .replaceAll(/\b[0-9a-f]{40}\b/g, '{SHASUM}') + .replaceAll(/\d*B index\.js/g, 'XXB index.js') + .replaceAll(/\d*B project\.json/g, 'XXB project.json') + .replaceAll(/\d*B package\.json/g, 'XXXB package.json') + .replaceAll(/size:\s*\d*\s?B/g, 'size: XXXB') + .replaceAll(/\d*\.\d*\s?kB/g, 'XXX.XXX kb') + .replaceAll(/[a-fA-F0-9]{7}/g, '{COMMIT_SHA}') + .replaceAll(/Test @[\w\d]+/g, 'Test @{COMMIT_AUTHOR}') + // Normalize the version title date. + .replaceAll(/\(\d{4}-\d{2}-\d{2}\)/g, '(YYYY-MM-DD)') + // We trim each line to reduce the chances of snapshot flakiness + .split('\n') + .map((r) => r.trim()) + .join('\n') + ); + }, + test(val: string) { + return val != null && typeof val === 'string'; + }, +}); + +describe('nx release pre-version command', () => { + let pkg1: string; + + beforeAll(() => { + newProject({ + unsetProjectNameAndRootFormat: false, + packages: ['@nx/js'], + }); + + pkg1 = uniq('my-pkg-1'); + runCLI( + `generate @nx/js:library ${pkg1} --publishable --importPath=${pkg1}` + ); + }); + afterAll(() => cleanupProject()); + + it('should run pre-version command before versioning step', async () => { + updateJson(`nx.json`, (json) => { + delete json.release; + return json; + }); + const result1 = runCLI('release patch -d --first-release', { + silenceError: true, + }); + + // command should fail because @nx/js:library configures the packageRoot to be dist/{project-name}, which doesn't exist yet + expect(result1).toContain( + `NX The project "${pkg1}" does not have a package.json available at dist/${pkg1}/package.json.` + ); + + updateJson(`nx.json`, (json) => { + json.release = { + version: { + preVersionCommand: 'nx run-many -t build', + }, + }; + return json; + }); + + // command should succeed because the pre-version command will build the package + const result2 = runCLI('release patch -d --first-release'); + + expect(result2).toContain('NX Executing pre-version command'); + + const result3 = runCLI('release patch -d --first-release --verbose'); + + expect(result3).toContain('NX Executing pre-version command'); + expect(result3).toContain('Executing the following pre-version command:'); + expect(result3).toContain('nx run-many -t build'); + expect(result3).toContain(`NX Running target build for project ${pkg1}:`); + + updateJson(`nx.json`, (json) => { + json.release = { + version: { + preVersionCommand: 'echo "error" && exit 1', + }, + }; + return json; + }); + + // command should fail because the pre-version command will fail + const result4 = runCLI('release patch -d --first-release', { + silenceError: true, + }); + expect(result4).toContain( + 'NX The pre-version command failed. Retry with --verbose to see the full output of the pre-version command.' + ); + expect(result4).toContain('echo "error" && exit 1'); + + const result5 = runCLI('release patch -d --first-release --verbose', { + silenceError: true, + }); + expect(result5).toContain( + 'NX The pre-version command failed. See the full output above.' + ); + expect(result4).toContain('echo "error" && exit 1'); + }); +}); diff --git a/e2e/release/src/private-js-packages.test.ts b/e2e/release/src/private-js-packages.test.ts index 46f46dcf76d15..f11be2e8473b0 100644 --- a/e2e/release/src/private-js-packages.test.ts +++ b/e2e/release/src/private-js-packages.test.ts @@ -92,7 +92,12 @@ describe('nx release - private JS packages', () => { }); afterAll(() => cleanupProject()); - it('should skip private packages and log a warning', async () => { + it('should skip private packages and log a warning when private packages are explicitly configured', async () => { + updateJson('nx.json', (json) => { + json.release.projects = [publicPkg1, publicPkg2, privatePkg]; + return json; + }); + runCLI(`release version 999.9.9`); // This is the verdaccio instance that the e2e tests themselves are working from @@ -222,4 +227,92 @@ describe('nx release - private JS packages', () => { /npm ERR! code E404/ ); }, 500000); + + it('should skip private packages and not log a warning when no projects config exists', async () => { + updateJson('nx.json', (json) => { + delete json.release.projects; + return json; + }); + + runCLI(`release version 999.9.10`); + + // This is the verdaccio instance that the e2e tests themselves are working from + const e2eRegistryUrl = execSync('npm config get registry') + .toString() + .trim(); + + // Thanks to the custom serializer above, the publish output should be deterministic + const publishOutput = runCLI(`release publish`); + expect(publishOutput).toMatchInlineSnapshot(` + + NX Running target nx-release-publish for 2 projects: + + - {public-project-name} + - {public-project-name} + + + + > nx run {public-project-name}:nx-release-publish + + + 📦 @proj/{public-project-name}@999.9.10 + === Tarball Contents === + + XXB index.js + XXXB package.json + XXB project.json + === Tarball Details === + name: @proj/{public-project-name} + version: 999.9.10 + filename: proj-{public-project-name}-999.9.10.tgz + package size: XXXB + unpacked size: XXXB + shasum: {SHASUM} + integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + total files: 3 + + Published to http://localhost:4873 with tag "latest" + + > nx run {public-project-name}:nx-release-publish + + + 📦 @proj/{public-project-name}@999.9.10 + === Tarball Contents === + + XXB index.js + XXXB package.json + XXB project.json + === Tarball Details === + name: @proj/{public-project-name} + version: 999.9.10 + filename: proj-{public-project-name}-999.9.10.tgz + package size: XXXB + unpacked size: XXXB + shasum: {SHASUM} + integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + total files: 3 + + Published to http://localhost:4873 with tag "latest" + + + + NX Successfully ran target nx-release-publish for 2 projects + + + + `); + + // The two public packages should have been published + expect( + execSync(`npm view @proj/${publicPkg1} version`).toString().trim() + ).toEqual('999.9.10'); + expect( + execSync(`npm view @proj/${publicPkg2} version`).toString().trim() + ).toEqual('999.9.10'); + + // The private package should have never been published + expect(() => execSync(`npm view @proj/${privatePkg} version`)).toThrowError( + /npm ERR! code E404/ + ); + }, 500000); }); diff --git a/e2e/workspace-create/src/create-nx-plugin.test.ts b/e2e/workspace-create/src/create-nx-plugin.test.ts index d54ddbc71ff11..dc51b5fa62756 100644 --- a/e2e/workspace-create/src/create-nx-plugin.test.ts +++ b/e2e/workspace-create/src/create-nx-plugin.test.ts @@ -1,11 +1,11 @@ import { checkFilesExist, + cleanupProject, getSelectedPackageManager, packageManagerLockFile, runCLI, - uniq, runCreatePlugin, - cleanupProject, + uniq, } from '@nx/e2e/utils'; describe('create-nx-plugin', () => { diff --git a/packages/js/src/generators/library/library.spec.ts b/packages/js/src/generators/library/library.spec.ts index 51e23626e2907..41488f28d620e 100644 --- a/packages/js/src/generators/library/library.spec.ts +++ b/packages/js/src/generators/library/library.spec.ts @@ -1,5 +1,7 @@ import { + getPackageManagerCommand, getProjects, + output, readJson, readProjectConfiguration, Tree, @@ -40,6 +42,7 @@ describe('lib', () => { }); expect(readJson(tree, '/my-lib/package.json')).toEqual({ name: '@proj/my-lib', + private: true, version: '0.0.1', type: 'commonjs', scripts: { @@ -1013,7 +1016,7 @@ describe('lib', () => { }); }); - it('should generate the publish target', async () => { + it('should update the nx-release-publish target to specify dist/{projectRoot} as the package root', async () => { await libraryGenerator(tree, { ...defaultOptions, name: 'my-lib', @@ -1024,24 +1027,356 @@ describe('lib', () => { }); const config = readProjectConfiguration(tree, 'my-lib'); - expect(config.targets.publish).toEqual({ - command: - 'node tools/scripts/publish.mjs my-lib {args.ver} {args.tag}', - dependsOn: ['build'], + expect(config.targets['nx-release-publish']).toEqual({ + options: { + packageRoot: 'dist/{projectRoot}', + }, }); }); - it('should generate publish script', async () => { - await libraryGenerator(tree, { - ...defaultOptions, - name: 'my-lib', - publishable: true, - importPath: '@proj/my-lib', - bundler: 'tsc', - projectNameAndRootFormat: 'as-provided', + describe('nx release config', () => { + it('should not change preVersionCommand if it already exists', async () => { + updateJson(tree, 'nx.json', (json) => { + json.release = { + version: { + preVersionCommand: 'echo "hello world"', + }, + }; + return json; + }); + + await libraryGenerator(tree, { + ...defaultOptions, + name: 'my-lib', + publishable: true, + importPath: '@proj/my-lib', + bundler: 'tsc', + projectNameAndRootFormat: 'as-provided', + }); + + const nxJson = readJson(tree, 'nx.json'); + expect(nxJson.release).toEqual({ + version: { + preVersionCommand: 'echo "hello world"', + }, + }); + }); + + it('should not add projects if no release config exists', async () => { + updateJson(tree, 'nx.json', (json) => { + delete json.release; + return json; + }); + + await libraryGenerator(tree, { + ...defaultOptions, + name: 'my-lib', + publishable: true, + importPath: '@proj/my-lib', + bundler: 'tsc', + projectNameAndRootFormat: 'as-provided', + }); + + const nxJson = readJson(tree, 'nx.json'); + expect(nxJson.release).toEqual({ + version: { + preVersionCommand: `${ + getPackageManagerCommand().dlx + } nx run-many -t build`, + }, + }); + }); + + it("should not add projects if release config exists but doesn't specify groups or projects", async () => { + const existingReleaseConfig = { + version: { + git: {}, + }, + changelog: { + projectChangelogs: true, + }, + }; + updateJson(tree, 'nx.json', (json) => { + json.release = existingReleaseConfig; + return json; + }); + + await libraryGenerator(tree, { + ...defaultOptions, + name: 'my-lib', + publishable: true, + importPath: '@proj/my-lib', + bundler: 'tsc', + projectNameAndRootFormat: 'as-provided', + }); + + const nxJson = readJson(tree, 'nx.json'); + expect(nxJson.release).toEqual({ + ...existingReleaseConfig, + version: { + ...existingReleaseConfig.version, + preVersionCommand: `${ + getPackageManagerCommand().dlx + } nx run-many -t build`, + }, + }); + }); + + it('should not change projects if it already exists as a string and matches the new project', async () => { + updateJson(tree, 'nx.json', (json) => { + json.release = { + projects: '*', + }; + return json; + }); + + await libraryGenerator(tree, { + ...defaultOptions, + name: 'my-lib', + publishable: true, + importPath: '@proj/my-lib', + bundler: 'tsc', + projectNameAndRootFormat: 'as-provided', + }); + + const nxJson = readJson(tree, 'nx.json'); + expect(nxJson.release).toEqual({ + projects: '*', + version: { + preVersionCommand: `${ + getPackageManagerCommand().dlx + } nx run-many -t build`, + }, + }); + }); + + it('should not change projects if it already exists as an array and matches the new project by name', async () => { + updateJson(tree, 'nx.json', (json) => { + json.release = { + projects: ['something-else', 'my-lib'], + }; + return json; + }); + + await libraryGenerator(tree, { + ...defaultOptions, + name: 'my-lib', + publishable: true, + importPath: '@proj/my-lib', + bundler: 'tsc', + projectNameAndRootFormat: 'as-provided', + }); + + const nxJson = readJson(tree, 'nx.json'); + expect(nxJson.release).toEqual({ + projects: ['something-else', 'my-lib'], + version: { + preVersionCommand: `${ + getPackageManagerCommand().dlx + } nx run-many -t build`, + }, + }); }); - expect(tree.exists('tools/scripts/publish.mjs')).toBeTruthy(); + it('should not change projects if it already exists and matches the new project by tag', async () => { + updateJson(tree, 'nx.json', (json) => { + json.release = { + projects: ['tag:one'], + }; + return json; + }); + + await libraryGenerator(tree, { + ...defaultOptions, + name: 'my-lib', + publishable: true, + importPath: '@proj/my-lib', + bundler: 'tsc', + projectNameAndRootFormat: 'as-provided', + tags: 'one,two', + }); + + const nxJson = readJson(tree, 'nx.json'); + expect(nxJson.release).toEqual({ + projects: ['tag:one'], + version: { + preVersionCommand: `${ + getPackageManagerCommand().dlx + } nx run-many -t build`, + }, + }); + }); + + it('should not change projects if it already exists and matches the new project by root directory', async () => { + updateJson(tree, 'nx.json', (json) => { + json.release = { + projects: ['packages/*'], + }; + return json; + }); + + await libraryGenerator(tree, { + ...defaultOptions, + name: 'my-lib', + publishable: true, + importPath: '@proj/my-lib', + bundler: 'tsc', + projectNameAndRootFormat: 'as-provided', + directory: 'packages/my-lib', + }); + + const nxJson = readJson(tree, 'nx.json'); + expect(nxJson.release).toEqual({ + projects: ['packages/*'], + version: { + preVersionCommand: `${ + getPackageManagerCommand().dlx + } nx run-many -t build`, + }, + }); + }); + + it("should append project to projects if projects exists as an array, but doesn't already match the new project", async () => { + updateJson(tree, 'nx.json', (json) => { + json.release = { + projects: ['something-else'], + }; + return json; + }); + + await libraryGenerator(tree, { + ...defaultOptions, + name: 'my-lib', + publishable: true, + importPath: '@proj/my-lib', + bundler: 'tsc', + projectNameAndRootFormat: 'as-provided', + }); + + const nxJson = readJson(tree, 'nx.json'); + expect(nxJson.release).toEqual({ + projects: ['something-else', 'my-lib'], + version: { + preVersionCommand: `${ + getPackageManagerCommand().dlx + } nx run-many -t build`, + }, + }); + }); + + it("should convert projects to an array and append the new project to it if projects exists as a string, but doesn't already match the new project", async () => { + updateJson(tree, 'nx.json', (json) => { + json.release = { + projects: 'packages', + }; + return json; + }); + + await libraryGenerator(tree, { + ...defaultOptions, + name: 'my-lib', + publishable: true, + importPath: '@proj/my-lib', + bundler: 'tsc', + projectNameAndRootFormat: 'as-provided', + }); + + const nxJson = readJson(tree, 'nx.json'); + expect(nxJson.release).toEqual({ + projects: ['packages', 'my-lib'], + version: { + preVersionCommand: `${ + getPackageManagerCommand().dlx + } nx run-many -t build`, + }, + }); + }); + + it('should not change projects if it already exists as groups config and matches the new project', async () => { + const existingReleaseConfig = { + groups: { + group1: { + projects: ['something-else'], + }, + group2: { + projects: ['my-lib'], + }, + }, + }; + updateJson(tree, 'nx.json', (json) => { + json.release = existingReleaseConfig; + return json; + }); + + await libraryGenerator(tree, { + ...defaultOptions, + name: 'my-lib', + publishable: true, + importPath: '@proj/my-lib', + bundler: 'tsc', + projectNameAndRootFormat: 'as-provided', + }); + + const nxJson = readJson(tree, 'nx.json'); + expect(nxJson.release).toEqual({ + groups: existingReleaseConfig.groups, + version: { + preVersionCommand: `${ + getPackageManagerCommand().dlx + } nx run-many -t build`, + }, + }); + }); + + it("should warn the user if their defined groups don't match the new project", async () => { + const outputSpy = jest + .spyOn(output, 'warn') + .mockImplementationOnce(() => { + return undefined as never; + }); + + const existingReleaseConfig = { + groups: { + group1: { + projects: ['something-else'], + }, + group2: { + projects: ['other-thing'], + }, + }, + }; + updateJson(tree, 'nx.json', (json) => { + json.release = existingReleaseConfig; + return json; + }); + + await libraryGenerator(tree, { + ...defaultOptions, + name: 'my-lib', + publishable: true, + importPath: '@proj/my-lib', + bundler: 'tsc', + projectNameAndRootFormat: 'as-provided', + }); + + const nxJson = readJson(tree, 'nx.json'); + expect(nxJson.release).toEqual({ + groups: existingReleaseConfig.groups, + version: { + preVersionCommand: `${ + getPackageManagerCommand().dlx + } nx run-many -t build`, + }, + }); + expect(outputSpy).toHaveBeenCalledWith({ + title: `Could not find a release group that includes my-lib`, + bodyLines: [ + `Ensure that my-lib is included in a release group's "projects" list in nx.json so it can be published with "nx release"`, + ], + }); + + outputSpy.mockRestore(); + }); }); }); diff --git a/packages/js/src/generators/library/library.ts b/packages/js/src/generators/library/library.ts index a8066f739d767..48d1311087fde 100644 --- a/packages/js/src/generators/library/library.ts +++ b/packages/js/src/generators/library/library.ts @@ -5,10 +5,13 @@ import { formatFiles, generateFiles, GeneratorCallback, + getPackageManagerCommand, joinPathFragments, names, offsetFromRoot, + output, ProjectConfiguration, + ProjectGraphProjectNode, readNxJson, readProjectConfiguration, runTasksInSerial, @@ -22,15 +25,19 @@ import { type ProjectNameAndRootOptions, } from '@nx/devkit/src/generators/project-name-and-root-utils'; -import { - addTsConfigPath, - getRelativePathToRootTsConfig, -} from '../../utils/typescript/ts-config'; +import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; +import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; +import { findMatchingProjects } from 'nx/src/utils/find-matching-projects'; +import { type PackageJson } from 'nx/src/utils/package-json'; import { join } from 'path'; -import { addMinimalPublishScript } from '../../utils/minimal-publish-script'; import { Bundler, LibraryGeneratorSchema } from '../../utils/schema'; import { addSwcConfig } from '../../utils/swc/add-swc-config'; import { addSwcDependencies } from '../../utils/swc/add-swc-dependencies'; +import { tsConfigBaseOptions } from '../../utils/typescript/create-ts-config'; +import { + addTsConfigPath, + getRelativePathToRootTsConfig, +} from '../../utils/typescript/ts-config'; import { esbuildVersion, nxVersion, @@ -39,11 +46,9 @@ import { typesNodeVersion, } from '../../utils/versions'; import jsInitGenerator from '../init/init'; -import { type PackageJson } from 'nx/src/utils/package-json'; import setupVerdaccio from '../setup-verdaccio/generator'; -import { tsConfigBaseOptions } from '../../utils/typescript/create-ts-config'; -import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; -import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults'; + +const defaultOutputDirectory = 'dist'; export async function libraryGenerator( tree: Tree, @@ -74,7 +79,7 @@ export async function libraryGeneratorInternal( createFiles(tree, options); - addProject(tree, options); + await addProject(tree, options); if (!options.skipPackageJson) { tasks.push(addProjectDependencies(tree, options)); @@ -166,6 +171,12 @@ export async function libraryGeneratorInternal( await formatFiles(tree); } + if (options.publishable) { + tasks.push(() => { + logNxReleaseDocsInfo(); + }); + } + tasks.push(() => { logShowProjectCommand(options.name); }); @@ -182,7 +193,7 @@ export interface NormalizedSchema extends LibraryGeneratorSchema { importPath?: string; } -function addProject(tree: Tree, options: NormalizedSchema) { +async function addProject(tree: Tree, options: NormalizedSchema) { const projectConfiguration: ProjectConfiguration = { root: options.projectRoot, sourceRoot: joinPathFragments(options.projectRoot, 'src'), @@ -240,12 +251,27 @@ function addProject(tree: Tree, options: NormalizedSchema) { } if (options.publishable) { - const publishScriptPath = addMinimalPublishScript(tree); + const packageRoot = join(defaultOutputDirectory, '{projectRoot}'); - projectConfiguration.targets.publish = { - command: `node ${publishScriptPath} ${options.name} {args.ver} {args.tag}`, - dependsOn: ['build'], + projectConfiguration.targets ??= {}; + projectConfiguration.targets['nx-release-publish'] = { + options: { + packageRoot, + }, }; + + projectConfiguration.release = { + version: { + generatorOptions: { + packageRoot, + // using git tags to determine the current version is required here because + // the version in the package root is overridden with every build + currentVersionResolver: 'git-tag', + }, + }, + }; + + await addProjectToNxReleaseConfig(tree, options, projectConfiguration); } } @@ -498,6 +524,9 @@ function createFiles(tree: Tree, options: NormalizedSchema) { if (json.private && (options.publishable || options.rootProject)) { delete json.private; } + if (!options.publishable && !options.rootProject) { + json.private = true; + } return { ...json, dependencies: { @@ -508,12 +537,16 @@ function createFiles(tree: Tree, options: NormalizedSchema) { }; }); } else { - writeJson(tree, packageJsonPath, { + const packageJson: PackageJson = { name: options.importPath, version: '0.0.1', dependencies: determineDependencies(options), ...determineEntryFields(options), - }); + }; + if (!options.publishable && !options.rootProject) { + packageJson.private = true; + } + writeJson(tree, packageJsonPath, packageJson); } if (options.config === 'npm-scripts') { @@ -633,7 +666,7 @@ async function normalizeOptions( } } - // This is to preserve old behaviour, buildable: false + // This is to preserve old behavior, buildable: false if (options.publishable === false && options.buildable === false) { options.bundler = 'none'; } @@ -761,7 +794,7 @@ function getBuildExecutor(bundler: Bundler) { } function getOutputPath(options: NormalizedSchema) { - const parts = ['dist']; + const parts = [defaultOutputDirectory]; if (options.projectRoot === '.') { parts.push(options.name); } else { @@ -866,4 +899,118 @@ function determineEntryFields( } } +function projectsConfigMatchesProject( + projectsConfig: string | string[] | undefined, + project: ProjectGraphProjectNode +): boolean { + if (!projectsConfig) { + return false; + } + + if (typeof projectsConfig === 'string') { + projectsConfig = [projectsConfig]; + } + + const graph: Record = { + [project.name]: project, + }; + + const matchingProjects = findMatchingProjects(projectsConfig, graph); + + return matchingProjects.includes(project.name); +} + +async function addProjectToNxReleaseConfig( + tree: Tree, + options: NormalizedSchema, + projectConfiguration: ProjectConfiguration +) { + const nxJson = readNxJson(tree); + + const addPreVersionCommand = () => { + const pmc = getPackageManagerCommand(); + + nxJson.release = { + ...nxJson.release, + version: { + preVersionCommand: `${pmc.dlx} nx run-many -t build`, + ...nxJson.release?.version, + }, + }; + }; + + if (!nxJson.release || (!nxJson.release.projects && !nxJson.release.groups)) { + // skip adding any projects configuration since the new project should be + // automatically included by nx release's default project detection logic + addPreVersionCommand(); + writeJson(tree, 'nx.json', nxJson); + return; + } + + const project: ProjectGraphProjectNode = { + name: options.name, + type: 'lib' as const, + data: { + root: projectConfiguration.root, + tags: projectConfiguration.tags, + }, + }; + + if (projectsConfigMatchesProject(nxJson.release.projects, project)) { + output.log({ + title: `Project already included in existing release configuration`, + }); + addPreVersionCommand(); + writeJson(tree, 'nx.json', nxJson); + return; + } + + if (Array.isArray(nxJson.release.projects)) { + nxJson.release.projects.push(options.name); + addPreVersionCommand(); + writeJson(tree, 'nx.json', nxJson); + output.log({ + title: `Added project to existing release configuration`, + }); + } + + if (nxJson.release.groups) { + const allGroups = Object.entries(nxJson.release.groups); + + for (const [name, group] of allGroups) { + if (projectsConfigMatchesProject(group.projects, project)) { + addPreVersionCommand(); + writeJson(tree, 'nx.json', nxJson); + return `Project already included in existing release configuration for group ${name}`; + } + } + + output.warn({ + title: `Could not find a release group that includes ${options.name}`, + bodyLines: [ + `Ensure that ${options.name} is included in a release group's "projects" list in nx.json so it can be published with "nx release"`, + ], + }); + addPreVersionCommand(); + writeJson(tree, 'nx.json', nxJson); + return; + } + + if (typeof nxJson.release.projects === 'string') { + nxJson.release.projects = [nxJson.release.projects, options.name]; + addPreVersionCommand(); + writeJson(tree, 'nx.json', nxJson); + output.log({ + title: `Added project to existing release configuration`, + }); + return; + } +} + +function logNxReleaseDocsInfo() { + output.log({ + title: `📦 To learn how to publish this library, see https://nx.dev/core-features/manage-releases.`, + }); +} + export default libraryGenerator; diff --git a/packages/js/src/generators/library/schema.json b/packages/js/src/generators/library/schema.json index f6cd27ec2e801..bc0122457709d 100644 --- a/packages/js/src/generators/library/schema.json +++ b/packages/js/src/generators/library/schema.json @@ -89,7 +89,7 @@ "publishable": { "type": "boolean", "default": false, - "description": "Generate a publishable library.", + "description": "Configure the library ready for use with `nx release` (https://nx.dev/core-features/manage-releases).", "x-priority": "important" }, "importPath": { diff --git a/packages/js/src/generators/release-version/schema.json b/packages/js/src/generators/release-version/schema.json index aab95e9e3d036..c63faae9bd3f6 100644 --- a/packages/js/src/generators/release-version/schema.json +++ b/packages/js/src/generators/release-version/schema.json @@ -49,6 +49,18 @@ "type": "object", "description": "Additional metadata to pass to the current version resolver.", "default": {} + }, + "skipLockFileUpdate": { + "type": "boolean", + "description": "Whether to skip updating the lock file after updating the version." + }, + "installArgs": { + "type": "string", + "description": "Additional arguments to pass to the package manager when updating the lock file with an install command." + }, + "installIgnoreScripts": { + "type": "boolean", + "description": "Whether to ignore install lifecycle scripts when updating the lock file with an install command." } }, "required": ["projects", "projectGraph", "releaseGroup"] diff --git a/packages/js/src/utils/add-local-registry-scripts.ts b/packages/js/src/utils/add-local-registry-scripts.ts index 947e11caadb52..e85ca2d937f83 100644 --- a/packages/js/src/utils/add-local-registry-scripts.ts +++ b/packages/js/src/utils/add-local-registry-scripts.ts @@ -1,4 +1,4 @@ -import { ProjectConfiguration, readJson, type Tree } from '@nx/devkit'; +import { output, ProjectConfiguration, readJson, type Tree } from '@nx/devkit'; const startLocalRegistryScript = (localRegistryTarget: string) => ` /** @@ -7,6 +7,7 @@ const startLocalRegistryScript = (localRegistryTarget: string) => ` */ import { startLocalRegistry } from '@nx/js/plugins/jest/local-registry'; import { execFileSync } from 'child_process'; +import { releasePublish, releaseVersion } from 'nx/release'; export default async () => { // local registry target to run @@ -19,12 +20,21 @@ export default async () => { storage, verbose: false, }); - const nx = require.resolve('nx'); - execFileSync( - nx, - ['run-many', '--targets', 'publish', '--ver', '0.0.0-e2e', '--tag', 'e2e'], - { env: process.env, stdio: 'inherit' } - ); + + await releaseVersion({ + specifier: '0.0.0-e2e', + stageChanges: false, + gitCommit: false, + gitTag: false, + firstRelease: true, + generatorOptionsOverrides: { + skipLockFileUpdate: true + } + }); + await releasePublish({ + tag: 'e2e', + firstRelease: true + }); }; `; @@ -49,12 +59,23 @@ export function addLocalRegistryScripts(tree: Tree) { tree, 'project.json' ); + const localRegistryTarget = `${projectConfiguration.name}:local-registry`; if (!tree.exists(startLocalRegistryPath)) { tree.write( startLocalRegistryPath, startLocalRegistryScript(localRegistryTarget) ); + } else { + const existingStartLocalRegistryScript = tree + .read(startLocalRegistryPath) + .toString(); + if (!existingStartLocalRegistryScript.includes('nx/release')) { + output.warn({ + title: + 'Your `start-local-registry.ts` script may be outdated. To ensure that newly generated packages are published appropriately when running end to end tests, update this script to use Nx Release. See https://nx.dev/recipes/nx-release/update-local-registry-setup for details.', + }); + } } if (!tree.exists(stopLocalRegistryPath)) { tree.write(stopLocalRegistryPath, stopLocalRegistryScript); diff --git a/packages/js/src/utils/minimal-publish-script.ts b/packages/js/src/utils/minimal-publish-script.ts deleted file mode 100644 index 9715810ca4110..0000000000000 --- a/packages/js/src/utils/minimal-publish-script.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { Tree } from '@nx/devkit'; - -const publishScriptContent = ` -/** - * This is a minimal script to publish your package to "npm". - * This is meant to be used as-is or customize as you see fit. - * - * This script is executed on "dist/path/to/library" as "cwd" by default. - * - * You might need to authenticate with NPM before running this script. - */ - -import { execSync } from 'child_process'; -import { readFileSync, writeFileSync } from 'fs'; - -import devkit from '@nx/devkit'; -const { readCachedProjectGraph } = devkit; - -function invariant(condition, message) { - if (!condition) { - console.error(message); - process.exit(1); - } -} - -// Executing publish script: node path/to/publish.mjs {name} --version {version} --tag {tag} -// Default "tag" to "next" so we won't publish the "latest" tag by accident. -const [, , name, version, tag = 'next'] = process.argv; - -// A simple SemVer validation to validate the version -const validVersion = /^\\d+\\.\\d+\\.\\d+(-\\w+\\.\\d+)?/; -invariant( - version && validVersion.test(version), - \`No version provided or version did not match Semantic Versioning, expected: #.#.#-tag.# or #.#.#, got \${version}.\` -); - - -const graph = readCachedProjectGraph(); -const project = graph.nodes[name]; - -invariant( - project, - \`Could not find project "\${name}" in the workspace. Is the project.json configured correctly?\` -); - -const outputPath = project.data?.targets?.build?.options?.outputPath; -invariant( - outputPath, - \`Could not find "build.options.outputPath" of project "\${name}". Is project.json configured correctly?\` -); - -process.chdir(outputPath); - -// Updating the version in "package.json" before publishing -try { - const json = JSON.parse(readFileSync(\`package.json\`).toString()); - json.version = version; - writeFileSync(\`package.json\`, JSON.stringify(json, null, 2)); -} catch (e) { - console.error(\`Error reading package.json file from library build output.\`); -} - -// Execute "npm publish" to publish -execSync(\`npm publish --access public --tag \${tag}\`); -`; - -export function addMinimalPublishScript(tree: Tree) { - const publishScriptPath = 'tools/scripts/publish.mjs'; - - if (!tree.exists(publishScriptPath)) { - tree.write(publishScriptPath, publishScriptContent); - } - - return publishScriptPath; -} diff --git a/packages/nx/schemas/nx-schema.json b/packages/nx/schemas/nx-schema.json index 26273a6f0e0c2..11b74df06c136 100644 --- a/packages/nx/schemas/nx-schema.json +++ b/packages/nx/schemas/nx-schema.json @@ -157,11 +157,16 @@ "$ref": "#/definitions/NxReleaseVersionConfiguration" }, { - "anyOf": [ + "allOf": [ { "not": { "required": ["git"] } + }, + { + "not": { + "required": ["preVersionCommand"] + } } ] } @@ -576,6 +581,10 @@ }, "git": { "$ref": "#/definitions/NxReleaseGitConfiguration" + }, + "preVersionCommand": { + "type": "string", + "description": "A command to run after validation of nx release configuration, but before versioning begins. Used for preparing build artifacts. If --dry-run is passed, the command is still executed, but with the NX_DRY_RUN environment variable set to 'true'." } } }, diff --git a/packages/nx/schemas/project-schema.json b/packages/nx/schemas/project-schema.json index ece8fde307032..969ddbf126dd2 100644 --- a/packages/nx/schemas/project-schema.json +++ b/packages/nx/schemas/project-schema.json @@ -127,6 +127,26 @@ "items": { "type": "string" } + }, + "release": { + "type": "object", + "description": "Configuration for the nx release commands.", + "properties": { + "version": { + "type": "object", + "description": "Configuration for the nx release version command.", + "properties": { + "generator": { + "type": "string", + "description": "The version generator to use. Defaults to @nx/js:release-version." + }, + "generatorOptions": { + "type": "object", + "description": "Options for the version generator." + } + } + } + } } }, "definitions": { diff --git a/packages/nx/src/command-line/release/changelog.ts b/packages/nx/src/command-line/release/changelog.ts index f6956da297265..43bcd2f6c870f 100644 --- a/packages/nx/src/command-line/release/changelog.ts +++ b/packages/nx/src/command-line/release/changelog.ts @@ -14,6 +14,7 @@ import { } from '../../config/project-graph'; import { FsTree, Tree } from '../../generators/tree'; import { registerTsProject } from '../../plugins/js/utils/register'; +import { createProjectFileMapUsingProjectGraph } from '../../project-graph/file-map-utils'; import { createProjectGraphAsync } from '../../project-graph/project-graph'; import { interpolate } from '../../tasks-runner/utils'; import { isCI } from '../../utils/is-ci'; @@ -94,6 +95,7 @@ export async function releaseChangelog( // Apply default configuration to any optional user configuration const { error: configError, nxReleaseConfig } = await createNxReleaseConfig( projectGraph, + await createProjectFileMapUsingProjectGraph(projectGraph), nxJson.release ); if (configError) { diff --git a/packages/nx/src/command-line/release/command-object.ts b/packages/nx/src/command-line/release/command-object.ts index 18c734b3a7de1..6302d7543b458 100644 --- a/packages/nx/src/command-line/release/command-object.ts +++ b/packages/nx/src/command-line/release/command-object.ts @@ -34,6 +34,7 @@ export type VersionOptions = NxReleaseArgs & specifier?: string; preid?: string; stageChanges?: boolean; + generatorOptionsOverrides?: Record; }; export type ChangelogOptions = NxReleaseArgs & 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 d2f4504efbda1..925b3dc379841 100644 --- a/packages/nx/src/command-line/release/config/config.spec.ts +++ b/packages/nx/src/command-line/release/config/config.spec.ts @@ -1,10 +1,33 @@ -import { type ProjectGraph } from '../../../devkit-exports'; +import { ProjectFileMap, ProjectGraph } from '../../../config/project-graph'; +import { TempFs } from '../../../internal-testing-utils/temp-fs'; import { createNxReleaseConfig } from './config'; describe('createNxReleaseConfig()', () => { let projectGraph: ProjectGraph; + let projectFileMap: ProjectFileMap; + let tempFs: TempFs; - beforeEach(() => { + beforeEach(async () => { + tempFs = new TempFs('nx-release-config-test'); + await tempFs.createFiles({ + 'package.json': JSON.stringify({ + name: 'root', + version: '0.0.0', + private: true, + }), + 'libs/lib-a/package.json': JSON.stringify({ + name: 'lib-a', + version: '0.0.0', + }), + 'libs/lib-b/package.json': JSON.stringify({ + name: 'lib-b', + version: '0.0.0', + }), + 'packages/nx/package.json': JSON.stringify({ + name: 'nx', + version: '0.0.0', + }), + }); projectGraph = { nodes: { 'lib-a': { @@ -37,16 +60,54 @@ describe('createNxReleaseConfig()', () => { }, } as any, }, + root: { + name: 'root', + type: 'lib', + data: { + root: '.', + targets: { + 'nx-release-publish': {}, + }, + } as any, + }, }, dependencies: {}, }; + + projectFileMap = { + 'lib-a': [ + { + file: 'libs/lib-a/package.json', + hash: 'abc', + }, + ], + 'lib-b': [ + { + file: 'libs/lib-b/package.json', + hash: 'abc', + }, + ], + nx: [ + { + file: 'packages/nx/package.json', + hash: 'abc', + }, + ], + root: [ + { + file: 'package.json', + hash: 'abc', + }, + ], + }; }); describe('zero/empty user config', () => { it('should create appropriate default NxReleaseConfig data from zero/empty user config', async () => { // zero user config - expect(await createNxReleaseConfig(projectGraph, undefined)) - .toMatchInlineSnapshot(` + expect( + await createNxReleaseConfig(projectGraph, projectFileMap, undefined) + ).toMatchInlineSnapshot(` { "error": null, "nxReleaseConfig": { @@ -115,13 +176,14 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } `); // empty user config - expect(await createNxReleaseConfig(projectGraph, {})) + expect(await createNxReleaseConfig(projectGraph, projectFileMap, {})) .toMatchInlineSnapshot(` { "error": null, @@ -191,6 +253,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -198,7 +261,7 @@ describe('createNxReleaseConfig()', () => { // empty groups expect( - await createNxReleaseConfig(projectGraph, { + await createNxReleaseConfig(projectGraph, projectFileMap, { groups: {}, }) ).toMatchInlineSnapshot(` @@ -270,6 +333,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -295,8 +359,324 @@ describe('createNxReleaseConfig()', () => { } as any, }; - expect(await createNxReleaseConfig(projectGraph, undefined)) - .toMatchInlineSnapshot(` + projectFileMap['app-1'] = [ + { + file: 'apps/app-1/package.json', + hash: 'abc', + }, + ]; + + projectFileMap['e2e-1'] = [ + { + file: 'apps/e2e-1/package.json', + hash: 'abc', + }, + ]; + + expect( + await createNxReleaseConfig(projectGraph, projectFileMap, undefined) + ).toMatchInlineSnapshot(` + { + "error": null, + "nxReleaseConfig": { + "changelog": { + "automaticFromRef": false, + "git": { + "commit": true, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": false, + "tag": true, + "tagArgs": "", + "tagMessage": "", + }, + "projectChangelogs": false, + "workspaceChangelog": { + "createRelease": false, + "entryWhenNoChanges": "This was a version bump only, there were no code changes.", + "file": "{workspaceRoot}/CHANGELOG.md", + "renderOptions": { + "authors": true, + "commitReferences": true, + "versionTitleDate": true, + }, + "renderer": "nx/release/changelog-renderer", + }, + }, + "git": { + "commit": false, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": false, + "tag": false, + "tagArgs": "", + "tagMessage": "", + }, + "groups": { + "__default__": { + "changelog": false, + "projects": [ + "lib-a", + "lib-b", + "nx", + ], + "projectsRelationship": "fixed", + "releaseTagPattern": "v{version}", + "version": { + "conventionalCommits": false, + "generator": "@nx/js:release-version", + "generatorOptions": {}, + }, + }, + }, + "projectsRelationship": "fixed", + "releaseTagPattern": "v{version}", + "version": { + "conventionalCommits": false, + "generator": "@nx/js:release-version", + "generatorOptions": {}, + "git": { + "commit": false, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": true, + "tag": false, + "tagArgs": "", + "tagMessage": "", + }, + "preVersionCommand": "", + }, + }, + } + `); + }); + + it('should filter out projects without package.json', async () => { + projectGraph.nodes['lib-c'] = { + name: 'lib-c', + type: 'lib', + data: { + root: 'libs/lib-c', + targets: {}, + } as any, + }; + + projectFileMap['lib-c'] = [ + { + file: 'libs/lib-c/cargo.toml', + hash: 'abc', + }, + ]; + + expect( + await createNxReleaseConfig(projectGraph, projectFileMap, undefined) + ).toMatchInlineSnapshot(` + { + "error": null, + "nxReleaseConfig": { + "changelog": { + "automaticFromRef": false, + "git": { + "commit": true, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": false, + "tag": true, + "tagArgs": "", + "tagMessage": "", + }, + "projectChangelogs": false, + "workspaceChangelog": { + "createRelease": false, + "entryWhenNoChanges": "This was a version bump only, there were no code changes.", + "file": "{workspaceRoot}/CHANGELOG.md", + "renderOptions": { + "authors": true, + "commitReferences": true, + "versionTitleDate": true, + }, + "renderer": "nx/release/changelog-renderer", + }, + }, + "git": { + "commit": false, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": false, + "tag": false, + "tagArgs": "", + "tagMessage": "", + }, + "groups": { + "__default__": { + "changelog": false, + "projects": [ + "lib-a", + "lib-b", + "nx", + ], + "projectsRelationship": "fixed", + "releaseTagPattern": "v{version}", + "version": { + "conventionalCommits": false, + "generator": "@nx/js:release-version", + "generatorOptions": {}, + }, + }, + }, + "projectsRelationship": "fixed", + "releaseTagPattern": "v{version}", + "version": { + "conventionalCommits": false, + "generator": "@nx/js:release-version", + "generatorOptions": {}, + "git": { + "commit": false, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": true, + "tag": false, + "tagArgs": "", + "tagMessage": "", + }, + "preVersionCommand": "", + }, + }, + } + `); + }); + + it('should filter out projects that are private', async () => { + projectGraph.nodes['root'] = { + name: 'root', + type: 'lib', + data: { + root: '.', + targets: {}, + } as any, + }; + + projectFileMap['root'] = [ + { + file: 'package.json', + hash: 'abc', + }, + ]; + + tempFs.writeFile( + 'package.json', + JSON.stringify({ name: 'root', version: '0.0.0', private: true }) + ); + tempFs.writeFile( + 'libs/lib-a/package.json', + JSON.stringify({ name: 'lib-a', version: '0.0.0', private: true }) + ); + + expect( + await createNxReleaseConfig(projectGraph, projectFileMap, undefined) + ).toMatchInlineSnapshot(` + { + "error": null, + "nxReleaseConfig": { + "changelog": { + "automaticFromRef": false, + "git": { + "commit": true, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": false, + "tag": true, + "tagArgs": "", + "tagMessage": "", + }, + "projectChangelogs": false, + "workspaceChangelog": { + "createRelease": false, + "entryWhenNoChanges": "This was a version bump only, there were no code changes.", + "file": "{workspaceRoot}/CHANGELOG.md", + "renderOptions": { + "authors": true, + "commitReferences": true, + "versionTitleDate": true, + }, + "renderer": "nx/release/changelog-renderer", + }, + }, + "git": { + "commit": false, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": false, + "tag": false, + "tagArgs": "", + "tagMessage": "", + }, + "groups": { + "__default__": { + "changelog": false, + "projects": [ + "lib-b", + "nx", + ], + "projectsRelationship": "fixed", + "releaseTagPattern": "v{version}", + "version": { + "conventionalCommits": false, + "generator": "@nx/js:release-version", + "generatorOptions": {}, + }, + }, + }, + "projectsRelationship": "fixed", + "releaseTagPattern": "v{version}", + "version": { + "conventionalCommits": false, + "generator": "@nx/js:release-version", + "generatorOptions": {}, + "git": { + "commit": false, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": true, + "tag": false, + "tagArgs": "", + "tagMessage": "", + }, + "preVersionCommand": "", + }, + }, + } + `); + }); + + it('should not filter out the root project if it is not private', async () => { + projectGraph.nodes['root'] = { + name: 'root', + type: 'lib', + data: { + root: '.', + targets: {}, + } as any, + }; + + projectFileMap['root'] = [ + { + file: 'package.json', + hash: 'abc', + }, + ]; + + tempFs.writeFile( + 'package.json', + JSON.stringify({ + name: 'root', + version: '0.0.0', + }) + ); + + expect( + await createNxReleaseConfig(projectGraph, projectFileMap, undefined) + ).toMatchInlineSnapshot(` { "error": null, "nxReleaseConfig": { @@ -340,6 +720,7 @@ describe('createNxReleaseConfig()', () => { "lib-a", "lib-b", "nx", + "root", ], "projectsRelationship": "fixed", "releaseTagPattern": "v{version}", @@ -365,6 +746,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -374,7 +756,7 @@ describe('createNxReleaseConfig()', () => { describe('user specified groups', () => { it('should ignore any projects not matched to user specified groups', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { groups: { 'group-1': { projects: ['lib-a'], // intentionally no lib-b, so it should be ignored @@ -448,6 +830,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -455,7 +838,7 @@ describe('createNxReleaseConfig()', () => { }); it('should convert any projects patterns into actual project names in the final config', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { groups: { 'group-1': { projects: ['lib-*'], // should match both lib-a and lib-b @@ -530,6 +913,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -537,7 +921,7 @@ describe('createNxReleaseConfig()', () => { }); it('should respect user overrides for "version" config at the group level', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { groups: { 'group-1': { projects: ['lib-a'], @@ -628,6 +1012,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -635,7 +1020,7 @@ describe('createNxReleaseConfig()', () => { }); it('should allow using true for group level changelog as an equivalent of an empty object (i.e. use the defaults)', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { groups: { 'group-1': { projects: ['lib-a'], @@ -720,6 +1105,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -727,7 +1113,7 @@ describe('createNxReleaseConfig()', () => { }); it('should disable workspaceChangelog if there are multiple groups', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { groups: { 'group-1': { projects: ['lib-a'], @@ -830,6 +1216,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -837,7 +1224,7 @@ describe('createNxReleaseConfig()', () => { }); it('should disable workspaceChangelog if the single group has an independent projects relationship', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { groups: { 'group-1': { projects: ['lib-a', 'lib-b'], @@ -915,6 +1302,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -924,7 +1312,7 @@ describe('createNxReleaseConfig()', () => { describe('user config -> top level version', () => { it('should respect modifying version at the top level and it should be inherited by the implicit default group', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { version: { // only modifying options, use default generator generatorOptions: { @@ -1005,6 +1393,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -1012,7 +1401,7 @@ describe('createNxReleaseConfig()', () => { }); it('should respect enabling git operations on the version command via the top level', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { git: { commit: true, commitArgs: '--no-verify', @@ -1087,6 +1476,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -1094,7 +1484,7 @@ describe('createNxReleaseConfig()', () => { }); it('should respect enabling git operations for the version command directly', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { version: { git: { commit: true, @@ -1172,6 +1562,89 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", + }, + }, + } + `); + }); + + it('should allow configuration of preVersionCommand', async () => { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { + version: { + preVersionCommand: 'nx run-many -t build', + }, + }); + expect(res).toMatchInlineSnapshot(` + { + "error": null, + "nxReleaseConfig": { + "changelog": { + "automaticFromRef": false, + "git": { + "commit": true, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": false, + "tag": true, + "tagArgs": "", + "tagMessage": "", + }, + "projectChangelogs": false, + "workspaceChangelog": { + "createRelease": false, + "entryWhenNoChanges": "This was a version bump only, there were no code changes.", + "file": "{workspaceRoot}/CHANGELOG.md", + "renderOptions": { + "authors": true, + "commitReferences": true, + "versionTitleDate": true, + }, + "renderer": "nx/release/changelog-renderer", + }, + }, + "git": { + "commit": false, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": false, + "tag": false, + "tagArgs": "", + "tagMessage": "", + }, + "groups": { + "__default__": { + "changelog": false, + "projects": [ + "lib-a", + "lib-b", + "nx", + ], + "projectsRelationship": "fixed", + "releaseTagPattern": "v{version}", + "version": { + "conventionalCommits": false, + "generator": "@nx/js:release-version", + "generatorOptions": {}, + }, + }, + }, + "projectsRelationship": "fixed", + "releaseTagPattern": "v{version}", + "version": { + "conventionalCommits": false, + "generator": "@nx/js:release-version", + "generatorOptions": {}, + "git": { + "commit": false, + "commitArgs": "", + "commitMessage": "chore(release): publish {version}", + "stageChanges": true, + "tag": false, + "tagArgs": "", + "tagMessage": "", + }, + "preVersionCommand": "nx run-many -t build", }, }, } @@ -1181,7 +1654,7 @@ describe('createNxReleaseConfig()', () => { describe('user config -> top level projects', () => { it('should return an error when both "projects" and "groups" are specified', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { projects: ['lib-a'], groups: { 'group-1': { @@ -1201,7 +1674,7 @@ describe('createNxReleaseConfig()', () => { }); it('should influence the projects configured for the implicit default group', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { projects: ['lib-a'], }); expect(res).toMatchInlineSnapshot(` @@ -1271,6 +1744,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -1280,7 +1754,7 @@ describe('createNxReleaseConfig()', () => { describe('user config -> top level releaseTagPattern', () => { it('should respect modifying releaseTagPattern at the top level and it should be inherited by the implicit default group', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { releaseTagPattern: '{projectName}__{version}', }); expect(res).toMatchInlineSnapshot(` @@ -1352,6 +1826,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -1359,7 +1834,7 @@ describe('createNxReleaseConfig()', () => { }); it('should respect top level releaseTagPatterns for fixed groups without explicit settings of their own', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { releaseTagPattern: '{version}', groups: { npm: { @@ -1454,6 +1929,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -1463,7 +1939,7 @@ describe('createNxReleaseConfig()', () => { describe('user config -> top level changelog', () => { it('should respect disabling all changelogs at the top level', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { changelog: { projectChangelogs: false, workspaceChangelog: false, @@ -1528,6 +2004,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -1535,7 +2012,7 @@ describe('createNxReleaseConfig()', () => { }); it('should respect any adjustments to default changelog config at the top level and apply as defaults at the group level', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { changelog: { workspaceChangelog: { // override single field in user config @@ -1640,6 +2117,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -1647,7 +2125,7 @@ describe('createNxReleaseConfig()', () => { }); it('should allow using true for workspaceChangelog and projectChangelogs as an equivalent of an empty object (i.e. use the defaults)', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { changelog: { projectChangelogs: true, workspaceChangelog: true, @@ -1742,6 +2220,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -1749,7 +2228,7 @@ describe('createNxReleaseConfig()', () => { }); it('should respect disabling git at the top level (thus disabling the default of true for changelog', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { git: { commit: false, tag: false, @@ -1824,6 +2303,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -1833,7 +2313,7 @@ describe('createNxReleaseConfig()', () => { describe('user config -> top level and group level changelog combined', () => { it('should respect any adjustments to default changelog config at the top level and group level in the final config, CASE 1', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { changelog: { projectChangelogs: { // overriding field at the root should be inherited by all groups that do not set their own override @@ -1978,6 +2458,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -1985,7 +2466,7 @@ describe('createNxReleaseConfig()', () => { }); it('should respect any adjustments to default changelog config at the top level and group level in the final config, CASE 2', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { groups: { foo: { projects: 'lib-a', @@ -2090,6 +2571,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -2097,7 +2579,7 @@ describe('createNxReleaseConfig()', () => { }); it('should return an error if no projects can be resolved for a group', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { groups: { 'group-1': { projects: ['lib-does-not-exist'], @@ -2120,7 +2602,7 @@ describe('createNxReleaseConfig()', () => { describe('user config -> mixed top level and granular git', () => { it('should return an error with version config and top level config', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { git: { commit: true, tag: false, @@ -2145,7 +2627,7 @@ describe('createNxReleaseConfig()', () => { }); it('should return an error with changelog config and top level config', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { git: { commit: true, tag: false, @@ -2170,7 +2652,7 @@ describe('createNxReleaseConfig()', () => { }); it('should return an error with version and changelog config and top level config', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { git: { commit: true, tag: false, @@ -2203,7 +2685,7 @@ describe('createNxReleaseConfig()', () => { describe('release group config errors', () => { it('should return an error if a project matches multiple groups', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { groups: { 'group-1': { projects: ['lib-a'], @@ -2227,7 +2709,7 @@ describe('createNxReleaseConfig()', () => { }); it('should return an error if no projects can be resolved for a group', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { groups: { 'group-1': { projects: ['lib-does-not-exist'], @@ -2248,7 +2730,7 @@ describe('createNxReleaseConfig()', () => { }); it("should return an error if a group's releaseTagPattern has no {version} placeholder", async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { groups: { 'group-1': { projects: '*', @@ -2270,7 +2752,7 @@ describe('createNxReleaseConfig()', () => { }); it("should return an error if a group's releaseTagPattern has more than one {version} placeholder", async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { groups: { 'group-1': { projects: '*', @@ -2294,7 +2776,7 @@ describe('createNxReleaseConfig()', () => { describe('projectsRelationship at the root', () => { it('should respect the user specified projectsRelationship value and apply it to any groups that do not specify their own value', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { projectsRelationship: 'independent', groups: { 'group-1': { @@ -2378,6 +2860,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -2385,7 +2868,7 @@ describe('createNxReleaseConfig()', () => { }); it('should override workspaceChangelog default if projectsRelationship is independent', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { projectsRelationship: 'independent', projects: ['lib-a', 'lib-b'], }); @@ -2448,6 +2931,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -2457,7 +2941,7 @@ describe('createNxReleaseConfig()', () => { describe('version.conventionalCommits shorthand', () => { it('should be implicitly false and not interfere with its long-form equivalent generatorOptions when not explicitly set', async () => { - const res1 = await createNxReleaseConfig(projectGraph, { + const res1 = await createNxReleaseConfig(projectGraph, projectFileMap, { version: { generatorOptions: { currentVersionResolver: 'git-tag', @@ -2540,12 +3024,13 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } `); - const res2 = await createNxReleaseConfig(projectGraph, { + const res2 = await createNxReleaseConfig(projectGraph, projectFileMap, { version: { generatorOptions: { currentVersionResolver: 'registry', @@ -2628,6 +3113,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -2635,7 +3121,7 @@ describe('createNxReleaseConfig()', () => { }); it('should update appropriate default values for generatorOptions when applied at the root', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { version: { conventionalCommits: true, }, @@ -2715,6 +3201,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -2722,7 +3209,7 @@ describe('createNxReleaseConfig()', () => { }); it('should be possible to override at the group level and produce the appropriate default generatorOptions', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { version: { conventionalCommits: true, }, @@ -2805,6 +3292,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -2812,7 +3300,7 @@ describe('createNxReleaseConfig()', () => { }); it('should not error if the shorthand is combined with unrelated generatorOptions', async () => { - const res = await createNxReleaseConfig(projectGraph, { + const res = await createNxReleaseConfig(projectGraph, projectFileMap, { version: { conventionalCommits: true, generatorOptions: { @@ -2897,6 +3385,7 @@ describe('createNxReleaseConfig()', () => { "tagArgs": "", "tagMessage": "", }, + "preVersionCommand": "", }, }, } @@ -2904,7 +3393,7 @@ describe('createNxReleaseConfig()', () => { }); it('should error if the shorthand is combined with related generatorOptions', async () => { - const res1 = await createNxReleaseConfig(projectGraph, { + const res1 = await createNxReleaseConfig(projectGraph, projectFileMap, { version: { conventionalCommits: true, generatorOptions: { @@ -2922,7 +3411,7 @@ describe('createNxReleaseConfig()', () => { } `); - const res2 = await createNxReleaseConfig(projectGraph, { + const res2 = await createNxReleaseConfig(projectGraph, projectFileMap, { version: { conventionalCommits: true, generatorOptions: { diff --git a/packages/nx/src/command-line/release/config/config.ts b/packages/nx/src/command-line/release/config/config.ts index 64dd16738e24c..77a8c273ca909 100644 --- a/packages/nx/src/command-line/release/config/config.ts +++ b/packages/nx/src/command-line/release/config/config.ts @@ -11,9 +11,14 @@ * defaults and user overrides, as well as handling common errors, up front to produce a single, consistent, * and easy to consume config object for all the `nx release` command implementations. */ +import { join } from 'path'; import { NxJsonConfiguration } from '../../../config/nx-json'; -import { output, type ProjectGraph } from '../../../devkit-exports'; +import { ProjectFileMap, ProjectGraph } from '../../../config/project-graph'; +import { readJsonFile } from '../../../utils/fileutils'; import { findMatchingProjects } from '../../../utils/find-matching-projects'; +import { output } from '../../../utils/output'; +import { PackageJson } from '../../../utils/package-json'; +import { workspaceRoot } from '../../../utils/workspace-root'; import { resolveNxJsonConfigErrorMessage } from '../utils/resolve-nx-json-error-message'; type DeepRequired = Required<{ @@ -80,6 +85,7 @@ export interface CreateNxReleaseConfigError { // Apply default configuration to any optional user configuration and handle known errors export async function createNxReleaseConfig( projectGraph: ProjectGraph, + projectFileMap: ProjectFileMap, userConfig: NxJsonConfiguration['release'] = {} ): Promise<{ error: null | CreateNxReleaseConfigError; @@ -163,6 +169,7 @@ export async function createNxReleaseConfig( conventionalCommits: userConfig.version?.conventionalCommits || false, generator: '@nx/js:release-version', generatorOptions: defaultGeneratorOptions, + preVersionCommand: userConfig.version?.preVersionCommand || '', }, changelog: { git: changelogGitDefaults, @@ -279,21 +286,23 @@ export async function createNxReleaseConfig( > ); - // git configuration is not supported at the group level, only the root/command level - const rootVersionWithoutGit = { ...rootVersionConfig }; - delete rootVersionWithoutGit.git; + // these options are not supported at the group level, only the root/command level + const rootVersionWithoutGlobalOptions = { ...rootVersionConfig }; + delete rootVersionWithoutGlobalOptions.git; + delete rootVersionWithoutGlobalOptions.preVersionCommand; // Apply conventionalCommits shorthand to the final group defaults if explicitly configured in the original user config if (userConfig.version?.conventionalCommits === true) { - rootVersionWithoutGit.generatorOptions = { - ...rootVersionWithoutGit.generatorOptions, + rootVersionWithoutGlobalOptions.generatorOptions = { + ...rootVersionWithoutGlobalOptions.generatorOptions, currentVersionResolver: 'git-tag', specifierSource: 'conventional-commits', }; } if (userConfig.version?.conventionalCommits === false) { - delete rootVersionWithoutGit.generatorOptions.currentVersionResolver; - delete rootVersionWithoutGit.generatorOptions.specifierSource; + delete rootVersionWithoutGlobalOptions.generatorOptions + .currentVersionResolver; + delete rootVersionWithoutGlobalOptions.generatorOptions.specifierSource; } const groups: NxReleaseConfig['groups'] = @@ -312,10 +321,8 @@ export async function createNxReleaseConfig( ensureArray(userConfig.projects), projectGraph.nodes ) - : // default to all library projects in the workspace - findMatchingProjects(['*'], projectGraph.nodes).filter( - (project) => projectGraph.nodes[project].type === 'lib' - ), + : await getDefaultProjects(projectGraph, projectFileMap), + /** * For properties which are overriding config at the root, we use the root level config as the * default values to merge with so that the group that matches a specific project will always @@ -323,7 +330,7 @@ export async function createNxReleaseConfig( */ version: deepMergeDefaults( [GROUP_DEFAULTS.version], - rootVersionWithoutGit + rootVersionWithoutGlobalOptions ), // If the user has set something custom for releaseTagPattern at the top level, respect it for the implicit default group releaseTagPattern: @@ -409,7 +416,7 @@ export async function createNxReleaseConfig( projects: matchingProjects, version: deepMergeDefaults( // First apply any group level defaults, then apply actual root level config, then group level config - [GROUP_DEFAULTS.version, rootVersionWithoutGit], + [GROUP_DEFAULTS.version, rootVersionWithoutGlobalOptions], releaseGroup.version ), // If the user has set any changelog config at all, including at the root level, then use one set of defaults, otherwise default to false for the whole feature @@ -694,3 +701,41 @@ function hasInvalidGitConfig( !!userConfig.git && !!(userConfig.version?.git || userConfig.changelog?.git) ); } + +async function getDefaultProjects( + projectGraph: ProjectGraph, + projectFileMap: ProjectFileMap +): Promise { + // default to all library projects in the workspace with a package.json file + return findMatchingProjects(['*'], projectGraph.nodes).filter( + (project) => + projectGraph.nodes[project].type === 'lib' && + // Exclude all projects with "private": true in their package.json because this is + // a common indicator that a project is not intended for release. + // Users can override this behavior by explicitly defining the projects they want to release. + isProjectPublic(project, projectGraph, projectFileMap) + ); +} + +function isProjectPublic( + project: string, + projectGraph: ProjectGraph, + projectFileMap: ProjectFileMap +): boolean { + const projectNode = projectGraph.nodes[project]; + const packageJsonPath = join(projectNode.data.root, 'package.json'); + + if (!projectFileMap[project]?.find((f) => f.file === packageJsonPath)) { + return false; + } + + try { + const fullPackageJsonPath = join(workspaceRoot, packageJsonPath); + const packageJson = readJsonFile(fullPackageJsonPath); + return !(packageJson.private === true); + } catch (e) { + // do nothing and assume that the project is not public if there is a parsing issue + // this will result in it being excluded from the default projects list + return false; + } +} diff --git a/packages/nx/src/command-line/release/config/filter-release-groups.spec.ts b/packages/nx/src/command-line/release/config/filter-release-groups.spec.ts index cf6b842a31904..c513f09af6de6 100644 --- a/packages/nx/src/command-line/release/config/filter-release-groups.spec.ts +++ b/packages/nx/src/command-line/release/config/filter-release-groups.spec.ts @@ -37,6 +37,7 @@ describe('filterReleaseGroups()', () => { tagMessage: '', tagArgs: '', }, + preVersionCommand: '', }, releaseTagPattern: '', git: { diff --git a/packages/nx/src/command-line/release/publish.ts b/packages/nx/src/command-line/release/publish.ts index 77d174767ba35..c12e3ae9fddc5 100644 --- a/packages/nx/src/command-line/release/publish.ts +++ b/packages/nx/src/command-line/release/publish.ts @@ -3,13 +3,14 @@ import { ProjectGraph, ProjectGraphProjectNode, } from '../../config/project-graph'; -import { output } from '../../devkit-exports'; +import { createProjectFileMapUsingProjectGraph } from '../../project-graph/file-map-utils'; import { createProjectGraphAsync } from '../../project-graph/project-graph'; import { runCommand } from '../../tasks-runner/run-command'; import { createOverrides, readGraphFileFromGraphArg, } from '../../utils/command-line-utils'; +import { output } from '../../utils/output'; import { handleErrors } from '../../utils/params'; import { projectHasTarget } from '../../utils/project-graph-utils'; import { generateGraph } from '../graph/graph'; @@ -51,6 +52,7 @@ export async function releasePublish( // Apply default configuration to any optional user configuration const { error: configError, nxReleaseConfig } = await createNxReleaseConfig( projectGraph, + await createProjectFileMapUsingProjectGraph(projectGraph), nxJson.release ); if (configError) { diff --git a/packages/nx/src/command-line/release/release.ts b/packages/nx/src/command-line/release/release.ts index e7ebd75ba8904..061781b5491f8 100644 --- a/packages/nx/src/command-line/release/release.ts +++ b/packages/nx/src/command-line/release/release.ts @@ -1,7 +1,8 @@ import { prompt } from 'enquirer'; import { readNxJson } from '../../config/nx-json'; -import { output } from '../../devkit-exports'; +import { createProjectFileMapUsingProjectGraph } from '../../project-graph/file-map-utils'; import { createProjectGraphAsync } from '../../project-graph/project-graph'; +import { output } from '../../utils/output'; import { handleErrors } from '../../utils/params'; import { releaseChangelog, shouldCreateGitHubRelease } from './changelog'; import { ReleaseOptions, VersionOptions } from './command-object'; @@ -55,6 +56,7 @@ export async function release( // Apply default configuration to any optional user configuration const { error: configError, nxReleaseConfig } = await createNxReleaseConfig( projectGraph, + await createProjectFileMapUsingProjectGraph(projectGraph), nxJson.release ); if (configError) { diff --git a/packages/nx/src/command-line/release/utils/git.ts b/packages/nx/src/command-line/release/utils/git.ts index 208c3d1431bca..f49e8fb1e8735 100644 --- a/packages/nx/src/command-line/release/utils/git.ts +++ b/packages/nx/src/command-line/release/utils/git.ts @@ -151,7 +151,29 @@ export async function gitAdd({ logFn?: (...messages: string[]) => void; }): Promise { logFn = logFn || console.log; - const commandArgs = ['add', ...changedFiles]; + + let ignoredFiles: string[] = []; + let filesToAdd: string[] = []; + for (const f of changedFiles) { + const isFileIgnored = await isIgnored(f); + if (isFileIgnored) { + ignoredFiles.push(f); + } else { + filesToAdd.push(f); + } + } + + if (verbose && ignoredFiles.length) { + logFn(`Will not add the following files because they are ignored by git:`); + ignoredFiles.forEach((f) => logFn(f)); + } + + if (!filesToAdd.length) { + logFn('\nNo files to stage. Skipping git add.'); + return; + } + + const commandArgs = ['add', ...filesToAdd]; const message = dryRun ? `Would stage files in git with the following command, but --dry-run was set:` : `Staging files in git with the following command:`; @@ -165,6 +187,16 @@ export async function gitAdd({ return execCommand('git', commandArgs); } +async function isIgnored(filePath: string): Promise { + try { + // This command will error if the file is not ignored + await execCommand('git', ['check-ignore', filePath]); + return true; + } catch { + return false; + } +} + export async function gitCommit({ messages, additionalArgs, diff --git a/packages/nx/src/command-line/release/version.ts b/packages/nx/src/command-line/release/version.ts index 4f70fff6d63f4..80c2a4a299110 100644 --- a/packages/nx/src/command-line/release/version.ts +++ b/packages/nx/src/command-line/release/version.ts @@ -1,24 +1,23 @@ import * as chalk from 'chalk'; +import { execSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; import { relative } from 'node:path'; import { Generator } from '../../config/misc-interfaces'; -import { readNxJson } from '../../config/nx-json'; +import { NxJsonConfiguration, readNxJson } from '../../config/nx-json'; import { ProjectGraph, ProjectGraphProjectNode, } from '../../config/project-graph'; -import { - NxJsonConfiguration, - joinPathFragments, - output, - workspaceRoot, -} from '../../devkit-exports'; import { FsTree, Tree, flushChanges } from '../../generators/tree'; +import { createProjectFileMapUsingProjectGraph } from '../../project-graph/file-map-utils'; import { createProjectGraphAsync, readProjectsConfigurationFromProjectGraph, } from '../../project-graph/project-graph'; +import { output } from '../../utils/output'; import { combineOptionsForGenerator, handleErrors } from '../../utils/params'; +import { joinPathFragments } from '../../utils/path'; +import { workspaceRoot } from '../../utils/workspace-root'; import { parseGeneratorString } from '../generate/generate'; import { getGeneratorInformation } from '../generate/generator-utils'; import { VersionOptions } from './command-object'; @@ -43,6 +42,8 @@ import { handleDuplicateGitTags, } from './utils/shared'; +const LARGE_BUFFER = 1024 * 1000000; + // Reexport some utils for use in plugin release-version generator implementations export { deriveNewSemverVersion } from './utils/semver'; export type { @@ -67,6 +68,9 @@ export interface ReleaseVersionGeneratorSchema { firstRelease?: boolean; // auto means the existing prefix will be preserved, and is the default behavior versionPrefix?: typeof validReleaseVersionPrefixes[number]; + skipLockFileUpdate?: boolean; + installArgs?: string; + installIgnoreScripts?: boolean; } export interface NxReleaseVersionResult { @@ -106,6 +110,7 @@ export async function releaseVersion( // Apply default configuration to any optional user configuration const { error: configError, nxReleaseConfig } = await createNxReleaseConfig( projectGraph, + await createProjectFileMapUsingProjectGraph(projectGraph), nxJson.release ); if (configError) { @@ -148,6 +153,11 @@ export async function releaseVersion( process.exit(1); } + runPreVersionCommand(nxReleaseConfig.version.preVersionCommand, { + dryRun: args.dryRun, + verbose: args.verbose, + }); + const tree = new FsTree(workspaceRoot, args.verbose); const versionData: VersionData = {}; @@ -197,6 +207,7 @@ export async function releaseVersion( args, tree, generatorData, + args.generatorOptionsOverrides, projectNames, releaseGroup, versionData @@ -206,7 +217,10 @@ export async function releaseVersion( const changedFiles = await generatorCallback(tree, { dryRun: !!args.dryRun, verbose: !!args.verbose, - generatorOptions, + generatorOptions: { + ...generatorOptions, + ...args.generatorOptionsOverrides, + }, }); changedFiles.forEach((f) => additionalChangedFiles.add(f)); }); @@ -324,6 +338,7 @@ export async function releaseVersion( args, tree, generatorData, + args.generatorOptionsOverrides, projectNames, releaseGroup, versionData @@ -333,7 +348,10 @@ export async function releaseVersion( const changedFiles = await generatorCallback(tree, { dryRun: !!args.dryRun, verbose: !!args.verbose, - generatorOptions, + generatorOptions: { + ...generatorOptions, + ...args.generatorOptionsOverrides, + }, }); changedFiles.forEach((f) => additionalChangedFiles.add(f)); }); @@ -445,6 +463,7 @@ async function runVersionOnProjects( args: VersionOptions, tree: Tree, generatorData: GeneratorData, + generatorOverrides: Record | undefined, projectNames: string[], releaseGroup: ReleaseGroupWithName, versionData: VersionData @@ -454,6 +473,7 @@ async function runVersionOnProjects( specifier: args.specifier ?? '', preid: args.preid ?? '', ...generatorData.configGeneratorOptions, + ...(generatorOverrides ?? {}), // The following are not overridable by user config projects: projectNames.map((p) => projectGraph.nodes[p]), projectGraph, @@ -610,3 +630,43 @@ function resolveGeneratorData({ throw err; } } +function runPreVersionCommand( + preVersionCommand: string, + { dryRun, verbose }: { dryRun: boolean; verbose: boolean } +) { + if (!preVersionCommand) { + return; + } + + output.logSingleLine(`Executing pre-version command`); + if (verbose) { + console.log(`Executing the following pre-version command:`); + console.log(preVersionCommand); + } + + let env: Record = { + ...process.env, + }; + if (dryRun) { + env.NX_DRY_RUN = 'true'; + } + + const stdio = verbose ? 'inherit' : 'pipe'; + try { + execSync(preVersionCommand, { + encoding: 'utf-8', + maxBuffer: LARGE_BUFFER, + stdio, + env, + }); + } catch (e) { + const title = verbose + ? `The pre-version command failed. See the full output above.` + : `The pre-version command failed. Retry with --verbose to see the full output of the pre-version command.`; + output.error({ + title, + bodyLines: [preVersionCommand, e], + }); + process.exit(1); + } +} diff --git a/packages/nx/src/config/nx-json.ts b/packages/nx/src/config/nx-json.ts index 3abfe052e08b0..c73633a3ca123 100644 --- a/packages/nx/src/config/nx-json.ts +++ b/packages/nx/src/config/nx-json.ts @@ -232,6 +232,12 @@ interface NxReleaseConfiguration { * Enable or override configuration for git operations as part of the version subcommand */ git?: NxReleaseGitConfiguration; + /** + * A command to run after validation of nx release configuration, but before versioning begins. + * Used for preparing build artifacts. If --dry-run is passed, the command is still executed, but + * with the NX_DRY_RUN environment variable set to 'true'. + */ + preVersionCommand?: string; }; /** * Optionally override the git/release tag pattern to use. This field is the source of truth