From 058023f4c02616e78acfff9537ec1c8949334918 Mon Sep 17 00:00:00 2001 From: Sarah Wang <55766946+sarahsonder@users.noreply.github.com> Date: Fri, 19 Jul 2024 08:28:43 -0400 Subject: [PATCH] Added stdin/out stream options to CLI (#52) --- CHANGELOG.md | 2 + README.md | 10 +- docs/docs/06-cli.md | 46 +++-- memory-viz/README.md | 10 +- memory-viz/bin/cli.js | 112 +++++++++---- .../src/tests/__snapshots__/cli.spec.tsx.snap | 22 ++- memory-viz/src/tests/cli.spec.tsx | 157 ++++++++++-------- 7 files changed, 227 insertions(+), 132 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1d366d5..865d962a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### ✨ Enhancements +- Changed the `filePath` argument in the MemoryViz CLI to be optional and added `--output` option. + ### 🐛 Bug fixes - Fixed issue where object boxes would be drawn on top of stack frames in diagrams with large left margins. diff --git a/README.md b/README.md index 86524587..0a7b4e9f 100644 --- a/README.md +++ b/README.md @@ -54,13 +54,19 @@ For more information, check out the project [documentation website](https://www. ### MemoryViz CLI -To run MemoryViz from your terminal, run: +To use the MemoryViz CLI, run: ```console $ npx memory-viz ``` -replacing `` with the path to a file containing MemoryViz-compatible JSON. The output is an SVG image generated by MemoryViz and the image is saved in the current working directory. For more information, check out the project [documentation website](https://www.cs.toronto.edu/~david/memory-viz/docs/cli). +where `` is the path to a file containing MemoryViz-compatible JSON. If a file path is not provided, the CLI will take input from standard input. + +You may also specify an output path using the `--output` option (see documentation). If no output path is provided, the CLI will print to standard output. + +_Note_: The CLI currently does not support typing input directly into the terminal. Instead, use piping or other strategies to pass data into standard input. + +For more information, check out the project [documentation website](https://www.cs.toronto.edu/~david/memory-viz/docs/cli). ## Developers diff --git a/docs/docs/06-cli.md b/docs/docs/06-cli.md index ef2aa46c..0f71f582 100644 --- a/docs/docs/06-cli.md +++ b/docs/docs/06-cli.md @@ -4,48 +4,62 @@ title: MemoryViz CLI # MemoryViz CLI -You can run MemoryViz straight from your terminal! +Run MemoryViz straight from your terminal! -## Input +The MemoryViz CLI takes in MemoryViz-compatible JSON and returns an SVG of the memory model. -To run the MemoryViz CLI, run: +## Usage + +To use the MemoryViz CLI, run: ```console $ npx memory-viz ``` -replacing `` with the path to a file containing MemoryViz-compatible JSON. If the file content is not compatible with MemoryViz, an error will be thrown. +where `` is the path to a file containing MemoryViz-compatible JSON. If a file path is not provided, the CLI will take input from standard input. -## Output +You may also specify an output path using the `--output` option (see below). If no output path is provided, the CLI will print to standard output. -The output is an SVG image generated by MemoryViz and the image is saved in the current working directory. The name of the SVG will be the same as that of the inputted file (i.e., if the inputted file is `david-is-cool.json`, the output will be `david-is-cool.svg`). +_Note_: The CLI currently does not support typing input directly into the terminal. Instead, use piping or other strategies to pass data into standard input. ## Options -Below are optional arguments used to specify the way in which the SVG image is generated. +Below are optional arguments used to specify the way in which the SVG is outputed and generated. + +### `--output=` + +Writes generated SVG to specified path. ### `--width` Specifies the width of the generated SVG. -```console -$ npx memory-viz --width=700 -``` - ### `--height` Specifies the height of the generated SVG. +### `--roughjs_config` + +Specifies the style of the generated SVG. Please refer to the [Rough.js documentation](https://github.com/rough-stuff/rough/wiki#options) for available options. + +The argument is a comma-separated list of key-value pairs in the form ``. + +## Examples + +This takes input from a file and prints to `stdout`. + ```console -$ npx memory-viz --height=700 +$ npx memory-viz ``` -### `--roughjs_config` +This takes input from `stdin`, generates the SVG with a width of 200, and prints to `stdout`. -Specifies the style of the generated SVG. Please refer to the [Rough.js documentation](https://github.com/rough-stuff/rough/wiki#options) for available options. +```console +$ npx memory-viz --width=200 +``` -The argument is a comma-separated list of key-value pairs in the form ``. +This takes input from a file, generates the SVG with a solid red fill, and writes the SVG to the specified path. ```console -$ npx memory-viz --roughjs-config fill=red,fillStyle=solid +$ npx memory-viz --output= --roughjs-config fill=red,fillStyle=solid ``` diff --git a/memory-viz/README.md b/memory-viz/README.md index bac52335..264f49e8 100644 --- a/memory-viz/README.md +++ b/memory-viz/README.md @@ -54,13 +54,19 @@ For more information, check out the project [documentation website](https://www. ### MemoryViz CLI -To run MemoryViz from your terminal, run: +To use the MemoryViz CLI, run: ```console $ npx memory-viz ``` -replacing `` with the path to a file containing MemoryViz-compatible JSON. The output is an SVG image generated by MemoryViz and the image is saved in the current working directory. For more information, check out the project [documentation website](https://www.cs.toronto.edu/~david/memory-viz/docs/cli). +where `` is the path to a file containing MemoryViz-compatible JSON. If a file path is not provided, the CLI will take input from standard input. + +You may also specify an output path using the `--output` option (see documentation). If no output path is provided, the CLI will print to standard output. + +_Note_: The CLI currently does not support typing input directly into the terminal. Instead, use piping or other strategies to pass data into standard input. + +For more information, check out the project [documentation website](https://www.cs.toronto.edu/~david/memory-viz/docs/cli). ## Developers diff --git a/memory-viz/bin/cli.js b/memory-viz/bin/cli.js index 4ed24f36..a75353cd 100644 --- a/memory-viz/bin/cli.js +++ b/memory-viz/bin/cli.js @@ -4,6 +4,30 @@ const fs = require("fs"); const path = require("path"); const { draw } = require("memory-viz"); const { program } = require("commander"); +const { json } = require("node:stream/consumers"); + +function parseFilePath(input) { + if (input) { + return pathExists(input, `File`); + } else { + return undefined; + } +} + +function parseOutputPath(input) { + return pathExists(input, `Output path`); +} + +// helper function for parsing paths +function pathExists(inputPath, errMsg) { + const fullPath = path.resolve(process.cwd(), inputPath); + if (!fs.existsSync(fullPath)) { + console.error(`Error: ${errMsg} ${fullPath} does not exist.`); + process.exit(1); + } else { + return fullPath; + } +} function parseRoughjsConfig(input) { const pairs = input.split(","); @@ -16,8 +40,14 @@ program "Command line interface for generating memory model diagrams with MemoryViz" ) .argument( - "", - "path to a file containing MemoryViz-compatible JSON" + "[filePath]", + "path to a file containing MemoryViz-compatible JSON", + parseFilePath + ) + .option( + "--output ", + "writes generated SVG to specified path", + parseOutputPath ) .option("--width ", "width of generated SVG", "1300") .option("--height ", "height of generated SVG") @@ -29,43 +59,55 @@ program ); program.parse(); +const filePath = program.processedArgs[0]; +const options = program.opts(); -const filePath = program.args[0]; -const absolutePath = path.resolve(process.cwd(), filePath); +if (filePath) { + const fileContent = fs.readFileSync(filePath, "utf8"); -// Checks if absolutePath exists -let fileContent; -if (!fs.existsSync(absolutePath)) { - console.error(`Error: File ${absolutePath} does not exist.`); - process.exit(1); -} else { - fileContent = fs.readFileSync(absolutePath, "utf8"); -} + let jsonContent; + try { + jsonContent = JSON.parse(fileContent); + } catch (err) { + console.error(`Error: Invalid JSON\n${err.message}.`); + process.exit(1); + } -let data; -try { - data = JSON.parse(fileContent); -} catch (err) { - console.error(`Error: Invalid JSON\n${err.message}.`); - process.exit(1); + runMemoryViz(jsonContent); +} else { + json(process.stdin) + .then((jsonContent) => { + runMemoryViz(jsonContent); + }) + .catch((err) => { + console.error(`Error: ${err.message}.`); + process.exit(1); + }); } -let m; -try { - m = draw(data, true, { - width: program.opts().width, - height: program.opts().height, - roughjs_config: { options: program.opts().roughjsConfig }, - }); -} catch (err) { - console.error(`Error: ${err.message}`); - process.exit(1); -} +function runMemoryViz(jsonContent) { + let m; + try { + m = draw(jsonContent, true, { + width: options.width, + height: options.height, + roughjs_config: { options: options.roughjsConfig }, + }); + } catch (err) { + console.error(`Error: ${err.message}`); + process.exit(1); + } -const outputName = path.parse(filePath).name + ".svg"; -try { - m.save(outputName); -} catch (err) { - console.error(`Error: ${err.message}`); - process.exit(1); + try { + if (options.output) { + const outputName = path.parse(options.output).name + ".svg"; + const outputPath = path.join(options.output, outputName); + m.save(outputPath); + } else { + m.save(); + } + } catch (err) { + console.error(`Error: ${err.message}`); + process.exit(1); + } } diff --git a/memory-viz/src/tests/__snapshots__/cli.spec.tsx.snap b/memory-viz/src/tests/__snapshots__/cli.spec.tsx.snap index 8d162274..5933acb1 100644 --- a/memory-viz/src/tests/__snapshots__/cli.spec.tsx.snap +++ b/memory-viz/src/tests/__snapshots__/cli.spec.tsx.snap @@ -1,11 +1,23 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`memory-viz cli produces consistent svg when provided a variety of rough-config options 1`] = `""David is cool!"id19str"`; +exports[`memory-viz cli produces consistent svg when provided filepath and a variety of rough-config option(s) 1`] = `""David is cool!"id19str"`; -exports[`memory-viz cli produces consistent svg when provided height and width options 1`] = `""David is cool!"id19str"`; +exports[`memory-viz cli produces consistent svg when provided filepath and output option(s) 1`] = `""David is cool!"id19str"`; -exports[`memory-viz cli produces consistent svg when provided height option 1`] = `""David is cool!"id19str"`; +exports[`memory-viz cli produces consistent svg when provided filepath and stdout 1`] = ` +""David is cool!"id19str +" +`; -exports[`memory-viz cli produces consistent svg when provided width option 1`] = `""David is cool!"id19str"`; +exports[`memory-viz cli produces consistent svg when provided filepath, output, and height option(s) 1`] = `""David is cool!"id19str"`; -exports[`memory-viz cli should produce an svg that matches snapshot 1`] = `""David is cool!"id19str"`; +exports[`memory-viz cli produces consistent svg when provided filepath, output, and width option(s) 1`] = `""David is cool!"id19str"`; + +exports[`memory-viz cli produces consistent svg when provided filepath, output, height, and width option(s) 1`] = `""David is cool!"id19str"`; + +exports[`memory-viz cli produces consistent svg when provided stdin and output 1`] = `""David is cool!"id19str"`; + +exports[`memory-viz cli produces consistent svg when provided stdin and stdout 1`] = ` +""David is cool!"id19str +" +`; diff --git a/memory-viz/src/tests/cli.spec.tsx b/memory-viz/src/tests/cli.spec.tsx index b6e3e5ff..eab381ef 100644 --- a/memory-viz/src/tests/cli.spec.tsx +++ b/memory-viz/src/tests/cli.spec.tsx @@ -1,4 +1,4 @@ -const { exec } = require("child_process"); +const { exec, spawn } = require("child_process"); const path = require("path"); const fs = require("fs"); const tmp = require("tmp"); @@ -7,6 +7,7 @@ tmp.setGracefulCleanup(); const tmpFile = tmp.fileSync({ postfix: ".json" }); const filePath = tmpFile.name; +const outputPath = ".."; const input = JSON.stringify( [ { @@ -19,106 +20,118 @@ const input = JSON.stringify( null ); -describe("memory-viz cli", () => { - it("should produce an svg that matches snapshot", (done) => { +// Helper function for determining the output path of the SVG +const getSVGPath = (isOutputOption: boolean) => { + let directoryPath = ""; + let fileName = ""; + + if (isOutputOption) { + directoryPath = path.resolve(process.cwd(), outputPath); + fileName = path.basename(directoryPath + ".svg"); + } else { + directoryPath = process.cwd(); + fileName = path.basename(filePath.replace(".json", ".svg")); + } + + return path.resolve(directoryPath, fileName); +}; + +describe.each([ + { + inputs: "filepath and output", + command: `${filePath} --output=${outputPath} --roughjs-config seed=12345`, + }, + { + inputs: "filepath, output, and width", + command: `${filePath} --output=${outputPath} --width=700 --roughjs-config seed=12345`, + }, + { + inputs: "filepath, output, and height", + command: `${filePath} --output=${outputPath} --height=700 --roughjs-config seed=12345`, + }, + { + inputs: "filepath, output, height, and width", + command: `${filePath} --output=${outputPath} --height=700 width=1200 --roughjs-config seed=12345`, + }, + { + inputs: "filepath and a variety of rough-config", + command: `${filePath} --output=${outputPath} --roughjs-config seed=12345,fill=red,fillStyle=solid`, + }, +])("memory-viz cli", ({ inputs, command }) => { + it(`produces consistent svg when provided ${inputs} option(s)`, (done) => { fs.writeFileSync(filePath, input); - exec(`memory-viz ${filePath} --roughjs-config seed=12345`, (err) => { + exec(`memory-viz ${command}`, (err) => { if (err) throw err; - const svgFilePath = path.resolve( - process.cwd(), - path.basename(filePath.replace(".json", ".svg")) - ); + + const svgFilePath = getSVGPath(command.includes("--output")); const fileContent = fs.readFileSync(svgFilePath, "utf8"); + expect(fileContent).toMatchSnapshot(); fs.unlinkSync(svgFilePath); + done(); }); }); +}); - it("produces consistent svg when provided width option", (done) => { +describe("memory-viz cli", () => { + it("produces consistent svg when provided filepath and stdout", (done) => { fs.writeFileSync(filePath, input); exec( - `memory-viz ${filePath} --width=700 --roughjs-config seed=12345`, - (err) => { + `memory-viz ${filePath} --roughjs-config seed=1234`, + (err, stdout) => { if (err) throw err; - const svgFilePath = path.resolve( - process.cwd(), - path.basename(filePath.replace(".json", ".svg")) - ); - const fileContent = fs.readFileSync(svgFilePath, "utf8"); - expect(fileContent).toMatchSnapshot(); - fs.unlinkSync(svgFilePath); + expect(stdout).toMatchSnapshot(); done(); } ); }); - it("produces consistent svg when provided height option", (done) => { - fs.writeFileSync(filePath, input); + it("produces consistent svg when provided stdin and stdout", (done) => { + const command = "memory-viz"; + const args = ["--roughjs-config seed=1234"]; - exec( - `memory-viz ${filePath} --height=700 --roughjs-config seed=12345`, - (err) => { - if (err) throw err; - const svgFilePath = path.resolve( - process.cwd(), - path.basename(filePath.replace(".json", ".svg")) - ); - const fileContent = fs.readFileSync(svgFilePath, "utf8"); - expect(fileContent).toMatchSnapshot(); - fs.unlinkSync(svgFilePath); - done(); - } - ); - }); + const child = spawn(command, args, { shell: true }); - it("produces consistent svg when provided height and width options", (done) => { - fs.writeFileSync(filePath, input); + child.stdin.write(input); + child.stdin.end(); - exec( - `memory-viz ${filePath} --height=700 width=1200 --roughjs-config seed=12345`, - (err) => { - if (err) throw err; - const svgFilePath = path.resolve( - process.cwd(), - path.basename(filePath.replace(".json", ".svg")) - ); - const fileContent = fs.readFileSync(svgFilePath, "utf8"); - expect(fileContent).toMatchSnapshot(); - fs.unlinkSync(svgFilePath); - done(); - } - ); + let output = ""; + child.stdout.on("data", (data) => { + output += data.toString(); + }); + + child.on("close", (err) => { + if (err) throw err; + expect(output).toMatchSnapshot(); + done(); + }); }); - it("produces consistent svg when provided a variety of rough-config options", (done) => { - fs.writeFileSync(filePath, input); + it("produces consistent svg when provided stdin and output", (done) => { + const command = "memory-viz"; + const args = [`--output=${outputPath}`, "--roughjs-config seed=1234"]; - exec( - `memory-viz ${filePath} --roughjs-config seed=1234,fill=red,fillStyle=solid`, - (err) => { - if (err) throw err; - const svgFilePath = path.resolve( - process.cwd(), - path.basename(filePath.replace(".json", ".svg")) - ); - const fileContent = fs.readFileSync(svgFilePath, "utf8"); - expect(fileContent).toMatchSnapshot(); - fs.unlinkSync(svgFilePath); - done(); - } - ); + const child = spawn(command, args, { shell: true }); + + child.stdin.write(input); + child.stdin.end(); + + child.on("close", (err) => { + if (err) throw err; + + const svgFilePath = getSVGPath(true); + const fileContent = fs.readFileSync(svgFilePath, "utf8"); + expect(fileContent).toMatchSnapshot(); + fs.unlinkSync(svgFilePath); + done(); + }); }); }); describe.each([ - { - errorType: "invalid arguments", - command: "memory-viz", - expectedErrorMessage: "error: missing required argument 'filepath'", - }, { errorType: "non-existent file", command: "memory-viz cli-test.json",