import test from "ava"; import { noop } from "lodash-es"; import { stub } from "sinon"; import normalize from "../../lib/plugins/normalize.js"; const cwd = process.cwd(); test.beforeEach((t) => { // Stub the logger functions t.context.log = stub(); t.context.error = stub(); t.context.success = stub(); t.context.stderr = { write: stub() }; t.context.logger = { log: t.context.log, error: t.context.error, success: t.context.success, scope: () => t.context.logger, }; }); test("Normalize and load plugin from string", async (t) => { const plugin = await normalize( { cwd, options: {}, logger: t.context.logger }, "verifyConditions", "./test/fixtures/plugin-noop.cjs", {} ); t.is(plugin.pluginName, "./test/fixtures/plugin-noop.cjs"); t.is(typeof plugin, "function"); t.deepEqual(t.context.success.args[0], ['Loaded plugin "verifyConditions" from "./test/fixtures/plugin-noop.cjs"']); }); test("Normalize and load plugin from object", async (t) => { const plugin = await normalize( { cwd, options: {}, logger: t.context.logger }, "publish", { path: "./test/fixtures/plugin-noop.cjs" }, {} ); t.is(plugin.pluginName, "./test/fixtures/plugin-noop.cjs"); t.is(typeof plugin, "function"); t.deepEqual(t.context.success.args[0], ['Loaded plugin "publish" from "./test/fixtures/plugin-noop.cjs"']); }); test("Normalize and load plugin from a base file path", async (t) => { const plugin = await normalize( { cwd, options: {}, logger: t.context.logger }, "verifyConditions", "./plugin-noop.cjs", { "./plugin-noop.cjs": "./test/fixtures", } ); t.is(plugin.pluginName, "./plugin-noop.cjs"); t.is(typeof plugin, "function"); t.deepEqual(t.context.success.args[0], [ 'Loaded plugin "verifyConditions" from "./plugin-noop.cjs" in shareable config "./test/fixtures"', ]); }); test('Wrap plugin in a function that add the "pluginName" to the error"', async (t) => { const plugin = await normalize({ cwd, options: {}, logger: t.context.logger }, "verifyConditions", "./plugin-error", { "./plugin-error": "./test/fixtures", }); const error = await t.throwsAsync(plugin({ options: {} })); t.is(error.pluginName, "./plugin-error"); }); test('Wrap plugin in a function that add the "pluginName" to multiple errors"', async (t) => { const plugin = await normalize( { cwd, options: {}, logger: t.context.logger }, "verifyConditions", "./plugin-errors", { "./plugin-errors": "./test/fixtures", } ); const errors = [...(await t.throwsAsync(plugin({ options: {} }))).errors]; for (const error of errors) { t.is(error.pluginName, "./plugin-errors"); } }); test("Normalize and load plugin from function", async (t) => { const pluginFunction = () => {}; const plugin = await normalize({ cwd, options: {}, logger: t.context.logger }, "", pluginFunction, {}); t.is(plugin.pluginName, "[Function: pluginFunction]"); t.is(typeof plugin, "function"); }); test("Normalize and load plugin that returns multiple functions", async (t) => { const plugin = await normalize( { cwd, options: {}, logger: t.context.logger }, "verifyConditions", "./test/fixtures/multi-plugin.cjs", {} ); t.is(typeof plugin, "function"); t.deepEqual(t.context.success.args[0], ['Loaded plugin "verifyConditions" from "./test/fixtures/multi-plugin.cjs"']); }); test('Wrap "analyzeCommits" plugin in a function that validate the output of the plugin', async (t) => { const analyzeCommits = stub().resolves(2); const plugin = await normalize( { cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger }, "analyzeCommits", analyzeCommits, {} ); const error = await t.throwsAsync(plugin({ options: {} })); t.is(error.code, "EANALYZECOMMITSOUTPUT"); t.is(error.name, "SemanticReleaseError"); t.truthy(error.message); t.truthy(error.details); t.regex(error.details, /2/); }); test('Wrap "generateNotes" plugin in a function that validate the output of the plugin', async (t) => { const generateNotes = stub().resolves(2); const plugin = await normalize( { cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger }, "generateNotes", generateNotes, {} ); const error = await t.throwsAsync(plugin({ options: {} })); t.is(error.code, "EGENERATENOTESOUTPUT"); t.is(error.name, "SemanticReleaseError"); t.truthy(error.message); t.truthy(error.details); t.regex(error.details, /2/); }); test('Wrap "publish" plugin in a function that validate the output of the plugin', async (t) => { const publish = stub().resolves(2); const plugin = await normalize( { cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger }, "publish", publish, {} ); const error = await t.throwsAsync(plugin({ options: {} })); t.is(error.code, "EPUBLISHOUTPUT"); t.is(error.name, "SemanticReleaseError"); t.truthy(error.message); t.truthy(error.details); t.regex(error.details, /2/); }); test('Wrap "addChannel" plugin in a function that validate the output of the plugin', async (t) => { const addChannel = stub().resolves(2); const plugin = await normalize( { cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger }, "addChannel", addChannel, {} ); const error = await t.throwsAsync(plugin({ options: {} })); t.is(error.code, "EADDCHANNELOUTPUT"); t.is(error.name, "SemanticReleaseError"); t.truthy(error.message); t.truthy(error.details); t.regex(error.details, /2/); }); test('Plugin is called with "pluginConfig" (with object definition) and input', async (t) => { const pluginFunction = stub().resolves(); const pluginConf = { path: pluginFunction, conf: "confValue" }; const options = { global: "globalValue" }; const plugin = await normalize({ cwd, options, logger: t.context.logger }, "", pluginConf, {}); await plugin({ options: {}, param: "param" }); t.true( pluginFunction.calledWithMatch( { conf: "confValue", global: "globalValue" }, { param: "param", logger: t.context.logger } ) ); }); test('Plugin is called with "pluginConfig" (with array definition) and input', async (t) => { const pluginFunction = stub().resolves(); const pluginConf = [pluginFunction, { conf: "confValue" }]; const options = { global: "globalValue" }; const plugin = await normalize({ cwd, options, logger: t.context.logger }, "", pluginConf, {}); await plugin({ options: {}, param: "param" }); t.true( pluginFunction.calledWithMatch( { conf: "confValue", global: "globalValue" }, { param: "param", logger: t.context.logger } ) ); }); test('Prevent plugins to modify "pluginConfig"', async (t) => { const pluginFunction = stub().callsFake((pluginConfig) => { pluginConfig.conf.subConf = "otherConf"; }); const pluginConf = { path: pluginFunction, conf: { subConf: "originalConf" } }; const options = { globalConf: { globalSubConf: "originalGlobalConf" } }; const plugin = await normalize({ cwd, options, logger: t.context.logger }, "", pluginConf, {}); await plugin({ options: {} }); t.is(pluginConf.conf.subConf, "originalConf"); t.is(options.globalConf.globalSubConf, "originalGlobalConf"); }); test("Prevent plugins to modify its input", async (t) => { const pluginFunction = stub().callsFake((pluginConfig, options) => { options.param.subParam = "otherParam"; }); const input = { param: { subParam: "originalSubParam" }, options: {} }; const plugin = await normalize({ cwd, options: {}, logger: t.context.logger }, "", pluginFunction, {}); await plugin(input); t.is(input.param.subParam, "originalSubParam"); }); test("Return noop if the plugin is not defined", async (t) => { const plugin = await normalize({ cwd, options: {}, logger: t.context.logger }); t.is(plugin, noop); }); test('Always pass a defined "pluginConfig" for plugin defined with string', async (t) => { // Call the normalize function with the path of a plugin that returns its config const plugin = await normalize( { cwd, options: {}, logger: t.context.logger }, "", "./test/fixtures/plugin-result-config", {} ); const pluginResult = await plugin({ options: {} }); t.deepEqual(pluginResult.pluginConfig, {}); }); test('Always pass a defined "pluginConfig" for plugin defined with path', async (t) => { // Call the normalize function with the path of a plugin that returns its config const plugin = await normalize( { cwd, options: {}, logger: t.context.logger }, "", { path: "./test/fixtures/plugin-result-config" }, {} ); const pluginResult = await plugin({ options: {} }); t.deepEqual(pluginResult.pluginConfig, {}); }); test("Throws an error if the plugin return an object without the expected plugin function", async (t) => { const error = await t.throwsAsync(() => normalize( { cwd, options: {}, logger: t.context.logger }, "nonExistentPlugin", "./test/fixtures/multi-plugin.cjs", {} ) ); t.is(error.code, "EPLUGIN"); t.is(error.name, "SemanticReleaseError"); t.truthy(error.message); t.truthy(error.details); }); test("Throws an error if the plugin is not found", async (t) => { await t.throwsAsync( () => normalize({ cwd, options: {}, logger: t.context.logger }, "nonExistentPlugin", "non-existing-path", {}), { message: /Cannot find module 'non-existing-path'/, code: "MODULE_NOT_FOUND", instanceOf: Error, } ); });