1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 | 'use strict';
|
10 |
|
11 | const child_process = require('child_process');
|
12 | const chalk = require('chalk');
|
13 | const fs = require('graceful-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 |
|
20 | const availableCpus = Math.max(require('os').cpus().length - 1, 1);
|
21 | const CHUNK_SIZE = 50;
|
22 |
|
23 | function lineBreak(str) {
|
24 | return /\n$/.test(str) ? str : str + '\n';
|
25 | }
|
26 |
|
27 | const 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 |
|
48 | const 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 |
|
63 | function report({file, msg}) {
|
64 | bufferedWrite(lineBreak(`${chalk.white.bgBlue(' REP ')}${file} ${msg}`));
|
65 | }
|
66 |
|
67 | function 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 |
|
77 | function 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 |
|
87 | function 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 |
|
95 | function dirFiles (dir, callback, acc) {
|
96 |
|
97 | acc = acc || { files: [], remaining: 1 };
|
98 |
|
99 | function done() {
|
100 |
|
101 | if (!--acc.remaining) {
|
102 | callback(acc.files);
|
103 | }
|
104 | }
|
105 |
|
106 | fs.readdir(dir, (err, files) => {
|
107 |
|
108 |
|
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 |
|
117 | process.stdout.write(
|
118 | 'Skipping path "' + name + '" which does not exist.\n'
|
119 | );
|
120 | done();
|
121 | } else if (ignores.shouldIgnore(name)) {
|
122 |
|
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 |
|
136 | function 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 |
|
153 | resolve([]);
|
154 | } else {
|
155 | resolve([file]);
|
156 | }
|
157 | })
|
158 | }))
|
159 | ).then(concatAll);
|
160 | }
|
161 |
|
162 | function 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 |
|
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 |
|
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 |
|
318 | exports.run = run;
|