From 965f9b780990e504caaca1d25d87eda9d1adc29a Mon Sep 17 00:00:00 2001 From: Eric Cornelissen Date: Thu, 8 Jun 2023 21:59:25 +0200 Subject: [PATCH] feat: lint SVG from stdin (#82) 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 --- README.md | 2 + bin/cli.js | 116 +++++++++++++++++++++++++++++++---------------- test/cli.spec.js | 16 ++++++- 3 files changed, 93 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index e3c770c..9ea75c3 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ $ 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 @@ -26,6 +27,7 @@ $ svglint --help --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. diff --git a/bin/cli.js b/bin/cli.js index fcf0293..de3965d 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -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" } } }); @@ -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; @@ -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); - }); - }); + }); + } })(); diff --git a/test/cli.spec.js b/test/cli.spec.js index 2be78df..be44d55 100644 --- a/test/cli.spec.js +++ b/test/cli.spec.js @@ -1,3 +1,4 @@ +import fs from "fs"; import path from "path"; import process from "process"; @@ -17,12 +18,12 @@ const INVALID_SVG = path.resolve("./test/svgs/elm.test.svg"); * @param {String} cwd The working directory * @returns {Promise} 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; @@ -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() {