UNPKG

9.64 kBJavaScriptView Raw
1
2/**
3 * Copyright (c) Facebook, Inc. and its affiliates.
4 *
5 * This source code is licensed under the MIT license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9'use strict';
10
11const child_process = require('child_process');
12const chalk = require('chalk');
13const fs = require('graceful-fs');
14const path = require('path');
15const http = require('http');
16const https = require('https');
17const temp = require('temp');
18const ignores = require('./ignoreFiles');
19
20const availableCpus = Math.max(require('os').cpus().length - 1, 1);
21const CHUNK_SIZE = 50;
22
23function lineBreak(str) {
24 return /\n$/.test(str) ? str : str + '\n';
25}
26
27const bufferedWrite = (function() {
28 const buffer = [];
29 let buffering = false;
30
31 process.stdout.on('drain', () => {
32 if (!buffering) return;
33 while (buffer.length > 0 && process.stdout.write(buffer.shift()) !== false);
34 if (buffer.length === 0) {
35 buffering = false;
36 }
37 });
38 return function write(msg) {
39 if (buffering) {
40 buffer.push(msg);
41 }
42 if (process.stdout.write(msg) === false) {
43 buffering = true;
44 }
45 };
46}());
47
48const log = {
49 ok(msg, verbose) {
50 verbose >= 2 && bufferedWrite(chalk.white.bgGreen(' OKK ') + msg);
51 },
52 nochange(msg, verbose) {
53 verbose >= 1 && bufferedWrite(chalk.white.bgYellow(' NOC ') + msg);
54 },
55 skip(msg, verbose) {
56 verbose >= 1 && bufferedWrite(chalk.white.bgYellow(' SKIP ') + msg);
57 },
58 error(msg, verbose) {
59 verbose >= 0 && bufferedWrite(chalk.white.bgRed(' ERR ') + msg);
60 },
61};
62
63function report({file, msg}) {
64 bufferedWrite(lineBreak(`${chalk.white.bgBlue(' REP ')}${file} ${msg}`));
65}
66
67function concatAll(arrays) {
68 const result = [];
69 for (const array of arrays) {
70 for (const element of array) {
71 result.push(element);
72 }
73 }
74 return result;
75}
76
77function showFileStats(fileStats) {
78 process.stdout.write(
79 'Results: \n'+
80 chalk.red(fileStats.error + ' errors\n')+
81 chalk.yellow(fileStats.nochange + ' unmodified\n')+
82 chalk.yellow(fileStats.skip + ' skipped\n')+
83 chalk.green(fileStats.ok + ' ok\n')
84 );
85}
86
87function showStats(stats) {
88 const names = Object.keys(stats).sort();
89 if (names.length) {
90 process.stdout.write(chalk.blue('Stats: \n'));
91 }
92 names.forEach(name => process.stdout.write(name + ': ' + stats[name] + '\n'));
93}
94
95function dirFiles (dir, callback, acc) {
96 // acc stores files found so far and counts remaining paths to be processed
97 acc = acc || { files: [], remaining: 1 };
98
99 function done() {
100 // decrement count and return if there are no more paths left to process
101 if (!--acc.remaining) {
102 callback(acc.files);
103 }
104 }
105
106 fs.readdir(dir, (err, files) => {
107 // if dir does not exist or is not a directory, bail
108 // (this should not happen as long as calls do the necessary checks)
109 if (err) throw err;
110
111 acc.remaining += files.length;
112 files.forEach(file => {
113 let name = path.join(dir, file);
114 fs.stat(name, (err, stats) => {
115 if (err) {
116 // probably a symlink issue
117 process.stdout.write(
118 'Skipping path "' + name + '" which does not exist.\n'
119 );
120 done();
121 } else if (ignores.shouldIgnore(name)) {
122 // ignore the path
123 done();
124 } else if (stats.isDirectory()) {
125 dirFiles(name + '/', callback, acc);
126 } else {
127 acc.files.push(name);
128 done();
129 }
130 });
131 });
132 done();
133 });
134}
135
136function getAllFiles(paths, filter) {
137 return Promise.all(
138 paths.map(file => new Promise(resolve => {
139 fs.lstat(file, (err, stat) => {
140 if (err) {
141 process.stderr.write('Skipping path ' + file + ' which does not exist. \n');
142 resolve([]);
143 return;
144 }
145
146 if (stat.isDirectory()) {
147 dirFiles(
148 file,
149 list => resolve(list.filter(filter))
150 );
151 } else if (ignores.shouldIgnore(file)) {
152 // ignoring the file
153 resolve([]);
154 } else {
155 resolve([file]);
156 }
157 })
158 }))
159 ).then(concatAll);
160}
161
162function run(transformFile, paths, options) {
163 let usedRemoteScript = false;
164 const cpus = options.cpus ? Math.min(availableCpus, options.cpus) : availableCpus;
165 const extensions =
166 options.extensions && options.extensions.split(',').map(ext => '.' + ext);
167 const fileCounters = {error: 0, ok: 0, nochange: 0, skip: 0};
168 const statsCounter = {};
169 const startTime = process.hrtime();
170
171 ignores.add(options.ignorePattern);
172 ignores.addFromFile(options.ignoreConfig);
173
174 if (/^http/.test(transformFile)) {
175 usedRemoteScript = true;
176 return new Promise((resolve, reject) => {
177 // call the correct `http` or `https` implementation
178 (transformFile.indexOf('https') !== 0 ? http : https).get(transformFile, (res) => {
179 let contents = '';
180 res
181 .on('data', (d) => {
182 contents += d.toString();
183 })
184 .on('end', () => {
185 const ext = path.extname(transformFile);
186 temp.open({ prefix: 'jscodeshift', suffix: ext }, (err, info) => {
187 if (err) return reject(err);
188 fs.write(info.fd, contents, function (err) {
189 if (err) return reject(err);
190 fs.close(info.fd, function(err) {
191 if (err) return reject(err);
192 transform(info.path).then(resolve, reject);
193 });
194 });
195 });
196 })
197 })
198 .on('error', (e) => {
199 reject(e);
200 });
201 });
202 } else if (!fs.existsSync(transformFile)) {
203 process.stderr.write(
204 chalk.white.bgRed('ERROR') + ' Transform file ' + transformFile + ' does not exist \n'
205 );
206 return;
207 } else {
208 return transform(transformFile);
209 }
210
211 function transform(transformFile) {
212 return getAllFiles(
213 paths,
214 name => !extensions || extensions.indexOf(path.extname(name)) != -1
215 ).then(files => {
216 const numFiles = files.length;
217
218 if (numFiles === 0) {
219 process.stdout.write('No files selected, nothing to do. \n');
220 return [];
221 }
222
223 const processes = options.runInBand ? 1 : Math.min(numFiles, cpus);
224 const chunkSize = processes > 1 ?
225 Math.min(Math.ceil(numFiles / processes), CHUNK_SIZE) :
226 numFiles;
227
228 let index = 0;
229 // return the next chunk of work for a free worker
230 function next() {
231 if (!options.silent && !options.runInBand && index < numFiles) {
232 process.stdout.write(
233 'Sending ' +
234 Math.min(chunkSize, numFiles-index) +
235 ' files to free worker...\n'
236 );
237 }
238 return files.slice(index, index += chunkSize);
239 }
240
241 if (!options.silent) {
242 process.stdout.write('Processing ' + files.length + ' files... \n');
243 if (!options.runInBand) {
244 process.stdout.write(
245 'Spawning ' + processes +' workers...\n'
246 );
247 }
248 if (options.dry) {
249 process.stdout.write(
250 chalk.green('Running in dry mode, no files will be written! \n')
251 );
252 }
253 }
254
255 const args = [transformFile, options.babel ? 'babel' : 'no-babel'];
256
257 const workers = [];
258 for (let i = 0; i < processes; i++) {
259 workers.push(options.runInBand ?
260 require('./Worker')(args) :
261 child_process.fork(require.resolve('./Worker'), args)
262 );
263 }
264
265 return workers.map(child => {
266 child.send({files: next(), options});
267 child.on('message', message => {
268 switch (message.action) {
269 case 'status':
270 fileCounters[message.status] += 1;
271 log[message.status](lineBreak(message.msg), options.verbose);
272 break;
273 case 'update':
274 if (!statsCounter[message.name]) {
275 statsCounter[message.name] = 0;
276 }
277 statsCounter[message.name] += message.quantity;
278 break;
279 case 'free':
280 child.send({files: next(), options});
281 break;
282 case 'report':
283 report(message);
284 break;
285 }
286 });
287 return new Promise(resolve => child.on('disconnect', resolve));
288 });
289 })
290 .then(pendingWorkers =>
291 Promise.all(pendingWorkers).then(() => {
292 const endTime = process.hrtime(startTime);
293 const timeElapsed = (endTime[0] + endTime[1]/1e9).toFixed(3);
294 if (!options.silent) {
295 process.stdout.write('All done. \n');
296 showFileStats(fileCounters);
297 showStats(statsCounter);
298 process.stdout.write(
299 'Time elapsed: ' + timeElapsed + 'seconds \n'
300 );
301
302 if (options.failOnError && fileCounters.error > 0) {
303 process.exit(1);
304 }
305 }
306 if (usedRemoteScript) {
307 temp.cleanupSync();
308 }
309 return Object.assign({
310 stats: statsCounter,
311 timeElapsed: timeElapsed
312 }, fileCounters);
313 })
314 );
315 }
316}
317
318exports.run = run;