diff --git a/__helpers__/build-graph.js b/__helpers__/build-graph.js index b27f72d69b..db75e806c8 100644 --- a/__helpers__/build-graph.js +++ b/__helpers__/build-graph.js @@ -6,7 +6,7 @@ const PackageGraph = require("@lerna/package-graph"); module.exports = buildGraph; -function buildGraph() { +function buildGraph(mapPackages = pkg => pkg) { // cat __fixtures__/toposort/packages/*/package.json const packages = [ { @@ -61,7 +61,9 @@ function buildGraph() { name: "package-standalone", version: "1.0.0", }, - ].map(json => new Package(json, `/test/packages/${json.name}`, "/test")); + ] + .map(mapPackages) + .map(json => new Package(json, `/test/packages/${json.name}`, "/test")); return new PackageGraph(packages); // require("console").dir(graph, { compact: false }) diff --git a/__tests__/collect-updates.test.js b/__tests__/collect-updates.test.js index 63452e79c3..10b222a74a 100644 --- a/__tests__/collect-updates.test.js +++ b/__tests__/collect-updates.test.js @@ -1,5 +1,7 @@ "use strict"; +const dedent = require("dedent"); + jest.mock("@lerna/describe-ref"); jest.mock("../lib/has-tags"); jest.mock("../lib/make-diff-predicate"); @@ -46,6 +48,10 @@ const ALL_NODES = Object.freeze([ expect.objectContaining({ name: "package-standalone" }), ]); +const toPrereleaseMapper = names => pkg => { + return !names || names.includes(pkg.name) ? Object.assign(pkg, { version: `${pkg.version}-alpha.0` }) : pkg; +}; + describe("collectUpdates()", () => { beforeEach(() => { // isolate each test @@ -227,6 +233,91 @@ describe("collectUpdates()", () => { ]); }); + it("returns all prereleased nodes with --conventional-graduate", () => { + const graph = buildGraph(toPrereleaseMapper()); + const pkgs = graph.rawPackageList; + const execOpts = { cwd: "/test" }; + + const updates = collectUpdates(pkgs, graph, execOpts, { + conventionalCommits: true, + conventionalGraduate: true, + }); + + expect(updates).toEqual(ALL_NODES); + }); + + it("returns all prereleased nodes with --conventional-graduate *", () => { + const graph = buildGraph(toPrereleaseMapper()); + const pkgs = graph.rawPackageList; + const execOpts = { cwd: "/test" }; + + const updates = collectUpdates(pkgs, graph, execOpts, { + conventionalCommits: true, + conventionalGraduate: "*", + }); + + expect(updates).toEqual(ALL_NODES); + }); + + it("always includes prereleased nodes targeted by --conventional-graduate ", () => { + changedPackages.add("package-dag-3"); + + const graph = buildGraph(toPrereleaseMapper(["package-dag-3", "package-standalone"])); + const pkgs = graph.rawPackageList; + const execOpts = { cwd: "/test" }; + + const updates = collectUpdates(pkgs, graph, execOpts, { + conventionalCommits: true, + conventionalGraduate: "package-standalone", + }); + + expect(updates).toEqual([ + expect.objectContaining({ name: "package-dag-3" }), + expect.objectContaining({ name: "package-standalone" }), + ]); + }); + + it("always includes prereleased nodes targeted by --conventional-graduate ,", () => { + changedPackages.add("package-dag-3"); + + const graph = buildGraph(toPrereleaseMapper(["package-dag-3", "package-standalone", "package-dag-2b"])); + const pkgs = graph.rawPackageList; + const execOpts = { cwd: "/test" }; + + const updates = collectUpdates(pkgs, graph, execOpts, { + forcePublish: "package-standalone,package-dag-2b", + }); + + expect(updates).toEqual([ + expect.objectContaining({ name: "package-dag-2b" }), + expect.objectContaining({ name: "package-dag-3" }), + expect.objectContaining({ name: "package-standalone" }), + ]); + }); + + it( + dedent` + always includes prereleased nodes targeted by --conventional-graduate --conventional-graduate + `, + () => { + changedPackages.add("package-dag-3"); + + const graph = buildGraph(toPrereleaseMapper(["package-dag-3", "package-standalone", "package-dag-2b"])); + const pkgs = graph.rawPackageList; + const execOpts = { cwd: "/test" }; + + const updates = collectUpdates(pkgs, graph, execOpts, { + forcePublish: ["package-standalone", "package-dag-2b"], + }); + + expect(updates).toEqual([ + expect.objectContaining({ name: "package-dag-2b" }), + expect.objectContaining({ name: "package-dag-3" }), + expect.objectContaining({ name: "package-standalone" }), + ]); + } + ); + it("uses revision range with --canary", () => { changedPackages.add("package-dag-2a"); diff --git a/__tests__/lib-collect-packages.test.js b/__tests__/lib-collect-packages.test.js new file mode 100644 index 0000000000..9fa6980647 --- /dev/null +++ b/__tests__/lib-collect-packages.test.js @@ -0,0 +1,54 @@ +"use strict"; + +// helpers +const buildGraph = require("../__helpers__/build-graph"); + +// file under test +const collectPackages = require("../lib/collect-packages"); + +const toNamesList = collection => Array.from(collection).map(pkg => pkg.name); + +test("returns all packages", () => { + const graph = buildGraph(); + const result = collectPackages(graph); + + expect(toNamesList(result)).toMatchInlineSnapshot(` +Array [ + "package-cycle-1", + "package-cycle-2", + "package-cycle-extraneous", + "package-dag-1", + "package-dag-2a", + "package-dag-2b", + "package-dag-3", + "package-standalone", +] +`); +}); + +test("filters packages through isCandidate, passing node and name", () => { + const graph = buildGraph(); + const packagesToInclude = ["package-cycle-1"]; + const isCandidate = (node, name) => { + return packagesToInclude.includes(node.name) && node.name === name; + }; + const result = collectPackages(graph, { isCandidate }); + + expect(toNamesList(result)).toMatchInlineSnapshot(` +Array [ + "package-cycle-1", + "package-cycle-2", + "package-cycle-extraneous", +] +`); +}); + +test("calls onInclude with included package name", () => { + const graph = buildGraph(); + const packagesToInclude = ["package-standalone"]; + const isCandidate = (node, name) => packagesToInclude.includes(name); + const onInclude = jest.fn(); + collectPackages(graph, { isCandidate, onInclude }); + + expect(onInclude).toHaveBeenCalledWith(packagesToInclude[0]); +}); diff --git a/__tests__/lib-get-forced-packages.test.js b/__tests__/lib-get-forced-packages.test.js deleted file mode 100644 index ee00258041..0000000000 --- a/__tests__/lib-get-forced-packages.test.js +++ /dev/null @@ -1,40 +0,0 @@ -"use strict"; - -// file under test -const getForcedPackages = require("../lib/get-forced-packages"); - -test("no argument", () => { - const result = getForcedPackages(); - - expect(Array.from(result)).toEqual([]); -}); - -test("--force-publish", () => { - const result = getForcedPackages(true); - - expect(Array.from(result)).toEqual(["*"]); -}); - -test("--force-publish *", () => { - const result = getForcedPackages("*"); - - expect(Array.from(result)).toEqual(["*"]); -}); - -test("--force-publish foo", () => { - const result = getForcedPackages("foo"); - - expect(Array.from(result)).toEqual(["foo"]); -}); - -test("--force-publish foo,bar", () => { - const result = getForcedPackages("foo,bar"); - - expect(Array.from(result)).toEqual(["foo", "bar"]); -}); - -test("--force-publish foo --force-publish bar", () => { - const result = getForcedPackages(["foo", "bar"]); - - expect(Array.from(result)).toEqual(["foo", "bar"]); -}); diff --git a/__tests__/lib-get-packages-for-option.test.js b/__tests__/lib-get-packages-for-option.test.js new file mode 100644 index 0000000000..601376cbb0 --- /dev/null +++ b/__tests__/lib-get-packages-for-option.test.js @@ -0,0 +1,40 @@ +"use strict"; + +// file under test +const getPackagesForOption = require("../lib/get-packages-for-option"); + +test("no argument", () => { + const result = getPackagesForOption(); + + expect(Array.from(result)).toEqual([]); +}); + +test("--config-option", () => { + const result = getPackagesForOption(true); + + expect(Array.from(result)).toEqual(["*"]); +}); + +test("--config-option *", () => { + const result = getPackagesForOption("*"); + + expect(Array.from(result)).toEqual(["*"]); +}); + +test("--config-option foo", () => { + const result = getPackagesForOption("foo"); + + expect(Array.from(result)).toEqual(["foo"]); +}); + +test("--config-option foo,bar", () => { + const result = getPackagesForOption("foo,bar"); + + expect(Array.from(result)).toEqual(["foo", "bar"]); +}); + +test("--config-option foo --config-option bar", () => { + const result = getPackagesForOption(["foo", "bar"]); + + expect(Array.from(result)).toEqual(["foo", "bar"]); +}); diff --git a/collect-updates.js b/collect-updates.js index 65aa5d6e16..7cc327aea3 100644 --- a/collect-updates.js +++ b/collect-updates.js @@ -4,14 +4,21 @@ const log = require("npmlog"); const describeRef = require("@lerna/describe-ref"); const hasTags = require("./lib/has-tags"); -const collectDependents = require("./lib/collect-dependents"); -const getForcedPackages = require("./lib/get-forced-packages"); +const collectPackages = require("./lib/collect-packages"); +const getPackagesForOption = require("./lib/get-packages-for-option"); const makeDiffPredicate = require("./lib/make-diff-predicate"); module.exports = collectUpdates; +module.exports.collectPackages = collectPackages; +module.exports.getPackagesForOption = getPackagesForOption; function collectUpdates(filteredPackages, packageGraph, execOpts, commandOptions) { - const forced = getForcedPackages(commandOptions.forcePublish); + const { forcePublish, conventionalCommits, conventionalGraduate } = commandOptions; + + // If --conventional-commits and --conventional-graduate are both set, ignore --force-publish + const useConventionalGraduate = conventionalCommits && conventionalGraduate; + const forced = getPackagesForOption(useConventionalGraduate ? conventionalGraduate : forcePublish); + const packages = filteredPackages.length === packageGraph.size ? packageGraph @@ -43,45 +50,41 @@ function collectUpdates(filteredPackages, packageGraph, execOpts, commandOptions if (forced.size) { // "warn" might seem a bit loud, but it is appropriate for logging anything _forced_ - log.warn("force-publish", forced.has("*") ? "all packages" : Array.from(forced.values()).join("\n")); + log.warn( + useConventionalGraduate ? "conventional-graduate" : "force-publish", + forced.has("*") ? "all packages" : Array.from(forced.values()).join("\n") + ); } - let candidates; - - if (!committish || forced.has("*")) { + if (useConventionalGraduate) { + // --conventional-commits --conventional-graduate + if (forced.has("*")) { + log.info("", "Graduating all prereleased packages"); + } else { + log.info("", "Graduating prereleased packages"); + } + } else if (!committish || forced.has("*")) { + // --force-publish or no tag log.info("", "Assuming all packages changed"); - candidates = new Set(packages.values()); - } else { - log.info("", `Looking for changed packages since ${committish}`); - candidates = new Set(); - - const hasDiff = makeDiffPredicate(committish, execOpts, commandOptions.ignoreChanges); - const needsBump = - !commandOptions.bump || commandOptions.bump.startsWith("pre") - ? () => false - : /* skip packages that have not been previously prereleased */ - node => node.prereleaseId; - - packages.forEach((node, name) => { - if (forced.has(name) || needsBump(node) || hasDiff(node)) { - candidates.add(node); - } + + return collectPackages(packages, { + onInclude: name => log.verbose("updated", name), }); } - const dependents = collectDependents(candidates); - dependents.forEach(node => candidates.add(node)); - - // The result should always be in the same order as the input - const updates = []; + log.info("", `Looking for changed packages since ${committish}`); - packages.forEach((node, name) => { - if (candidates.has(node)) { - log.verbose("updated", name); + const hasDiff = makeDiffPredicate(committish, execOpts, commandOptions.ignoreChanges); + const needsBump = + !commandOptions.bump || commandOptions.bump.startsWith("pre") + ? () => false + : /* skip packages that have not been previously prereleased */ + node => node.prereleaseId; + const isForced = (node, name) => + (forced.has("*") || forced.has(name)) && (useConventionalGraduate ? node.prereleaseId : true); - updates.push(node); - } + return collectPackages(packages, { + isCandidate: (node, name) => isForced(node, name) || needsBump(node) || hasDiff(node), + onInclude: name => log.verbose("updated", name), }); - - return updates; } diff --git a/lib/collect-packages.js b/lib/collect-packages.js new file mode 100644 index 0000000000..1317a74fa6 --- /dev/null +++ b/lib/collect-packages.js @@ -0,0 +1,32 @@ +"use strict"; + +const collectDependents = require("./collect-dependents"); + +module.exports = collectPackages; + +function collectPackages(packages, { isCandidate = () => true, onInclude } = {}) { + const candidates = new Set(); + + packages.forEach((node, name) => { + if (isCandidate(node, name)) { + candidates.add(node); + } + }); + + const dependents = collectDependents(candidates); + dependents.forEach(node => candidates.add(node)); + + // The result should always be in the same order as the input + const updates = []; + + packages.forEach((node, name) => { + if (candidates.has(node)) { + if (onInclude) { + onInclude(name); + } + updates.push(node); + } + }); + + return updates; +} diff --git a/lib/get-forced-packages.js b/lib/get-forced-packages.js deleted file mode 100644 index 98f503b03c..0000000000 --- a/lib/get-forced-packages.js +++ /dev/null @@ -1,24 +0,0 @@ -"use strict"; - -module.exports = getForcedPackages; - -function getForcedPackages(forcePublish) { - // new Set(null) is equivalent to new Set([]) - // i.e., an empty Set - let inputs = null; - - if (forcePublish === true) { - // --force-publish - inputs = ["*"]; - } else if (typeof forcePublish === "string") { - // --force-publish=* - // --force-publish=foo - // --force-publish=foo,bar - inputs = forcePublish.split(","); - } else if (Array.isArray(forcePublish)) { - // --force-publish foo --force-publish baz - inputs = [...forcePublish]; - } - - return new Set(inputs); -} diff --git a/lib/get-packages-for-option.js b/lib/get-packages-for-option.js new file mode 100644 index 0000000000..16ad959421 --- /dev/null +++ b/lib/get-packages-for-option.js @@ -0,0 +1,26 @@ +"use strict"; + +module.exports = getPackagesForOption; + +function getPackagesForOption(option) { + // new Set(null) is equivalent to new Set([]) + // i.e., an empty Set + let inputs = null; + + if (option === true) { + // option passed without specific packages, eg. --force-publish + inputs = ["*"]; + } else if (typeof option === "string") { + // option passed with one or more comma separated package names, eg.: + // --force-publish=* + // --force-publish=foo + // --force-publish=foo,bar + inputs = option.split(","); + } else if (Array.isArray(option)) { + // option passed multiple times with individual package names + // --force-publish foo --force-publish baz + inputs = [...option]; + } + + return new Set(inputs); +}