resume-cli 3.1.2 → 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/README.md +14 -7
- package/build/builder.js +12 -2
- package/build/export-resume.js +23 -10
- package/build/get-resume.js +1 -1
- package/build/get-resume.test.js +16 -16
- package/build/get-schema.js +1 -1
- package/build/init.js +2 -2
- package/build/main.js +17 -3
- package/build/main.test.js +71 -18
- package/build/render-html.js +4 -3
- package/build/render-html.test.js +1 -1
- package/build/test-utils/cli-test-entry.js +2 -3
- package/build/test-utils/mocked-volume-builder.js +3 -2
- package/build/theme-errors.js +35 -0
- package/build/validate.js +25 -15
- package/build/validate.test.js +13 -5
- package/package.json +23 -30
package/README.md
CHANGED
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
# resume-cli
|
|
2
2
|
|
|
3
|
-
[](https://matrix.to/#/#json-resume:one.ems.host)
|
|
4
|
-
[](https://github.com/jsonresume/resume-cli/actions)
|
|
5
3
|
[](https://www.npmjs.org/package/resume-cli)
|
|
6
4
|
|
|
7
5
|
This is the command line tool for [JSON Resume](https://jsonresume.org), the open-source initiative to create a JSON-based standard for resumes.
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
This repository is not actively maintained. It's recommended to use one of the third-party clients that support the JSON Resume standard instead:
|
|
12
|
-
|
|
13
|
-
* [Resumed](https://github.com/rbardini/resumed)
|
|
7
|
+
> **Note:** `resume-cli` has been revived and now lives in the [jsonresume.org monorepo](https://github.com/jsonresume/jsonresume.org) as `packages/cli` (modernized for Node.js 18+). It keeps its npm identity as [`resume-cli`](https://www.npmjs.com/package/resume-cli) and is published from this workspace. The old standalone [jsonresume/resume-cli](https://github.com/jsonresume/resume-cli) repository is archived — please open issues and PRs against the monorepo.
|
|
14
8
|
|
|
15
9
|
## Getting Started
|
|
16
10
|
|
|
11
|
+
Requires Node.js 18 or newer.
|
|
12
|
+
|
|
17
13
|
Install the command-line tool:
|
|
18
14
|
|
|
19
15
|
```
|
|
@@ -91,6 +87,17 @@ Supported resume data MIME types are:
|
|
|
91
87
|
- `application/json`
|
|
92
88
|
- `text/yaml`
|
|
93
89
|
|
|
90
|
+
## Development
|
|
91
|
+
|
|
92
|
+
This package is part of the [jsonresume.org monorepo](https://github.com/jsonresume/jsonresume.org). From `packages/cli`:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
pnpm dev # run the CLI from source (babel-node lib/main.js)
|
|
96
|
+
pnpm build # compile lib/ to build/ with Babel
|
|
97
|
+
pnpm test # run the Jest test suite
|
|
98
|
+
pnpm lint # run ESLint
|
|
99
|
+
```
|
|
100
|
+
|
|
94
101
|
## License
|
|
95
102
|
|
|
96
103
|
Available under [the MIT license](http://mths.be/mit).
|
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
|
};
|
|
@@ -28,7 +32,7 @@ module.exports = function resumeBuilder(theme, dir, resumeFilename, cb) {
|
|
|
28
32
|
if (err) {
|
|
29
33
|
console.log(chalk.yellow('Could not find:'), resumeFilename);
|
|
30
34
|
console.log(chalk.cyan('Using example resume.json from resume-schema instead...'));
|
|
31
|
-
resumeJson = require('
|
|
35
|
+
resumeJson = require('@jsonresume/schema/sample.resume.json');
|
|
32
36
|
} else {
|
|
33
37
|
try {
|
|
34
38
|
// todo: test resume schema
|
|
@@ -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
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
var _renderHtml = _interopRequireDefault(require("./render-html"));
|
|
4
4
|
var _util = require("util");
|
|
5
5
|
var _fs = _interopRequireDefault(require("fs"));
|
|
6
|
-
function _interopRequireDefault(
|
|
6
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
7
|
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/get-resume.js
CHANGED
|
@@ -11,7 +11,7 @@ var _quaff = _interopRequireDefault(require("quaff"));
|
|
|
11
11
|
var _streamToString = _interopRequireDefault(require("stream-to-string"));
|
|
12
12
|
var _yamlJs = _interopRequireDefault(require("yaml-js"));
|
|
13
13
|
var _util = require("util");
|
|
14
|
-
function _interopRequireDefault(
|
|
14
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
15
15
|
const {
|
|
16
16
|
createReadStream
|
|
17
17
|
} = _fs.default;
|
package/build/get-resume.test.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
var _waait = _interopRequireDefault(require("waait"));
|
|
4
4
|
var _getResume = _interopRequireDefault(require("./get-resume"));
|
|
5
5
|
var _mockStdin = require("mock-stdin");
|
|
6
|
-
function _interopRequireDefault(
|
|
6
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
7
|
jest.mock('fs', () => {
|
|
8
8
|
const build = require('./test-utils/mocked-volume-builder');
|
|
9
9
|
const {
|
|
@@ -17,8 +17,8 @@ describe('get-resume', () => {
|
|
|
17
17
|
expect(await (0, _getResume.default)({
|
|
18
18
|
path: '/resume.yaml'
|
|
19
19
|
})).toMatchInlineSnapshot(`
|
|
20
|
-
|
|
21
|
-
"basics":
|
|
20
|
+
{
|
|
21
|
+
"basics": {
|
|
22
22
|
"email": "thomas@example.com",
|
|
23
23
|
"name": "thomas",
|
|
24
24
|
},
|
|
@@ -29,25 +29,25 @@ describe('get-resume', () => {
|
|
|
29
29
|
expect(await (0, _getResume.default)({
|
|
30
30
|
path: '/resume.json'
|
|
31
31
|
})).toMatchInlineSnapshot(`
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
{
|
|
33
|
+
"basics": {
|
|
34
|
+
"email": "thomas@example.com",
|
|
35
|
+
"name": "thomas",
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
`);
|
|
39
39
|
});
|
|
40
40
|
it('should consume an entire directory as if it were a json object', async () => {
|
|
41
41
|
expect(await (0, _getResume.default)({
|
|
42
42
|
path: '/quaff'
|
|
43
43
|
})).toMatchInlineSnapshot(`
|
|
44
|
-
|
|
45
|
-
"basics":
|
|
44
|
+
{
|
|
45
|
+
"basics": {
|
|
46
46
|
"email": "thomas@example.com",
|
|
47
47
|
"name": "thomas",
|
|
48
48
|
},
|
|
49
|
-
"work":
|
|
50
|
-
|
|
49
|
+
"work": [
|
|
50
|
+
{
|
|
51
51
|
"company": "Pied Piper",
|
|
52
52
|
"endDate": "2014-12-01",
|
|
53
53
|
"position": "CEO/President",
|
|
@@ -71,8 +71,8 @@ describe('get-resume', () => {
|
|
|
71
71
|
}));
|
|
72
72
|
stdin.send(null);
|
|
73
73
|
expect(await gotResume).toMatchInlineSnapshot(`
|
|
74
|
-
|
|
75
|
-
"basics":
|
|
74
|
+
{
|
|
75
|
+
"basics": {
|
|
76
76
|
"email": "thomas@example.com",
|
|
77
77
|
"name": "thomas",
|
|
78
78
|
},
|
package/build/get-schema.js
CHANGED
|
@@ -12,7 +12,7 @@ var _default = async ({
|
|
|
12
12
|
} = {}) => {
|
|
13
13
|
let path = pathArg;
|
|
14
14
|
if (!path) {
|
|
15
|
-
path = require.resolve('
|
|
15
|
+
path = require.resolve('@jsonresume/schema/schema.json');
|
|
16
16
|
}
|
|
17
17
|
return JSON.parse(await readFile(path, {
|
|
18
18
|
encoding: 'utf-8'
|
package/build/init.js
CHANGED
|
@@ -11,10 +11,10 @@ var _yesno = _interopRequireDefault(require("yesno"));
|
|
|
11
11
|
var _objectPathImmutable = require("object-path-immutable");
|
|
12
12
|
var _fileExists = _interopRequireDefault(require("file-exists"));
|
|
13
13
|
var _read = _interopRequireDefault(require("read"));
|
|
14
|
-
function _interopRequireDefault(
|
|
14
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
15
15
|
const writeFile = (0, _util.promisify)(_fs.default.writeFile);
|
|
16
16
|
const read = (0, _util.promisify)(_read.default);
|
|
17
|
-
const resume = require('
|
|
17
|
+
const resume = require('@jsonresume/schema/sample.resume.json');
|
|
18
18
|
var _default = async ({
|
|
19
19
|
resumePath
|
|
20
20
|
}) => {
|
package/build/main.js
CHANGED
|
@@ -6,13 +6,17 @@ var _init = _interopRequireDefault(require("./init"));
|
|
|
6
6
|
var _getResume = _interopRequireDefault(require("./get-resume"));
|
|
7
7
|
var _getSchema = _interopRequireDefault(require("./get-schema"));
|
|
8
8
|
var _validate = _interopRequireDefault(require("./validate"));
|
|
9
|
-
function _interopRequireDefault(
|
|
9
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
10
|
const pkg = require('../package.json');
|
|
11
11
|
const exportResume = require('./export-resume');
|
|
12
12
|
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
|
@@ -4,16 +4,19 @@ var _child_process = require("child_process");
|
|
|
4
4
|
var _streamToString = _interopRequireDefault(require("stream-to-string"));
|
|
5
5
|
var _util = require("util");
|
|
6
6
|
var _package = _interopRequireDefault(require("../package.json"));
|
|
7
|
-
function _interopRequireDefault(
|
|
7
|
+
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,21 +60,22 @@ 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
|
-
|
|
60
|
-
|
|
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:
|
|
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
|
|
63
71
|
'-' to read from stdin (default:
|
|
64
|
-
|
|
72
|
+
"resume.json")
|
|
65
73
|
-p, --port <port> Used by \`serve\` (default: 4000) (default:
|
|
66
74
|
4000)
|
|
67
75
|
-s, --silent Used by \`serve\` to tell it if open
|
|
68
76
|
browser auto or not. (default: false)
|
|
69
77
|
-d, --dir <path> Used by \`serve\` to indicate a public
|
|
70
|
-
directory path. (default:
|
|
78
|
+
directory path. (default: "public")
|
|
71
79
|
--schema <relativePath> Used by \`validate\` to validate against a
|
|
72
80
|
custom schema.
|
|
73
81
|
-h, --help display help for command
|
|
@@ -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', () => {
|
|
@@ -105,8 +136,12 @@ describe('cli configuration', () => {
|
|
|
105
136
|
const {
|
|
106
137
|
stdout,
|
|
107
138
|
volume
|
|
108
|
-
} = await run(['export', '/test-resumes/exported-resume-from-stdin.html', '--resume', '-'
|
|
109
|
-
|
|
139
|
+
} = await run(['export', '/test-resumes/exported-resume-from-stdin.html', '--resume', '-',
|
|
140
|
+
// this is the dash
|
|
141
|
+
// The default theme (elegant) throws on resumes missing a
|
|
142
|
+
// `basics.location`; `even` renders minimal resumes, and this test
|
|
143
|
+
// only exercises the export plumbing, not a specific theme.
|
|
144
|
+
'--theme', 'even'], {
|
|
110
145
|
stdin: JSON.stringify({
|
|
111
146
|
basics: {
|
|
112
147
|
name: 'thomas-from-stdin'
|
|
@@ -124,7 +159,10 @@ describe('cli configuration', () => {
|
|
|
124
159
|
it('should export a resume from the path specified by --resume to the path specified immediately after the export command', async () => {
|
|
125
160
|
const {
|
|
126
161
|
stdout
|
|
127
|
-
} = await run(['export', '/test-resumes/exported-resume.html', '--resume', '/test-resumes/resume.json'
|
|
162
|
+
} = await run(['export', '/test-resumes/exported-resume.html', '--resume', '/test-resumes/resume.json',
|
|
163
|
+
// See note above: `even` renders minimal resumes; the default
|
|
164
|
+
// `elegant` theme requires a `basics.location`.
|
|
165
|
+
'--theme', 'even']);
|
|
128
166
|
expect(stdout).toMatchInlineSnapshot(`
|
|
129
167
|
"
|
|
130
168
|
Done! Find your new .html resume at:
|
|
@@ -132,5 +170,20 @@ describe('cli configuration', () => {
|
|
|
132
170
|
"
|
|
133
171
|
`);
|
|
134
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
|
+
});
|
|
135
188
|
});
|
|
136
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,10 +37,10 @@ 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
|
-
if (typeof
|
|
43
|
+
if (typeof theme?.render !== 'function') {
|
|
43
44
|
throw new Error('theme.render is not a function');
|
|
44
45
|
}
|
|
45
46
|
return theme.render(resume);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
var _renderHtml = _interopRequireDefault(require("./render-html"));
|
|
4
|
-
function _interopRequireDefault(
|
|
4
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
5
5
|
describe('renderHTML', () => {
|
|
6
6
|
beforeAll(() => {
|
|
7
7
|
const originalRequireResolve = require.resolve;
|
|
@@ -4,9 +4,8 @@ var _mockedVolumeBuilder = _interopRequireDefault(require("./mocked-volume-build
|
|
|
4
4
|
var _fsMonkey = require("fs-monkey");
|
|
5
5
|
var _unionfs = require("unionfs");
|
|
6
6
|
var fs = _interopRequireWildcard(require("fs"));
|
|
7
|
-
function
|
|
8
|
-
function
|
|
9
|
-
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
7
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
8
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
9
|
const mockVolume = (0, _mockedVolumeBuilder.default)({
|
|
11
10
|
mount: '/test-resumes'
|
|
12
11
|
});
|
|
@@ -14,8 +14,9 @@ module.exports = ({
|
|
|
14
14
|
}),
|
|
15
15
|
'only-number.json': '123',
|
|
16
16
|
'invalid-resume.json': JSON.stringify({
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
basics: {
|
|
18
|
+
// name must be a string per the JSON Resume schema
|
|
19
|
+
name: 123
|
|
19
20
|
}
|
|
20
21
|
}),
|
|
21
22
|
'resume.json': JSON.stringify({
|
|
@@ -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
|
+
};
|
package/build/validate.js
CHANGED
|
@@ -4,25 +4,35 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = void 0;
|
|
7
|
-
var
|
|
8
|
-
var
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
const
|
|
7
|
+
var _ajv = _interopRequireDefault(require("ajv"));
|
|
8
|
+
var _ajvFormats = _interopRequireDefault(require("ajv-formats"));
|
|
9
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
|
+
// Format a single Ajv error into a readable `field message` line.
|
|
11
|
+
const formatError = error => {
|
|
12
|
+
// instancePath is like "/basics/name"; the root is an empty string.
|
|
13
|
+
const field = error.instancePath ? `data${error.instancePath}` : 'data';
|
|
14
|
+
if (error.keyword === 'additionalProperties') {
|
|
15
|
+
return `${field} ${error.message} (${error.params.additionalProperty})`;
|
|
16
|
+
}
|
|
17
|
+
return `${field} ${error.message}`;
|
|
18
|
+
};
|
|
14
19
|
var _default = async ({
|
|
15
20
|
resume,
|
|
16
21
|
schema
|
|
17
22
|
}) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
// strict:false is required: the JSON Resume schema is an externally authored
|
|
24
|
+
// draft-07 document that uses keywords (e.g. additionalItems without an
|
|
25
|
+
// array `items`) that Ajv's strict mode would otherwise reject.
|
|
26
|
+
const ajv = new _ajv.default({
|
|
27
|
+
allErrors: true,
|
|
28
|
+
strict: false
|
|
29
|
+
});
|
|
30
|
+
(0, _ajvFormats.default)(ajv);
|
|
31
|
+
const validateFn = ajv.compile(schema);
|
|
32
|
+
if (validateFn(resume)) {
|
|
33
|
+
return true;
|
|
26
34
|
}
|
|
35
|
+
const details = (validateFn.errors || []).map(formatError).join('\n ');
|
|
36
|
+
throw new Error(`Invalid resume:\n ${details}`);
|
|
27
37
|
};
|
|
28
38
|
exports.default = _default;
|
package/build/validate.test.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var _validate = _interopRequireDefault(require("./validate"));
|
|
4
4
|
var _getSchema = _interopRequireDefault(require("./get-schema"));
|
|
5
|
-
function _interopRequireDefault(
|
|
5
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
6
6
|
describe('validate', () => {
|
|
7
7
|
let defaultSchema;
|
|
8
8
|
beforeEach(async () => {
|
|
@@ -16,13 +16,18 @@ describe('validate', () => {
|
|
|
16
16
|
schema: defaultSchema
|
|
17
17
|
});
|
|
18
18
|
});
|
|
19
|
-
it('should throw
|
|
19
|
+
it('should throw a per-field error for an invalid resume object', async () => {
|
|
20
20
|
await expect((0, _validate.default)({
|
|
21
21
|
resume: {
|
|
22
|
-
|
|
22
|
+
basics: {
|
|
23
|
+
name: 123
|
|
24
|
+
}
|
|
23
25
|
},
|
|
24
26
|
schema: defaultSchema
|
|
25
|
-
})).rejects.toMatchInlineSnapshot(`
|
|
27
|
+
})).rejects.toMatchInlineSnapshot(`
|
|
28
|
+
[Error: Invalid resume:
|
|
29
|
+
data/basics/name must be string]
|
|
30
|
+
`);
|
|
26
31
|
});
|
|
27
32
|
it('should accept a schema override', async () => {
|
|
28
33
|
await (0, _validate.default)({
|
|
@@ -36,6 +41,9 @@ describe('validate', () => {
|
|
|
36
41
|
schema: {
|
|
37
42
|
type: 'number'
|
|
38
43
|
}
|
|
39
|
-
})).rejects.toMatchInlineSnapshot(`
|
|
44
|
+
})).rejects.toMatchInlineSnapshot(`
|
|
45
|
+
[Error: Invalid resume:
|
|
46
|
+
data must be number]
|
|
47
|
+
`);
|
|
40
48
|
});
|
|
41
49
|
});
|
package/package.json
CHANGED
|
@@ -1,25 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "resume-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "The JSON Resume command line interface",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "build/main.js",
|
|
6
6
|
"engines": {
|
|
7
|
-
"node": ">=
|
|
7
|
+
"node": ">=18"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"build/*",
|
|
11
11
|
"!test-utils",
|
|
12
12
|
"!*.test.js"
|
|
13
13
|
],
|
|
14
|
-
"scripts": {
|
|
15
|
-
"dev": "babel-node lib/main.js",
|
|
16
|
-
"lint": "eslint --ignore-path .gitignore .",
|
|
17
|
-
"_postinstall": "husky install",
|
|
18
|
-
"prepublishOnly": "pinst --disable",
|
|
19
|
-
"postpublish": "pinst --enable",
|
|
20
|
-
"prepare": "babel lib -d build --copy-files",
|
|
21
|
-
"test": "jest"
|
|
22
|
-
},
|
|
23
14
|
"repository": {
|
|
24
15
|
"type": "git",
|
|
25
16
|
"url": "https://github.com/jsonresume/resume-cli.git"
|
|
@@ -29,6 +20,8 @@
|
|
|
29
20
|
"resume": "build/main.js"
|
|
30
21
|
},
|
|
31
22
|
"dependencies": {
|
|
23
|
+
"ajv": "^8.17.1",
|
|
24
|
+
"ajv-formats": "^3.0.1",
|
|
32
25
|
"async": "^3.2.0",
|
|
33
26
|
"browser-sync": "^2.29.3",
|
|
34
27
|
"btoa": "^1.2.1",
|
|
@@ -38,29 +31,25 @@
|
|
|
38
31
|
"file-exists": "^5.0.1",
|
|
39
32
|
"jest-extended": "^0.11.5",
|
|
40
33
|
"jsonresume-theme-elegant": "^1.16.1",
|
|
41
|
-
"jsonresume-theme-even": "
|
|
34
|
+
"jsonresume-theme-even": "0.6.0",
|
|
42
35
|
"mime-types": "^2.1.27",
|
|
43
36
|
"object-path-immutable": "^4.1.1",
|
|
44
|
-
"puppeteer": "^
|
|
37
|
+
"puppeteer": "^23.0.0",
|
|
45
38
|
"quaff": "^4.2.0",
|
|
46
39
|
"read": "^1.0.7",
|
|
47
|
-
"resume-schema": "^1.0.0",
|
|
48
40
|
"stream-to-string": "^1.2.1",
|
|
49
41
|
"superagent": "^6.0.0",
|
|
50
42
|
"yaml-js": "^0.2.3",
|
|
51
43
|
"yesno": "^0.3.1",
|
|
52
|
-
"
|
|
53
|
-
"z-schema-errors": "^0.2.1"
|
|
44
|
+
"@jsonresume/schema": "1.2.1"
|
|
54
45
|
},
|
|
55
46
|
"devDependencies": {
|
|
56
|
-
"@babel/cli": "7.
|
|
57
|
-
"@babel/core": "7.
|
|
58
|
-
"@babel/eslint-parser": "7.
|
|
59
|
-
"@babel/node": "7.
|
|
60
|
-
"@babel/
|
|
61
|
-
"
|
|
62
|
-
"babel-eslint": "10.1.0",
|
|
63
|
-
"babel-jest": "28.1.2",
|
|
47
|
+
"@babel/cli": "^7.25.0",
|
|
48
|
+
"@babel/core": "^7.25.0",
|
|
49
|
+
"@babel/eslint-parser": "^7.25.0",
|
|
50
|
+
"@babel/node": "^7.25.0",
|
|
51
|
+
"@babel/preset-env": "^7.25.0",
|
|
52
|
+
"babel-jest": "^29.7.0",
|
|
64
53
|
"dedent": "0.7.0",
|
|
65
54
|
"eslint": "7.15.0",
|
|
66
55
|
"eslint-config-prettier": "7.0.0",
|
|
@@ -68,13 +57,17 @@
|
|
|
68
57
|
"eslint-plugin-prettier": "3.2.0",
|
|
69
58
|
"flat": "5.0.2",
|
|
70
59
|
"fs-monkey": "1.0.1",
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"lint-staged": "10.5.3",
|
|
74
|
-
"memfs": "3.2.0",
|
|
60
|
+
"jest": "^29.7.0",
|
|
61
|
+
"memfs": "3.6.0",
|
|
75
62
|
"mock-stdin": "1.0.0",
|
|
76
63
|
"prettier": "2.2.1",
|
|
77
64
|
"unionfs": "4.4.0",
|
|
78
65
|
"waait": "1.0.5"
|
|
66
|
+
},
|
|
67
|
+
"scripts": {
|
|
68
|
+
"dev": "babel-node lib/main.js",
|
|
69
|
+
"lint": "eslint --ignore-path .gitignore .",
|
|
70
|
+
"build": "babel lib -d build --copy-files",
|
|
71
|
+
"test": "jest"
|
|
79
72
|
}
|
|
80
|
-
}
|
|
73
|
+
}
|