diff --git a/commands/version/README.md b/commands/version/README.md index f30c5ec5a7..9800481996 100644 --- a/commands/version/README.md +++ b/commands/version/README.md @@ -70,6 +70,7 @@ Running `lerna version --conventional-commits` without the above flags will rele - [`--no-granular-pathspec`](#--no-granular-pathspec) - [`--no-private`](#--no-private) - [`--no-push`](#--no-push) + - [`--npm-client-args`](#--npm-client-args) - [`--preid`](#--preid) - [`--signoff-git-commit`](#--signoff-git-commit) - [`--sign-git-commit`](#--sign-git-commit) @@ -398,6 +399,42 @@ Note that this option does _not_ exclude [private scoped packages](https://docs. By default, `lerna version` will push the committed and tagged changes to the configured [git remote](#--git-remote-name). Pass `--no-push` to disable this behavior. +### `--npm-client-args` + +This option allows arguments to be passed to the `npm install` that `lerna version` performs to update the lockfile. + +For example: + +```sh +lerna version 3.3.3 --npm-client-args=--legacy-peer-deps + +lerna version 3.3.3 --npm-client-args="--legacy-peer-deps,--force" + +lerna version 3.3.3 --npm-client-args="--legacy-peer-deps --force" +``` + +This can also be set in `lerna.json`: + +```json +{ + ... + "npmClientArgs": ["--legacy-peer-deps", "--production"] +} +``` + +or specifically for the version command: + +```json +{ + ... + "command": { + "version": { + "npmClientArgs": ["--legacy-peer-deps", "--production"] + } + } +} +``` + ### `--preid` ```sh diff --git a/commands/version/command.js b/commands/version/command.js index dee6d6e76c..f840931b93 100644 --- a/commands/version/command.js +++ b/commands/version/command.js @@ -168,6 +168,10 @@ exports.builder = (yargs, composed) => { requiresArg: true, defaultDescription: "v", }, + "npm-client-args": { + describe: "Additional arguments to pass to the npm client when performing 'npm install'.", + type: "array", + }, y: { describe: "Skip all confirmation prompts.", alias: "yes", diff --git a/commands/version/index.js b/commands/version/index.js index 8f210e827f..b9abcbc1cd 100644 --- a/commands/version/index.js +++ b/commands/version/index.js @@ -616,11 +616,14 @@ class VersionCommand extends Command { ); } + const npmClientArgsRaw = this.options.npmClientArgs || []; + const npmClientArgs = npmClientArgsRaw.reduce((args, arg) => args.concat(arg.split(/\s|,/)), []); + if (this.options.npmClient === "pnpm") { chain = chain.then(() => { this.logger.verbose("version", "Updating root pnpm-lock.yaml"); return childProcess - .exec("pnpm", ["install", "--lockfile-only", "--ignore-scripts"], this.execOpts) + .exec("pnpm", ["install", "--lockfile-only", "--ignore-scripts", ...npmClientArgs], this.execOpts) .then(() => { const lockfilePath = path.join(this.project.rootPath, "pnpm-lock.yaml"); changedFiles.add(lockfilePath); @@ -634,7 +637,11 @@ class VersionCommand extends Command { chain = chain.then(() => { this.logger.verbose("version", "Updating root package-lock.json"); return childProcess - .exec("npm", ["install", "--package-lock-only", "--ignore-scripts"], this.execOpts) + .exec( + "npm", + ["install", "--package-lock-only", "--ignore-scripts", ...npmClientArgs], + this.execOpts + ) .then(() => { changedFiles.add(lockfilePath); }); diff --git a/core/lerna/schemas/lerna-schema.json b/core/lerna/schemas/lerna-schema.json index 2fd7e24cce..4db8c6eb07 100644 --- a/core/lerna/schemas/lerna-schema.json +++ b/core/lerna/schemas/lerna-schema.json @@ -1067,6 +1067,9 @@ "npmClient": { "$ref": "#/$defs/globals/npmClient" }, + "npmClientArgs": { + "$ref": "#/$defs/globals/npmClientArgs" + }, "loglevel": { "$ref": "#/$defs/globals/loglevel" }, @@ -1123,6 +1126,9 @@ "npmClient": { "$ref": "#/$defs/globals/npmClient" }, + "npmClientArgs": { + "$ref": "#/$defs/globals/npmClientArgs" + }, "loglevel": { "$ref": "#/$defs/globals/loglevel" }, @@ -1423,6 +1429,13 @@ "default": "npm", "enum": ["npm", "yarn", "pnpm"] }, + "npmClientArgs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Arguments to pass to the npm client when running commands." + }, "loglevel": { "type": "string", "description": "The level of logging to use when running commands. Defaults to info if unspecified.", diff --git a/e2e/version/src/npm-client-args.spec.ts b/e2e/version/src/npm-client-args.spec.ts new file mode 100644 index 0000000000..c04b8c466d --- /dev/null +++ b/e2e/version/src/npm-client-args.spec.ts @@ -0,0 +1,97 @@ +import { Fixture, normalizeCommitSHAs, normalizeEnvironment } from "@lerna/e2e-utils"; + +expect.addSnapshotSerializer({ + serialize(str: string) { + return normalizeCommitSHAs(normalizeEnvironment(str)); + }, + test(val: string) { + return val != null && typeof val === "string"; + }, +}); + +describe("lerna-version-npm-client-args", () => { + let fixture: Fixture; + + beforeEach(async () => { + fixture = await Fixture.create({ + name: "lerna-version-npm-client-args", + packageManager: "npm", + initializeGit: true, + runLernaInit: true, + installDependencies: true, + }); + await fixture.lerna("create package-a -y"); + + // eslint-plugin-react-app@6.2.2 requires a peer dependency of eslint@"6.x". + // Without it, the npm install at the end of `lerna version` will fail if + // --legacy-peer-deps is not passed correctly. + await fixture.updateJson("package.json", (json) => ({ + ...json, + dependencies: { + ...(json.dependencies as Record), + "eslint-plugin-react-app": "6.2.2", + eslint: "8.25.0", + }, + })); + await fixture.exec( + "npm install eslint-plugin-react-app@6.2.2 eslint@8.25.0 --save=false --legacy-peer-deps" + ); + await fixture.createInitialGitCommit(); + await fixture.exec("git push origin test-main"); + }); + afterEach(() => fixture.destroy()); + + it("should add npmClientArgs to npm install at the end of the version command", async () => { + const output = await fixture.lerna("version 3.3.3 -y --npm-client-args=--legacy-peer-deps"); + expect(output.combinedOutput).toMatchInlineSnapshot(` + lerna notice cli v999.9.9-e2e.0 + lerna info current version 0.0.0 + lerna info Assuming all packages changed + + Changes: + - package-a: 0.0.0 => 3.3.3 + + lerna info auto-confirmed + lerna info execute Skipping releases + lerna info git Pushing tags... + lerna success version finished + + `); + }); + + it("should support multiple arguments, comma delimited", async () => { + const output = await fixture.lerna('version 3.3.3 -y --npm-client-args="--legacy-peer-deps,--fund"'); + expect(output.combinedOutput).toMatchInlineSnapshot(` + lerna notice cli v999.9.9-e2e.0 + lerna info current version 0.0.0 + lerna info Assuming all packages changed + + Changes: + - package-a: 0.0.0 => 3.3.3 + + lerna info auto-confirmed + lerna info execute Skipping releases + lerna info git Pushing tags... + lerna success version finished + + `); + }); + + it("should support multiple arguments, space delimited", async () => { + const output = await fixture.lerna('version 3.3.3 -y --npm-client-args="--legacy-peer-deps --fund"'); + expect(output.combinedOutput).toMatchInlineSnapshot(` + lerna notice cli v999.9.9-e2e.0 + lerna info current version 0.0.0 + lerna info Assuming all packages changed + + Changes: + - package-a: 0.0.0 => 3.3.3 + + lerna info auto-confirmed + lerna info execute Skipping releases + lerna info git Pushing tags... + lerna success version finished + + `); + }); +});