Skip to content

Commit 738387e

Browse files
authored
fix: read from stdin when resume path is a dash (#465)
* 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
1 parent c5d057e commit 738387e

8 files changed

Lines changed: 408 additions & 104 deletions

File tree

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,13 @@ When developing themes, simply change into your theme directory and run `resume
7777

7878
# resume data
7979

80-
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:
80+
- Setting `--resume -` tells the cli to read resume data from standard input (`stdin`), and defaults `--type` to `application/json`.
81+
- Setting `--resume <path>` reads resume data from `path`.
82+
- Leaving `--resume` unset defaults to reading from `resume.json` on the current working directory.
83+
84+
# resume mime types
85+
86+
Supported resume data mime types are:
8187

8288
- `application/json`
8389
- `text/yaml`

lib/get-resume.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import quaff from 'quaff';
55
import toString from 'stream-to-string';
66
import yaml from 'yaml-js';
77
import { promisify } from 'util';
8-
import { not as stdinIsNotAPipe } from './utils/stdin-is-pipe';
98

109
const { createReadStream } = fs;
1110
const stat = promisify(fs.stat);
@@ -15,19 +14,20 @@ const parsers = {
1514
'application/json': (string) => JSON.parse(string),
1615
};
1716
export default async ({ path, mime: inputMime }) => {
18-
if (path && (await stat(path)).isDirectory()) {
19-
const quaffed = quaff(path);
20-
return quaffed;
21-
}
2217
let input;
2318
let mime;
24-
if ((await stdinIsNotAPipe()) && path) {
19+
if ('-' === path) {
20+
mime = inputMime || lookup('.json');
21+
input = process.stdin;
22+
} else if (path && (await stat(path)).isDirectory()) {
23+
return quaff(path);
24+
}
25+
if (!input) {
2526
mime = inputMime || lookup(path);
2627
input = createReadStream(resolvePath(process.cwd(), path));
2728
}
2829
if (!input) {
29-
mime = inputMime || lookup('.json');
30-
input = process.stdin;
30+
throw new Error('resume could not be gotten from path or stdin');
3131
}
3232
const resumeString = await toString(input);
3333
const parser = parsers[mime];

lib/get-resume.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ describe('get-resume', () => {
4848
}
4949
`);
5050
});
51-
it('should read from process.stdin when path is falsy', async () => {
51+
it('should read from process.stdin when path is a dash', async () => {
5252
const stdin = mockStdin();
53-
const gotResume = getResume({});
53+
const gotResume = getResume({ path: '-' });
5454
await wait();
5555
stdin.send(
5656
JSON.stringify({

lib/main.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const normalizeTheme = (value, defaultValue) => {
4343
.option('-f, --format <file type extension>', 'Used by `export`.')
4444
.option(
4545
'-r, --resume <resume filename>',
46-
'path to the resume in json format',
46+
"path to the resume in json format. Use '-' to read from stdin",
4747
'resume.json',
4848
)
4949
.option('-p, --port <port>', 'Used by `serve` (default: 4000)', 4000)

lib/main.test.js

Lines changed: 95 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,54 @@
1-
import { exec as execCB } from 'child_process';
1+
import { spawn, exec as execCB } from 'child_process';
2+
import streamToString from 'stream-to-string';
23
import { promisify } from 'util';
34
import packageJson from '../package.json';
45

56
const exec = promisify(execCB);
67

7-
const run = (argv) =>
8-
exec(
9-
[process.execPath, 'build/test-utils/cli-test-entry.js', argv].join(' '),
8+
const run = async (argv, { waitForVolumeExport = true, stdin = '' } = {}) => {
9+
let volume;
10+
let exitCode;
11+
const child = spawn(
12+
process.execPath,
13+
['build/test-utils/cli-test-entry.js', ...argv],
14+
{
15+
stdio: ['pipe', 'pipe', 2, 'ipc'],
16+
},
1017
);
18+
const allChecks = Promise.all([
19+
waitForVolumeExport
20+
? new Promise((volumeSet) => {
21+
child.on('message', async (message) => {
22+
if (message.type === 'volumeExport') {
23+
volume = message.data;
24+
volumeSet();
25+
}
26+
});
27+
})
28+
: true,
29+
new Promise((processExited) => {
30+
child.on('exit', (code) => {
31+
exitCode = code;
32+
processExited();
33+
});
34+
}),
35+
]);
36+
child.stdin.write(stdin);
37+
child.stdin.end();
38+
const stdout = await streamToString(child.stdout);
39+
await allChecks;
40+
return {
41+
volume,
42+
code: exitCode,
43+
stdout,
44+
};
45+
};
1146

1247
describe('cli configuration', () => {
1348
beforeAll(() => exec(packageJson.scripts.prepare));
1449
it('should show help', async () => {
15-
expect((await run('help')).stdout).toMatchInlineSnapshot(`
50+
const { stdout } = await run(['help'], { waitForVolumeExport: false });
51+
expect(stdout).toMatchInlineSnapshot(`
1652
"Usage: resume [command] [options]
1753
1854
Options:
@@ -25,8 +61,9 @@ describe('cli configuration', () => {
2561
../some/other/dir) (default:
2662
\\"jsonresume-theme-even\\")
2763
-f, --format <file type extension> Used by \`export\`.
28-
-r, --resume <resume filename> path to the resume in json format
29-
(default: \\"resume.json\\")
64+
-r, --resume <resume filename> path to the resume in json format. Use
65+
'-' to read from stdin (default:
66+
\\"resume.json\\")
3067
-p, --port <port> Used by \`serve\` (default: 4000) (default:
3168
4000)
3269
-s, --silent Used by \`serve\` to tell it if open
@@ -50,37 +87,69 @@ describe('cli configuration', () => {
5087
});
5188
describe('validate', () => {
5289
it('should use the schema override arg', async () => {
53-
const output = await run('validate --schema /test-resumes/only-number-schema.json --resume /test-resumes/only-number.json')
54-
expect(output.stdout).toMatchInlineSnapshot(`""`);
55-
expect(output.stderr).toMatchInlineSnapshot(`""`);
56-
})
90+
const { stdout } = await run([
91+
'validate',
92+
'--schema',
93+
'/test-resumes/only-number-schema.json',
94+
'--resume',
95+
'/test-resumes/only-number.json',
96+
]);
97+
expect(stdout).toMatchInlineSnapshot(`""`);
98+
});
5799
it('should fail when trying to validate an invalid resume specified by the --resume option', async () => {
58-
await expect(
59-
run('validate --resume /test-resumes/invalid-resume.json'),
60-
).rejects.toEqual(
61-
expect.objectContaining({
62-
code: 1,
63-
}),
64-
);
100+
expect(
101+
(
102+
await run([
103+
'validate',
104+
'--resume',
105+
'/test-resumes/invalid-resume.json',
106+
])
107+
).code,
108+
).toEqual(1);
65109
});
66110
it('should validate a resume specified by the --resume option', async () => {
67-
const output = await run('validate --resume /test-resumes/resume.json');
68-
expect(output.stdout).toMatchInlineSnapshot(`""`);
69-
expect(output.stderr).toMatchInlineSnapshot(`""`);
111+
const { stdout } = await run([
112+
'validate',
113+
'--resume',
114+
'/test-resumes/resume.json',
115+
]);
116+
expect(stdout).toMatchInlineSnapshot(`""`);
70117
});
71118
});
72119
describe('export', () => {
73-
it('should export a resume from the path specified by --resume to the path specified immediately after the export command', async () => {
74-
const output = await run(
75-
'export /test-resumes/exported-resume.html --resume /test-resumes/resume.json',
120+
it('should read from stdin when path is a dash', async () => {
121+
const { stdout, volume } = await run(
122+
[
123+
'export',
124+
'/test-resumes/exported-resume-from-stdin.html',
125+
'--resume',
126+
'-', // this is the dash
127+
],
128+
{ stdin: JSON.stringify({ basics: { name: 'thomas-from-stdin' } }) },
76129
);
77-
expect(output.stdout).toMatchInlineSnapshot(`
130+
expect(volume['/test-resumes/exported-resume-from-stdin.html']).toEqual(
131+
expect.stringContaining('thomas-from-stdin'),
132+
);
133+
expect(stdout).toMatchInlineSnapshot(`
134+
"
135+
Done! Find your new .html resume at:
136+
/test-resumes/exported-resume-from-stdin.html
137+
"
138+
`);
139+
});
140+
it('should export a resume from the path specified by --resume to the path specified immediately after the export command', async () => {
141+
const { stdout } = await run([
142+
'export',
143+
'/test-resumes/exported-resume.html',
144+
'--resume',
145+
'/test-resumes/resume.json',
146+
]);
147+
expect(stdout).toMatchInlineSnapshot(`
78148
"
79149
Done! Find your new .html resume at:
80150
/test-resumes/exported-resume.html
81151
"
82152
`);
83-
expect(output.stderr).toMatchInlineSnapshot(`""`);
84153
});
85154
});
86155
});

lib/test-utils/cli-test-entry.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { patchFs } from 'fs-monkey';
33
import { ufs } from 'unionfs';
44
import * as fs from 'fs';
55

6-
const vol = ufs.use(build({ mount: '/test-resumes' })).use(fs);
6+
const mockVolume = build({ mount: '/test-resumes' });
7+
const vol = ufs.use(mockVolume).use(fs);
78
patchFs(vol);
89
require('../main.js');
10+
process.once('beforeExit', () => {
11+
process.send({ data: mockVolume.toJSON(), type: 'volumeExport' });
12+
});

lib/utils/stdin-is-pipe.js

Lines changed: 0 additions & 16 deletions
This file was deleted.

0 commit comments

Comments
 (0)