Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: autodiscover ESM and CJS configuration files #64

Merged
merged 8 commits into from
Oct 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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";
ericcornelissen marked this conversation as resolved.
Show resolved Hide resolved

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"}