Runner.js 9.0 KB

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