From 5d84b61499cef8f0e3337ffa29a655ab2d5964ea Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Fri, 10 May 2019 20:47:44 -0400 Subject: [PATCH] feat(conventional-commits): Add conventional prerelease/graduation (#1991) Fixes #1433 Fixes #1675 --- .../prerelease-independent/lerna.json | 3 + .../prerelease-independent/package.json | 3 + .../packages/package-1/package.json | 4 + .../packages/package-2/package.json | 7 + .../packages/package-3/package.json | 7 + .../packages/package-4/package.json | 7 + .../packages/package-5/package.json | 8 + __fixtures__/prerelease/lerna.json | 3 + __fixtures__/prerelease/package.json | 3 + .../packages/package-1/package.json | 4 + .../packages/package-2/package.json | 7 + .../packages/package-3/package.json | 10 + .../packages/package-4/package.json | 7 + .../packages/package-5/package.json | 11 + commands/__mocks__/@lerna/collect-updates.js | 4 + commands/publish/README.md | 9 + .../publish/__tests__/publish-tagging.test.js | 62 +++ commands/publish/index.js | 24 +- commands/publish/package.json | 1 + commands/version/README.md | 35 +- .../version-conventional-commits.test.js.snap | 65 +++ .../version/__tests__/prompt-version.test.js | 6 +- .../version/__tests__/version-command.test.js | 22 + .../version-conventional-commits.test.js | 130 +++++- commands/version/index.js | 47 +- commands/version/package.json | 1 + .../__tests__/conventional-commits.test.js | 86 ++++ .../lib/recommend-version.js | 27 +- core/package-graph/index.js | 3 +- core/package-graph/package.json | 1 + ...lish-conventional-fixed-prerelease.test.js | 434 ++++++++++++++++++ ...onventional-independent-prerelease.test.js | 393 ++++++++++++++++ package-lock.json | 57 +-- .../__helpers__/build-graph.js | 6 +- .../__tests__/collect-updates.test.js | 91 ++++ .../__tests__/lib-collect-packages.test.js | 54 +++ .../__tests__/lib-get-forced-packages.test.js | 40 -- .../lib-get-packages-for-option.test.js | 40 ++ utils/collect-updates/collect-updates.js | 73 +-- utils/collect-updates/lib/collect-packages.js | 32 ++ .../lib/get-forced-packages.js | 24 - .../lib/get-packages-for-option.js | 26 ++ utils/prerelease-id-from-version/README.md | 17 + .../prerelease-id-from-version.test.js | 17 + .../lib/prerelease-id-from-version.js | 9 + utils/prerelease-id-from-version/package.json | 33 ++ 46 files changed, 1797 insertions(+), 156 deletions(-) create mode 100644 __fixtures__/prerelease-independent/lerna.json create mode 100644 __fixtures__/prerelease-independent/package.json create mode 100644 __fixtures__/prerelease-independent/packages/package-1/package.json create mode 100644 __fixtures__/prerelease-independent/packages/package-2/package.json create mode 100644 __fixtures__/prerelease-independent/packages/package-3/package.json create mode 100644 __fixtures__/prerelease-independent/packages/package-4/package.json create mode 100644 __fixtures__/prerelease-independent/packages/package-5/package.json create mode 100644 __fixtures__/prerelease/lerna.json create mode 100644 __fixtures__/prerelease/package.json create mode 100644 __fixtures__/prerelease/packages/package-1/package.json create mode 100644 __fixtures__/prerelease/packages/package-2/package.json create mode 100644 __fixtures__/prerelease/packages/package-3/package.json create mode 100644 __fixtures__/prerelease/packages/package-4/package.json create mode 100644 __fixtures__/prerelease/packages/package-5/package.json create mode 100644 integration/lerna-publish-conventional-fixed-prerelease.test.js create mode 100644 integration/lerna-publish-conventional-independent-prerelease.test.js create mode 100644 utils/collect-updates/__tests__/lib-collect-packages.test.js delete mode 100644 utils/collect-updates/__tests__/lib-get-forced-packages.test.js create mode 100644 utils/collect-updates/__tests__/lib-get-packages-for-option.test.js create mode 100644 utils/collect-updates/lib/collect-packages.js delete mode 100644 utils/collect-updates/lib/get-forced-packages.js create mode 100644 utils/collect-updates/lib/get-packages-for-option.js create mode 100644 utils/prerelease-id-from-version/README.md create mode 100644 utils/prerelease-id-from-version/__tests__/prerelease-id-from-version.test.js create mode 100644 utils/prerelease-id-from-version/lib/prerelease-id-from-version.js create mode 100644 utils/prerelease-id-from-version/package.json diff --git a/__fixtures__/prerelease-independent/lerna.json b/__fixtures__/prerelease-independent/lerna.json new file mode 100644 index 0000000000..053f72e27e --- /dev/null +++ b/__fixtures__/prerelease-independent/lerna.json @@ -0,0 +1,3 @@ +{ + "version": "independent" +} diff --git a/__fixtures__/prerelease-independent/package.json b/__fixtures__/prerelease-independent/package.json new file mode 100644 index 0000000000..46358b1693 --- /dev/null +++ b/__fixtures__/prerelease-independent/package.json @@ -0,0 +1,3 @@ +{ + "name": "independent" +} diff --git a/__fixtures__/prerelease-independent/packages/package-1/package.json b/__fixtures__/prerelease-independent/packages/package-1/package.json new file mode 100644 index 0000000000..b2cf9f513a --- /dev/null +++ b/__fixtures__/prerelease-independent/packages/package-1/package.json @@ -0,0 +1,4 @@ +{ + "name": "package-1", + "version": "1.0.0-alpha.0" +} diff --git a/__fixtures__/prerelease-independent/packages/package-2/package.json b/__fixtures__/prerelease-independent/packages/package-2/package.json new file mode 100644 index 0000000000..20cea29605 --- /dev/null +++ b/__fixtures__/prerelease-independent/packages/package-2/package.json @@ -0,0 +1,7 @@ +{ + "name": "package-2", + "version": "2.0.0-alpha.0", + "dependencies": { + "package-1": "^1.0.0-alpha.0" + } +} diff --git a/__fixtures__/prerelease-independent/packages/package-3/package.json b/__fixtures__/prerelease-independent/packages/package-3/package.json new file mode 100644 index 0000000000..0fc52f5ca1 --- /dev/null +++ b/__fixtures__/prerelease-independent/packages/package-3/package.json @@ -0,0 +1,7 @@ +{ + "name": "package-3", + "version": "3.0.0-beta.3", + "devDependencies": { + "package-2": "^2.0.0-alpha.0" + } +} diff --git a/__fixtures__/prerelease-independent/packages/package-4/package.json b/__fixtures__/prerelease-independent/packages/package-4/package.json new file mode 100644 index 0000000000..cca2c950af --- /dev/null +++ b/__fixtures__/prerelease-independent/packages/package-4/package.json @@ -0,0 +1,7 @@ +{ + "name": "package-4", + "version": "4.0.0", + "dependencies": { + "package-1": "^0.0.0" + } +} diff --git a/__fixtures__/prerelease-independent/packages/package-5/package.json b/__fixtures__/prerelease-independent/packages/package-5/package.json new file mode 100644 index 0000000000..970355bebf --- /dev/null +++ b/__fixtures__/prerelease-independent/packages/package-5/package.json @@ -0,0 +1,8 @@ +{ + "name": "package-5", + "dependencies": { + "package-3": "^3.0.0-beta.3" + }, + "private": true, + "version": "5.0.0-alpha.0" +} diff --git a/__fixtures__/prerelease/lerna.json b/__fixtures__/prerelease/lerna.json new file mode 100644 index 0000000000..64476f8ce0 --- /dev/null +++ b/__fixtures__/prerelease/lerna.json @@ -0,0 +1,3 @@ +{ + "version": "1.0.0-alpha.0" +} diff --git a/__fixtures__/prerelease/package.json b/__fixtures__/prerelease/package.json new file mode 100644 index 0000000000..3de5846216 --- /dev/null +++ b/__fixtures__/prerelease/package.json @@ -0,0 +1,3 @@ +{ + "name": "normal" +} diff --git a/__fixtures__/prerelease/packages/package-1/package.json b/__fixtures__/prerelease/packages/package-1/package.json new file mode 100644 index 0000000000..b2cf9f513a --- /dev/null +++ b/__fixtures__/prerelease/packages/package-1/package.json @@ -0,0 +1,4 @@ +{ + "name": "package-1", + "version": "1.0.0-alpha.0" +} diff --git a/__fixtures__/prerelease/packages/package-2/package.json b/__fixtures__/prerelease/packages/package-2/package.json new file mode 100644 index 0000000000..b838a6c68d --- /dev/null +++ b/__fixtures__/prerelease/packages/package-2/package.json @@ -0,0 +1,7 @@ +{ + "name": "package-2", + "version": "1.0.0-alpha.0", + "dependencies": { + "package-1": "^1.0.0-alpha.0" + } +} diff --git a/__fixtures__/prerelease/packages/package-3/package.json b/__fixtures__/prerelease/packages/package-3/package.json new file mode 100644 index 0000000000..6a354028e6 --- /dev/null +++ b/__fixtures__/prerelease/packages/package-3/package.json @@ -0,0 +1,10 @@ +{ + "name": "package-3", + "version": "1.0.0-alpha.0", + "peerDependencies": { + "package-2": "^1.0.0-alpha.0" + }, + "devDependencies": { + "package-2": "^1.0.0-alpha.0" + } +} diff --git a/__fixtures__/prerelease/packages/package-4/package.json b/__fixtures__/prerelease/packages/package-4/package.json new file mode 100644 index 0000000000..20008924a8 --- /dev/null +++ b/__fixtures__/prerelease/packages/package-4/package.json @@ -0,0 +1,7 @@ +{ + "name": "package-4", + "version": "1.0.0-alpha.0", + "dependencies": { + "package-1": "^0.0.0" + } +} diff --git a/__fixtures__/prerelease/packages/package-5/package.json b/__fixtures__/prerelease/packages/package-5/package.json new file mode 100644 index 0000000000..32d7222db9 --- /dev/null +++ b/__fixtures__/prerelease/packages/package-5/package.json @@ -0,0 +1,11 @@ +{ + "name": "package-5", + "dependencies": { + "package-1": "^1.0.0-alpha.0" + }, + "optionalDependencies": { + "package-3": "^1.0.0-alpha.0" + }, + "private": true, + "version": "1.0.0-alpha.0" +} diff --git a/commands/__mocks__/@lerna/collect-updates.js b/commands/__mocks__/@lerna/collect-updates.js index b484ca8455..ba2665c56b 100644 --- a/commands/__mocks__/@lerna/collect-updates.js +++ b/commands/__mocks__/@lerna/collect-updates.js @@ -1,5 +1,7 @@ "use strict"; +const { collectPackages, getPackagesForOption } = jest.requireActual("@lerna/collect-updates"); + // collectUpdates.setUpdated(cwd, packageNames...) // otherwise, enables everything const updated = new Map(); @@ -20,3 +22,5 @@ afterEach(() => { module.exports = mockCollectUpdates; module.exports.setUpdated = setUpdated; +module.exports.collectPackages = collectPackages; +module.exports.getPackagesForOption = getPackagesForOption; diff --git a/commands/publish/README.md b/commands/publish/README.md index 8a632e52d4..a366b2700e 100644 --- a/commands/publish/README.md +++ b/commands/publish/README.md @@ -52,6 +52,7 @@ This is useful when a previous `lerna publish` failed to publish all packages to - [`--no-git-reset`](#--no-git-reset) - [`--no-verify-access`](#--no-verify-access) - [`--preid`](#--preid) +- [`--pre-dist-tag `](#--pre-dist-tag-tag) - [`--registry `](#--registry-url) - [`--temp-tag`](#--temp-tag) - [`--ignore-scripts`](#--ignore-scripts) @@ -157,6 +158,14 @@ lerna publish --canary --preid next When run with this flag, `lerna publish --canary` will increment `premajor`, `preminor`, `prepatch`, or `prerelease` semver bumps using the specified [prerelease identifier](http://semver.org/#spec-item-9). +### `--pre-dist-tag ` + +```sh +lerna publish --pre-dist-tag next +``` + +Works the same as [`--dist-tag`](#--dist-tag-tag), except only applies to packages being released with a prerelease version. + ### `--registry ` When run with this flag, forwarded npm commands will use the specified registry for your package(s). diff --git a/commands/publish/__tests__/publish-tagging.test.js b/commands/publish/__tests__/publish-tagging.test.js index 515db1c0d8..9859ad90cf 100644 --- a/commands/publish/__tests__/publish-tagging.test.js +++ b/commands/publish/__tests__/publish-tagging.test.js @@ -96,3 +96,65 @@ Map { expect(npmDistTag.add).toHaveBeenCalledWith("@integration/package-1@1.0.1", "beta", conf); // <-- expect(npmDistTag.add).toHaveBeenCalledWith("@integration/package-2@1.0.1", "beta", conf); }); + +test("publish prerelease --pre-dist-tag beta", async () => { + const cwd = await initFixture("normal"); + + collectUpdates.setUpdated(cwd, "package-1"); + + await lernaPublish(cwd)("prerelease", "--pre-dist-tag", "beta"); + + expect(npmPublish.registry.get("package-1")).toBe("beta"); + expect(npmDistTag.remove).not.toHaveBeenCalled(); +}); + +test("publish non-prerelease --pre-dist-tag beta", async () => { + const cwd = await initFixture("normal"); + + collectUpdates.setUpdated(cwd, "package-1"); + + await lernaPublish(cwd)("--pre-dist-tag", "beta"); + + expect(npmPublish.registry.get("package-1")).toBe("latest"); + expect(npmDistTag.remove).not.toHaveBeenCalled(); +}); + +test("publish non-prerelease --dist-tag next --pre-dist-tag beta", async () => { + const cwd = await initFixture("normal"); + + collectUpdates.setUpdated(cwd, "package-1"); + + await lernaPublish(cwd)("--dist-tag", "next", "--pre-dist-tag", "beta"); + + expect(npmPublish.registry.get("package-1")).toBe("next"); + expect(npmDistTag.remove).not.toHaveBeenCalled(); +}); + +test("publish --pre-dist-tag beta --temp-tag", async () => { + const cwd = await initFixture("integration"); + + await lernaPublish(cwd)( + "prerelease", + "--dist-tag", + "next", + "--preid", + "beta", + "--pre-dist-tag", + "beta", + "--temp-tag" + ); + + expect(npmPublish.registry).toMatchInlineSnapshot(` +Map { + "@integration/package-1" => "lerna-temp", + "@integration/package-2" => "lerna-temp", +} +`); + + const conf = expect.objectContaining({ + tag: "next", + }); + + expect(npmDistTag.add).toHaveBeenCalledWith("@integration/package-1@1.0.1-beta.0", "beta", conf); + expect(npmDistTag.add).toHaveBeenCalledWith("@integration/package-2@1.0.1-beta.0", "beta", conf); +}); diff --git a/commands/publish/index.js b/commands/publish/index.js index e8bbd5f8d8..fbe7343172 100644 --- a/commands/publish/index.js +++ b/commands/publish/index.js @@ -26,6 +26,7 @@ const batchPackages = require("@lerna/batch-packages"); const runParallelBatches = require("@lerna/run-parallel-batches"); const pulseTillDone = require("@lerna/pulse-till-done"); const versionCommand = require("@lerna/version"); +const prereleaseIdFromVersion = require("@lerna/prerelease-id-from-version"); const createTempLicenses = require("./lib/create-temp-licenses"); const getCurrentSHA = require("./lib/get-current-sha"); @@ -642,15 +643,19 @@ class PublishCommand extends Command { const mapper = pPipe( [ - pkg => - pulseTillDone(npmPublish(pkg, pkg.packed.tarFilePath, opts)).then(() => { + pkg => { + const preDistTag = this.getPreDistTag(pkg); + const tag = !this.options.tempTag && preDistTag ? preDistTag : opts.tag; + const pkgOpts = Object.assign({}, opts, { tag }); + return pulseTillDone(npmPublish(pkg, pkg.packed.tarFilePath, pkgOpts)).then(() => { tracker.success("published", pkg.name, pkg.version); tracker.completeWork(1); logPacked(pkg.packed); return pkg; - }), + }); + }, this.options.requireScripts && (pkg => this.execScript(pkg, "postpublish")), ].filter(Boolean) @@ -685,7 +690,8 @@ class PublishCommand extends Command { }; const mapper = pkg => { const spec = `${pkg.name}@${pkg.version}`; - const distTag = getDistTag(pkg.get("publishConfig")); + const preDistTag = this.getPreDistTag(pkg); + const distTag = preDistTag || getDistTag(pkg.get("publishConfig")); return Promise.resolve() .then(() => pulseTillDone(npmDistTag.remove(spec, "lerna-temp", opts))) @@ -714,6 +720,16 @@ class PublishCommand extends Command { // undefined defaults to "latest" OR whatever is in pkg.publishConfig.tag } + + getPreDistTag(pkg) { + if (!this.options.preDistTag) { + return; + } + const isPrerelease = prereleaseIdFromVersion(pkg.version); + if (isPrerelease) { + return this.options.preDistTag; + } + } } module.exports.PublishCommand = PublishCommand; diff --git a/commands/publish/package.json b/commands/publish/package.json index 5aa4911af3..fc79ba88aa 100644 --- a/commands/publish/package.json +++ b/commands/publish/package.json @@ -46,6 +46,7 @@ "@lerna/npm-publish": "file:../../utils/npm-publish", "@lerna/output": "file:../../utils/output", "@lerna/pack-directory": "file:../../utils/pack-directory", + "@lerna/prerelease-id-from-version": "file:../../utils/prerelease-id-from-version", "@lerna/prompt": "file:../../core/prompt", "@lerna/pulse-till-done": "file:../../utils/pulse-till-done", "@lerna/run-lifecycle": "file:../../utils/run-lifecycle", diff --git a/commands/version/README.md b/commands/version/README.md index 735d52d4ba..d595278980 100644 --- a/commands/version/README.md +++ b/commands/version/README.md @@ -30,15 +30,23 @@ lerna version [major | minor | patch | premajor | preminor | prepatch | prerelea When this positional parameter is passed, `lerna version` will skip the version selection prompt and [increment](https://github.com/npm/node-semver#functions) the version by that keyword. You must still use the `--yes` flag to avoid all prompts. -#### "Graduating" prereleases +## Prerelease If you have any packages with a prerelease version number (e.g. `2.0.0-beta.3`) and you run `lerna version` with and a non-prerelease bump (`major`, `minor`, or `patch`), it will publish those previously pre-released packages _as well as_ the packages that have changed since the last release. +For projects using conventional commits, use the following flags for prerelease management: +**[`--conventional-prerelease`](#--conventional-prerelease):** release current changes as prerelease versions. +**[`--conventional-graduate`](#--conventional-graduate):** graduate prerelease versioned packages to stable versions. + +Running `lerna version --conventional-commits` without the above flags will release current changes as prerelease only if the version is already in prerelease. + ## Options - [`--allow-branch`](#--allow-branch-glob) - [`--amend`](#--amend) - [`--conventional-commits`](#--conventional-commits) +- [`--conventional-graduate`](#--conventional-graduate) +- [`--conventional-prerelease`](#--conventional-prerelease) - [`--changelog-preset`](#--changelog-preset) - [`--exact`](#--exact) - [`--force-publish`](#--force-publish) @@ -117,6 +125,31 @@ When run with this flag, `lerna version` will use the [Conventional Commits Spec Passing [`--no-changelog`](#--no-changelog) will disable the generation (or updating) of `CHANGELOG.md` files. +### `--conventional-graduate` + +```sh +lerna version --conventional-commits --conventional-graduate=package-2,package-4 + +# force all prerelease packages to be graduated +lerna version --conventional-commits --conventional-graduate +``` +When run with this flag, `lerna version` will graduate the specified packages (comma-separated) or all packages using `*`. This command works regardless of whether the current HEAD has been released, similar to `--force-publish`, except that any non-prerelease packages are ignored. If changes are present for packages that are not specified (if specifying packages), or for packages that are not in prerelease, those packages will be versioned as they normally would using `--conventional-commits`. + +"Graduating" a package means bumping to the non-prerelease variant of a prerelease version, eg. `package-1@1.0.0-alpha.0 => package-1@1.0.0`. + +> NOTE: when specifying packages, dependents of specified packages will be released, but will not be graduated. + +### `--conventional-prerelease` + +```sh +lerna version --conventional-commits --conventional-prerelease=package-2,package-4 + +# force all prerelease packages to be graduated +lerna version --conventional-commits --conventional-graduate +``` + +When run with this flag, `lerna version` will release with prerelease versions the specified packages (comma-separated) or all packages using `*`. Releases all unreleased changes as pre(patch/minor/major/release) by prefixing the version recommendation from `conventional-commits` with `pre`, eg. if present changes include a feature commit, the recommended bump will be `minor`, so this flag will result in a `preminor` release. If changes are present for packages that are not specified (if specifying packages), or for packages that are already in prerelease, those packages will be versioned as they normally would using `--conventional-commits`. + ### `--changelog-preset` ```sh diff --git a/commands/version/__tests__/__snapshots__/version-conventional-commits.test.js.snap b/commands/version/__tests__/__snapshots__/version-conventional-commits.test.js.snap index 3f7ed10272..da63ce8ce9 100644 --- a/commands/version/__tests__/__snapshots__/version-conventional-commits.test.js.snap +++ b/commands/version/__tests__/__snapshots__/version-conventional-commits.test.js.snap @@ -1,5 +1,24 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`--conventional-commits fixed mode should guess prerelease version bumps and generate CHANGELOG 1`] = ` +"v2.0.0-alpha.0 + +HEAD -> master, tag: v2.0.0-alpha.0 + +CHANGELOG.md +lerna.json +packages/package-1/CHANGELOG.md +packages/package-1/package.json +packages/package-2/CHANGELOG.md +packages/package-2/package.json +packages/package-3/CHANGELOG.md +packages/package-3/package.json +packages/package-4/CHANGELOG.md +packages/package-4/package.json +packages/package-5/CHANGELOG.md +packages/package-5/package.json" +`; + exports[`--conventional-commits fixed mode should use conventional-commits utility to guess version bump and generate CHANGELOG 1`] = ` "v2.0.0 @@ -19,6 +38,52 @@ packages/package-5/CHANGELOG.md packages/package-5/package.json" `; +exports[`--conventional-commits independent should graduate prerelease version bumps and generate CHANGELOG 1`] = ` +"Publish + + - package-1@1.0.1 + - package-2@2.1.0 + - package-3@4.0.0 + - package-4@4.1.0 + - package-5@5.0.1 + +HEAD -> master, tag: package-5@5.0.1, tag: package-4@4.1.0, tag: package-3@4.0.0, tag: package-2@2.1.0, tag: package-1@1.0.1 + +packages/package-1/CHANGELOG.md +packages/package-1/package.json +packages/package-2/CHANGELOG.md +packages/package-2/package.json +packages/package-3/CHANGELOG.md +packages/package-3/package.json +packages/package-4/CHANGELOG.md +packages/package-4/package.json +packages/package-5/CHANGELOG.md +packages/package-5/package.json" +`; + +exports[`--conventional-commits independent should guess prerelease version bumps and generate CHANGELOG 1`] = ` +"Publish + + - package-1@1.0.1-alpha.0 + - package-2@2.1.0-alpha.0 + - package-3@4.0.0-beta.0 + - package-4@4.1.0-alpha.0 + - package-5@5.0.1-alpha.0 + +HEAD -> master, tag: package-5@5.0.1-alpha.0, tag: package-4@4.1.0-alpha.0, tag: package-3@4.0.0-beta.0, tag: package-2@2.1.0-alpha.0, tag: package-1@1.0.1-alpha.0 + +packages/package-1/CHANGELOG.md +packages/package-1/package.json +packages/package-2/CHANGELOG.md +packages/package-2/package.json +packages/package-3/CHANGELOG.md +packages/package-3/package.json +packages/package-4/CHANGELOG.md +packages/package-4/package.json +packages/package-5/CHANGELOG.md +packages/package-5/package.json" +`; + exports[`--conventional-commits independent should use conventional-commits utility to guess version bump and generate CHANGELOG 1`] = ` "Publish diff --git a/commands/version/__tests__/prompt-version.test.js b/commands/version/__tests__/prompt-version.test.js index b1b874a237..339d1b465f 100644 --- a/commands/version/__tests__/prompt-version.test.js +++ b/commands/version/__tests__/prompt-version.test.js @@ -4,9 +4,9 @@ jest.mock("@lerna/prompt"); const semver = require("semver"); const prompt = require("@lerna/prompt"); +const prereleaseIdFromVersion = require("@lerna/prerelease-id-from-version"); const makePromptVersion = require("../lib/prompt-version"); -const getExistingPreId = version => (semver.prerelease(version) || []).shift(); const resolvePrereleaseId = jest.fn(() => "alpha"); const versionPrompt = makePromptVersion(resolvePrereleaseId); @@ -130,7 +130,7 @@ describe("custom prerelease", () => { const version = "1.0.0"; const node = { version, - prereleaseId: getExistingPreId(version), + prereleaseId: prereleaseIdFromVersion(version), }; const defaultIdentifier = scopedResolveId(node.prereleaseId); const message = await versionPrompt(node); @@ -147,7 +147,7 @@ describe("custom prerelease", () => { const version = "1.0.1-beta.1"; const node = { version, - prereleaseId: getExistingPreId(version), + prereleaseId: prereleaseIdFromVersion(version), }; const defaultIdentifier = scopedResolveId(node.prereleaseId); const message = await versionPrompt(node); diff --git a/commands/version/__tests__/version-command.test.js b/commands/version/__tests__/version-command.test.js index c74f646de0..c8247e253d 100644 --- a/commands/version/__tests__/version-command.test.js +++ b/commands/version/__tests__/version-command.test.js @@ -86,6 +86,20 @@ describe("VersionCommand", () => { } }); + it("throws an error if conventional prerelease and graduate flags are both passed", async () => { + const testDir = await initFixture("normal"); + + try { + await lernaVersion(testDir)("--conventional-prerelease", "--conventional-graduate"); + } catch (err) { + expect(err.message).toMatchInlineSnapshot( + `"--conventional-prerelease cannot be combined with --conventional-graduate."` + ); + } + + expect.assertions(1); + }); + it("throws an error when remote branch doesn't exist", async () => { remoteBranchExists.mockReturnValueOnce(false); @@ -134,6 +148,14 @@ describe("VersionCommand", () => { expect.assertions(1); }); + it("calls `checkWorkingTree.throwIfUncommitted` when using --force-publish", async () => { + const testDir = await initFixture("normal"); + + await lernaVersion(testDir)("--force-publish"); + + expect(checkWorkingTree.throwIfUncommitted).toHaveBeenCalled(); + }); + it("only bumps changed packages when non-major version selected", async () => { const testDir = await initFixture("normal"); diff --git a/commands/version/__tests__/version-conventional-commits.test.js b/commands/version/__tests__/version-conventional-commits.test.js index fe8b697ccd..86df000613 100644 --- a/commands/version/__tests__/version-conventional-commits.test.js +++ b/commands/version/__tests__/version-conventional-commits.test.js @@ -31,11 +31,17 @@ describe("--conventional-commits", () => { ["package-5", "5.0.1"], ]); - beforeEach(() => { - versionBumps.forEach(bump => ConventionalCommitUtilities.recommendVersion.mockResolvedValueOnce(bump)); - }); + const prereleaseVersionBumps = new Map([ + ["package-1", "1.0.1-alpha.0"], + ["package-2", "2.1.0-alpha.0"], + ["package-3", "4.0.0-beta.0"], + ["package-4", "4.1.0-alpha.0"], + ["package-5", "5.0.1-alpha.0"], + ]); it("should use conventional-commits utility to guess version bump and generate CHANGELOG", async () => { + versionBumps.forEach(bump => ConventionalCommitUtilities.recommendVersion.mockResolvedValueOnce(bump)); + const cwd = await initFixture("independent"); await lernaVersion(cwd)("--conventional-commits"); @@ -47,8 +53,57 @@ describe("--conventional-commits", () => { expect(ConventionalCommitUtilities.recommendVersion).toHaveBeenCalledWith( expect.objectContaining({ name }), "independent", + { changelogPreset: undefined, rootPath: cwd, tagPrefix: "v", prereleaseId: undefined } + ); + expect(ConventionalCommitUtilities.updateChangelog).toHaveBeenCalledWith( + expect.objectContaining({ name, version }), + "independent", + { changelogPreset: undefined, rootPath: cwd, tagPrefix: "v", prereleaseId: undefined } + ); + }); + }); + + it("should guess prerelease version bumps and generate CHANGELOG", async () => { + prereleaseVersionBumps.forEach(bump => + ConventionalCommitUtilities.recommendVersion.mockResolvedValueOnce(bump) + ); + const cwd = await initFixture("prerelease-independent"); + + await lernaVersion(cwd)("--conventional-commits", "--conventional-prerelease"); + + const changedFiles = await showCommit(cwd, "--name-only"); + expect(changedFiles).toMatchSnapshot(); + + prereleaseVersionBumps.forEach((version, name) => { + const prereleaseId = semver.prerelease(version)[0]; + expect(ConventionalCommitUtilities.recommendVersion).toHaveBeenCalledWith( + expect.objectContaining({ name }), + "independent", + { changelogPreset: undefined, rootPath: cwd, tagPrefix: "v", prereleaseId } + ); + expect(ConventionalCommitUtilities.updateChangelog).toHaveBeenCalledWith( + expect.objectContaining({ name, version }), + "independent", { changelogPreset: undefined, rootPath: cwd, tagPrefix: "v" } ); + }); + }); + + it("should graduate prerelease version bumps and generate CHANGELOG", async () => { + versionBumps.forEach(bump => ConventionalCommitUtilities.recommendVersion.mockResolvedValueOnce(bump)); + const cwd = await initFixture("prerelease-independent"); + + await lernaVersion(cwd)("--conventional-commits", "--conventional-graduate"); + + const changedFiles = await showCommit(cwd, "--name-only"); + expect(changedFiles).toMatchSnapshot(); + + versionBumps.forEach((version, name) => { + expect(ConventionalCommitUtilities.recommendVersion).toHaveBeenCalledWith( + expect.objectContaining({ name }), + "independent", + { changelogPreset: undefined, rootPath: cwd, tagPrefix: "v", prerelease: undefined } + ); expect(ConventionalCommitUtilities.updateChangelog).toHaveBeenCalledWith( expect.objectContaining({ name, version }), "independent", @@ -59,7 +114,12 @@ describe("--conventional-commits", () => { it("accepts --changelog-preset option", async () => { const cwd = await initFixture("independent"); - const changelogOpts = { changelogPreset: "foo-bar", rootPath: cwd, tagPrefix: "v" }; + const changelogOpts = { + changelogPreset: "foo-bar", + rootPath: cwd, + tagPrefix: "v", + prereleaseId: undefined, + }; await lernaVersion(cwd)("--conventional-commits", "--changelog-preset", "foo-bar"); @@ -84,16 +144,14 @@ describe("--conventional-commits", () => { }); describe("fixed mode", () => { - beforeEach(() => { + it("should use conventional-commits utility to guess version bump and generate CHANGELOG", async () => { ConventionalCommitUtilities.recommendVersion .mockResolvedValueOnce("1.0.1") .mockResolvedValueOnce("1.1.0") .mockResolvedValueOnce("2.0.0") .mockResolvedValueOnce("1.1.0") .mockResolvedValueOnce("1.0.0"); - }); - it("should use conventional-commits utility to guess version bump and generate CHANGELOG", async () => { const cwd = await initFixture("normal"); await lernaVersion(cwd)("--conventional-commits"); @@ -107,12 +165,59 @@ describe("--conventional-commits", () => { expect(ConventionalCommitUtilities.recommendVersion).toHaveBeenCalledWith( expect.objectContaining({ name, location }), "fixed", - { changelogPreset: undefined, rootPath: cwd, tagPrefix: "v" } + { changelogPreset: undefined, rootPath: cwd, tagPrefix: "v", prereleaseId: undefined } ); expect(ConventionalCommitUtilities.updateChangelog).toHaveBeenCalledWith( expect.objectContaining({ name, version: "2.0.0" }), "fixed", + { changelogPreset: undefined, rootPath: cwd, tagPrefix: "v", prereleaseId: undefined } + ); + }); + + expect(ConventionalCommitUtilities.updateChangelog).toHaveBeenLastCalledWith( + expect.objectContaining({ + name: "normal", + location: cwd, + }), + "root", + { + changelogPreset: undefined, + rootPath: cwd, + tagPrefix: "v", + version: "2.0.0", + prereleaseId: undefined, + } + ); + }); + + it("should guess prerelease version bumps and generate CHANGELOG", async () => { + ConventionalCommitUtilities.recommendVersion + .mockResolvedValueOnce("1.0.1-alpha.0") + .mockResolvedValueOnce("1.1.0-alpha.0") + .mockResolvedValueOnce("2.0.0-alpha.0") + .mockResolvedValueOnce("1.1.0-alpha.0") + .mockResolvedValueOnce("1.0.0-alpha.0"); + + const cwd = await initFixture("normal"); + + await lernaVersion(cwd)("--conventional-commits", "--conventional-prerelease"); + + const changedFiles = await showCommit(cwd, "--name-only"); + expect(changedFiles).toMatchSnapshot(); + + ["package-1", "package-2", "package-3", "package-4", "package-5"].forEach(name => { + const location = path.join(cwd, "packages", name); + + expect(ConventionalCommitUtilities.recommendVersion).toHaveBeenCalledWith( + expect.objectContaining({ name, location }), + "fixed", + { changelogPreset: undefined, rootPath: cwd, tagPrefix: "v", prereleaseId: "alpha" } + ); + + expect(ConventionalCommitUtilities.updateChangelog).toHaveBeenCalledWith( + expect.objectContaining({ name, version: "2.0.0-alpha.0" }), + "fixed", { changelogPreset: undefined, rootPath: cwd, tagPrefix: "v" } ); }); @@ -123,7 +228,13 @@ describe("--conventional-commits", () => { location: cwd, }), "root", - { changelogPreset: undefined, rootPath: cwd, tagPrefix: "v", version: "2.0.0" } + { + changelogPreset: undefined, + rootPath: cwd, + tagPrefix: "v", + version: "2.0.0-alpha.0", + prereleaseId: undefined, + } ); }); @@ -133,6 +244,7 @@ describe("--conventional-commits", () => { changelogPreset: "baz-qux", rootPath: cwd, tagPrefix: "dragons-are-awesome", + prereleaseId: undefined, }; await lernaVersion(cwd)( diff --git a/commands/version/index.js b/commands/version/index.js index 01dcee77f7..ec23bedca2 100644 --- a/commands/version/index.js +++ b/commands/version/index.js @@ -20,6 +20,7 @@ const { createRunner } = require("@lerna/run-lifecycle"); const batchPackages = require("@lerna/batch-packages"); const ValidationError = require("@lerna/validation-error"); const { createGitHubClient, parseGitRepo } = require("@lerna/github-client"); +const prereleaseIdFromVersion = require("@lerna/prerelease-id-from-version"); const getCurrentBranch = require("./lib/get-current-branch"); const gitAdd = require("./lib/git-add"); @@ -32,6 +33,8 @@ const isBreakingChange = require("./lib/is-breaking-change"); const isAnythingCommitted = require("./lib/is-anything-committed"); const makePromptVersion = require("./lib/prompt-version"); +const { collectPackages, getPackagesForOption } = collectUpdates; + module.exports = factory; function factory(argv) { @@ -176,6 +179,15 @@ class VersionCommand extends Command { ); } + if (this.options.conventionalPrerelease && this.options.conventionalGraduate) { + throw new ValidationError( + "ENOTALLOWED", + dedent` + --conventional-prerelease cannot be combined with --conventional-graduate. + ` + ); + } + this.updates = collectUpdates( this.packageGraph.rawPackageList, this.packageGraph, @@ -232,7 +244,10 @@ class VersionCommand extends Command { // amending a commit probably means the working tree is dirty if (this.commitAndTag && this.gitOpts.amend !== true) { - tasks.unshift(() => checkWorkingTree(this.execOpts)); + const { forcePublish, conventionalCommits, conventionalGraduate } = this.options; + const checkUncommittedOnly = forcePublish || (conventionalCommits && conventionalGraduate); + const check = checkUncommittedOnly ? checkWorkingTree.throwIfUncommitted : checkWorkingTree; + tasks.unshift(() => check(this.execOpts)); } else { this.logger.warn("version", "Skipping working tree validation, proceed at your own risk"); } @@ -279,7 +294,6 @@ class VersionCommand extends Command { const repoVersion = bump ? semver.clean(bump) : ""; const increment = bump && !semver.valid(bump) ? bump : ""; - const getExistingPreId = version => (semver.prerelease(version) || []).shift(); const resolvePrereleaseId = existingPreid => preid || existingPreid || "alpha"; const makeGlobalVersionPredicate = nextVersion => { @@ -298,19 +312,19 @@ class VersionCommand extends Command { predicate = node => semver.inc(node.version, increment, resolvePrereleaseId(node.prereleaseId)); } else if (increment) { // compute potential prerelease ID once for all fixed updates - const prereleaseId = getExistingPreId(this.project.version); + const prereleaseId = prereleaseIdFromVersion(this.project.version); const nextVersion = semver.inc(this.project.version, increment, resolvePrereleaseId(prereleaseId)); predicate = makeGlobalVersionPredicate(nextVersion); } else if (conventionalCommits) { // it's a bit weird to have a return here, true - return this.recommendVersions(); + return this.recommendVersions(resolvePrereleaseId); } else if (independentVersions) { // prompt for each independent update with potential prerelease ID predicate = makePromptVersion(resolvePrereleaseId); } else { // prompt once with potential prerelease ID - const prereleaseId = getExistingPreId(this.project.version); + const prereleaseId = prereleaseIdFromVersion(this.project.version); const node = { version: this.project.version, prereleaseId }; predicate = makePromptVersion(resolvePrereleaseId); @@ -327,11 +341,29 @@ class VersionCommand extends Command { return pReduce(this.updates, iterator, new Map()); } - recommendVersions() { + getPrereleasePackageNames() { + const prereleasePackageNames = getPackagesForOption(this.options.conventionalPrerelease); + const isCandidate = prereleasePackageNames.has("*") + ? () => true + : (node, name) => prereleasePackageNames.has(name); + + return collectPackages(this.packageGraph, { isCandidate }).map(pkg => pkg.name); + } + + recommendVersions(resolvePrereleaseId) { const independentVersions = this.project.isIndependent(); - const { changelogPreset } = this.options; + const { changelogPreset, conventionalGraduate } = this.options; const rootPath = this.project.manifest.location; const type = independentVersions ? "independent" : "fixed"; + const prereleasePackageNames = this.getPrereleasePackageNames(); + const graduatePackageNames = Array.from(getPackagesForOption(conventionalGraduate)); + const shouldPrerelease = name => prereleasePackageNames && prereleasePackageNames.includes(name); + const shouldGraduate = name => graduatePackageNames.includes("*") || graduatePackageNames.includes(name); + const getPrereleaseId = node => { + if (!shouldGraduate(node.name) && (shouldPrerelease(node.name) || node.prereleaseId)) { + return resolvePrereleaseId(node.prereleaseId); + } + }; let chain = Promise.resolve(); @@ -345,6 +377,7 @@ class VersionCommand extends Command { changelogPreset, rootPath, tagPrefix: this.tagPrefix, + prereleaseId: getPrereleaseId(node), }) ) ); diff --git a/commands/version/package.json b/commands/version/package.json index 89c4f08960..6c381ccc64 100644 --- a/commands/version/package.json +++ b/commands/version/package.json @@ -42,6 +42,7 @@ "@lerna/conventional-commits": "file:../../core/conventional-commits", "@lerna/github-client": "file:../../utils/github-client", "@lerna/output": "file:../../utils/output", + "@lerna/prerelease-id-from-version": "file:../../utils/prerelease-id-from-version", "@lerna/prompt": "file:../../core/prompt", "@lerna/run-lifecycle": "file:../../utils/run-lifecycle", "@lerna/validation-error": "file:../../core/validation-error", diff --git a/core/conventional-commits/__tests__/conventional-commits.test.js b/core/conventional-commits/__tests__/conventional-commits.test.js index d9f56bf1f9..216849a911 100644 --- a/core/conventional-commits/__tests__/conventional-commits.test.js +++ b/core/conventional-commits/__tests__/conventional-commits.test.js @@ -31,6 +31,19 @@ describe("conventional-commits", () => { expect(bump).toBe("1.1.0"); }); + it("returns next version prerelease bump with prereleaseId", async () => { + const cwd = await initFixture("fixed"); + const [pkg1] = await getPackages(cwd); + + // make a change in package-1 + await pkg1.set("changed", 1).serialize(); + await gitAdd(cwd, pkg1.manifestLocation); + await gitCommit(cwd, "feat: changed 1"); + + const bump = await recommendVersion(pkg1, "fixed", { prereleaseId: "alpha" }); + expect(bump).toBe("1.1.0-alpha.0"); + }); + it("returns package-specific bumps in independent mode", async () => { const cwd = await initFixture("independent"); const [pkg1, pkg2] = await getPackages(cwd); @@ -54,6 +67,29 @@ describe("conventional-commits", () => { expect(bump2).toBe("1.1.0"); }); + it("returns package-specific prerelease bumps in independent mode with prereleaseId", async () => { + const cwd = await initFixture("independent"); + const [pkg1, pkg2] = await getPackages(cwd); + const opts = { changelogPreset: "angular" }; + + // make a change in package-1 and package-2 + await pkg1.set("changed", 1).serialize(); + await pkg2.set("changed", 2).serialize(); + + await gitAdd(cwd, pkg1.manifestLocation); + await gitCommit(cwd, "fix: changed 1"); + + await gitAdd(cwd, pkg2.manifestLocation); + await gitCommit(cwd, "feat: changed 2"); + + const [bump1, bump2] = await Promise.all([ + recommendVersion(pkg1, "independent", Object.assign(opts, { prereleaseId: "alpha" })), + recommendVersion(pkg2, "independent", Object.assign(opts, { prereleaseId: "beta" })), + ]); + expect(bump1).toBe("1.0.1-alpha.0"); + expect(bump2).toBe("1.1.0-beta.0"); + }); + it("falls back to patch bumps for non-bumping commit types", async () => { const cwd = await initFixture("independent"); const [pkg1, pkg2] = await getPackages(cwd); @@ -198,6 +234,56 @@ describe("conventional-commits", () => { expect.hasAssertions(); }); + + describe("prerelease bumps", () => { + let cwd; + let pkg; + let opts; + let recommend; + + beforeEach(async () => { + let value = 0; + cwd = await initFixture("independent"); + [pkg] = await getPackages(cwd); + opts = { changelogPreset: "angular" }; + recommend = async (commitMessage, { initVersion } = {}) => { + if (initVersion) { + await pkg.set("version", initVersion).serialize(); + await gitAdd(cwd, pkg.manifestLocation); + await gitCommit(cwd, commitMessage); + } + await pkg.set("changed", (value += 1)).serialize(); + await gitAdd(cwd, pkg.manifestLocation); + await gitCommit(cwd, commitMessage); + return recommendVersion(pkg, "independent", Object.assign(opts, { prereleaseId: "beta" })); + }; + }); + + it("stable + fix/minor/major => prepatch/preminor/premajor", async () => { + // default initial version is "1.0.0" + expect(await recommend("fix: changed")).toBe("1.0.1-beta.0"); + expect(await recommend("feat: changed")).toBe("1.1.0-beta.0"); + expect(await recommend("feat: changed\n\nBREAKING CHANGE: changed")).toBe("2.0.0-beta.0"); + }); + + it("prepatch + fix/minor/major => prerelease/preminor/premajor", async () => { + expect(await recommend("fix: changed", { initVersion: "1.0.1-beta.0" })).toBe("1.0.1-beta.1"); + expect(await recommend("feat: changed")).toBe("1.1.0-beta.0"); + expect(await recommend("feat: changed\n\nBREAKING CHANGE: changed")).toBe("2.0.0-beta.0"); + }); + + it("preminor + fix/minor/major => prerelease/prerelease/premajor", async () => { + expect(await recommend("fix: changed", { initVersion: "1.1.0-beta.0" })).toBe("1.1.0-beta.1"); + expect(await recommend("feat: changed")).toBe("1.1.0-beta.1"); + expect(await recommend("feat: changed\n\nBREAKING CHANGE: changed")).toBe("2.0.0-beta.0"); + }); + + it("premajor + fix/minor/major => prerelease", async () => { + expect(await recommend("fix: changed", { initVersion: "2.0.0-beta.0" })).toBe("2.0.0-beta.1"); + expect(await recommend("feat: changed")).toBe("2.0.0-beta.1"); + expect(await recommend("feat: changed\n\nBREAKING CHANGE: changed")).toBe("2.0.0-beta.1"); + }); + }); }); describe("updateChangelog()", () => { diff --git a/core/conventional-commits/lib/recommend-version.js b/core/conventional-commits/lib/recommend-version.js index c3a180cfd8..7274c5f469 100644 --- a/core/conventional-commits/lib/recommend-version.js +++ b/core/conventional-commits/lib/recommend-version.js @@ -7,7 +7,7 @@ const getChangelogConfig = require("./get-changelog-config"); module.exports = recommendVersion; -function recommendVersion(pkg, type, { changelogPreset, rootPath, tagPrefix }) { +function recommendVersion(pkg, type, { changelogPreset, rootPath, tagPrefix, prereleaseId }) { log.silly(type, "for %s at %s", pkg.name, pkg.location); const options = { @@ -21,6 +21,20 @@ function recommendVersion(pkg, type, { changelogPreset, rootPath, tagPrefix }) { options.tagPrefix = tagPrefix; } + const shouldBumpPrerelease = (releaseType, version) => { + if (!semver.prerelease(version)) { + return true; + } + switch (releaseType) { + case "major": + return semver.minor(version) !== 0 || semver.patch(version) !== 0; + case "minor": + return semver.patch(version) !== 0; + default: + return false; + } + }; + return getChangelogConfig(changelogPreset, rootPath).then(config => { // "new" preset API options.config = config; @@ -35,8 +49,15 @@ function recommendVersion(pkg, type, { changelogPreset, rootPath, tagPrefix }) { // we still need to bump _something_ because lerna saw a change here const releaseType = data.releaseType || "patch"; - log.verbose(type, "increment %s by %s", pkg.version, releaseType); - resolve(semver.inc(pkg.version, releaseType)); + if (prereleaseId) { + const shouldBump = shouldBumpPrerelease(releaseType, pkg.version); + const prereleaseType = shouldBump ? `pre${releaseType}` : "prerelease"; + log.verbose(type, "increment %s by %s", pkg.version, prereleaseType); + resolve(semver.inc(pkg.version, prereleaseType, prereleaseId)); + } else { + log.verbose(type, "increment %s by %s", pkg.version, releaseType); + resolve(semver.inc(pkg.version, releaseType)); + } }); }); }); diff --git a/core/package-graph/index.js b/core/package-graph/index.js index 6ae9aec686..c9ee9e9ba3 100644 --- a/core/package-graph/index.js +++ b/core/package-graph/index.js @@ -3,6 +3,7 @@ const npa = require("npm-package-arg"); const semver = require("semver"); const ValidationError = require("@lerna/validation-error"); +const prereleaseIdFromVersion = require("@lerna/prerelease-id-from-version"); /** * Represents a node in a PackageGraph. @@ -22,7 +23,7 @@ class PackageGraphNode { }, prereleaseId: { // an existing prerelease ID only matters at the beginning - value: (semver.prerelease(pkg.version) || []).shift(), + value: prereleaseIdFromVersion(pkg.version), }, // properties that might change over time version: { diff --git a/core/package-graph/package.json b/core/package-graph/package.json index d3b60bd0ed..a950ec2682 100644 --- a/core/package-graph/package.json +++ b/core/package-graph/package.json @@ -31,6 +31,7 @@ "test": "echo \"Run tests from root\" && exit 1" }, "dependencies": { + "@lerna/prerelease-id-from-version": "file:../../utils/prerelease-id-from-version", "@lerna/validation-error": "file:../validation-error", "npm-package-arg": "^6.1.0", "semver": "^5.5.0" diff --git a/integration/lerna-publish-conventional-fixed-prerelease.test.js b/integration/lerna-publish-conventional-fixed-prerelease.test.js new file mode 100644 index 0000000000..e5dcf56fab --- /dev/null +++ b/integration/lerna-publish-conventional-fixed-prerelease.test.js @@ -0,0 +1,434 @@ +"use strict"; + +const fs = require("fs-extra"); +const globby = require("globby"); +const path = require("path"); +const os = require("os"); + +const cliRunner = require("@lerna-test/cli-runner"); +const commitChangeToPackage = require("@lerna-test/commit-change-to-package"); +const gitTag = require("@lerna-test/git-tag"); +const cloneFixture = require("@lerna-test/clone-fixture")( + path.resolve(__dirname, "../commands/publish/__tests__") +); + +// stabilize changelog commit SHA and datestamp +expect.addSnapshotSerializer(require("@lerna-test/serialize-changelog")); + +const env = { + // never actually upload when calling `npm publish` + npm_config_dry_run: true, + // skip npm package validation, none of the stubs are real + LERNA_INTEGRATION: "SKIP", +}; + +describe(`lerna publish --conventional-prerelease/graduate fixed w/ changelog`, () => { + let cwd; + + beforeAll(async () => { + ({ cwd } = await cloneFixture("normal", "chore: init repo")); + await gitTag(cwd, "v1.0.0"); + }); + + test(`release specified stable packages as prerelease, ignoring specified packages`, async () => { + const args = [ + "publish", + "--conventional-commits", + "--conventional-prerelease=package-2,package-3", + "--yes", + ]; + await commitChangeToPackage(cwd, "package-1", "feat(package-1): Add foo", { foo: true }); + await commitChangeToPackage(cwd, "package-2", "fix(package-2): Fix bar", { bar: true }); + await commitChangeToPackage( + cwd, + "package-3", + `feat(package-3): Add baz feature${os.EOL}${os.EOL}BREAKING CHANGE: yup`, + { baz: true } + ); + + const { stdout } = await cliRunner(cwd, env)(...args); + expect(stdout).toMatchInlineSnapshot(` + +Changes: + - package-1: 1.0.0 => 2.0.0-alpha.0 + - package-2: 1.0.0 => 2.0.0-alpha.0 + - package-3: 1.0.0 => 2.0.0-alpha.0 + - package-5: 1.0.0 => 2.0.0-alpha.0 (private) + +Successfully published: + - package-1@2.0.0-alpha.0 + - package-2@2.0.0-alpha.0 + - package-3@2.0.0-alpha.0 +`); + }); + + test(`bump while maintaining current prerelease status`, async () => { + const args = ["publish", "--conventional-commits", "--yes"]; + await commitChangeToPackage(cwd, "package-1", "fix(package-1): Fix foo", { foo: false }); + await commitChangeToPackage(cwd, "package-2", "feat(package-2): Add baz", { baz: true }); + + const { stdout } = await cliRunner(cwd, env)(...args); + expect(stdout).toMatchInlineSnapshot(` + +Changes: + - package-1: 2.0.0-alpha.0 => 2.0.0-alpha.1 + - package-2: 2.0.0-alpha.0 => 2.0.0-alpha.1 + - package-3: 2.0.0-alpha.0 => 2.0.0-alpha.1 + - package-5: 2.0.0-alpha.0 => 2.0.0-alpha.1 (private) + +Successfully published: + - package-1@2.0.0-alpha.1 + - package-2@2.0.0-alpha.1 + - package-3@2.0.0-alpha.1 +`); + }); + + test(`graduate prerelease packages`, async () => { + const args = [ + "publish", + "--conventional-commits", + "--conventional-graduate=package-2,package-4", + "--yes", + ]; + await commitChangeToPackage(cwd, "package-1", "feat(package-1): Add baz", { baz: true }); + + const { stdout } = await cliRunner(cwd, env)(...args); + expect(stdout).toMatchInlineSnapshot(` + +Changes: + - package-1: 2.0.0-alpha.1 => 2.0.0 + - package-2: 2.0.0-alpha.1 => 2.0.0 + - package-3: 2.0.0-alpha.1 => 2.0.0 + - package-5: 2.0.0-alpha.1 => 2.0.0 (private) + +Successfully published: + - package-1@2.0.0 + - package-2@2.0.0 + - package-3@2.0.0 +`); + }); + + test(`graduate all prerelease packages with released HEAD`, async () => { + const args = ["publish", "--conventional-commits", "--conventional-prerelease", "--yes"]; + await commitChangeToPackage(cwd, "package-4", "fix(package-4): And another thing", { thing: true }); + await cliRunner(cwd, env)(...args); + + const graduateArgs = ["publish", "--conventional-commits", "--conventional-graduate", "--yes"]; + const { stdout } = await cliRunner(cwd, env)(...graduateArgs); + expect(stdout).toMatchInlineSnapshot(` + +Changes: + - package-4: 2.0.1-alpha.0 => 2.0.1 + +Successfully published: + - package-4@2.0.1 +`); + }); + + test(`generate accurate changelog`, async () => { + // ensure changelog header is not duplicated + const args = ["publish", "--conventional-commits", "--yes"]; + await commitChangeToPackage(cwd, "package-2", "fix(package-2): And another thing", { thing: true }); + await cliRunner(cwd, env)(...args); + + const changelogFilePaths = await globby(["**/CHANGELOG.md"], { + cwd, + absolute: true, + followSymlinkedDirectories: false, + }); + const [ + rootChangelog, + pkg1Changelog, + pkg2Changelog, + pkg3Changelog, + pkg4Changelog, + pkg5Changelog, + ] = await Promise.all(changelogFilePaths.sort().map(fp => fs.readFile(fp, "utf8"))); + + /** + * ./CHANGELOG.md + */ + expect(rootChangelog).toMatchInlineSnapshot(` +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [2.0.2](/compare/v2.0.1...v2.0.2) (YYYY-MM-DD) + + +### Bug Fixes + +* **package-2:** And another thing ([SHA](COMMIT_URL)) + + + + + +## [2.0.1](/compare/v2.0.1-alpha.0...v2.0.1) (YYYY-MM-DD) + +**Note:** Version bump only for package normal + + + + + +## [2.0.1-alpha.0](/compare/v2.0.0...v2.0.1-alpha.0) (YYYY-MM-DD) + + +### Bug Fixes + +* **package-4:** And another thing ([SHA](COMMIT_URL)) + + + + + +# [2.0.0](/compare/v2.0.0-alpha.1...v2.0.0) (YYYY-MM-DD) + + +### Features + +* **package-1:** Add baz ([SHA](COMMIT_URL)) + + + + + +# [2.0.0-alpha.1](/compare/v2.0.0-alpha.0...v2.0.0-alpha.1) (YYYY-MM-DD) + + +### Bug Fixes + +* **package-1:** Fix foo ([SHA](COMMIT_URL)) + + +### Features + +* **package-2:** Add baz ([SHA](COMMIT_URL)) + + + + + +# [2.0.0-alpha.0](/compare/v1.0.0...v2.0.0-alpha.0) (YYYY-MM-DD) + + +### Bug Fixes + +* **package-2:** Fix bar ([SHA](COMMIT_URL)) + + +### Features + +* **package-1:** Add foo ([SHA](COMMIT_URL)) +* **package-3:** Add baz feature ([SHA](COMMIT_URL)) + + +### BREAKING CHANGES + +* **package-3:** yup + +`); + + /** + * ./packages/package-1/CHANGELOG.md + */ + expect(pkg1Changelog).toMatchInlineSnapshot(` +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [2.0.0](/compare/v2.0.0-alpha.1...v2.0.0) (YYYY-MM-DD) + + +### Features + +* **package-1:** Add baz ([SHA](COMMIT_URL)) + + + + + +# [2.0.0-alpha.1](/compare/v2.0.0-alpha.0...v2.0.0-alpha.1) (YYYY-MM-DD) + + +### Bug Fixes + +* **package-1:** Fix foo ([SHA](COMMIT_URL)) + + + + + +# [2.0.0-alpha.0](/compare/v1.0.0...v2.0.0-alpha.0) (YYYY-MM-DD) + + +### Features + +* **package-1:** Add foo ([SHA](COMMIT_URL)) + +`); + + /** + * ./packages/package-2/CHANGELOG.md + */ + expect(pkg2Changelog).toMatchInlineSnapshot(` +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [2.0.2](/compare/v2.0.1...v2.0.2) (YYYY-MM-DD) + + +### Bug Fixes + +* **package-2:** And another thing ([SHA](COMMIT_URL)) + + + + + +# [2.0.0](/compare/v2.0.0-alpha.1...v2.0.0) (YYYY-MM-DD) + +**Note:** Version bump only for package package-2 + + + + + +# [2.0.0-alpha.1](/compare/v2.0.0-alpha.0...v2.0.0-alpha.1) (YYYY-MM-DD) + + +### Features + +* **package-2:** Add baz ([SHA](COMMIT_URL)) + + + + + +# [2.0.0-alpha.0](/compare/v1.0.0...v2.0.0-alpha.0) (YYYY-MM-DD) + + +### Bug Fixes + +* **package-2:** Fix bar ([SHA](COMMIT_URL)) + +`); + + /** + * ./packages/package-3/CHANGELOG.md + */ + expect(pkg3Changelog).toMatchInlineSnapshot(` +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [2.0.2](/compare/v2.0.1...v2.0.2) (YYYY-MM-DD) + +**Note:** Version bump only for package package-3 + + + + + +# [2.0.0](/compare/v2.0.0-alpha.1...v2.0.0) (YYYY-MM-DD) + +**Note:** Version bump only for package package-3 + + + + + +# [2.0.0-alpha.1](/compare/v2.0.0-alpha.0...v2.0.0-alpha.1) (YYYY-MM-DD) + +**Note:** Version bump only for package package-3 + + + + + +# [2.0.0-alpha.0](/compare/v1.0.0...v2.0.0-alpha.0) (YYYY-MM-DD) + + +### Features + +* **package-3:** Add baz feature ([SHA](COMMIT_URL)) + + +### BREAKING CHANGES + +* **package-3:** yup + +`); + + /** + * ./packages/package-4/CHANGELOG.md + */ + expect(pkg4Changelog).toMatchInlineSnapshot(` +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [2.0.1](/compare/v2.0.1-alpha.0...v2.0.1) (YYYY-MM-DD) + +**Note:** Version bump only for package package-4 + + + + + +## [2.0.1-alpha.0](/compare/v2.0.0...v2.0.1-alpha.0) (YYYY-MM-DD) + + +### Bug Fixes + +* **package-4:** And another thing ([SHA](COMMIT_URL)) + +`); + + /** + * ./packages/package-5/CHANGELOG.md + */ + expect(pkg5Changelog).toMatchInlineSnapshot(` +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [2.0.2](/compare/v2.0.1...v2.0.2) (YYYY-MM-DD) + +**Note:** Version bump only for package package-5 + + + + + +# [2.0.0](/compare/v2.0.0-alpha.1...v2.0.0) (YYYY-MM-DD) + +**Note:** Version bump only for package package-5 + + + + + +# [2.0.0-alpha.1](/compare/v2.0.0-alpha.0...v2.0.0-alpha.1) (YYYY-MM-DD) + +**Note:** Version bump only for package package-5 + + + + + +# [2.0.0-alpha.0](/compare/v1.0.0...v2.0.0-alpha.0) (YYYY-MM-DD) + +**Note:** Version bump only for package package-5 + +`); + }); +}); diff --git a/integration/lerna-publish-conventional-independent-prerelease.test.js b/integration/lerna-publish-conventional-independent-prerelease.test.js new file mode 100644 index 0000000000..99975cd848 --- /dev/null +++ b/integration/lerna-publish-conventional-independent-prerelease.test.js @@ -0,0 +1,393 @@ +"use strict"; + +const fs = require("fs-extra"); +const globby = require("globby"); +const path = require("path"); +const os = require("os"); + +const cliRunner = require("@lerna-test/cli-runner"); +const commitChangeToPackage = require("@lerna-test/commit-change-to-package"); +const gitTag = require("@lerna-test/git-tag"); +const cloneFixture = require("@lerna-test/clone-fixture")( + path.resolve(__dirname, "../commands/publish/__tests__") +); + +// stabilize changelog commit SHA and datestamp +expect.addSnapshotSerializer(require("@lerna-test/serialize-changelog")); + +const env = { + // never actually upload when calling `npm publish` + npm_config_dry_run: true, + // skip npm package validation, none of the stubs are real + LERNA_INTEGRATION: "SKIP", +}; + +describe(`lerna publish --conventional-prerelease/graduate independent w/ changelog`, () => { + let cwd; + + beforeAll(async () => { + ({ cwd } = await cloneFixture("independent", "chore: init repo")); + await gitTag(cwd, "v1.0.0"); + }); + + test(`release specified stable packages as prerelease`, async () => { + const args = [ + "publish", + "--conventional-commits", + "--conventional-prerelease=package-2,package-3", + "--yes", + ]; + await commitChangeToPackage(cwd, "package-1", "feat(package-1): Add foo", { foo: true }); + await commitChangeToPackage(cwd, "package-2", "fix(package-2): Fix bar", { bar: true }); + await commitChangeToPackage( + cwd, + "package-3", + `feat(package-3): Add baz feature${os.EOL}${os.EOL}BREAKING CHANGE: yup`, + { baz: true } + ); + + const { stdout } = await cliRunner(cwd, env)(...args); + expect(stdout).toMatchInlineSnapshot(` + +Changes: + - package-1: 1.0.0 => 1.1.0 + - package-2: 2.0.0 => 2.0.1-alpha.0 + - package-3: 3.0.0 => 4.0.0-alpha.0 + - package-5: 5.0.0 => 5.0.1-alpha.0 (private) + +Successfully published: + - package-1@1.1.0 + - package-2@2.0.1-alpha.0 + - package-3@4.0.0-alpha.0 +`); + }); + + test(`bump while maintaining current prerelease status`, async () => { + const args = ["publish", "--conventional-commits", "--yes"]; + await commitChangeToPackage(cwd, "package-1", "fix(package-1): Fix foo", { foo: false }); + await commitChangeToPackage(cwd, "package-2", "feat(package-2): Add baz", { baz: true }); + + const { stdout } = await cliRunner(cwd, env)(...args); + expect(stdout).toMatchInlineSnapshot(` + +Changes: + - package-1: 1.1.0 => 1.1.1 + - package-2: 2.0.1-alpha.0 => 2.1.0-alpha.0 + - package-3: 4.0.0-alpha.0 => 4.0.0-alpha.1 + - package-5: 5.0.1-alpha.0 => 5.0.1-alpha.1 (private) + +Successfully published: + - package-1@1.1.1 + - package-2@2.1.0-alpha.0 + - package-3@4.0.0-alpha.1 +`); + }); + + test(`release all changes as prerelease`, async () => { + const args = ["publish", "--conventional-commits", "--conventional-prerelease", "--yes"]; + await commitChangeToPackage(cwd, "package-1", "fix(package-1): Unfix foo", { foo: true }); + + const { stdout } = await cliRunner(cwd, env)(...args); + expect(stdout).toMatchInlineSnapshot(` + +Changes: + - package-1: 1.1.1 => 1.1.2-alpha.0 + - package-2: 2.1.0-alpha.0 => 2.1.0-alpha.1 + - package-3: 4.0.0-alpha.1 => 4.0.0-alpha.2 + - package-5: 5.0.1-alpha.1 => 5.0.1-alpha.2 (private) + +Successfully published: + - package-1@1.1.2-alpha.0 + - package-2@2.1.0-alpha.1 + - package-3@4.0.0-alpha.2 +`); + }); + + test(`graduate specific prerelease packages`, async () => { + const args = [ + "publish", + "--conventional-commits", + "--conventional-graduate=package-2,package-4", + "--yes", + ]; + await commitChangeToPackage(cwd, "package-1", "feat(package-1): Add baz", { baz: true }); + + const { stdout } = await cliRunner(cwd, env)(...args); + expect(stdout).toMatchInlineSnapshot(` + +Changes: + - package-1: 1.1.2-alpha.0 => 1.2.0-alpha.0 + - package-2: 2.1.0-alpha.1 => 2.1.0 + - package-3: 4.0.0-alpha.2 => 4.0.0-alpha.3 + - package-5: 5.0.1-alpha.2 => 5.0.1-alpha.3 (private) + +Successfully published: + - package-1@1.2.0-alpha.0 + - package-2@2.1.0 + - package-3@4.0.0-alpha.3 +`); + }); + + test(`graduate all prerelease packages with released HEAD`, async () => { + const args = ["publish", "--conventional-commits", "--conventional-graduate", "--yes"]; + const { stdout } = await cliRunner(cwd, env)(...args); + expect(stdout).toMatchInlineSnapshot(` + +Changes: + - package-1: 1.2.0-alpha.0 => 1.2.0 + - package-2: 2.1.0 => 2.1.1 + - package-3: 4.0.0-alpha.3 => 4.0.0 + - package-5: 5.0.1-alpha.3 => 5.0.1 (private) + +Successfully published: + - package-1@1.2.0 + - package-2@2.1.1 + - package-3@4.0.0 +`); + }); + + test(`generate accurate changelog`, async () => { + const changelogFilePaths = await globby(["**/CHANGELOG.md"], { + cwd, + absolute: true, + followSymlinkedDirectories: false, + }); + const [ + // no root changelog + pkg1Changelog, + pkg2Changelog, + pkg3Changelog, + pkg5Changelog, + ] = await Promise.all(changelogFilePaths.sort().map(fp => fs.readFile(fp, "utf8"))); + + /** + * ./packages/package-1/CHANGELOG.md + */ + expect(pkg1Changelog).toMatchInlineSnapshot(` +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [1.2.0](/compare/package-1@1.2.0-alpha.0...package-1@1.2.0) (YYYY-MM-DD) + +**Note:** Version bump only for package package-1 + + + + + +# [1.2.0-alpha.0](/compare/package-1@1.1.2-alpha.0...package-1@1.2.0-alpha.0) (YYYY-MM-DD) + + +### Features + +* **package-1:** Add baz ([SHA](COMMIT_URL)) + + + + + +## [1.1.2-alpha.0](/compare/package-1@1.1.1...package-1@1.1.2-alpha.0) (YYYY-MM-DD) + + +### Bug Fixes + +* **package-1:** Unfix foo ([SHA](COMMIT_URL)) + + + + + +## [1.1.1](/compare/package-1@1.1.0...package-1@1.1.1) (YYYY-MM-DD) + + +### Bug Fixes + +* **package-1:** Fix foo ([SHA](COMMIT_URL)) + + + + + +# 1.1.0 (YYYY-MM-DD) + + +### Features + +* **package-1:** Add foo ([SHA](COMMIT_URL)) + + + +# 1.0.0 (YYYY-MM-DD) + +`); + + /** + * ./packages/package-2/CHANGELOG.md + */ + expect(pkg2Changelog).toMatchInlineSnapshot(` +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [2.1.1](/compare/package-2@2.1.0...package-2@2.1.1) (YYYY-MM-DD) + +**Note:** Version bump only for package package-2 + + + + + +# [2.1.0](/compare/package-2@2.1.0-alpha.1...package-2@2.1.0) (YYYY-MM-DD) + +**Note:** Version bump only for package package-2 + + + + + +# [2.1.0-alpha.1](/compare/package-2@2.1.0-alpha.0...package-2@2.1.0-alpha.1) (YYYY-MM-DD) + +**Note:** Version bump only for package package-2 + + + + + +# [2.1.0-alpha.0](/compare/package-2@2.0.1-alpha.0...package-2@2.1.0-alpha.0) (YYYY-MM-DD) + + +### Features + +* **package-2:** Add baz ([SHA](COMMIT_URL)) + + + + + +## 2.0.1-alpha.0 (YYYY-MM-DD) + + +### Bug Fixes + +* **package-2:** Fix bar ([SHA](COMMIT_URL)) + + + +# 1.0.0 (YYYY-MM-DD) + +`); + + /** + * ./packages/package-3/CHANGELOG.md + */ + expect(pkg3Changelog).toMatchInlineSnapshot(` +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [4.0.0](/compare/package-3@4.0.0-alpha.3...package-3@4.0.0) (YYYY-MM-DD) + +**Note:** Version bump only for package package-3 + + + + + +# [4.0.0-alpha.3](/compare/package-3@4.0.0-alpha.2...package-3@4.0.0-alpha.3) (YYYY-MM-DD) + +**Note:** Version bump only for package package-3 + + + + + +# [4.0.0-alpha.2](/compare/package-3@4.0.0-alpha.1...package-3@4.0.0-alpha.2) (YYYY-MM-DD) + +**Note:** Version bump only for package package-3 + + + + + +# [4.0.0-alpha.1](/compare/package-3@4.0.0-alpha.0...package-3@4.0.0-alpha.1) (YYYY-MM-DD) + +**Note:** Version bump only for package package-3 + + + + + +# 4.0.0-alpha.0 (YYYY-MM-DD) + + +### Features + +* **package-3:** Add baz feature ([SHA](COMMIT_URL)) + + +### BREAKING CHANGES + +* **package-3:** yup + + + +# 1.0.0 (YYYY-MM-DD) + +`); + + /** + * ./packages/package-5/CHANGELOG.md + */ + expect(pkg5Changelog).toMatchInlineSnapshot(` +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [5.0.1](/compare/package-5@5.0.1-alpha.3...package-5@5.0.1) (YYYY-MM-DD) + +**Note:** Version bump only for package package-5 + + + + + +## [5.0.1-alpha.3](/compare/package-5@5.0.1-alpha.2...package-5@5.0.1-alpha.3) (YYYY-MM-DD) + +**Note:** Version bump only for package package-5 + + + + + +## [5.0.1-alpha.2](/compare/package-5@5.0.1-alpha.1...package-5@5.0.1-alpha.2) (YYYY-MM-DD) + +**Note:** Version bump only for package package-5 + + + + + +## [5.0.1-alpha.1](/compare/package-5@5.0.1-alpha.0...package-5@5.0.1-alpha.1) (YYYY-MM-DD) + +**Note:** Version bump only for package package-5 + + + + + +## 5.0.1-alpha.0 (YYYY-MM-DD) + + + +# 1.0.0 (YYYY-MM-DD) + +**Note:** Version bump only for package package-5 + +`); + }); +}); diff --git a/package-lock.json b/package-lock.json index 007597d85a..055d720456 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1056,11 +1056,18 @@ "@lerna/package-graph": { "version": "file:core/package-graph", "requires": { + "@lerna/prerelease-id-from-version": "file:utils/prerelease-id-from-version", "@lerna/validation-error": "file:core/validation-error", "npm-package-arg": "^6.1.0", "semver": "^5.5.0" } }, + "@lerna/prerelease-id-from-version": { + "version": "file:utils/prerelease-id-from-version", + "requires": { + "semver": "^5.5.0" + } + }, "@lerna/project": { "version": "file:core/project", "requires": { @@ -1100,6 +1107,7 @@ "@lerna/npm-publish": "file:utils/npm-publish", "@lerna/output": "file:utils/output", "@lerna/pack-directory": "file:utils/pack-directory", + "@lerna/prerelease-id-from-version": "file:utils/prerelease-id-from-version", "@lerna/prompt": "file:core/prompt", "@lerna/pulse-till-done": "file:utils/pulse-till-done", "@lerna/run-lifecycle": "file:utils/run-lifecycle", @@ -1214,6 +1222,7 @@ "@lerna/conventional-commits": "file:core/conventional-commits", "@lerna/github-client": "file:utils/github-client", "@lerna/output": "file:utils/output", + "@lerna/prerelease-id-from-version": "file:utils/prerelease-id-from-version", "@lerna/prompt": "file:core/prompt", "@lerna/run-lifecycle": "file:utils/run-lifecycle", "@lerna/validation-error": "file:core/validation-error", @@ -3996,6 +4005,27 @@ "os-tmpdir": "^1.0.0" } }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", + "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==" + }, "path-is-absolute": { "version": "1.0.1", "bundled": true, @@ -4137,33 +4167,6 @@ "safe-buffer": "^5.1.2", "yallist": "^3.0.2" } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.0.3", - "bundled": true, - "dev": true, - "optional": true } } }, diff --git a/utils/collect-updates/__helpers__/build-graph.js b/utils/collect-updates/__helpers__/build-graph.js index b27f72d69b..db75e806c8 100644 --- a/utils/collect-updates/__helpers__/build-graph.js +++ b/utils/collect-updates/__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/utils/collect-updates/__tests__/collect-updates.test.js b/utils/collect-updates/__tests__/collect-updates.test.js index 63452e79c3..10b222a74a 100644 --- a/utils/collect-updates/__tests__/collect-updates.test.js +++ b/utils/collect-updates/__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/utils/collect-updates/__tests__/lib-collect-packages.test.js b/utils/collect-updates/__tests__/lib-collect-packages.test.js new file mode 100644 index 0000000000..9fa6980647 --- /dev/null +++ b/utils/collect-updates/__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/utils/collect-updates/__tests__/lib-get-forced-packages.test.js b/utils/collect-updates/__tests__/lib-get-forced-packages.test.js deleted file mode 100644 index ee00258041..0000000000 --- a/utils/collect-updates/__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/utils/collect-updates/__tests__/lib-get-packages-for-option.test.js b/utils/collect-updates/__tests__/lib-get-packages-for-option.test.js new file mode 100644 index 0000000000..601376cbb0 --- /dev/null +++ b/utils/collect-updates/__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/utils/collect-updates/collect-updates.js b/utils/collect-updates/collect-updates.js index 65aa5d6e16..7cc327aea3 100644 --- a/utils/collect-updates/collect-updates.js +++ b/utils/collect-updates/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/utils/collect-updates/lib/collect-packages.js b/utils/collect-updates/lib/collect-packages.js new file mode 100644 index 0000000000..1317a74fa6 --- /dev/null +++ b/utils/collect-updates/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/utils/collect-updates/lib/get-forced-packages.js b/utils/collect-updates/lib/get-forced-packages.js deleted file mode 100644 index 98f503b03c..0000000000 --- a/utils/collect-updates/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/utils/collect-updates/lib/get-packages-for-option.js b/utils/collect-updates/lib/get-packages-for-option.js new file mode 100644 index 0000000000..16ad959421 --- /dev/null +++ b/utils/collect-updates/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); +} diff --git a/utils/prerelease-id-from-version/README.md b/utils/prerelease-id-from-version/README.md new file mode 100644 index 0000000000..9d3a14c73b --- /dev/null +++ b/utils/prerelease-id-from-version/README.md @@ -0,0 +1,17 @@ +# `@lerna/prerelease-id-from-version` + +> Get the prerelease ID from a version string + +## Usage + +```js +const prereleaseIdFromVersion = require("@lerna/prerelease-id-from-version"); + +prereleaseIdFromVersion(1.0.0-alpha.0); // => "alpha" + +prereleaseIdFromVersion(1.0.0); // => undefined + +prereleaseIdFromVersion(); // => undefined +``` + +Install [lerna](https://www.npmjs.com/package/lerna) for access to the `lerna` CLI. diff --git a/utils/prerelease-id-from-version/__tests__/prerelease-id-from-version.test.js b/utils/prerelease-id-from-version/__tests__/prerelease-id-from-version.test.js new file mode 100644 index 0000000000..8386a96fde --- /dev/null +++ b/utils/prerelease-id-from-version/__tests__/prerelease-id-from-version.test.js @@ -0,0 +1,17 @@ +"use strict"; + +const prereleaseIdFromVersion = require("../lib/prerelease-id-from-version"); + +test("prereleaseIdFromVersion() returns prerelease identifier", () => { + expect(prereleaseIdFromVersion("1.0.0-alpha.0")).toBe("alpha"); +}); + +test("prereleaseIdFromVersion() returns undefined for non-prerelease versions", () => { + expect(prereleaseIdFromVersion("1.0.0")).toBe(undefined); +}); + +test("prereleaseIdFromVersion() returns undefined for invalid version argument", () => { + expect(prereleaseIdFromVersion()).toBe(undefined); + expect(prereleaseIdFromVersion({})).toBe(undefined); + expect(prereleaseIdFromVersion("foo")).toBe(undefined); +}); diff --git a/utils/prerelease-id-from-version/lib/prerelease-id-from-version.js b/utils/prerelease-id-from-version/lib/prerelease-id-from-version.js new file mode 100644 index 0000000000..ca38a579e4 --- /dev/null +++ b/utils/prerelease-id-from-version/lib/prerelease-id-from-version.js @@ -0,0 +1,9 @@ +"use strict"; + +const semver = require("semver"); + +module.exports = prereleaseIdFromVersion; + +function prereleaseIdFromVersion(version) { + return (semver.prerelease(version) || []).shift(); +} diff --git a/utils/prerelease-id-from-version/package.json b/utils/prerelease-id-from-version/package.json new file mode 100644 index 0000000000..2a1498d432 --- /dev/null +++ b/utils/prerelease-id-from-version/package.json @@ -0,0 +1,33 @@ +{ + "name": "@lerna/prerelease-id-from-version", + "version": "3.13.0", + "description": "Get the prerelease ID from a version string", + "keywords": [ + "lerna", + "utils" + ], + "author": "Daniel Stockman ", + "homepage": "https://github.com/lerna/lerna/tree/master/utils/prerelease-id-from-version#readme", + "license": "MIT", + "main": "lib/prerelease-id-from-version.js", + "files": [ + "lib" + ], + "engines": { + "node": ">= 6.9.0" + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/lerna/lerna.git", + "directory": "utils/prerelease-id-from-version" + }, + "scripts": { + "test": "echo \"Error: run tests from root\" && exit 1" + }, + "dependencies": { + "semver": "^5.5.0" + } +}