diff --git a/README.md b/README.md index 0b872811..b7d72626 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,8 @@ With this configuration: ##### External package / file -`releaseRules` can also reference a module, either by it's `npm` name or path: +`releaseRules` can also reference a module, either by it's `npm` name or path. Note that the path must include the `.js` extension. + ```json { "plugins": [ @@ -174,9 +175,12 @@ With this configuration: ] } ``` + +The file must be an ES Module exporting an array as default + ```js // File: config/release-rules.js -module.exports = [ +export default [ {type: 'docs', scope: 'README', release: 'patch'}, {type: 'refactor', scope: 'core-*', release: 'minor'}, {type: 'refactor', release: 'patch'}, diff --git a/index.js b/index.js index 65d4f298..06764144 100644 --- a/index.js +++ b/index.js @@ -1,13 +1,16 @@ -const {isUndefined} = require('lodash'); -const parser = require('conventional-commits-parser').sync; -const filter = require('conventional-commits-filter'); -const debug = require('debug')('semantic-release:commit-analyzer'); -const loadParserConfig = require('./lib/load-parser-config'); -const loadReleaseRules = require('./lib/load-release-rules'); -const analyzeCommit = require('./lib/analyze-commit'); -const compareReleaseTypes = require('./lib/compare-release-types'); -const RELEASE_TYPES = require('./lib/default-release-types'); -const DEFAULT_RELEASE_RULES = require('./lib/default-release-rules'); +import lodash from 'lodash'; +const {isUndefined} = lodash; +import {sync as parser} from 'conventional-commits-parser'; +import filter from 'conventional-commits-filter'; +import debug from 'debug'; +import loadParserConfig from './lib/load-parser-config.js'; +import loadReleaseRules from './lib/load-release-rules.js'; +import analyzeCommit from './lib/analyze-commit.js'; +import compareReleaseTypes from './lib/compare-release-types.js'; +import RELEASE_TYPES from './lib/default-release-types.js'; +import DEFAULT_RELEASE_RULES from './lib/default-release-rules.js'; + +debug('semantic-release:commit-analyzer'); /** * Determine the type of release to create based on a list of commits. @@ -25,7 +28,7 @@ const DEFAULT_RELEASE_RULES = require('./lib/default-release-rules'); */ async function analyzeCommits(pluginConfig, context) { const {commits, logger} = context; - const releaseRules = loadReleaseRules(pluginConfig, context); + const releaseRules = await loadReleaseRules(pluginConfig, context); const config = await loadParserConfig(pluginConfig, context); let releaseType = null; @@ -79,4 +82,4 @@ async function analyzeCommits(pluginConfig, context) { return releaseType; } -module.exports = {analyzeCommits}; +export {analyzeCommits}; diff --git a/lib/analyze-commit.js b/lib/analyze-commit.js index 757a45dc..968ea926 100644 --- a/lib/analyze-commit.js +++ b/lib/analyze-commit.js @@ -1,8 +1,11 @@ -const {isMatchWith, isString} = require('lodash'); -const micromatch = require('micromatch'); -const debug = require('debug')('semantic-release:commit-analyzer'); -const RELEASE_TYPES = require('./default-release-types'); -const compareReleaseTypes = require('./compare-release-types'); +import lodash from 'lodash'; +import micromatch from 'micromatch'; +import debug from 'debug'; +import RELEASE_TYPES from './default-release-types.js'; +import compareReleaseTypes from './compare-release-types.js'; +const {isMatchWith, isString} = lodash; +const {isMatch} = micromatch; +debug('semantic-release:commit-analyzer'); /** * Find all the rules matching and return the highest release type of the matching rules. @@ -11,7 +14,7 @@ const compareReleaseTypes = require('./compare-release-types'); * @param {Commit} commit a parsed commit. * @return {string} the highest release type of the matching rules or `undefined` if no rule match the commit. */ -module.exports = (releaseRules, commit) => { +export default (releaseRules, commit) => { let releaseType; releaseRules @@ -23,7 +26,7 @@ module.exports = (releaseRules, commit) => { (!revert || commit.revert) && // Otherwise match the regular rules isMatchWith(commit, rule, (object, src) => - isString(src) && isString(object) ? micromatch.isMatch(object, src) : undefined + isString(src) && isString(object) ? isMatch(object, src) : undefined ) ) .every(match => { diff --git a/lib/compare-release-types.js b/lib/compare-release-types.js index 51c97b35..5fdaee5f 100644 --- a/lib/compare-release-types.js +++ b/lib/compare-release-types.js @@ -1,4 +1,4 @@ -const RELEASE_TYPES = require('./default-release-types'); +import RELEASE_TYPES from './default-release-types.js'; /** * Test if a realease type is of higher level than a given one. @@ -7,5 +7,5 @@ const RELEASE_TYPES = require('./default-release-types'); * @param {string} releaseType the release type to compare with. * @return {Boolean} true if `releaseType` is higher than `currentReleaseType`. */ -module.exports = (currentReleaseType, releaseType) => +export default (currentReleaseType, releaseType) => !currentReleaseType || RELEASE_TYPES.indexOf(releaseType) < RELEASE_TYPES.indexOf(currentReleaseType); diff --git a/lib/default-release-rules.js b/lib/default-release-rules.js index 338e4ef1..4f1b1ed7 100644 --- a/lib/default-release-rules.js +++ b/lib/default-release-rules.js @@ -3,7 +3,7 @@ * * @type {Array} */ -module.exports = [ +export default [ {breaking: true, release: 'major'}, {revert: true, release: 'patch'}, // Angular diff --git a/lib/default-release-types.js b/lib/default-release-types.js index 937b01a0..d292fb96 100644 --- a/lib/default-release-types.js +++ b/lib/default-release-types.js @@ -3,4 +3,4 @@ * * @type {Array} */ -module.exports = ['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease']; +export default ['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease']; diff --git a/lib/esm-import.js b/lib/esm-import.js new file mode 100644 index 00000000..3a2dcbf2 --- /dev/null +++ b/lib/esm-import.js @@ -0,0 +1,11 @@ +export const esmImport = async name => { + try { + return (await import(name)).default; + } catch (error) { + if (error.code === 'ERR_MODULE_NOT_FOUND') { + error.code = 'MODULE_NOT_FOUND'; + } + + throw error; + } +}; diff --git a/lib/load-parser-config.js b/lib/load-parser-config.js index af093eba..6c185e0f 100644 --- a/lib/load-parser-config.js +++ b/lib/load-parser-config.js @@ -1,7 +1,7 @@ -const {promisify} = require('util'); -const {isPlainObject} = require('lodash'); -const importFrom = require('import-from'); -const conventionalChangelogAngular = require('conventional-changelog-angular'); +import {promisify} from 'util'; +import lodash from 'lodash'; +const {isPlainObject} = lodash; +import {esmImport} from './esm-import.js'; /** * Load `conventional-changelog-parser` options. Handle presets that return either a `Promise` or a `Promise`. @@ -14,16 +14,16 @@ const conventionalChangelogAngular = require('conventional-changelog-angular'); * @param {String} context.cwd The current working directory. * @return {Promise} a `Promise` that resolve to the `conventional-changelog-parser` options. */ -module.exports = async ({preset, config, parserOpts, presetConfig}, {cwd}) => { +export default async ({preset, config, parserOpts, presetConfig}, {_}) => { let loadedConfig; if (preset) { const presetPackage = `conventional-changelog-${preset.toLowerCase()}`; - loadedConfig = importFrom.silent(__dirname, presetPackage) || importFrom(cwd, presetPackage); + loadedConfig = await esmImport(presetPackage); } else if (config) { - loadedConfig = importFrom.silent(__dirname, config) || importFrom(cwd, config); + loadedConfig = await esmImport(config); } else { - loadedConfig = conventionalChangelogAngular; + loadedConfig = await esmImport('conventional-changelog-angular'); } loadedConfig = await (typeof loadedConfig === 'function' diff --git a/lib/load-release-rules.js b/lib/load-release-rules.js index 508b0349..fd189d39 100644 --- a/lib/load-release-rules.js +++ b/lib/load-release-rules.js @@ -1,6 +1,9 @@ -const {isUndefined} = require('lodash'); -const importFrom = require('import-from'); -const RELEASE_TYPES = require('./default-release-types'); +import lodash from 'lodash'; +const {isUndefined} = lodash; +import {esmImport} from './esm-import.js'; +import RELEASE_TYPES from './default-release-types.js'; +import {resolve} from 'path'; +import {pathToFileURL} from 'url'; /** * Load and validate the `releaseRules` rules. @@ -15,14 +18,12 @@ const RELEASE_TYPES = require('./default-release-types'); * * @return {Array} the loaded and validated `releaseRules`. */ -module.exports = ({releaseRules}, {cwd}) => { +export default async ({releaseRules}, {cwd}) => { let loadedReleaseRules; if (releaseRules) { loadedReleaseRules = - typeof releaseRules === 'string' - ? importFrom.silent(__dirname, releaseRules) || importFrom(cwd, releaseRules) - : releaseRules; + typeof releaseRules === 'string' ? await esmImport(pathToFileURL(resolve(cwd, releaseRules)).href) : releaseRules; if (!Array.isArray(loadedReleaseRules)) { throw new TypeError('Error in commit-analyzer configuration: "releaseRules" must be an array of rules'); diff --git a/package.json b/package.json index 28c57343..fe093dd3 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "@semantic-release/commit-analyzer", + "type": "module", "description": "semantic-release plugin to analyze commits with conventional-changelog", "version": "0.0.0-development", "author": "Pierre Vanduynslager (https://twitter.com/@pvdlg_)", @@ -56,7 +57,7 @@ "semantic-release" ], "license": "MIT", - "main": "index.js", + "exports": "./index.js", "nyc": { "include": [ "lib/**/*.js", @@ -94,7 +95,11 @@ "prettier": true, "space": true, "rules": { - "unicorn/string-content": "off" + "unicorn/string-content": "off", + "unicorn/import-index": "off", + "import/extensions": "off", + "import/no-useless-path-segments": "off", + "node/no-unsupported-features/es-syntax": "off" } }, "renovate": { diff --git a/test/analyze-commit.test.js b/test/analyze-commit.test.js index 45f07715..2f8a7329 100644 --- a/test/analyze-commit.test.js +++ b/test/analyze-commit.test.js @@ -1,5 +1,5 @@ -const test = require('ava'); -const analyzeCommit = require('../lib/analyze-commit'); +import test from 'ava'; +import analyzeCommit from '../lib/analyze-commit.js'; test('Match breaking change', t => { const commit = { diff --git a/test/compare-release-types.test.js b/test/compare-release-types.test.js index 82fc3bce..1d61b52d 100644 --- a/test/compare-release-types.test.js +++ b/test/compare-release-types.test.js @@ -1,5 +1,5 @@ -const test = require('ava'); -const compareReleaseTypes = require('../lib/compare-release-types'); +import test from 'ava'; +import compareReleaseTypes from '../lib/compare-release-types.js'; test('Compares release types', t => { t.true(compareReleaseTypes('patch', 'minor')); diff --git a/test/fixtures/release-rules-invalid.js b/test/fixtures/release-rules-invalid.js index 888cae37..7a4e8a72 100644 --- a/test/fixtures/release-rules-invalid.js +++ b/test/fixtures/release-rules-invalid.js @@ -1 +1 @@ -module.exports = 42; +export default 42; diff --git a/test/fixtures/release-rules.js b/test/fixtures/release-rules.js index 87297d18..8cb14f87 100644 --- a/test/fixtures/release-rules.js +++ b/test/fixtures/release-rules.js @@ -1,4 +1,4 @@ -module.exports = [ +export default [ {breaking: true, release: 'major'}, {type: 'feat', release: 'minor'}, {type: 'fix', release: 'patch'}, diff --git a/test/integration.test.js b/test/integration.test.js index e7ac70ae..3c52034c 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -1,11 +1,11 @@ -const test = require('ava'); -const {stub} = require('sinon'); -const {analyzeCommits} = require('..'); +import test from 'ava'; +import sinon from 'sinon'; +import {analyzeCommits} from '../index.js'; const cwd = process.cwd(); test.beforeEach(t => { - const log = stub(); + const log = sinon.stub(); t.context.log = log; t.context.logger = {log}; }); @@ -117,7 +117,7 @@ test('Accept a "releaseRules" option that reference a requierable module', async {hash: '456', message: 'feat(scope2): Second feature'}, ]; const releaseType = await analyzeCommits( - {releaseRules: './test/fixtures/release-rules'}, + {releaseRules: './test/fixtures/release-rules.js'}, {cwd, commits, logger: t.context.logger} ); @@ -357,7 +357,7 @@ test('Throw error if "releaseRules" is not an Array or a String', async t => { }); test('Throw error if "releaseRules" option reference a requierable module that is not an Array or a String', async t => { - await t.throwsAsync(analyzeCommits({releaseRules: './test/fixtures/release-rules-invalid'}, {cwd}), { + await t.throwsAsync(analyzeCommits({releaseRules: './test/fixtures/release-rules-invalid.js'}, {cwd}), { message: /Error in commit-analyzer configuration: "releaseRules" must be an array of rules/, }); }); diff --git a/test/load-parser-config.test.js b/test/load-parser-config.test.js index 233cb284..88e15cb7 100644 --- a/test/load-parser-config.test.js +++ b/test/load-parser-config.test.js @@ -1,5 +1,6 @@ -const test = require('ava'); -const loadParserConfig = require('../lib/load-parser-config'); +import test from 'ava'; +import loadParserConfig from '../lib/load-parser-config.js'; +import conventionalChangelogAngular from 'conventional-changelog-angular'; const cwd = process.cwd(); @@ -34,7 +35,7 @@ async function loadConfig(t, config, pluginOptions) { loadConfig.title = (providedTitle, config) => `${providedTitle} Load "${config}" config`.trim(); test('Load "conventional-changelog-angular" by default', async t => { - t.deepEqual(await loadParserConfig({}, {cwd}), (await require('conventional-changelog-angular')).parserOpts); + t.deepEqual(await loadParserConfig({}, {cwd}), (await conventionalChangelogAngular).parserOpts); }); test('Accept a "parserOpts" object as option', async t => { diff --git a/test/load-release-rules.test.js b/test/load-release-rules.test.js index a6818dae..ae928e56 100644 --- a/test/load-release-rules.test.js +++ b/test/load-release-rules.test.js @@ -1,29 +1,29 @@ -const test = require('ava'); -const loadReleaseRules = require('../lib/load-release-rules'); -const testReleaseRules = require('./fixtures/release-rules'); +import test from 'ava'; +import loadReleaseRules from '../lib/load-release-rules.js'; +import testReleaseRules from './fixtures/release-rules.js'; const cwd = process.cwd(); -test('Accept a "releaseRules" option', t => { - const releaseRules = loadReleaseRules({releaseRules: testReleaseRules}, {cwd}); +test('Accept a "releaseRules" option', async t => { + const releaseRules = await loadReleaseRules({releaseRules: testReleaseRules}, {cwd}); t.deepEqual(releaseRules, testReleaseRules); }); -test('Accept a "releaseRules" option that reference a requierable module', t => { - const releaseRules = loadReleaseRules({releaseRules: './test/fixtures/release-rules'}, {cwd}); +test('Accept a "releaseRules" option that reference a requierable module', async t => { + const releaseRules = await loadReleaseRules({releaseRules: './test/fixtures/release-rules.js'}, {cwd}); t.deepEqual(releaseRules, testReleaseRules); }); -test('Return undefined if "releaseRules" not set', t => { - const releaseRules = loadReleaseRules({}, {cwd}); +test('Return undefined if "releaseRules" not set', async t => { + const releaseRules = await loadReleaseRules({}, {cwd}); t.is(releaseRules, undefined); }); -test('Preserve release rules set to "false" or "null"', t => { - const releaseRules = loadReleaseRules( +test('Preserve release rules set to "false" or "null"', async t => { + const releaseRules = await loadReleaseRules( { releaseRules: [ {type: 'feat', release: false}, @@ -39,32 +39,32 @@ test('Preserve release rules set to "false" or "null"', t => { ]); }); -test('Throw error if "releaseRules" reference invalid commit type', t => { - t.throws(() => loadReleaseRules({releaseRules: [{tag: 'Update', release: 'invalid'}]}, {cwd}), { +test('Throw error if "releaseRules" reference invalid commit type', async t => { + await t.throwsAsync(() => loadReleaseRules({releaseRules: [{tag: 'Update', release: 'invalid'}]}, {cwd}), { message: /Error in commit-analyzer configuration: "invalid" is not a valid release type\. Valid values are:\[?.*]/, }); }); -test('Throw error if a rule in "releaseRules" does not have a release type', t => { - t.throws(() => loadReleaseRules({releaseRules: [{tag: 'Update'}]}, {cwd}), { +test('Throw error if a rule in "releaseRules" does not have a release type', async t => { + await t.throwsAsync(() => loadReleaseRules({releaseRules: [{tag: 'Update'}]}, {cwd}), { message: /Error in commit-analyzer configuration: rules must be an object with a "release" property/, }); }); -test('Throw error if "releaseRules" is not an Array or a String', t => { - t.throws(() => loadReleaseRules({releaseRules: {}}, {cwd}), { +test('Throw error if "releaseRules" is not an Array or a String', async t => { + await t.throwsAsync(() => loadReleaseRules({releaseRules: {}}, {cwd}), { message: /Error in commit-analyzer configuration: "releaseRules" must be an array of rules/, }); }); -test('Throw error if "releaseRules" option reference a requierable module that is not an Array or a String', t => { - t.throws(() => loadReleaseRules({releaseRules: './test/fixtures/release-rules-invalid'}, {cwd}), { +test('Throw error if "releaseRules" option reference a requierable module that is not an Array or a String', async t => { + await t.throwsAsync(() => loadReleaseRules({releaseRules: './test/fixtures/release-rules-invalid.js'}, {cwd}), { message: /Error in commit-analyzer configuration: "releaseRules" must be an array of rules/, }); }); -test('Throw error if "releaseRules" contains an undefined rule', t => { - t.throws(() => loadReleaseRules({releaseRules: [{type: 'feat', release: 'minor'}, undefined]}, {cwd}), { +test('Throw error if "releaseRules" contains an undefined rule', async t => { + await t.throwsAsync(() => loadReleaseRules({releaseRules: [{type: 'feat', release: 'minor'}, undefined]}, {cwd}), { message: /Error in commit-analyzer configuration: rules must be an object with a "release" property/, }); });