UNPKG

5.56 kBJavaScriptView Raw
1"use strict";
2
3const { exec } = require("child_process");
4const path = require("path");
5
6const filterObject = require("object-filter");
7const mapValues = require("map-values");
8const parallel = require("run-parallel");
9const semver = require("semver");
10
11const tools = require("./tools");
12
13const runningOnWindows = (process.platform === "win32");
14
15const originalPath = process.env.PATH;
16
17const pathSeparator = runningOnWindows ? ";" : ":";
18const localBinPath = path.resolve("node_modules/.bin")
19// ignore locally installed packages
20const globalPath = originalPath
21 .split(pathSeparator)
22 .filter(p => path.resolve(p)!==localBinPath)
23 .join(pathSeparator)
24;
25
26module.exports = function check(wanted, callback) {
27 // Normalize arguments
28 if (typeof wanted === "function") {
29 callback = wanted;
30 wanted = null;
31 }
32
33 const options = { callback };
34
35 options.wanted = normalizeWanted(wanted);
36
37 options.commands = mapValues(
38 (
39 Object.keys(options.wanted).length
40 ? filterObject(tools, (_, key) => options.wanted[key])
41 : tools
42 ),
43 ({ getVersion }) => ( runVersionCommand.bind(null, getVersion) )
44 );
45
46 if (runningOnWindows) {
47 runForWindows(options);
48 } else {
49 run(options);
50 }
51}
52
53function runForWindows(options) {
54 // See and understand https://github.com/parshap/check-node-version/issues/35
55 // before trying to optimize this function
56 //
57 // `chcp` is used instead of `where` on account of its more extensive availablity
58 // chcp: MS-DOS 6.22+, Windows 95+; where: Windows 7+
59 //
60 // Plus, in order to be absolutely certain, the error message of `where` would still need evaluation.
61
62 exec("chcp", (error, stdout) => {
63 const finalCallback = options.callback;
64
65 if (error) {
66 finalCallback(chcpError(error, 1));
67 return;
68 }
69
70 const codepage = stdout.match(/\d+/)[0];
71
72 if (codepage === "65001" || codepage === "437") {
73 // need not switch codepage
74 return run(options);
75 }
76
77 // reset codepage before exiting
78 options.callback = (...args) => exec(`chcp ${codepage}`, (error) => {
79 if (error) {
80 finalCallback(chcpError(error, 3));
81 return;
82 }
83
84 finalCallback(...args);
85 });
86
87 // switch to Unicode
88 exec("chcp 65001", (error) => {
89 if (error) {
90 finalCallback(chcpError(error, 2));
91 return;
92 }
93
94 run(options);
95 });
96
97 function chcpError(error, step) {
98 switch (step) {
99 case 1:
100 error.message = `[CHCP] error while getting current codepage:\n${error.message}`;
101 break;
102
103 case 2:
104 error.message = `[CHCP] error while switching to Unicode codepage:\n${error.message}`;
105 break;
106
107 case 3:
108 error.message = `
109 [CHCP] error while resetting current codepage:
110 ${error.message}
111
112 Please note that your terminal is now using the Unicode codepage.
113 Therefore, codepage-dependent actions may work in an unusual manner.
114 You can run \`chcp ${codepage}\` yourself in order to reset your codepage,
115 or just close this terminal and work in another.
116 `.trim().replace(/^ +/gm,'') // strip indentation
117 break;
118
119 // no default
120 }
121
122 return error
123 }
124 });
125}
126
127function run({ commands, callback, wanted }) {
128 parallel(commands, (err, versionsResult) => {
129 if (err) {
130 callback(err);
131 return;
132 }
133
134 const versions = mapValues(versionsResult, (_, name) => {
135 const programInfo = {
136 isSatisfied: true,
137 };
138
139 if (versionsResult[name].version) {
140 programInfo.version = semver(versionsResult[name].version);
141 }
142
143 if (versionsResult[name].notfound) {
144 programInfo.notfound = versionsResult[name].notfound;
145 }
146
147 if (wanted[name]) {
148 programInfo.wanted = new semver.Range(wanted[name]);
149 programInfo.isSatisfied = Boolean(
150 programInfo.version
151 &&
152 semver.satisfies(programInfo.version, programInfo.wanted)
153 );
154 }
155 return programInfo;
156 });
157
158 callback(null, {
159 versions: versions,
160 isSatisfied: Object.keys(wanted).every(name => versions[name].isSatisfied),
161 });
162 });
163};
164
165
166// Return object containing only keys that a program exists for and
167// something valid was given.
168function normalizeWanted(wanted) {
169 if (!wanted) {
170 return {};
171 }
172
173 // Validate keys
174 wanted = filterObject(wanted, Boolean);
175
176 // Normalize to strings
177 wanted = mapValues(wanted, String);
178
179 // Filter existing programs
180 wanted = filterObject(wanted, (_, key) => tools[key]);
181
182 return wanted;
183}
184
185
186function runVersionCommand(command, callback) {
187 process.env.PATH = globalPath;
188
189 exec(command, (execError, stdout, stderr) => {
190 const commandDescription = JSON.stringify(command);
191
192 if (!execError) {
193 return callback(null, {
194 version: stdout,
195 });
196 }
197
198 if (toolNotFound(execError)) {
199 return callback(null, {
200 notfound: true,
201 });
202 }
203
204 // something went very wrong during execution
205 let errorMessage = `Command failed: ${commandDescription}`
206
207 if (stderr) {
208 errorMessage += `\n\nstderr:\n${stderr.toString().trim()}\n`;
209 }
210
211 errorMessage += `\n\noriginal error message:\n${execError.message}\n`;
212
213 return callback(new Error(errorMessage));
214 });
215
216 process.env.PATH = originalPath;
217}
218
219
220function toolNotFound(execError) {
221 if (runningOnWindows) {
222 return execError.message.includes("is not recognized");
223 }
224
225 return execError.code === 127;
226}