diff --git a/.gitignore b/.gitignore index fa6f8ca..e712e92 100644 --- a/.gitignore +++ b/.gitignore @@ -129,3 +129,7 @@ $RECYCLE.BIN/ package-lock.json yarn.lock + +# Dist subfolder + +dist diff --git a/README.md b/README.md index c0a3cfb..64a00b9 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,61 @@ -# **contributors** - -[**semantic-release**](https://github.com/semantic-release/semantic-release) plugin to automatically update contributors list from git history - -[![Travis](https://img.shields.io/travis/flo-sch/semantic-release-contributors.svg)](https://travis-ci.org/flo-sch/semantic-release-contributors) -[![Codecov](https://img.shields.io/codecov/c/github/flo-sch/semantic-release-contributors.svg)](https://codecov.io/gh/flo-sch/semantic-release-contributors) -[![Known Vulnerabilities](https://snyk.io/test/github/flo-sch/semantic-release-contributors/badge.svg?targetFile=package.json)](https://snyk.io/test/github/flo-sch/semantic-release-contributors?targetFile=package.json) -[![Maintainability](https://api.codeclimate.com/v1/badges/0c542e19db095ddb9947/maintainability)](https://codeclimate.com/github/flo-sch/semantic-release-contributors/maintainability) +

📦🤖 semantic-release-contributors

+

+ semantic-release + plugin to automatically update contributors list based on commits history +

+

+ + Semantic Release + + + Commitizen friendly + +

+

+ + Travis + + + Codecov + + + Known Vulnerabilities + + + Maintainability + +

+

+ + npm latest version + + + npm bundle size + + + npm bundle size + + + LICENSE + +

