From 0333df7656d9b7279e04cb35a72e328570290b6c Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Thu, 4 Aug 2022 14:08:46 -0400 Subject: [PATCH 01/28] feat(bootstrap): prevent bootstrapping when using pnpm --- .../bootstrap/__tests__/__fixtures__/pnpm/lerna.json | 7 +++++++ .../__tests__/__fixtures__/pnpm/package.json | 10 ++++++++++ .../bootstrap/__tests__/bootstrap-command.test.js | 11 +++++++++++ commands/bootstrap/index.js | 7 +++++++ core/lerna/schemas/lerna-schema.json | 4 ++-- 5 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 commands/bootstrap/__tests__/__fixtures__/pnpm/lerna.json create mode 100644 commands/bootstrap/__tests__/__fixtures__/pnpm/package.json 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__/bootstrap-command.test.js b/commands/bootstrap/__tests__/bootstrap-command.test.js index 20ace78c0c..c8016c5dae 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( + "Bootstraping 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..1c75b99ffa 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", + "Bootstraping 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/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", From c4cdcdc365f04183cca6f6f33f9d072e1ae1468a Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Fri, 5 Aug 2022 00:27:30 -0400 Subject: [PATCH 02/28] chore(run): add tests for pnpm repo --- .../__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 +++++++++++ commands/run/__tests__/run-command.test.js | 19 +++++++++++++++++++ 5 files changed, 55 insertions(+) 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 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__/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", () => { From ded2d63c318751dfa75fd4a30165e474f6762597 Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Fri, 5 Aug 2022 12:21:19 -0400 Subject: [PATCH 03/28] feat: enable parsing of workspace: dependency specs --- .../__tests__/package-graph.test.js | 242 +++++++++++++++++- core/package-graph/index.js | 38 ++- core/package/index.js | 9 +- 3 files changed, 283 insertions(+), 6 deletions(-) diff --git a/core/package-graph/__tests__/package-graph.test.js b/core/package-graph/__tests__/package-graph.test.js index 7d716d875f..6fa3641947 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("localizes sibling 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("localizes sibling 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("localizes sibling 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("localizes sibling 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..604449e679 100644 --- a/core/package/index.js +++ b/core/package/index.js @@ -270,7 +270,14 @@ class Package { depCollection = this.devDependencies; } - if (resolved.registry || resolved.type === "directory") { + if (resolved.workspaceSpec) { + if (resolved.workspaceAlias) { + // do nothing, since aliases don't directly specify a version + } else { + 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) { From 963de358c5c9378e5230ace1f7f2cbc6024535c3 Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Sun, 7 Aug 2022 16:50:08 -0400 Subject: [PATCH 04/28] feat(version): update pnpm lockfile --- commands/version/index.js | 11 +++++++++++ commands/version/lib/update-lockfile-version.js | 7 ++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/commands/version/index.js b/commands/version/index.js index ce968ccfe4..9d99ae4dcb 100644 --- a/commands/version/index.js +++ b/commands/version/index.js @@ -9,6 +9,7 @@ const pPipe = require("p-pipe"); const pReduce = require("p-reduce"); const pWaterfall = require("p-waterfall"); const semver = require("semver"); +const path = require("path"); const { Command } = require("@lerna/command"); const { recommendVersion, updateChangelog } = require("@lerna/conventional-commits"); @@ -20,6 +21,7 @@ const { createRunner } = require("@lerna/run-lifecycle"); const { runTopologically } = require("@lerna/run-topologically"); const { ValidationError } = require("@lerna/validation-error"); const { prereleaseIdFromVersion } = require("@lerna/prerelease-id-from-version"); +const childProcess = require("@lerna/child-process"); const { getCurrentBranch } = require("./lib/get-current-branch"); const { gitAdd } = require("./lib/git-add"); @@ -582,6 +584,15 @@ class VersionCommand extends Command { if (!independentVersions) { this.project.version = this.globalVersion; + if (this.options.npmClient === "pnpm") { + chain = chain.then(() => + childProcess.exec("pnpm", ["install", "--lockfile-only"], this.execOpts).then(() => { + const lockfilePath = path.join(this.project.rootPath, "pnpm-lock.yaml"); + changedFiles.add(lockfilePath); + }) + ); + } + if (conventionalCommits && changelog) { chain = chain.then(() => updateChangelog(this.project.manifest, "root", { diff --git a/commands/version/lib/update-lockfile-version.js b/commands/version/lib/update-lockfile-version.js index 06f131b870..cde633f8b7 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(`${pkg.name} has no lockfile. Skipping lockfile update.`); + }) + ); chain = chain.then((obj) => { if (obj) { obj.version = pkg.version; From f932e635e75e7f2222f51eff2333d9550c345a2d Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Mon, 8 Aug 2022 13:58:30 -0400 Subject: [PATCH 05/28] chore(e2e): add lerna version test for pnpm --- .../positional-arguments-pnpm.spec.ts | 161 +++++++++++++ e2e/utils/fixture.ts | 27 ++- package-lock.json | 227 ++++++------------ package.json | 2 + 4 files changed, 256 insertions(+), 161 deletions(-) create mode 100644 e2e/tests/lerna-version/positional-arguments-pnpm.spec.ts 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..693375421e --- /dev/null +++ b/e2e/tests/lerna-version/positional-arguments-pnpm.spec.ts @@ -0,0 +1,161 @@ +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", + }, + })); + + 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, + }, + }, + } + `); + }); +}); diff --git a/e2e/utils/fixture.ts b/e2e/utils/fixture.ts index b89c81fba9..a384ac4e66 100644 --- a/e2e/utils/fixture.ts +++ b/e2e/utils/fixture.ts @@ -2,6 +2,7 @@ import { joinPathFragments, readJsonFile, writeJsonFile } from "@nrwl/devkit"; import { exec, spawn } from "child_process"; import { ensureDir, ensureDirSync, 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; @@ -80,15 +81,29 @@ export class Fixture { await fixture.lernaInit(); } + await fixture.initializeNpmEnvironment(packageManager, installDependencies); + + return fixture; + } + + private async initializeNpmEnvironment( + packageManager: PackageManager, + installDependencies: boolean + ): Promise { if (packageManager !== "npm") { - await fixture.overrideLernaConfig({ npmClient: packageManager }); + await this.overrideLernaConfig({ npmClient: packageManager }); } - if (installDependencies) { - await fixture.install(); + if (packageManager === "pnpm") { + const pnpmWorkspaceContent = dump({ + packages: ["packages/*", "!**/__test__/**"], + }); + writeFile(this.getWorkspacePath("pnpm-workspace.yaml"), pnpmWorkspaceContent, "utf-8"); } - return fixture; + if (installDependencies) { + await this.install(); + } } /** @@ -168,6 +183,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 --registry=${REGISTRY} install${args ? ` ${args}` : ""}`); default: throw new Error(`Unsupported package manager: ${this.packageManager}`); } diff --git a/package-lock.json b/package-lock.json index a33113bd67..0d3053f0a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -90,6 +90,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", @@ -107,6 +108,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", @@ -1232,24 +1234,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", @@ -1331,6 +1315,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", @@ -1344,6 +1337,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", @@ -3505,6 +3511,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", @@ -4290,13 +4302,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", @@ -7506,12 +7514,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", @@ -7563,18 +7565,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", @@ -11574,13 +11564,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 +13454,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 +13483,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", @@ -15504,7 +15476,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": { @@ -16888,12 +16860,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", @@ -16903,18 +16869,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", @@ -18426,21 +18380,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", @@ -18512,6 +18451,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", @@ -18522,6 +18470,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", @@ -20745,6 +20703,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", @@ -21318,13 +21282,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", @@ -23575,12 +23535,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", @@ -23613,15 +23567,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", @@ -26991,13 +26936,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": { @@ -28484,11 +28427,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", @@ -28512,14 +28450,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", @@ -30018,7 +29948,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": { @@ -31033,12 +30963,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", @@ -31048,15 +30972,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 de31d681d5..867f4884ff 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,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", @@ -135,6 +136,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", From bf285b329192a7f2d4b26086252e69a505ee325e Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Mon, 8 Aug 2022 14:53:36 -0400 Subject: [PATCH 06/28] feat(link): throw error when using link with pnpm workspace --- .../__tests__/bootstrap-command.test.js | 2 +- commands/bootstrap/index.js | 2 +- .../__tests__/__fixtures__/pnpm/lerna.json | 7 + .../__tests__/__fixtures__/pnpm/package.json | 10 + commands/link/__tests__/link-command.test.js | 325 +++++++++--------- commands/link/index.js | 8 + commands/link/package.json | 1 + 7 files changed, 196 insertions(+), 159 deletions(-) create mode 100644 commands/link/__tests__/__fixtures__/pnpm/lerna.json create mode 100644 commands/link/__tests__/__fixtures__/pnpm/package.json diff --git a/commands/bootstrap/__tests__/bootstrap-command.test.js b/commands/bootstrap/__tests__/bootstrap-command.test.js index c8016c5dae..419bdbb7ae 100644 --- a/commands/bootstrap/__tests__/bootstrap-command.test.js +++ b/commands/bootstrap/__tests__/bootstrap-command.test.js @@ -144,7 +144,7 @@ describe("BootstrapCommand", () => { const command = lernaBootstrap(testDir)(); await expect(command).rejects.toThrow( - "Bootstraping with pnpm is not supported. Use pnpm directly to manage dependencies (https://pnpm.io/cli/install)." + "Bootstrapping with pnpm is not supported. Use pnpm directly to manage dependencies (https://pnpm.io/cli/install)." ); }); }); diff --git a/commands/bootstrap/index.js b/commands/bootstrap/index.js index 1c75b99ffa..7438e21457 100644 --- a/commands/bootstrap/index.js +++ b/commands/bootstrap/index.js @@ -41,7 +41,7 @@ class BootstrapCommand extends Command { if (npmClient === "pnpm") { throw new ValidationError( "EWORKSPACES", - "Bootstraping with pnpm is not supported. Use pnpm directly to manage dependencies (https://pnpm.io/cli/install)." + "Bootstrapping with pnpm is not supported. Use pnpm directly to manage dependencies (https://pnpm.io/cli/install)." ); } 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__/link-command.test.js b/commands/link/__tests__/link-command.test.js index b8b1311610..419ad9aacc 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..75b351f37f 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 ca3571dac7..1c12656d4e 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" } From 293c1b79f1755cf63a8fad6d349bdd2307235fb8 Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Tue, 9 Aug 2022 15:05:56 -0400 Subject: [PATCH 07/28] chore(e2e): update yarn and npm tests --- e2e/tests/lerna-publish/lerna-publish-npm.spec.ts | 11 +++++------ .../lerna-publish/lerna-publish-yarn.spec.ts | 11 +++++------ e2e/utils/fixture.ts | 15 ++++++--------- 3 files changed, 16 insertions(+), 21 deletions(-) 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-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/utils/fixture.ts b/e2e/utils/fixture.ts index a384ac4e66..2c899a61cb 100644 --- a/e2e/utils/fixture.ts +++ b/e2e/utils/fixture.ts @@ -81,15 +81,16 @@ export class Fixture { await fixture.lernaInit(); } - await fixture.initializeNpmEnvironment(packageManager, installDependencies); + await fixture.initializeNpmEnvironment(packageManager); + + if (installDependencies) { + await fixture.install(); + } return fixture; } - private async initializeNpmEnvironment( - packageManager: PackageManager, - installDependencies: boolean - ): Promise { + private async initializeNpmEnvironment(packageManager: PackageManager): Promise { if (packageManager !== "npm") { await this.overrideLernaConfig({ npmClient: packageManager }); } @@ -100,10 +101,6 @@ export class Fixture { }); writeFile(this.getWorkspacePath("pnpm-workspace.yaml"), pnpmWorkspaceContent, "utf-8"); } - - if (installDependencies) { - await this.install(); - } } /** From 8c7c989d4cf07a590f5de322028ab227a65bcda0 Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Wed, 10 Aug 2022 10:37:52 -0400 Subject: [PATCH 08/28] chore: minor PR style suggestions --- commands/version/lib/update-lockfile-version.js | 2 +- core/package/index.js | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/commands/version/lib/update-lockfile-version.js b/commands/version/lib/update-lockfile-version.js index cde633f8b7..417750787a 100644 --- a/commands/version/lib/update-lockfile-version.js +++ b/commands/version/lib/update-lockfile-version.js @@ -14,7 +14,7 @@ function updateLockfileVersion(pkg) { chain = chain.then(() => loadJsonFile(lockfilePath).catch(() => { - log.verbose(`${pkg.name} has no lockfile. Skipping lockfile update.`); + log.verbose("version", `${pkg.name} has no lockfile. Skipping lockfile update.`); }) ); chain = chain.then((obj) => { diff --git a/core/package/index.js b/core/package/index.js index 604449e679..a9609436fc 100644 --- a/core/package/index.js +++ b/core/package/index.js @@ -271,9 +271,8 @@ class Package { } if (resolved.workspaceSpec) { - if (resolved.workspaceAlias) { - // do nothing, since aliases don't directly specify a version - } else { + // 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}`; } From f1e01179bc353a713b84510bd3b6faefaa48ee7b Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Wed, 10 Aug 2022 15:41:19 -0400 Subject: [PATCH 09/28] chore(e2e): add support for pnpm package manager --- .../lerna-publish/lerna-publish-pnpm.spec.ts | 93 +++++++++++++++++++ e2e/utils/fixture.ts | 57 +++++++++--- 2 files changed, 138 insertions(+), 12 deletions(-) create mode 100644 e2e/tests/lerna-publish/lerna-publish-pnpm.spec.ts 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/utils/fixture.ts b/e2e/utils/fixture.ts index 2c899a61cb..bc8bfd5dde 100644 --- a/e2e/utils/fixture.ts +++ b/e2e/utils/fixture.ts @@ -1,11 +1,11 @@ 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 { +export interface RunCommandOptions { silenceError?: boolean; env?: Record; cwd?: string; @@ -23,7 +23,7 @@ interface FixtureCreateOptions { forceDeterministicTerminalOutput?: boolean; } -type RunCommandResult = { stdout: string; stderr: string; combinedOutput: string }; +export type RunCommandResult = { stdout: string; stderr: string; combinedOutput: string }; const ORIGIN_GIT = "origin.git"; const REGISTRY = "http://localhost:4872/"; @@ -77,8 +77,11 @@ export class Fixture { await fixture.createEmptyDirectoryForWorkspace(); } + await fixture.setNpmRegistry(packageManager); + if (runLernaInit) { - await fixture.lernaInit(); + const initOptions = packageManager === "pnpm" ? ({ keepDefaultOptions: true } as const) : {}; + await fixture.lernaInit("", initOptions); } await fixture.initializeNpmEnvironment(packageManager); @@ -90,8 +93,14 @@ export class Fixture { return fixture; } + private async setNpmRegistry(packageManager: PackageManager): Promise { + if (packageManager === "pnpm") { + await this.exec(`echo "registry=${REGISTRY}" > .npmrc`); + } + } + private async initializeNpmEnvironment(packageManager: PackageManager): Promise { - if (packageManager !== "npm") { + if (packageManager !== "npm" && existsSync(joinPathFragments(this.fixtureWorkspacePath, "lerna.json"))) { await this.overrideLernaConfig({ npmClient: packageManager }); } @@ -99,7 +108,7 @@ export class Fixture { const pnpmWorkspaceContent = dump({ packages: ["packages/*", "!**/__test__/**"], }); - writeFile(this.getWorkspacePath("pnpm-workspace.yaml"), pnpmWorkspaceContent, "utf-8"); + await writeFile(this.getWorkspacePath("pnpm-workspace.yaml"), pnpmWorkspaceContent, "utf-8"); } } @@ -142,10 +151,26 @@ 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) => + 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}`); + } + return execCommandResult.then((initResult) => options?.keepDefaultOptions ? initResult : this.revertDefaultInitOptions().then(() => initResult) ); } @@ -196,8 +221,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 { From e5601b2d19d0d63439562d8af10adc6c9420c449 Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Wed, 10 Aug 2022 16:04:19 -0400 Subject: [PATCH 10/28] feat: prevent usage of pnpm without workspaces --- core/command/__tests__/command.test.js | 19 +++++++++++++++++++ core/command/index.js | 7 +++++++ 2 files changed, 26 insertions(+) diff --git a/core/command/__tests__/command.test.js b/core/command/__tests__/command.test.js index 65310780d0..367605b94e 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("basic"); + + const lernaConfigPath = path.join(cwd, "lerna.json"); + const lernaConfig = await fs.readJson(lernaConfigPath); + await fs.writeJson(lernaConfigPath, { + ...lernaConfig, + npmClient: "pnpm", + }); + + 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() { From 1a02f268fc681a5d872fe9a0c11e0809ab14d7f3 Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Wed, 10 Aug 2022 16:28:57 -0400 Subject: [PATCH 11/28] feat(add): prevent usage with pnpm workspaces --- commands/add/__tests__/__fixtures__/pnpm/lerna.json | 7 +++++++ commands/add/__tests__/__fixtures__/pnpm/package.json | 10 ++++++++++ .../__fixtures__/pnpm/packages/package-1/package.json | 8 ++++++++ .../__fixtures__/pnpm/packages/package-2/package.json | 11 +++++++++++ commands/add/__tests__/add-command.test.js | 9 +++++++++ commands/add/index.js | 7 +++++++ core/command/__fixtures__/pnpm/lerna.json | 7 +++++++ core/command/__fixtures__/pnpm/package.json | 10 ++++++++++ .../__fixtures__/pnpm/packages/package-1/package.json | 8 ++++++++ .../__fixtures__/pnpm/packages/package-2/package.json | 11 +++++++++++ core/command/__tests__/command.test.js | 4 ++-- 11 files changed, 90 insertions(+), 2 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 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 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__/add-command.test.js b/commands/add/__tests__/add-command.test.js index 46fed96e3d..ea609ebeca 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( + "The 'add' command 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..c901114dfa 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", + "The 'add' command 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/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 367605b94e..91abb62446 100644 --- a/core/command/__tests__/command.test.js +++ b/core/command/__tests__/command.test.js @@ -389,13 +389,13 @@ describe("core-command", () => { }); it("throws ENOWORKSPACES when npm client is pnpm and useWorkspaces is not true", async () => { - const cwd = await initFixture("basic"); + const cwd = await initFixture("pnpm"); const lernaConfigPath = path.join(cwd, "lerna.json"); const lernaConfig = await fs.readJson(lernaConfigPath); await fs.writeJson(lernaConfigPath, { ...lernaConfig, - npmClient: "pnpm", + useWorkspaces: false, }); await expect(testFactory({ cwd })).rejects.toThrow( From 18ee9e87e9a8605fe6edcd7da8f9062bb3388016 Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Thu, 11 Aug 2022 15:46:43 -0400 Subject: [PATCH 12/28] feat: read packages config from pnpm-workspace.yaml --- 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 + 6 files changed, 126 insertions(+) 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 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 70aee7790b..05d4c1326d 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", From 4a973d8b565faef8b68fc4ff9fb74707a26654a3 Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Thu, 11 Aug 2022 16:55:15 -0400 Subject: [PATCH 13/28] chore: update unit test fixtures to have pnpm-workspace.yaml --- commands/add/__tests__/__fixtures__/pnpm/pnpm-workspace.yaml | 2 ++ .../bootstrap/__tests__/__fixtures__/pnpm/pnpm-workspace.yaml | 2 ++ commands/link/__tests__/__fixtures__/pnpm/pnpm-workspace.yaml | 2 ++ commands/run/__tests__/__fixtures__/pnpm/pnpm-workspace.yaml | 2 ++ 4 files changed, 8 insertions(+) create mode 100644 commands/add/__tests__/__fixtures__/pnpm/pnpm-workspace.yaml create mode 100644 commands/bootstrap/__tests__/__fixtures__/pnpm/pnpm-workspace.yaml create mode 100644 commands/link/__tests__/__fixtures__/pnpm/pnpm-workspace.yaml create mode 100644 commands/run/__tests__/__fixtures__/pnpm/pnpm-workspace.yaml 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/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/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/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/*" From 6809a67a96464f15c6d5e512ea15e04845cf2858 Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Thu, 11 Aug 2022 17:08:32 -0400 Subject: [PATCH 14/28] chore(e2e): Update github action to include installing pnpm globally --- .github/actions/install-node-and-dependencies/action.yml | 4 ++++ 1 file changed, 4 insertions(+) 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 From 5544dfc2ebcaf5f42d92679e0a64b0e155631858 Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Thu, 11 Aug 2022 17:41:54 -0400 Subject: [PATCH 15/28] chore(e2e): commit changes for pnpm test --- e2e/tests/lerna-version/positional-arguments-pnpm.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/e2e/tests/lerna-version/positional-arguments-pnpm.spec.ts b/e2e/tests/lerna-version/positional-arguments-pnpm.spec.ts index 693375421e..1007034d7b 100644 --- a/e2e/tests/lerna-version/positional-arguments-pnpm.spec.ts +++ b/e2e/tests/lerna-version/positional-arguments-pnpm.spec.ts @@ -102,6 +102,8 @@ describe("lerna-version-positional-arguments-pnpm", () => { "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(` From 22d822aa9a99cfc17336476a3df76d702e2f811a Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Thu, 11 Aug 2022 18:23:36 -0400 Subject: [PATCH 16/28] chore(docs): add doc for using pnpm with lerna --- doc/using-pnpm-with-lerna.md | 48 ++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 doc/using-pnpm-with-lerna.md diff --git a/doc/using-pnpm-with-lerna.md b/doc/using-pnpm-with-lerna.md new file mode 100644 index 0000000000..1b2823ede7 --- /dev/null +++ b/doc/using-pnpm-with-lerna.md @@ -0,0 +1,48 @@ +# Using [`pnpm`](https://pnpm.io) 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` space, 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 + // package.json + { + "workspaces": ["packages/*"] + } + ``` + + and + + ```json + // lerna.json + { + "packages": ["packages/*"] + } + ``` + + become: + + ```yaml + # pnpm-workspace.yaml + packages: + - "packages/*" + ``` + +5. Run `pnpm install`. From 05aca022d6b520f7a37cc56495d4d59d2ffeccbb Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Thu, 11 Aug 2022 18:45:30 -0400 Subject: [PATCH 17/28] chore(e2e): remove useless registry argument from fixture --- e2e/utils/fixture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/utils/fixture.ts b/e2e/utils/fixture.ts index bc8bfd5dde..a949770fed 100644 --- a/e2e/utils/fixture.ts +++ b/e2e/utils/fixture.ts @@ -206,7 +206,7 @@ export class Fixture { case "yarn": return this.exec(`yarn --registry=${REGISTRY} install${args ? ` ${args}` : ""}`); case "pnpm": - return this.exec(`pnpm --registry=${REGISTRY} install${args ? ` ${args}` : ""}`); + return this.exec(`pnpm install${args ? ` ${args}` : ""}`); default: throw new Error(`Unsupported package manager: ${this.packageManager}`); } From 5ca1b4297c8c5e4da060d26ffe2adf2609b5b8cc Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Fri, 12 Aug 2022 12:48:28 -0400 Subject: [PATCH 18/28] chore(docs): add recipes section with pnpm guide --- .../docs/recipes}/using-pnpm-with-lerna.md | 16 +++++++--------- website/sidebars.js | 5 +++++ 2 files changed, 12 insertions(+), 9 deletions(-) rename {doc => website/docs/recipes}/using-pnpm-with-lerna.md (83%) diff --git a/doc/using-pnpm-with-lerna.md b/website/docs/recipes/using-pnpm-with-lerna.md similarity index 83% rename from doc/using-pnpm-with-lerna.md rename to website/docs/recipes/using-pnpm-with-lerna.md index 1b2823ede7..91a792c416 100644 --- a/doc/using-pnpm-with-lerna.md +++ b/website/docs/recipes/using-pnpm-with-lerna.md @@ -1,8 +1,8 @@ -# Using [`pnpm`](https://pnpm.io) with Lerna +# 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` space, Lerna will: +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`). @@ -21,8 +21,7 @@ To set up pnpm with Lerna: 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 - // package.json + ```json title="package.json" { "workspaces": ["packages/*"] } @@ -30,8 +29,7 @@ To set up pnpm with Lerna: and - ```json - // lerna.json + ```json title="lerna.json" { "packages": ["packages/*"] } @@ -39,10 +37,10 @@ To set up pnpm with Lerna: become: - ```yaml - # pnpm-workspace.yaml + ```yaml title="pnpm-workspace.yaml" packages: - "packages/*" ``` -5. Run `pnpm install`. +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", From d89992b65657c4c9ecd2eb25f8db0f7ff9849260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CJamesHenry=E2=80=9D?= Date: Sun, 14 Aug 2022 18:14:15 +0400 Subject: [PATCH 19/28] chore: fix merge conflict resolution typo --- commands/version/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/version/index.js b/commands/version/index.js index 85d00e40e4..6406ae00e8 100644 --- a/commands/version/index.js +++ b/commands/version/index.js @@ -585,7 +585,7 @@ class VersionCommand extends Command { if (!independentVersions) { this.project.version = this.globalVersion; - if (this.options.npmClient === "su") { + if (this.options.npmClient === "pnpm") { chain = chain.then(() => childProcess.exec("pnpm", ["install", "--lockfile-only"], this.execOpts).then(() => { const lockfilePath = path.join(this.project.rootPath, "pnpm-lock.yaml"); From f80edbc919420913706bb3184f5321cec94dce80 Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Mon, 15 Aug 2022 11:36:47 -0400 Subject: [PATCH 20/28] chore: error message format, async await in fixture, remove extra argument --- commands/add/__tests__/add-command.test.js | 2 +- commands/add/index.js | 2 +- .../__tests__/bootstrap-command.test.js | 2 +- commands/bootstrap/index.js | 2 +- commands/link/__tests__/link-command.test.js | 2 +- commands/link/index.js | 2 +- e2e/utils/fixture.ts | 29 ++++++++++++------- package-lock.json | 4 +++ 8 files changed, 28 insertions(+), 17 deletions(-) diff --git a/commands/add/__tests__/add-command.test.js b/commands/add/__tests__/add-command.test.js index ea609ebeca..1675a4b491 100644 --- a/commands/add/__tests__/add-command.test.js +++ b/commands/add/__tests__/add-command.test.js @@ -49,7 +49,7 @@ describe("AddCommand", () => { const command = lernaAdd(testDir)("@test/package-1"); await expect(command).rejects.toThrow( - "The 'add' command is not supported when using `pnpm` workspaces. Use `pnpm` directly to add dependencies to packages: https://pnpm.io/cli/add." + "The 'add' command is not supported when using `pnpm` workspaces. Use `pnpm` directly to add dependencies to packages: https://pnpm.io/cli/add" ); }); diff --git a/commands/add/index.js b/commands/add/index.js index c901114dfa..34374bda7a 100644 --- a/commands/add/index.js +++ b/commands/add/index.js @@ -39,7 +39,7 @@ class AddCommand extends Command { if (this.options.npmClient === "pnpm") { throw new ValidationError( "EPNPMNOTSUPPORTED", - "The 'add' command is not supported when using `pnpm` workspaces. Use `pnpm` directly to add dependencies to packages: https://pnpm.io/cli/add." + "The 'add' command is not supported when using `pnpm` workspaces. Use `pnpm` directly to add dependencies to packages: https://pnpm.io/cli/add" ); } diff --git a/commands/bootstrap/__tests__/bootstrap-command.test.js b/commands/bootstrap/__tests__/bootstrap-command.test.js index 419bdbb7ae..28bf0d9156 100644 --- a/commands/bootstrap/__tests__/bootstrap-command.test.js +++ b/commands/bootstrap/__tests__/bootstrap-command.test.js @@ -144,7 +144,7 @@ describe("BootstrapCommand", () => { 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)." + "Bootstrapping with pnpm is not supported. Use pnpm directly to manage dependencies: https://pnpm.io/cli/install" ); }); }); diff --git a/commands/bootstrap/index.js b/commands/bootstrap/index.js index 7438e21457..e901a7efd3 100644 --- a/commands/bootstrap/index.js +++ b/commands/bootstrap/index.js @@ -41,7 +41,7 @@ class BootstrapCommand extends Command { if (npmClient === "pnpm") { throw new ValidationError( "EWORKSPACES", - "Bootstrapping with pnpm is not supported. Use pnpm directly to manage dependencies (https://pnpm.io/cli/install)." + "Bootstrapping with pnpm is not supported. Use pnpm directly to manage dependencies: https://pnpm.io/cli/install" ); } diff --git a/commands/link/__tests__/link-command.test.js b/commands/link/__tests__/link-command.test.js index 419ad9aacc..c906c7f499 100644 --- a/commands/link/__tests__/link-command.test.js +++ b/commands/link/__tests__/link-command.test.js @@ -239,7 +239,7 @@ describe("LinkCommand", () => { 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." + "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 75b351f37f..56ddfcf73a 100644 --- a/commands/link/index.js +++ b/commands/link/index.js @@ -23,7 +23,7 @@ class LinkCommand extends Command { 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." + "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/e2e/utils/fixture.ts b/e2e/utils/fixture.ts index a949770fed..f4d3ec5ee7 100644 --- a/e2e/utils/fixture.ts +++ b/e2e/utils/fixture.ts @@ -77,14 +77,14 @@ export class Fixture { await fixture.createEmptyDirectoryForWorkspace(); } - await fixture.setNpmRegistry(packageManager); + await fixture.setNpmRegistry(); if (runLernaInit) { const initOptions = packageManager === "pnpm" ? ({ keepDefaultOptions: true } as const) : {}; await fixture.lernaInit("", initOptions); } - await fixture.initializeNpmEnvironment(packageManager); + await fixture.initializeNpmEnvironment(); if (installDependencies) { await fixture.install(); @@ -93,18 +93,21 @@ export class Fixture { return fixture; } - private async setNpmRegistry(packageManager: PackageManager): Promise { - if (packageManager === "pnpm") { + private async setNpmRegistry(): Promise { + if (this.packageManager === "pnpm") { await this.exec(`echo "registry=${REGISTRY}" > .npmrc`); } } - private async initializeNpmEnvironment(packageManager: PackageManager): Promise { - if (packageManager !== "npm" && existsSync(joinPathFragments(this.fixtureWorkspacePath, "lerna.json"))) { - await this.overrideLernaConfig({ npmClient: packageManager }); + private async initializeNpmEnvironment(): Promise { + if ( + this.packageManager !== "npm" && + existsSync(joinPathFragments(this.fixtureWorkspacePath, "lerna.json")) + ) { + await this.overrideLernaConfig({ npmClient: this.packageManager }); } - if (packageManager === "pnpm") { + if (this.packageManager === "pnpm") { const pnpmWorkspaceContent = dump({ packages: ["packages/*", "!**/__test__/**"], }); @@ -170,9 +173,13 @@ export class Fixture { default: throw new Error(`Unsupported package manager: ${this.packageManager}`); } - return execCommandResult.then((initResult) => - options?.keepDefaultOptions ? initResult : this.revertDefaultInitOptions().then(() => initResult) - ); + + const initResult = await execCommandResult; + if (!options?.keepDefaultOptions) { + await this.revertDefaultInitOptions(); + } + + return initResult; } async overrideLernaConfig(lernaConfig: Record): Promise { diff --git a/package-lock.json b/package-lock.json index 601f7816bc..d46c6fac0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -326,6 +326,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" }, @@ -611,6 +612,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", @@ -19622,6 +19624,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" } @@ -19774,6 +19777,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", From 9d77773aca1eb9ac7988e763e64971cd653af127 Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Mon, 15 Aug 2022 12:13:51 -0400 Subject: [PATCH 21/28] chore(add): update wording of pnpm error for consistency --- commands/add/__tests__/add-command.test.js | 2 +- commands/add/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/commands/add/__tests__/add-command.test.js b/commands/add/__tests__/add-command.test.js index 1675a4b491..f22d70c63e 100644 --- a/commands/add/__tests__/add-command.test.js +++ b/commands/add/__tests__/add-command.test.js @@ -49,7 +49,7 @@ describe("AddCommand", () => { const command = lernaAdd(testDir)("@test/package-1"); await expect(command).rejects.toThrow( - "The 'add' command is not supported when using `pnpm` workspaces. Use `pnpm` directly to add dependencies to packages: https://pnpm.io/cli/add" + "Add is not supported when using `pnpm` workspaces. Use `pnpm` directly to add dependencies to packages: https://pnpm.io/cli/add" ); }); diff --git a/commands/add/index.js b/commands/add/index.js index 34374bda7a..c0c02c323e 100644 --- a/commands/add/index.js +++ b/commands/add/index.js @@ -39,7 +39,7 @@ class AddCommand extends Command { if (this.options.npmClient === "pnpm") { throw new ValidationError( "EPNPMNOTSUPPORTED", - "The 'add' command is not supported when using `pnpm` workspaces. Use `pnpm` directly to add dependencies to packages: https://pnpm.io/cli/add" + "Add is not supported when using `pnpm` workspaces. Use `pnpm` directly to add dependencies to packages: https://pnpm.io/cli/add" ); } From e24e6065f69cea24f5ee2512a0a1d16aaf79c18b Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Fri, 19 Aug 2022 14:58:52 -0400 Subject: [PATCH 22/28] chore(e2e): remove unused exports --- e2e/utils/fixture.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/utils/fixture.ts b/e2e/utils/fixture.ts index f4d3ec5ee7..33137698cb 100644 --- a/e2e/utils/fixture.ts +++ b/e2e/utils/fixture.ts @@ -5,7 +5,7 @@ import isCI from "is-ci"; import { dump } from "js-yaml"; import { dirSync } from "tmp"; -export interface RunCommandOptions { +interface RunCommandOptions { silenceError?: boolean; env?: Record; cwd?: string; @@ -23,7 +23,7 @@ interface FixtureCreateOptions { forceDeterministicTerminalOutput?: boolean; } -export type RunCommandResult = { stdout: string; stderr: string; combinedOutput: string }; +type RunCommandResult = { stdout: string; stderr: string; combinedOutput: string }; const ORIGIN_GIT = "origin.git"; const REGISTRY = "http://localhost:4872/"; From e28e1ba03dac85f76188acd51b5dfb5b5515e0b9 Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Fri, 19 Aug 2022 15:00:57 -0400 Subject: [PATCH 23/28] chore(e2e): clarify test description message --- core/package-graph/__tests__/package-graph.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/package-graph/__tests__/package-graph.test.js b/core/package-graph/__tests__/package-graph.test.js index 6fa3641947..5542f18d8e 100644 --- a/core/package-graph/__tests__/package-graph.test.js +++ b/core/package-graph/__tests__/package-graph.test.js @@ -94,7 +94,7 @@ describe("PackageGraph", () => { }); describe("with spec containing workspace: prefix", () => { - describe("localizes sibling when semver is satisfied", () => { + describe("creates graph links for sibling package when semver is satisfied", () => { it("with exact match", () => { const packages = [ new Package( @@ -186,7 +186,7 @@ describe("PackageGraph", () => { }); }); - it("localizes sibling when using * alias", () => { + it("creates graph links for sibling package when using * alias", () => { const packages = [ new Package( { @@ -216,7 +216,7 @@ describe("PackageGraph", () => { expect(package2.localDependencies.get("test-1").workspaceAlias).toBe("*"); }); - it("localizes sibling when using ~ alias", () => { + it("creates graph links for sibling package when using ~ alias", () => { const packages = [ new Package( { @@ -246,7 +246,7 @@ describe("PackageGraph", () => { expect(package2.localDependencies.get("test-1").workspaceAlias).toBe("~"); }); - it("localizes sibling when using ^ alias", () => { + it("creates graph links for sibling package when using ^ alias", () => { const packages = [ new Package( { From 4eeb8b8304cb3f2cb5b2a9eb61ce6a2d312f787a Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Fri, 19 Aug 2022 15:50:26 -0400 Subject: [PATCH 24/28] fix(version): ignore scripts on pnpm install to update lockfile --- commands/version/index.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/commands/version/index.js b/commands/version/index.js index cd0706c1d1..54aad6a25d 100644 --- a/commands/version/index.js +++ b/commands/version/index.js @@ -587,10 +587,12 @@ class VersionCommand extends Command { if (this.options.npmClient === "pnpm") { chain = chain.then(() => - childProcess.exec("pnpm", ["install", "--lockfile-only"], this.execOpts).then(() => { - const lockfilePath = path.join(this.project.rootPath, "pnpm-lock.yaml"); - changedFiles.add(lockfilePath); - }) + childProcess + .exec("pnpm", ["install", "--lockfile-only", "--ignore-scripts"], this.execOpts) + .then(() => { + const lockfilePath = path.join(this.project.rootPath, "pnpm-lock.yaml"); + changedFiles.add(lockfilePath); + }) ); } From 711364c26a29c389fc73ca54a78ab59061b89c51 Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Fri, 19 Aug 2022 16:17:58 -0400 Subject: [PATCH 25/28] chore(e2e): add test to cover pnpm lockfile update skipping install scripts --- .../positional-arguments-pnpm.spec.ts | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/e2e/tests/lerna-version/positional-arguments-pnpm.spec.ts b/e2e/tests/lerna-version/positional-arguments-pnpm.spec.ts index 1007034d7b..e770176a5b 100644 --- a/e2e/tests/lerna-version/positional-arguments-pnpm.spec.ts +++ b/e2e/tests/lerna-version/positional-arguments-pnpm.spec.ts @@ -160,4 +160,75 @@ describe("lerna-version-positional-arguments-pnpm", () => { } `); }); + + 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, + }, + }, + } + `); + }); }); From 2b47157322a076aecc400265c2aa884be08ae3b5 Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Tue, 23 Aug 2022 11:13:34 -0400 Subject: [PATCH 26/28] fix(version): Update pnpm lockfile when on independent versions --- commands/version/index.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/commands/version/index.js b/commands/version/index.js index 54aad6a25d..e4fbf24c87 100644 --- a/commands/version/index.js +++ b/commands/version/index.js @@ -585,17 +585,6 @@ class VersionCommand extends Command { if (!independentVersions) { this.project.version = this.globalVersion; - if (this.options.npmClient === "pnpm") { - chain = chain.then(() => - 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 (conventionalCommits && changelog) { chain = chain.then(() => updateChangelog(this.project.manifest, "root", { @@ -624,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)) { From fd2ead373c73cf27aa9fe8146657ab0d013eee3b Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Fri, 26 Aug 2022 12:47:05 -0400 Subject: [PATCH 27/28] fix: add typescript dependency --- core/lerna/package.json | 3 ++- package-lock.json | 24 +++++++----------------- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/core/lerna/package.json b/core/lerna/package.json index 2b8fac5e20..d609ab2f11 100644 --- a/core/lerna/package.json +++ b/core/lerna/package.json @@ -62,6 +62,7 @@ "@nrwl/devkit": ">=14.5.8 < 16", "import-local": "^3.0.2", "npmlog": "^6.0.2", - "nx": ">=14.5.8 < 16" + "nx": ">=14.5.8 < 16", + "typescript": "^3 || ^4" } } diff --git a/package-lock.json b/package-lock.json index 5f8cac3a92..b80c1aff86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -387,6 +387,7 @@ "commands/repair": { "name": "@lerna/repair", "version": "5.4.3", + "extraneous": true, "license": "MIT", "dependencies": { "@lerna/command": "file:../../core/command", @@ -551,6 +552,7 @@ "@lerna/changed": "file:../../commands/changed", "@lerna/clean": "file:../../commands/clean", "@lerna/cli": "file:../cli", + "@lerna/command": "file:../command", "@lerna/create": "file:../../commands/create", "@lerna/diff": "file:../../commands/diff", "@lerna/exec": "file:../../commands/exec", @@ -560,13 +562,13 @@ "@lerna/link": "file:../../commands/link", "@lerna/list": "file:../../commands/list", "@lerna/publish": "file:../../commands/publish", - "@lerna/repair": "file:../../commands/repair", "@lerna/run": "file:../../commands/run", "@lerna/version": "file:../../commands/version", "@nrwl/devkit": ">=14.5.8 < 16", "import-local": "^3.0.2", "npmlog": "^6.0.2", - "nx": ">=14.5.8 < 16" + "nx": ">=14.5.8 < 16", + "typescript": "^3 || ^4" }, "bin": { "lerna": "cli.js" @@ -2586,10 +2588,6 @@ "resolved": "utils/query-graph", "link": true }, - "node_modules/@lerna/repair": { - "resolved": "commands/repair", - "link": true - }, "node_modules/@lerna/resolve-symlink": { "resolved": "utils/resolve-symlink", "link": true @@ -19869,15 +19867,6 @@ "@lerna/package-graph": "file:../../core/package-graph" } }, - "@lerna/repair": { - "version": "file:commands/repair", - "requires": { - "@lerna/command": "file:../../core/command", - "lerna": "file:../../core/lerna", - "npmlog": "^6.0.2", - "nx": ">=14.5.8 < 16" - } - }, "@lerna/resolve-symlink": { "version": "file:utils/resolve-symlink", "requires": { @@ -27259,6 +27248,7 @@ "@lerna/changed": "file:../../commands/changed", "@lerna/clean": "file:../../commands/clean", "@lerna/cli": "file:../cli", + "@lerna/command": "file:../command", "@lerna/create": "file:../../commands/create", "@lerna/diff": "file:../../commands/diff", "@lerna/exec": "file:../../commands/exec", @@ -27268,13 +27258,13 @@ "@lerna/link": "file:../../commands/link", "@lerna/list": "file:../../commands/list", "@lerna/publish": "file:../../commands/publish", - "@lerna/repair": "file:../../commands/repair", "@lerna/run": "file:../../commands/run", "@lerna/version": "file:../../commands/version", "@nrwl/devkit": ">=14.5.8 < 16", "import-local": "^3.0.2", "npmlog": "^6.0.2", - "nx": ">=14.5.8 < 16" + "nx": ">=14.5.8 < 16", + "typescript": "^3 || ^4" } }, "leven": { From 3b5b5aa194e31830b8c15e7af99ed490da97551b Mon Sep 17 00:00:00 2001 From: Austin Fahsl Date: Fri, 26 Aug 2022 15:24:58 -0400 Subject: [PATCH 28/28] chore(e2e): Add test for lerna run with nx and pnpm --- e2e/tests/lerna-run/lerna-run-nx-pnpm.spec.ts | 318 ++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 e2e/tests/lerna-run/lerna-run-nx-pnpm.spec.ts 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 + +`); + }); +});