feat(js): replace publish script with nx release config (#21474)

This commit is contained in:
Austin Fahsl 2024-03-05 15:53:07 -07:00 committed by GitHub
parent 391f3ab8e6
commit 38179ad278
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 1676 additions and 194 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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": [

View File

@ -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": {

View File

@ -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"],

View File

@ -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"
}
]
},

View File

@ -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,
});
};
```

View File

@ -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)

View File

@ -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: {},

View File

@ -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",

View File

@ -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);

View File

@ -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');
});
});

View File

@ -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);
});

View File

@ -1,11 +1,11 @@
import {
checkFilesExist,
cleanupProject,
getSelectedPackageManager,
packageManagerLockFile,
runCLI,
uniq,
runCreatePlugin,
cleanupProject,
uniq,
} from '@nx/e2e/utils';
describe('create-nx-plugin', () => {

View File

@ -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"',
},
});
});
expect(tree.exists('tools/scripts/publish.mjs')).toBeTruthy();
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`,
},
});
});
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();
});
});
});

View File

@ -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 { 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 { 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 { 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 {
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<PackageJson>(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<PackageJson>(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<string, ProjectGraphProjectNode> = {
[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;

View File

@ -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": {

View File

@ -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"]

View File

@ -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);

View File

@ -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;
}

View File

@ -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'."
}
}
},

View File

@ -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": {

View File

@ -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) {

View File

@ -34,6 +34,7 @@ export type VersionOptions = NxReleaseArgs &
specifier?: string;
preid?: string;
stageChanges?: boolean;
generatorOptionsOverrides?: Record<string, unknown>;
};
export type ChangelogOptions = NxReleaseArgs &

File diff suppressed because it is too large Load Diff

View File

@ -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<T> = 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<string[]> {
// 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<PackageJson>(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;
}
}

View File

@ -37,6 +37,7 @@ describe('filterReleaseGroups()', () => {
tagMessage: '',
tagArgs: '',
},
preVersionCommand: '',
},
releaseTagPattern: '',
git: {

View File

@ -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) {

View File

@ -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) {

View File

@ -151,7 +151,29 @@ export async function gitAdd({
logFn?: (...messages: string[]) => void;
}): Promise<string> {
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<boolean> {
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,

View File

@ -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<string, unknown> | 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<string, string> = {
...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);
}
}

View File

@ -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