Skip to content

Commit

Permalink
feat: lint SVG from stdin (#82)
Browse files Browse the repository at this point in the history
Extend the CLI to support providing an SVG on `stdin` [1]. The CLI will
only do this when provided with the `--stdin` flag, I choose this design
as it was the only reliable way of switching between files and stdin I
could find.

Despite the awkward diff, the CLI flow for files (i.e. if `--stdin` is
not used) is unchanged. The stdin flow is based on [2] and just calls
the JS API's `lintSource` function on the entire input.

--
1. https://nodejs.org/api/process.html#processstdin
2. https://nodejs.org/api/stream.html#readablereadsize
  • Loading branch information
ericcornelissen committed Jun 8, 2023
1 parent fb87cb7 commit 965f9b7
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 41 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ $ svglint --help
Usage:
svglint [--config config.js] [--ci] [--debug] file1.svg file2.svg
svglint --stdin [--config config.js] [--ci] [--debug] < file1.svg
Options:
--help Display this help text
--version Show the current SVGLint version
--config, -c Specify the config file. Defaults to '.svglintrc.js'
--debug, -d Show debug logs
--ci, -C Only output to stdout once, when linting is finished
--stdin Read an SVG from stdin
```

The tool can also be used through the JS API.
Expand Down
116 changes: 77 additions & 39 deletions bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,21 @@ process.on("SIGINT", () => {
const cli = meow(`
${chalk.yellow("Usage:")}
${chalk.bold("svglint")} [--config config.js] [--ci] [--debug] ${chalk.bold("file1.svg file2.svg")}
${chalk.bold("svglint")} --stdin [--config config.js] [--ci] [--debug] < ${chalk.bold("file1.svg")}
${chalk.yellow("Options:")}
${chalk.bold("--help")} Display this help text
${chalk.bold("--version")} Show the current SVGLint version
${chalk.bold("--config, -c")} Specify the config file. Defaults to '.svglintrc.js'
${chalk.bold("--debug, -d")} Show debug logs
${chalk.bold("--ci, -C")} Only output to stdout once, when linting is finished`, {
${chalk.bold("--ci, -C")} Only output to stdout once, when linting is finished
${chalk.bold("--stdin")} Read an SVG from stdin`, {
importMeta: import.meta,
flags: {
config: { type: "string", alias: "c", },
debug: { type: "boolean", alias: "d" },
ci: { type: "boolean", alias: "C" }
ci: { type: "boolean", alias: "C" },
stdin: { type: "boolean" }
}
});

Expand All @@ -70,10 +73,6 @@ process.on("exit", () => {
Logger.setLevel(Logger.LEVELS.debug);
}
GUI.setCI(cli.flags.ci);
const files = cli.input
.map(v => glob.sync(v))
.reduce((a, v) => a.concat(v), [])
.map(v => path.resolve(process.cwd(), v));

// load the config
let configObj;
Expand All @@ -93,39 +92,78 @@ process.on("exit", () => {
process.exit(EXIT_CODES.configuration);
}

// lint all the files
// also keep track so we know when every linting has finished
let hasErrors = false;
let activeLintings = files.length;
const onLintingDone = () => {
--activeLintings;
logger.debug("Linting done,", activeLintings, "to go");
if (activeLintings <= 0) {
process.exit(
hasErrors ? EXIT_CODES.violations : EXIT_CODES.success
);
}
};
files.forEach(filePath => {
SVGLint.lintFile(filePath, configObj)
.then(linting => {
// handle case where linting failed (e.g. invalid file)
if (!linting) {
onLintingDone();
return;
}

// otherwise add it to GUI and wait for it to finish
GUI.addLinting(linting);
linting.on("done", () => {
if (linting.state === linting.STATES.error) {
hasErrors = true;
if (cli.flags.stdin) {
// lint what's provided on stdin
const chunks = [];

process.stdin.on("readable", () => {
let chunk;
while (null !== (chunk = process.stdin.read())) {
chunks.push(chunk);
}
});

process.stdin.on("end", () => {
SVGLint.lintSource(chunks.join(""), configObj)
.then(linting => {
// handle case where linting failed (e.g. invalid file)
if (!linting) {
process.exit(EXIT_CODES.success);
}

// otherwise add it to GUI and wait for it to finish
GUI.addLinting(linting);
linting.on("done", () => {
if (linting.state === linting.STATES.error) {
process.exit(EXIT_CODES.violations);
} else {
process.exit(EXIT_CODES.success);
}
});
})
.catch(e => {
logger.error("Failed to lint\n", e);
});
});
} else {
// lint all the CLI specified files
const files = cli.input
.map(v => glob.sync(v))
.reduce((a, v) => a.concat(v), [])
.map(v => path.resolve(process.cwd(), v));
// keep track so we know when every linting has finished
let hasErrors = false;
let activeLintings = files.length;
const onLintingDone = () => {
--activeLintings;
logger.debug("Linting done,", activeLintings, "to go");
if (activeLintings <= 0) {
process.exit(
hasErrors ? EXIT_CODES.violations : EXIT_CODES.success
);
}
};
files.forEach(filePath => {
SVGLint.lintFile(filePath, configObj)
.then(linting => {
// handle case where linting failed (e.g. invalid file)
if (!linting) {
onLintingDone();
return;
}
onLintingDone();

// otherwise add it to GUI and wait for it to finish
GUI.addLinting(linting);
linting.on("done", () => {
if (linting.state === linting.STATES.error) {
hasErrors = true;
}
onLintingDone();
});
})
.catch(e => {
logger.error("Failed to lint file", filePath, "\n", e);
});
})
.catch(e => {
logger.error("Failed to lint file", filePath, "\n", e);
});
});
});
}
})();
16 changes: 14 additions & 2 deletions test/cli.spec.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import fs from "fs";
import path from "path";
import process from "process";

Expand All @@ -17,12 +18,12 @@ const INVALID_SVG = path.resolve("./test/svgs/elm.test.svg");
* @param {String} cwd The working directory
* @returns {Promise<Object>} The CLI output
*/
async function execCliWith(args, cwd=process.cwd()) {
async function execCliWith(args, cwd=process.cwd(), input=null) {
try {
return await execa(
path.resolve("./bin/cli.js"),
args,
{cwd: path.resolve(cwd)},
{cwd: path.resolve(cwd), input},
);
} catch (error) {
return error;
Expand Down Expand Up @@ -52,6 +53,17 @@ describe("CLI", function(){
expect(failed).toBeTruthy();
expect(exitCode).toBe(1);
});

it("should succeed with a valid SVG on stdin", async function(){
const { failed } = await execCliWith(["--stdin"], process.cwd(), fs.readFileSync(VALID_SVG));
expect(failed).toBeFalsy();
});

it("should fail with an invalid SVG on stdin", async function(){
const { failed, exitCode } = await execCliWith(["--stdin"], "test/projects/with-config", fs.readFileSync(INVALID_SVG));
expect(failed).toBeTruthy();
expect(exitCode).toBe(1);
});
});

describe("Configuration files", function() {
Expand Down

0 comments on commit 965f9b7

Please sign in to comment.