From 1b18dbeb03e98c70b5428a9fe457781a59f8d65d Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Wed, 31 Aug 2022 13:03:54 -0400 Subject: [PATCH] feat: pnpm workspaces support (#3284) --- .../install-node-and-dependencies/action.yml | 4 + .../__tests__/__fixtures__/pnpm/lerna.json | 7 + .../__tests__/__fixtures__/pnpm/package.json | 10 + .../pnpm/packages/package-1/package.json | 8 + .../pnpm/packages/package-2/package.json | 11 + .../__fixtures__/pnpm/pnpm-workspace.yaml | 2 + commands/add/__tests__/add-command.test.js | 9 + commands/add/index.js | 7 + .../__tests__/__fixtures__/pnpm/lerna.json | 7 + .../__tests__/__fixtures__/pnpm/package.json | 10 + .../__fixtures__/pnpm/pnpm-workspace.yaml | 2 + .../__tests__/bootstrap-command.test.js | 11 + commands/bootstrap/index.js | 7 + .../__tests__/__fixtures__/pnpm/lerna.json | 7 + .../__tests__/__fixtures__/pnpm/package.json | 10 + .../__fixtures__/pnpm/pnpm-workspace.yaml | 2 + commands/link/__tests__/link-command.test.js | 325 +++++++++--------- commands/link/index.js | 8 + commands/link/package.json | 1 + .../__tests__/__fixtures__/pnpm/lerna.json | 7 + .../__tests__/__fixtures__/pnpm/package.json | 10 + .../pnpm/packages/package-1/package.json | 8 + .../pnpm/packages/package-2/package.json | 11 + .../__fixtures__/pnpm/pnpm-workspace.yaml | 2 + commands/run/__tests__/run-command.test.js | 19 + commands/version/index.js | 12 + .../version/lib/update-lockfile-version.js | 7 +- core/command/__fixtures__/pnpm/lerna.json | 7 + core/command/__fixtures__/pnpm/package.json | 10 + .../pnpm/packages/package-1/package.json | 8 + .../pnpm/packages/package-2/package.json | 11 + core/command/__tests__/command.test.js | 19 + core/command/index.js | 7 + core/lerna/package.json | 3 +- core/lerna/schemas/lerna-schema.json | 4 +- .../__tests__/package-graph.test.js | 242 ++++++++++++- core/package-graph/index.js | 38 +- core/package/index.js | 8 +- core/project/__fixtures__/pnpm/lerna.json | 7 + core/project/__fixtures__/pnpm/package.json | 7 + .../__fixtures__/pnpm/pnpm-workspace.yaml | 3 + core/project/__tests__/project.test.js | 56 +++ core/project/index.js | 52 +++ core/project/package.json | 1 + .../lerna-publish/lerna-publish-npm.spec.ts | 11 +- .../lerna-publish/lerna-publish-pnpm.spec.ts | 93 +++++ .../lerna-publish/lerna-publish-yarn.spec.ts | 11 +- e2e/tests/lerna-run/lerna-run-nx-pnpm.spec.ts | 318 +++++++++++++++++ .../positional-arguments-pnpm.spec.ts | 234 +++++++++++++ e2e/utils/fixture.ts | 82 ++++- package-lock.json | 256 +++++--------- package.json | 2 + website/docs/recipes/using-pnpm-with-lerna.md | 46 +++ website/sidebars.js | 5 + 54 files changed, 1711 insertions(+), 354 deletions(-) create mode 100644 commands/add/__tests__/__fixtures__/pnpm/lerna.json create mode 100644 commands/add/__tests__/__fixtures__/pnpm/package.json create mode 100644 commands/add/__tests__/__fixtures__/pnpm/packages/package-1/package.json create mode 100644 commands/add/__tests__/__fixtures__/pnpm/packages/package-2/package.json create mode 100644 commands/add/__tests__/__fixtures__/pnpm/pnpm-workspace.yaml create mode 100644 commands/bootstrap/__tests__/__fixtures__/pnpm/lerna.json create mode 100644 commands/bootstrap/__tests__/__fixtures__/pnpm/package.json create mode 100644 commands/bootstrap/__tests__/__fixtures__/pnpm/pnpm-workspace.yaml create mode 100644 commands/link/__tests__/__fixtures__/pnpm/lerna.json create mode 100644 commands/link/__tests__/__fixtures__/pnpm/package.json create mode 100644 commands/link/__tests__/__fixtures__/pnpm/pnpm-workspace.yaml create mode 100644 commands/run/__tests__/__fixtures__/pnpm/lerna.json create mode 100644 commands/run/__tests__/__fixtures__/pnpm/package.json create mode 100644 commands/run/__tests__/__fixtures__/pnpm/packages/package-1/package.json create mode 100644 commands/run/__tests__/__fixtures__/pnpm/packages/package-2/package.json create mode 100644 commands/run/__tests__/__fixtures__/pnpm/pnpm-workspace.yaml create mode 100644 core/command/__fixtures__/pnpm/lerna.json create mode 100644 core/command/__fixtures__/pnpm/package.json create mode 100644 core/command/__fixtures__/pnpm/packages/package-1/package.json create mode 100644 core/command/__fixtures__/pnpm/packages/package-2/package.json create mode 100644 core/project/__fixtures__/pnpm/lerna.json create mode 100644 core/project/__fixtures__/pnpm/package.json create mode 100644 core/project/__fixtures__/pnpm/pnpm-workspace.yaml create mode 100644 e2e/tests/lerna-publish/lerna-publish-pnpm.spec.ts create mode 100644 e2e/tests/lerna-run/lerna-run-nx-pnpm.spec.ts create mode 100644 e2e/tests/lerna-version/positional-arguments-pnpm.spec.ts create mode 100644 website/docs/recipes/using-pnpm-with-lerna.md diff --git a/.github/actions/install-node-and-dependencies/action.yml b/.github/actions/install-node-and-dependencies/action.yml index 5dc2b3d164..d96816eb66 100644 --- a/.github/actions/install-node-and-dependencies/action.yml +++ b/.github/actions/install-node-and-dependencies/action.yml @@ -31,3 +31,7 @@ runs: - name: Install dependencies run: npm ci shell: bash + + - name: Install pnpm + run: npm install -g pnpm + shell: bash diff --git a/commands/add/__tests__/__fixtures__/pnpm/lerna.json b/commands/add/__tests__/__fixtures__/pnpm/lerna.json new file mode 100644 index 0000000000..59b5363af6 --- /dev/null +++ b/commands/add/__tests__/__fixtures__/pnpm/lerna.json @@ -0,0 +1,7 @@ +{ + "$schema": "node_modules/lerna/schemas/lerna-schema.json", + "useNx": false, + "useWorkspaces": true, + "version": "1.0.0", + "npmClient": "pnpm" +} diff --git a/commands/add/__tests__/__fixtures__/pnpm/package.json b/commands/add/__tests__/__fixtures__/pnpm/package.json new file mode 100644 index 0000000000..9c4a66e34b --- /dev/null +++ b/commands/add/__tests__/__fixtures__/pnpm/package.json @@ -0,0 +1,10 @@ +{ + "name": "root", + "private": true, + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "lerna": "^5.3.0" + } +} diff --git a/commands/add/__tests__/__fixtures__/pnpm/packages/package-1/package.json b/commands/add/__tests__/__fixtures__/pnpm/packages/package-1/package.json new file mode 100644 index 0000000000..b6b638e487 --- /dev/null +++ b/commands/add/__tests__/__fixtures__/pnpm/packages/package-1/package.json @@ -0,0 +1,8 @@ +{ + "name": "package-1", + "version": "1.0.0", + "scripts": { + "fail": "exit 1", + "my-script": "echo package-1" + } +} diff --git a/commands/add/__tests__/__fixtures__/pnpm/packages/package-2/package.json b/commands/add/__tests__/__fixtures__/pnpm/packages/package-2/package.json new file mode 100644 index 0000000000..702a52a434 --- /dev/null +++ b/commands/add/__tests__/__fixtures__/pnpm/packages/package-2/package.json @@ -0,0 +1,11 @@ +{ + "name": "package-2", + "version": "1.0.0", + "scripts": { + "fail": "exit 1", + "my-script": "echo package-2" + }, + "dependencies": { + "package-1": "^1.0.0" + } +} diff --git a/commands/add/__tests__/__fixtures__/pnpm/pnpm-workspace.yaml b/commands/add/__tests__/__fixtures__/pnpm/pnpm-workspace.yaml new file mode 100644 index 0000000000..dee51e928d --- /dev/null +++ b/commands/add/__tests__/__fixtures__/pnpm/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - "packages/*" diff --git a/commands/add/__tests__/add-command.test.js b/commands/add/__tests__/add-command.test.js index 46fed96e3d..f22d70c63e 100644 --- a/commands/add/__tests__/add-command.test.js +++ b/commands/add/__tests__/add-command.test.js @@ -44,6 +44,15 @@ describe("AddCommand", () => { await expect(command).rejects.toThrow(/Requested package has no version:/); }); + it("should throw when using pnpm", async () => { + const testDir = await initFixture("pnpm"); + const command = lernaAdd(testDir)("@test/package-1"); + + await expect(command).rejects.toThrow( + "Add is not supported when using `pnpm` workspaces. Use `pnpm` directly to add dependencies to packages: https://pnpm.io/cli/add" + ); + }); + it("should reference remote dependencies", async () => { const testDir = await initFixture("basic"); diff --git a/commands/add/index.js b/commands/add/index.js index fb1fff8bf8..c0c02c323e 100644 --- a/commands/add/index.js +++ b/commands/add/index.js @@ -36,6 +36,13 @@ class AddCommand extends Command { } initialize() { + if (this.options.npmClient === "pnpm") { + throw new ValidationError( + "EPNPMNOTSUPPORTED", + "Add is not supported when using `pnpm` workspaces. Use `pnpm` directly to add dependencies to packages: https://pnpm.io/cli/add" + ); + } + this.spec = npa(this.options.pkg); this.dirs = new Set(this.options.globs.map((fp) => path.resolve(this.project.rootPath, fp))); this.selfSatisfied = this.packageSatisfied(); diff --git a/commands/bootstrap/__tests__/__fixtures__/pnpm/lerna.json b/commands/bootstrap/__tests__/__fixtures__/pnpm/lerna.json new file mode 100644 index 0000000000..de57bb2a7a --- /dev/null +++ b/commands/bootstrap/__tests__/__fixtures__/pnpm/lerna.json @@ -0,0 +1,7 @@ +{ + "$schema": "node_modules/lerna/schemas/lerna-schema.json", + "useNx": true, + "useWorkspaces": true, + "version": "1.0.0", + "npmClient": "pnpm" +} diff --git a/commands/bootstrap/__tests__/__fixtures__/pnpm/package.json b/commands/bootstrap/__tests__/__fixtures__/pnpm/package.json new file mode 100644 index 0000000000..9c4a66e34b --- /dev/null +++ b/commands/bootstrap/__tests__/__fixtures__/pnpm/package.json @@ -0,0 +1,10 @@ +{ + "name": "root", + "private": true, + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "lerna": "^5.3.0" + } +} diff --git a/commands/bootstrap/__tests__/__fixtures__/pnpm/pnpm-workspace.yaml b/commands/bootstrap/__tests__/__fixtures__/pnpm/pnpm-workspace.yaml new file mode 100644 index 0000000000..dee51e928d --- /dev/null +++ b/commands/bootstrap/__tests__/__fixtures__/pnpm/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - "packages/*" diff --git a/commands/bootstrap/__tests__/bootstrap-command.test.js b/commands/bootstrap/__tests__/bootstrap-command.test.js index 20ace78c0c..28bf0d9156 100644 --- a/commands/bootstrap/__tests__/bootstrap-command.test.js +++ b/commands/bootstrap/__tests__/bootstrap-command.test.js @@ -138,6 +138,17 @@ describe("BootstrapCommand", () => { }); }); + describe("with pnpm", () => { + it("should throw validation error", async () => { + const testDir = await initFixture("pnpm"); + const command = lernaBootstrap(testDir)(); + + await expect(command).rejects.toThrow( + "Bootstrapping with pnpm is not supported. Use pnpm directly to manage dependencies: https://pnpm.io/cli/install" + ); + }); + }); + describe("with hoisting", () => { it("should hoist", async () => { const testDir = await initFixture("basic"); diff --git a/commands/bootstrap/index.js b/commands/bootstrap/index.js index a30f17475b..e901a7efd3 100644 --- a/commands/bootstrap/index.js +++ b/commands/bootstrap/index.js @@ -38,6 +38,13 @@ class BootstrapCommand extends Command { initialize() { const { registry, npmClient = "npm", npmClientArgs = [], mutex, hoist, nohoist } = this.options; + if (npmClient === "pnpm") { + throw new ValidationError( + "EWORKSPACES", + "Bootstrapping with pnpm is not supported. Use pnpm directly to manage dependencies: https://pnpm.io/cli/install" + ); + } + if (npmClient === "yarn" && hoist) { throw new ValidationError( "EWORKSPACES", diff --git a/commands/link/__tests__/__fixtures__/pnpm/lerna.json b/commands/link/__tests__/__fixtures__/pnpm/lerna.json new file mode 100644 index 0000000000..de57bb2a7a --- /dev/null +++ b/commands/link/__tests__/__fixtures__/pnpm/lerna.json @@ -0,0 +1,7 @@ +{ + "$schema": "node_modules/lerna/schemas/lerna-schema.json", + "useNx": true, + "useWorkspaces": true, + "version": "1.0.0", + "npmClient": "pnpm" +} diff --git a/commands/link/__tests__/__fixtures__/pnpm/package.json b/commands/link/__tests__/__fixtures__/pnpm/package.json new file mode 100644 index 0000000000..9c4a66e34b --- /dev/null +++ b/commands/link/__tests__/__fixtures__/pnpm/package.json @@ -0,0 +1,10 @@ +{ + "name": "root", + "private": true, + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "lerna": "^5.3.0" + } +} diff --git a/commands/link/__tests__/__fixtures__/pnpm/pnpm-workspace.yaml b/commands/link/__tests__/__fixtures__/pnpm/pnpm-workspace.yaml new file mode 100644 index 0000000000..dee51e928d --- /dev/null +++ b/commands/link/__tests__/__fixtures__/pnpm/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - "packages/*" diff --git a/commands/link/__tests__/link-command.test.js b/commands/link/__tests__/link-command.test.js index b8b1311610..c906c7f499 100644 --- a/commands/link/__tests__/link-command.test.js +++ b/commands/link/__tests__/link-command.test.js @@ -46,44 +46,44 @@ describe("LinkCommand", () => { await lernaLink(testDir)(); expect(symlinkedDirectories(testDir)).toMatchInlineSnapshot(` -Array [ - Object { - "_src": "packages/package-1", - "dest": "packages/package-2/node_modules/@test/package-1", - "type": "junction", - }, - Object { - "_src": "packages/package-1", - "dest": "packages/package-3/node_modules/@test/package-1", - "type": "junction", - }, - Object { - "_src": "packages/package-2", - "dest": "packages/package-3/node_modules/@test/package-2", - "type": "junction", - }, - Object { - "_src": "packages/package-2/cli.js", - "dest": "packages/package-3/node_modules/.bin/package-2", - "type": "exec", - }, - Object { - "_src": "packages/package-3", - "dest": "packages/package-4/node_modules/package-3", - "type": "junction", - }, - Object { - "_src": "packages/package-3/cli1.js", - "dest": "packages/package-4/node_modules/.bin/package3cli1", - "type": "exec", - }, - Object { - "_src": "packages/package-3/cli2.js", - "dest": "packages/package-4/node_modules/.bin/package3cli2", - "type": "exec", - }, -] -`); + Array [ + Object { + "_src": "packages/package-1", + "dest": "packages/package-2/node_modules/@test/package-1", + "type": "junction", + }, + Object { + "_src": "packages/package-1", + "dest": "packages/package-3/node_modules/@test/package-1", + "type": "junction", + }, + Object { + "_src": "packages/package-2", + "dest": "packages/package-3/node_modules/@test/package-2", + "type": "junction", + }, + Object { + "_src": "packages/package-2/cli.js", + "dest": "packages/package-3/node_modules/.bin/package-2", + "type": "exec", + }, + Object { + "_src": "packages/package-3", + "dest": "packages/package-4/node_modules/package-3", + "type": "junction", + }, + Object { + "_src": "packages/package-3/cli1.js", + "dest": "packages/package-4/node_modules/.bin/package3cli1", + "type": "exec", + }, + Object { + "_src": "packages/package-3/cli2.js", + "dest": "packages/package-4/node_modules/.bin/package3cli2", + "type": "exec", + }, + ] + `); }); }); @@ -93,44 +93,44 @@ Array [ await lernaLink(testDir)(); expect(symlinkedDirectories(testDir)).toMatchInlineSnapshot(` -Array [ - Object { - "_src": "packages/package-1/dist", - "dest": "packages/package-2/node_modules/@test/package-1", - "type": "junction", - }, - Object { - "_src": "packages/package-1/dist", - "dest": "packages/package-3/node_modules/@test/package-1", - "type": "junction", - }, - Object { - "_src": "packages/package-2/dist", - "dest": "packages/package-3/node_modules/@test/package-2", - "type": "junction", - }, - Object { - "_src": "packages/package-2/dist/cli.js", - "dest": "packages/package-3/node_modules/.bin/package-2", - "type": "exec", - }, - Object { - "_src": "packages/package-3/dist", - "dest": "packages/package-4/node_modules/package-3", - "type": "junction", - }, - Object { - "_src": "packages/package-3/dist/cli1.js", - "dest": "packages/package-4/node_modules/.bin/package3cli1", - "type": "exec", - }, - Object { - "_src": "packages/package-3/dist/cli2.js", - "dest": "packages/package-4/node_modules/.bin/package3cli2", - "type": "exec", - }, -] -`); + Array [ + Object { + "_src": "packages/package-1/dist", + "dest": "packages/package-2/node_modules/@test/package-1", + "type": "junction", + }, + Object { + "_src": "packages/package-1/dist", + "dest": "packages/package-3/node_modules/@test/package-1", + "type": "junction", + }, + Object { + "_src": "packages/package-2/dist", + "dest": "packages/package-3/node_modules/@test/package-2", + "type": "junction", + }, + Object { + "_src": "packages/package-2/dist/cli.js", + "dest": "packages/package-3/node_modules/.bin/package-2", + "type": "exec", + }, + Object { + "_src": "packages/package-3/dist", + "dest": "packages/package-4/node_modules/package-3", + "type": "junction", + }, + Object { + "_src": "packages/package-3/dist/cli1.js", + "dest": "packages/package-4/node_modules/.bin/package3cli1", + "type": "exec", + }, + Object { + "_src": "packages/package-3/dist/cli2.js", + "dest": "packages/package-4/node_modules/.bin/package3cli2", + "type": "exec", + }, + ] + `); }); }); @@ -140,49 +140,49 @@ Array [ await lernaLink(testDir)(); expect(symlinkedDirectories(testDir)).toMatchInlineSnapshot(` -Array [ - Object { - "_src": "packages/package-1", - "dest": "packages/package-2/node_modules/@test/package-1", - "type": "junction", - }, - Object { - "_src": "packages/package-1", - "dest": "packages/package-3/node_modules/@test/package-1", - "type": "junction", - }, - Object { - "_src": "packages/package-1", - "dest": "packages/package-4/node_modules/@test/package-1", - "type": "junction", - }, - Object { - "_src": "packages/package-2", - "dest": "packages/package-3/node_modules/@test/package-2", - "type": "junction", - }, - Object { - "_src": "packages/package-2/cli.js", - "dest": "packages/package-3/node_modules/.bin/package-2", - "type": "exec", - }, - Object { - "_src": "packages/package-3", - "dest": "packages/package-4/node_modules/package-3", - "type": "junction", - }, - Object { - "_src": "packages/package-3/cli1.js", - "dest": "packages/package-4/node_modules/.bin/package3cli1", - "type": "exec", - }, - Object { - "_src": "packages/package-3/cli2.js", - "dest": "packages/package-4/node_modules/.bin/package3cli2", - "type": "exec", - }, -] -`); + Array [ + Object { + "_src": "packages/package-1", + "dest": "packages/package-2/node_modules/@test/package-1", + "type": "junction", + }, + Object { + "_src": "packages/package-1", + "dest": "packages/package-3/node_modules/@test/package-1", + "type": "junction", + }, + Object { + "_src": "packages/package-1", + "dest": "packages/package-4/node_modules/@test/package-1", + "type": "junction", + }, + Object { + "_src": "packages/package-2", + "dest": "packages/package-3/node_modules/@test/package-2", + "type": "junction", + }, + Object { + "_src": "packages/package-2/cli.js", + "dest": "packages/package-3/node_modules/.bin/package-2", + "type": "exec", + }, + Object { + "_src": "packages/package-3", + "dest": "packages/package-4/node_modules/package-3", + "type": "junction", + }, + Object { + "_src": "packages/package-3/cli1.js", + "dest": "packages/package-4/node_modules/.bin/package3cli1", + "type": "exec", + }, + Object { + "_src": "packages/package-3/cli2.js", + "dest": "packages/package-4/node_modules/.bin/package3cli2", + "type": "exec", + }, + ] + `); }); }); @@ -192,44 +192,55 @@ Array [ await lernaLink(testDir)("--contents", "build"); expect(symlinkedDirectories(testDir)).toMatchInlineSnapshot(` -Array [ - Object { - "_src": "packages/package-1/build", - "dest": "packages/package-2/node_modules/@test/package-1", - "type": "junction", - }, - Object { - "_src": "packages/package-1/build", - "dest": "packages/package-3/node_modules/@test/package-1", - "type": "junction", - }, - Object { - "_src": "packages/package-2/build", - "dest": "packages/package-3/node_modules/@test/package-2", - "type": "junction", - }, - Object { - "_src": "packages/package-2/build/cli.js", - "dest": "packages/package-3/node_modules/.bin/package-2", - "type": "exec", - }, - Object { - "_src": "packages/package-3/build", - "dest": "packages/package-4/node_modules/package-3", - "type": "junction", - }, - Object { - "_src": "packages/package-3/build/cli1.js", - "dest": "packages/package-4/node_modules/.bin/package3cli1", - "type": "exec", - }, - Object { - "_src": "packages/package-3/build/cli2.js", - "dest": "packages/package-4/node_modules/.bin/package3cli2", - "type": "exec", - }, -] -`); + Array [ + Object { + "_src": "packages/package-1/build", + "dest": "packages/package-2/node_modules/@test/package-1", + "type": "junction", + }, + Object { + "_src": "packages/package-1/build", + "dest": "packages/package-3/node_modules/@test/package-1", + "type": "junction", + }, + Object { + "_src": "packages/package-2/build", + "dest": "packages/package-3/node_modules/@test/package-2", + "type": "junction", + }, + Object { + "_src": "packages/package-2/build/cli.js", + "dest": "packages/package-3/node_modules/.bin/package-2", + "type": "exec", + }, + Object { + "_src": "packages/package-3/build", + "dest": "packages/package-4/node_modules/package-3", + "type": "junction", + }, + Object { + "_src": "packages/package-3/build/cli1.js", + "dest": "packages/package-4/node_modules/.bin/package3cli1", + "type": "exec", + }, + Object { + "_src": "packages/package-3/build/cli2.js", + "dest": "packages/package-4/node_modules/.bin/package3cli2", + "type": "exec", + }, + ] + `); + }); + }); + + describe("with pnpm", () => { + it("should throw validation error", async () => { + const testDir = await initFixture("pnpm"); + const command = lernaLink(testDir)(); + + await expect(command).rejects.toThrow( + "Link is not supported with pnpm workspaces, since pnpm will automatically link dependencies during `pnpm install`. See the pnpm docs for details: https://pnpm.io/workspaces" + ); }); }); }); diff --git a/commands/link/index.js b/commands/link/index.js index bb0cc9fbd5..56ddfcf73a 100644 --- a/commands/link/index.js +++ b/commands/link/index.js @@ -6,6 +6,7 @@ const slash = require("slash"); const { Command } = require("@lerna/command"); const { PackageGraph } = require("@lerna/package-graph"); const { symlinkDependencies } = require("@lerna/symlink-dependencies"); +const { ValidationError } = require("@lerna/validation-error"); module.exports = factory; @@ -19,6 +20,13 @@ class LinkCommand extends Command { } initialize() { + if (this.options.npmClient === "pnpm") { + throw new ValidationError( + "EWORKSPACES", + "Link is not supported with pnpm workspaces, since pnpm will automatically link dependencies during `pnpm install`. See the pnpm docs for details: https://pnpm.io/workspaces" + ); + } + this.allPackages = this.packageGraph.rawPackageList; if (this.options.contents) { diff --git a/commands/link/package.json b/commands/link/package.json index d3c43754cb..541c7df369 100644 --- a/commands/link/package.json +++ b/commands/link/package.json @@ -35,6 +35,7 @@ "@lerna/command": "file:../../core/command", "@lerna/package-graph": "file:../../core/package-graph", "@lerna/symlink-dependencies": "file:../../utils/symlink-dependencies", + "@lerna/validation-error": "file:../../core/validation-error", "p-map": "^4.0.0", "slash": "^3.0.0" } diff --git a/commands/run/__tests__/__fixtures__/pnpm/lerna.json b/commands/run/__tests__/__fixtures__/pnpm/lerna.json new file mode 100644 index 0000000000..59b5363af6 --- /dev/null +++ b/commands/run/__tests__/__fixtures__/pnpm/lerna.json @@ -0,0 +1,7 @@ +{ + "$schema": "node_modules/lerna/schemas/lerna-schema.json", + "useNx": false, + "useWorkspaces": true, + "version": "1.0.0", + "npmClient": "pnpm" +} diff --git a/commands/run/__tests__/__fixtures__/pnpm/package.json b/commands/run/__tests__/__fixtures__/pnpm/package.json new file mode 100644 index 0000000000..9c4a66e34b --- /dev/null +++ b/commands/run/__tests__/__fixtures__/pnpm/package.json @@ -0,0 +1,10 @@ +{ + "name": "root", + "private": true, + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "lerna": "^5.3.0" + } +} diff --git a/commands/run/__tests__/__fixtures__/pnpm/packages/package-1/package.json b/commands/run/__tests__/__fixtures__/pnpm/packages/package-1/package.json new file mode 100644 index 0000000000..b6b638e487 --- /dev/null +++ b/commands/run/__tests__/__fixtures__/pnpm/packages/package-1/package.json @@ -0,0 +1,8 @@ +{ + "name": "package-1", + "version": "1.0.0", + "scripts": { + "fail": "exit 1", + "my-script": "echo package-1" + } +} diff --git a/commands/run/__tests__/__fixtures__/pnpm/packages/package-2/package.json b/commands/run/__tests__/__fixtures__/pnpm/packages/package-2/package.json new file mode 100644 index 0000000000..702a52a434 --- /dev/null +++ b/commands/run/__tests__/__fixtures__/pnpm/packages/package-2/package.json @@ -0,0 +1,11 @@ +{ + "name": "package-2", + "version": "1.0.0", + "scripts": { + "fail": "exit 1", + "my-script": "echo package-2" + }, + "dependencies": { + "package-1": "^1.0.0" + } +} diff --git a/commands/run/__tests__/__fixtures__/pnpm/pnpm-workspace.yaml b/commands/run/__tests__/__fixtures__/pnpm/pnpm-workspace.yaml new file mode 100644 index 0000000000..dee51e928d --- /dev/null +++ b/commands/run/__tests__/__fixtures__/pnpm/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - "packages/*" diff --git a/commands/run/__tests__/run-command.test.js b/commands/run/__tests__/run-command.test.js index cd9ae33258..a99fb883d0 100644 --- a/commands/run/__tests__/run-command.test.js +++ b/commands/run/__tests__/run-command.test.js @@ -298,6 +298,25 @@ describe("RunCommand", () => { }); }); + describe("in a pnpm repo with workspaces", () => { + it("runs a script on all packages", async () => { + const testDir = await initFixture("pnpm"); + await lernaRun(testDir)("my-script"); + + expect(output.logged()).toMatchInlineSnapshot(` + "package-1 + package-2" + `); + }); + + it("runs a script only in scoped packages", async () => { + const testDir = await initFixture("pnpm"); + await lernaRun(testDir)("my-script", "--scope", "package-1"); + + expect(output.logged()).toMatchInlineSnapshot(`"package-1"`); + }); + }); + // this is a temporary set of tests, which will be replaced by verdacio-driven tests // once the required setup is fully set up describe("in a repo powered by Nx", () => { diff --git a/commands/version/index.js b/commands/version/index.js index 34fbf00523..e4fbf24c87 100644 --- a/commands/version/index.js +++ b/commands/version/index.js @@ -613,6 +613,18 @@ class VersionCommand extends Command { ); } + 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) + .then(() => { + const lockfilePath = path.join(this.project.rootPath, "pnpm-lock.yaml"); + changedFiles.add(lockfilePath); + }); + }); + } + if (this.options.npmClient === "npm" || !this.options.npmClient) { const lockfilePath = path.join(this.project.rootPath, "package-lock.json"); if (fs.existsSync(lockfilePath)) { diff --git a/commands/version/lib/update-lockfile-version.js b/commands/version/lib/update-lockfile-version.js index aa029cc7ff..b3819f7fdd 100644 --- a/commands/version/lib/update-lockfile-version.js +++ b/commands/version/lib/update-lockfile-version.js @@ -1,5 +1,6 @@ "use strict"; +const log = require("npmlog"); const path = require("path"); const loadJsonFile = require("load-json-file"); const writeJsonFile = require("write-json-file"); @@ -11,7 +12,11 @@ function updateLockfileVersion(pkg) { let chain = Promise.resolve(); - chain = chain.then(() => loadJsonFile(lockfilePath).catch(() => {})); + chain = chain.then(() => + loadJsonFile(lockfilePath).catch(() => { + log.verbose("version", `${pkg.name} has no lockfile. Skipping lockfile update.`); + }) + ); chain = chain.then((obj) => { if (obj) { obj.version = pkg.version; diff --git a/core/command/__fixtures__/pnpm/lerna.json b/core/command/__fixtures__/pnpm/lerna.json new file mode 100644 index 0000000000..59b5363af6 --- /dev/null +++ b/core/command/__fixtures__/pnpm/lerna.json @@ -0,0 +1,7 @@ +{ + "$schema": "node_modules/lerna/schemas/lerna-schema.json", + "useNx": false, + "useWorkspaces": true, + "version": "1.0.0", + "npmClient": "pnpm" +} diff --git a/core/command/__fixtures__/pnpm/package.json b/core/command/__fixtures__/pnpm/package.json new file mode 100644 index 0000000000..9c4a66e34b --- /dev/null +++ b/core/command/__fixtures__/pnpm/package.json @@ -0,0 +1,10 @@ +{ + "name": "root", + "private": true, + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "lerna": "^5.3.0" + } +} diff --git a/core/command/__fixtures__/pnpm/packages/package-1/package.json b/core/command/__fixtures__/pnpm/packages/package-1/package.json new file mode 100644 index 0000000000..b6b638e487 --- /dev/null +++ b/core/command/__fixtures__/pnpm/packages/package-1/package.json @@ -0,0 +1,8 @@ +{ + "name": "package-1", + "version": "1.0.0", + "scripts": { + "fail": "exit 1", + "my-script": "echo package-1" + } +} diff --git a/core/command/__fixtures__/pnpm/packages/package-2/package.json b/core/command/__fixtures__/pnpm/packages/package-2/package.json new file mode 100644 index 0000000000..702a52a434 --- /dev/null +++ b/core/command/__fixtures__/pnpm/packages/package-2/package.json @@ -0,0 +1,11 @@ +{ + "name": "package-2", + "version": "1.0.0", + "scripts": { + "fail": "exit 1", + "my-script": "echo package-2" + }, + "dependencies": { + "package-1": "^1.0.0" + } +} diff --git a/core/command/__tests__/command.test.js b/core/command/__tests__/command.test.js index 65310780d0..91abb62446 100644 --- a/core/command/__tests__/command.test.js +++ b/core/command/__tests__/command.test.js @@ -387,5 +387,24 @@ describe("core-command", () => { }) ); }); + + it("throws ENOWORKSPACES when npm client is pnpm and useWorkspaces is not true", async () => { + const cwd = await initFixture("pnpm"); + + const lernaConfigPath = path.join(cwd, "lerna.json"); + const lernaConfig = await fs.readJson(lernaConfigPath); + await fs.writeJson(lernaConfigPath, { + ...lernaConfig, + useWorkspaces: false, + }); + + await expect(testFactory({ cwd })).rejects.toThrow( + expect.objectContaining({ + prefix: "ENOWORKSPACES", + message: + "Usage of pnpm without workspaces is not supported. To use pnpm with lerna, set useWorkspaces to true in lerna.json and configure pnpm to use workspaces: https://pnpm.io/workspaces.", + }) + ); + }); }); }); diff --git a/core/command/index.js b/core/command/index.js index 6b1ae27ca4..1e5cca3ece 100644 --- a/core/command/index.js +++ b/core/command/index.js @@ -247,6 +247,13 @@ class Command { ` ); } + + if (this.options.npmClient === "pnpm" && !this.options.useWorkspaces) { + throw new ValidationError( + "ENOWORKSPACES", + "Usage of pnpm without workspaces is not supported. To use pnpm with lerna, set useWorkspaces to true in lerna.json and configure pnpm to use workspaces: https://pnpm.io/workspaces." + ); + } } runPreparations() { diff --git a/core/lerna/package.json b/core/lerna/package.json index a0b258b7b0..017e117c5d 100644 --- a/core/lerna/package.json +++ b/core/lerna/package.json @@ -54,6 +54,7 @@ "@lerna/version": "file:../../commands/version", "import-local": "^3.0.2", "npmlog": "^6.0.2", - "nx": ">=14.5.4 < 16" + "nx": ">=14.5.4 < 16", + "typescript": "^3 || ^4" } } diff --git a/core/lerna/schemas/lerna-schema.json b/core/lerna/schemas/lerna-schema.json index cfba84c192..ab604b0beb 100644 --- a/core/lerna/schemas/lerna-schema.json +++ b/core/lerna/schemas/lerna-schema.json @@ -1401,9 +1401,9 @@ "globals": { "npmClient": { "type": "string", - "description": "The npm client to use when running commands (either npm or yarn). Defaults to npm if unspecified.", + "description": "The npm client to use when running commands (either npm, yarn, or pnpm). Defaults to npm if unspecified.", "default": "npm", - "enum": ["npm", "yarn"] + "enum": ["npm", "yarn", "pnpm"] }, "loglevel": { "type": "string", diff --git a/core/package-graph/__tests__/package-graph.test.js b/core/package-graph/__tests__/package-graph.test.js index 7d716d875f..5542f18d8e 100644 --- a/core/package-graph/__tests__/package-graph.test.js +++ b/core/package-graph/__tests__/package-graph.test.js @@ -15,10 +15,10 @@ describe("PackageGraph", () => { ]; expect(() => new PackageGraph(pkgs)).toThrowErrorMatchingInlineSnapshot(` -"Package name \\"pkg-2\\" used in multiple packages: - /test/pkg-2 - /test/pkg-3" -`); + "Package name \\"pkg-2\\" used in multiple packages: + /test/pkg-2 + /test/pkg-3" + `); }); it("externalizes non-satisfied semver of local sibling", () => { @@ -92,6 +92,240 @@ describe("PackageGraph", () => { expect(pkg1.localDependents.has("pkg-2")).toBe(true); expect(pkg2.localDependencies.has("pkg-1")).toBe(true); }); + + describe("with spec containing workspace: prefix", () => { + describe("creates graph links for sibling package when semver is satisfied", () => { + it("with exact match", () => { + const packages = [ + new Package( + { + name: "test-1", + version: "1.0.2", + }, + "/test/test-1" + ), + new Package( + { + name: "test-2", + version: "1.0.0", + dependencies: { + "test-1": "workspace:1.0.2", + }, + }, + "/test/test-2" + ), + ]; + const graph = new PackageGraph(packages, "allDependencies"); + const package1 = graph.get("test-1"); + const package2 = graph.get("test-2"); + + expect(package1.localDependents.has("test-2")).toBe(true); + expect(package2.localDependencies.has("test-1")).toBe(true); + expect(package2.localDependencies.get("test-1").workspaceSpec).toBe("workspace:1.0.2"); + expect(package2.localDependencies.get("test-1").workspaceAlias).toBeUndefined(); + }); + + it("with ^", () => { + const packages = [ + new Package( + { + name: "test-1", + version: "1.1.2", + }, + "/test/test-1" + ), + new Package( + { + name: "test-2", + version: "1.0.0", + dependencies: { + "test-1": "workspace:^1.0.0", + }, + }, + "/test/test-2" + ), + ]; + const graph = new PackageGraph(packages, "allDependencies"); + const package1 = graph.get("test-1"); + const package2 = graph.get("test-2"); + + expect(package1.localDependents.has("test-2")).toBe(true); + expect(package2.localDependencies.has("test-1")).toBe(true); + expect(package2.localDependencies.get("test-1").workspaceSpec).toBe("workspace:^1.0.0"); + expect(package2.localDependencies.get("test-1").workspaceAlias).toBeUndefined(); + }); + + it("with ~", () => { + const packages = [ + new Package( + { + name: "test-1", + version: "1.1.2", + }, + "/test/test-1" + ), + new Package( + { + name: "test-2", + version: "1.0.0", + dependencies: { + "test-1": "workspace:~1.1.0", + }, + }, + "/test/test-2" + ), + ]; + const graph = new PackageGraph(packages, "allDependencies"); + const package1 = graph.get("test-1"); + const package2 = graph.get("test-2"); + + expect(package1.localDependents.has("test-2")).toBe(true); + expect(package2.localDependencies.has("test-1")).toBe(true); + expect(package2.localDependencies.get("test-1").workspaceSpec).toBe("workspace:~1.1.0"); + expect(package2.localDependencies.get("test-1").workspaceAlias).toBeUndefined(); + }); + }); + + it("creates graph links for sibling package when using * alias", () => { + const packages = [ + new Package( + { + name: "test-1", + version: "1.0.0", + }, + "/test/test-1" + ), + new Package( + { + name: "test-2", + version: "1.0.0", + dependencies: { + "test-1": "workspace:*", + }, + }, + "/test/test-2" + ), + ]; + const graph = new PackageGraph(packages, "allDependencies"); + const package1 = graph.get("test-1"); + const package2 = graph.get("test-2"); + + expect(package1.localDependents.has("test-2")).toBe(true); + expect(package2.localDependencies.has("test-1")).toBe(true); + expect(package2.localDependencies.get("test-1").workspaceSpec).toBe("workspace:*"); + expect(package2.localDependencies.get("test-1").workspaceAlias).toBe("*"); + }); + + it("creates graph links for sibling package when using ~ alias", () => { + const packages = [ + new Package( + { + name: "test-1", + version: "1.0.0", + }, + "/test/test-1" + ), + new Package( + { + name: "test-2", + version: "1.0.0", + dependencies: { + "test-1": "workspace:~", + }, + }, + "/test/test-2" + ), + ]; + const graph = new PackageGraph(packages, "allDependencies"); + const package1 = graph.get("test-1"); + const package2 = graph.get("test-2"); + + expect(package1.localDependents.has("test-2")).toBe(true); + expect(package2.localDependencies.has("test-1")).toBe(true); + expect(package2.localDependencies.get("test-1").workspaceSpec).toBe("workspace:~"); + expect(package2.localDependencies.get("test-1").workspaceAlias).toBe("~"); + }); + + it("creates graph links for sibling package when using ^ alias", () => { + const packages = [ + new Package( + { + name: "test-1", + version: "1.0.0", + }, + "/test/test-1" + ), + new Package( + { + name: "test-2", + version: "1.0.0", + dependencies: { + "test-1": "workspace:^", + }, + }, + "/test/test-2" + ), + ]; + const graph = new PackageGraph(packages, "allDependencies"); + const package1 = graph.get("test-1"); + const package2 = graph.get("test-2"); + + expect(package1.localDependents.has("test-2")).toBe(true); + expect(package2.localDependencies.has("test-1")).toBe(true); + expect(package2.localDependencies.get("test-1").workspaceSpec).toBe("workspace:^"); + expect(package2.localDependencies.get("test-1").workspaceAlias).toBe("^"); + }); + + it("throws an error when sibling package exists in the workspace, but with a version that does not match the specification", () => { + const packages = [ + new Package( + { + name: "test-1", + version: "1.0.9", + }, + "/test/test-1" + ), + new Package( + { + name: "test-2", + version: "1.0.0", + dependencies: { + "test-1": "workspace:^1.1.0", + }, + }, + "/test/test-2" + ), + ]; + expect(() => new PackageGraph(packages)).toThrowErrorMatchingInlineSnapshot( + `"Package specification \\"test-1@^1.1.0\\" could not be resolved within the workspace. To reference a non-matching, remote version of a local dependency, remove the 'workspace:' prefix."` + ); + }); + + it("throws an error when sibling package does not exist in the workspace, regardless of version specification", () => { + const packages = [ + new Package( + { + name: "test-1", + version: "1.0.0", + }, + "/test/test-1" + ), + new Package( + { + name: "test-2", + version: "1.0.0", + dependencies: { + "test-3": "workspace:^1.0.0", + }, + }, + "/test/test-2" + ), + ]; + expect(() => new PackageGraph(packages)).toThrowErrorMatchingInlineSnapshot( + `"Package specification \\"test-3@^1.0.0\\" could not be resolved within the workspace. To use the 'workspace:' protocol, ensure that a package with name \\"test-3\\" exists in the current workspace."` + ); + }); + }); }); describe("Node", () => { diff --git a/core/package-graph/index.js b/core/package-graph/index.js index dd0e7acf5c..be48e91dbb 100644 --- a/core/package-graph/index.js +++ b/core/package-graph/index.js @@ -62,8 +62,36 @@ class PackageGraph extends Map { // Yarn decided to ignore https://github.com/npm/npm/pull/15900 and implemented "link:" // As they apparently have no intention of being compatible, we have to do it for them. // @see https://github.com/yarnpkg/yarn/issues/4212 - const spec = graphDependencies[depName].replace(/^link:/, "file:"); + let spec = graphDependencies[depName].replace(/^link:/, "file:"); + + // Support workspace: protocol for pnpm and yarn 2+ (https://pnpm.io/workspaces#workspace-protocol-workspace) + const isWorkspaceSpec = /^workspace:/.test(spec); + + let fullWorkspaceSpec; + let workspaceAlias; + if (isWorkspaceSpec) { + fullWorkspaceSpec = spec; + spec = spec.replace(/^workspace:/, ""); + + if (!depNode) { + throw new ValidationError( + "EWORKSPACE", + `Package specification "${depName}@${spec}" could not be resolved within the workspace. To use the 'workspace:' protocol, ensure that a package with name "${depName}" exists in the current workspace.` + ); + } + + // replace aliases (https://pnpm.io/workspaces#referencing-workspace-packages-through-aliases) + if (spec === "*" || spec === "^" || spec === "~") { + workspaceAlias = spec; + const prefix = spec === "*" ? "" : spec; + const version = depNode.version; + spec = `${prefix}${version}`; + } + } + const resolved = npa.resolve(depName, spec, currentNode.location); + resolved.workspaceSpec = fullWorkspaceSpec; + resolved.workspaceAlias = workspaceAlias; if (!depNode) { // it's an external dependency, store the resolution and bail @@ -75,6 +103,14 @@ class PackageGraph extends Map { currentNode.localDependencies.set(depName, resolved); depNode.localDependents.set(currentName, currentNode); } else { + if (isWorkspaceSpec) { + // pnpm refuses to resolve remote dependencies when using the workspace: protocol, so lerna does too. See: https://pnpm.io/workspaces#workspace-protocol-workspace. + throw new ValidationError( + "EWORKSPACE", + `Package specification "${depName}@${spec}" could not be resolved within the workspace. To reference a non-matching, remote version of a local dependency, remove the 'workspace:' prefix.` + ); + } + // non-matching semver of a local dependency currentNode.externalDependencies.set(depName, resolved); } diff --git a/core/package/index.js b/core/package/index.js index b1ee4dd51c..a9609436fc 100644 --- a/core/package/index.js +++ b/core/package/index.js @@ -270,7 +270,13 @@ class Package { depCollection = this.devDependencies; } - if (resolved.registry || resolved.type === "directory") { + if (resolved.workspaceSpec) { + // do nothing if there is a workspace alias since they don't specify a version number + if (!resolved.workspaceAlias) { + const workspacePrefix = resolved.workspaceSpec.match(/^(workspace:[*|~|^]?)/)[0]; + depCollection[depName] = `${workspacePrefix}${depVersion}`; + } + } else if (resolved.registry || resolved.type === "directory") { // a version (1.2.3) OR range (^1.2.3) OR directory (file:../foo-pkg) depCollection[depName] = `${savePrefix}${depVersion}`; } else if (resolved.gitCommittish) { diff --git a/core/project/__fixtures__/pnpm/lerna.json b/core/project/__fixtures__/pnpm/lerna.json new file mode 100644 index 0000000000..59b5363af6 --- /dev/null +++ b/core/project/__fixtures__/pnpm/lerna.json @@ -0,0 +1,7 @@ +{ + "$schema": "node_modules/lerna/schemas/lerna-schema.json", + "useNx": false, + "useWorkspaces": true, + "version": "1.0.0", + "npmClient": "pnpm" +} diff --git a/core/project/__fixtures__/pnpm/package.json b/core/project/__fixtures__/pnpm/package.json new file mode 100644 index 0000000000..824520e1f4 --- /dev/null +++ b/core/project/__fixtures__/pnpm/package.json @@ -0,0 +1,7 @@ +{ + "name": "root", + "private": true, + "devDependencies": { + "lerna": "^5.3.0" + } +} diff --git a/core/project/__fixtures__/pnpm/pnpm-workspace.yaml b/core/project/__fixtures__/pnpm/pnpm-workspace.yaml new file mode 100644 index 0000000000..ff33e3c9bf --- /dev/null +++ b/core/project/__fixtures__/pnpm/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - "packages/*" + - "modules/*" diff --git a/core/project/__tests__/project.test.js b/core/project/__tests__/project.test.js index 3355c0a633..2c67a97b06 100644 --- a/core/project/__tests__/project.test.js +++ b/core/project/__tests__/project.test.js @@ -2,6 +2,7 @@ const fs = require("fs-extra"); const path = require("path"); +const { dump } = require("js-yaml"); const { loggingOutput } = require("@lerna-test/helpers/logging-output"); // helpers @@ -221,6 +222,61 @@ describe("Project", () => { }); describe("get .packageConfigs", () => { + describe("when npmClient is pnpm", () => { + it("returns packages from pnpm workspace config", async () => { + const cwd = await initFixture("pnpm"); + + const project = new Project(cwd); + + expect(project.packageConfigs).toEqual(["packages/*", "modules/*"]); + + const warningLogs = loggingOutput("warn"); + expect(warningLogs).toEqual([]); + + const verboseLogs = loggingOutput("verbose"); + expect( + verboseLogs.includes( + "Package manager 'pnpm' detected. Resolving packages using 'pnpm-workspace.yaml'." + ) + ).toBe(true); + }); + + it("throws with friendly error if pnpm workspaces file does not exist", async () => { + const cwd = await initFixture("pnpm"); + + await fs.remove(path.join(cwd, "pnpm-workspace.yaml")); + + const project = new Project(cwd); + + expect(() => project.packageConfigs).toThrowErrorMatchingInlineSnapshot( + `"No pnpm-workspace.yaml found. See https://pnpm.io/workspaces for help configuring workspaces in pnpm."` + ); + + const verboseLogs = loggingOutput("verbose"); + expect( + verboseLogs.includes( + "Package manager 'pnpm' detected. Resolving packages using 'pnpm-workspace.yaml'." + ) + ).toBe(true); + }); + + it("throws with friendly error if pnpm workspaces file has no packages property", async () => { + const cwd = await initFixture("pnpm"); + + const pnpmWorkspaceConfigPath = path.join(cwd, "pnpm-workspace.yaml"); + const pnpmWorkspaceConfigContent = dump({ + otherProperty: ["someValue"], + }); + await fs.writeFile(pnpmWorkspaceConfigPath, pnpmWorkspaceConfigContent); + + const project = new Project(cwd); + + expect(() => project.packageConfigs).toThrowErrorMatchingInlineSnapshot( + `"No 'packages' property found in pnpm-workspace.yaml. See https://pnpm.io/workspaces for help configuring workspaces in pnpm."` + ); + }); + }); + it("returns the default packageConfigs and warns when neither workspaces nor packages are explicitly configured", () => { const project = new Project(testDir); expect(project.packageConfigs).toEqual(["packages/*"]); diff --git a/core/project/index.js b/core/project/index.js index f57e68c8c2..4448b7bb26 100644 --- a/core/project/index.js +++ b/core/project/index.js @@ -7,8 +7,10 @@ const globParent = require("glob-parent"); const loadJsonFile = require("load-json-file"); const log = require("npmlog"); const pMap = require("p-map"); +const fs = require("fs"); const path = require("path"); const writeJsonFile = require("write-json-file"); +const { load } = require("js-yaml"); const { ValidationError } = require("@lerna/validation-error"); const { Package } = require("@lerna/package"); @@ -22,6 +24,12 @@ const { makeFileFinder, makeSyncFileFinder } = require("./lib/make-file-finder") * @property {boolean} useNx * @property {boolean} useWorkspaces * @property {string} version + * @property {string} npmClient + */ + +/** + * @typedef {object} PnpmWorkspaceConfig + * @property {string[]} packages */ /** @@ -103,6 +111,24 @@ class Project { } get packageConfigs() { + if (this.config.npmClient === "pnpm") { + log.verbose( + "packageConfigs", + "Package manager 'pnpm' detected. Resolving packages using 'pnpm-workspace.yaml'." + ); + + const workspaces = this.pnpmWorkspaceConfig.packages; + + if (!workspaces) { + throw new ValidationError( + "EWORKSPACES", + "No 'packages' property found in pnpm-workspace.yaml. See https://pnpm.io/workspaces for help configuring workspaces in pnpm." + ); + } + + return workspaces; + } + if (this.config.useWorkspaces) { const workspaces = this.manifest.get("workspaces"); @@ -175,6 +201,32 @@ class Project { return manifest; } + /** @type {PnpmWorkspaceConfig} */ + get pnpmWorkspaceConfig() { + let config; + + try { + const configLocation = path.join(this.rootPath, "pnpm-workspace.yaml"); + const configContent = fs.readFileSync(configLocation); + config = load(configContent); + + Object.defineProperty(this, "pnpmWorkspaceConfig", { + value: config, + }); + } catch (err) { + if (err.message.includes("ENOENT: no such file or directory")) { + throw new ValidationError( + "ENOENT", + "No pnpm-workspace.yaml found. See https://pnpm.io/workspaces for help configuring workspaces in pnpm." + ); + } + + throw new ValidationError(err.name, err.message); + } + + return config; + } + get licensePath() { let licensePath; diff --git a/core/project/package.json b/core/project/package.json index 3b0423dde2..5199c2ed38 100644 --- a/core/project/package.json +++ b/core/project/package.json @@ -39,6 +39,7 @@ "dot-prop": "^6.0.1", "glob-parent": "^5.1.1", "globby": "^11.0.2", + "js-yaml": "^4.1.0", "load-json-file": "^6.2.0", "npmlog": "^6.0.2", "p-map": "^4.0.0", diff --git a/e2e/tests/lerna-publish/lerna-publish-npm.spec.ts b/e2e/tests/lerna-publish/lerna-publish-npm.spec.ts index 64120ca18e..da78c5f52c 100644 --- a/e2e/tests/lerna-publish/lerna-publish-npm.spec.ts +++ b/e2e/tests/lerna-publish/lerna-publish-npm.spec.ts @@ -1,13 +1,12 @@ import { Fixture } from "../../utils/fixture"; -import { normalizeEnvironment } from "../../utils/snapshot-serializer-utils"; +import { normalizeCommitSHAs, normalizeEnvironment } from "../../utils/snapshot-serializer-utils"; const randomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min; const randomVersion = () => `${randomInt(10, 89)}.${randomInt(10, 89)}.${randomInt(10, 89)}`; expect.addSnapshotSerializer({ serialize(str: string) { - return normalizeEnvironment(str) - .replaceAll(/shasum:\s*\w*/g, "shasum: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX") + return normalizeCommitSHAs(normalizeEnvironment(str)) .replaceAll(/integrity:\s*.*/g, "integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX") .replaceAll(/\d*B package\.json/g, "XXXB package.json") .replaceAll(/size:\s*\d*\s?B/g, "size: XXXB") @@ -23,7 +22,7 @@ describe("lerna-publish-npm", () => { beforeEach(async () => { fixture = await Fixture.create({ - name: "lerna-publish-npm", + name: "lerna-publish", packageManager: "npm", initializeGit: true, runLernaInit: true, @@ -74,7 +73,7 @@ describe("lerna-publish-npm", () => { lerna notice filename: test-1-XX.XX.XX.tgz lerna notice package size: XXXB lerna notice unpacked size: XXX.XXX kb - lerna notice shasum: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + lerna notice shasum: {FULL_COMMIT_SHA} lerna notice integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX lerna notice total files: 3 lerna notice @@ -137,7 +136,7 @@ describe("lerna-publish-npm", () => { lerna notice filename: test-1-XX.XX.XX.tgz lerna notice package size: XXXB lerna notice unpacked size: XXX.XXX kb - lerna notice shasum: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + lerna notice shasum: {FULL_COMMIT_SHA} lerna notice integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX lerna notice total files: 3 lerna notice diff --git a/e2e/tests/lerna-publish/lerna-publish-pnpm.spec.ts b/e2e/tests/lerna-publish/lerna-publish-pnpm.spec.ts new file mode 100644 index 0000000000..398a763772 --- /dev/null +++ b/e2e/tests/lerna-publish/lerna-publish-pnpm.spec.ts @@ -0,0 +1,93 @@ +import { Fixture } from "../../utils/fixture"; +import { normalizeCommitSHAs, normalizeEnvironment } from "../../utils/snapshot-serializer-utils"; + +const randomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min; +const randomVersion = () => `${randomInt(10, 89)}.${randomInt(10, 89)}.${randomInt(10, 89)}`; + +expect.addSnapshotSerializer({ + serialize(str: string) { + return normalizeCommitSHAs(normalizeEnvironment(str)) + .replaceAll(/integrity:\s*.*/g, "integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX") + .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"); + }, + test(val: string) { + return val != null && typeof val === "string"; + }, +}); + +describe("lerna-publish-pnpm", () => { + let fixture: Fixture; + + beforeEach(async () => { + fixture = await Fixture.create({ + name: "lerna-publish", + packageManager: "pnpm", + initializeGit: true, + runLernaInit: true, + installDependencies: true, + }); + }); + afterEach(() => fixture.destroy()); + + describe("from-git", () => { + it("should publish to the remote registry", async () => { + await fixture.lerna("create test-1 -y"); + await fixture.createInitialGitCommit(); + await fixture.exec("git push origin test-main"); + + const version = randomVersion(); + await fixture.lerna(`version ${version} -y`); + + const output = await fixture.lerna("publish from-git -y"); + const unpublishOutput = await fixture.exec( + `npm unpublish --force test-1@${version} --registry=http://localhost:4872` + ); + + const replaceVersion = (str: string) => str.replaceAll(version, "XX.XX.XX"); + + expect(replaceVersion(output.combinedOutput)).toMatchInlineSnapshot(` + lerna notice cli v999.9.9-e2e.0 + + Found 1 package to publish: + - test-1 => XX.XX.XX + + lerna info auto-confirmed + lerna info publish Publishing packages to npm... + lerna notice Skipping all user and access validation due to third-party registry + lerna notice Make sure you're authenticated properly ¯\\_(ツ)_/¯ + lerna WARN ENOLICENSE Package test-1 is missing a license. + lerna WARN ENOLICENSE One way to fix this is to add a LICENSE.md file to the root of this repository. + lerna WARN ENOLICENSE See https://choosealicense.com for additional guidance. + lerna success published test-1 XX.XX.XX + lerna notice + lerna notice 📦 test-1@XX.XX.XX + lerna notice === Tarball Contents === + lerna notice 92B lib/test-1.js + lerna notice XXXB package.json + lerna notice 110B README.md + lerna notice === Tarball Details === + lerna notice name: test-1 + lerna notice version: XX.XX.XX + lerna notice filename: test-1-XX.XX.XX.tgz + lerna notice package size: XXXB + lerna notice unpacked size: XXX.XXX kb + lerna notice shasum: {FULL_COMMIT_SHA} + lerna notice integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + lerna notice total files: 3 + lerna notice + Successfully published: + - test-1@XX.XX.XX + lerna success published 1 package + + `); + + expect(replaceVersion(unpublishOutput.combinedOutput)).toMatchInlineSnapshot(` + npm WARN using --force Recommended protections disabled. + - test-1@XX.XX.XX + + `); + }); + }); +}); diff --git a/e2e/tests/lerna-publish/lerna-publish-yarn.spec.ts b/e2e/tests/lerna-publish/lerna-publish-yarn.spec.ts index 3ead1161bf..6444a99484 100644 --- a/e2e/tests/lerna-publish/lerna-publish-yarn.spec.ts +++ b/e2e/tests/lerna-publish/lerna-publish-yarn.spec.ts @@ -1,13 +1,12 @@ import { Fixture } from "../../utils/fixture"; -import { normalizeEnvironment } from "../../utils/snapshot-serializer-utils"; +import { normalizeCommitSHAs, normalizeEnvironment } from "../../utils/snapshot-serializer-utils"; const randomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min; const randomVersion = () => `${randomInt(10, 89)}.${randomInt(10, 89)}.${randomInt(10, 89)}`; expect.addSnapshotSerializer({ serialize(str: string) { - return normalizeEnvironment(str) - .replaceAll(/shasum:\s*\w*/g, "shasum: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX") + return normalizeCommitSHAs(normalizeEnvironment(str)) .replaceAll(/integrity:\s*.*/g, "integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX") .replaceAll(/\d*B package\.json/g, "XXXB package.json") .replaceAll(/size:\s*\d*\s?B/g, "size: XXXB") @@ -23,7 +22,7 @@ describe("lerna-publish-yarn", () => { beforeEach(async () => { fixture = await Fixture.create({ - name: "lerna-publish-yarn", + name: "lerna-publish", packageManager: "yarn", initializeGit: true, runLernaInit: true, @@ -74,7 +73,7 @@ describe("lerna-publish-yarn", () => { lerna notice filename: test-1-XX.XX.XX.tgz lerna notice package size: XXXB lerna notice unpacked size: XXX.XXX kb - lerna notice shasum: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + lerna notice shasum: {FULL_COMMIT_SHA} lerna notice integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX lerna notice total files: 3 lerna notice @@ -137,7 +136,7 @@ describe("lerna-publish-yarn", () => { lerna notice filename: test-1-XX.XX.XX.tgz lerna notice package size: XXXB lerna notice unpacked size: XXX.XXX kb - lerna notice shasum: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + lerna notice shasum: {FULL_COMMIT_SHA} lerna notice integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX lerna notice total files: 3 lerna notice diff --git a/e2e/tests/lerna-run/lerna-run-nx-pnpm.spec.ts b/e2e/tests/lerna-run/lerna-run-nx-pnpm.spec.ts new file mode 100644 index 0000000000..be097ab465 --- /dev/null +++ b/e2e/tests/lerna-run/lerna-run-nx-pnpm.spec.ts @@ -0,0 +1,318 @@ +import { Fixture } from "../../utils/fixture"; +import { normalizeCommandOutput, normalizeEnvironment } from "../../utils/snapshot-serializer-utils"; + +expect.addSnapshotSerializer({ + serialize(str: string) { + return normalizeCommandOutput(normalizeEnvironment(str)) + .replaceAll(/package-X\w/g, "package-X") + .replaceAll(/lerna-run-pnpm-\d*\//g, "lerna-run-pnpm-XXXXXXXX/"); + }, + test(val: string) { + return val != null && typeof val === "string"; + }, +}); + +describe("lerna-run-nx", () => { + let fixture: Fixture; + + beforeAll(async () => { + fixture = await Fixture.create({ + name: "lerna-run", + packageManager: "pnpm", + initializeGit: true, + runLernaInit: true, + installDependencies: true, + /** + * Because lerna run involves spawning further child processes, the tests would be too flaky + * if we didn't force deterministic terminal output by appending stderr to stdout instead + * of interleaving them. + */ + forceDeterministicTerminalOutput: true, + }); + + await fixture.lerna("create package-1 -y"); + await fixture.addScriptsToPackage({ + packagePath: "packages/package-1", + scripts: { + "print-name": "echo test-package-1", + }, + }); + + await fixture.lerna("create package-2 -y"); + await fixture.addScriptsToPackage({ + packagePath: "packages/package-2", + scripts: { + "print-name": "echo test-package-2", + }, + }); + + await fixture.lerna("create package-3 -y"); + await fixture.addScriptsToPackage({ + packagePath: "packages/package-3", + scripts: { + "print-name": "echo test-package-3", + }, + }); + + await fixture.lerna("create package-4 -y"); + await fixture.addScriptsToPackage({ + packagePath: "packages/package-4", + scripts: { + "print-name": "echo test-package-4", + }, + }); + + await fixture.lerna("create package-4a -y"); + await fixture.addScriptsToPackage({ + packagePath: "packages/package-4a", + scripts: { + "print-name": "echo test-package-4a", + }, + }); + + await fixture.lerna("create package-4b -y"); + await fixture.addScriptsToPackage({ + packagePath: "packages/package-4b", + scripts: { + "print-name": "echo test-package-4b", + }, + }); + + await fixture.lerna("create package-5 -y"); + await fixture.addScriptsToPackage({ + packagePath: "packages/package-5", + scripts: { + "print-name": "echo test-package-5", + }, + }); + + await fixture.lerna("create package-6 -y"); + await fixture.addScriptsToPackage({ + packagePath: "packages/package-6", + scripts: { + "print-name": "echo test-package-6", + }, + }); + + await fixture.lerna("create package-7 -y"); + await fixture.addScriptsToPackage({ + packagePath: "packages/package-7", + scripts: { + "print-name": "echo test-package-7", + }, + }); + + await fixture.lerna("create package-8 -y"); + await fixture.addScriptsToPackage({ + packagePath: "packages/package-8", + scripts: { + "print-name": "echo test-package-8", + }, + }); + + await fixture.lerna("create package-9 -y"); + await fixture.addScriptsToPackage({ + packagePath: "packages/package-9", + scripts: { + "print-name": "echo test-package-9", + }, + }); + + await fixture.lerna("create package-app -y"); + await fixture.addScriptsToPackage({ + packagePath: "packages/package-app", + scripts: { + "print-name": "echo test-package-app", + }, + }); + + await fixture.createInitialGitCommit(); + await fixture.exec("git push --set-upstream origin test-main"); + await fixture.lerna("version 1.0.0 -y"); + + await fixture.addDependencyToPackage({ + packagePath: "packages/package-app", + dependencyName: "package-1", + version: "^1.0.0", + }); + await fixture.addDependencyToPackage({ + packagePath: "packages/package-app", + dependencyName: "package-2", + version: "~1.0.0", + }); + await fixture.addDependencyToPackage({ + packagePath: "packages/package-app", + dependencyName: "package-3", + version: "1.0.0", + }); + await fixture.addDependencyToPackage({ + packagePath: "packages/package-app", + dependencyName: "package-4", + version: "workspace:^1.0.0", + }); + await fixture.addDependencyToPackage({ + packagePath: "packages/package-4", + dependencyName: "package-4a", + version: "workspace:^1.0.0", + }); + await fixture.addDependencyToPackage({ + packagePath: "packages/package-4", + dependencyName: "package-4b", + version: "workspace:*", + }); + await fixture.addDependencyToPackage({ + packagePath: "packages/package-app", + dependencyName: "package-5", + version: "workspace:~1.0.0", + }); + await fixture.addDependencyToPackage({ + packagePath: "packages/package-app", + dependencyName: "package-6", + version: "workspace:1.0.0", + }); + await fixture.addDependencyToPackage({ + packagePath: "packages/package-app", + dependencyName: "package-7", + version: "workspace:*", + }); + await fixture.addDependencyToPackage({ + packagePath: "packages/package-app", + dependencyName: "package-8", + version: "workspace:^", + }); + await fixture.addDependencyToPackage({ + packagePath: "packages/package-app", + dependencyName: "package-9", + version: "workspace:~", + }); + }); + afterAll(() => fixture.destroy()); + + it("should run script on all child packages", async () => { + const output = await fixture.lerna("run print-name"); + + expect(output.combinedOutput).toMatchInlineSnapshot(` + + > Lerna (powered by Nx) Running target print-name for 12 project(s): + + - package-X + - package-X + - package-X + - package-X + - package-X + - package-X + - package-X + - package-X + - package-X + - package-X + - package-X + - package-app + + + +> package-X:print-name + + +> package-X@1.0.0 print-name /tmp/lerna-e2e/lerna-run-pnpm-XXXXXXXX/lerna-workspace/packages/package-X +> echo test-package-X + +test-package-X + +> package-X:print-name + + +> package-X@1.0.0 print-name /tmp/lerna-e2e/lerna-run-pnpm-XXXXXXXX/lerna-workspace/packages/package-X +> echo test-package-X + +test-package-X + +> package-X:print-name + + +> package-X@1.0.0 print-name /tmp/lerna-e2e/lerna-run-pnpm-XXXXXXXX/lerna-workspace/packages/package-X +> echo test-package-X + +test-package-X + +> package-X:print-name + + +> package-X@1.0.0 print-name /tmp/lerna-e2e/lerna-run-pnpm-XXXXXXXX/lerna-workspace/packages/package-X +> echo test-package-X + +test-package-X + +> package-X:print-name + + +> package-X@1.0.0 print-name /tmp/lerna-e2e/lerna-run-pnpm-XXXXXXXX/lerna-workspace/packages/package-X +> echo test-package-X + +test-package-X + +> package-X:print-name + + +> package-X@1.0.0 print-name /tmp/lerna-e2e/lerna-run-pnpm-XXXXXXXX/lerna-workspace/packages/package-X +> echo test-package-X + +test-package-X + +> package-X:print-name + + +> package-X@1.0.0 print-name /tmp/lerna-e2e/lerna-run-pnpm-XXXXXXXX/lerna-workspace/packages/package-X +> echo test-package-X + +test-package-X + +> package-X:print-name + + +> package-X@1.0.0 print-name /tmp/lerna-e2e/lerna-run-pnpm-XXXXXXXX/lerna-workspace/packages/package-X +> echo test-package-X + +test-package-X + +> package-X:print-name + + +> package-X@1.0.0 print-name /tmp/lerna-e2e/lerna-run-pnpm-XXXXXXXX/lerna-workspace/packages/package-X +> echo test-package-X + +test-package-X + +> package-X:print-name + + +> package-X@1.0.0 print-name /tmp/lerna-e2e/lerna-run-pnpm-XXXXXXXX/lerna-workspace/packages/package-X +> echo test-package-X + +test-package-X + +> package-X:print-name + + +> package-X@1.0.0 print-name /tmp/lerna-e2e/lerna-run-pnpm-XXXXXXXX/lerna-workspace/packages/package-X +> echo test-package-X + +test-package-X + +> package-app:print-name + + +> package-app@1.0.0 print-name /tmp/lerna-e2e/lerna-run-pnpm-XXXXXXXX/lerna-workspace/packages/package-app +> echo test-package-app + +test-package-app + + + + > Lerna (powered by Nx) Successfully ran target print-name for 12 projects + + +lerna notice cli v999.9.9-e2e.0 + +`); + }); +}); diff --git a/e2e/tests/lerna-version/positional-arguments-pnpm.spec.ts b/e2e/tests/lerna-version/positional-arguments-pnpm.spec.ts new file mode 100644 index 0000000000..e770176a5b --- /dev/null +++ b/e2e/tests/lerna-version/positional-arguments-pnpm.spec.ts @@ -0,0 +1,234 @@ +import { load } from "js-yaml"; +import { Fixture } from "../../utils/fixture"; +import { normalizeCommitSHAs, normalizeEnvironment } from "../../utils/snapshot-serializer-utils"; + +interface PnpmLockfile { + importers: { + dependencies?: Record; + specifiers?: Record; + devDependencies?: Record; + }; +} + +expect.addSnapshotSerializer({ + serialize(str: string) { + return normalizeCommitSHAs(normalizeEnvironment(str)); + }, + test(val: string) { + return val != null && typeof val === "string"; + }, +}); + +describe("lerna-version-positional-arguments-pnpm", () => { + let fixture: Fixture; + + beforeEach(async () => { + fixture = await Fixture.create({ + name: "lerna-version-positional-arguments", + packageManager: "pnpm", + initializeGit: true, + runLernaInit: true, + installDependencies: true, + }); + await fixture.lerna("create package-a -y"); + await fixture.lerna("create package-b -y --dependencies package-a"); + await fixture.createInitialGitCommit(); + await fixture.exec("git push origin test-main"); + }); + afterEach(() => fixture.destroy()); + + it("should support setting a specific version imperatively", async () => { + const output = await fixture.lerna("version 3.3.3 -y"); + 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 + - package-b: 0.0.0 => 3.3.3 + + lerna info auto-confirmed + lerna info execute Skipping releases + lerna info git Pushing tags... + lerna success version finished + + `); + + const checkTagIsPresentLocally = await fixture.exec("git describe --abbrev=0"); + expect(checkTagIsPresentLocally.combinedOutput).toMatchInlineSnapshot(` + v3.3.3 + + `); + + const checkTagIsPresentOnRemote = await fixture.exec("git ls-remote origin refs/tags/v3.3.3"); + expect(checkTagIsPresentOnRemote.combinedOutput).toMatchInlineSnapshot(` + {FULL_COMMIT_SHA} refs/tags/v3.3.3 + + `); + + const pnpmLockfileContent = await fixture.readWorkspaceFile("pnpm-lock.yaml"); + const pnpmLockfileObject = load(pnpmLockfileContent); + expect(pnpmLockfileObject.importers).toMatchInlineSnapshot(` + Object { + .: Object { + devDependencies: Object { + lerna: 999.9.9-e2e.0, + }, + specifiers: Object { + lerna: ^999.9.9-e2e.0, + }, + }, + packages/package-a: Object { + specifiers: Object {}, + }, + packages/package-b: Object { + dependencies: Object { + package-a: link:../package-a, + }, + specifiers: Object { + package-a: ^3.3.3, + }, + }, + } + `); + }); + + it("should support setting a specific version imperatively on packages using the workspace: protocol", async () => { + await fixture.updateJson("packages/package-b/package.json", (pkg) => ({ + ...pkg, + dependencies: { + ...(pkg.dependencies as any), + "package-a": "workspace:^0.0.0", + }, + })); + await fixture.exec("git add packages/package-b/package.json"); + await fixture.exec("git commit -m 'Add package-a dependency'"); + + const output = await fixture.lerna("version 3.3.3 -y"); + 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 + - package-b: 0.0.0 => 3.3.3 + + lerna info auto-confirmed + lerna info execute Skipping releases + lerna info git Pushing tags... + lerna success version finished + + `); + + const checkTagIsPresentLocally = await fixture.exec("git describe --abbrev=0"); + expect(checkTagIsPresentLocally.combinedOutput).toMatchInlineSnapshot(` + v3.3.3 + + `); + + const checkTagIsPresentOnRemote = await fixture.exec("git ls-remote origin refs/tags/v3.3.3"); + expect(checkTagIsPresentOnRemote.combinedOutput).toMatchInlineSnapshot(` + {FULL_COMMIT_SHA} refs/tags/v3.3.3 + + `); + + const pnpmLockfileContent = await fixture.readWorkspaceFile("pnpm-lock.yaml"); + const pnpmLockfileObject = load(pnpmLockfileContent) as any; + expect(pnpmLockfileObject.importers).toMatchInlineSnapshot(` + Object { + .: Object { + devDependencies: Object { + lerna: 999.9.9-e2e.0, + }, + specifiers: Object { + lerna: ^999.9.9-e2e.0, + }, + }, + packages/package-a: Object { + specifiers: Object {}, + }, + packages/package-b: Object { + dependencies: Object { + package-a: link:../package-a, + }, + specifiers: Object { + package-a: workspace:^3.3.3, + }, + }, + } + `); + }); + + it("should not run pnpm install lifecycle scripts, but still update pnpm-lock.yaml", async () => { + await fixture.updateJson("package.json", (json) => ({ + ...json, + scripts: { + preinstall: "exit 1", + install: "exit 1", + postinstall: "exit 1", + prepublish: "exit 1", + prepare: "exit 1", + }, + })); + await fixture.exec("git add package.json"); + await fixture.exec("git commit -m 'Add failing lifecycle scripts'"); + + const output = await fixture.lerna("version 3.3.3 -y"); + + 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 + - package-b: 0.0.0 => 3.3.3 + + lerna info auto-confirmed + lerna info execute Skipping releases + lerna info git Pushing tags... + lerna success version finished + + `); + + const checkTagIsPresentLocally = await fixture.exec("git describe --abbrev=0"); + expect(checkTagIsPresentLocally.combinedOutput).toMatchInlineSnapshot(` + v3.3.3 + + `); + + const checkTagIsPresentOnRemote = await fixture.exec("git ls-remote origin refs/tags/v3.3.3"); + expect(checkTagIsPresentOnRemote.combinedOutput).toMatchInlineSnapshot(` + {FULL_COMMIT_SHA} refs/tags/v3.3.3 + + `); + + const pnpmLockfileContent = await fixture.readWorkspaceFile("pnpm-lock.yaml"); + const pnpmLockfileObject = load(pnpmLockfileContent); + expect(pnpmLockfileObject.importers).toMatchInlineSnapshot(` + Object { + .: Object { + devDependencies: Object { + lerna: 999.9.9-e2e.0, + }, + specifiers: Object { + lerna: ^999.9.9-e2e.0, + }, + }, + packages/package-a: Object { + specifiers: Object {}, + }, + packages/package-b: Object { + dependencies: Object { + package-a: link:../package-a, + }, + specifiers: Object { + package-a: ^3.3.3, + }, + }, + } + `); + }); +}); diff --git a/e2e/utils/fixture.ts b/e2e/utils/fixture.ts index b89c81fba9..33137698cb 100644 --- a/e2e/utils/fixture.ts +++ b/e2e/utils/fixture.ts @@ -1,7 +1,8 @@ import { joinPathFragments, readJsonFile, writeJsonFile } from "@nrwl/devkit"; import { exec, spawn } from "child_process"; -import { ensureDir, ensureDirSync, readFile, remove, writeFile } from "fs-extra"; +import { ensureDir, ensureDirSync, existsSync, readFile, remove, writeFile } from "fs-extra"; import isCI from "is-ci"; +import { dump } from "js-yaml"; import { dirSync } from "tmp"; interface RunCommandOptions { @@ -11,7 +12,7 @@ interface RunCommandOptions { silent?: boolean; } -type PackageManager = "npm" | "yarn"; +type PackageManager = "npm" | "yarn" | "pnpm"; interface FixtureCreateOptions { name: string; @@ -76,13 +77,14 @@ export class Fixture { await fixture.createEmptyDirectoryForWorkspace(); } + await fixture.setNpmRegistry(); + if (runLernaInit) { - await fixture.lernaInit(); + const initOptions = packageManager === "pnpm" ? ({ keepDefaultOptions: true } as const) : {}; + await fixture.lernaInit("", initOptions); } - if (packageManager !== "npm") { - await fixture.overrideLernaConfig({ npmClient: packageManager }); - } + await fixture.initializeNpmEnvironment(); if (installDependencies) { await fixture.install(); @@ -91,6 +93,28 @@ export class Fixture { return fixture; } + private async setNpmRegistry(): Promise { + if (this.packageManager === "pnpm") { + await this.exec(`echo "registry=${REGISTRY}" > .npmrc`); + } + } + + private async initializeNpmEnvironment(): Promise { + if ( + this.packageManager !== "npm" && + existsSync(joinPathFragments(this.fixtureWorkspacePath, "lerna.json")) + ) { + await this.overrideLernaConfig({ npmClient: this.packageManager }); + } + + if (this.packageManager === "pnpm") { + const pnpmWorkspaceContent = dump({ + packages: ["packages/*", "!**/__test__/**"], + }); + await writeFile(this.getWorkspacePath("pnpm-workspace.yaml"), pnpmWorkspaceContent, "utf-8"); + } + } + /** * Tear down the entirety of the fixture including the git origin and the lerna workspace under test. */ @@ -130,12 +154,32 @@ export class Fixture { * Resolve the locally published version of lerna and run the `init` command, with an optionally * provided arguments. Reverts useNx and useWorkspaces to false when options.keepDefaultOptions is not provided, since those options are off for most users. */ - async lernaInit(args?: string, options?: { keepDefaultOptions: true }): Promise { - return this.exec( - `npx --registry=http://localhost:4872/ --yes lerna@${getPublishedVersion()} init ${args || ""}` - ).then((initResult) => - options?.keepDefaultOptions ? initResult : this.revertDefaultInitOptions().then(() => initResult) - ); + async lernaInit(args?: string, options?: { keepDefaultOptions?: true }): Promise { + let execCommandResult: Promise; + switch (this.packageManager) { + case "npm": + execCommandResult = this.exec( + `npx --registry=${REGISTRY} --yes lerna@${getPublishedVersion()} init ${args || ""}` + ); + break; + case "yarn": + execCommandResult = this.exec( + `npx --registry=${REGISTRY} --yes lerna@${getPublishedVersion()} init ${args || ""}` + ); + break; + case "pnpm": + execCommandResult = this.exec(`pnpm dlx lerna@${getPublishedVersion()} init ${args || ""}`); + break; + default: + throw new Error(`Unsupported package manager: ${this.packageManager}`); + } + + const initResult = await execCommandResult; + if (!options?.keepDefaultOptions) { + await this.revertDefaultInitOptions(); + } + + return initResult; } async overrideLernaConfig(lernaConfig: Record): Promise { @@ -168,6 +212,8 @@ export class Fixture { return this.exec(`npm --registry=${REGISTRY} install${args ? ` ${args}` : ""}`); case "yarn": return this.exec(`yarn --registry=${REGISTRY} install${args ? ` ${args}` : ""}`); + case "pnpm": + return this.exec(`pnpm install${args ? ` ${args}` : ""}`); default: throw new Error(`Unsupported package manager: ${this.packageManager}`); } @@ -182,8 +228,16 @@ export class Fixture { opts: { silenceError?: true; allowNetworkRequests?: true } = {} ): Promise { const offlineFlag = opts.allowNetworkRequests ? "" : "--offline "; - // Ensure we reference the locally installed copy of lerna - return this.exec(`npx ${offlineFlag}--no lerna ${args}`, { silenceError: opts.silenceError }); + switch (this.packageManager) { + case "npm": + return this.exec(`npx ${offlineFlag}--no lerna ${args}`, { silenceError: opts.silenceError }); + case "yarn": + return this.exec(`npx ${offlineFlag}--no lerna ${args}`, { silenceError: opts.silenceError }); + case "pnpm": + return this.exec(`pnpm exec lerna ${args}`, { silenceError: opts.silenceError }); + default: + throw new Error(`Unsupported package manager: ${this.packageManager}`); + } } async addNxToWorkspace(): Promise { diff --git a/package-lock.json b/package-lock.json index b84e7466ac..40849b0538 100644 --- a/package-lock.json +++ b/package-lock.json @@ -89,6 +89,7 @@ "@types/fs-extra": "^9.0.13", "@types/is-ci": "^2.0.0", "@types/jest": "^28.1.4", + "@types/js-yaml": "^4.0.5", "@types/tmp": "^0.2.3", "@typescript-eslint/parser": "^5.28.0", "all-contributors-cli": "^6.20.0", @@ -106,6 +107,7 @@ "jest": "^28.1.2", "jest-diff": "^28.1.1", "jest-matcher-utils": "^28.1.1", + "js-yaml": "^4.1.0", "node-jq": "^2.3.3", "normalize-newline": "^3.0.0", "normalize-path": "^3.0.0", @@ -322,6 +324,7 @@ "@lerna/command": "file:../../core/command", "@lerna/package-graph": "file:../../core/package-graph", "@lerna/symlink-dependencies": "file:../../utils/symlink-dependencies", + "@lerna/validation-error": "file:../../core/validation-error", "p-map": "^4.0.0", "slash": "^3.0.0" }, @@ -381,6 +384,21 @@ "node": "^14.15.0 || >=16.0.0" } }, + "commands/repair": { + "name": "@lerna/repair", + "version": "5.4.3", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@lerna/command": "file:../../core/command", + "lerna": "file:../../core/lerna", + "npmlog": "^6.0.2", + "nx": ">=14.5.8 < 16" + }, + "engines": { + "node": "^14.15.0 || >=16.0.0" + } + }, "commands/run": { "name": "@lerna/run", "version": "5.4.3", @@ -547,7 +565,8 @@ "@lerna/version": "file:../../commands/version", "import-local": "^3.0.2", "npmlog": "^6.0.2", - "nx": ">=14.5.4 < 16" + "nx": ">=14.5.4 < 16", + "typescript": "^3 || ^4" }, "bin": { "lerna": "cli.js" @@ -607,6 +626,7 @@ "dot-prop": "^6.0.1", "glob-parent": "^5.1.1", "globby": "^11.0.2", + "js-yaml": "^4.1.0", "load-json-file": "^6.2.0", "npmlog": "^6.0.2", "p-map": "^4.0.0", @@ -1231,24 +1251,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@eslint/eslintrc/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1330,6 +1332,15 @@ "node": ">=8" } }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -1343,6 +1354,19 @@ "node": ">=8" } }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -3504,6 +3528,12 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "node_modules/@types/js-yaml": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz", + "integrity": "sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -4289,13 +4319,9 @@ "dev": true }, "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/array-differ": { "version": "3.0.0", @@ -7505,12 +7531,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/eslint/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -7562,18 +7582,6 @@ "node": ">=10.13.0" } }, - "node_modules/eslint/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -11573,13 +11581,11 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" @@ -13466,11 +13472,6 @@ } } }, - "node_modules/nx/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, "node_modules/nx/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -13500,17 +13501,6 @@ "node": "*" } }, - "node_modules/nx/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/nx/node_modules/tslib": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", @@ -15505,7 +15495,7 @@ "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, "node_modules/sshpk": { @@ -16554,7 +16544,6 @@ "version": "4.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.3.tgz", "integrity": "sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==", - "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16890,12 +16879,6 @@ "url": "https://opencollective.com/verdaccio" } }, - "node_modules/verdaccio/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "node_modules/verdaccio/node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -16905,18 +16888,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/verdaccio/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/verdaccio/node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -18430,21 +18401,6 @@ "strip-json-comments": "^3.1.1" }, "dependencies": { - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -18516,6 +18472,15 @@ "resolve-from": "^5.0.0" }, "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, "find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -18526,6 +18491,16 @@ "path-exists": "^4.0.0" } }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -19666,6 +19641,7 @@ "@lerna/command": "file:../../core/command", "@lerna/package-graph": "file:../../core/package-graph", "@lerna/symlink-dependencies": "file:../../utils/symlink-dependencies", + "@lerna/validation-error": "file:../../core/validation-error", "p-map": "^4.0.0", "slash": "^3.0.0" } @@ -19818,6 +19794,7 @@ "dot-prop": "^6.0.1", "glob-parent": "^5.1.1", "globby": "^11.0.2", + "js-yaml": "^4.1.0", "load-json-file": "^6.2.0", "npmlog": "^6.0.2", "p-map": "^4.0.0", @@ -20747,6 +20724,12 @@ } } }, + "@types/js-yaml": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz", + "integrity": "sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==", + "dev": true + }, "@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -21320,13 +21303,9 @@ "dev": true }, "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "array-differ": { "version": "3.0.0", @@ -23577,12 +23556,6 @@ "v8-compile-cache": "^2.0.3" }, "dependencies": { - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -23615,15 +23588,6 @@ "is-glob": "^4.0.3" } }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -26993,13 +26957,11 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" } }, "jsbn": { @@ -27277,7 +27239,8 @@ "@lerna/version": "file:../../commands/version", "import-local": "^3.0.2", "npmlog": "^6.0.2", - "nx": ">=14.5.4 < 16" + "nx": ">=14.5.4 < 16", + "typescript": "^3 || ^4" } }, "leven": { @@ -28487,11 +28450,6 @@ "yargs-parser": "21.0.1" }, "dependencies": { - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, "fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -28515,14 +28473,6 @@ "path-is-absolute": "^1.0.0" } }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "requires": { - "argparse": "^2.0.1" - } - }, "tslib": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", @@ -30022,7 +29972,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, "sshpk": { @@ -30802,8 +30752,7 @@ "typescript": { "version": "4.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.3.tgz", - "integrity": "sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==", - "dev": true + "integrity": "sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==" }, "uglify-js": { "version": "3.12.7", @@ -31038,12 +30987,6 @@ "verdaccio-htpasswd": "10.5.0" }, "dependencies": { - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -31053,15 +30996,6 @@ "balanced-match": "^1.0.0" } }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, "kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", diff --git a/package.json b/package.json index 2d248d9147..9903420643 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "@types/fs-extra": "^9.0.13", "@types/is-ci": "^2.0.0", "@types/jest": "^28.1.4", + "@types/js-yaml": "^4.0.5", "@types/tmp": "^0.2.3", "@typescript-eslint/parser": "^5.28.0", "all-contributors-cli": "^6.20.0", @@ -134,6 +135,7 @@ "jest": "^28.1.2", "jest-diff": "^28.1.1", "jest-matcher-utils": "^28.1.1", + "js-yaml": "^4.1.0", "node-jq": "^2.3.3", "normalize-newline": "^3.0.0", "normalize-path": "^3.0.0", diff --git a/website/docs/recipes/using-pnpm-with-lerna.md b/website/docs/recipes/using-pnpm-with-lerna.md new file mode 100644 index 0000000000..91a792c416 --- /dev/null +++ b/website/docs/recipes/using-pnpm-with-lerna.md @@ -0,0 +1,46 @@ +# Using pnpm with Lerna + +Lerna can be used in a [`pnpm` workspace](https://pnpm.io/workspaces) to get the full benefits of both [`pnpm`](https://pnpm.io) and Lerna. + +When used in a `pnpm` workspace, Lerna will: + +- resolve package locations with `pnpm-workspace.yaml` (https://pnpm.io/workspaces) +- enforce `useWorkspaces: true` in `lerna.json` (and ignore `packages:` in `package.json`). +- block usage of `bootstrap`, `link`, and `add` commands. Instead, you should use `pnpm` commands directly to manage dependencies (https://pnpm.io/cli/install). +- respect the [workspace protocol](https://pnpm.io/workspaces#workspace-protocol-workspace) for package dependencies. + - During `lerna version`, dependencies will be updated as normal, but will preserve the `workspace:` prefix if it exists. + - If a [workspace alias](https://pnpm.io/workspaces#referencing-workspace-packages-through-aliases) is used, then `lerna version` will not bump the version of the dependency, since aliases don't specify a version number to bump. + +## Getting Started + +To set up pnpm with Lerna: + +1. If not installed already, install `pnpm`: https://pnpm.io/installation. +2. Remove the `node_modules/` folder in the root, if it exists. If not already using workspaces, run `lerna clean` to remove the `node_modules/` folder in all packages. +3. Set `"npmClient": "pnpm"` and `"useWorkspaces": true` in `lerna.json`. +4. Create a `pnpm-workspace.yaml` file in the root of your project. + If you are already using npm or yarn workspaces, move the "workspaces" property from `package.json` to `pnpm-workspace.yaml`. If you were not already using workspaces, move the "packages" property from `lerna.json` to `pnpm-workspace.yaml`. For example: + + ```json title="package.json" + { + "workspaces": ["packages/*"] + } + ``` + + and + + ```json title="lerna.json" + { + "packages": ["packages/*"] + } + ``` + + become: + + ```yaml title="pnpm-workspace.yaml" + packages: + - "packages/*" + ``` + +5. (optional) Run `pnpm import` to generate a `pnpm-lock.yaml` file from an existing lockfile. See https://pnpm.io/cli/import for supported lockfile sources. +6. Run `pnpm install`. diff --git a/website/sidebars.js b/website/sidebars.js index 2c9826915f..154032e7e6 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -58,6 +58,11 @@ const sidebars = { keywords: ["caching", "dte", "versioning", "publishing"], }, }, + { + type: "category", + label: "Recipes", + items: ["recipes/using-pnpm-with-lerna"], + }, { type: "category", label: "API Reference",