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 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
- 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
  }
@@ -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
- 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);
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>', '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
  });
@@ -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
- 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,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> 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:
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
  });
@@ -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,7 +37,7 @@ 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
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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "resume-cli",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "The JSON Resume command line interface",
5
5
  "main": "build/main.js",
6
6
  "engines": {