Skip to content

Commit

Permalink
feat: esm (#247)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `@semantic-release/commit-analyzer` is now a native ES Module

BREAKING CHANGE: When setting the `releaseRules` option to a path, the path must include the `.js` extension, and the file must be an ES Module itself, exporting an array as default

Before:

```json
{
  "plugins": [
    ["@semantic-release/commit-analyzer", {
      "preset": "angular",
      "releaseRules": "./config/release-rules"
    }],
    "@semantic-release/release-notes-generator"
  ]
}
```
```js
// File: config/release-rules.js
module.exports = [
  {type: 'docs', scope: 'README', release: 'patch'},
  {type: 'refactor', scope: 'core-*', release: 'minor'},
  {type: 'refactor', release: 'patch'},
];
```

After:

```json
{
  "plugins": [
    ["@semantic-release/commit-analyzer", {
      "preset": "angular",
      "releaseRules": "./config/release-rules.js"
    }],
    "@semantic-release/release-notes-generator"
  ]
}
```
```js
// File: config/release-rules.js
export default [
  {type: 'docs', scope: 'README', release: 'patch'},
  {type: 'refactor', scope: 'core-*', release: 'minor'},
  {type: 'refactor', release: 'patch'},
];
```

Co-authored-by: Gregor Martynus <39992+gr2m@users.noreply.github.com>
  • Loading branch information
jpedroh and gr2m committed Aug 21, 2021
1 parent e7e56f6 commit a773c8b
Show file tree
Hide file tree
Showing 17 changed files with 106 additions and 78 deletions.
8 changes: 6 additions & 2 deletions README.md
Expand Up @@ -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": [
Expand All @@ -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'},
Expand Down
27 changes: 15 additions & 12 deletions 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.
Expand All @@ -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;

Expand Down Expand Up @@ -79,4 +82,4 @@ async function analyzeCommits(pluginConfig, context) {
return releaseType;
}

module.exports = {analyzeCommits};
export {analyzeCommits};
17 changes: 10 additions & 7 deletions 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.
Expand All @@ -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
Expand All @@ -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 => {
Expand Down
4 changes: 2 additions & 2 deletions 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.
Expand All @@ -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);
2 changes: 1 addition & 1 deletion lib/default-release-rules.js
Expand Up @@ -3,7 +3,7 @@
*
* @type {Array}
*/
module.exports = [
export default [
{breaking: true, release: 'major'},
{revert: true, release: 'patch'},
// Angular
Expand Down
2 changes: 1 addition & 1 deletion lib/default-release-types.js
Expand Up @@ -3,4 +3,4 @@
*
* @type {Array}
*/
module.exports = ['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease'];
export default ['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease'];
11 changes: 11 additions & 0 deletions 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;
}
};
16 changes: 8 additions & 8 deletions 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<Array>` or a `Promise<Function>`.
Expand All @@ -14,16 +14,16 @@ const conventionalChangelogAngular = require('conventional-changelog-angular');
* @param {String} context.cwd The current working directory.
* @return {Promise<Object>} 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'
Expand Down
15 changes: 8 additions & 7 deletions 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.
Expand All @@ -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');
Expand Down
9 changes: 7 additions & 2 deletions 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_)",
Expand Down Expand Up @@ -56,7 +57,7 @@
"semantic-release"
],
"license": "MIT",
"main": "index.js",
"exports": "./index.js",
"nyc": {
"include": [
"lib/**/*.js",
Expand Down Expand Up @@ -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": {
Expand Down
4 changes: 2 additions & 2 deletions 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 = {
Expand Down
4 changes: 2 additions & 2 deletions 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'));
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/release-rules-invalid.js
@@ -1 +1 @@
module.exports = 42;
export default 42;
2 changes: 1 addition & 1 deletion 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'},
Expand Down
12 changes: 6 additions & 6 deletions 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};
});
Expand Down Expand Up @@ -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}
);

Expand Down Expand Up @@ -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/,
});
});
Expand Down
7 changes: 4 additions & 3 deletions 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();

Expand Down Expand Up @@ -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 => {
Expand Down
42 changes: 21 additions & 21 deletions 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},
Expand All @@ -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/,
});
});

0 comments on commit a773c8b

Please sign in to comment.