diff --git a/e2e/release/src/publishable-libraries-release.test.ts b/e2e/release/src/publishable-libraries-release.test.ts new file mode 100644 index 0000000000000..ea34e9f4bfd79 --- /dev/null +++ b/e2e/release/src/publishable-libraries-release.test.ts @@ -0,0 +1,38 @@ +import { + cleanupProject, + newProject, + runCLI, + runCommandAsync, + uniq, +} from '@nx/e2e/utils'; + +describe('publishable libraries release', () => { + 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` + ); + }); + afterAll(() => cleanupProject()); + + it('should be able to release publishable js library', async () => { + const jsLib = uniq('js-lib'); + runCLI( + `generate @nx/js:lib ${jsLib} --publishable --importPath=@proj/${jsLib}` + ); + + let versionOutput = runCLI(`release --first-release`); + versionOutput = runCLI(`release patch`); + expect(versionOutput).toContain('Executing pre-version command'); + }); +}); diff --git a/packages/js/src/generators/library/library.spec.ts b/packages/js/src/generators/library/library.spec.ts index c40ba9956755c..731657441fcba 100644 --- a/packages/js/src/generators/library/library.spec.ts +++ b/packages/js/src/generators/library/library.spec.ts @@ -1,9 +1,7 @@ import 'nx/src/internal-testing-utils/mock-project-graph'; import { - getPackageManagerCommand, getProjects, - output, readJson, readProjectConfiguration, Tree, @@ -31,7 +29,6 @@ describe('lib', () => { beforeEach(() => { tree = createTreeWithEmptyWorkspace(); tree.write('/.gitignore', ''); - tree.write('/.gitignore', ''); }); it.each` @@ -1144,357 +1141,6 @@ describe('lib', () => { outputs: ['{options.outputPath}'], }); }); - - it('should update the nx-release-publish target to specify dist/{projectRoot} as the package root', async () => { - await libraryGenerator(tree, { - ...defaultOptions, - directory: 'my-lib', - publishable: true, - importPath: '@proj/my-lib', - bundler: 'tsc', - }); - - const config = readProjectConfiguration(tree, 'my-lib'); - expect(config.targets['nx-release-publish']).toEqual({ - options: { - packageRoot: 'dist/{projectRoot}', - }, - }); - }); - - 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, - directory: 'my-lib', - publishable: true, - importPath: '@proj/my-lib', - bundler: 'tsc', - }); - - 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, - directory: 'my-lib', - publishable: true, - importPath: '@proj/my-lib', - bundler: 'tsc', - }); - - 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, - directory: 'my-lib', - publishable: true, - importPath: '@proj/my-lib', - bundler: 'tsc', - }); - - 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, - directory: 'my-lib', - publishable: true, - importPath: '@proj/my-lib', - bundler: 'tsc', - }); - - 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, - directory: 'my-lib', - publishable: true, - importPath: '@proj/my-lib', - bundler: 'tsc', - }); - - 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, - directory: 'my-lib', - publishable: true, - importPath: '@proj/my-lib', - bundler: 'tsc', - 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', - 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, - directory: 'my-lib', - publishable: true, - importPath: '@proj/my-lib', - bundler: 'tsc', - }); - - 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, - directory: 'my-lib', - publishable: true, - importPath: '@proj/my-lib', - bundler: 'tsc', - }); - - 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, - directory: 'my-lib', - publishable: true, - importPath: '@proj/my-lib', - bundler: 'tsc', - }); - - 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, - directory: 'my-lib', - publishable: true, - importPath: '@proj/my-lib', - bundler: 'tsc', - }); - - 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('--includeBabelRc', () => { diff --git a/packages/js/src/generators/library/library.ts b/packages/js/src/generators/library/library.ts index 9938f2188fea2..db060530560a1 100644 --- a/packages/js/src/generators/library/library.ts +++ b/packages/js/src/generators/library/library.ts @@ -5,14 +5,11 @@ import { formatFiles, generateFiles, GeneratorCallback, - getPackageManagerCommand, installPackagesTask, joinPathFragments, names, offsetFromRoot, - output, ProjectConfiguration, - ProjectGraphProjectNode, readNxJson, readProjectConfiguration, runTasksInSerial, @@ -69,6 +66,10 @@ import type { } from './schema'; import { sortPackageJsonFields } from '../../utils/package-json/sort-fields'; import { getImportPath } from '../../utils/get-import-path'; +import { + addReleaseOptionForPublishableTarget, + releaseTasks, +} from './utils/add-release-config'; const defaultOutputDirectory = 'dist'; @@ -115,10 +116,6 @@ export async function libraryGeneratorInternal( tasks.push(addProjectDependencies(tree, options)); } - if (options.publishable) { - tasks.push(await setupVerdaccio(tree, { ...options, skipFormat: true })); - } - if (options.bundler === 'rollup') { const { configurationGenerator } = ensurePackage('@nx/rollup', nxVersion); await configurationGenerator(tree, { @@ -249,9 +246,7 @@ export async function libraryGeneratorInternal( } if (options.publishable) { - tasks.push(() => { - logNxReleaseDocsInfo(); - }); + tasks.push(await releaseTasks(tree)); } // Always run install to link packages. @@ -337,32 +332,13 @@ async function configureProject( } if (options.publishable) { - if (!options.isUsingTsSolutionConfig) { - 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', - }, - }, - }; - } - - await addProjectToNxReleaseConfig(tree, options, projectConfiguration); + await addReleaseOptionForPublishableTarget( + tree, + options.name, + projectConfiguration, + defaultOutputDirectory, + options.isUsingTsSolutionConfig + ); } if (!options.useProjectJson) { @@ -1259,120 +1235,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 = { - [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 { const ext = ['js', 'cjs', 'mjs'].find((ext) => tree.exists(`jest.preset.${ext}`) diff --git a/packages/js/src/generators/library/utils/add-release-config.spec.ts b/packages/js/src/generators/library/utils/add-release-config.spec.ts new file mode 100644 index 0000000000000..f7438b3d9906a --- /dev/null +++ b/packages/js/src/generators/library/utils/add-release-config.spec.ts @@ -0,0 +1,305 @@ +import { + getPackageManagerCommand, + readJson, + Tree, + updateJson, + output, + ProjectConfiguration, +} from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from 'nx/src/devkit-testing-exports'; +import { addReleaseOptionForPublishableTarget } from './add-release-config'; + +describe('add release config', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + tree.write('/.gitignore', ''); + }); + + 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 addReleaseOptionForPublishableTarget(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 addReleaseOptionForPublishableTarget(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 addReleaseOptionForPublishableTarget(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 addReleaseOptionForPublishableTarget(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 addReleaseOptionForPublishableTarget(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 addReleaseOptionForPublishableTarget(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 addReleaseOptionForPublishableTarget(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 addReleaseOptionForPublishableTarget(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 addReleaseOptionForPublishableTarget(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 addReleaseOptionForPublishableTarget(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 addReleaseOptionForPublishableTarget(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 addReleaseOptionForPublishableTarget(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(); + }); +}); diff --git a/packages/js/src/generators/library/utils/add-release-config.ts b/packages/js/src/generators/library/utils/add-release-config.ts new file mode 100644 index 0000000000000..4d5118ef35a4f --- /dev/null +++ b/packages/js/src/generators/library/utils/add-release-config.ts @@ -0,0 +1,178 @@ +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'; + +/** + * This function adds the release option to the project configuration for the publishable target + * It is going to modify projectConfiguration in place and add the release option in nx.json if necessary + * @returns the modified project configuration + */ +export async function addReleaseOptionForPublishableTarget( + tree: Tree, + projectName: string, + projectConfiguration: ProjectConfiguration, + defaultOutputDirectory: string = 'dist', + isUsingTsSolutionConfig: boolean = false +): Promise { + if (!isUsingTsSolutionConfig) { + 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 addProjectToNxReleaseConfig(tree, projectName, projectConfiguration); + + return projectConfiguration; +} + +async function addProjectToNxReleaseConfig( + tree: Tree, + projectName: string, + 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: projectName, + 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(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); + return `Project already included in existing release configuration for group ${name}`; + } + } + + 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; + } +} + +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); +} + +export async function releaseTasks(tree: Tree): Promise { + 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.`, + }); +} diff --git a/packages/js/src/utils/npm-config.ts b/packages/js/src/utils/npm-config.ts index 0316fa31417c8..13542412d7ce1 100644 --- a/packages/js/src/utils/npm-config.ts +++ b/packages/js/src/utils/npm-config.ts @@ -108,13 +108,10 @@ async function getNpmConfigValue(key: string, cwd: string): Promise { async function execAsync(command: string, cwd: string): Promise { // Must be non-blocking async to allow spinner to render return new Promise((resolve, reject) => { - exec(command, { cwd, windowsHide: false }, (error, stdout, stderr) => { + exec(command, { cwd, windowsHide: false }, (error, stdout) => { if (error) { return reject(error); } - if (stderr) { - return reject(stderr); - } return resolve(stdout.trim()); }); });