fix(js): allow publishable library to run release command (#29775)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->
- there is no option `fallbackCurrentVersionResolver: 'disk',`
- can't run release for newly created publishable libraries

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
- move code to add release for publishable libraries into its own file
to be reused by other stacks
- add `fallbackCurrentVersionResolver: 'disk',` to project's
release.version. generatorOptions

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes https://github.com/nrwl/nx/issues/29723
This commit is contained in:
Emily Xiong 2025-02-10 11:02:15 -08:00 committed by GitHub
parent 48421fdb9e
commit 540aeff488
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 1047 additions and 153 deletions

View File

@ -0,0 +1,124 @@
import {
cleanupProject,
newProject,
runCLI,
runCommandAsync,
uniq,
} from '@nx/e2e/utils';
import { execSync } from 'node:child_process';
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}')
.replaceAll(/(\w+) lock file/g, 'PM lock file')
// 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())
.filter(Boolean)
.join('\n')
);
},
test(val: string) {
return val != null && typeof val === 'string';
},
});
describe('release publishable libraries', () => {
let e2eRegistryUrl: string;
beforeAll(async () => {
newProject({
packages: ['@nx/js'],
});
// Normalize git committer information so it is deterministic in snapshots
await runCommandAsync(`git config user.email "test@test.com"`);
await runCommandAsync(`git config user.name "Test"`);
// Create a baseline version tag
await runCommandAsync(`git tag v0.0.0`);
// We need a valid git origin to exist for the commit references to work (and later the test for createRelease)
await runCommandAsync(
`git remote add origin https://github.com/nrwl/fake-repo.git`
);
// This is the verdaccio instance that the e2e tests themselves are working from
e2eRegistryUrl = execSync('npm config get registry').toString().trim();
});
afterAll(() => cleanupProject());
it('should be able to release publishable js library', async () => {
const jsLib = uniq('my-pkg-');
runCLI(
`generate @nx/js:lib ${jsLib} --publishable --importPath=@proj/${jsLib}`
);
let releaseOutput = runCLI(`release --first-release`);
expect(releaseOutput).toContain('Executing pre-version command');
releaseOutput = runCLI(`release --specifier 0.0.2 --yes`);
expect(releaseOutput).toMatchInlineSnapshot(`
NX Executing pre-version command
NX Running release version for project: {project-name}
{project-name} 🔍 Reading data for package "@proj/{project-name}" from dist/{project-name}/package.json
{project-name} 📄 Resolved the current version as 0.0.0 from git tag "v0.0.0".
{project-name} 📄 Using the provided version specifier "0.0.2".
{project-name} New version 0.0.2 written to dist/{project-name}/package.json
"name": "@proj/{project-name}",
- "version": "0.0.1",
+ "version": "0.0.2",
"type": "commonjs",
}
+
NX Staging changed files with git
No files to stage. Skipping git add.
NX Generating an entry in CHANGELOG.md for v0.0.2
+ ## 0.0.2 (YYYY-MM-DD)
+
+ This was a version bump only, there were no code changes.
NX Staging changed files with git
NX Committing changes with git
NX Tagging commit with git
NX Running target nx-release-publish for project {project-name}:
- {project-name}
> nx run {project-name}:nx-release-publish
📦 @proj/{project-name}@0.0.2
=== Tarball Contents ===
248B README.md
XXXB package.json
38B src/index.d.ts
208B src/index.js
137B src/index.js.map
48B src/lib/{project-name}.d.ts
213B src/lib/{project-name}.js
210B src/lib/{project-name}.js.map
=== Tarball Details ===
name: @proj/{project-name}
version: 0.0.2
filename: proj-{project-name}-0.0.2.tgz
package size: XXXB
unpacked size: XXX.XXX kb
shasum: {SHASUM}
integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
total files: 8
Published to ${e2eRegistryUrl} with tag "latest"
NX Successfully ran target nx-release-publish for project {project-name}
`);
});
});

View File

