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 + plugin to automatically update contributors list based on commits history +
+ + + -[![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
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;