From 5c33c6766671b37c3a26743d195001462cd4a462 Mon Sep 17 00:00:00 2001 From: Rafael Bardini Date: Mon, 9 Jan 2023 01:23:48 +0100 Subject: [PATCH] feat(theme)!: remove theme auto-load (#4) --- README.md | 11 +- examples/with-jsonresume-theme/package.json | 4 +- examples/with-local-theme/package.json | 4 +- examples/with-node-api/package.json | 2 +- examples/with-pdf-export/package.json | 2 +- package-lock.json | 87 ++++++++---- package.json | 3 - src/cli.ts | 40 +++--- src/index.ts | 1 - src/load-themes.ts | 58 -------- test/cli.test.ts | 142 ++++++++------------ test/load-themes.test.ts | 53 -------- 12 files changed, 149 insertions(+), 258 deletions(-) delete mode 100644 src/load-themes.ts delete mode 100644 test/load-themes.test.ts diff --git a/README.md b/README.md index be67b4d..b323447 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,9 @@ ๐Ÿ‘” Lightweight [JSON Resume](https://jsonresume.org/) builder, no-frills [alternative to resume-cli](#motivation). -- ๐Ÿ—œ๏ธ Small (~125 lines) +- ๐Ÿ—œ๏ธ Small (~120 LOC) - ๐Ÿงฉ CLI and Node.js API - ๐Ÿค– TypeScript typings -- ๐ŸŽจ Theme auto-load - โฑ๏ธ Async render support - ๐Ÿงช 100% code coverage @@ -20,8 +19,6 @@ npm install resumed jsonresume-theme-even # or your theme of choice ``` -โ„น๏ธ Global installation is not supported, as it breaks theme discovery. - ## Usage ```console @@ -59,7 +56,7 @@ Render resume. **Options:** - `-o`, `--output`: Output filename (default `resume.html`) -- `-t`, `--theme`: Theme to use, if more than one is installed +- `-t`, `--theme`: Theme to use - `-h`, `--help`: Display help message ### `init` @@ -92,9 +89,9 @@ Resumed is a _complete reimplementation_ of resume-cli, using more modern techno ### Theme resolution -Resumed automatically loads and uses the first installed [theme](https://www.npmjs.com/search?q=jsonresume-theme) found when rendering (exporting) a resume, similar to how [Prettier plugins](https://prettier.io/docs/en/plugins.html#using-plugins) work. If no theme is installed, Resumed will guide you on how to proceed. It will also let you know if _multiple_ themes are found, which one it picked, and how to [use another one](#render-default). +Resumed does not install any themes. You must [pick and install one](https://www.npmjs.com/search?q=jsonresume-theme) yourself, and specify your choice via the `--theme` option or the `.meta.theme` field of your resume. -In contrast, resume-cli comes with a theme, and requires specifying what theme to use if the default does not suit you. This is fine for most users, but it ties the default theme package release cycle to that of the CLI, and is a little more verbose. +In contrast, resume-cli comes with a default theme, and only supports the `--theme` option. This is fine for most users, but it ties the default theme package release cycle to that of the CLI, and may be a little more verbose. ### Interface diff --git a/examples/with-jsonresume-theme/package.json b/examples/with-jsonresume-theme/package.json index 1c4b043..b3608c8 100644 --- a/examples/with-jsonresume-theme/package.json +++ b/examples/with-jsonresume-theme/package.json @@ -3,10 +3,10 @@ "type": "module", "scripts": { "init": "resumed init", - "start": "resumed" + "start": "resumed --theme jsonresume-theme-even" }, "dependencies": { "jsonresume-theme-even": "^0.17.0", - "resumed": "^1.0.0" + "resumed": "^2.0.0" } } diff --git a/examples/with-local-theme/package.json b/examples/with-local-theme/package.json index 03a69b0..9821f2a 100644 --- a/examples/with-local-theme/package.json +++ b/examples/with-local-theme/package.json @@ -3,10 +3,10 @@ "type": "module", "scripts": { "init": "resumed init", - "start": "resumed" + "start": "resumed --theme jsonresume-theme-local" }, "dependencies": { "jsonresume-theme-local": "file:./theme", - "resumed": "^1.0.0" + "resumed": "^2.0.0" } } diff --git a/examples/with-node-api/package.json b/examples/with-node-api/package.json index 3f591eb..57e47e5 100644 --- a/examples/with-node-api/package.json +++ b/examples/with-node-api/package.json @@ -8,6 +8,6 @@ "dependencies": { "jsonresume-theme-even": "^0.17.0", "puppeteer": "^13.0.0", - "resumed": "^1.0.0" + "resumed": "^2.0.0" } } diff --git a/examples/with-pdf-export/package.json b/examples/with-pdf-export/package.json index 3f591eb..57e47e5 100644 --- a/examples/with-pdf-export/package.json +++ b/examples/with-pdf-export/package.json @@ -8,6 +8,6 @@ "dependencies": { "jsonresume-theme-even": "^0.17.0", "puppeteer": "^13.0.0", - "resumed": "^1.0.0" + "resumed": "^2.0.0" } } diff --git a/package-lock.json b/package-lock.json index 77de893..51973cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,6 @@ "license": "MIT", "dependencies": { "colorette": "^2.0.0", - "escalade": "^3.1.0", - "globby": "^11.0.0", - "lodash.uniqby": "^4.7.0", "resume-schema": "^1.0.0", "sade": "^1.7.0" }, @@ -2335,6 +2332,7 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -2347,6 +2345,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, "engines": { "node": ">= 8" } @@ -2355,6 +2354,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -3204,6 +3204,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, "engines": { "node": ">=8" } @@ -3537,6 +3538,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -4252,6 +4254,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, "dependencies": { "path-type": "^4.0.0" }, @@ -4497,6 +4500,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, "engines": { "node": ">=6" } @@ -5160,6 +5164,7 @@ "version": "3.2.11", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -5175,6 +5180,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -5198,6 +5204,7 @@ "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -5246,6 +5253,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -5520,6 +5528,7 @@ "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -5748,6 +5757,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true, "engines": { "node": ">= 4" } @@ -5967,6 +5977,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -5996,6 +6007,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -6044,6 +6056,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "engines": { "node": ">=0.12.0" } @@ -7435,11 +7448,6 @@ "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", "dev": true }, - "node_modules/lodash.uniqby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", - "integrity": "sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI=" - }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -7625,6 +7633,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, "engines": { "node": ">= 8" } @@ -7653,6 +7662,7 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -8133,6 +8143,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, "engines": { "node": ">=8" } @@ -8147,6 +8158,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "engines": { "node": ">=8.6" }, @@ -8631,6 +8643,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, "funding": [ { "type": "github", @@ -8886,6 +8899,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -9059,6 +9073,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "funding": [ { "type": "github", @@ -9213,6 +9228,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, "engines": { "node": ">=8" } @@ -9737,6 +9753,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -12146,6 +12163,7 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "requires": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -12154,12 +12172,14 @@ "@nodelib/fs.stat": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true }, "@nodelib/fs.walk": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, "requires": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -12791,7 +12811,8 @@ "array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true }, "array.prototype.flat": { "version": "1.3.0", @@ -13045,6 +13066,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, "requires": { "fill-range": "^7.0.1" } @@ -13570,6 +13592,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, "requires": { "path-type": "^4.0.0" } @@ -13777,7 +13800,8 @@ "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true }, "escape-string-regexp": { "version": "4.0.0", @@ -14263,6 +14287,7 @@ "version": "3.2.11", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -14275,6 +14300,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "requires": { "is-glob": "^4.0.1" } @@ -14297,6 +14323,7 @@ "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, "requires": { "reusify": "^1.0.4" } @@ -14339,6 +14366,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, "requires": { "to-regex-range": "^5.0.1" } @@ -14537,6 +14565,7 @@ "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, "requires": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -14688,7 +14717,8 @@ "ignore": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==" + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true }, "import-fresh": { "version": "3.3.0", @@ -14838,7 +14868,8 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true }, "is-fullwidth-code-point": { "version": "4.0.0", @@ -14856,6 +14887,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "requires": { "is-extglob": "^2.1.1" } @@ -14887,7 +14919,8 @@ "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true }, "is-number-object": { "version": "1.0.7", @@ -15946,11 +15979,6 @@ "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", "dev": true }, - "lodash.uniqby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", - "integrity": "sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI=" - }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -16097,7 +16125,8 @@ "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true }, "micromark": { "version": "2.11.4", @@ -16113,6 +16142,7 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, "requires": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -16477,7 +16507,8 @@ "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true }, "picocolors": { "version": "1.0.0", @@ -16488,7 +16519,8 @@ "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true }, "pidtree": { "version": "0.5.0", @@ -16844,7 +16876,8 @@ "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true }, "randombytes": { "version": "2.1.0", @@ -17032,7 +17065,8 @@ "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true }, "rfdc": { "version": "1.3.0", @@ -17153,6 +17187,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "requires": { "queue-microtask": "^1.2.2" } @@ -17265,7 +17300,8 @@ "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true }, "slice-ansi": { "version": "5.0.0", @@ -17673,6 +17709,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "requires": { "is-number": "^7.0.0" } diff --git a/package.json b/package.json index 204cf39..ae6e9d6 100644 --- a/package.json +++ b/package.json @@ -46,9 +46,6 @@ }, "dependencies": { "colorette": "^2.0.0", - "escalade": "^3.1.0", - "globby": "^11.0.0", - "lodash.uniqby": "^4.7.0", "resume-schema": "^1.0.0", "sade": "^1.7.0" }, diff --git a/src/cli.ts b/src/cli.ts index dba056c..9a20e36 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,7 +2,7 @@ import { promises as fs } from 'fs' import { red, yellow } from 'colorette' import sade from 'sade' -import { init, loadThemes, render, validate } from '.' +import { init, render, validate } from '.' const pkg = require('../package.json') @@ -19,39 +19,39 @@ cli default: true, }) .option('-o, --output', 'Output filename', 'resume.html') - .option('-t, --theme', 'Theme to use, if more than one is installed') + .option('-t, --theme', 'Theme to use') .action( async ( filename: string = 'resume.json', { output, theme }: RenderOptions, ) => { const resume = JSON.parse(await fs.readFile(filename, 'utf-8')) - const [loadedTheme, ...otherLoadedThemes] = await loadThemes(theme) - if (loadedTheme == null) { - console.log( - `Could not find a JSON Resume theme to render. Try installing one (e.g. ${yellow( - 'npm i jsonresume-theme-even', - )}) and run the command again. ๐Ÿ˜‰`, + const themeName = theme ?? resume?.meta?.theme + if (!themeName) { + console.error( + `No theme to use. Please specify one via the ${yellow( + '--theme', + )} option or the ${yellow('.meta.theme')} field of your resume.`, ) process.exitCode = 1 return } - if (otherLoadedThemes.length > 0) { - console.log( - `Found ${ - otherLoadedThemes.length + 1 - } JSON Resume themes installed, defaulting to ${yellow( - loadedTheme.name, - )}. Pass the ${yellow( - '--theme', - )} option if you would like to use another one.`, + let themeModule + try { + themeModule = await import(themeName) + } catch { + console.error( + `Could not load theme ${yellow(themeName)}. Is it installed?`, ) + + process.exitCode = 1 + return } - const rendered = await render(resume, loadedTheme.module) + const rendered = await render(resume, themeModule) await fs.writeFile(output, rendered) console.log( @@ -82,11 +82,11 @@ cli throw err } - console.log( + console.error( `Uh-oh! The following errors were found in ${yellow(filename)}:\n`, ) err.forEach((err: { message: string; path: string }) => - console.log(` ${red(`โŒ ${err.message}`)} at ${yellow(err.path)}.`), + console.error(` ${red(`โŒ ${err.message}`)} at ${yellow(err.path)}.`), ) process.exitCode = 1 diff --git a/src/index.ts b/src/index.ts index e997a0e..7afcf12 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,4 @@ export { cli } from './cli' export { init } from './init' -export { loadThemes } from './load-themes' export { render } from './render' export { validate } from './validate' diff --git a/src/load-themes.ts b/src/load-themes.ts deleted file mode 100644 index fb4f3ae..0000000 --- a/src/load-themes.ts +++ /dev/null @@ -1,58 +0,0 @@ -// Based on https://github.com/prettier/prettier/blob/master/src/common/load-plugins.js -import { promises as fs } from 'fs' -import path from 'path' -import escalade from 'escalade' -import globby from 'globby' -import uniqBy from 'lodash.uniqby' - -const isDir = async (dir: string) => { - try { - return (await fs.stat(dir)).isDirectory() - } catch { - return false - } -} - -const findThemesInNodeModules = async (theme = '*', nodeModulesDir: string) => - ( - await globby( - [ - `jsonresume-theme-${theme}/package.json`, - `@*/jsonresume-theme-${theme}/package.json`, - `@jsonresume/theme-${theme}/package.json`, - ], - { cwd: nodeModulesDir, expandDirectories: false }, - ) - ).map(path.dirname) - -export const loadThemes = async (theme?: string) => { - const themeSearchDir = await escalade( - process.cwd(), - (_dir, names) => names.includes('node_modules') && '.', - ) - - if (!themeSearchDir) { - throw new Error('node_modules directory not found') - } - - const nodeModulesDir = path.resolve(themeSearchDir, 'node_modules') - - // In some fringe cases (e.g. files "mounted" as virtual directories), - // the isDir(themeSearchDir) check might be false even though node_modules - // actually exists. - if (!(await isDir(nodeModulesDir)) && !(await isDir(themeSearchDir))) { - throw new Error(`${themeSearchDir} does not exist or is not a directory`) - } - - const themesInfo = (await findThemesInNodeModules(theme, nodeModulesDir)).map( - name => ({ - name: name.split('-').pop()!, - path: require.resolve(name, { paths: [themeSearchDir] }), - }), - ) - - return uniqBy(themesInfo, 'path').map(info => ({ - ...info, - module: require(info.path), - })) -} diff --git a/test/cli.test.ts b/test/cli.test.ts index b62e33a..ec98d55 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -1,5 +1,7 @@ import { promises as fs } from 'fs' -import { init, loadThemes, render, validate } from '../src' +// @ts-ignore +import * as theme from 'jsonresume-theme-even' +import { init, render, validate } from '../src' import { cli } from '../src/cli' jest.mock('fs', () => { @@ -18,6 +20,7 @@ jest.mock('fs', () => { jest.mock('../src') const logSpy = jest.spyOn(console, 'log').mockImplementation() +const errorSpy = jest.spyOn(console, 'error').mockImplementation() describe('init', () => { it('creates a sample resume with default filename', async () => { @@ -47,25 +50,18 @@ describe('init', () => { describe('render', () => { it('renders a resume with default filename', async () => { - const resume = { resume: {} } - const loadedThemes = [ - { module: { render: 'theme1' }, name: 'theme1', path: '/theme1' }, - ] + const resume = {} jest.mocked(fs.readFile).mockResolvedValueOnce(JSON.stringify(resume)) - jest.mocked(loadThemes).mockResolvedValueOnce(loadedThemes) jest.mocked(render).mockResolvedValueOnce('rendered') - await cli.parse(['', '', 'render']) + await cli.parse(['', '', 'render', '--theme', 'jsonresume-theme-even']) expect(fs.readFile).toHaveBeenCalledTimes(1) expect(fs.readFile).toHaveBeenCalledWith('resume.json', 'utf-8') - expect(loadThemes).toHaveBeenCalledTimes(1) - expect(loadThemes).toHaveBeenCalledWith(undefined) - expect(render).toHaveBeenCalledTimes(1) - expect(render).toHaveBeenCalledWith(resume, loadedThemes[0].module) + expect(render).toHaveBeenCalledWith(resume, theme) expect(fs.writeFile).toHaveBeenCalledTimes(1) expect(fs.writeFile).toHaveBeenCalledWith('resume.html', 'rendered') @@ -77,25 +73,25 @@ describe('render', () => { }) it('renders a resume with custom filename', async () => { - const resume = { resume: {} } - const loadedThemes = [ - { module: { render: 'theme1' }, name: 'theme1', path: '/theme1' }, - ] + const resume = {} jest.mocked(fs.readFile).mockResolvedValueOnce(JSON.stringify(resume)) - jest.mocked(loadThemes).mockResolvedValueOnce(loadedThemes) jest.mocked(render).mockResolvedValueOnce('rendered') - await cli.parse(['', '', 'render', 'custom.json']) + await cli.parse([ + '', + '', + 'render', + 'custom.json', + '--theme', + 'jsonresume-theme-even', + ]) expect(fs.readFile).toHaveBeenCalledTimes(1) expect(fs.readFile).toHaveBeenCalledWith('custom.json', 'utf-8') - expect(loadThemes).toHaveBeenCalledTimes(1) - expect(loadThemes).toHaveBeenCalledWith(undefined) - expect(render).toHaveBeenCalledTimes(1) - expect(render).toHaveBeenCalledWith(resume, loadedThemes[0].module) + expect(render).toHaveBeenCalledWith(resume, theme) expect(fs.writeFile).toHaveBeenCalledTimes(1) expect(fs.writeFile).toHaveBeenCalledWith('resume.html', 'rendered') @@ -106,61 +102,50 @@ describe('render', () => { ) }) - it('renders a resume with first theme found when multiple are installed', async () => { - const resume = { resume: {} } - const loadedThemes = [ - { module: { render: 'theme1' }, name: 'theme1', path: '/theme1' }, - { module: { render: 'theme2' }, name: 'theme2', path: '/theme2' }, - ] + it('renders a resume with custom output', async () => { + const resume = {} jest.mocked(fs.readFile).mockResolvedValueOnce(JSON.stringify(resume)) - jest.mocked(loadThemes).mockResolvedValueOnce(loadedThemes) jest.mocked(render).mockResolvedValueOnce('rendered') - await cli.parse(['', '', 'render']) + await cli.parse([ + '', + '', + 'render', + '--theme', + 'jsonresume-theme-even', + '--output', + 'custom-output.html', + ]) expect(fs.readFile).toHaveBeenCalledTimes(1) expect(fs.readFile).toHaveBeenCalledWith('resume.json', 'utf-8') - expect(loadThemes).toHaveBeenCalledTimes(1) - expect(loadThemes).toHaveBeenCalledWith(undefined) - expect(render).toHaveBeenCalledTimes(1) - expect(render).toHaveBeenCalledWith(resume, loadedThemes[0].module) + expect(render).toHaveBeenCalledWith(resume, theme) expect(fs.writeFile).toHaveBeenCalledTimes(1) - expect(fs.writeFile).toHaveBeenCalledWith('resume.html', 'rendered') + expect(fs.writeFile).toHaveBeenCalledWith('custom-output.html', 'rendered') - expect(logSpy).toHaveBeenCalledTimes(2) - expect(logSpy.mock.calls.join('\n')).toMatchInlineSnapshot(` - "Found 2 JSON Resume themes installed, defaulting to theme1. Pass the --theme option if you would like to use another one. - You can find your rendered resume at resume.html. Nice work! ๐Ÿš€" - `) + expect(logSpy).toHaveBeenCalledTimes(1) + expect(logSpy.mock.calls.join('\n')).toMatchInlineSnapshot( + `"You can find your rendered resume at custom-output.html. Nice work! ๐Ÿš€"`, + ) }) - it('renders a resume with specific theme', async () => { - const resume = { resume: {} } + it('renders a resume with theme defined via the `.meta.theme` field', async () => { + const resume = { meta: { theme: 'jsonresume-theme-even' } } jest.mocked(fs.readFile).mockResolvedValueOnce(JSON.stringify(resume)) - jest - .mocked(loadThemes) - .mockImplementationOnce(theme => - Promise.resolve([ - { module: { render: theme }, name: theme!, path: `/${theme}` }, - ]), - ) jest.mocked(render).mockResolvedValueOnce('rendered') - await cli.parse(['', '', 'render', '--theme', 'custom-theme']) + await cli.parse(['', '', 'render']) expect(fs.readFile).toHaveBeenCalledTimes(1) expect(fs.readFile).toHaveBeenCalledWith('resume.json', 'utf-8') - expect(loadThemes).toHaveBeenCalledTimes(1) - expect(loadThemes).toHaveBeenCalledWith('custom-theme') - expect(render).toHaveBeenCalledTimes(1) - expect(render).toHaveBeenCalledWith(resume, { render: 'custom-theme' }) + expect(render).toHaveBeenCalledWith(resume, theme) expect(fs.writeFile).toHaveBeenCalledTimes(1) expect(fs.writeFile).toHaveBeenCalledWith('resume.html', 'rendered') @@ -171,51 +156,38 @@ describe('render', () => { ) }) - it('renders a resume with custom output', async () => { - const resume = { resume: {} } - const loadedThemes = [ - { module: { render: 'theme1' }, name: 'theme1', path: '/theme1' }, - ] + it('asks to define a theme if none specified and exits with failure code', async () => { + const resume = {} jest.mocked(fs.readFile).mockResolvedValueOnce(JSON.stringify(resume)) - jest.mocked(loadThemes).mockResolvedValueOnce(loadedThemes) - jest.mocked(render).mockResolvedValueOnce('rendered') - await cli.parse(['', '', 'render', '--output', 'custom-output.html']) + await cli.parse(['', '', 'render']) expect(fs.readFile).toHaveBeenCalledTimes(1) expect(fs.readFile).toHaveBeenCalledWith('resume.json', 'utf-8') - expect(loadThemes).toHaveBeenCalledTimes(1) - expect(loadThemes).toHaveBeenCalledWith(undefined) - - expect(render).toHaveBeenCalledTimes(1) - expect(render).toHaveBeenCalledWith(resume, { render: 'theme1' }) - - expect(fs.writeFile).toHaveBeenCalledTimes(1) - expect(fs.writeFile).toHaveBeenCalledWith('custom-output.html', 'rendered') - - expect(logSpy).toHaveBeenCalledTimes(1) - expect(logSpy.mock.calls.join('\n')).toMatchInlineSnapshot( - `"You can find your rendered resume at custom-output.html. Nice work! ๐Ÿš€"`, + expect(errorSpy).toHaveBeenCalledTimes(1) + expect(errorSpy.mock.calls[0][0]).toMatchInlineSnapshot( + `"No theme to use. Please specify one via the --theme option or the .meta.theme field of your resume."`, ) + + expect(render).not.toHaveBeenCalled() + expect(process.exitCode).toBe(1) }) - it('asks to install a theme if none found and exits with failure code', async () => { - jest.mocked(fs.readFile).mockResolvedValueOnce('{}') - jest.mocked(loadThemes).mockResolvedValueOnce([]) + it('asks if theme is installed if theme cannot be loaded and exits with failure code', async () => { + const resume = {} - await cli.parse(['', '', 'render']) + jest.mocked(fs.readFile).mockResolvedValueOnce(JSON.stringify(resume)) + + await cli.parse(['', '', 'render', '--theme', 'jsonresume-theme-missing']) expect(fs.readFile).toHaveBeenCalledTimes(1) expect(fs.readFile).toHaveBeenCalledWith('resume.json', 'utf-8') - expect(loadThemes).toHaveBeenCalledTimes(1) - expect(loadThemes).toHaveBeenCalledWith(undefined) - - expect(logSpy).toHaveBeenCalledTimes(1) - expect(logSpy.mock.calls[0][0]).toMatchInlineSnapshot( - `"Could not find a JSON Resume theme to render. Try installing one (e.g. npm i jsonresume-theme-even) and run the command again. ๐Ÿ˜‰"`, + expect(errorSpy).toHaveBeenCalledTimes(1) + expect(errorSpy.mock.calls[0][0]).toMatchInlineSnapshot( + `"Could not load theme jsonresume-theme-missing. Is it installed?"`, ) expect(render).not.toHaveBeenCalled() @@ -266,8 +238,8 @@ describe('validate', () => { await cli.parse(['', '', 'validate']) - expect(logSpy).toHaveBeenCalledTimes(errors.length + 1) - expect(logSpy.mock.calls.join('\n')).toMatchInlineSnapshot(` + expect(errorSpy).toHaveBeenCalledTimes(errors.length + 1) + expect(errorSpy.mock.calls.join('\n')).toMatchInlineSnapshot(` "Uh-oh! The following errors were found in resume.json: โŒ message 0 at path 0. diff --git a/test/load-themes.test.ts b/test/load-themes.test.ts deleted file mode 100644 index 773d3a2..0000000 --- a/test/load-themes.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -beforeEach(() => jest.resetModules()) - -it('loads installed themes', async () => { - const { loadThemes } = await import('../src/load-themes') - - await expect(loadThemes()).resolves.toStrictEqual([ - { - module: expect.objectContaining({ - render: expect.any(Function), - }), - name: 'even', - path: expect.stringContaining('/node_modules/jsonresume-theme-even/'), - }, - ]) -}) - -it('loads specific installed theme', async () => { - const { loadThemes } = await import('../src/load-themes') - - await expect(loadThemes('even')).resolves.toStrictEqual([ - { - module: expect.objectContaining({ - render: expect.any(Function), - }), - name: 'even', - path: expect.stringContaining('/node_modules/jsonresume-theme-even/'), - }, - ]) -}) - -it('loads nothing if specific theme not installed', async () => { - const { loadThemes } = await import('../src/load-themes') - - await expect(loadThemes('flat')).resolves.toStrictEqual([]) -}) - -it('throws if node_modules directory not found', async () => { - jest.doMock('escalade', () => jest.fn().mockResolvedValue(undefined)) - - const { loadThemes } = await import('../src/load-themes') - - await expect(loadThemes()).rejects.toThrow('node_modules directory not found') -}) - -it('throws if node_modules is not a directory', async () => { - jest.doMock('escalade', () => jest.fn().mockResolvedValue('/node_modules')) - - const { loadThemes } = await import('../src/load-themes') - - await expect(loadThemes()).rejects.toThrow( - '/node_modules does not exist or is not a directory', - ) -})