@ -0,0 +1,123 @@
import {
cleanupProject,
newProject,
runCLI,
runCommandAsync,
uniq,
} from '@nx/e2e/utils';
import { execSync } from 'node:child_process';
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}')
.replaceAll(/(\w+) lock file/g, 'PM lock file')
// 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())
.filter(Boolean)
.join('\n')
);
},
test(val: string) {
return val != null && typeof val === 'string';
},
});
describe('release publishable libraries in workspace with ts solution setup', () => {
let e2eRegistryUrl: string;
beforeAll(async () => {
newProject({
packages: ['@nx/js'],
preset: 'ts',
});
// Normalize git committer information so it is deterministic in snapshots
await runCommandAsync(`git config user.email "test@test.com"`);
await runCommandAsync(`git config user.name "Test"`);
// Create a baseline version tag
await runCommandAsync(`git tag v0.0.0`);
// We need a valid git origin to exist for the commit references to work (and later the test for createRelease)
await runCommandAsync(
`git remote add origin https://github.com/nrwl/fake-repo.git`
);
// This is the verdaccio instance that the e2e tests themselves are working from
e2eRegistryUrl = execSync('npm config get registry').toString().trim();
});
afterAll(() => cleanupProject());
it('should be able to release publishable js library', async () => {
const jsLib = uniq('my-pkg-');
runCLI(
`generate @nx/js:lib ${jsLib} --publishable --importPath=@proj/${jsLib}`
);
let releaseOutput = runCLI(`release --first-release`);
expect(releaseOutput).toContain('Executing pre-version command');
releaseOutput = runCLI(`release --specifier 0.0.2 --yes`);
expect(releaseOutput).toMatchInlineSnapshot(`
NX Executing pre-version command
NX Running release version for project: @proj/{project-name}
@proj/{project-name} 🔍 Reading data for package "@proj/{project-name}" from {project-name}/package.json
@proj/{project-name} 📄 Resolved the current version as 0.0.1 from {project-name}/package.json
@proj/{project-name} 📄 Using the provided version specifier "0.0.2".
@proj/{project-name} New version 0.0.2 written to {project-name}/package.json
"name": "@proj/{project-name}",
- "version": "0.0.1",
+ "version": "0.0.2",
"type": "module",
NX Updating PM lock file
NX Staging changed files with git
NX Generating an entry in CHANGELOG.md for v0.0.2
+ ## 0.0.2 (YYYY-MM-DD)
+
+ This was a version bump only, there were no code changes.
NX Staging changed files with git
NX Committing changes with git
NX Tagging commit with git
NX Running target nx-release-publish for project @proj/{project-name}:
- @proj/{project-name}
> nx run @proj/{project-name}:nx-release-publish
📦 @proj/{project-name}@0.0.2
=== Tarball Contents ===
138B README.md
76B dist/index.d.ts
125B dist/index.d.ts.map
41B dist/index.js
92B dist/lib/{project-name}.d.ts
161B dist/lib/{project-name}.d.ts.map
64B dist/lib/{project-name}.js
XXXB package.json
=== Tarball Details ===
name: @proj/{project-name}
version: 0.0.2
filename: proj-{project-name}-0.0.2.tgz
package size: XXXB
unpacked size: XXX.XXX kb
shasum: {SHASUM}
integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
total files: 8
Published to ${e2eRegistryUrl} with tag "latest"
NX Successfully ran target nx-release-publish for project @proj/{project-name}
`);
});
});

View File

