From d15401841e17efcb08f54a25829834f586e84a8e Mon Sep 17 00:00:00 2001 From: Pierre Cavin Date: Tue, 3 Oct 2023 02:58:52 +0200 Subject: [PATCH 1/5] feat: add support for ESM presets --- index.js | 4 ++-- lib/load-parser-config.js | 12 ++++------- lib/load-release-rules.js | 14 ++++--------- lib/module-loader.js | 24 ++++++++++++++++++++++ package-lock.json | 2 +- package.json | 1 - test/load-release-rules.test.js | 36 ++++++++++++++++----------------- 7 files changed, 53 insertions(+), 40 deletions(-) create mode 100644 lib/module-loader.js diff --git a/index.js b/index.js index 4ff2f2ec..98e36b22 100644 --- a/index.js +++ b/index.js @@ -23,11 +23,11 @@ const debug = debugFactory("semantic-release:commit-analyzer"); * @param {Array} context.commits The commits to analyze. * @param {String} context.cwd The current working directory. * - * @returns {String|null} the type of release to create based on the list of commits or `null` if no release has to be done. + * @returns {Promise} the type of release to create based on the list of commits or `null` if no release has to be done. */ export 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; diff --git a/lib/load-parser-config.js b/lib/load-parser-config.js index 44163652..bfea8051 100644 --- a/lib/load-parser-config.js +++ b/lib/load-parser-config.js @@ -1,9 +1,5 @@ -import { dirname } from "node:path"; -import { fileURLToPath } from "node:url"; -import { promisify } from "node:util"; -import { isPlainObject } from "lodash-es"; -import importFrom from "import-from"; import conventionalChangelogAngular from "conventional-changelog-angular"; +import { importModule } from "./module-loader.js"; /** * Load `conventional-changelog-parser` options. Handle presets that return either a `Promise` or a `Promise`. @@ -14,17 +10,17 @@ import conventionalChangelogAngular from "conventional-changelog-angular"; * @param {Object} pluginConfig.parserOpts Additional `conventional-changelog-parser` options that will overwrite ones loaded by `preset` or `config`. * @param {Object} context The semantic-release context. * @param {String} context.cwd The current working directory. + * * @return {Promise} a `Promise` that resolve to the `conventional-changelog-parser` options. */ export default async ({ preset, config, parserOpts, presetConfig }, { cwd }) => { let loadedConfig; - const __dirname = dirname(fileURLToPath(import.meta.url)); if (preset) { const presetPackage = `conventional-changelog-${preset.toLowerCase()}`; - loadedConfig = await (importFrom.silent(__dirname, presetPackage) || importFrom(cwd, presetPackage))(presetConfig); + loadedConfig = await (await importModule(cwd, presetPackage))(presetConfig); } else if (config) { - loadedConfig = await (importFrom.silent(__dirname, config) || importFrom(cwd, config))(); + loadedConfig = await (await importModule(cwd, config))(); } else { loadedConfig = await conventionalChangelogAngular(); } diff --git a/lib/load-release-rules.js b/lib/load-release-rules.js index 66d5dfbe..312f0a72 100644 --- a/lib/load-release-rules.js +++ b/lib/load-release-rules.js @@ -1,8 +1,6 @@ -import { dirname } from "node:path"; -import { fileURLToPath } from "node:url"; import { isUndefined } from "lodash-es"; -import importFrom from "import-from"; import RELEASE_TYPES from "./default-release-types.js"; +import { importModule } from "./module-loader.js"; /** * Load and validate the `releaseRules` rules. @@ -15,17 +13,13 @@ import RELEASE_TYPES from "./default-release-types.js"; * @param {Object} context The semantic-release context. * @param {String} context.cwd The current working directory. * - * @return {Array} the loaded and validated `releaseRules`. + * @return {Promise} the loaded and validated `releaseRules`. */ -export default ({ releaseRules }, { cwd }) => { +export default async ({ releaseRules }, { cwd }) => { let loadedReleaseRules; - const __dirname = dirname(fileURLToPath(import.meta.url)); if (releaseRules) { - loadedReleaseRules = - typeof releaseRules === "string" - ? importFrom.silent(__dirname, releaseRules) || importFrom(cwd, releaseRules) - : releaseRules; + loadedReleaseRules = typeof releaseRules === "string" ? await importModule(cwd, releaseRules) : releaseRules; if (!Array.isArray(loadedReleaseRules)) { throw new TypeError('Error in commit-analyzer configuration: "releaseRules" must be an array of rules'); diff --git a/lib/module-loader.js b/lib/module-loader.js new file mode 100644 index 00000000..494e77bf --- /dev/null +++ b/lib/module-loader.js @@ -0,0 +1,24 @@ +import { join } from "node:path"; + +/** + * Import a module from node_modules or current working directory. + * + * @param {string} cwd the current working directory. + * @param {string} moduleName npm package name or path relative to cwd. + * + * @return {Promise} the loaded module's default export. + */ +export const importModule = async (cwd, moduleName) => { + const localModulePath = join(cwd, moduleName); + try { + return (await import(moduleName)).default; + } catch (e) { + try { + return (await import(localModulePath)).default; + } catch (e) { + const error = new Error(`Cannot find module "${moduleName}" or "${localModulePath}".`); + error.code = "MODULE_NOT_FOUND"; + throw error; + } + } +}; diff --git a/package-lock.json b/package-lock.json index 2758578f..412cfd8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "conventional-commits-filter": "^4.0.0", "conventional-commits-parser": "^5.0.0", "debug": "^4.0.0", - "import-from": "^4.0.0", "lodash-es": "^4.17.21", "micromatch": "^4.0.2" }, @@ -2226,6 +2225,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/import-from/-/import-from-4.0.0.tgz", "integrity": "sha512-P9J71vT5nLlDeV8FHs5nNxaLbrpfAV5cF5srvbZfpwpcJoM/xZR3hiv+q+SAnuSmuGbXMWud063iIMx/V/EWZQ==", + "dev": true, "engines": { "node": ">=12.2" }, diff --git a/package.json b/package.json index 1b7b7d83..407d4ea5 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "conventional-commits-filter": "^4.0.0", "conventional-commits-parser": "^5.0.0", "debug": "^4.0.0", - "import-from": "^4.0.0", "lodash-es": "^4.17.21", "micromatch": "^4.0.2" }, diff --git a/test/load-release-rules.test.js b/test/load-release-rules.test.js index 65ba3d09..dc15df84 100644 --- a/test/load-release-rules.test.js +++ b/test/load-release-rules.test.js @@ -4,26 +4,26 @@ import testReleaseRules from "./fixtures/release-rules.cjs"; 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 requireable module', (t) => { - const releaseRules = loadReleaseRules({ releaseRules: "./test/fixtures/release-rules.cjs" }, { cwd }); +test('Accept a "releaseRules" option that reference a requireable module', async (t) => { + const releaseRules = await loadReleaseRules({ releaseRules: "./test/fixtures/release-rules.cjs" }, { 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 requirable module that is not an Array or a String', (t) => { - t.throws(() => loadReleaseRules({ releaseRules: "./test/fixtures/release-rules-invalid.cjs" }, { cwd }), { +test('Throw error if "releaseRules" option reference a requirable module that is not an Array or a String', async (t) => { + await t.throwsAsync(loadReleaseRules({ releaseRules: "./test/fixtures/release-rules-invalid.cjs" }, { 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/, }); }); From c5dfdb437d0d0565d9935065bbb583a14846f687 Mon Sep 17 00:00:00 2001 From: Pierre Cavin Date: Thu, 12 Oct 2023 01:07:21 +0200 Subject: [PATCH 2/5] refactor: restore importFrom strategy using ESM port --- lib/load-parser-config.js | 11 ++++++++--- lib/load-release-rules.js | 10 ++++++++-- lib/module-loader.js | 24 ------------------------ package-lock.json | 9 +++++++++ package.json | 1 + 5 files changed, 26 insertions(+), 29 deletions(-) delete mode 100644 lib/module-loader.js diff --git a/lib/load-parser-config.js b/lib/load-parser-config.js index bfea8051..fecbec0f 100644 --- a/lib/load-parser-config.js +++ b/lib/load-parser-config.js @@ -1,5 +1,7 @@ +import { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import importFrom from "import-from-esm"; import conventionalChangelogAngular from "conventional-changelog-angular"; -import { importModule } from "./module-loader.js"; /** * Load `conventional-changelog-parser` options. Handle presets that return either a `Promise` or a `Promise`. @@ -15,12 +17,15 @@ import { importModule } from "./module-loader.js"; */ export default async ({ preset, config, parserOpts, presetConfig }, { cwd }) => { let loadedConfig; + const __dirname = dirname(fileURLToPath(import.meta.url)); if (preset) { const presetPackage = `conventional-changelog-${preset.toLowerCase()}`; - loadedConfig = await (await importModule(cwd, presetPackage))(presetConfig); + loadedConfig = await ( + (await importFrom.silent(__dirname, presetPackage)) || (await importFrom(cwd, presetPackage)) + )(presetConfig); } else if (config) { - loadedConfig = await (await importModule(cwd, config))(); + loadedConfig = await ((await importFrom.silent(__dirname, config)) || (await importFrom(cwd, config)))(); } else { loadedConfig = await conventionalChangelogAngular(); } diff --git a/lib/load-release-rules.js b/lib/load-release-rules.js index 312f0a72..dcd3e601 100644 --- a/lib/load-release-rules.js +++ b/lib/load-release-rules.js @@ -1,6 +1,8 @@ +import { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; import { isUndefined } from "lodash-es"; +import importFrom from "import-from-esm"; import RELEASE_TYPES from "./default-release-types.js"; -import { importModule } from "./module-loader.js"; /** * Load and validate the `releaseRules` rules. @@ -17,9 +19,13 @@ import { importModule } from "./module-loader.js"; */ export default async ({ releaseRules }, { cwd }) => { let loadedReleaseRules; + const __dirname = dirname(fileURLToPath(import.meta.url)); if (releaseRules) { - loadedReleaseRules = typeof releaseRules === "string" ? await importModule(cwd, releaseRules) : releaseRules; + loadedReleaseRules = + typeof releaseRules === "string" + ? (await importFrom.silent(__dirname, releaseRules)) || (await importFrom(cwd, releaseRules)) + : releaseRules; if (!Array.isArray(loadedReleaseRules)) { throw new TypeError('Error in commit-analyzer configuration: "releaseRules" must be an array of rules'); diff --git a/lib/module-loader.js b/lib/module-loader.js deleted file mode 100644 index 494e77bf..00000000 --- a/lib/module-loader.js +++ /dev/null @@ -1,24 +0,0 @@ -import { join } from "node:path"; - -/** - * Import a module from node_modules or current working directory. - * - * @param {string} cwd the current working directory. - * @param {string} moduleName npm package name or path relative to cwd. - * - * @return {Promise} the loaded module's default export. - */ -export const importModule = async (cwd, moduleName) => { - const localModulePath = join(cwd, moduleName); - try { - return (await import(moduleName)).default; - } catch (e) { - try { - return (await import(localModulePath)).default; - } catch (e) { - const error = new Error(`Cannot find module "${moduleName}" or "${localModulePath}".`); - error.code = "MODULE_NOT_FOUND"; - throw error; - } - } -}; diff --git a/package-lock.json b/package-lock.json index 412cfd8e..236209dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "conventional-commits-filter": "^4.0.0", "conventional-commits-parser": "^5.0.0", "debug": "^4.0.0", + "import-from-esm": "^1.0.0", "lodash-es": "^4.17.21", "micromatch": "^4.0.2" }, @@ -2233,6 +2234,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-from-esm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/import-from-esm/-/import-from-esm-1.0.0.tgz", + "integrity": "sha512-9Rg+eQ3qC2sonLssXHTjmVmN3LBkh1PvWe7H3rCAFjR4vW/2mfiLQIwhwO9vvQvJUhM8DymRt0AneYFI6Fty2g==", + "engines": { + "node": ">=16.20" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", diff --git a/package.json b/package.json index 407d4ea5..bff6c21d 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "conventional-commits-filter": "^4.0.0", "conventional-commits-parser": "^5.0.0", "debug": "^4.0.0", + "import-from-esm": "^1.0.0", "lodash-es": "^4.17.21", "micromatch": "^4.0.2" }, From d6931a8bf5c7eb6b280b29ce43bacd0256905b78 Mon Sep 17 00:00:00 2001 From: Pierre Cavin Date: Thu, 12 Oct 2023 01:50:39 +0200 Subject: [PATCH 3/5] test: restore 100% branch coverage --- test/load-parser-config.test.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/load-parser-config.test.js b/test/load-parser-config.test.js index 6c3de649..46da4248 100644 --- a/test/load-parser-config.test.js +++ b/test/load-parser-config.test.js @@ -1,4 +1,6 @@ import test from "ava"; +import importFrom from "import-from-esm"; +import sinon from "sinon"; import loadParserConfig from "../lib/load-parser-config.js"; const cwd = process.cwd(); @@ -100,3 +102,12 @@ test('Throw error if "config" doesn`t exist', async (t) => { test('Throw error if "preset" doesn`t exist', async (t) => { await t.throwsAsync(loadParserConfig({ preset: "unknown-preset" }, { cwd }), { code: "MODULE_NOT_FOUND" }); }); + +test.serial("Load preset and config correctly when importFrom.silent fails", async (t) => { + sinon.stub(importFrom, "silent").returns(undefined); + + await loadPreset(t, "angular"); + await loadConfig(t, "angular"); + + sinon.restore(); +}); From 3f984be13ce46ae155a1ead3932a32a2ea335f18 Mon Sep 17 00:00:00 2001 From: Pierre Cavin Date: Sat, 21 Oct 2023 11:18:28 +0200 Subject: [PATCH 4/5] build(deps): upgrade import-from-esm to latest --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 236209dc..108b064f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "conventional-commits-filter": "^4.0.0", "conventional-commits-parser": "^5.0.0", "debug": "^4.0.0", - "import-from-esm": "^1.0.0", + "import-from-esm": "^1.0.2", "lodash-es": "^4.17.21", "micromatch": "^4.0.2" }, @@ -2235,9 +2235,9 @@ } }, "node_modules/import-from-esm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/import-from-esm/-/import-from-esm-1.0.0.tgz", - "integrity": "sha512-9Rg+eQ3qC2sonLssXHTjmVmN3LBkh1PvWe7H3rCAFjR4vW/2mfiLQIwhwO9vvQvJUhM8DymRt0AneYFI6Fty2g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/import-from-esm/-/import-from-esm-1.0.2.tgz", + "integrity": "sha512-3ffS93KSiD1sObPDvXAg5TeLGNEGJo6Sde3G6GIPWMZF/dn5tbMZ//0PyK2txckzaPgZymw3r3GE54zXmfCq/g==", "engines": { "node": ">=16.20" } diff --git a/package.json b/package.json index bff6c21d..b884a3dc 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "conventional-commits-filter": "^4.0.0", "conventional-commits-parser": "^5.0.0", "debug": "^4.0.0", - "import-from-esm": "^1.0.0", + "import-from-esm": "^1.0.2", "lodash-es": "^4.17.21", "micromatch": "^4.0.2" }, From 3d51397cd9cfae36dbcca1d378a810560438d0be Mon Sep 17 00:00:00 2001 From: Pierre Cavin Date: Mon, 6 Nov 2023 00:36:19 +0100 Subject: [PATCH 5/5] build(deps): upgrade import-from-esm to v1.0.3 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 108b064f..ec2a78dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "conventional-commits-filter": "^4.0.0", "conventional-commits-parser": "^5.0.0", "debug": "^4.0.0", - "import-from-esm": "^1.0.2", + "import-from-esm": "^1.0.3", "lodash-es": "^4.17.21", "micromatch": "^4.0.2" }, @@ -2235,9 +2235,9 @@ } }, "node_modules/import-from-esm": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/import-from-esm/-/import-from-esm-1.0.2.tgz", - "integrity": "sha512-3ffS93KSiD1sObPDvXAg5TeLGNEGJo6Sde3G6GIPWMZF/dn5tbMZ//0PyK2txckzaPgZymw3r3GE54zXmfCq/g==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/import-from-esm/-/import-from-esm-1.0.3.tgz", + "integrity": "sha512-bncoEU7L4Vi5OoeygZuBUP9IC1AxavLR4UMCwZ9FtujOhDG1PDEo7IpCdfeOxKfrMCGwoK6UXpF9q/cXTA/ejg==", "engines": { "node": ">=16.20" } diff --git a/package.json b/package.json index b884a3dc..619a9ba8 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "conventional-commits-filter": "^4.0.0", "conventional-commits-parser": "^5.0.0", "debug": "^4.0.0", - "import-from-esm": "^1.0.2", + "import-from-esm": "^1.0.3", "lodash-es": "^4.17.21", "micromatch": "^4.0.2" },