Skip to content

Commit

Permalink
feat: autodiscover ESM and CJS configuration files (#64)
Browse files Browse the repository at this point in the history
  • Loading branch information
mondeja authored Oct 22, 2022
1 parent ac049dd commit 2cceb34
Show file tree
Hide file tree
Showing 13 changed files with 221 additions and 35 deletions.
9 changes: 3 additions & 6 deletions bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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");
Expand Down
126 changes: 103 additions & 23 deletions src/cli/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<String,Boolean>} The path to the configuration file, or false
* Check if a file exists
* @param {String} filepath The file to check for existence
* @returns {Promise<Boolean>} 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<String,Boolean>} 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<String,Boolean>} 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<String,Boolean>} 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<Object,null>} 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,
};
62 changes: 56 additions & 6 deletions test/cli.spec.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import path from "path";

import { execa } from "execa";
import expect from "expect";

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<Object>} 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;
}
Expand All @@ -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();
});
});
11 changes: 11 additions & 0 deletions test/projects/cjs/bar/.svglintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = {
rules: {
attr: {
"rule::selector": "path",
"d": true,
},
elm: {
"g": true,
}
}
};
Empty file.
1 change: 1 addition & 0 deletions test/projects/cjs/bar/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"type": "commonjs", "private": true}
11 changes: 11 additions & 0 deletions test/projects/cjs/foo/.svglintrc.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default {
rules: {
attr: {
"rule::selector": "path",
"d": true,
},
elm: {
"g": true,
}
}
};
1 change: 1 addition & 0 deletions test/projects/cjs/foo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"private": true, "type": "commonjs"}
11 changes: 11 additions & 0 deletions test/projects/esm/bar/.svglintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = {
rules: {
attr: {
"rule::selector": "path",
"d": true,
},
elm: {
"g": true,
}
}
};
1 change: 1 addition & 0 deletions test/projects/esm/bar/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"private": true, "type": "module"}
11 changes: 11 additions & 0 deletions test/projects/esm/foo/.svglintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default {
rules: {
attr: {
"rule::selector": "path",
"d": true,
},
elm: {
"g": true,
}
}
};
11 changes: 11 additions & 0 deletions test/projects/esm/foo/custom-svglint-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default {
rules: {
attr: {
"rule::selector": "path",
"d": true,
},
elm: {
"g": true,
}
}
};
1 change: 1 addition & 0 deletions test/projects/esm/foo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"private": true, "type": "module"}

0 comments on commit 2cceb34

Please sign in to comment.