-[![npm latest version](https://img.shields.io/npm/v/semantic-release-contributors/latest.svg)](https://www.npmjs.com/package/semantic-release-contributors) +------------------ -| Step | Description | -|----------------|------------------------------------------------------------------------------------------------------------| -| `prepare` | Determine the contributors list by analyzing git history. | +| Step | Description | +|----------------|-----------------------------------------------------------------------------------------------------| +| `prepare` | Determine the contributors list by analyzing commits history. | ## Install ```bash -$ npm install semantic-release-contributors -D +npm install semantic-release-contributors -D ``` -## How does it work? +## How does it work -Whenener someone commit to the project, his/her name will be appended to the [contributors list of your package.json](https://docs.npmjs.com/files/package.json#people-fields-author-contributors) file. +Whenener someone commit to the project, his/her name will be appended +to the [contributors list of your package.json](https://docs.npmjs.com/files/package.json#people-fields-author-contributors) file. If `Paul Smith` commits to a project with the following set-up: @@ -48,26 +82,39 @@ The `package.json` file would then be updated to: } ``` -**NOTE**: this package internally deserialize the contributors to objects (name, email, url) and make sure duplicated emails are removed. Contributors objects are then potentially re-serialized before being written to the package file (unless you opt for a different format) +**NOTE**: this package internally deserialize the contributors to +objects (name, email, url) and make sure duplicated emails are removed. +Contributors objects are then potentially re-serialized before being written +to the package file (unless you opt for a different format) ## Usage The plugin can be configured in the [**semantic-release** configuration file](https://github.com/semantic-release/semantic-release/blob/master/docs/usage/configuration.md#configuration): +**IMPORTANT**: since this plugin acts on semantic-release's "prepare" step +and do not commit the updated package.json file itself, +it *requires* to be placed *before* "@semantic-release/git". + ```json { "plugins": [ + // important: insert it before @semantic-release/git ["semantic-release-contributors", { "format": "string", "pkgRoot": "." - }] + }], + // ... + "@semantic-release/git" + // ... ] } ``` With this example: -- the contributors will be stringified to `name ` -- the package file containing the contributors will be read then updated in the current directory + +* the contributors will be stringified to `name ` +* the package file containing the contributors will be read +then updated in the current directory ## Configuration @@ -80,8 +127,8 @@ With this example: ## Similar or related projects -- [parse-author](https://www.npmjs.com/package/parse-author) -- [stringify-author](https://www.npmjs.com/package/stringify-author) +* [parse-author](https://www.npmjs.com/package/parse-author) +* [stringify-author](https://www.npmjs.com/package/stringify-author)

Kill all humans diff --git a/index.js b/index.js deleted file mode 100644 index 2f1c545..0000000 --- a/index.js +++ /dev/null @@ -1,26 +0,0 @@ -const path = require('path'); -const AggregateError = require('aggregate-error'); -const getContributors = require('./lib/get-contributors-from-commits'); -const saveContributors = require('./lib/save-contributors'); - -async function prepare(pluginConfig, context) { - const errors = []; - const {cwd, commits, logger} = context; - - const format = ['string', 'object'].includes(pluginConfig.format) ? pluginConfig.format : 'string'; - const pkgRoot = pluginConfig.pkgRoot || '.'; - - try { - await saveContributors(path.resolve(cwd, pkgRoot, 'package.json'), getContributors(commits), format, logger); - } catch (error) { - errors.push(error); - } - - if (errors.length > 0) { - throw new AggregateError(errors); - } -} - -module.exports = { - prepare, -}; diff --git a/lib/contributors/get-contributors-from-commits.js b/lib/contributors/get-contributors-from-commits.js new file mode 100644 index 0000000..e54688f --- /dev/null +++ b/lib/contributors/get-contributors-from-commits.js @@ -0,0 +1,18 @@ +import uniqBy from '../helpers/uniq-by'; +import sortBy from '../helpers/sort-by'; + +/** + * Extract an array of contributors from an array of commits, removing duplicates on email + * + * @param array commits + */ +const getContributorsFromCommits = (commits = []) => + uniqBy( + sortBy(commits, 'date').map((commit) => ({ + email: commit.author.email, + name: commit.author.name, + })), + 'email' + ); + +export default getContributorsFromCommits; diff --git a/lib/contributors/merge-contributors.js b/lib/contributors/merge-contributors.js new file mode 100644 index 0000000..c99276f --- /dev/null +++ b/lib/contributors/merge-contributors.js @@ -0,0 +1,24 @@ +import parseAuthor from 'parse-author'; +import uniqBy from '../helpers/uniq-by'; + +/** + * Parse a contributor string with parse-author + * + * @param mixed contributor + * + * @return object + */ +const parseContributor = (contributor) => (typeof contributor === 'string' ? parseAuthor(contributor) : contributor); + +/** + * Merge array of contributors, parsing them and removing duplicates on email + * + * @param array packageContributors + * @param array commitsContributors + * + * @return array + */ +const mergeContributors = (packageContributors = [], commitsContributors = []) => + uniqBy(packageContributors.concat(commitsContributors).map(parseContributor), 'email'); + +export default mergeContributors; diff --git a/lib/contributors/save-contributors.js b/lib/contributors/save-contributors.js new file mode 100644 index 0000000..f5a48e0 --- /dev/null +++ b/lib/contributors/save-contributors.js @@ -0,0 +1,33 @@ +import {readFile, writeFile} from 'jsonfile'; +import stringifyAuthor from 'stringify-author'; +import mergeContributors from './merge-contributors'; + +/** + * Save an array of contributors to a package.json file + * + * @param string packageFilePath + * @param array contributors + * @param string format + * @param mixed logger + */ +const saveContributors = async (packageFilePath, contributors = [], format = 'string', logger = undefined) => { + const pkg = await readFile(packageFilePath); + + let allContributors = mergeContributors(pkg.contributors, contributors); + + if (format === 'string') { + allContributors = allContributors.map(stringifyAuthor); + } + + if (logger) { + logger.info('Updated contributors list', allContributors); + } + + pkg.contributors = allContributors; + + await writeFile(packageFilePath, pkg, { + spaces: 2, + }); +}; + +export default saveContributors; diff --git a/lib/get-contributors-from-commits.js b/lib/get-contributors-from-commits.js deleted file mode 100644 index 57ac249..0000000 --- a/lib/get-contributors-from-commits.js +++ /dev/null @@ -1,10 +0,0 @@ -const {chain} = require('lodash'); - -module.exports = function (commits = []) { - return chain(commits) - .map((commit) => commit.author) - .sortBy('date') - .map(({email, name}) => ({email, name})) - .uniqBy('email') - .value(); -}; diff --git a/lib/helpers/sort-by.js b/lib/helpers/sort-by.js new file mode 100644 index 0000000..dcdfc3d --- /dev/null +++ b/lib/helpers/sort-by.js @@ -0,0 +1,22 @@ +/** + * Sort an array of objects by a given property + * + * @param array items + * @param string property + * + * @return array + */ +const sortBy = (items, property) => + items.sort((itemA, itemB) => { + if (itemA[property] > itemB[property]) { + return -1; + } + + if (itemA[property] < itemB[property]) { + return 1; + } + + return 0; + }); + +export default sortBy; diff --git a/lib/helpers/uniq-by.js b/lib/helpers/uniq-by.js new file mode 100644 index 0000000..7db07c3 --- /dev/null +++ b/lib/helpers/uniq-by.js @@ -0,0 +1,21 @@ +/** + * Remove duplicates from an array of objects by a given property + * + * @param array items + * @param string property + * + * @return array + */ +const uniqBy = (items, property) => + items.reduce((uniqItems, item) => { + const value = item[property]; + const uniqKeys = uniqItems.map((i) => i[property]); + + if (typeof value === 'undefined' || !uniqKeys.includes(value)) { + uniqItems.push(item); + } + + return uniqItems; + }, []); + +export default uniqBy; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..7300fca --- /dev/null +++ b/lib/index.js @@ -0,0 +1,3 @@ +import prepare from './steps/prepare'; + +export {prepare}; diff --git a/lib/merge-contributors.js b/lib/merge-contributors.js deleted file mode 100644 index 162d56a..0000000 --- a/lib/merge-contributors.js +++ /dev/null @@ -1,12 +0,0 @@ -const {chain, concat} = require('lodash'); -const parseAuthor = require('parse-author'); - -const parseContributor = function (contributor) { - return typeof contributor === 'string' ? parseAuthor(contributor) : contributor; -}; - -module.exports = function (packageContributors = [], commitsContributors = []) { - return chain(concat(packageContributors.map(parseContributor), commitsContributors.map(parseContributor))) - .uniqBy('email') - .value(); -}; diff --git a/lib/save-contributors.js b/lib/save-contributors.js deleted file mode 100644 index ab6e363..0000000 --- a/lib/save-contributors.js +++ /dev/null @@ -1,23 +0,0 @@ -const fs = require('fs-extra'); -const stringifyAuthor = require('stringify-author'); -const mergeContributors = require('./merge-contributors'); - -module.exports = async function (packageFilePath, contributors = [], format = 'string', logger = undefined) { - const pkg = await fs.readJson(packageFilePath); - - let allContributors = mergeContributors(pkg.contributors, contributors); - - if (format === 'string') { - allContributors = allContributors.map(stringifyAuthor); - } - - if (logger) { - logger.info('Updated contributors list', allContributors); - } - - pkg.contributors = allContributors; - - await fs.writeJson(packageFilePath, pkg, { - spaces: 2, - }); -}; diff --git a/lib/steps/prepare.js b/lib/steps/prepare.js new file mode 100644 index 0000000..54edd5f --- /dev/null +++ b/lib/steps/prepare.js @@ -0,0 +1,29 @@ +import path from 'path'; +import AggregateError from 'aggregate-error'; +import getContributorsFromCommits from '../contributors/get-contributors-from-commits'; +import saveContributors from '../contributors/save-contributors'; + +const prepare = async (pluginConfig, context) => { + const errors = []; + const {cwd, commits, logger} = context; + + const format = ['string', 'object'].includes(pluginConfig.format) ? pluginConfig.format : 'string'; + const pkgRoot = pluginConfig.pkgRoot || '.'; + + try { + await saveContributors( + path.resolve(cwd, pkgRoot, 'package.json'), + getContributorsFromCommits(commits), + format, + logger + ); + } catch (error) { + errors.push(error); + } + + if (errors.length > 0) { + throw new AggregateError(errors); + } +}; + +export default prepare; diff --git a/package.json b/package.json index 09e3d43..ff6a2ae 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,8 @@ "node": ">=10.18" }, "files": [ - "lib", - "index.js" + "dist", + "lib" ], "keywords": [ "author", @@ -39,19 +39,25 @@ "version" ], "license": "MIT", - "main": "index.js", + "module": "dist/index.esm.js", + "main": "dist/index.js", + "source": "lib/index.js", "scripts": { + "build": "microbundle -f cjs,es --target node", "codecov": "codecov -f coverage/coverage-final.json", "commit": "git-cz", "lint": "xo", + "postinstall": "npm run build", + "prebuild": "rimraf dist/*", + "presemantic-release": "npm run build", "pretest": "npm run lint", "semantic-release": "semantic-release", - "test": "nyc ava -v" + "test": "nyc ava -v", + "watch": "microbundle watch" }, "dependencies": { "aggregate-error": "^3.0.0", - "fs-extra": "^9.0.0", - "lodash": "^4.17.15", + "jsonfile": "^6.0.1", "parse-author": "^2.0.0", "stringify-author": "^0.1.3" }, @@ -69,10 +75,14 @@ "codecov": "^3.5.0", "commitizen": "^4.0.3", "cz-conventional-changelog": "^3.0.2", + "esm": "^3.2.25", + "fs-extra": "^9.0.0", "husky": "^4.2.3", "lint-staged": "^10.0.3", + "microbundle": "^0.12.0-next.8", "nyc": "^15.0.1", "prettier": "^2.0.4", + "rimraf": "^3.0.2", "semantic-release": "^17.0.4", "sinon": "^9.0.2", "tempy": "^0.5.0", @@ -89,12 +99,14 @@ "babel": true, "files": [ "test/**/*.test.js" + ], + "require": [ + "esm" ] }, "nyc": { "include": [ - "lib/**/*.js", - "index.js" + "lib/**/*.js" ], "reporter": [ "json", diff --git a/test/get-contributors-from-commits.test.js b/test/contributors/get-contributors-from-commits.test.js similarity index 87% rename from test/get-contributors-from-commits.test.js rename to test/contributors/get-contributors-from-commits.test.js index 0c34f5d..97cbf16 100644 --- a/test/get-contributors-from-commits.test.js +++ b/test/contributors/get-contributors-from-commits.test.js @@ -1,6 +1,6 @@ import test from 'ava'; import {stub} from 'sinon'; -import getContributors from '../lib/get-contributors-from-commits'; +import getContributorsFromCommits from '../../lib/contributors/get-contributors-from-commits'; test.beforeEach((t) => { // Stub the logger functions @@ -17,7 +17,7 @@ test.beforeEach((t) => { }); test('Extract an empty list if the commit list is empty', (t) => { - const contributors = getContributors(); + const contributors = getContributorsFromCommits(); t.is(contributors.length, 0); }); @@ -26,7 +26,7 @@ test('Extract a list of contributors from a commits list', (t) => { const firstCommiterName = 'John Doe'; const secondCommiterEmail = 'john.smith@domain.tld'; - const contributors = getContributors([ + const contributors = getContributorsFromCommits([ { message: 'feat(something): add something', author: { @@ -52,7 +52,7 @@ test('Removes duplicates email from the commiters list', (t) => { const name = 'John Doe'; const email = 'john.doe@domain.tld'; - const contributors = getContributors([ + const contributors = getContributorsFromCommits([ { message: 'feat(something): add something', author: { diff --git a/test/merge-contributors.test.js b/test/contributors/merge-contributors.test.js similarity index 96% rename from test/merge-contributors.test.js rename to test/contributors/merge-contributors.test.js index 22a9ca6..878e190 100644 --- a/test/merge-contributors.test.js +++ b/test/contributors/merge-contributors.test.js @@ -1,6 +1,6 @@ import test from 'ava'; import {stub} from 'sinon'; -import mergeContributors from '../lib/merge-contributors'; +import mergeContributors from '../../lib/contributors/merge-contributors'; test.beforeEach((t) => { // Stub the logger functions diff --git a/test/save-contributors.test.js b/test/contributors/save-contributors.test.js similarity index 91% rename from test/save-contributors.test.js rename to test/contributors/save-contributors.test.js index 73ce601..b67db2c 100644 --- a/test/save-contributors.test.js +++ b/test/contributors/save-contributors.test.js @@ -2,8 +2,9 @@ import path from 'path'; import test from 'ava'; import {stub} from 'sinon'; import tempy from 'tempy'; -import {outputJson, readJson} from 'fs-extra'; -import saveContributors from '../lib/save-contributors'; +import {readFile} from 'jsonfile'; +import {outputJson} from 'fs-extra'; +import saveContributors from '../../lib/contributors/save-contributors'; test.beforeEach((t) => { // Stub the logger functions @@ -62,7 +63,7 @@ test('Save an updated list of contributors in "string" format', async (t) => { ); }); - pkg = await readJson(filepath); + pkg = await readFile(filepath); t.is(pkg.contributors.length, 2); t.is(typeof pkg.contributors[0], 'string'); @@ -97,7 +98,7 @@ test('Save an updated list of contributors in "object" format', async (t) => { ); }); - pkg = await readJson(filepath); + pkg = await readFile(filepath); t.is(pkg.contributors.length, 2); t.is(typeof pkg.contributors[0], 'object'); diff --git a/test/helpers/sort-by.test.js b/test/helpers/sort-by.test.js new file mode 100644 index 0000000..999c12e --- /dev/null +++ b/test/helpers/sort-by.test.js @@ -0,0 +1,60 @@ +import test from 'ava'; +import sortBy from '../../lib/helpers/sort-by'; + +test('Sort by a given property', (t) => { + const value1 = 'foo'; + const value2 = 'bar'; + const value3 = 'baz'; + + const sortedItems = sortBy( + [ + { + id: 1, + value: value1, + }, + { + id: 3, + value: value3, + }, + { + id: 2, + value: value2, + }, + ], + 'id' + ); + + t.is(sortedItems.length, 3); + t.is(sortedItems[0].value, value3); + t.is(sortedItems[1].value, value2); + t.is(sortedItems[2].value, value1); +}); + +test('Return the array as such when the property does not exists', (t) => { + const value1 = 'foo'; + const value2 = 'bar'; + const value3 = 'baz'; + + const sortedItems = sortBy( + [ + { + id: 1, + value: value1, + }, + { + id: 3, + value: value3, + }, + { + id: 2, + value: value2, + }, + ], + 'unexisting-property' + ); + + t.is(sortedItems.length, 3); + t.is(sortedItems[0].value, value1); + t.is(sortedItems[1].value, value3); + t.is(sortedItems[2].value, value2); +}); diff --git a/test/helpers/uniq-by.test.js b/test/helpers/uniq-by.test.js new file mode 100644 index 0000000..46f3f2d --- /dev/null +++ b/test/helpers/uniq-by.test.js @@ -0,0 +1,66 @@ +import test from 'ava'; +import uniqBy from '../../lib/helpers/uniq-by'; + +test('Removes duplicate by a given property', (t) => { + const value = 'foo'; + const uniqItems = uniqBy( + [ + { + id: 1, + value, + }, + { + id: 1, + value: 'bar', + }, + ], + 'id' + ); + + t.is(uniqItems.length, 1); + t.is(uniqItems[0].value, value); +}); + +test('Returns the array without change if there is no duplicate', (t) => { + const value1 = 'foo'; + const value2 = 'bar'; + const uniqItems = uniqBy( + [ + { + id: 1, + value: value1, + }, + { + id: 2, + value: value2, + }, + ], + 'id' + ); + + t.is(uniqItems.length, 2); + t.is(uniqItems[0].value, value1); + t.is(uniqItems[1].value, value2); +}); + +test('Returns the array as such if the property does not exist', (t) => { + const value1 = 'foo'; + const value2 = 'bar'; + const uniqItems = uniqBy( + [ + { + id: 1, + value: value1, + }, + { + id: 2, + value: value2, + }, + ], + 'unexisting-property' + ); + + t.is(uniqItems.length, 2); + t.is(uniqItems[0].value, value1); + t.is(uniqItems[1].value, value2); +}); diff --git a/test/integration.test.js b/test/integration.test.js index a4cd300..537aea0 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -5,7 +5,7 @@ import {spy} from 'sinon'; import tempy from 'tempy'; test.beforeEach((t) => { - t.context.m = require('..'); + t.context.m = require('../lib'); const log = spy(); t.context.log = log;