From 7c65038bf7577d2ca0d53d1608c68e7bdf5ff1e7 Mon Sep 17 00:00:00 2001 From: Joe Bottigliero Date: Mon, 3 Jun 2019 20:39:11 -0500 Subject: [PATCH 1/8] feat: custom 'bumpFiles' and 'packageFiles' support --- command.js | 8 ++++++ defaults.js | 13 +++++++++ index.js | 7 +++-- lib/lifecycles/bump.js | 61 ++++++++++++++++++++++++------------------ test.js | 22 +++++++++++++++ test/mocks/mix.exs | 12 +++++++++ 6 files changed, 93 insertions(+), 30 deletions(-) create mode 100644 test/mocks/mix.exs diff --git a/command.js b/command.js index 1674aec74..95d15825a 100755 --- a/command.js +++ b/command.js @@ -5,6 +5,14 @@ const { START_OF_LAST_RELEASE_PATTERN } = require('./lib/lifecycles/changelog') const yargs = require('yargs') .usage('Usage: $0 [options]') + .option('packageFiles', { + default: defaults.packageFiles, + array: true + }) + .option('bumpFiles', { + default: defaults.bumpFiles, + array: true + }) .option('release-as', { alias: 'r', describe: 'Specify the release type manually (like npm version )', diff --git a/defaults.js b/defaults.js index 202b8f4d4..65ffc61d9 100644 --- a/defaults.js +++ b/defaults.js @@ -23,4 +23,17 @@ Object.keys(spec.properties).forEach(propertyKey => { defaults[propertyKey] = property.default }) +defaults.packageFiles = [ + 'package.json', + 'bower.json', + 'manifest.json', + 'composer.json' +] + +defaults.bumpFiles = defaults.packageFiles.concat([ + 'package-lock.json', + 'npm-shrinkwrap.json', + 'composer.lock' +]) + module.exports = defaults diff --git a/index.js b/index.js index 00dbadd8b..6a6253684 100755 --- a/index.js +++ b/index.js @@ -8,6 +8,7 @@ const printError = require('./lib/print-error') const tag = require('./lib/lifecycles/tag') module.exports = function standardVersion (argv) { + let defaults = require('./defaults') /** * `--message` (`-m`) support will be removed in the next major version. */ @@ -24,8 +25,9 @@ module.exports = function standardVersion (argv) { } } + const args = Object.assign({}, defaults, argv) let pkg - bump.pkgFiles.forEach((filename) => { + args.packageFiles.forEach((filename) => { if (pkg) return const pkgPath = path.resolve(process.cwd(), filename) try { @@ -34,9 +36,6 @@ module.exports = function standardVersion (argv) { } catch (err) {} }) let newVersion - const defaults = require('./defaults') - const args = Object.assign({}, defaults, argv) - return Promise.resolve() .then(() => { if (!pkg && args.gitTagFallback) { diff --git a/lib/lifecycles/bump.js b/lib/lifecycles/bump.js index 86bb77455..fdd4f4fd1 100644 --- a/lib/lifecycles/bump.js +++ b/lib/lifecycles/bump.js @@ -51,19 +51,6 @@ Bump.getUpdatedConfigs = function () { return configsToUpdate } -Bump.pkgFiles = [ - 'package.json', - 'bower.json', - 'manifest.json', - 'composer.json' -] - -Bump.lockFiles = [ - 'package-lock.json', - 'npm-shrinkwrap.json', - 'composer.lock' -] - function getReleaseType (prerelease, expectedReleaseType, currentVersion) { if (isString(prerelease)) { if (isInPrerelease(currentVersion)) { @@ -154,31 +141,53 @@ function bumpVersion (releaseAs, currentVersion, args) { } /** - * attempt to update the version # in a collection of common config - * files, e.g., package.json, bower.json. - * + * attempt to update the version number in provided `bumpFiles` * @param args config object - * @param newVersion version # to update to. - * @return {string} + * @param newVersion version number to update to. + * @return void */ function updateConfigs (args, newVersion) { const dotgit = DotGitignore() - Bump.pkgFiles.concat(Bump.lockFiles).forEach(function (filename) { - const configPath = path.resolve(process.cwd(), filename) + args.bumpFiles.forEach(function (bumpFile) { + if (typeof bumpFile !== 'object') { + bumpFile = { + filename: bumpFile + } + } + + const configPath = path.resolve(process.cwd(), bumpFile.filename) + try { if (dotgit.ignore(configPath)) return const stat = fs.lstatSync(configPath) if (stat.isFile()) { - const data = fs.readFileSync(configPath, 'utf8') + let data = fs.readFileSync(configPath, 'utf8') const indent = detectIndent(data).indent const newline = detectNewline(data) - const config = JSON.parse(data) - checkpoint(args, 'bumping version in ' + filename + ' from %s to %s', [config.version, newVersion]) - config.version = newVersion - writeFile(args, configPath, stringifyPackage(config, indent, newline)) + + let config + if (!bumpFile.replacer) { + config = JSON.parse(data) + checkpoint(args, 'bumping version in ' + bumpFile.filename + ' from %s to %s', [config.version, newVersion]) + config.version = newVersion + writeFile(args, configPath, stringifyPackage(config, indent, newline)) + } else { + let matches + let replacement = false + while ( + (matches = bumpFile.replacer.exec(data)) !== null && + matches[0] !== replacement + ) { + if (!replacement) { + replacement = matches[0].replace(matches[1], newVersion) + } + data = data.replace(matches[1], newVersion) + } + writeFile(args, configPath, data) + } // flag any config files that we modify the version # for // as having been updated. - configsToUpdate[filename] = true + configsToUpdate[bumpFile.filename] = true } } catch (err) { if (err.code !== 'ENOENT') console.warn(err.message) diff --git a/test.js b/test.js index 278091671..4ea057593 100644 --- a/test.js +++ b/test.js @@ -922,6 +922,28 @@ describe('standard-version', function () { }) }) + describe('custom `bumpFiles` support', function () { + it('mix.exs', function () { + // @todo This file path is relative to the `tmp` directory, which is a little confusing + fs.copyFileSync('../test/mocks/mix.exs', 'mix.exs') + commit('feat: first commit') + shell.exec('git tag -a v1.0.0 -m "my awesome first release"') + commit('feat: new feature!') + return require('./index')({ + silent: true, + bumpFiles: [ + { + filename: 'mix.exs', + replacer: /version: "(.*)"/ + } + ] + }) + .then(() => { + fs.readFileSync('mix.exs', 'utf-8').should.contain('version: "1.1.0"') + }) + }) + }) + describe('npm-shrinkwrap.json support', function () { beforeEach(function () { writeNpmShrinkwrapJson('1.0.0') diff --git a/test/mocks/mix.exs b/test/mocks/mix.exs new file mode 100644 index 000000000..c48db130c --- /dev/null +++ b/test/mocks/mix.exs @@ -0,0 +1,12 @@ +defmodule StandardVersion.MixProject do + use Mix.Project + def project do + [ + app: :standard_version + version: "0.0.1", + elixir: "~> 1.8", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end +end \ No newline at end of file From e694d91570c86ce2ebb34b9c9adf610025a87969 Mon Sep 17 00:00:00 2001 From: Joe Bottigliero Date: Mon, 10 Jun 2019 21:59:12 -0500 Subject: [PATCH 2/8] imp: package-files/updaters support --- lib/lifecycles/bump.js | 70 ++++++++++++++------------ lib/package-files/json.js | 15 ++++++ lib/package-files/version.txt.js | 7 +++ test.js | 8 ++- test/mocks/updater/customer-updater.js | 12 +++++ test/mocks/version.txt | 0 6 files changed, 77 insertions(+), 35 deletions(-) create mode 100644 lib/package-files/json.js create mode 100644 lib/package-files/version.txt.js create mode 100644 test/mocks/updater/customer-updater.js create mode 100644 test/mocks/version.txt diff --git a/lib/lifecycles/bump.js b/lib/lifecycles/bump.js index fdd4f4fd1..e51612832 100644 --- a/lib/lifecycles/bump.js +++ b/lib/lifecycles/bump.js @@ -3,8 +3,6 @@ const chalk = require('chalk') const checkpoint = require('../checkpoint') const conventionalRecommendedBump = require('conventional-recommended-bump') -const detectIndent = require('detect-indent') -const detectNewline = require('detect-newline') const figures = require('figures') const fs = require('fs') const DotGitignore = require('dotgitignore') @@ -12,8 +10,8 @@ const path = require('path') const presetLoader = require('../preset-loader') const runLifecycleScript = require('../run-lifecycle-script') const semver = require('semver') -const stringifyPackage = require('stringify-package') const writeFile = require('../write-file') +const JSON_BUMP_FILES = require('../../defaults').bumpFiles let configsToUpdate = {} @@ -140,6 +138,19 @@ function bumpVersion (releaseAs, currentVersion, args) { }) } +function getUpdaterByFileName (filename) { + if (JSON_BUMP_FILES.includes(filename)) { + return require('../package-files/json') + } + try { + return require(`../package-files/${filename}.js`) + } catch (err) { + throw Error( + `Unsupported file (${filename}) provided for updating – please provide a custom updater.` + ) + } +} + /** * attempt to update the version number in provided `bumpFiles` * @param args config object @@ -154,41 +165,34 @@ function updateConfigs (args, newVersion) { filename: bumpFile } } - const configPath = path.resolve(process.cwd(), bumpFile.filename) - try { if (dotgit.ignore(configPath)) return const stat = fs.lstatSync(configPath) - if (stat.isFile()) { - let data = fs.readFileSync(configPath, 'utf8') - const indent = detectIndent(data).indent - const newline = detectNewline(data) - - let config - if (!bumpFile.replacer) { - config = JSON.parse(data) - checkpoint(args, 'bumping version in ' + bumpFile.filename + ' from %s to %s', [config.version, newVersion]) - config.version = newVersion - writeFile(args, configPath, stringifyPackage(config, indent, newline)) - } else { - let matches - let replacement = false - while ( - (matches = bumpFile.replacer.exec(data)) !== null && - matches[0] !== replacement - ) { - if (!replacement) { - replacement = matches[0].replace(matches[1], newVersion) - } - data = data.replace(matches[1], newVersion) - } - writeFile(args, configPath, data) - } - // flag any config files that we modify the version # for - // as having been updated. - configsToUpdate[bumpFile.filename] = true + + if (!stat.isFile()) return + + if (!bumpFile.updater) { + bumpFile.updater = getUpdaterByFileName(bumpFile.filename) + } else { + bumpFile.updater = require( + path.resolve(process.cwd(), bumpFile.updater) + ) } + const contents = fs.readFileSync(configPath, 'utf8') + checkpoint( + args, + 'bumping version in ' + bumpFile.filename + ' from %s to %s', + [bumpFile.updater.readVersion(contents), newVersion] + ) + writeFile( + args, + configPath, + bumpFile.updater.writeVersion(contents, newVersion) + ) + // flag any config files that we modify the version # for + // as having been updated. + configsToUpdate[bumpFile.filename] = true } catch (err) { if (err.code !== 'ENOENT') console.warn(err.message) } diff --git a/lib/package-files/json.js b/lib/package-files/json.js new file mode 100644 index 000000000..dbe5b6f3f --- /dev/null +++ b/lib/package-files/json.js @@ -0,0 +1,15 @@ +const stringifyPackage = require('stringify-package') +const detectIndent = require('detect-indent') +const detectNewline = require('detect-newline') + +module.exports.readVersion = function (contents) { + return JSON.parse(contents).version +} + +module.exports.writeVersion = function (contents, version) { + const json = JSON.parse(contents) + let indent = detectIndent(contents).indent + let newline = detectNewline(contents) + json.version = version + return stringifyPackage(json, indent, newline) +} diff --git a/lib/package-files/version.txt.js b/lib/package-files/version.txt.js new file mode 100644 index 000000000..18bcabed2 --- /dev/null +++ b/lib/package-files/version.txt.js @@ -0,0 +1,7 @@ +module.exports.readVersion = function (contents) { + return contents +} + +module.exports.writeVersion = function (_contents, version) { + return version +} diff --git a/test.js b/test.js index 4ea057593..312909a6f 100644 --- a/test.js +++ b/test.js @@ -923,23 +923,27 @@ describe('standard-version', function () { }) describe('custom `bumpFiles` support', function () { - it('mix.exs', function () { + it('mix.exs + version.txt', function () { // @todo This file path is relative to the `tmp` directory, which is a little confusing fs.copyFileSync('../test/mocks/mix.exs', 'mix.exs') + fs.copyFileSync('../test/mocks/version.txt', 'version.txt') + fs.copyFileSync('../test/mocks/updater/customer-updater.js', 'custom-updater.js') commit('feat: first commit') shell.exec('git tag -a v1.0.0 -m "my awesome first release"') commit('feat: new feature!') return require('./index')({ silent: true, bumpFiles: [ + 'version.txt', { filename: 'mix.exs', - replacer: /version: "(.*)"/ + updater: 'custom-updater.js' } ] }) .then(() => { fs.readFileSync('mix.exs', 'utf-8').should.contain('version: "1.1.0"') + fs.readFileSync('version.txt', 'utf-8').should.equal('1.1.0') }) }) }) diff --git a/test/mocks/updater/customer-updater.js b/test/mocks/updater/customer-updater.js new file mode 100644 index 000000000..989aba3c0 --- /dev/null +++ b/test/mocks/updater/customer-updater.js @@ -0,0 +1,12 @@ +const REPLACER = /version: "(.*)"/ + +module.exports.readVersion = function (contents) { + return REPLACER.exec(contents)[1] +} + +module.exports.writeVersion = function (contents, version) { + return contents.replace( + REPLACER.exec(contents)[0], + `version: "${version}"` + ) +} diff --git a/test/mocks/version.txt b/test/mocks/version.txt new file mode 100644 index 000000000..e69de29bb From ac1cc8b1aab2d6e4ded3b404db8a2b5918d27a64 Mon Sep 17 00:00:00 2001 From: Joe Bottigliero Date: Tue, 6 Aug 2019 21:56:58 -0500 Subject: [PATCH 3/8] docs: updates README, slight change to the API --- README.md | 222 +++++++++++------- lib/lifecycles/bump.js | 26 +- lib/updaters/index.js | 29 +++ lib/{package-files => updaters/types}/json.js | 0 .../types/plain-text.js} | 0 test.js | 17 ++ test/mocks/VERSION-1.0.0.txt | 1 + test/mocks/mix.exs | 24 +- 8 files changed, 213 insertions(+), 106 deletions(-) create mode 100644 lib/updaters/index.js rename lib/{package-files => updaters/types}/json.js (100%) rename lib/{package-files/version.txt.js => updaters/types/plain-text.js} (100%) create mode 100644 test/mocks/VERSION-1.0.0.txt diff --git a/README.md b/README.md index e614ff20e..4dc8d8557 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,48 @@ # Standard Version +A utility for versioning using [semver](https://semver.org/) and CHANGELOG generation powered by [Conventional Commits](https://conventionalcommits.org). + [![Build Status](https://travis-ci.org/conventional-changelog/standard-version.svg?branch=master)](https://travis-ci.org/conventional-changelog/standard-version) [![NPM version](https://img.shields.io/npm/v/standard-version.svg)](https://www.npmjs.com/package/standard-version) [![Coverage Status](https://coveralls.io/repos/conventional-changelog/standard-version/badge.svg?branch=)](https://coveralls.io/r/conventional-changelog/standard-version?branch=master) [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org) -[![community slack](http://devtoolscommunity.herokuapp.com/badge.svg)](http://devtoolscommunity.herokuapp.com) +[![Community slack](http://devtoolscommunity.herokuapp.com/badge.svg)](http://devtoolscommunity.herokuapp.com) + +_Having problems? Want to contribute? Join us on the [node-tooling community Slack](http://devtoolscommunity.herokuapp.com)_. + + +_How It Works:_ -_Having problems? want to contribute? join our [community slack](http://devtoolscommunity.herokuapp.com)_. +1. Follow the [Conventional Commits Specification](https://conventionalcommits.org) in your repository. +2. When you're ready to release, run `standard-version`. +`standard-version` will then do the following: -Automate versioning and CHANGELOG generation, with [semver](https://semver.org/) and -[conventional commit messages](https://conventionalcommits.org). +1. Retreive the current version of your repository by looking at `bumpFiles`[1](), falling back to the last `git tag`. +2. `bump` the version in `bumpFiles`[1]() based on your commits. +4. Generates a `changelog` based on your commints (uses [conventional-changelog](https://github.com/conventional-changelog/conventional-changelog) under the hood). +5. Creates a new `commit` including your `bumpFiles`[1]() and updated CHANGELOG. +6. Creates a new `tag` with the new version number. -_how it works:_ -1. when you land commits on your `master` branch, select the _Squash and Merge_ option. -2. add a title and body that follows the [Conventional Commits Specification](https://conventionalcommits.org). -3. when you're ready to release: - 1. `git checkout master; git pull origin master` - 2. run `standard-version` - 3. `git push --follow-tags origin master && npm publish` - _(or, `docker push`, `gem push`, etc.)_ +### `bumpFiles`, `packageFiles` and `updaters` -`standard-version` does the following: +`standard-version` uses a few key concepts for handling version bumping in your project. -1. bumps the version in metadata files (package.json, composer.json, etc). -2. uses [conventional-changelog](https://github.com/conventional-changelog/conventional-changelog) to update _CHANGELOG.md_ -3. commits _package.json (et al.)_ and _CHANGELOG.md_ -4. tags a new release +- **`packageFiles`** – User-defined files where versions can be read from _and_ "bumped". + - Examples: `package.json`, `manifest.json` + - In most cases (including the default), `packageFiles` are a subset of `bumpFiles`. +- **`bumpFiles`** – User-defined files where versions should be "bumped", but not explicitly read from. + - Examples: `package-lock.json`, `npm-shrinkwrap.json` +- **`updaters`** – Simple modules used for reading `packageFiles` and writing to `bumpFiles`. -## Installation +By default, `standard-version` assumes you're working in a NodeJS based project... because of this, for the majority of projects you might never need to interact with these options. -### As `npm run` script +That said, if you find your self asking ["How can I use `standard-version` for additional metadata files, languages or version files?"](#how-can-I-use-standard-version-for-additional metadata-files-languages-or-version-files) – these configuration options will help! + +## Installing `standard-version` + +### As a local `npm run` script Install and add to `devDependencies`: @@ -39,7 +50,7 @@ Install and add to `devDependencies`: npm i --save-dev standard-version ``` -Add an [`npm run` script](https://docs.npmjs.com/cli/run-script) to your _package.json_: +Add an [`npm run` script](https://docs.npmjs.com/cli/run-script) to your `package.json`: ```json { @@ -53,7 +64,7 @@ Now you can use `npm run release` in place of `npm version`. This has the benefit of making your repo/package more portable, so that other developers can cut releases without having to globally install `standard-version` on their machine. -### As global bin +### As global `bin` Install globally (add to your `PATH`): @@ -65,6 +76,12 @@ Now you can use `standard-version` in place of `npm version`. This has the benefit of allowing you to use `standard-version` on any repo/package without adding a dev dependency to each one. +### Using `npx` + +As of `npm@5.2.0`, `npx` is installed alongside `npm`. Using `npx` you can use `standard-version` without having to keep a `package.json` file by running: `npx standard-version`. + +This method is especially useful when using `standard-version` in non-JavaScript projects. + ## Configuration You can configure `standard-version` either by: @@ -80,15 +97,15 @@ be provided via configuration. Please refer to the [conventional-changelog-confi ### Customizing CHANGELOG Generation -By default, `standard-version` uses the [conventionalcommits preset](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-conventionalcommits). +By default (as of `6.0.0`), `standard-version` uses the [conventionalcommits preset](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-conventionalcommits). This preset: -* adheres closely to the [conventionalcommits.org](https://www.conventionalcommits.org) +* Adheres closely to the [conventionalcommits.org](https://www.conventionalcommits.org) specification. -* is highly configurable, following the configuration specification +* Is highly configurable, following the configuration specification [maintained here](https://github.com/conventional-changelog/conventional-changelog-config-spec). - * _we've documented these config settings as a recommendation to other tooling makers._ + * _We've documented these config settings as a recommendation to other tooling makers._ There are a variety of dials and knobs you can turn related to CHANGELOG generation. @@ -111,15 +128,17 @@ To generate your changelog for your first release, simply do: ```sh # npm run script npm run release -- --first-release -# or global bin +# global bin standard-version --first-release +# npx +npx standard-version --first-release ``` -This will tag a release **without bumping the version in package.json (_et al._)**. +This will tag a release **without bumping the version `bumpFiles`[1]()**. -When ready, push the git tag and `npm publish` your first release. \o/ +When you are ready, push the git tag and `npm publish` your first release. \o/ -### Cut a Release +### Cutting Releases If you typically use `npm version` to cut a new release, do this instead: @@ -134,7 +153,7 @@ 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 +### Release as a Pre-Release Use the flag `--prerelease` to generate pre-releases: @@ -144,7 +163,7 @@ Suppose the last version of your code is `1.0.0`, and your code to be committed # npm run script npm run release -- --prerelease ``` -you will get version `1.0.1-0`. +This will tag your version as: `1.0.1-0`. If you want to name the pre-release, you specify the name via `--prerelease `. @@ -155,14 +174,14 @@ For example, suppose your pre-release should contain the `alpha` prefix: npm run release -- --prerelease alpha ``` -this will tag the version `1.0.1-alpha.0` +This will tag the version as: `1.0.1-alpha.0` -### Release as a target type imperatively like `npm version` +### Release as a Target Type Imperatively (`npm version`-like) -To forgo the automated version bump use `--release-as` with the argument `major`, `minor` or `patch`: +To forgo the automated version bump use `--release-as` with the argument `major`, `minor` or `patch`. Suppose the last version of your code is `1.0.0`, you've only landed `fix:` commits, but -you would like your next release to be a `minor`. Simply do: +you would like your next release to be a `minor`. Simply run the following: ```bash # npm run script @@ -171,7 +190,7 @@ npm run release -- --release-as minor npm run release -- --release-as 1.1.0 ``` -you will get version `1.1.0` rather than the auto generated version `1.0.1`. +you will get version `1.1.0` rather than what would be the auto-generated version `1.0.1`. > **NOTE:** you can combine `--release-as` and `--prerelease` to generate a release. This is useful when publishing experimental feature(s). @@ -186,11 +205,11 @@ npm run release -- --no-verify standard-version --no-verify ``` -### Signing commits and tags +### Signing Commits and Tags If you have your GPG key set up, add the `--sign` or `-s` flag to your `standard-version` command. -### Lifecycle scripts +### Lifecycle Scripts `standard-version` supports lifecycle scripts. These allow you to execute your own supplementary commands during the release. The following @@ -231,7 +250,7 @@ with a link to your Jira - assuming you have already installed [replace](https:/ } ``` -### Skipping lifecycle steps +### Skipping Lifecycle Steps You can skip any of the lifecycle steps (`bump`, `changelog`, `commit`, `tag`), by adding the following to your package.json: @@ -246,7 +265,7 @@ by adding the following to your package.json: } ``` -### Committing generated artifacts in the release commit +### Committing Generated Artifacts in the Release Commit If you want to commit generated artifacts in the release commit (e.g. [#96](https://github.com/conventional-changelog/standard-version/issues/96)), you can use the `--commit-all` or `-a` flag. You will need to stage the artifacts you want to commit, so your `release` command could look like this: @@ -255,7 +274,7 @@ If you want to commit generated artifacts in the release commit (e.g. [#96](http "release": "git add && standard-version -a" ``` -### Dry run mode +### Dry Run Mode running `standard-version` with the flag `--dry-run` allows you to see what commands would be run, without committing to git or updating files. @@ -288,10 +307,7 @@ npm run release -- --help standard-version --help ``` -## Code usage - -Use the `silent` option to stop `standard-version` from printing anything -to the console. +## Code Usage ```js const standardVersion = require('standard-version') @@ -309,69 +325,111 @@ standardVersion({ }) ``` -## Commit Message Convention, at a Glance +_TIP: Use the `silent` option to prevent `standard-version` from printing to the `console`._ -_patches:_ +## FAQ -```sh -git commit -a -m "fix(parsing): fixed a bug in our parser" -``` +### How is `standard-version` different from `semantic-release`? -_features:_ +[`semantic-release`](https://github.com/semantic-release/semantic-release) is described as: -```sh -git commit -a -m "feat(parser): we now have a parser \o/" -``` +> semantic-release automates the whole package release workflow including: determining the next version number, generating the release notes and publishing the package. -_breaking changes:_ +While both are based on the same foundation of structured commit messages, `standard-version` takes a different approach by handling versioning, changelog generation, and git tagging for you **without** automatic pushing (to GitHub) or publishing (to an npm registry). Use of `standard-version` only affects your local git repo - it doesn't affect remote resources at all. After you run `standard-version`, you can review your release state, correct mistakes and follow the release strategy that makes the most sense for your codebase. -```sh -git commit -a -m "feat(new-parser): introduces a new parsing library -BREAKING CHANGE: new library does not support foo-construct" -``` +We think they are both fantastic tools, and we encourage folks to use `semantic-release` instead of `standard-version` if it makes sense for their use-case. -_other changes:_ +### Should I always squash commits when merging PRs? -You decide, e.g., docs, chore, etc. +The instructions to squash commits when merging pull requests assumes that **one PR equals, at most, one feature or fix**. -```sh -git commit -a -m "docs: fixed up the docs a bit" -``` +If you have multiple features or fixes landing in a single PR and each commit uses a structured message, then you can do a standard merge when accepting the PR. This will preserve the commit history from your branch after the merge. -_but wait, there's more!_ +Although this will allow each commit to be included as separate entries in your CHANGELOG, the entries will **not** be able to reference the PR that pulled the changes in because the preserved commit messages do not include the PR number. -Github usernames (`@bcoe`) and issue references (#133) will be swapped out for the -appropriate URLs in your CHANGELOG. +For this reason, we recommend keeping the scope of each PR to one general feature or fix. In practice, this allows you to use unstructured commit messages when committing each little change and then squash them into a single commit with a structured message (referencing the PR number) once they have been reviewed and accepted. -## Badges! +### Can I use `standard-version` for additional metadata files, languages or version files? -Tell your users that you adhere to the Conventional Commits specification: +YES! Using `bumpFiles` (and `packageFiles`) configurations you should be able to configure `standard-version` to work for you. -```markdown -[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org) +1. Specify a custom `bumpFile` "`file`", this is the path to the file you want to "bump" +2. Specify the `bumpFile` "`updater`", this is _how_ the file will be bumped. + + a. If your using a common type, you can use one of `standard-version`'s built-in `updaters` by specifying a `type`. + + b. If your using an less-common version file, you can create your own `updater`. + +```json +// .versionrc +{ + "bumpFiles": [ + { + "file": "MY_VERSION_TRACKER.txt", + // The `plain-text` updater assumes the file contents represents the version. + "type": "plain-text" + }, + { + "file": "a/deep/package/dot/json/file/package.json", + // The `json` updater assumes the version is available under a `version` key in the provided JSON document. + "type": "json" + } + { + "file": "VERSION_TRACKER.json", + // See "Custom `updater`s" for more details. + "updater": "standard-version-updater.js" + } + ] +} ``` -## FAQ +#### Custom `updater`s -### How is `standard-version` different from `semantic-release`? +An `updater` is expected to be a Javascript module with _atleast_ two methods exposed: `readVersion` and `writeVersion`. -[`semantic-release`](https://github.com/semantic-release/semantic-release) is described as: +##### `readVersion(contents = string): string` -> semantic-release automates the whole package release workflow including: determining the next version number, generating the release notes and publishing the package. +This method is used to read the version from the provided file contents. -While both are based on the same foundation of structured commit messages, `standard-version` takes a different approach by handling versioning, changelog generation, and git tagging for you **without** automatic pushing (to GitHub) or publishing (to an npm registry). Use of `standard-version` only affects your local git repo - it doesn't affect remote resources at all. After you run `standard-version`, you can review your release state, correct mistakes and follow the release strategy that makes the most sense for your codebase. +##### `writeVersion(contents = string, version: string): string` -We think they are both fantastic tools, and we encourage folks to use `semantic-release` instead of `standard-version` if it makes sense for their use-case. +This method is used to write the version to the provided contents. -### Should I always squash commits when merging PRs? +--- -The instructions to squash commits when merging pull requests assumes that **one PR equals, at most, one feature or fix**. +Let's assume our `VERSION_TRACKER.json` has the following contents: -If you have multiple features or fixes landing in a single PR and each commit uses a structured message, then you can do a standard merge when accepting the PR. This will preserve the commit history from your branch after the merge. +```json +{ + "tracker": { + "package": { + "version": "1.0.0" + } + } +} -Although this will allow each commit to be included as separate entries in your CHANGELOG, the entries will **not** be able to reference the PR that pulled the changes in because the preserved commit messages do not include the PR number. +``` -For this reason, we recommend keeping the scope of each PR to one general feature or fix. In practice, this allows you to use unstructured commit messages when committing each little change and then squash them into a single commit with a structured message (referencing the PR number) once they have been reviewed and accepted. +An acceptable `standard-version-updater.js` would be: + +```js +// standard-version-updater.js +const stringifyPackage = require('stringify-package') +const detectIndent = require('detect-indent') +const detectNewline = require('detect-newline') + +module.exports.readVersion = function (contents) { + return JSON.parse(contents).tracker.package.version; +} + +module.exports.writeVersion = function (contents, version) { + const json = JSON.parse(contents) + let indent = detectIndent(contents).indent + let newline = detectNewline(contents) + json.tracker.package.version = version + return stringifyPackage(json, indent, newline) +} +``` ## License diff --git a/lib/lifecycles/bump.js b/lib/lifecycles/bump.js index e51612832..05b8f8b35 100644 --- a/lib/lifecycles/bump.js +++ b/lib/lifecycles/bump.js @@ -11,8 +11,7 @@ const presetLoader = require('../preset-loader') const runLifecycleScript = require('../run-lifecycle-script') const semver = require('semver') const writeFile = require('../write-file') -const JSON_BUMP_FILES = require('../../defaults').bumpFiles - +const { getUpdaterByFilename, getUpdaterByType, getCustomUpdater } = require('../updaters') let configsToUpdate = {} function Bump (args, version) { @@ -138,19 +137,6 @@ function bumpVersion (releaseAs, currentVersion, args) { }) } -function getUpdaterByFileName (filename) { - if (JSON_BUMP_FILES.includes(filename)) { - return require('../package-files/json') - } - try { - return require(`../package-files/${filename}.js`) - } catch (err) { - throw Error( - `Unsupported file (${filename}) provided for updating – please provide a custom updater.` - ) - } -} - /** * attempt to update the version number in provided `bumpFiles` * @param args config object @@ -172,12 +158,12 @@ function updateConfigs (args, newVersion) { if (!stat.isFile()) return - if (!bumpFile.updater) { - bumpFile.updater = getUpdaterByFileName(bumpFile.filename) + if (bumpFile.updater) { + bumpFile.updater = getCustomUpdater(bumpFile.updater) + } else if (bumpFile.type) { + bumpFile.updater = getUpdaterByType(bumpFile.type) } else { - bumpFile.updater = require( - path.resolve(process.cwd(), bumpFile.updater) - ) + bumpFile.updater = getUpdaterByFilename(bumpFile.filename) } const contents = fs.readFileSync(configPath, 'utf8') checkpoint( diff --git a/lib/updaters/index.js b/lib/updaters/index.js new file mode 100644 index 000000000..a49fc23de --- /dev/null +++ b/lib/updaters/index.js @@ -0,0 +1,29 @@ +const path = require('path') +const JSON_BUMP_FILES = require('../../defaults').bumpFiles +const PLAIN_TEXT_BUMP_FILES = ['VERSION.txt', 'version.txt'] + +function getUpdaterByType (type) { + try { + return require(`./types/${type}`) + } catch (e) { + throw Error(`Unable to locate updated for provided type (${type}).`) + } +} + +module.exports.getUpdaterByType = getUpdaterByType + +module.exports.getUpdaterByFilename = function (filename) { + if (JSON_BUMP_FILES.includes(filename)) { + return getUpdaterByType('json') + } + if (PLAIN_TEXT_BUMP_FILES.includes(filename)) { + return getUpdaterByType('plain-text') + } + throw Error( + `Unsupported file (${filename}) provided for bumping.\n Please specifcy the updater \`type\` or use a custom \`updater\`.` + ) +} + +module.exports.getCustomUpdater = function (updater) { + return require(path.resolve(process.cwd(), updater)) +} diff --git a/lib/package-files/json.js b/lib/updaters/types/json.js similarity index 100% rename from lib/package-files/json.js rename to lib/updaters/types/json.js diff --git a/lib/package-files/version.txt.js b/lib/updaters/types/plain-text.js similarity index 100% rename from lib/package-files/version.txt.js rename to lib/updaters/types/plain-text.js diff --git a/test.js b/test.js index 312909a6f..5ee90b102 100644 --- a/test.js +++ b/test.js @@ -946,6 +946,23 @@ describe('standard-version', function () { fs.readFileSync('version.txt', 'utf-8').should.equal('1.1.0') }) }) + + it('bumps a custom `plain-text` file', function () { + fs.copyFileSync('../test/mocks/VERSION-1.0.0.txt', 'VERSION_TRACKER.txt') + commit('feat: first commit') + return require('./index')({ + silent: true, + bumpFiles: [ + { + filename: 'VERSION_TRACKER.txt', + type: 'plain-text' + } + ] + }) + .then(() => { + fs.readFileSync('VERSION_TRACKER.txt', 'utf-8').should.equal('1.1.0') + }) + }) }) describe('npm-shrinkwrap.json support', function () { diff --git a/test/mocks/VERSION-1.0.0.txt b/test/mocks/VERSION-1.0.0.txt new file mode 100644 index 000000000..afaf360d3 --- /dev/null +++ b/test/mocks/VERSION-1.0.0.txt @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file diff --git a/test/mocks/mix.exs b/test/mocks/mix.exs index c48db130c..bc539135e 100644 --- a/test/mocks/mix.exs +++ b/test/mocks/mix.exs @@ -1,12 +1,28 @@ defmodule StandardVersion.MixProject do use Mix.Project + def project do [ - app: :standard_version - version: "0.0.1", - elixir: "~> 1.8", + app: :standard_version, + version: "0.1.0", + elixir: "~> 1.9", start_permanent: Mix.env() == :prod, deps: deps() ] end -end \ No newline at end of file + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + # {:dep_from_hexpm, "~> 0.3.0"}, + # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} + ] + end +end From 681d28781da04e858d305e681c4edc4650db208d Mon Sep 17 00:00:00 2001 From: Joe Bottigliero Date: Wed, 7 Aug 2019 09:13:24 -0500 Subject: [PATCH 4/8] docs: add expected return value details --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 4dc8d8557..e0c9f2a43 100644 --- a/README.md +++ b/README.md @@ -391,10 +391,14 @@ An `updater` is expected to be a Javascript module with _atleast_ two methods ex This method is used to read the version from the provided file contents. +The return value is expected to be a semantic version string. + ##### `writeVersion(contents = string, version: string): string` This method is used to write the version to the provided contents. +The return value will be written directly (overwrite) to the provided file. + --- Let's assume our `VERSION_TRACKER.json` has the following contents: From 4969ff0b89bcbf4193daa1030628e08f63f447b0 Mon Sep 17 00:00:00 2001 From: Joe Bottigliero Date: Wed, 7 Aug 2019 11:59:09 -0500 Subject: [PATCH 5/8] update: Use path.basename to lookup provided bumpFiles.filename MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Sébastien Règne --- lib/updaters/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/updaters/index.js b/lib/updaters/index.js index a49fc23de..3bc890d16 100644 --- a/lib/updaters/index.js +++ b/lib/updaters/index.js @@ -13,7 +13,7 @@ function getUpdaterByType (type) { module.exports.getUpdaterByType = getUpdaterByType module.exports.getUpdaterByFilename = function (filename) { - if (JSON_BUMP_FILES.includes(filename)) { + if (JSON_BUMP_FILES.includes(path.basename(filename))) { return getUpdaterByType('json') } if (PLAIN_TEXT_BUMP_FILES.includes(filename)) { From 376ca60bd489f191e2bfacb9d3eba02be1ca5818 Mon Sep 17 00:00:00 2001 From: Joe Bottigliero Date: Sat, 24 Aug 2019 08:26:56 -0500 Subject: [PATCH 6/8] fix: packageFiles - Ensure `packageFile` parsing supports custo updaters. - Abstracts the updater resolution code for bumpFiles and packageFiles --- index.js | 13 +++++++++---- lib/lifecycles/bump.js | 27 +++++++++------------------ lib/updaters/index.js | 38 ++++++++++++++++++++++++++++++++++---- lib/updaters/types/json.js | 4 ++++ 4 files changed, 56 insertions(+), 26 deletions(-) diff --git a/index.js b/index.js index 6a6253684..5f89c59c1 100755 --- a/index.js +++ b/index.js @@ -6,6 +6,7 @@ const latestSemverTag = require('./lib/latest-semver-tag') const path = require('path') const printError = require('./lib/print-error') const tag = require('./lib/lifecycles/tag') +const { resolveUpdaterObjectFromArgument } = require('./lib/updaters') module.exports = function standardVersion (argv) { let defaults = require('./defaults') @@ -27,12 +28,16 @@ module.exports = function standardVersion (argv) { const args = Object.assign({}, defaults, argv) let pkg - args.packageFiles.forEach((filename) => { + args.packageFiles.forEach((packageFile) => { if (pkg) return - const pkgPath = path.resolve(process.cwd(), filename) + const updater = resolveUpdaterObjectFromArgument(packageFile) + const pkgPath = path.resolve(process.cwd(), updater.filename) try { - const data = fs.readFileSync(pkgPath, 'utf8') - pkg = JSON.parse(data) + const contents = fs.readFileSync(pkgPath, 'utf8') + pkg = { + version: updater.updater.readVersion(contents), + private: typeof updater.updater.isPrivate === 'function' ? updater.updater.isPrivate(contents) : false + } } catch (err) {} }) let newVersion diff --git a/lib/lifecycles/bump.js b/lib/lifecycles/bump.js index 05b8f8b35..b59c46cb5 100644 --- a/lib/lifecycles/bump.js +++ b/lib/lifecycles/bump.js @@ -11,7 +11,7 @@ const presetLoader = require('../preset-loader') const runLifecycleScript = require('../run-lifecycle-script') const semver = require('semver') const writeFile = require('../write-file') -const { getUpdaterByFilename, getUpdaterByType, getCustomUpdater } = require('../updaters') +const { resolveUpdaterObjectFromArgument } = require('../updaters') let configsToUpdate = {} function Bump (args, version) { @@ -146,39 +146,30 @@ function bumpVersion (releaseAs, currentVersion, args) { function updateConfigs (args, newVersion) { const dotgit = DotGitignore() args.bumpFiles.forEach(function (bumpFile) { - if (typeof bumpFile !== 'object') { - bumpFile = { - filename: bumpFile - } + const updater = resolveUpdaterObjectFromArgument(bumpFile) + if (!updater) { + return } - const configPath = path.resolve(process.cwd(), bumpFile.filename) + const configPath = path.resolve(process.cwd(), updater.filename) try { if (dotgit.ignore(configPath)) return const stat = fs.lstatSync(configPath) if (!stat.isFile()) return - - if (bumpFile.updater) { - bumpFile.updater = getCustomUpdater(bumpFile.updater) - } else if (bumpFile.type) { - bumpFile.updater = getUpdaterByType(bumpFile.type) - } else { - bumpFile.updater = getUpdaterByFilename(bumpFile.filename) - } const contents = fs.readFileSync(configPath, 'utf8') checkpoint( args, - 'bumping version in ' + bumpFile.filename + ' from %s to %s', - [bumpFile.updater.readVersion(contents), newVersion] + 'bumping version in ' + updater.filename + ' from %s to %s', + [updater.updater.readVersion(contents), newVersion] ) writeFile( args, configPath, - bumpFile.updater.writeVersion(contents, newVersion) + updater.updater.writeVersion(contents, newVersion) ) // flag any config files that we modify the version # for // as having been updated. - configsToUpdate[bumpFile.filename] = true + configsToUpdate[updater.filename] = true } catch (err) { if (err.code !== 'ENOENT') console.warn(err.message) } diff --git a/lib/updaters/index.js b/lib/updaters/index.js index 3bc890d16..84ed1a0fa 100644 --- a/lib/updaters/index.js +++ b/lib/updaters/index.js @@ -10,9 +10,7 @@ function getUpdaterByType (type) { } } -module.exports.getUpdaterByType = getUpdaterByType - -module.exports.getUpdaterByFilename = function (filename) { +function getUpdaterByFilename (filename) { if (JSON_BUMP_FILES.includes(path.basename(filename))) { return getUpdaterByType('json') } @@ -24,6 +22,38 @@ module.exports.getUpdaterByFilename = function (filename) { ) } -module.exports.getCustomUpdater = function (updater) { +function getCustomUpdater (updater) { return require(path.resolve(process.cwd(), updater)) } + +module.exports.resolveUpdaterObjectFromArgument = function (arg) { + /** + * If an Object was not provided, we assume it's the path/filename + * of the updater. + */ + let updater = arg + if (typeof updater !== 'object') { + updater = { + filename: arg + } + } + try { + if (updater.updater) { + updater.updater = getCustomUpdater(updater.updater) + } else if (updater.type) { + updater.updater = getUpdaterByType(updater.type) + } else { + updater.updater = getUpdaterByFilename(updater.filename) + } + } catch (err) { + if (err.code !== 'ENOENT') console.warn(err.message) + } + /** + * We weren't able to resolve an updater for the argument. + */ + if (!updater.updater) { + return false + } + + return updater +} diff --git a/lib/updaters/types/json.js b/lib/updaters/types/json.js index dbe5b6f3f..5e6924cd1 100644 --- a/lib/updaters/types/json.js +++ b/lib/updaters/types/json.js @@ -13,3 +13,7 @@ module.exports.writeVersion = function (contents, version) { json.version = version return stringifyPackage(json, indent, newline) } + +module.exports.isPrivate = function (contents) { + return JSON.parse(contents).private +} From 73df59c8075d31a26a220bef1e507bdd99779288 Mon Sep 17 00:00:00 2001 From: Joe Bottigliero Date: Sat, 24 Aug 2019 08:35:27 -0500 Subject: [PATCH 7/8] test: adds a test for packageFiles --- test.js | 25 +++++++++++++++++++++++++ test/mocks/VERSION-6.3.1.txt | 1 + 2 files changed, 26 insertions(+) create mode 100644 test/mocks/VERSION-6.3.1.txt diff --git a/test.js b/test.js index 5ee90b102..5947d7e12 100644 --- a/test.js +++ b/test.js @@ -965,6 +965,31 @@ describe('standard-version', function () { }) }) + describe('custom `packageFiles` support', function () { + it('reads and writes to a custom `plain-text` file', function () { + fs.copyFileSync('../test/mocks/VERSION-6.3.1.txt', 'VERSION_TRACKER.txt') + commit('feat: yet another commit') + return require('./index')({ + silent: true, + packageFiles: [ + { + filename: 'VERSION_TRACKER.txt', + type: 'plain-text' + } + ], + bumpFiles: [ + { + filename: 'VERSION_TRACKER.txt', + type: 'plain-text' + } + ] + }) + .then(() => { + fs.readFileSync('VERSION_TRACKER.txt', 'utf-8').should.equal('6.4.0') + }) + }) + }) + describe('npm-shrinkwrap.json support', function () { beforeEach(function () { writeNpmShrinkwrapJson('1.0.0') diff --git a/test/mocks/VERSION-6.3.1.txt b/test/mocks/VERSION-6.3.1.txt new file mode 100644 index 000000000..39ee137ba --- /dev/null +++ b/test/mocks/VERSION-6.3.1.txt @@ -0,0 +1 @@ +6.3.1 \ No newline at end of file From 3be2404c0e38d0c9911e06677d071813cadee4a2 Mon Sep 17 00:00:00 2001 From: Joe Bottigliero Date: Wed, 4 Dec 2019 19:05:02 -0600 Subject: [PATCH 8/8] chore: eslint updates --- index.js | 2 +- lib/updaters/types/json.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 5f89c59c1..9ee458584 100755 --- a/index.js +++ b/index.js @@ -9,7 +9,7 @@ const tag = require('./lib/lifecycles/tag') const { resolveUpdaterObjectFromArgument } = require('./lib/updaters') module.exports = function standardVersion (argv) { - let defaults = require('./defaults') + const defaults = require('./defaults') /** * `--message` (`-m`) support will be removed in the next major version. */ diff --git a/lib/updaters/types/json.js b/lib/updaters/types/json.js index 5e6924cd1..4494aab76 100644 --- a/lib/updaters/types/json.js +++ b/lib/updaters/types/json.js @@ -8,8 +8,8 @@ module.exports.readVersion = function (contents) { module.exports.writeVersion = function (contents, version) { const json = JSON.parse(contents) - let indent = detectIndent(contents).indent - let newline = detectNewline(contents) + const indent = detectIndent(contents).indent + const newline = detectNewline(contents) json.version = version return stringifyPackage(json, indent, newline) }