diff --git a/.gitignore b/.gitignore index 35884956f..7e95d3c2b 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ npm-debug.log /.settings /.idea /.vscode + +# coverage +coverage diff --git a/README.md b/README.md index 18323c175..cf17b76c2 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,40 @@ As long as your git commit messages are conventional and accurate, you no longer After you cut a release, you can push the new git tag and `npm publish` (or `npm publish --tag next`) when you're ready. +### Release as a pre-release + +Use `--prerelease`, you can generate a pre-release. + +Suppose the last version of your code is `1.0.0`, and your code to be committed has patched changes. Run +```bash +# npm run script +npm run release -- --prerelease +``` +you will get version `1.0.1-0`. + +If you want to name the pre-release, you specify the name via `--prerelease `. + +For example, the wanted name is `alpha` +. Use the example above: +```bash +# npm run script +npm run release -- --prerelease alpha +``` +you will get version `1.0.1-alpha.0` + +### Release as a target type imperatively like `npm version` + +You can use `--release-as` to generate a `major`, `minor` or `patch` release imperatively. + +Suppose the last version of your code is `1.0.0`, and your code to be committed has patched changes. Run +```bash +# npm run script +npm run release -- --release-as minor +``` +you will get version `1.1.0` rather than the smartly generated version `1.0.1`. + +**NOTE:** you can combine `--release-as` and `--prerelease` to generate a release. That's useful when publishing experimental feature(s). + ### Prevent Git Hooks If you use git hooks, like pre-commit, to test your code before committing, you can prevent hooks from being verified during the commit step by passing the `--no-verify` option: diff --git a/bin/cli.js b/bin/cli.js new file mode 100755 index 000000000..c80a2e129 --- /dev/null +++ b/bin/cli.js @@ -0,0 +1,9 @@ +#!/usr/bin/env node +var standardVersion = require('../index') +var cmdParser = require('../command') + +standardVersion(cmdParser.argv, function (err) { + if (err) { + process.exit(1) + } +}) diff --git a/cli.js b/command.js similarity index 75% rename from cli.js rename to command.js index f1cbe3088..e6fcdd443 100755 --- a/cli.js +++ b/command.js @@ -1,9 +1,21 @@ -#!/usr/bin/env node -var standardVersion = require('./index') var defaults = require('./defaults') -var argv = require('yargs') +module.exports = require('yargs') .usage('Usage: $0 [options]') + .option('release-as', { + alias: 'r', + describe: 'Specify the release type manually. like npm version xxx with limited choices', + requiresArg: true, + string: true, + choices: ['major', 'minor', 'patch'], + global: true + }) + .option('prerelease', { + alias: 'p', + describe: 'make a pre-release with optional option value to specify a tag id', + string: true, + global: true + }) .option('infile', { alias: 'i', describe: 'Read the CHANGELOG from this file', @@ -51,15 +63,10 @@ var argv = require('yargs') default: defaults.silent, global: true }) + .version() + .alias('version', 'v') .help() .alias('help', 'h') .example('$0', 'Update changelog and tag release') .example('$0 -m "%s: see changelog for details"', 'Update changelog and tag release with custom commit message') .wrap(97) - .argv - -standardVersion(argv, function (err) { - if (err) { - process.exit(1) - } -}) diff --git a/index.js b/index.js index 1b57b7837..a619c9ccb 100755 --- a/index.js +++ b/index.js @@ -16,40 +16,121 @@ module.exports = function standardVersion (argv, done) { var pkg = require(pkgPath) var defaults = require('./defaults') - argv = objectAssign(defaults, argv) + var args = objectAssign({}, defaults, argv) - conventionalRecommendedBump({ - preset: 'angular' - }, function (err, release) { + bumpVersion(args.releaseAs, function (err, release) { if (err) { - printError(argv, err.message) + printError(args, err.message) return done(err) } var newVersion = pkg.version - if (!argv.firstRelease) { - newVersion = semver.inc(pkg.version, release.releaseType) - checkpoint(argv, 'bumping version in package.json from %s to %s', [pkg.version, newVersion]) + + if (!args.firstRelease) { + var releaseType = getReleaseType(args.prerelease, release.releaseType, pkg.version) + newVersion = semver.inc(pkg.version, releaseType, args.prerelease) + + checkpoint(args, 'bumping version in package.json from %s to %s', [pkg.version, newVersion]) + pkg.version = newVersion fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8') } else { - checkpoint(argv, 'skip version bump on first release', [], chalk.red(figures.cross)) + checkpoint(args, 'skip version bump on first release', [], chalk.red(figures.cross)) } - outputChangelog(argv, function (err) { + outputChangelog(args, function (err) { if (err) { return done(err) } - commit(argv, newVersion, function (err) { + commit(args, newVersion, function (err) { if (err) { return done(err) } - return tag(newVersion, pkg.private, argv, done) + return tag(newVersion, pkg.private, args, done) }) }) }) } +function getReleaseType (prerelease, expectedReleaseType, currentVersion) { + if (isString(prerelease)) { + if (isInPrerelease(currentVersion)) { + if (shouldContinuePrerelease(currentVersion, expectedReleaseType) || + getTypePriority(getCurrentActiveType(currentVersion)) > getTypePriority(expectedReleaseType) + ) { + return 'prerelease' + } + } + + return 'pre' + expectedReleaseType + } else { + return expectedReleaseType + } +} + +function isString (val) { + return typeof val === 'string' +} + +/** + * if a version is currently in pre-release state, + * and if it current in-pre-release type is same as expect type, + * it should continue the pre-release with the same type + * + * @param version + * @param expectType + * @return {boolean} + */ +function shouldContinuePrerelease (version, expectType) { + return getCurrentActiveType(version) === expectType +} + +function isInPrerelease (version) { + return Array.isArray(semver.prerelease(version)) +} + +var TypeList = ['major', 'minor', 'patch'].reverse() + +/** + * extract the in-pre-release type in target version + * + * @param version + * @return {string} + */ +function getCurrentActiveType (version) { + var typelist = TypeList + for (var i = 0; i < typelist.length; i++) { + if (semver[typelist[i]](version)) { + return typelist[i] + } + } +} + +/** + * calculate the priority of release type, + * major - 2, minor - 1, patch - 0 + * + * @param type + * @return {number} + */ +function getTypePriority (type) { + return TypeList.indexOf(type) +} + +function bumpVersion (releaseAs, callback) { + if (releaseAs) { + callback(null, { + releaseType: releaseAs + }) + } else { + conventionalRecommendedBump({ + preset: 'angular' + }, function (err, release) { + callback(err, release) + }) + } +} + function outputChangelog (argv, cb) { createIfMissing(argv) var header = '# Change Log\n\nAll notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.\n' diff --git a/package.json b/package.json index 66462cb2b..626133b45 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,12 @@ "name": "standard-version", "version": "3.0.0", "description": "replacement for `npm version` with automatic CHANGELOG generation", - "bin": "cli.js", + "bin": "bin/cli.js", "scripts": { "pretest": "standard", "coverage": "nyc report --reporter=text-lcov | coveralls", "test": "nyc mocha --timeout=20000 test.js", - "release": "./cli.js" + "release": "bin/cli.js" }, "repository": { "type": "git", @@ -43,6 +43,7 @@ "yargs": "^6.0.0" }, "devDependencies": { + "bluebird": "^3.4.6", "chai": "^3.5.0", "coveralls": "^2.11.9", "mocha": "^3.1.0", diff --git a/test.js b/test.js index 51a72205b..f7cb8cd76 100644 --- a/test.js +++ b/test.js @@ -9,10 +9,14 @@ var path = require('path') var stream = require('stream') var mockGit = require('mock-git') var mockery = require('mockery') +var semver = require('semver') +var Promise = require('bluebird') +var cli = require('./command') +var standardVersion = require('./index') var should = require('chai').should() -var cliPath = path.resolve(__dirname, './cli.js') +var cliPath = path.resolve(__dirname, './bin/cli.js') function branch (branch) { shell.exec('git branch ' + branch) @@ -34,10 +38,15 @@ function execCli (argString) { return shell.exec('node ' + cliPath + (argString != null ? ' ' + argString : '')) } +function execCliAsync (argString) { + return Promise.promisify(standardVersion)(cli.parse('standard-version ' + argString + ' --silent')) +} + function writePackageJson (version, option) { option = option || {} var pkg = objectAssign(option, {version: version}) fs.writeFileSync('package.json', JSON.stringify(pkg), 'utf-8') + delete require.cache[require.resolve(path.join(process.cwd(), 'package.json'))] } function writeGitPreCommitHook () { @@ -60,6 +69,10 @@ function finishTemp () { shell.rm('-rf', 'tmp') } +function getPackageVersion () { + return JSON.parse(fs.readFileSync('package.json', 'utf-8')).version +} + describe('cli', function () { beforeEach(initInTempFolder) afterEach(finishTemp) @@ -119,9 +132,9 @@ describe('cli', function () { execCli('--commit-all').code.should.equal(0) var content = fs.readFileSync('CHANGELOG.md', 'utf-8') - var status = shell.exec('git status') + var status = shell.exec('git status --porcelain') // see http://unix.stackexchange.com/questions/155046/determine-if-git-working-directory-is-clean-from-a-script - status.should.match(/On branch master\nnothing to commit, working (directory|tree) clean\n/) + status.should.equal('') status.should.not.match(/STUFF.md/) content.should.match(/1\.0\.1/) @@ -197,6 +210,126 @@ describe('cli', function () { }) }) + describe('pre-release', function () { + it('works fine without specifying a tag id when prereleasing', function () { + writePackageJson('1.0.0') + fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8') + + commit('feat: first commit') + return execCliAsync('--prerelease') + .then(function () { + // it's a feature commit, so it's minor type + getPackageVersion().should.equal('1.1.0-0') + }) + }) + }) + + describe('manual-release', function () { + it('throws error when not specifying a release type', function () { + writePackageJson('1.0.0') + fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8') + + commit('fix: first commit') + execCli('--release-as').code.should.above(0) + }) + + describe('release-types', function () { + var regularTypes = ['major', 'minor', 'patch'] + + regularTypes.forEach(function (type) { + it('creates a ' + type + ' release', function () { + var originVer = '1.0.0' + writePackageJson(originVer) + fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8') + + commit('fix: first commit') + + return execCliAsync('--release-as ' + type) + .then(function () { + var version = { + major: semver.major(originVer), + minor: semver.minor(originVer), + patch: semver.patch(originVer) + } + + version[type] += 1 + + getPackageVersion().should.equal(version.major + '.' + version.minor + '.' + version.patch) + }) + }) + }) + + // this is for pre-releases + regularTypes.forEach(function (type) { + it('creates a pre' + type + ' release', function () { + var originVer = '1.0.0' + writePackageJson(originVer) + fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8') + + commit('fix: first commit') + + return execCliAsync('--release-as ' + type + ' --prerelease ' + type) + .then(function () { + var version = { + major: semver.major(originVer), + minor: semver.minor(originVer), + patch: semver.patch(originVer) + } + + version[type] += 1 + + getPackageVersion().should.equal(version.major + '.' + version.minor + '.' + version.patch + '-' + type + '.0') + }) + }) + }) + }) + + it('creates a prerelease with a new minor version after two prerelease patches', function () { + writePackageJson('1.0.0') + fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8') + + commit('fix: first patch') + return execCliAsync('--release-as patch --prerelease dev') + .then(function () { + getPackageVersion().should.equal('1.0.1-dev.0') + }) + + // second + .then(function () { + commit('fix: second patch') + return execCliAsync('--prerelease dev') + }) + .then(function () { + getPackageVersion().should.equal('1.0.1-dev.1') + }) + + // third + .then(function () { + commit('feat: first new feat') + return execCliAsync('--release-as minor --prerelease dev') + }) + .then(function () { + getPackageVersion().should.equal('1.1.0-dev.0') + }) + + .then(function () { + commit('fix: third patch') + return execCliAsync('--release-as minor --prerelease dev') + }) + .then(function () { + getPackageVersion().should.equal('1.1.0-dev.1') + }) + + .then(function () { + commit('fix: forth patch') + return execCliAsync('--prerelease dev') + }) + .then(function () { + getPackageVersion().should.equal('1.1.0-dev.2') + }) + }) + }) + it('handles commit messages longer than 80 characters', function () { commit('feat: first commit') shell.exec('git tag -a v1.0.0 -m "my awesome first release"')