resume-cli 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/builder.js +11 -1
- package/build/export-resume.js +22 -9
- package/build/main.js +16 -2
- package/build/main.test.js +57 -11
- package/build/render-html.js +3 -2
- package/build/theme-errors.js +35 -0
- package/package.json +1 -1
package/build/builder.js
CHANGED
|
@@ -5,6 +5,10 @@ const fs = require('fs');
|
|
|
5
5
|
const request = require('superagent');
|
|
6
6
|
const chalk = require('chalk');
|
|
7
7
|
const renderHtml = require('./render-html').default;
|
|
8
|
+
const {
|
|
9
|
+
ThemeNotFoundError,
|
|
10
|
+
formatThemeNotFound
|
|
11
|
+
} = require('./theme-errors');
|
|
8
12
|
const denormalizeTheme = value => {
|
|
9
13
|
return value.match(/jsonresume-theme-(.*)/)[1];
|
|
10
14
|
};
|
|
@@ -45,7 +49,13 @@ module.exports = function resumeBuilder(theme, dir, resumeFilename, cb) {
|
|
|
45
49
|
});
|
|
46
50
|
cb(null, html);
|
|
47
51
|
} catch (err) {
|
|
48
|
-
|
|
52
|
+
if (err instanceof ThemeNotFoundError) {
|
|
53
|
+
// Theme isn't installed locally. Show an actionable message, then fall
|
|
54
|
+
// back to the hosted theme server (preserving existing behavior).
|
|
55
|
+
console.log(formatThemeNotFound(err.theme));
|
|
56
|
+
} else {
|
|
57
|
+
console.log(err);
|
|
58
|
+
}
|
|
49
59
|
console.log(chalk.yellow('Could not run the render function from local theme.'));
|
|
50
60
|
sendExportHTML(resumeJson, theme, cb);
|
|
51
61
|
}
|
package/build/export-resume.js
CHANGED
|
@@ -8,6 +8,9 @@ const writeFile = (0, _util.promisify)(_fs.default.writeFile);
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const puppeteer = require('puppeteer');
|
|
10
10
|
const btoa = require('btoa');
|
|
11
|
+
const {
|
|
12
|
+
ThemeNotFoundError
|
|
13
|
+
} = require('./theme-errors');
|
|
11
14
|
module.exports = ({
|
|
12
15
|
resume: resumeJson,
|
|
13
16
|
fileName,
|
|
@@ -24,14 +27,16 @@ module.exports = ({
|
|
|
24
27
|
const formatToUse = '.' + fileFormatToUse;
|
|
25
28
|
if (formatToUse === '.html') {
|
|
26
29
|
createHtml(resumeJson, fileName, theme, formatToUse, error => {
|
|
27
|
-
|
|
30
|
+
// ThemeNotFoundError is reported as a friendly message by the caller;
|
|
31
|
+
// don't dump the raw object here.
|
|
32
|
+
if (error && !(error instanceof ThemeNotFoundError)) {
|
|
28
33
|
console.error(error, '`createHtml` errored out');
|
|
29
34
|
}
|
|
30
35
|
callback(error, fileName, formatToUse);
|
|
31
36
|
});
|
|
32
37
|
} else if (formatToUse === '.pdf') {
|
|
33
38
|
createPdf(resumeJson, fileName, theme, formatToUse, error => {
|
|
34
|
-
if (error) {
|
|
39
|
+
if (error && !(error instanceof ThemeNotFoundError)) {
|
|
35
40
|
console.error(error, '`createPdf` errored out');
|
|
36
41
|
}
|
|
37
42
|
callback(error, fileName, formatToUse);
|
|
@@ -58,16 +63,24 @@ const getThemePkg = theme => {
|
|
|
58
63
|
const themePkg = require(theme);
|
|
59
64
|
return themePkg;
|
|
60
65
|
} catch (err) {
|
|
61
|
-
// Theme not installed
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
// Theme not installed. Throw a typed error so the caller can render an
|
|
67
|
+
// actionable, stack-trace-free message and exit with a non-zero code.
|
|
68
|
+
throw new ThemeNotFoundError(theme);
|
|
64
69
|
}
|
|
65
70
|
};
|
|
66
71
|
async function createHtml(resumeJson, fileName, themePath, format, callback) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
let html;
|
|
73
|
+
try {
|
|
74
|
+
html = await (0, _renderHtml.default)({
|
|
75
|
+
resume: resumeJson,
|
|
76
|
+
themePath
|
|
77
|
+
});
|
|
78
|
+
} catch (err) {
|
|
79
|
+
// Surface theme-resolution (and other render) failures to the caller so
|
|
80
|
+
// they can be reported as an actionable message rather than an
|
|
81
|
+
// unhandled-rejection stack trace.
|
|
82
|
+
return callback(err);
|
|
83
|
+
}
|
|
71
84
|
const pathToStream = path.resolve(process.cwd(), fileName + format);
|
|
72
85
|
await writeFile(pathToStream, ''); // workaround for https://github.com/streamich/unionfs/issues/428
|
|
73
86
|
const stream = _fs.default.createWriteStream(pathToStream);
|
package/build/main.js
CHANGED
|
@@ -13,6 +13,10 @@ const serve = require('./serve');
|
|
|
13
13
|
const program = require('commander');
|
|
14
14
|
const chalk = require('chalk');
|
|
15
15
|
const path = require('path');
|
|
16
|
+
const {
|
|
17
|
+
ThemeNotFoundError,
|
|
18
|
+
formatThemeNotFound
|
|
19
|
+
} = require('./theme-errors');
|
|
16
20
|
const normalizeTheme = (value, defaultValue) => {
|
|
17
21
|
const theme = value || defaultValue;
|
|
18
22
|
// TODO - This is not great, but bypasses this function if it is a relative path
|
|
@@ -22,7 +26,7 @@ const normalizeTheme = (value, defaultValue) => {
|
|
|
22
26
|
return theme.match('jsonresume-theme-.*') ? theme : `jsonresume-theme-${theme}`;
|
|
23
27
|
};
|
|
24
28
|
(async () => {
|
|
25
|
-
program.name('resume').usage('[command] [options]').version(pkg.version).option('-F, --force', 'Used by `publish` and `export` - bypasses schema testing.').option('-t, --theme <theme name>', '
|
|
29
|
+
program.name('resume').usage('[command] [options]').version(pkg.version).option('-F, --force', 'Used by `publish` and `export` - bypasses schema testing.').option('-t, --theme <theme name>', 'Theme used by `export` and `serve` (browse themes at https://jsonresume.org/themes/), or a path starting with . (use . for current directory or ../some/other/dir)', normalizeTheme, 'jsonresume-theme-elegant').option('-f, --format <file type extension>', 'Used by `export`.').option('-r, --resume <resume filename>', "path to the resume in json format. Use '-' to read from stdin", 'resume.json').option('-p, --port <port>', 'Used by `serve` (default: 4000)', 4000).option('-s, --silent', 'Used by `serve` to tell it if open browser auto or not.', false).option('-d, --dir <path>', 'Used by `serve` to indicate a public directory path.', 'public').option('--schema <relativePath>', 'Used by `validate` to validate against a custom schema.');
|
|
26
30
|
program.command('init').description('Initialize a resume.json file').action(async () => {
|
|
27
31
|
await (0, _init.default)({
|
|
28
32
|
resumePath: program.resume
|
|
@@ -40,12 +44,13 @@ const normalizeTheme = (value, defaultValue) => {
|
|
|
40
44
|
resume,
|
|
41
45
|
schema
|
|
42
46
|
});
|
|
47
|
+
console.log(chalk.green(`✓ ${program.resume} is valid`));
|
|
43
48
|
} catch (e) {
|
|
44
49
|
console.error(e.message);
|
|
45
50
|
process.exitCode = 1;
|
|
46
51
|
}
|
|
47
52
|
});
|
|
48
|
-
program.command('export [fileName]').description('Export locally to .html or .pdf. Supply a --format <file format> flag and argument to specify export format.').action(async fileName => {
|
|
53
|
+
program.command('export [fileName]').description('Export locally to .html or .pdf. Supply a --format <file format> flag and argument to specify export format. Pick a theme with --theme (https://jsonresume.org/themes/).').action(async fileName => {
|
|
49
54
|
const resume = await (0, _getResume.default)({
|
|
50
55
|
path: program.resume
|
|
51
56
|
});
|
|
@@ -54,6 +59,15 @@ const normalizeTheme = (value, defaultValue) => {
|
|
|
54
59
|
resume,
|
|
55
60
|
fileName
|
|
56
61
|
}, (err, fileName, format) => {
|
|
62
|
+
if (err) {
|
|
63
|
+
if (err instanceof ThemeNotFoundError) {
|
|
64
|
+
console.error(formatThemeNotFound(err.theme));
|
|
65
|
+
} else {
|
|
66
|
+
console.error(err.message || err);
|
|
67
|
+
}
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
57
71
|
console.log(chalk.green('\nDone! Find your new', format, 'resume at:\n', path.resolve(process.cwd(), fileName + format)));
|
|
58
72
|
});
|
|
59
73
|
});
|
package/build/main.test.js
CHANGED
|
@@ -8,12 +8,15 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
|
|
|
8
8
|
const exec = (0, _util.promisify)(_child_process.exec);
|
|
9
9
|
const run = async (argv, {
|
|
10
10
|
waitForVolumeExport = true,
|
|
11
|
-
stdin = ''
|
|
11
|
+
stdin = '',
|
|
12
|
+
captureStderr = false
|
|
12
13
|
} = {}) => {
|
|
13
14
|
let volume;
|
|
14
15
|
let exitCode;
|
|
15
16
|
const child = (0, _child_process.spawn)(process.execPath, ['build/test-utils/cli-test-entry.js', ...argv], {
|
|
16
|
-
|
|
17
|
+
// When captureStderr is set, pipe fd 2 so the test can read the
|
|
18
|
+
// friendly error output; otherwise inherit it (existing behavior).
|
|
19
|
+
stdio: ['pipe', 'pipe', captureStderr ? 'pipe' : 2, 'ipc']
|
|
17
20
|
});
|
|
18
21
|
const allChecks = Promise.all([waitForVolumeExport ? new Promise(volumeSet => {
|
|
19
22
|
child.on('message', async message => {
|
|
@@ -30,12 +33,16 @@ const run = async (argv, {
|
|
|
30
33
|
})]);
|
|
31
34
|
child.stdin.write(stdin);
|
|
32
35
|
child.stdin.end();
|
|
33
|
-
const
|
|
36
|
+
const stdoutPromise = (0, _streamToString.default)(child.stdout);
|
|
37
|
+
const stderrPromise = captureStderr ? (0, _streamToString.default)(child.stderr) : Promise.resolve('');
|
|
38
|
+
const stdout = await stdoutPromise;
|
|
39
|
+
const stderr = await stderrPromise;
|
|
34
40
|
await allChecks;
|
|
35
41
|
return {
|
|
36
42
|
volume,
|
|
37
43
|
code: exitCode,
|
|
38
|
-
stdout
|
|
44
|
+
stdout,
|
|
45
|
+
stderr
|
|
39
46
|
};
|
|
40
47
|
};
|
|
41
48
|
describe('cli configuration', () => {
|
|
@@ -53,10 +60,11 @@ describe('cli configuration', () => {
|
|
|
53
60
|
-V, --version output the version number
|
|
54
61
|
-F, --force Used by \`publish\` and \`export\` - bypasses
|
|
55
62
|
schema testing.
|
|
56
|
-
-t, --theme <theme name>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
-t, --theme <theme name> Theme used by \`export\` and \`serve\`
|
|
64
|
+
(browse themes at
|
|
65
|
+
https://jsonresume.org/themes/), or a
|
|
66
|
+
path starting with . (use . for current
|
|
67
|
+
directory or ../some/other/dir) (default:
|
|
60
68
|
"jsonresume-theme-elegant")
|
|
61
69
|
-f, --format <file type extension> Used by \`export\`.
|
|
62
70
|
-r, --resume <resume filename> path to the resume in json format. Use
|
|
@@ -77,7 +85,9 @@ describe('cli configuration', () => {
|
|
|
77
85
|
validate Validate your resume's schema
|
|
78
86
|
export [fileName] Export locally to .html or .pdf. Supply
|
|
79
87
|
a --format <file format> flag and
|
|
80
|
-
argument to specify export format.
|
|
88
|
+
argument to specify export format. Pick a
|
|
89
|
+
theme with --theme
|
|
90
|
+
(https://jsonresume.org/themes/).
|
|
81
91
|
serve Serve resume at http://localhost:4000/
|
|
82
92
|
help [command] display help for command
|
|
83
93
|
"
|
|
@@ -88,16 +98,37 @@ describe('cli configuration', () => {
|
|
|
88
98
|
const {
|
|
89
99
|
stdout
|
|
90
100
|
} = await run(['validate', '--schema', '/test-resumes/only-number-schema.json', '--resume', '/test-resumes/only-number.json']);
|
|
91
|
-
expect(stdout).toMatchInlineSnapshot(`
|
|
101
|
+
expect(stdout).toMatchInlineSnapshot(`
|
|
102
|
+
"✓ /test-resumes/only-number.json is valid
|
|
103
|
+
"
|
|
104
|
+
`);
|
|
92
105
|
});
|
|
93
106
|
it('should fail when trying to validate an invalid resume specified by the --resume option', async () => {
|
|
94
107
|
expect((await run(['validate', '--resume', '/test-resumes/invalid-resume.json'])).code).toEqual(1);
|
|
95
108
|
});
|
|
109
|
+
it('should print the per-field error list (not a success line) when validation fails', async () => {
|
|
110
|
+
const {
|
|
111
|
+
code,
|
|
112
|
+
stdout,
|
|
113
|
+
stderr
|
|
114
|
+
} = await run(['validate', '--resume', '/test-resumes/invalid-resume.json'], {
|
|
115
|
+
waitForVolumeExport: false,
|
|
116
|
+
captureStderr: true
|
|
117
|
+
});
|
|
118
|
+
expect(code).toEqual(1);
|
|
119
|
+
expect(stderr).toContain('Invalid resume:');
|
|
120
|
+
expect(stderr).toContain('data/basics/name must be string');
|
|
121
|
+
// The success line must not appear for an invalid resume.
|
|
122
|
+
expect(stdout).not.toContain('is valid');
|
|
123
|
+
});
|
|
96
124
|
it('should validate a resume specified by the --resume option', async () => {
|
|
97
125
|
const {
|
|
98
126
|
stdout
|
|
99
127
|
} = await run(['validate', '--resume', '/test-resumes/resume.json']);
|
|
100
|
-
expect(stdout).toMatchInlineSnapshot(`
|
|
128
|
+
expect(stdout).toMatchInlineSnapshot(`
|
|
129
|
+
"✓ /test-resumes/resume.json is valid
|
|
130
|
+
"
|
|
131
|
+
`);
|
|
101
132
|
});
|
|
102
133
|
});
|
|
103
134
|
describe('export', () => {
|
|
@@ -139,5 +170,20 @@ describe('cli configuration', () => {
|
|
|
139
170
|
"
|
|
140
171
|
`);
|
|
141
172
|
});
|
|
173
|
+
it('should print a friendly, actionable message and exit non-zero when the theme is not installed', async () => {
|
|
174
|
+
const {
|
|
175
|
+
code,
|
|
176
|
+
stderr
|
|
177
|
+
} = await run(['export', '/test-resumes/exported-resume.html', '--resume', '/test-resumes/resume.json', '--theme', 'nonexistent-xyz'], {
|
|
178
|
+
waitForVolumeExport: false,
|
|
179
|
+
captureStderr: true
|
|
180
|
+
});
|
|
181
|
+
expect(code).toEqual(1);
|
|
182
|
+
expect(stderr).toContain("Theme 'nonexistent-xyz' not found.");
|
|
183
|
+
expect(stderr).toContain('npm install jsonresume-theme-nonexistent-xyz');
|
|
184
|
+
expect(stderr).toContain('https://jsonresume.org/themes/');
|
|
185
|
+
// The raw stack trace must not leak to the user.
|
|
186
|
+
expect(stderr).not.toContain('at _default');
|
|
187
|
+
});
|
|
142
188
|
});
|
|
143
189
|
});
|
package/build/render-html.js
CHANGED
|
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = void 0;
|
|
7
|
+
var _themeErrors = require("./theme-errors");
|
|
7
8
|
const tryResolve = (...args) => {
|
|
8
9
|
try {
|
|
9
10
|
return require.resolve(...args);
|
|
@@ -22,7 +23,7 @@ var _default = async ({
|
|
|
22
23
|
paths: [cwd]
|
|
23
24
|
});
|
|
24
25
|
if (!path) {
|
|
25
|
-
throw new
|
|
26
|
+
throw new _themeErrors.ThemeNotFoundError(themePath);
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
if (!path) {
|
|
@@ -36,7 +37,7 @@ var _default = async ({
|
|
|
36
37
|
});
|
|
37
38
|
}
|
|
38
39
|
if (!path) {
|
|
39
|
-
throw new
|
|
40
|
+
throw new _themeErrors.ThemeNotFoundError(themePath);
|
|
40
41
|
}
|
|
41
42
|
const theme = require(path);
|
|
42
43
|
if (typeof theme?.render !== 'function') {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
|
|
5
|
+
// Strip the `jsonresume-theme-` prefix (and any relative-path noise) so the
|
|
6
|
+
// message we show the user matches what they would type with `--theme`.
|
|
7
|
+
const shortThemeName = theme => {
|
|
8
|
+
if (!theme) {
|
|
9
|
+
return theme;
|
|
10
|
+
}
|
|
11
|
+
const base = theme.replace(/^.*jsonresume-theme-/, '');
|
|
12
|
+
return base.replace(/[/\\].*$/, '');
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Error raised when a `--theme` cannot be resolved. Carries the requested
|
|
16
|
+
// theme name so the CLI can render an actionable, stack-trace-free message.
|
|
17
|
+
class ThemeNotFoundError extends Error {
|
|
18
|
+
constructor(theme) {
|
|
19
|
+
super(`Theme not found: ${theme}`);
|
|
20
|
+
this.name = 'ThemeNotFoundError';
|
|
21
|
+
this.theme = theme;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Build the user-facing, actionable message for a missing theme.
|
|
26
|
+
const formatThemeNotFound = theme => {
|
|
27
|
+
const short = shortThemeName(theme);
|
|
28
|
+
const pkg = short.startsWith('jsonresume-theme-') ? short : `jsonresume-theme-${short}`;
|
|
29
|
+
return [chalk.red(`Theme '${short}' not found.`), `Install it: ${chalk.cyan(`npm install ${pkg}`)}`, `Browse themes: ${chalk.cyan('https://jsonresume.org/themes/')}`].join('\n');
|
|
30
|
+
};
|
|
31
|
+
module.exports = {
|
|
32
|
+
ThemeNotFoundError,
|
|
33
|
+
formatThemeNotFound,
|
|
34
|
+
shortThemeName
|
|
35
|
+
};
|