Skip to content

Commit

Permalink
fix: read from stdin when resume path is a dash (#465)
Browse files Browse the repository at this point in the history
* path cli arg overrides check for stdin being a pipe

* reading from stdin only when resume is specified as a dash

* using spawn to start the child process that runs the command. exporting memfs through IPC so it can be verified in tests

* updated docs

* mysterious update to package-lock
  • Loading branch information
antialias committed Dec 17, 2020
1 parent c5d057e commit 738387e
Show file tree
Hide file tree
Showing 8 changed files with 408 additions and 104 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,13 @@ When developing themes, simply change into your theme directory and run `resume

# resume data

Resume data is read from `stdin` if [`stdin.isTTY`](https://nodejs.org/api/tty.html#tty_readstream_istty) is falsy. Otherwise, the resume is read from `--path` as resolved from `process.cwd()`. `--type` defaults to `application/json`. Supported resume data mime types are:
- Setting `--resume -` tells the cli to read resume data from standard input (`stdin`), and defaults `--type` to `application/json`.
- Setting `--resume <path>` reads resume data from `path`.
- Leaving `--resume` unset defaults to reading from `resume.json` on the current working directory.

# resume mime types

Supported resume data mime types are:

- `application/json`
- `text/yaml`
Expand Down
16 changes: 8 additions & 8 deletions lib/get-resume.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import quaff from 'quaff';
import toString from 'stream-to-string';
import yaml from 'yaml-js';
import { promisify } from 'util';
import { not as stdinIsNotAPipe } from './utils/stdin-is-pipe';

const { createReadStream } = fs;
const stat = promisify(fs.stat);
Expand All @@ -15,19 +14,20 @@ const parsers = {
'application/json': (string) => JSON.parse(string),
};
export default async ({ path, mime: inputMime }) => {
if (path && (await stat(path)).isDirectory()) {
const quaffed = quaff(path);
return quaffed;
}
let input;
let mime;
if ((await stdinIsNotAPipe()) && path) {
if ('-' === path) {
mime = inputMime || lookup('.json');
input = process.stdin;
} else if (path && (await stat(path)).isDirectory()) {
return quaff(path);
}
if (!input) {
mime = inputMime || lookup(path);
input = createReadStream(resolvePath(process.cwd(), path));
}
if (!input) {
mime = inputMime || lookup('.json');
input = process.stdin;
throw new Error('resume could not be gotten from path or stdin');
}
const resumeString = await toString(input);
const parser = parsers[mime];
Expand Down
4 changes: 2 additions & 2 deletions lib/get-resume.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ describe('get-resume', () => {
}
`);
});
it('should read from process.stdin when path is falsy', async () => {
it('should read from process.stdin when path is a dash', async () => {
const stdin = mockStdin();
const gotResume = getResume({});
const gotResume = getResume({ path: '-' });
await wait();
stdin.send(
JSON.stringify({
Expand Down
2 changes: 1 addition & 1 deletion lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const normalizeTheme = (value, defaultValue) => {
.option('-f, --format <file type extension>', 'Used by `export`.')
.option(
'-r, --resume <resume filename>',
'path to the resume in json format',
"path to the resume in json format. Use '-' to read from stdin",
'resume.json',
)
.option('-p, --port <port>', 'Used by `serve` (default: 4000)', 4000)
Expand Down
121 changes: 95 additions & 26 deletions lib/main.test.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,54 @@
import { exec as execCB } from 'child_process';
import { spawn, exec as execCB } from 'child_process';
import streamToString from 'stream-to-string';
import { promisify } from 'util';
import packageJson from '../package.json';

const exec = promisify(execCB);

const run = (argv) =>
exec(
[process.execPath, 'build/test-utils/cli-test-entry.js', argv].join(' '),
const run = async (argv, { waitForVolumeExport = true, stdin = '' } = {}) => {
let volume;
let exitCode;
const child = spawn(
process.execPath,
['build/test-utils/cli-test-entry.js', ...argv],
{
stdio: ['pipe', 'pipe', 2, 'ipc'],
},
);
const allChecks = Promise.all([
waitForVolumeExport
? new Promise((volumeSet) => {
child.on('message', async (message) => {
if (message.type === 'volumeExport') {
volume = message.data;
volumeSet();
}
});
})
: true,
new Promise((processExited) => {
child.on('exit', (code) => {
exitCode = code;
processExited();
});
}),
]);
child.stdin.write(stdin);
child.stdin.end();
const stdout = await streamToString(child.stdout);
await allChecks;
return {
volume,
code: exitCode,
stdout,
};
};

describe('cli configuration', () => {
beforeAll(() => exec(packageJson.scripts.prepare));
it('should show help', async () => {
expect((await run('help')).stdout).toMatchInlineSnapshot(`
const { stdout } = await run(['help'], { waitForVolumeExport: false });
expect(stdout).toMatchInlineSnapshot(`
"Usage: resume [command] [options]
Options:
Expand All @@ -25,8 +61,9 @@ describe('cli configuration', () => {
../some/other/dir) (default:
\\"jsonresume-theme-even\\")
-f, --format <file type extension> Used by \`export\`.
-r, --resume <resume filename> path to the resume in json format
(default: \\"resume.json\\")
-r, --resume <resume filename> path to the resume in json format. Use
'-' to read from stdin (default:
\\"resume.json\\")
-p, --port <port> Used by \`serve\` (default: 4000) (default:
4000)
-s, --silent Used by \`serve\` to tell it if open
Expand All @@ -50,37 +87,69 @@ describe('cli configuration', () => {
});
describe('validate', () => {
it('should use the schema override arg', async () => {
const output = await run('validate --schema /test-resumes/only-number-schema.json --resume /test-resumes/only-number.json')
expect(output.stdout).toMatchInlineSnapshot(`""`);
expect(output.stderr).toMatchInlineSnapshot(`""`);
})
const { stdout } = await run([
'validate',
'--schema',
'/test-resumes/only-number-schema.json',
'--resume',
'/test-resumes/only-number.json',
]);
expect(stdout).toMatchInlineSnapshot(`""`);
});
it('should fail when trying to validate an invalid resume specified by the --resume option', async () => {
await expect(
run('validate --resume /test-resumes/invalid-resume.json'),
).rejects.toEqual(
expect.objectContaining({
code: 1,
}),
);
expect(
(
await run([
'validate',
'--resume',
'/test-resumes/invalid-resume.json',
])
).code,
).toEqual(1);
});
it('should validate a resume specified by the --resume option', async () => {
const output = await run('validate --resume /test-resumes/resume.json');
expect(output.stdout).toMatchInlineSnapshot(`""`);
expect(output.stderr).toMatchInlineSnapshot(`""`);
const { stdout } = await run([
'validate',
'--resume',
'/test-resumes/resume.json',
]);
expect(stdout).toMatchInlineSnapshot(`""`);
});
});
describe('export', () => {
it('should export a resume from the path specified by --resume to the path specified immediately after the export command', async () => {
const output = await run(
'export /test-resumes/exported-resume.html --resume /test-resumes/resume.json',
it('should read from stdin when path is a dash', async () => {
const { stdout, volume } = await run(
[
'export',
'/test-resumes/exported-resume-from-stdin.html',
'--resume',
'-', // this is the dash
],
{ stdin: JSON.stringify({ basics: { name: 'thomas-from-stdin' } }) },
);
expect(output.stdout).toMatchInlineSnapshot(`
expect(volume['/test-resumes/exported-resume-from-stdin.html']).toEqual(
expect.stringContaining('thomas-from-stdin'),
);
expect(stdout).toMatchInlineSnapshot(`
"
Done! Find your new .html resume at:
/test-resumes/exported-resume-from-stdin.html
"
`);
});
it('should export a resume from the path specified by --resume to the path specified immediately after the export command', async () => {
const { stdout } = await run([
'export',
'/test-resumes/exported-resume.html',
'--resume',
'/test-resumes/resume.json',
]);
expect(stdout).toMatchInlineSnapshot(`
"
Done! Find your new .html resume at:
/test-resumes/exported-resume.html
"
`);
expect(output.stderr).toMatchInlineSnapshot(`""`);
});
});
});
6 changes: 5 additions & 1 deletion lib/test-utils/cli-test-entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { patchFs } from 'fs-monkey';
import { ufs } from 'unionfs';
import * as fs from 'fs';

const vol = ufs.use(build({ mount: '/test-resumes' })).use(fs);
const mockVolume = build({ mount: '/test-resumes' });
const vol = ufs.use(mockVolume).use(fs);
patchFs(vol);
require('../main.js');
process.once('beforeExit', () => {
process.send({ data: mockVolume.toJSON(), type: 'volumeExport' });
});
16 changes: 0 additions & 16 deletions lib/utils/stdin-is-pipe.js

This file was deleted.

Loading

0 comments on commit 738387e

Please sign in to comment.