@ -31,7 +31,6 @@ describe('lib', () => {
beforeEach(() => { beforeEach(() => {
tree = createTreeWithEmptyWorkspace(); tree = createTreeWithEmptyWorkspace();
tree.write('/.gitignore', ''); tree.write('/.gitignore', '');
tree.write('/.gitignore', '');
}); });
it.each` it.each`

View File

@ -5,14 +5,11 @@ import {
formatFiles, formatFiles,
generateFiles, generateFiles,
GeneratorCallback, GeneratorCallback,
getPackageManagerCommand,
installPackagesTask, installPackagesTask,
joinPathFragments, joinPathFragments,
names, names,
offsetFromRoot, offsetFromRoot,
output,
ProjectConfiguration, ProjectConfiguration,
ProjectGraphProjectNode,
readNxJson, readNxJson,
readProjectConfiguration, readProjectConfiguration,
runTasksInSerial, runTasksInSerial,
@ -69,6 +66,11 @@ import type {
} from './schema'; } from './schema';
import { sortPackageJsonFields } from '../../utils/package-json/sort-fields'; import { sortPackageJsonFields } from '../../utils/package-json/sort-fields';
import { getImportPath } from '../../utils/get-import-path'; import { getImportPath } from '../../utils/get-import-path';
import {
addReleaseConfigForNonTsSolution,
addReleaseConfigForTsSolution,
releaseTasks,
} from './utils/add-release-config';
const defaultOutputDirectory = 'dist'; const defaultOutputDirectory = 'dist';
@ -115,10 +117,6 @@ export async function libraryGeneratorInternal(
tasks.push(addProjectDependencies(tree, options)); tasks.push(addProjectDependencies(tree, options));
} }
if (options.publishable) {
tasks.push(await setupVerdaccio(tree, { ...options, skipFormat: true }));
}
if (options.bundler === 'rollup') { if (options.bundler === 'rollup') {
const { configurationGenerator } = ensurePackage('@nx/rollup', nxVersion); const { configurationGenerator } = ensurePackage('@nx/rollup', nxVersion);
await configurationGenerator(tree, { await configurationGenerator(tree, {
@ -249,9 +247,7 @@ export async function libraryGeneratorInternal(
} }
if (options.publishable) { if (options.publishable) {
tasks.push(() => { tasks.push(await releaseTasks(tree));
logNxReleaseDocsInfo();
});
} }
// Always run install to link packages. // Always run install to link packages.
@ -337,32 +333,20 @@ async function configureProject(
} }
if (options.publishable) { if (options.publishable) {
if (!options.isUsingTsSolutionConfig) { if (options.isUsingTsSolutionConfig) {
const packageRoot = joinPathFragments( await addReleaseConfigForTsSolution(
defaultOutputDirectory, tree,
'{projectRoot}' options.name,
projectConfiguration
);
} else {
await addReleaseConfigForNonTsSolution(
tree,
options.name,
projectConfiguration,
defaultOutputDirectory
); );
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);
} }
if (!options.useProjectJson) { if (!options.useProjectJson) {
@ -1259,120 +1243,6 @@ 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: NormalizedLibraryGeneratorOptions,
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.`,
});
}
function findRootJestPreset(tree: Tree): string | null { function findRootJestPreset(tree: Tree): string | null {
const ext = ['js', 'cjs', 'mjs'].find((ext) => const ext = ['js', 'cjs', 'mjs'].find((ext) =>
tree.exists(`jest.preset.${ext}`) tree.exists(`jest.preset.${ext}`)

View File

@ -0,0 +1,598 @@
import {
getPackageManagerCommand,
readJson,
Tree,
updateJson,
output,
ProjectConfiguration,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from 'nx/src/devkit-testing-exports';
import {
addReleaseConfigForNonTsSolution,
addReleaseConfigForTsSolution,
} from './add-release-config';
describe('add release config', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
tree.write('/.gitignore', '');
});
describe('addReleaseConfigForNonTsSolution', () => {
it('should update the nx-release-publish target to specify dist/{projectRoot} as the package root', async () => {
const projectConfig: ProjectConfiguration = { root: 'libs/my-lib' };
await addReleaseConfigForNonTsSolution(tree, 'my-lib', projectConfig);
expect(projectConfig.targets?.['nx-release-publish']).toEqual({
options: {
packageRoot: 'dist/{projectRoot}',
},
});
});
it('should not change preVersionCommand if it already exists', async () => {
updateJson(tree, 'nx.json', (json) => {
json.release = {
version: {
preVersionCommand: 'echo "hello world"',
},
};
return json;
});
const projectConfig = { root: 'libs/my-lib' };
await addReleaseConfigForNonTsSolution(tree, 'my-lib', projectConfig);
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;
});
const projectConfig = { root: 'libs/my-lib' };
await addReleaseConfigForNonTsSolution(tree, 'my-lib', projectConfig);
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;
});
const projectConfig = { root: 'libs/my-lib' };
await addReleaseConfigForNonTsSolution(tree, 'my-lib', projectConfig);
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;
});
const projectConfig = { root: 'libs/my-lib' };
await addReleaseConfigForNonTsSolution(tree, 'my-lib', projectConfig);
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;
});
const projectConfig = { root: 'libs/my-lib' };
await addReleaseConfigForNonTsSolution(tree, 'my-lib', projectConfig);
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;
});
const projectConfig: ProjectConfiguration = {
root: 'libs/my-lib',
tags: ['one', 'two'],
};
await addReleaseConfigForNonTsSolution(tree, 'my-lib', projectConfig);
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;
});
const projectConfig = { root: 'packages/my-lib' };
await addReleaseConfigForNonTsSolution(tree, 'my-lib', projectConfig);
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;
});
const projectConfig = { root: 'libs/my-lib' };
await addReleaseConfigForNonTsSolution(tree, 'my-lib', projectConfig);
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;
});
const projectConfig = { root: 'libs/my-lib' };
await addReleaseConfigForNonTsSolution(tree, 'my-lib', projectConfig);
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;
});
const projectConfig = { root: 'libs/my-lib' };
await addReleaseConfigForNonTsSolution(tree, 'my-lib', projectConfig);
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;
});
const projectConfig = { root: 'libs/my-lib' };
await addReleaseConfigForNonTsSolution(tree, 'my-lib', projectConfig);
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();
});
});
describe('addReleaseConfigForTsSolution', () => {
it('should not update set nx-release-publish target', async () => {
const projectConfig: ProjectConfiguration = { root: 'libs/my-lib' };
await addReleaseConfigForTsSolution(tree, 'my-lib', projectConfig);
expect(projectConfig.targets?.['nx-release-publish']).toBeUndefined();
});
it('should not change preVersionCommand if it already exists', async () => {
updateJson(tree, 'nx.json', (json) => {
json.release = {
version: {
preVersionCommand: 'echo "hello world"',
},
};
return json;
});
const projectConfig = { root: 'libs/my-lib' };
await addReleaseConfigForTsSolution(tree, 'my-lib', projectConfig);
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;
});
const projectConfig = { root: 'libs/my-lib' };
await addReleaseConfigForTsSolution(tree, 'my-lib', projectConfig);
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;
});
const projectConfig = { root: 'libs/my-lib' };
await addReleaseConfigForTsSolution(tree, 'my-lib', projectConfig);
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;
});
const projectConfig = { root: 'libs/my-lib' };
await addReleaseConfigForTsSolution(tree, 'my-lib', projectConfig);
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;
});
const projectConfig = { root: 'libs/my-lib' };
await addReleaseConfigForTsSolution(tree, 'my-lib', projectConfig);
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;
});
const projectConfig: ProjectConfiguration = {
root: 'libs/my-lib',
tags: ['one', 'two'],
};
await addReleaseConfigForTsSolution(tree, 'my-lib', projectConfig);
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;
});
const projectConfig = { root: 'packages/my-lib' };
await addReleaseConfigForTsSolution(tree, 'my-lib', projectConfig);
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;
});
const projectConfig = { root: 'libs/my-lib' };
await addReleaseConfigForTsSolution(tree, 'my-lib', projectConfig);
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;
});
const projectConfig = { root: 'libs/my-lib' };
await addReleaseConfigForTsSolution(tree, 'my-lib', projectConfig);
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;
});
const projectConfig = { root: 'libs/my-lib' };
await addReleaseConfigForTsSolution(tree, 'my-lib', projectConfig);
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;
});
const projectConfig = { root: 'libs/my-lib' };
await addReleaseConfigForTsSolution(tree, 'my-lib', projectConfig);
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

@ -0,0 +1,183 @@
import {
GeneratorCallback,
getPackageManagerCommand,
joinPathFragments,
output,
ProjectConfiguration,
ProjectGraphProjectNode,
readNxJson,
runTasksInSerial,
Tree,
writeJson,
} from '@nx/devkit';
import { findMatchingProjects } from 'nx/src/utils/find-matching-projects';
import setupVerdaccio from '../../setup-verdaccio/generator';
/**
* Adds release option in nx.json to build the project before versioning
*/
export async function addReleaseConfigForTsSolution(
tree: Tree,
projectName: string,
projectConfiguration: ProjectConfiguration
): Promise<void> {
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 the release configuration does not exist, it will be created
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: projectName,
type: 'lib' as const,
data: {
root: projectConfiguration.root,
tags: projectConfiguration.tags,
},
};
// if the project is already included in the release configuration, it will not be added again
if (projectsConfigMatchesProject(nxJson.release.projects, project)) {
output.log({
title: `Project already included in existing release configuration`,
});
addPreVersionCommand();
writeJson(tree, 'nx.json', nxJson);
return;
}
// if the release configuration is a string, it will be converted to an array and added to it
if (Array.isArray(nxJson.release.projects)) {
nxJson.release.projects.push(projectName);
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);
output.log({
title: `Project already included in existing release configuration for group ${name}`,
});
return;
}
}
output.warn({
title: `Could not find a release group that includes ${projectName}`,
bodyLines: [
`Ensure that ${projectName} 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, projectName];
addPreVersionCommand();
writeJson(tree, 'nx.json', nxJson);
output.log({
title: `Added project to existing release configuration`,
});
return;
}
}
/**
* Add release configuration for non-ts solution projects
* Add release option in project.json and add packageRoot to nx-release-publish target
*/
export async function addReleaseConfigForNonTsSolution(
tree: Tree,
projectName: string,
projectConfiguration: ProjectConfiguration,
defaultOutputDirectory: string = 'dist'
) {
const packageRoot = joinPathFragments(
defaultOutputDirectory,
'{projectRoot}'
);
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',
fallbackCurrentVersionResolver: 'disk',
},
},
};
await addReleaseConfigForTsSolution(tree, projectName, projectConfiguration);
return projectConfiguration;
}
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);
}
export async function releaseTasks(tree: Tree): Promise<GeneratorCallback> {
return runTasksInSerial(
await setupVerdaccio(tree, { skipFormat: true }),
() => logNxReleaseDocsInfo()
);
}
function logNxReleaseDocsInfo() {
output.log({
title: `📦 To learn how to publish this library, see https://nx.dev/core-features/manage-releases.`,
});
}

View File

@ -110,10 +110,7 @@ async function execAsync(command: string, cwd: string): Promise<string> {
return new Promise<string>((resolve, reject) => { return new Promise<string>((resolve, reject) => {
exec(command, { cwd, windowsHide: false }, (error, stdout, stderr) => { exec(command, { cwd, windowsHide: false }, (error, stdout, stderr) => {
if (error) { if (error) {
return reject(error); return reject((stderr ? `${stderr}\n` : '') + error);
}
if (stderr) {
return reject(stderr);
} }
return resolve(stdout.trim()); return resolve(stdout.trim());
}); });