Runner.js 8.4 KB

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