1 | /**
|
2 | * @fileoverview Utility for executing npm commands.
|
3 | * @author Ian VanSchooten
|
4 | */
|
5 |
|
6 | ;
|
7 |
|
8 | //------------------------------------------------------------------------------
|
9 | // Requirements
|
10 | //------------------------------------------------------------------------------
|
11 |
|
12 | const fs = require("fs"),
|
13 | spawn = require("cross-spawn"),
|
14 | path = require("path"),
|
15 | log = require("./logging");
|
16 |
|
17 | //------------------------------------------------------------------------------
|
18 | // Helpers
|
19 | //------------------------------------------------------------------------------
|
20 |
|
21 | /**
|
22 | * Find the closest package.json file, starting at process.cwd (by default),
|
23 | * and working up to root.
|
24 | *
|
25 | * @param {string} [startDir=process.cwd()] Starting directory
|
26 | * @returns {string} Absolute path to closest package.json file
|
27 | */
|
28 | function findPackageJson(startDir) {
|
29 | let dir = path.resolve(startDir || process.cwd());
|
30 |
|
31 | do {
|
32 | const pkgFile = path.join(dir, "package.json");
|
33 |
|
34 | if (!fs.existsSync(pkgFile) || !fs.statSync(pkgFile).isFile()) {
|
35 | dir = path.join(dir, "..");
|
36 | continue;
|
37 | }
|
38 | return pkgFile;
|
39 | } while (dir !== path.resolve(dir, ".."));
|
40 | return null;
|
41 | }
|
42 |
|
43 | //------------------------------------------------------------------------------
|
44 | // Private
|
45 | //------------------------------------------------------------------------------
|
46 |
|
47 | /**
|
48 | * Install node modules synchronously and save to devDependencies in package.json
|
49 | * @param {string|string[]} packages Node module or modules to install
|
50 | * @returns {void}
|
51 | */
|
52 | function installSyncSaveDev(packages) {
|
53 | const packageList = Array.isArray(packages) ? packages : [packages];
|
54 | const npmProcess = spawn.sync("npm", ["i", "--save-dev"].concat(packageList),
|
55 | { stdio: "inherit" });
|
56 | const error = npmProcess.error;
|
57 |
|
58 | if (error && error.code === "ENOENT") {
|
59 | const pluralS = packageList.length > 1 ? "s" : "";
|
60 |
|
61 | log.error(`Could not execute npm. Please install the following package${pluralS} with a package manager of your choice: ${packageList.join(", ")}`);
|
62 | }
|
63 | }
|
64 |
|
65 | /**
|
66 | * Fetch `peerDependencies` of the given package by `npm show` command.
|
67 | * @param {string} packageName The package name to fetch peerDependencies.
|
68 | * @returns {Object} Gotten peerDependencies. Returns null if npm was not found.
|
69 | */
|
70 | function fetchPeerDependencies(packageName) {
|
71 | const npmProcess = spawn.sync(
|
72 | "npm",
|
73 | ["show", "--json", packageName, "peerDependencies"],
|
74 | { encoding: "utf8" }
|
75 | );
|
76 |
|
77 | const error = npmProcess.error;
|
78 |
|
79 | if (error && error.code === "ENOENT") {
|
80 | return null;
|
81 | }
|
82 | const fetchedText = npmProcess.stdout.trim();
|
83 |
|
84 | return JSON.parse(fetchedText || "{}");
|
85 |
|
86 |
|
87 | }
|
88 |
|
89 | /**
|
90 | * Check whether node modules are include in a project's package.json.
|
91 | *
|
92 | * @param {string[]} packages Array of node module names
|
93 | * @param {Object} opt Options Object
|
94 | * @param {boolean} opt.dependencies Set to true to check for direct dependencies
|
95 | * @param {boolean} opt.devDependencies Set to true to check for development dependencies
|
96 | * @param {boolean} opt.startdir Directory to begin searching from
|
97 | * @returns {Object} An object whose keys are the module names
|
98 | * and values are booleans indicating installation.
|
99 | */
|
100 | function check(packages, opt) {
|
101 | let deps = [];
|
102 | const pkgJson = (opt) ? findPackageJson(opt.startDir) : findPackageJson();
|
103 | let fileJson;
|
104 |
|
105 | if (!pkgJson) {
|
106 | throw new Error("Could not find a package.json file. Run 'npm init' to create one.");
|
107 | }
|
108 |
|
109 | try {
|
110 | fileJson = JSON.parse(fs.readFileSync(pkgJson, "utf8"));
|
111 | } catch (e) {
|
112 | const error = new Error(e);
|
113 |
|
114 | error.messageTemplate = "failed-to-read-json";
|
115 | error.messageData = {
|
116 | path: pkgJson,
|
117 | message: e.message
|
118 | };
|
119 | throw error;
|
120 | }
|
121 |
|
122 | if (opt.devDependencies && typeof fileJson.devDependencies === "object") {
|
123 | deps = deps.concat(Object.keys(fileJson.devDependencies));
|
124 | }
|
125 | if (opt.dependencies && typeof fileJson.dependencies === "object") {
|
126 | deps = deps.concat(Object.keys(fileJson.dependencies));
|
127 | }
|
128 | return packages.reduce((status, pkg) => {
|
129 | status[pkg] = deps.indexOf(pkg) !== -1;
|
130 | return status;
|
131 | }, {});
|
132 | }
|
133 |
|
134 | /**
|
135 | * Check whether node modules are included in the dependencies of a project's
|
136 | * package.json.
|
137 | *
|
138 | * Convienience wrapper around check().
|
139 | *
|
140 | * @param {string[]} packages Array of node modules to check.
|
141 | * @param {string} rootDir The directory contianing a package.json
|
142 | * @returns {Object} An object whose keys are the module names
|
143 | * and values are booleans indicating installation.
|
144 | */
|
145 | function checkDeps(packages, rootDir) {
|
146 | return check(packages, { dependencies: true, startDir: rootDir });
|
147 | }
|
148 |
|
149 | /**
|
150 | * Check whether node modules are included in the devDependencies of a project's
|
151 | * package.json.
|
152 | *
|
153 | * Convienience wrapper around check().
|
154 | *
|
155 | * @param {string[]} packages Array of node modules to check.
|
156 | * @returns {Object} An object whose keys are the module names
|
157 | * and values are booleans indicating installation.
|
158 | */
|
159 | function checkDevDeps(packages) {
|
160 | return check(packages, { devDependencies: true });
|
161 | }
|
162 |
|
163 | /**
|
164 | * Check whether package.json is found in current path.
|
165 | *
|
166 | * @param {string=} startDir Starting directory
|
167 | * @returns {boolean} Whether a package.json is found in current path.
|
168 | */
|
169 | function checkPackageJson(startDir) {
|
170 | return !!findPackageJson(startDir);
|
171 | }
|
172 |
|
173 | //------------------------------------------------------------------------------
|
174 | // Public Interface
|
175 | //------------------------------------------------------------------------------
|
176 |
|
177 | module.exports = {
|
178 | installSyncSaveDev,
|
179 | fetchPeerDependencies,
|
180 | checkDeps,
|
181 | checkDevDeps,
|
182 | checkPackageJson
|
183 | };
|