An open API service indexing awesome lists of open source software.

https://github.com/yowainwright/stdouttojson

Transforms stdout (standard out) to JSON πŸ“‡
https://github.com/yowainwright/stdouttojson

cli cli-testing exec json stdout testing typescript

Last synced: 2 months ago
JSON representation

Transforms stdout (standard out) to JSON πŸ“‡

Awesome Lists containing this project

README

        

# stdoutToJSON πŸ“‡

![Typed with TypeScript](https://flat.badgen.net/badge/icon/Typed?icon=typescript&label&labelColor=blue&color=555555)
[![npm version](https://badge.fury.io/js/stdouttojson.svg)](https://badge.fury.io/js/stdouttojson)
![ci](https://github.com/yowainwright/stdouttojson/actions/workflows/ci.yml/badge.svg)
[![Github](https://badgen.net/badge/icon/github?icon=github&label&color=grey)](https://github.com/yowainwright/stdouttojson)
![Twitter](https://img.shields.io/twitter/url?url=https%3A%2F%2Fgithub.com%2Fyowainwright%2Fstdouttojson)

**stdoutToJSON** is JavaScript utility for converting [stdout](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)) to JSON.

This is useful for using `JSON` which has been output as `stdout`.

---

## Why Use _stdoutToJSON_?

**stdoutToJSON** takes in a `stdout` string of **JSON-like** shape and reconstructs to be parsable by `JSON.parse`. πŸ‘Œ

**_This is very useful for testing CLI stdout outputs!_**

**_For example, say you have some `stdout` like so._**

```typescript
"{\n" +
" options: { isTestingCLI: true },\n" +
" urls: [ 'https://example.com?gclid=test-clickid' ],\n" +
" cookies: [ { name: 'foo', value: '1' } ]\n" +
"}\n";
```

**_With **stdoutToJSON**, you can pass that `stdout` as an argument!_**

```typescript
const stdout = "{\n" +
" options: { isTestingCLI: true },\n" +
" urls: [ 'https://example.com?gclid=test-clickid' ],\n" +
" cookies: [ { name: 'foo', value: '1' } ]\n" +
"}\n";
const json = stdoutToJSON(stdout);
```

**_And you will be get usable JSON!!_**

```typescript
{
options: { isTestingCLI: "true" },
urls: ["https://example.com?gclid=test-clickid"],
cookies: [{ name: "foo", value: "1" }],
}
```

For more detail, here's an [architectural gist](https://gist.github.com/yowainwright/ba8164ad5d968f35ae86e2ba6c91c592) for reference.

---

## Basic Usage

The following snippet (a CLI unit test) represents a basic use-case and what the **stdoutToJSON** does.

```typescript
import { exec } from 'child_process';
import { stdoutToJSON } from 'stdoutToJSON';
// or, const stdoutToJSON from 'stdoutToJSON';
// or, const { stdoutToJSON } = require('stdoutToJSON')
// or, const stdoutToJSON = require('stdoutToJSON').default

describe('cli', () => {
it('returns stdout of an expected shape', (done) => {
exec(`${ --someJSONKey 'foo' }`, (_, stdout) => {
const { someJSONKey } = stdoutToJSON(stdout); // where "someJSONKey" could be any expected key
expect(someJSONkey).toEqual('foo');
});
});
});

```

---

## Arguments

| argument | required or optional | description |
| --- | --- | --- |
| **`stdout`** | `required` | a string of JSON-like shape |
| **`matchers`** | `optional` | an optional array to perform further string operations \***or** `null` |
| **`debug`** | `optional` | an optional boolean to enable debugging |

\***nullish matcher** arguments can be used to enable debugging with the default matchers.

```typescript
stoutToJSON('{"foo": "bar"}', null, true); // enables debugging with standard matchers
```

---

## Advanced Usage

This example provides insight into using the `matchers` argument.

```typescript
import { exec } from 'child_process';
import stdoutToJSON, { INITIAL_MATCHERS } from 'stdoutToJSON';
// import stdoutJSON from 'stdoutJSON'; (also works)

describe('cli', () => {
it('returns stdout of an expected shape', (done) => {
exec(`${ --someJSONKey 'foo'}`, (_, stdout) => {
const UPDATED_MATCHERS = INITIAL_MATCHERS.concat([{ value: ' = T & { [key: string]: unknown };
```

### `Matcher`

A type used to describe a **"matcher"** which is an input **value** (a regex string to match) and an output **edit** (a string to be output for each match within a string)

```typescript
/**
* @description the Matcher shape matches a regex input string and expected output string, useful `String.prototype.replace`
* @param {value} string a string contain a regex pattern to match
* @param {edit} string
*/
export type Matcher = {
value: string;
edit: string;
};
```

---

## Synopsis

Being able to quickly test CLI commands is imperative to my daily workflow.

`stdoutToJSON` allows me to hack CLI programs and quickly test the `stdout` ouput within tests. See the end-to-end example below for the full picture.
## End-to-end Example

The example below displays a CLI program code block and a code block which tests the CLI program.
### Example CLI Program

Using a boolean flag (`--isTestingCLI`), the CLI program is able to exited before actually executing it's purpose (the `script`).

Adding a `console.log` in the if block of the flag check produces an `stdout` output which can be tested.

```typescript
#!/usr/bin/env node
const { program } = require("commander");
const { cosmiconfigSync } = require("cosmiconfig");
const { script } = require("./script");
const version = "VERSION";

/**
* @notes
* This config name is intentionally not specific to this pragram.
* Hopefully, more scripts can be added!
*/
const explorer = cosmiconfigSync("config");

/**
* action
* @param {Options} options
* @notes
* a default config is used by default
* a config passed in via arguments trumps the default config
* an individual config trumps the config passed in via arguments
*/
export function action(options: Options = {}): void {
const { config: defaultConfig = {} } = explorer.search() || {};
const urls = options?.urls || defaultConfig?.urls || [];
const config = options?.config || defaultConfig;
if (options.isTestingCLI) {
console.log({ urls, config });
return;
}
script({ options });
}

program
.version(version)
.description("tests cli")
.option("-u, --urls [urls...]", "urls to run scripts on")
.option("-c, --config ", "config file to use")
.option("-t, --isTestingCLI", "enables CLI testing, no scripts are run")
.action(action)
.parse(process.argv);

export { program };
```

### Example CLI Program Test

Because the CLI program exits and outputs `stdout`, the `stdout` output can be tested! However, `stdout` produces an awkward string if the `console.log` contains more than a simple string. This is the the big initial use-case for `stdoutToJSON`.

Using `stdoutToJSON` we can do a deep test of the `stdout` output!

This makes it easy the test the CLI itself in an efficient way!

```typescript
import { exec } from "child_process";
import { stdoutToJSON } from "stdoutToJSON";

describe("program", () => {
it("works with defaults", (done) => {
exec(
`ts-node ../src/program.ts --isTestingCLI`,
(err, stdout) => {
if (err) {
done();
return;
}

const { config, url } =
convertStdoutToJson(stdout);
expect(url).toEqual([]);
expect(config).toEqual({});
done();
}
);
});

it("prefers config urls to an empty array", (done) => {
exec(
`ts-node ../src/program.ts --isTestingCLI --config .configrc`,
(err, stdout) => {
if (err) {
done();
return;
}

const { config, urls } =
convertStdoutToJson(stdout);
expect(urls).toEqual(['https://localhost:3000/', 'https://test.com']);
expect(config.urls).toEqual(['https://localhost:3000/', 'https://test.com']);
done();
}
);
});

it("prefers urls options over config.urls or an empty array", (done) => {
exec(
`ts-node ../src/program.ts --isTestingCLI --config .configrc --urls 'https://foo.com' 'https://bar.com'`,
(err, stdout) => {
if (err) {
done();
return;
}

const { config, urls } =
convertStdoutToJson(stdout);
expect(urls).toEqual(['https://foo.com', 'https://bar.com']);
expect(config.urls).toEqual(['https://localhost:3000/', 'https://test.com']);
done();
}
);
});
});
```

---

## Debugging

Listed below are some issue with using this tool and how to fix them.

### Types Errors with the returned result

```typescript
import { Options } from '../types'

...

const { options } = stdoutToJSON(stdout)
const optionsResults = (options as Options)
// should be good to go!

...

```

---

## Security

**stdoutToJSON** has no dependencies and is meant to be installed as a `devDependency`.

AKA if you're testing a CLI's interface it's a no-brainer to use for unit testing! Its tiny and secure. πŸ›‘

---

## Local Setup

1. Clone
```
git clone [email protected]:yowainwright/stdoutToJSON.git
```

2. Setup
```
nvm i && pnpm i -g && pnpm i && pnpm prepare
# nvm or equivalent
```

3. Write awesomeness + a test. πŸš€

---

## Videos

[![Loom video](https://cdn.loom.com/sessions/thumbnails/6b7082bd802b43618646242477701def-with-play.gif)](https://loom.com/share/6b7082bd802b43618646242477701def)

---

The name was changed from `stdoutJSON` to `stdoutToJSON`. Thanks to [OolongHell](https://www.reddit.com/r/node/comments/tx8sxo/stdoutjson_a_simple_node_js_utility_to_make/i3njq3w/?context=3) for assistance in making the reasoning and use case of this utility clearer.

Feel free to reach/fork with improvementsβ€”or if I can help clarify the docs. If you have a stdout string that doesn't work, please make an [issue](/issues), or submit a [pull request](/pulls) with a [test](src/__tests__/index.test.ts) and an updated [matcher](src/index.ts). See [the setup](#local-setup) instructions. Thanks! 🀝

---

Made by [@yowainwright](https://github.com/yowainwright), MIT 2022