From d170f73e0b2a8a853a7bc58977a6b4b0887d8f17 Mon Sep 17 00:00:00 2001 From: Matt Travi Date: Tue, 24 Jan 2023 23:24:02 -0600 Subject: [PATCH] feat(plugins): add support for loading ESM plugins (#2688) Co-authored-by: Matt Travi Co-authored-by: Gregor Martynus <39992+gr2m@users.noreply.github.com> --- lib/plugins/utils.js | 16 +++++++++------ test/fixtures/plugin-esm-named-exports.js | 3 +++ test/integration.test.js | 24 +++++++++++++++++++++++ test/plugins/utils.test.js | 13 +++++++++++- 4 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 test/fixtures/plugin-esm-named-exports.js diff --git a/lib/plugins/utils.js b/lib/plugins/utils.js index dbc892d8a2..addc8d1e2d 100644 --- a/lib/plugins/utils.js +++ b/lib/plugins/utils.js @@ -52,14 +52,18 @@ export async function loadPlugin({cwd}, name, pluginsPath) { ? dirname(resolveFrom.silent(__dirname, pluginsPath[name]) || resolveFrom(cwd, pluginsPath[name])) : __dirname; - if (!isFunction(name)) { - const file = resolveFrom.silent(basePath, name) || resolveFrom(cwd, name); - // See https://github.com/mysticatea/eslint-plugin-node/issues/250 - // eslint-disable-next-line node/no-unsupported-features/es-syntax - name = (await import(`file://${file}`)).default; + if (isFunction(name)) { + return name; } - return name; + const file = resolveFrom.silent(basePath, name) || resolveFrom(cwd, name); + const { default: cjsExport, ...esmNamedExports } = await import(`file://${file}`); + + if (cjsExport) { + return cjsExport; + } + + return esmNamedExports; } export function parseConfig(plugin) { diff --git a/test/fixtures/plugin-esm-named-exports.js b/test/fixtures/plugin-esm-named-exports.js new file mode 100644 index 0000000000..1678f959a9 --- /dev/null +++ b/test/fixtures/plugin-esm-named-exports.js @@ -0,0 +1,3 @@ +export async function verifyConditions(pluginConfig, context) { + context.logger.log("verifyConditions called"); +} diff --git a/test/integration.test.js b/test/integration.test.js index 484b6136a7..235b1b2e97 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -44,6 +44,7 @@ const cli = path.resolve("./bin/semantic-release.js"); const pluginError = path.resolve("./test/fixtures/plugin-error"); const pluginInheritedError = path.resolve("./test/fixtures/plugin-error-inherited"); const pluginLogEnv = path.resolve("./test/fixtures/plugin-log-env"); +const pluginEsmNamedExports = path.resolve("./test/fixtures/plugin-esm-named-exports"); test.before(async () => { await Promise.all([gitbox.start(), npmRegistry.start(), mockServer.start()]); @@ -713,3 +714,26 @@ test("Use the repository URL as is if none of the given git credentials are vali dummyUrl ); }); + +test("ESM Plugin with named exports", async (t) => { + const packageName = "log-secret"; + // Create a git repository, set the current working directory at the root of the repo + t.log("Create git repository"); + const { cwd, repositoryUrl } = await gitbox.createRepo(packageName); + await writeJson(path.resolve(cwd, "package.json"), { + name: packageName, + version: "0.0.0-dev", + repository: { url: repositoryUrl }, + release: { plugins: [pluginEsmNamedExports] }, + }); + + t.log("$ semantic-release"); + const { stdout, stderr } = await execa(cli, [], { + env: { ...env, MY_TOKEN: "secret token" }, + cwd, + reject: false, + extendEnv: false, + }); + + t.regex(stdout, new RegExp(`verifyConditions called`)); +}); diff --git a/test/plugins/utils.test.js b/test/plugins/utils.test.js index 2e9bfd95b1..160c01d7c3 100644 --- a/test/plugins/utils.test.js +++ b/test/plugins/utils.test.js @@ -199,7 +199,18 @@ test('loadPlugin', async (t) => { await loadPlugin({cwd}, './plugin-noop.cjs', {'./plugin-noop.cjs': './test/fixtures'}), 'From a shareable config context' ); - t.is(func, await loadPlugin({cwd}, func, {}), 'Defined as a function'); + t.is( + (await import("../fixtures/plugin-noop.cjs")).default, + await loadPlugin({ cwd }, "./plugin-noop.cjs", { "./plugin-noop.cjs": "./test/fixtures" }), + "From a shareable config context" + ); + const { ...namedExports } = await import("../fixtures/plugin-esm-named-exports.js"); + const plugin = await loadPlugin({ cwd }, "./plugin-esm-named-exports.js", { + "./plugin-esm-named-exports.js": "./test/fixtures", + }); + + t.deepEqual(namedExports, plugin, "ESM with named exports"); + t.is(func, await loadPlugin({ cwd }, func, {}), "Defined as a function"); }); test('parseConfig', (t) => {