From 2cceb3459a014bf825b94863f4924917a7b060dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Mond=C3=A9jar?= Date: Sat, 22 Oct 2022 16:33:52 +0200 Subject: [PATCH] feat: autodiscover ESM and CJS configuration files (#64) --- bin/cli.js | 9 +- src/cli/config.js | 126 ++++++++++++++---- test/cli.spec.js | 62 ++++++++- test/projects/cjs/bar/.svglintrc.js | 11 ++ test/projects/cjs/bar/a/b/c/.keep | 0 test/projects/cjs/bar/package.json | 1 + test/projects/cjs/foo/.svglintrc.mjs | 11 ++ test/projects/cjs/foo/package.json | 1 + test/projects/esm/bar/.svglintrc.cjs | 11 ++ test/projects/esm/bar/package.json | 1 + test/projects/esm/foo/.svglintrc.js | 11 ++ .../projects/esm/foo/custom-svglint-config.js | 11 ++ test/projects/esm/foo/package.json | 1 + 13 files changed, 221 insertions(+), 35 deletions(-) create mode 100644 test/projects/cjs/bar/.svglintrc.js create mode 100644 test/projects/cjs/bar/a/b/c/.keep create mode 100644 test/projects/cjs/bar/package.json create mode 100644 test/projects/cjs/foo/.svglintrc.mjs create mode 100644 test/projects/cjs/foo/package.json create mode 100644 test/projects/esm/bar/.svglintrc.cjs create mode 100644 test/projects/esm/bar/package.json create mode 100644 test/projects/esm/foo/.svglintrc.js create mode 100644 test/projects/esm/foo/custom-svglint-config.js create mode 100644 test/projects/esm/foo/package.json diff --git a/bin/cli.js b/bin/cli.js index 192963a..b13ffd3 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -8,7 +8,7 @@ import gui from "../src/cli/gui.js"; import Logger from "../src/lib/logger.js"; import SVGLint from "../src/svglint.js"; // @ts-ignore -import { getConfigurationFile } from "../src/cli/config.js"; +import { loadConfigurationFile } from "../src/cli/config.js"; import meow from "meow"; import { chalk } from "../src/cli/util.js"; import glob from "glob"; @@ -64,11 +64,8 @@ process.on("exit", () => { // load the config let configObj; try { - const configFile = await getConfigurationFile(cli.flags.config); - if (configFile) { - const module = await import(`file://${configFile}`); - configObj = module.default; - } else { + configObj = await loadConfigurationFile(cli.flags.config); + if (configObj === null) { logger.debug("No configuration file found"); if (cli.flags.config) { logger.error("Configuration file not found"); diff --git a/src/cli/config.js b/src/cli/config.js index fb8ecf3..ff80000 100644 --- a/src/cli/config.js +++ b/src/cli/config.js @@ -2,35 +2,115 @@ import path from "path"; import fs from "fs"; /** - * Gets the configuration file to use - * @param {String} filename The filename to look for - * @param {String} folder The folder to look in - * @returns {Promise} The path to the configuration file, or false + * Check if a file exists + * @param {String} filepath The file to check for existence + * @returns {Promise} true if the file exists, false otherwise */ -function getConfigurationFile(filename=".svglintrc.js", folder=process.cwd()) { - const resolved = path.isAbsolute(filename) - ? filename - : path.resolve(folder, filename); - return new Promise((res,rej)=>{ - fs.access(resolved, fs.constants.F_OK, err=>{ +function fileExists(filepath) { + return new Promise((res)=>{ + fs.access(filepath, fs.constants.F_OK, err => { if (!err) { - // if file exists, finalize - res(resolved); - } else { - const parent = path.resolve(folder, ".."); - if (parent === folder) { - return res(false); - } - // if not, get next folder - getConfigurationFile( - filename, - path.resolve(folder, "..") - ).then(res).catch(rej); + return res(true); } + res(false); }); }); } +/** + * Check if the package is an ESM project + * @param {String} filepath The package.json file path + * @returns {Boolean} true if the package is ESM based, false otherwise + **/ +function isEsmPackageJson(filename) { + try { + let pkg = JSON.parse(fs.readFileSync(filename, "utf8")); + return pkg.type && pkg.type === "module"; + } catch (err) { + return false; + } +} + +/** + * Check if the default configuration file for SVGLint exists in a folder + * @param {String} folder The folder to check + * @returns {Promise} The path to the file if it exists, false otherwise + */ +async function getDefaultConfigurationFile(folder) { + let filepath = path.resolve(folder, ".svglintrc.js"); + if (await fileExists(filepath)) { + return filepath; + } + + const packageJsonPath = path.resolve(folder, "package.json"); + if (await fileExists(packageJsonPath)) { + filepath = path.resolve( + folder, + isEsmPackageJson(packageJsonPath) ? ".svglintrc.cjs" : ".svglintrc.mjs", + ); + if (await fileExists(filepath)) { + return filepath; + } + } + + return false; +} + +/** + * Gets the configuration file to use, traversing the parent folders + * @param {String} folder The folder to look in + * @returns {Promise} The path to the configuration file, or false + */ +async function getDefaultConfigurationFileTraversingParents(folder) { + let filepath = await getDefaultConfigurationFile(folder); + if (filepath) { + return filepath; + } else { + const parent = path.resolve(folder, ".."); + if (parent === folder) { + return false; + } + return await getDefaultConfigurationFileTraversingParents(parent); + } +} + +/** + * Get the configuration file to use + * @param {String} filename The filename to look for + * @param {String} folder The folder to look in + * @returns {Promise} The path to the configuration file, or false + */ +async function getConfigurationFile(filename, folder) { + let filepath; + if (filename) { + filepath = path.isAbsolute(filename) + ? filename + : path.resolve(folder, filename); + if (await fileExists(filepath)) { + return filepath; + } else { + return false; + } + } + + return await getDefaultConfigurationFileTraversingParents(folder); +} + +/** + * Load the configuration object from the SVGLint configuration file + * @param {String} folder The folder to start looking in + * @returns {Promise} The configuration object, or null if no SVGLint configuration file is found + */ +async function loadConfigurationFile(filename, folder=process.cwd()) { + const filepath = await getConfigurationFile(filename, folder); + if (filepath) { + const module = await import(`file://${filepath}`); + return module.default; + } else { + return null; + } +} + export { - getConfigurationFile, + loadConfigurationFile, }; diff --git a/test/cli.spec.js b/test/cli.spec.js index e052085..57b900f 100644 --- a/test/cli.spec.js +++ b/test/cli.spec.js @@ -1,3 +1,5 @@ +import path from "path"; + import { execa } from "execa"; import expect from "expect"; @@ -5,14 +7,22 @@ process.on("unhandledRejection", error => { console.error(error); // eslint-disable-line no-console }); +const VALID_SVG = path.resolve("./test/svgs/attr.test.svg"); +const INVALID_SVG = path.resolve("./test/svgs/elm.test.svg"); + /** * Run the CLI with a given list of arguments * @param {String[]} args The list of args + * @param {String} cwd The working directory * @returns {Promise} The CLI output */ -async function execCliWith(args) { +async function execCliWith(args, cwd=process.cwd()) { try { - return await execa("./bin/cli.js", args); + return await execa( + path.resolve("./bin/cli.js"), + args, + {cwd: path.resolve(cwd)}, + ); } catch (error) { return error; } @@ -32,15 +42,55 @@ describe("CLI", function(){ }); it("should succeed with a valid SVG", async function(){ - const validSvg = "./test/svgs/attr.test.svg"; - const { failed } = await execCliWith([validSvg]); + const { failed } = await execCliWith([VALID_SVG]); expect(failed).toBeFalsy(); }); it("should fail with an invalid SVG", async function(){ - const invalidSvg = "./test/svgs/elm.test.svg"; - const { failed, exitCode } = await execCliWith([invalidSvg]); + const { failed, exitCode } = await execCliWith([INVALID_SVG]); + expect(failed).toBeTruthy(); + expect(exitCode).toBe(1); + }); +}); + +describe("Configuration files", function() { + it("should fail passing an non-existent file path to --config", async function() { + const { failed, exitCode } = await execCliWith( + [VALID_SVG, "--config", "./this/file/does/not-exist.js"], + ); expect(failed).toBeTruthy(); expect(exitCode).toBe(1); }); + + it("should succeed passing an existent file path to --config", async function() { + const { failed } = await execCliWith( + [VALID_SVG, "--config", "test/projects/esm/foo/custom-svglint-config.js"] + ); + expect(failed).toBeFalsy(); + }); + + it("should succeed with an ESM .js config in a ESM project with type=module", async function() { + const { failed } = await execCliWith([VALID_SVG], "test/projects/esm/foo"); + expect(failed).toBeFalsy(); + }); + + it("should succeed with an CJS .js config in a CJS project with type=commonjs", async function() { + const { failed } = await execCliWith([VALID_SVG], "test/projects/cjs/bar"); + expect(failed).toBeFalsy(); + }); + + it("should succeed with a ESM .mjs config in a CJS project with type=commonjs", async function() { + const { failed } = await execCliWith([VALID_SVG], "test/projects/cjs/foo"); + expect(failed).toBeFalsy(); + }); + + it("should succeed with a CJS .cjs config in a ESM project with type=module", async function() { + const { failed } = await execCliWith([VALID_SVG], "test/projects/esm/bar"); + expect(failed).toBeFalsy(); + }); + + it("should succeed in a nested folder inside a project with a root config file", async function() { + const { failed } = await execCliWith([VALID_SVG], "test/projects/cjs/bar/a/b/c"); + expect(failed).toBeFalsy(); + }); }); diff --git a/test/projects/cjs/bar/.svglintrc.js b/test/projects/cjs/bar/.svglintrc.js new file mode 100644 index 0000000..53b97f1 --- /dev/null +++ b/test/projects/cjs/bar/.svglintrc.js @@ -0,0 +1,11 @@ +module.exports = { + rules: { + attr: { + "rule::selector": "path", + "d": true, + }, + elm: { + "g": true, + } + } +}; diff --git a/test/projects/cjs/bar/a/b/c/.keep b/test/projects/cjs/bar/a/b/c/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/projects/cjs/bar/package.json b/test/projects/cjs/bar/package.json new file mode 100644 index 0000000..01c2c99 --- /dev/null +++ b/test/projects/cjs/bar/package.json @@ -0,0 +1 @@ +{"type": "commonjs", "private": true} diff --git a/test/projects/cjs/foo/.svglintrc.mjs b/test/projects/cjs/foo/.svglintrc.mjs new file mode 100644 index 0000000..23edc41 --- /dev/null +++ b/test/projects/cjs/foo/.svglintrc.mjs @@ -0,0 +1,11 @@ +export default { + rules: { + attr: { + "rule::selector": "path", + "d": true, + }, + elm: { + "g": true, + } + } +}; diff --git a/test/projects/cjs/foo/package.json b/test/projects/cjs/foo/package.json new file mode 100644 index 0000000..11177c5 --- /dev/null +++ b/test/projects/cjs/foo/package.json @@ -0,0 +1 @@ +{"private": true, "type": "commonjs"} diff --git a/test/projects/esm/bar/.svglintrc.cjs b/test/projects/esm/bar/.svglintrc.cjs new file mode 100644 index 0000000..53b97f1 --- /dev/null +++ b/test/projects/esm/bar/.svglintrc.cjs @@ -0,0 +1,11 @@ +module.exports = { + rules: { + attr: { + "rule::selector": "path", + "d": true, + }, + elm: { + "g": true, + } + } +}; diff --git a/test/projects/esm/bar/package.json b/test/projects/esm/bar/package.json new file mode 100644 index 0000000..2c53bf1 --- /dev/null +++ b/test/projects/esm/bar/package.json @@ -0,0 +1 @@ +{"private": true, "type": "module"} diff --git a/test/projects/esm/foo/.svglintrc.js b/test/projects/esm/foo/.svglintrc.js new file mode 100644 index 0000000..23edc41 --- /dev/null +++ b/test/projects/esm/foo/.svglintrc.js @@ -0,0 +1,11 @@ +export default { + rules: { + attr: { + "rule::selector": "path", + "d": true, + }, + elm: { + "g": true, + } + } +}; diff --git a/test/projects/esm/foo/custom-svglint-config.js b/test/projects/esm/foo/custom-svglint-config.js new file mode 100644 index 0000000..23edc41 --- /dev/null +++ b/test/projects/esm/foo/custom-svglint-config.js @@ -0,0 +1,11 @@ +export default { + rules: { + attr: { + "rule::selector": "path", + "d": true, + }, + elm: { + "g": true, + } + } +}; diff --git a/test/projects/esm/foo/package.json b/test/projects/esm/foo/package.json new file mode 100644 index 0000000..2c53bf1 --- /dev/null +++ b/test/projects/esm/foo/package.json @@ -0,0 +1 @@ +{"private": true, "type": "module"}