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 CHANGED
@@ -1,19 +1,15 @@
1
1
  # resume-cli
2
2
 
3
- [![matrix](https://img.shields.io/badge/matrix-join%20chat-%230dbd8b)](https://matrix.to/#/#json-resume:one.ems.host)
4
- [![Build status](https://img.shields.io/github/actions/workflow/status/jsonresume/resume-cli/test.yml?branch=master)](https://github.com/jsonresume/resume-cli/actions)
5
3
  [![npm package](https://badge.fury.io/js/resume-cli.svg)](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
- ## Project Status
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('resume-schema/sample.resume.json');
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
- console.log(err);
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
  }
@@ -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(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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
- if (error) {
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
- console.log('You have to install this theme relative to the folder to use it e.g. `npm install ' + theme + '`');
63
- process.exit();
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
- const html = await (0, _renderHtml.default)({
68
- resume: resumeJson,
69
- themePath
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);
@@ -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(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
14
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
15
15
  const {
16
16
  createReadStream
17
17
  } = _fs.default;
@@ -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(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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
- Object {
21
- "basics": Object {
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
- Object {
33
- "basics": Object {
34
- "email": "thomas@example.com",
35
- "name": "thomas",
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
- Object {
45
- "basics": Object {
44
+ {
45
+ "basics": {
46
46
  "email": "thomas@example.com",
47
47
  "name": "thomas",
48
48
  },
49
- "work": Array [
50
- Object {
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
- Object {
75
- "basics": Object {
74
+ {
75
+ "basics": {
76
76
  "email": "thomas@example.com",
77
77
  "name": "thomas",
78
78
  },
@@ -12,7 +12,7 @@ var _default = async ({
12
12
  } = {}) => {
13
13
  let path = pathArg;
14
14
  if (!path) {
15
- path = require.resolve('resume-schema/schema.json');
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(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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('resume-schema/sample.resume.json');
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(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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>', 'Specify theme used by `export` and `serve` or specify 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.');
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
  });
@@ -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(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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
- stdio: ['pipe', 'pipe', 2, 'ipc']
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 stdout = await (0, _streamToString.default)(child.stdout);
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> Specify theme used by \`export\` and
57
- \`serve\` or specify a path starting with .
58
- (use . for current directory or
59
- ../some/other/dir) (default:
60
- \\"jsonresume-theme-even\\")
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
- \\"resume.json\\")
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: \\"public\\")
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', '-' // this is the dash
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
  });
@@ -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 Error(`Theme ${themePath} could not be resolved relative to ${cwd}`);
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 Error(`theme path ${themePath} could not be resolved from current working directory`);
40
+ throw new _themeErrors.ThemeNotFoundError(themePath);
40
41
  }
41
42
  const theme = require(path);
42
- if (typeof (theme === null || theme === void 0 ? void 0 : theme.render) !== 'function') {
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(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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 _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }
8
- function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
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
- notAValidKey: {
18
- foo: 'bar'
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 _util = require("util");
8
- var _zSchema = _interopRequireDefault(require("z-schema"));
9
- var _zSchemaErrors = _interopRequireDefault(require("z-schema-errors"));
10
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
11
- const reporter = _zSchemaErrors.default.init();
12
- const validator = new _zSchema.default();
13
- const validate = (0, _util.promisify)((...args) => validator.validate(...args)); // maintains context
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
- try {
19
- return await validate(resume, schema);
20
- } catch (errors) {
21
- throw new Error(reporter.extractMessage({
22
- report: {
23
- errors
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;
@@ -2,7 +2,7 @@
2
2
 
3
3
  var _validate = _interopRequireDefault(require("./validate"));
4
4
  var _getSchema = _interopRequireDefault(require("./get-schema"));
5
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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 an error for an invalid resume object', async () => {
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
- notInTheSchema: true
22
+ basics: {
23
+ name: 123
24
+ }
23
25
  },
24
26
  schema: defaultSchema
25
- })).rejects.toMatchInlineSnapshot(`[Error: An error occurred 'Additional properties not allowed: notInTheSchema'.]`);
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(`[Error: An error occurred 'Expected type number but found type string'.]`);
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.1.2",
3
+ "version": "3.3.0",
4
4
  "description": "The JSON Resume command line interface",
5
- "main": "index.js",
5
+ "main": "build/main.js",
6
6
  "engines": {
7
- "node": ">=12 <18"
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": "^0.6.0",
34
+ "jsonresume-theme-even": "0.6.0",
42
35
  "mime-types": "^2.1.27",
43
36
  "object-path-immutable": "^4.1.1",
44
- "puppeteer": "^18.2.1",
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
- "z-schema": "^5.0.0",
53
- "z-schema-errors": "^0.2.1"
44
+ "@jsonresume/schema": "1.2.1"
54
45
  },
55
46
  "devDependencies": {
56
- "@babel/cli": "7.12.10",
57
- "@babel/core": "7.12.10",
58
- "@babel/eslint-parser": "7.12.1",
59
- "@babel/node": "7.12.10",
60
- "@babel/plugin-proposal-optional-chaining": "7.12.7",
61
- "@babel/preset-env": "7.12.11",
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
- "husky": "5.0.6",
72
- "jest": "28.1.2",
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
+ }