From 6900865324c4eae56c06f01bde9a5ae2f3690917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominykas=20Bly=C5=BE=C4=97?= Date: Fri, 3 Nov 2023 22:33:43 +0200 Subject: [PATCH] Support extending ESM based configurations (#3037) Co-authored-by: Matt Travi --- lib/get-config.js | 16 ++++++-- test/get-config.test.js | 86 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 4 deletions(-) diff --git a/lib/get-config.js b/lib/get-config.js index 429577e1a4..aa119f302d 100644 --- a/lib/get-config.js +++ b/lib/get-config.js @@ -1,6 +1,5 @@ -import { dirname, resolve } from "node:path"; +import { dirname, extname } from "node:path"; import { fileURLToPath } from "node:url"; -import { createRequire } from "node:module"; import { castArray, isNil, isPlainObject, isString, pickBy } from "lodash-es"; import { readPackageUp } from "read-pkg-up"; @@ -14,7 +13,6 @@ import { parseConfig, validatePlugin } from "./plugins/utils.js"; const debug = debugConfig("semantic-release:config"); const __dirname = dirname(fileURLToPath(import.meta.url)); -const require = createRequire(import.meta.url); const CONFIG_NAME = "release"; @@ -35,7 +33,17 @@ export default async (context, cliOptions) => { options = { ...(await castArray(extendPaths).reduce(async (eventualResult, extendPath) => { const result = await eventualResult; - const extendsOptions = require(resolveFrom.silent(__dirname, extendPath) || resolveFrom(cwd, extendPath)); + const resolvedPath = resolveFrom.silent(__dirname, extendPath) || resolveFrom(cwd, extendPath); + const importAssertions = + extname(resolvedPath) === ".json" + ? { + assert: { + type: "json", + }, + } + : undefined; + + const { default: extendsOptions } = await import(resolvedPath, importAssertions); // For each plugin defined in a shareable config, save in `pluginsPath` the extendable config path, // so those plugin will be loaded relative to the config file diff --git a/test/get-config.test.js b/test/get-config.test.js index 395c8b575c..c2e874f86f 100644 --- a/test/get-config.test.js +++ b/test/get-config.test.js @@ -389,6 +389,92 @@ test.serial('Read configuration from an array of paths in "extends"', async (t) t.deepEqual(result, { options: expectedOptions, plugins: pluginsConfig }); }); +test.serial('Read configuration from an array of CJS files in "extends"', async (t) => { + // Create a git repository, set the current working directory at the root of the repo + const { cwd } = await gitRepo(); + const pkgOptions = { extends: ["./shareable1.cjs", "./shareable2.cjs"] }; + const options1 = { + verifyRelease: "verifyRelease1", + analyzeCommits: { path: "analyzeCommits1", param: "analyzeCommits_param1" }, + branches: ["test_branch"], + repositoryUrl: "https://host.null/owner/module.git", + }; + const options2 = { + verifyRelease: "verifyRelease2", + generateNotes: "generateNotes2", + analyzeCommits: { path: "analyzeCommits2", param: "analyzeCommits_param2" }, + branches: ["test_branch"], + tagFormat: `v\${version}`, + plugins: false, + }; + // Create package.json and shareable.json in repository root + await outputJson(path.resolve(cwd, "package.json"), { release: pkgOptions }); + await writeFile(path.resolve(cwd, "shareable1.cjs"), `module.exports = ${JSON.stringify(options1)}`); + await writeFile(path.resolve(cwd, "shareable2.cjs"), `module.exports = ${JSON.stringify(options2)}`); + const expectedOptions = { ...options1, ...options2, branches: ["test_branch"] }; + // Verify the plugins module is called with the plugin options from shareable1.mjs and shareable2.mjs + td.when( + plugins( + { options: expectedOptions, cwd }, + { + verifyRelease1: "./shareable1.cjs", + verifyRelease2: "./shareable2.cjs", + generateNotes2: "./shareable2.cjs", + analyzeCommits1: "./shareable1.cjs", + analyzeCommits2: "./shareable2.cjs", + } + ) + ).thenResolve(pluginsConfig); + + const result = await t.context.getConfig({ cwd }); + + // Verify the options contains the plugin config from shareable1.json and shareable2.json + t.deepEqual(result, { options: expectedOptions, plugins: pluginsConfig }); +}); + +test.serial('Read configuration from an array of ESM files in "extends"', async (t) => { + // Create a git repository, set the current working directory at the root of the repo + const { cwd } = await gitRepo(); + const pkgOptions = { extends: ["./shareable1.mjs", "./shareable2.mjs"] }; + const options1 = { + verifyRelease: "verifyRelease1", + analyzeCommits: { path: "analyzeCommits1", param: "analyzeCommits_param1" }, + branches: ["test_branch"], + repositoryUrl: "https://host.null/owner/module.git", + }; + const options2 = { + verifyRelease: "verifyRelease2", + generateNotes: "generateNotes2", + analyzeCommits: { path: "analyzeCommits2", param: "analyzeCommits_param2" }, + branches: ["test_branch"], + tagFormat: `v\${version}`, + plugins: false, + }; + // Create package.json and shareable.json in repository root + await outputJson(path.resolve(cwd, "package.json"), { release: pkgOptions }); + await writeFile(path.resolve(cwd, "shareable1.mjs"), `export default ${JSON.stringify(options1)}`); + await writeFile(path.resolve(cwd, "shareable2.mjs"), `export default ${JSON.stringify(options2)}`); + const expectedOptions = { ...options1, ...options2, branches: ["test_branch"] }; + // Verify the plugins module is called with the plugin options from shareable1.mjs and shareable2.mjs + td.when( + plugins( + { options: expectedOptions, cwd }, + { + verifyRelease1: "./shareable1.mjs", + verifyRelease2: "./shareable2.mjs", + generateNotes2: "./shareable2.mjs", + analyzeCommits1: "./shareable1.mjs", + analyzeCommits2: "./shareable2.mjs", + } + ) + ).thenResolve(pluginsConfig); + + const result = await t.context.getConfig({ cwd }); + + // Verify the options contains the plugin config from shareable1.json and shareable2.json + t.deepEqual(result, { options: expectedOptions, plugins: pluginsConfig }); +}); + test.serial('Prioritize configuration from config file over "extends"', async (t) => { // Create a git repository, set the current working directory at the root of the repo const { cwd } = await gitRepo();