Skip to content

Commit

Permalink
Refactor plugin loader + tests (remove proxyquire)
Browse files Browse the repository at this point in the history
  • Loading branch information
webpro committed Jul 21, 2021
1 parent ad03f71 commit 570e349
Show file tree
Hide file tree
Showing 10 changed files with 213 additions and 130 deletions.
14 changes: 9 additions & 5 deletions lib/plugin/factory.js
@@ -1,13 +1,15 @@
import path from 'path';
import { createRequire } from 'module';
import _ from 'lodash';
import requireCwd from 'import-cwd';
import _debug from 'debug';
import Version from './version/Version.js';
import Git from './git/Git.js';
import GitLab from './gitlab/GitLab.js';
import GitHub from './github/GitHub.js';
import npm from './npm/npm.js';

const require = createRequire(import.meta.url);

const debug = _debug('release-it:plugins');

const pluginNames = ['npm', 'git', 'github', 'gitlab', 'version'];
Expand All @@ -20,12 +22,14 @@ const plugins = {
npm: npm
};

const load = pluginName => {
const load = async pluginName => {
let plugin = null;
try {
plugin = require(pluginName);
const module = await import(require.resolve(pluginName));
plugin = module.default;
} catch (err) {
plugin = requireCwd(pluginName);
const module = await import(require.resolve(path.resolve(pluginName)));
plugin = module.default;
}
return [path.parse(pluginName).name, plugin];
};
Expand All @@ -37,7 +41,7 @@ export let getPlugins = async (config, container) => {
const enabledExternalPlugins = await _.reduce(
context.plugins,
async (result, pluginConfig, pluginName) => {
const [name, Plugin] = load(pluginName);
const [name, Plugin] = await load(pluginName);
const [namespace, options] = pluginConfig.length === 2 ? pluginConfig : [name, pluginConfig];
config.setContext({ [namespace]: options });
if (await Plugin.isEnabled(options)) {
Expand Down
2 changes: 0 additions & 2 deletions package.json
Expand Up @@ -71,7 +71,6 @@
"git-url-parse": "11.5.0",
"globby": "11.0.4",
"got": "11.8.2",
"import-cwd": "3.0.0",
"inquirer": "8.1.1",
"is-ci": "3.0.0",
"lodash": "4.17.21",
Expand Down Expand Up @@ -102,7 +101,6 @@
"nock": "13.1.1",
"nyc": "15.1.0",
"prettier": "2.3.2",
"proxyquire": "2.1.3",
"sinon": "11.1.1",
"strip-ansi": "6.0.0"
},
Expand Down
4 changes: 2 additions & 2 deletions test/config.js
Expand Up @@ -44,8 +44,8 @@ test('should set CI mode', t => {
t.is(config.isCI, true);
});

test('should detect CI mode', t => {
const isCI = require('is-ci');
test('should detect CI mode', async t => {
const { default: isCI } = await import('is-ci');
const config = new Config();
t.is(config.options.ci, isCI);
t.is(config.isCI, isCI);
Expand Down
4 changes: 2 additions & 2 deletions test/github.js
Expand Up @@ -266,8 +266,8 @@ test.serial('should skip authentication and collaborator checks when running on

authStub.restore();
collaboratorStub.restore();
process.env.GITHUB_ACTIONS = GITHUB_ACTIONS;
process.env.GITHUB_ACTOR = GITHUB_ACTOR;
process.env.GITHUB_ACTIONS = GITHUB_ACTIONS ?? '';
process.env.GITHUB_ACTOR = GITHUB_ACTOR ?? '';
});

test('should handle octokit client error (without retries)', async t => {
Expand Down
2 changes: 0 additions & 2 deletions test/npm.js
Expand Up @@ -279,7 +279,6 @@ test('should skip checks', async t => {
});

test('should publish to a different/scoped registry', async t => {
delete require.cache[require.resolve('../package.json')];
mock({
[path.resolve('package.json')]: JSON.stringify({
name: '@my-scope/my-pkg',
Expand Down Expand Up @@ -317,7 +316,6 @@ test('should publish to a different/scoped registry', async t => {
});

test('should not publish when `npm version` fails', async t => {
delete require.cache[require.resolve('../package.json')];
mock({
[path.resolve('package.json')]: JSON.stringify({
name: '@my-scope/my-pkg',
Expand Down
216 changes: 108 additions & 108 deletions test/plugins.js
@@ -1,13 +1,15 @@
import path from 'path';
import test from 'ava';
import sh from 'shelljs';
import proxyquire from 'proxyquire';
import sinon from 'sinon';
import Log from '../lib/log.js';
import Spinner from '../lib/spinner.js';
import Config from '../lib/config.js';
import Plugin from '../lib/plugin/Plugin.js';
import { mkTmpDir, gitAdd } from './util/helpers.js';
import { parseGitUrl } from '../lib/util.js';
import runTasks from '../lib/tasks.js';
import MyPlugin from './stub/plugin.js';
import ReplacePlugin from './stub/plugin-replace.js';
import ContextPlugin from './stub/plugin-context.js';
import { mkTmpDir } from './util/helpers.js';
import ShellStub from './stub/shell.js';

const noop = Promise.resolve();
Expand Down Expand Up @@ -36,45 +38,36 @@ const getContainer = options => {
};

test.serial.beforeEach(t => {
const bare = mkTmpDir();
const target = mkTmpDir();
sh.pushd('-q', bare);
sh.exec(`git init --bare .`);
sh.exec(`git clone ${bare} ${target}`);
sh.pushd('-q', target);
gitAdd('line', 'file', 'Add file');
t.context = { bare, target };
const dir = mkTmpDir();
sh.pushd('-q', dir);
t.context = { dir };
});

test.serial.afterEach(() => {
sandbox.resetHistory();
});

const myPlugin = sandbox.createStubInstance(Plugin);
myPlugin.namespace = 'my-plugin';
const MyPlugin = sandbox.stub().callsFake(() => myPlugin);
const myLocalPlugin = sandbox.createStubInstance(Plugin);
const MyLocalPlugin = sandbox.stub().callsFake(() => myLocalPlugin);
const replacePlugin = sandbox.createStubInstance(Plugin);
const ReplacePlugin = sandbox.stub().callsFake(() => replacePlugin);

const staticMembers = { isEnabled: () => true, disablePlugin: () => null };
const options = { '@global': true, '@noCallThru': true };
const runTasks = proxyquire('../lib/tasks', {
'my-plugin': Object.assign(MyPlugin, staticMembers, options),
'/my/plugin': Object.assign(MyLocalPlugin, staticMembers, options),
'replace-plugin': Object.assign(ReplacePlugin, staticMembers, options, {
disablePlugin: () => ['version', 'git']
})
});

test.serial('should instantiate plugins and execute all release-cycle methods', async t => {
sh.exec('npm init -f');

sh.mkdir('my-plugin');
sh.pushd('-q', 'my-plugin');
sh.exec('npm link release-it');
sh.ShellString("const { Plugin } = require('release-it'); module.exports = " + MyPlugin.toString()).toEnd('index.js');
sh.popd();

sh.mkdir('-p', 'my/plugin');
sh.pushd('-q', 'my/plugin');
sh.exec('npm link release-it');
sh.ShellString("const { Plugin } = require('release-it'); module.exports = " + MyPlugin.toString()).toEnd('index.js');
sh.popd();

const config = {
plugins: {
'my-plugin': {
name: 'foo'
},
'/my/plugin': [
'./my/plugin': [
'named-plugin',
{
name: 'bar'
Expand All @@ -84,38 +77,45 @@ test.serial('should instantiate plugins and execute all release-cycle methods',
};
const container = getContainer(config);

await runTasks({}, container);
const result = await runTasks({}, container);

t.is(MyPlugin.firstCall.args[0].namespace, 'my-plugin');
t.deepEqual(MyPlugin.firstCall.args[0].options['my-plugin'], { name: 'foo' });
t.is(MyLocalPlugin.firstCall.args[0].namespace, 'named-plugin');
t.deepEqual(MyLocalPlugin.firstCall.args[0].options['named-plugin'], { name: 'bar' });

[
'init',
'getName',
'getLatestVersion',
'getIncrement',
'getIncrementedVersionCI',
'beforeBump',
'bump',
'beforeRelease',
'release',
'afterRelease'
].forEach(method => {
t.is(myPlugin[method].callCount, 1);
t.is(myLocalPlugin[method].callCount, 1);
});
t.deepEqual(container.log.info.args, [
['my-plugin:foo:init'],
['named-plugin:bar:init'],
['my-plugin:foo:getName'],
['my-plugin:foo:getLatestVersion'],
['my-plugin:foo:getIncrement'],
['my-plugin:foo:getIncrementedVersionCI'],
['named-plugin:bar:getIncrementedVersionCI'],
['my-plugin:foo:beforeBump'],
['named-plugin:bar:beforeBump'],
['my-plugin:foo:bump:1.3.0'],
['named-plugin:bar:bump:1.3.0'],
['my-plugin:foo:beforeRelease'],
['named-plugin:bar:beforeRelease'],
['my-plugin:foo:release'],
['named-plugin:bar:release'],
['my-plugin:foo:afterRelease'],
['named-plugin:bar:afterRelease']
]);

const incrementBase = { latestVersion: '0.0.0', increment: undefined, isPreRelease: false, preReleaseId: undefined };
t.deepEqual(myPlugin.getIncrement.firstCall.args[0], incrementBase);
t.deepEqual(myPlugin.getIncrementedVersionCI.firstCall.args[0], incrementBase);
t.deepEqual(myLocalPlugin.getIncrementedVersionCI.firstCall.args[0], incrementBase);
t.is(myPlugin.bump.firstCall.args[0], '0.0.1');
t.is(myLocalPlugin.bump.firstCall.args[0], '0.0.1');
t.deepEqual(result, {
changelog: undefined,
name: 'new-project-name',
latestVersion: '1.2.3',
version: '1.3.0'
});
});

test.serial('should disable core plugins', async t => {
sh.exec('npm init -f');
sh.mkdir('replace-plugin');
sh.pushd('-q', 'replace-plugin');
sh.exec('npm link release-it');
const content = "const { Plugin } = require('release-it'); module.exports = " + ReplacePlugin.toString();
sh.ShellString(content).toEnd('index.js');
sh.popd();

const config = {
plugins: {
'replace-plugin': {}
Expand All @@ -133,56 +133,50 @@ test.serial('should disable core plugins', async t => {
});
});

test.serial('should expose context to execute commands', async t => {
const { bare } = t.context;
const latestVersion = '1.0.0';
const project = path.basename(bare);
const pkgName = 'plugin-context';
const owner = path.basename(path.dirname(bare));
gitAdd(`{"name":"${pkgName}","version":"${latestVersion}"}`, 'package.json', 'Add package.json');

class MyPlugin extends Plugin {
init() {
this.exec('echo ${version.isPreRelease}');
}
beforeBump() {
const context = this.config.getContext();
t.is(context.name, pkgName);
this.exec('echo ${name} ${repo.owner} ${repo.project} ${latestVersion} ${version}');
}
bump() {
const repo = this.config.getContext('repo');
t.is(repo.owner, owner);
t.is(repo.project, project);
t.is(repo.repository, `${owner}/${project}`);
this.exec('echo ${name} ${repo.owner} ${repo.project} ${latestVersion} ${version}');
}
beforeRelease() {
const context = this.config.getContext();
t.is(context.name, pkgName);
this.exec('echo ${name} ${repo.owner} ${repo.project} ${latestVersion} ${version} ${tagName}');
}
release() {
const context = this.config.getContext();
t.is(context.latestVersion, latestVersion);
t.is(context.version, '1.0.1');
this.exec('echo ${name} ${repo.owner} ${repo.project} ${latestVersion} ${version} ${tagName}');
}
afterRelease() {
const context = this.config.getContext();
t.is(context.tagName, '1.0.1');
this.exec('echo ${name} ${repo.owner} ${repo.project} ${latestVersion} ${version} ${tagName}');
test.serial('should support ESM-based plugins', async t => {
sh.exec('npm init -f');
sh.mkdir('my-plugin');
sh.pushd('-q', 'my-plugin');
sh.ShellString('{"name":"my-plugin","version":"1.0.0","type": "module"}').toEnd('package.json');
sh.exec('npm link release-it');
const content = "import { Plugin } from 'release-it'; " + MyPlugin.toString() + '; export default MyPlugin;';
sh.ShellString(content).toEnd('index.js');
sh.popd();

const config = {
plugins: {
'my-plugin': {}
}
}
const statics = { isEnabled: () => true, disablePlugin: () => null };
const options = { '@global': true, '@noCallThru': true };
const runTasks = proxyquire('../lib/tasks', {
'my-plugin': Object.assign(MyPlugin, statics, options)
};
const container = getContainer(config);

const result = await runTasks({}, container);

t.deepEqual(result, {
changelog: undefined,
name: 'new-project-name',
latestVersion: '1.2.3',
version: '1.3.0'
});
});

test.serial('should expose context to execute commands', async t => {
sh.ShellString('{"name":"pkg-name","version":"1.0.0"}').toEnd('package.json');
const repo = parseGitUrl('https://github.com/user/pkg');

sh.mkdir('context-plugin');
sh.pushd('-q', 'context-plugin');
sh.exec('npm link release-it');
const content = "const { Plugin } = require('release-it'); module.exports = " + ContextPlugin.toString();
sh.ShellString(content).toEnd('index.js');
sh.popd();

const container = getContainer({ plugins: { 'my-plugin': {} } });
const container = getContainer({ plugins: { 'context-plugin': {} } });
const exec = sinon.spy(container.shell, 'execFormattedCommand');

container.config.setContext({ repo });
container.config.setContext({ tagName: '1.0.1' });

await runTasks({}, container);

const pluginExecArgs = exec.args
Expand All @@ -191,10 +185,16 @@ test.serial('should expose context to execute commands', async t => {

t.deepEqual(pluginExecArgs, [
'echo false',
`echo ${pkgName} ${owner} ${project} ${latestVersion} 1.0.1`,
`echo ${pkgName} ${owner} ${project} ${latestVersion} 1.0.1`,
`echo ${pkgName} ${owner} ${project} ${latestVersion} 1.0.1 1.0.1`,
`echo ${pkgName} ${owner} ${project} ${latestVersion} 1.0.1 1.0.1`,
`echo ${pkgName} ${owner} ${project} ${latestVersion} 1.0.1 1.0.1`
'echo false',
`echo pkg-name user 1.0.0 1.0.1`,
`echo pkg-name user 1.0.0 1.0.1`,
`echo user pkg user/pkg 1.0.1`,
`echo user pkg user/pkg 1.0.1`,
`echo user pkg user/pkg 1.0.1`,
`echo user pkg user/pkg 1.0.1`,
`echo pkg 1.0.0 1.0.1 1.0.1`,
`echo pkg 1.0.0 1.0.1 1.0.1`,
`echo pkg 1.0.0 1.0.1 1.0.1`,
`echo pkg 1.0.0 1.0.1 1.0.1`
]);
});

0 comments on commit 570e349

Please sign in to comment.