| 'use strict'; |
| |
| const rulesDirs = ['tools/eslint-rules']; |
| const extensions = ['.js', '.mjs', '.md']; |
| // This is the maximum number of files to be linted per worker at any given time |
| const maxWorkload = 60; |
| |
| const cluster = require('cluster'); |
| const path = require('path'); |
| const fs = require('fs'); |
| const totalCPUs = require('os').cpus().length; |
| |
| const CLIEngine = require('eslint').CLIEngine; |
| const glob = require('eslint/node_modules/glob'); |
| |
| const cliOptions = { |
| rulePaths: rulesDirs, |
| extensions: extensions, |
| }; |
| |
| // Check if we should fix errors that are fixable |
| if (process.argv.indexOf('-F') !== -1) |
| cliOptions.fix = true; |
| |
| const cli = new CLIEngine(cliOptions); |
| |
| if (cluster.isMaster) { |
| let numCPUs = 1; |
| const paths = []; |
| let files = null; |
| let totalPaths = 0; |
| let failures = 0; |
| let successes = 0; |
| let lastLineLen = 0; |
| let curPath = 'Starting ...'; |
| let showProgress = true; |
| const globOptions = { |
| nodir: true, |
| ignore: '**/node_modules/**/*' |
| }; |
| const workerConfig = {}; |
| let startTime; |
| let formatter; |
| let outFn; |
| let fd; |
| let i; |
| |
| // Check if spreading work among all cores/cpus |
| if (process.argv.indexOf('-J') !== -1) |
| numCPUs = totalCPUs; |
| |
| // Check if spreading work among an explicit number of cores/cpus |
| i = process.argv.indexOf('-j'); |
| if (i !== -1) { |
| if (!process.argv[i + 1]) |
| throw new Error('Missing parallel job count'); |
| numCPUs = parseInt(process.argv[i + 1], 10); |
| if (!isFinite(numCPUs) || numCPUs <= 0) |
| throw new Error('Bad parallel job count'); |
| } |
| |
| // Check for custom ESLint report formatter |
| i = process.argv.indexOf('-f'); |
| if (i !== -1) { |
| if (!process.argv[i + 1]) |
| throw new Error('Missing format name'); |
| const format = process.argv[i + 1]; |
| formatter = cli.getFormatter(format); |
| if (!formatter) |
| throw new Error('Invalid format name'); |
| // Automatically disable progress display |
| showProgress = false; |
| // Tell worker to send all results, not just linter errors |
| workerConfig.sendAll = true; |
| } else { |
| // Use default formatter |
| formatter = cli.getFormatter(); |
| } |
| |
| // Check if outputting ESLint report to a file instead of stdout |
| i = process.argv.indexOf('-o'); |
| if (i !== -1) { |
| if (!process.argv[i + 1]) |
| throw new Error('Missing output filename'); |
| const outPath = path.resolve(process.argv[i + 1]); |
| fd = fs.openSync(outPath, 'w'); |
| outFn = function(str) { |
| fs.writeSync(fd, str, 'utf8'); |
| }; |
| process.on('exit', () => { fs.closeSync(fd); }); |
| } else { |
| outFn = function(str) { |
| process.stdout.write(str); |
| }; |
| } |
| |
| // Process the rest of the arguments as paths to lint, ignoring any unknown |
| // flags |
| for (i = 2; i < process.argv.length; ++i) { |
| if (process.argv[i][0] === '-') { |
| switch (process.argv[i]) { |
| case '-f': // Skip format name |
| case '-o': // Skip filename |
| case '-j': // Skip parallel job count number |
| ++i; |
| break; |
| } |
| continue; |
| } |
| paths.push(process.argv[i]); |
| } |
| |
| if (paths.length === 0) |
| return; |
| totalPaths = paths.length; |
| |
| if (showProgress) { |
| // Start the progress display update timer when the first worker is ready |
| cluster.once('online', () => { |
| startTime = process.hrtime(); |
| setInterval(printProgress, 1000).unref(); |
| printProgress(); |
| }); |
| } |
| |
| cluster.on('online', (worker) => { |
| // Configure worker and give it some initial work to do |
| worker.send(workerConfig); |
| sendWork(worker); |
| }); |
| |
| process.on('exit', (code) => { |
| if (showProgress) { |
| curPath = 'Done'; |
| printProgress(); |
| outFn('\r\n'); |
| } |
| if (code === 0) |
| process.exit(failures ? 1 : 0); |
| }); |
| |
| for (i = 0; i < numCPUs; ++i) |
| cluster.fork().on('message', onWorkerMessage).on('exit', onWorkerExit); |
| |
| function onWorkerMessage(results) { |
| if (typeof results !== 'number') { |
| // The worker sent us results that are not all successes |
| if (workerConfig.sendAll) { |
| failures += results.errorCount; |
| results = results.results; |
| } else { |
| failures += results.length; |
| } |
| outFn(`${formatter(results)}\r\n`); |
| printProgress(); |
| } else { |
| successes += results; |
| } |
| // Try to give the worker more work to do |
| sendWork(this); |
| } |
| |
| function onWorkerExit(code, signal) { |
| if (code !== 0 || signal) |
| process.exit(2); |
| } |
| |
| function sendWork(worker) { |
| if (!files || !files.length) { |
| // We either just started or we have no more files to lint for the current |
| // path. Find the next path that has some files to be linted. |
| while (paths.length) { |
| let dir = paths.shift(); |
| curPath = dir; |
| const patterns = cli.resolveFileGlobPatterns([dir]); |
| dir = path.resolve(patterns[0]); |
| files = glob.sync(dir, globOptions); |
| if (files.length) |
| break; |
| } |
| if ((!files || !files.length) && !paths.length) { |
| // We exhausted all input paths and thus have nothing left to do, so end |
| // the worker |
| return worker.disconnect(); |
| } |
| } |
| // Give the worker an equal portion of the work left for the current path, |
| // but not exceeding a maximum file count in order to help keep *all* |
| // workers busy most of the time instead of only a minority doing most of |
| // the work. |
| const sliceLen = Math.min(maxWorkload, Math.ceil(files.length / numCPUs)); |
| let slice; |
| if (sliceLen === files.length) { |
| // Micro-optimization to avoid splicing to an empty array |
| slice = files; |
| files = null; |
| } else { |
| slice = files.splice(0, sliceLen); |
| } |
| worker.send(slice); |
| } |
| |
| function printProgress() { |
| if (!showProgress) |
| return; |
| |
| // Clear line |
| outFn(`\r ${' '.repeat(lastLineLen)}\r`); |
| |
| // Calculate and format the data for displaying |
| const elapsed = process.hrtime(startTime)[0]; |
| const mins = `${Math.floor(elapsed / 60)}`.padStart(2, '0'); |
| const secs = `${elapsed % 60}`.padStart(2, '0'); |
| const passed = `${successes}`.padStart(6); |
| const failed = `${failures}`.padStart(6); |
| let pct = `${Math.ceil(((totalPaths - paths.length) / totalPaths) * 100)}`; |
| pct = pct.padStart(3); |
| |
| let line = `[${mins}:${secs}|%${pct}|+${passed}|-${failed}]: ${curPath}`; |
| |
| // Truncate line like cpplint does in case it gets too long |
| if (line.length > 75) |
| line = `${line.slice(0, 75)}...`; |
| |
| // Store the line length so we know how much to erase the next time around |
| lastLineLen = line.length; |
| |
| outFn(line); |
| } |
| } else { |
| // Worker |
| |
| let config = {}; |
| process.on('message', (files) => { |
| if (files instanceof Array) { |
| // Lint some files |
| const report = cli.executeOnFiles(files); |
| |
| // If we were asked to fix the fixable issues, do so. |
| if (cliOptions.fix) |
| CLIEngine.outputFixes(report); |
| |
| if (config.sendAll) { |
| // Return both success and error results |
| |
| const results = report.results; |
| // Silence warnings for files with no errors while keeping the "ok" |
| // status |
| if (report.warningCount > 0) { |
| for (let i = 0; i < results.length; ++i) { |
| const result = results[i]; |
| if (result.errorCount === 0 && result.warningCount > 0) { |
| result.warningCount = 0; |
| result.messages = []; |
| } |
| } |
| } |
| process.send({ results: results, errorCount: report.errorCount }); |
| } else if (report.errorCount === 0) { |
| // No errors, return number of successful lint operations |
| process.send(files.length); |
| } else { |
| // One or more errors, return the error results only |
| process.send(CLIEngine.getErrorResults(report.results)); |
| } |
| } else if (typeof files === 'object') { |
| // The master process is actually sending us our configuration and not a |
| // list of files to lint |
| config = files; |
| } |
| }); |
| } |