bulk-decaffeinate
Version:
Run decaffeinate and related operations on a whole codebase, or just part of one.
1 lines • 89.6 kB
Source Map (JSON)
{"version":3,"file":null,"sources":["../src/util/CLIError.js","../src/config/getFilesFromPathFile.js","../src/util/getFilesUnderPath.js","../src/util/getTrackedFiles.js","../src/util/FilePaths.js","../src/config/getFilesToProcess.js","../src/runner/makeCLIFn.js","../src/runner/makeDecaffeinateVerifyFn.js","../src/runner/runInParallel.js","../src/util/pluralize.js","../src/util/momentPreciseDiff.js","../src/runner/runWithProgressBar.js","../src/check.js","../src/clean.js","../src/util/execLive.js","../src/config/resolveConfig.js","../src/util/prependToFile.js","../src/modernize/prependCodePrefix.js","../src/modernize/prependMochaEnv.js","../src/modernize/runEslintFix.js","../src/modernize/runFixImports.js","../src/modernize/runJscodeshiftScripts.js","../src/util/makeCommit.js","../src/convert.js","../src/land.js","../src/modernize/removeAutogeneratedHeader.js","../src/modernizeJS.js","../src/viewErrors.js","../src/cli.js"],"sourcesContent":["const PREFIX = 'bulk-decaffeinate CLIError: ';\n\n/**\n * Exception class for a nice-looking error.\n *\n * Apparently async/await propagation doesn't preserve the exception, so to work\n * around this, we put a special prefix at the start of CLI errors and format\n * the error without a stack trace if the message starts with that prefix.\n */\nexport default class CLIError extends Error {\n constructor(message) {\n super(PREFIX + message);\n }\n\n static formatError(e) {\n if (!e) {\n return e;\n }\n if (e.message.startsWith(PREFIX)) {\n return e.message.substring(PREFIX.length);\n } else {\n return e;\n }\n }\n}\n","import { exists, readFile } from 'mz/fs';\n\nimport CLIError from '../util/CLIError';\n\n/**\n * Read a list of files from a file and return it. Verify that all files\n * actually exist.\n */\nexport default async function getFilesFromPathFile(filePath) {\n let fileContents = await readFile(filePath);\n let lines = fileContents.toString().split('\\n');\n let resultLines = [];\n for (let line of lines) {\n line = line.trim();\n if (line.length === 0 || line.startsWith('#')) {\n continue;\n }\n if (!(await exists(line))) {\n throw new CLIError(`The file \"${line}\" did not exist.`);\n }\n resultLines.push(line);\n }\n return resultLines;\n}\n","import { readdir, stat } from 'mz/fs';\nimport { join, resolve } from 'path';\n\n/**\n * Recursively discover any matching files in the current directory, ignoring\n * things like node_modules and .git.\n */\nexport default async function getFilesUnderPath(dirPath, asyncPathPredicate) {\n let resultFiles = [];\n let children = await readdir(dirPath);\n for (let child of children) {\n if (['node_modules', '.git'].includes(child)) {\n continue;\n }\n let childPath = resolve(join(dirPath, child));\n if ((await stat(childPath)).isDirectory()) {\n let subdirCoffeeFiles = await getFilesUnderPath(childPath, asyncPathPredicate);\n resultFiles.push(...subdirCoffeeFiles);\n } else if (await asyncPathPredicate(childPath)) {\n resultFiles.push(childPath);\n }\n }\n return resultFiles;\n}\n","import git from 'simple-git/promise';\nimport { resolve } from 'path';\n\nexport default async function getTrackedFiles() {\n let stdout = await git().raw(['ls-files']);\n return new Set(stdout.split('\\n').map(s => s.trim()).map(s => resolve(s)));\n}\n","import executable from 'executable';\nimport { readFile } from 'mz/fs';\nimport { basename, dirname, extname, join } from 'path';\n\nexport const COFFEE_FILE_RECOGNIZER = {\n extensions: ['.coffee', '.litcoffee', '.coffee.md'],\n shebangSuffix: 'coffee',\n};\n\nexport const JS_FILE_RECOGNIZER = {\n extensions: ['.js'],\n shebangSuffix: 'node',\n};\n\nfunction extensionFor(path) {\n if (path.endsWith('.coffee.md')) {\n return '.coffee.md';\n }\n return extname(path);\n}\n\nfunction basePathFor(path) {\n let extension = extensionFor(path);\n return join(dirname(path), basename(path, extension));\n}\n\nexport async function shouldConvertFile(path, recognizer, trackedFiles) {\n if (!hasRecognizedExtension(path, recognizer) &&\n !await isExecutableScript(path, recognizer)) {\n return false;\n }\n if (!trackedFiles.has(path)) {\n console.log(\n `Warning: Skipping ${path} because the file is not tracked in the git repo.`);\n return false;\n }\n return true;\n}\n\nfunction hasRecognizedExtension(path, recognizer) {\n return recognizer.extensions.some(ext =>\n path.endsWith(ext) && !path.endsWith(`.original${ext}`));\n}\n\nasync function isExecutableScript(path, recognizer) {\n if (isExtensionless(path) && await executable(path)) {\n let contents = await readFile(path);\n let firstLine = contents.toString().split('\\n')[0];\n if (firstLine.startsWith('#!') && firstLine.includes(recognizer.shebangSuffix)) {\n return true;\n }\n }\n return false;\n}\n\nexport function isExtensionless(path) {\n return extensionFor(path) === '';\n}\n\nexport function backupPathFor(path) {\n let extension = extensionFor(path);\n let basePath = basePathFor(path);\n return basePath + '.original' + extension;\n}\n\n/**\n * The resulting path where we should send the given input file. Note that when\n * the input file is an extensionless script, we prefer to keep it extensionless\n * (and decaffeinate handles the shebang line).\n */\nexport function jsPathFor(path, config) {\n if (config.customNames[path]) {\n return config.customNames[path];\n }\n if (isExtensionless(path)) {\n return path;\n } else {\n return basePathFor(path) + '.' + config.outputFileExtension;\n }\n}\n\n/**\n * The file generated by decaffeinate for the input file with this name.\n */\nexport function decaffeinateOutPathFor(path) {\n return basePathFor(path) + '.js';\n}\n\nexport function isLiterate(path) {\n return path.endsWith('.litcoffee') || path.endsWith('.coffee.md');\n}\n","import { exists } from 'mz/fs';\nimport { resolve } from 'path';\n\nimport getFilesFromPathFile from './getFilesFromPathFile';\nimport getFilesUnderPath from '../util/getFilesUnderPath';\nimport getTrackedFiles from '../util/getTrackedFiles';\nimport { shouldConvertFile, jsPathFor } from '../util/FilePaths';\nimport CLIError from '../util/CLIError';\n\n/**\n * Get the files that we should process based on the config. \"recognizer\" should\n * be an object describing the files to auto-recognize, e.g.\n * COFFEE_FILE_RECOGNIZER.\n */\nexport default async function getFilesToProcess(config, recognizer) {\n let filesToProcess = await resolveFilesToProcess(config, recognizer);\n filesToProcess = resolveFileFilter(filesToProcess, config);\n await validateFilesToProcess(filesToProcess, config);\n return filesToProcess;\n}\n\nasync function resolveFilesToProcess(config, recognizer) {\n let {filesToProcess, pathFile, searchDirectory} = config;\n if (!filesToProcess && !pathFile && !searchDirectory) {\n let trackedFiles = await getTrackedFiles();\n return await getFilesUnderPath('.', async (path) =>\n await shouldConvertFile(path, recognizer, trackedFiles));\n }\n let files = [];\n if (filesToProcess) {\n files.push(...filesToProcess);\n }\n if (pathFile) {\n files.push(...await getFilesFromPathFile(pathFile));\n }\n if (searchDirectory) {\n let trackedFiles = await getTrackedFiles();\n files.push(...await getFilesUnderPath(searchDirectory, async (path) =>\n await shouldConvertFile(path, recognizer, trackedFiles)));\n }\n files = files.map(path => resolve(path));\n files = Array.from(new Set(files)).sort();\n return files;\n}\n\nfunction resolveFileFilter(filesToProcess, config) {\n if (!config.fileFilterFn) {\n return filesToProcess;\n }\n return filesToProcess.filter(path => config.fileFilterFn(path));\n}\n\nasync function validateFilesToProcess(filesToProcess, config) {\n let trackedFiles = await getTrackedFiles();\n for (let path of filesToProcess) {\n if (!trackedFiles.has(path)) {\n throw new CLIError(`The file ${path} is not tracked in the git repo.`);\n }\n let jsPath = jsPathFor(path, config);\n if (jsPath !== path && await exists(jsPath)) {\n throw new CLIError(`The file ${jsPath} already exists.`);\n }\n }\n}\n","import { exec } from 'mz/child_process';\n\nexport default function makeCLIFn(commandByPath) {\n return async function(path) {\n try {\n await exec(commandByPath(path), {maxBuffer: 5 * 1024 * 1024});\n return {path, error: null};\n } catch (e) {\n return {path, error: e.message};\n }\n };\n}\n","import { isLiterate } from '../util/FilePaths';\nimport makeCLIFn from './makeCLIFn';\n\nexport default function makeDecaffeinateVerifyFn(config) {\n let { decaffeinatePath, decaffeinateArgs } = config;\n return makeCLIFn(path => {\n let literateFlag = isLiterate(path) ? '--literate' : '';\n return `${decaffeinatePath} ${literateFlag} ${decaffeinateArgs.join(' ')} < ${path}`;\n });\n}\n","/**\n * Run the given one-argument async function on an array of arguments, keeping a\n * logical worker pool to increase throughput without overloading the system.\n *\n * Results are provided as they come in with the result handler. Results look\n * like {index: 3, result: \"Hello\"}. This can be used e.g. to update a progress\n * bar.\n *\n * An array of all results is returned at the end.\n */\nexport default async function runInParallel(\n args, asyncFn, numConcurrentProcesses, resultHandler) {\n let results = [];\n let activePromises = {};\n\n let handleResult = ({index, result}) => {\n results[index] = result;\n delete activePromises[index];\n resultHandler({index, result});\n };\n\n for (let i = 0; i < args.length; i++) {\n let arg = args[i];\n activePromises[i] = async function() {\n return {\n index: i,\n result: await asyncFn(arg),\n };\n }();\n if (Object.keys(activePromises).length >= numConcurrentProcesses) {\n handleResult(await Promise.race(Object.values(activePromises)));\n }\n }\n while (Object.keys(activePromises).length > 0) {\n handleResult(await Promise.race(Object.values(activePromises)));\n }\n return results;\n}\n","export default function pluralize(num, noun) {\n return num === 1 ? `${num} ${noun}` : `${num} ${noun}s`;\n}\n","/**\n * Copied from moment-precise-range, which is MIT-licensed, with minor cleanups\n * to appease ESLint and remove an unused option.\n *\n * https://github.com/codebox/moment-precise-range\n *\n * The original plugin worked by modifying the global moment installation, which\n * caused problems if multiple moment instances were installed, so this should\n * avoid that.\n */\n\nimport moment from 'moment';\n\nconst STRINGS = {\n nodiff: '',\n year: 'year',\n years: 'years',\n month: 'month',\n months: 'months',\n day: 'day',\n days: 'days',\n hour: 'hour',\n hours: 'hours',\n minute: 'minute',\n minutes: 'minutes',\n second: 'second',\n seconds: 'seconds',\n delimiter: ' ',\n};\n\nfunction pluralize(num, word) {\n return num + ' ' + STRINGS[word + (num === 1 ? '' : 's')];\n}\n\nfunction buildStringFromValues(yDiff, mDiff, dDiff, hourDiff, minDiff, secDiff){\n let result = [];\n\n if (yDiff) {\n result.push(pluralize(yDiff, 'year'));\n }\n if (mDiff) {\n result.push(pluralize(mDiff, 'month'));\n }\n if (dDiff) {\n result.push(pluralize(dDiff, 'day'));\n }\n if (hourDiff) {\n result.push(pluralize(hourDiff, 'hour'));\n }\n if (minDiff) {\n result.push(pluralize(minDiff, 'minute'));\n }\n if (secDiff) {\n result.push(pluralize(secDiff, 'second'));\n }\n\n return result.join(STRINGS.delimiter);\n}\n\nexport default function momentPreciseDiff(m1, m2) {\n m1.add(m2.utcOffset() - m1.utcOffset(), 'minutes'); // shift timezone of m1 to m2\n\n if (m1.isSame(m2)) {\n return STRINGS.nodiff;\n }\n\n if (m1.isAfter(m2)) {\n let tmp = m1;\n m1 = m2;\n m2 = tmp;\n }\n\n let yDiff = m2.year() - m1.year();\n let mDiff = m2.month() - m1.month();\n let dDiff = m2.date() - m1.date();\n let hourDiff = m2.hour() - m1.hour();\n let minDiff = m2.minute() - m1.minute();\n let secDiff = m2.second() - m1.second();\n\n if (secDiff < 0) {\n secDiff = 60 + secDiff;\n minDiff--;\n }\n if (minDiff < 0) {\n minDiff = 60 + minDiff;\n hourDiff--;\n }\n if (hourDiff < 0) {\n hourDiff = 24 + hourDiff;\n dDiff--;\n }\n if (dDiff < 0) {\n let daysInLastFullMonth = moment(m2.year() + '-' + (m2.month() + 1), 'YYYY-MM').subtract(1, 'M').daysInMonth();\n if (daysInLastFullMonth < m1.date()) { // 31/01 -> 2/03\n dDiff = daysInLastFullMonth + dDiff + (m1.date() - daysInLastFullMonth);\n } else {\n dDiff = daysInLastFullMonth + dDiff;\n }\n mDiff--;\n }\n if (mDiff < 0) {\n mDiff = 12 + mDiff;\n yDiff--;\n }\n\n return buildStringFromValues(yDiff, mDiff, dDiff, hourDiff, minDiff, secDiff);\n}\n","import moment from 'moment';\n\nimport runInParallel from './runInParallel';\nimport CLIError from '../util/CLIError';\nimport pluralize from '../util/pluralize';\nimport momentPreciseDiff from '../util/momentPreciseDiff';\n\n/**\n * Run the given command in parallel, showing a progress bar of results.\n *\n * The provided async function should return an object that at least contains\n * a field called \"error\" that is truthy if there was a problem, but may contain\n * any other fields.\n */\nexport default async function runWithProgressBar(\n config, description, files, asyncFn, {runInSeries, allowFailures}={}) {\n let numProcessed = 0;\n let numFailures = 0;\n let numTotal = files.length;\n let startTime = moment();\n let numConcurrentProcesses = runInSeries ? 1 : config.numWorkers;\n console.log(`${description} (${pluralize(numConcurrentProcesses, 'worker')})`);\n let results;\n try {\n results = await runInParallel(files, asyncFn, numConcurrentProcesses, ({result}) => {\n if (result && result.error) {\n if (!allowFailures) {\n throw new CLIError(`Error:\\n${result.error}`);\n }\n numFailures++;\n }\n numProcessed++;\n let errorString = numFailures === 0 ? '' : ` (${pluralize(numFailures, 'failure')} so far)`;\n process.stdout.write(`\\r${numProcessed}/${numTotal}${errorString}`);\n });\n } finally {\n process.stdout.write('\\n');\n let endTime = moment();\n let diffStr = momentPreciseDiff(startTime, endTime) || '0 seconds';\n console.log(`Finished in ${diffStr} (Time: ${moment().format()})`);\n }\n return results;\n}\n","import { writeFile } from 'mz/fs';\n\nimport getFilesToProcess from './config/getFilesToProcess';\nimport makeDecaffeinateVerifyFn from './runner/makeDecaffeinateVerifyFn';\nimport runWithProgressBar from './runner/runWithProgressBar';\nimport { COFFEE_FILE_RECOGNIZER } from './util/FilePaths';\nimport pluralize from './util/pluralize';\n\nexport default async function check(config) {\n let filesToProcess = await getFilesToProcess(config, COFFEE_FILE_RECOGNIZER);\n let decaffeinateResults = await runWithProgressBar(\n config,\n `Doing a dry run of decaffeinate on ${pluralize(filesToProcess.length, 'file')}...`,\n filesToProcess, makeDecaffeinateVerifyFn(config),\n {allowFailures: true});\n await printResults(decaffeinateResults);\n}\n\nasync function printResults(results) {\n let errorResults = results.filter(r => r.error !== null);\n if (errorResults.length === 0) {\n console.log(`All checks succeeded! decaffeinate can convert all ${pluralize(results.length, 'file')}.`);\n console.log('Run \"bulk-decaffeinate convert\" to convert the files to JavaScript.');\n } else {\n console.log(`${pluralize(errorResults.length, 'file')} failed to convert:`);\n for (let result of errorResults) {\n console.log(result.path);\n }\n let successPaths = results.filter(r => r.error === null).map(r => r.path);\n console.log();\n await writeFile('decaffeinate-errors.log', getVerboseErrors(results));\n await writeFile('decaffeinate-results.json', JSON.stringify(results, null, 2));\n await writeFile('decaffeinate-successful-files.txt', successPaths.join('\\n'));\n console.log('Wrote decaffeinate-errors.log and decaffeinate-results.json with more detailed info.');\n console.log('To open failures in the online repl, run \"bulk-decaffeinate view-errors\".');\n console.log('To convert the successful files, run \"bulk-decaffeinate convert -p decaffeinate-successful-files.txt\".');\n }\n}\n\nfunction getVerboseErrors(results) {\n let errorMessages = [];\n for (let {path, error} of results) {\n if (error) {\n errorMessages.push(`===== ${path}`);\n errorMessages.push(getStdout(error));\n }\n }\n return errorMessages.join('\\n');\n}\n\nfunction getStdout(message) {\n let matchString = '\\nstdin: ';\n if (message.indexOf(matchString) !== -1) {\n return message.substring(message.indexOf(matchString) + matchString.length);\n } else {\n return message.substring(message.indexOf('\\n') + 1);\n }\n}\n","import { unlink } from 'mz/fs';\nimport { basename } from 'path';\n\nimport getFilesUnderPath from './util/getFilesUnderPath';\n\nexport default async function clean() {\n let filesToDelete = await getFilesUnderPath('.', p => basename(p).includes('.original'));\n if (filesToDelete.length === 0) {\n console.log('No .original files were found.');\n return;\n }\n for (let path of filesToDelete) {\n console.log(`Deleting ${path}`);\n await unlink(path);\n }\n console.log('Done deleting .original files.');\n}\n","import { spawn } from 'child_process';\n\n/**\n * Variant of exec that connects stdout, stderr, and stdin, mostly so console\n * output is shown continuously. As with the mz version of exec, this returns a\n * promise that resolves when the shell command finishes.\n */\nexport default function execLive(command) {\n return new Promise((resolve, reject) => {\n let childProcess = spawn('/bin/sh', ['-c', command], {stdio: 'inherit'});\n childProcess.on('close', code => {\n if (code === 0) {\n resolve();\n } else {\n reject();\n }\n });\n });\n}\n","import { exec } from 'mz/child_process';\nimport { exists, readdir, stat } from 'mz/fs';\nimport readline from 'mz/readline';\nimport { resolve } from 'path';\nimport requireUncached from 'require-uncached';\n\nimport CLIError from '../util/CLIError';\nimport execLive from '../util/execLive';\n\n/**\n * Resolve the configuration from a number of sources: any number of config\n * files and CLI options. Then \"canonicalize\" the config as much as we can.\n */\nexport default async function resolveConfig(commander, {needsJscodeshift, needsEslint} = {}) {\n let config = {};\n\n if (commander.config && commander.config.length > 0) {\n for (let filename of commander.config) {\n config = applyConfig(filename, config);\n }\n } else {\n let currentDirFiles = await readdir('.');\n currentDirFiles.sort();\n for (let filename of currentDirFiles) {\n config = await applyPossibleConfig(filename, config);\n }\n }\n config = getCLIParamsConfig(config, commander);\n return {\n decaffeinateArgs: resolveDecaffeinateArgs(config),\n filesToProcess: config.filesToProcess,\n pathFile: config.pathFile,\n searchDirectory: config.searchDirectory,\n fileFilterFn: config.fileFilterFn,\n customNames: resolveCustomNames(config.customNames),\n outputFileExtension: config.outputFileExtension || 'js',\n fixImportsConfig: resolveFixImportsConfig(config),\n jscodeshiftScripts: config.jscodeshiftScripts,\n landConfig: config.landConfig,\n mochaEnvFilePattern: config.mochaEnvFilePattern,\n codePrefix: config.codePrefix,\n landBase: config.landBase,\n numWorkers: config.numWorkers || 8,\n skipVerify: config.skipVerify,\n skipEslintFix: config.skipEslintFix,\n decaffeinatePath: await resolveDecaffeinatePath(config),\n jscodeshiftPath: needsJscodeshift ? await resolveJscodeshiftPath(config) : null,\n eslintPath: needsEslint ? await resolveEslintPath(config) : null,\n };\n}\n\nfunction resolveDecaffeinateArgs(config) {\n let args = config.decaffeinateArgs || [];\n if (config.useJSModules && !args.includes('--use-js-modules')) {\n args.push('--use-js-modules');\n }\n return args;\n}\n\nfunction resolveFixImportsConfig(config) {\n let fixImportsConfig = config.fixImportsConfig;\n if (!fixImportsConfig && config.useJSModules) {\n fixImportsConfig = {\n searchPath: '.',\n };\n }\n return fixImportsConfig;\n}\n\nasync function applyPossibleConfig(filename, config) {\n if (!filename.startsWith('bulk-decaffeinate') ||\n (await stat(filename)).isDirectory()) {\n return config;\n }\n\n if (filename.endsWith('.config.js')) {\n return applyConfig(filename, config);\n } else {\n return config;\n }\n}\n\nfunction applyConfig(filename, config) {\n let filePath = resolve(filename);\n try {\n let newConfig = requireUncached(filePath);\n return Object.assign(config, newConfig);\n } catch (e) {\n throw new CLIError(\n `Error reading file ${filePath}. Make sure it is a valid JS file.`);\n }\n}\n\n/**\n * Fill in a configuration from the CLI arguments.\n */\nfunction getCLIParamsConfig(config, commander) {\n let {\n file,\n pathFile,\n dir,\n useJsModules,\n landBase,\n numWorkers,\n skipVerify,\n decaffeinatePath,\n jscodeshiftPath,\n eslintPath,\n } = commander;\n // As a special case, specifying files to process from the CLI should cause\n // any equivalent config file settings to be ignored.\n if ((file && file.length > 0) || dir || pathFile) {\n config.filesToProcess = null;\n config.searchDirectory = null;\n config.pathFile = null;\n }\n\n if (file && file.length > 0) {\n config.filesToProcess = file;\n }\n if (dir) {\n config.searchDirectory = dir;\n }\n if (pathFile) {\n config.pathFile = pathFile;\n }\n if (useJsModules) {\n config.useJSModules = true;\n }\n if (landBase) {\n config.landBase = landBase;\n }\n if (numWorkers) {\n config.numWorkers = numWorkers;\n }\n if (skipVerify) {\n config.skipVerify = true;\n }\n if (decaffeinatePath) {\n config.decaffeinatePath = decaffeinatePath;\n }\n if (jscodeshiftPath) {\n config.jscodeshiftPath = jscodeshiftPath;\n }\n if (eslintPath) {\n config.eslintPath = eslintPath;\n }\n return config;\n}\n\nasync function resolveDecaffeinatePath(config) {\n if (config.decaffeinatePath) {\n return config.decaffeinatePath;\n }\n return await resolveBinary('decaffeinate');\n}\n\nasync function resolveJscodeshiftPath(config) {\n // jscodeshift is an optional step, so don't prompt to install it if we won't\n // be using it.\n if (!config.jscodeshiftScripts && !config.fixImportsConfig && !config.useJSModules) {\n return null;\n }\n if (config.jscodeshiftPath) {\n return config.jscodeshiftPath;\n }\n return await resolveBinary('jscodeshift');\n}\n\nasync function resolveEslintPath(config) {\n if (config.skipEslintFix) {\n return null;\n }\n if (config.eslintPath) {\n return config.eslintPath;\n }\n return await resolveBinary('eslint');\n}\n\n/**\n * Determine the shell command that can be used to run the given binary,\n * prompting to globally install it if necessary.\n */\nasync function resolveBinary(binaryName) {\n let nodeModulesPath = `./node_modules/.bin/${binaryName}`;\n if (await exists(nodeModulesPath)) {\n return nodeModulesPath;\n } else {\n try {\n await exec(`which ${binaryName}`);\n return binaryName;\n } catch (e) {\n console.log(`${binaryName} binary not found on the PATH or in node_modules.`);\n let rl = readline.createInterface(process.stdin, process.stdout);\n let answer = await rl.question(`Run \"npm install -g ${binaryName}\"? [Y/n] `);\n rl.close();\n if (answer.toLowerCase().startsWith('n')) {\n throw new CLIError(`${binaryName} must be installed.`);\n }\n console.log(`Installing ${binaryName} globally...`);\n await execLive(`npm install -g ${binaryName}`);\n console.log(`Successfully installed ${binaryName}\\n`);\n return binaryName;\n }\n }\n}\n\nfunction resolveCustomNames(customNames) {\n let result = {};\n if (customNames) {\n for (const [key, value] of Object.entries(customNames)) {\n result[resolve(key)] = resolve(value);\n }\n }\n return result;\n}\n","import { readFile, writeFile } from 'fs-promise';\n\nexport default async function prependToFile(path, prependText) {\n let contents = await readFile(path);\n let lines = contents.toString().split('\\n');\n if (lines[0] && lines[0].startsWith('#!')) {\n contents = lines[0] + '\\n' + prependText + lines.slice(1).join('\\n');\n } else {\n contents = prependText + contents;\n }\n await writeFile(path, contents);\n}\n","import prependToFile from '../util/prependToFile';\nimport runWithProgressBar from '../runner/runWithProgressBar';\n\nexport default async function prependCodePrefix(config, jsFiles, codePrefix) {\n await runWithProgressBar(\n config,\n 'Adding code prefix to converted files...', jsFiles, async function(path) {\n await prependToFile(path, codePrefix);\n return {error: null};\n });\n}\n","import runWithProgressBar from '../runner/runWithProgressBar';\nimport prependToFile from '../util/prependToFile';\n\nexport default async function prependMochaEnv(config, jsFiles, mochaEnvFilePattern) {\n let regex = new RegExp(mochaEnvFilePattern);\n let testFiles = jsFiles.filter(f => regex.test(f));\n await runWithProgressBar(\n config,\n 'Adding /* eslint-env mocha */ to test files...', testFiles, async function(path) {\n await prependToFile(path, '/* eslint-env mocha */\\n');\n return {error: null};\n });\n}\n","import { exec } from 'mz/child_process';\n\nimport runWithProgressBar from '../runner/runWithProgressBar';\nimport CLIError from '../util/CLIError';\nimport prependToFile from '../util/prependToFile';\n\nexport default async function runEslintFix(jsFiles, config, {isUpdate}) {\n let eslintResults = await runWithProgressBar(\n config,\n 'Running eslint --fix on all files...', jsFiles, makeEslintFixFn(config, {isUpdate}));\n for (let result of eslintResults) {\n for (let message of result.messages) {\n console.log(message);\n }\n }\n}\n\nexport const HEADER_COMMENT_LINES = {\n todo: '// TODO: This file was created by bulk-decaffeinate.',\n todoUpdated: '// TODO: This file was updated by bulk-decaffeinate.',\n fixIssues: '// Fix any style issues and re-enable lint.',\n sanityCheck: '// Sanity-check the conversion and remove this comment.',\n};\n\nfunction makeEslintFixFn(config, {isUpdate}) {\n return async function runEslint(path) {\n let messages = [];\n\n // Ignore the eslint exit code; it gives useful stdout in the same format\n // regardless of the exit code. Also keep a 10MB buffer since sometimes\n // there can be a LOT of lint failures.\n let [eslintStdout, eslintStderr] = (await exec(\n `${config.eslintPath} --fix --format json ${path}; :`,\n {maxBuffer: 10000*1024}));\n\n let ruleIds;\n if ((eslintStdout + eslintStderr).includes(\"ESLint couldn't find a configuration file\")) {\n messages.push(`Skipping \"eslint --fix\" on ${path} because there was no eslint config file.`);\n ruleIds = [];\n } else {\n let eslintOutput;\n try {\n eslintOutput = JSON.parse(eslintStdout);\n } catch (e) {\n throw new CLIError(`Error while running eslint:\\n${eslintStdout}\\n${eslintStderr}`);\n }\n ruleIds = eslintOutput[0].messages\n .map(message => message.ruleId).filter(ruleId => ruleId);\n ruleIds = Array.from(new Set(ruleIds)).sort();\n }\n\n if (isUpdate) {\n // When we're just updating a JS file, a TODO is useful if there's real\n // stuff to fix.\n if (ruleIds.length > 0) {\n await prependToFile(\n `${path}`, `${HEADER_COMMENT_LINES.todoUpdated}\\n${HEADER_COMMENT_LINES.fixIssues}\\n`);\n }\n } else {\n // If we generated the whole file from CoffeeScript, always leave a\n // suggestion to clean up the file.\n if (ruleIds.length > 0) {\n await prependToFile(\n `${path}`, `${HEADER_COMMENT_LINES.todo}\\n${HEADER_COMMENT_LINES.fixIssues}\\n`);\n } else {\n await prependToFile(\n `${path}`, `${HEADER_COMMENT_LINES.todo}\\n${HEADER_COMMENT_LINES.sanityCheck}\\n`);\n }\n }\n if (ruleIds.length > 0) {\n await prependToFile(`${path}`, `\\\n/* eslint-disable\n${ruleIds.map(ruleId => ` ${ruleId},`).join('\\n')}\n*/\n`);\n }\n return {error: null, messages};\n };\n}\n","/**\n * Runs the fix-imports step on all specified JS files, and return an array of\n * the files that changed.\n */\nimport { readFile } from 'fs-promise';\nimport { basename, join, relative, resolve } from 'path';\nimport zlib from 'zlib';\n\nimport runWithProgressBar from '../runner/runWithProgressBar';\nimport execLive from '../util/execLive';\nimport getFilesUnderPath from '../util/getFilesUnderPath';\n\nexport default async function runFixImports(jsFiles, config) {\n let {searchPath, absoluteImportPaths} = config.fixImportsConfig;\n if (!absoluteImportPaths) {\n absoluteImportPaths = [];\n }\n let scriptPath = join(__dirname, '../jscodeshift-scripts-dist/fix-imports.js');\n\n let options = {\n convertedFiles: jsFiles.map(p => resolve(p)),\n absoluteImportPaths: absoluteImportPaths.map(p => resolve(p)),\n };\n let eligibleFixImportsFiles = await getEligibleFixImportsFiles(\n config, searchPath, jsFiles);\n console.log('Fixing any imports across the whole codebase...');\n if (eligibleFixImportsFiles.length > 0) {\n // Note that the args can get really long, so we take reasonable steps to\n // reduce the chance of hitting the system limit on arg length\n // (256K by default on Mac).\n let eligibleRelativePaths = eligibleFixImportsFiles.map(p => relative('', p));\n let encodedOptions = zlib.deflateSync(JSON.stringify(options)).toString('base64');\n await execLive(`\\\n ${config.jscodeshiftPath} --parser flow -t ${scriptPath} \\\n ${eligibleRelativePaths.join(' ')} --encoded-options=${encodedOptions}`);\n }\n return eligibleFixImportsFiles;\n}\n\nasync function getEligibleFixImportsFiles(config, searchPath, jsFiles) {\n let jsBasenames = jsFiles.map(p => basename(p, '.js'));\n let resolvedPaths = jsFiles.map(p => resolve(p));\n let allJsFiles = await getFilesUnderPath(searchPath, p => p.endsWith('.js'));\n await runWithProgressBar(\n config,\n 'Searching for files that may need to have updated imports...',\n allJsFiles,\n async function(p) {\n let resolvedPath = resolve(p);\n if (resolvedPaths.includes(resolvedPath)) {\n return {error: null};\n }\n let contents = (await readFile(resolvedPath)).toString();\n for (let jsBasename of jsBasenames) {\n if (contents.includes(jsBasename)) {\n resolvedPaths.push(resolvedPath);\n return {error: null};\n }\n }\n return {error: null};\n });\n return resolvedPaths;\n}\n","import { join, relative } from 'path';\n\nimport execLive from '../util/execLive';\n\nexport default async function runJscodeshiftScripts(jsFiles, config) {\n for (let scriptPath of config.jscodeshiftScripts) {\n let resolvedPath = resolveJscodeshiftScriptPath(scriptPath);\n console.log(`Running jscodeshift script ${resolvedPath}...`);\n await execLive(`${config.jscodeshiftPath} --parser flow \\\n -t ${resolvedPath} ${jsFiles.map(p => relative('', p)).join(' ')}`);\n }\n}\n\nfunction resolveJscodeshiftScriptPath(scriptPath) {\n if ([\n 'prefer-function-declarations.js',\n 'remove-coffee-from-imports.js',\n 'top-level-this-to-exports.js',\n ].includes(scriptPath)) {\n return join(__dirname, `../jscodeshift-scripts-dist/${scriptPath}`);\n }\n return scriptPath;\n}\n","import git from 'simple-git/promise';\n\n/**\n * Make an autogenerated commit with the \"decaffeinate\" author.\n */\nexport default async function makeCommit(commitMessage) {\n const userEmail = await git().raw(['config', 'user.email']);\n const author = `decaffeinate <${userEmail}>`;\n await git().commit(commitMessage, {'--author': author, '--no-verify': null});\n}\n","import { copy, move, unlink } from 'fs-promise';\nimport { basename } from 'path';\nimport git from 'simple-git/promise';\n\nimport getFilesToProcess from './config/getFilesToProcess';\nimport prependCodePrefix from './modernize/prependCodePrefix';\nimport prependMochaEnv from './modernize/prependMochaEnv';\nimport runEslintFix from './modernize/runEslintFix';\nimport runFixImports from './modernize/runFixImports';\nimport runJscodeshiftScripts from './modernize/runJscodeshiftScripts';\nimport makeCLIFn from './runner/makeCLIFn';\nimport makeDecaffeinateVerifyFn from './runner/makeDecaffeinateVerifyFn';\nimport runWithProgressBar from './runner/runWithProgressBar';\nimport CLIError from './util/CLIError';\nimport {\n backupPathFor,\n COFFEE_FILE_RECOGNIZER,\n decaffeinateOutPathFor,\n jsPathFor,\n} from './util/FilePaths';\nimport makeCommit from './util/makeCommit';\nimport pluralize from './util/pluralize';\n\nexport default async function convert(config) {\n await assertGitWorktreeClean();\n\n let coffeeFiles = await getFilesToProcess(config, COFFEE_FILE_RECOGNIZER);\n if (coffeeFiles.length === 0) {\n console.log('There were no CoffeeScript files to convert.');\n return;\n }\n\n let movingCoffeeFiles = coffeeFiles.filter(p => jsPathFor(p, config) !== p);\n let {decaffeinateArgs = [], decaffeinatePath} = config;\n\n if (!config.skipVerify) {\n try {\n await runWithProgressBar(\n config,\n 'Verifying that decaffeinate can successfully convert these files...',\n coffeeFiles, makeDecaffeinateVerifyFn(config));\n } catch (e) {\n throw new CLIError(`\\\nSome files could not be converted with decaffeinate.\nRe-run with the \"check\" command for more details.`);\n }\n }\n\n await runWithProgressBar(\n config,\n 'Backing up files to .original.coffee...',\n coffeeFiles,\n async function(coffeePath) {\n await copy(`${coffeePath}`, `${backupPathFor(coffeePath)}`);\n });\n\n await runWithProgressBar(\n config,\n `Renaming files from .coffee to .${config.outputFileExtension}...`,\n movingCoffeeFiles,\n async function(coffeePath) {\n await move(coffeePath, jsPathFor(coffeePath, config));\n });\n\n let shortDescription = getShortDescription(coffeeFiles);\n let renameCommitMsg =\n `decaffeinate: Rename ${shortDescription} from .coffee to .${config.outputFileExtension}`;\n\n if (movingCoffeeFiles.length > 0) {\n console.log(`Generating the first commit: \"${renameCommitMsg}\"...`);\n await git().rm(movingCoffeeFiles);\n await git().raw(['add', '-f', ...movingCoffeeFiles.map(p => jsPathFor(p, config))]);\n await makeCommit(renameCommitMsg);\n }\n\n await runWithProgressBar(\n config,\n 'Moving files back...',\n movingCoffeeFiles,\n async function(coffeePath) {\n await move(jsPathFor(coffeePath, config), coffeePath);\n });\n\n await runWithProgressBar(\n config,\n 'Running decaffeinate on all files...',\n coffeeFiles,\n makeCLIFn(path => `${decaffeinatePath} ${decaffeinateArgs.join(' ')} ${path}`)\n );\n\n await runWithProgressBar(\n config,\n 'Deleting old files...',\n coffeeFiles,\n async function(coffeePath) {\n await unlink(coffeePath);\n });\n\n await runWithProgressBar(\n config,\n 'Setting proper extension for all files...',\n coffeeFiles,\n async function(coffeePath) {\n let decaffeinateOutPath = decaffeinateOutPathFor(coffeePath);\n let jsPath = jsPathFor(coffeePath, config);\n if (decaffeinateOutPath !== jsPath) {\n await move(decaffeinateOutPath, jsPath);\n }\n });\n\n let decaffeinateCommitMsg =\n `decaffeinate: Convert ${shortDescription} to JS`;\n console.log(`Generating the second commit: ${decaffeinateCommitMsg}...`);\n let jsFiles = coffeeFiles.map(f => jsPathFor(f, config));\n await git().raw(['add', '-f', ...jsFiles]);\n await makeCommit(decaffeinateCommitMsg);\n\n if (config.jscodeshiftScripts) {\n await runJscodeshiftScripts(jsFiles, config);\n }\n if (config.mochaEnvFilePattern) {\n await prependMochaEnv(config, jsFiles, config.mochaEnvFilePattern);\n }\n let thirdCommitModifiedFiles = jsFiles.slice();\n if (config.fixImportsConfig) {\n thirdCommitModifiedFiles = await runFixImports(jsFiles, config);\n }\n if (!config.skipEslintFix) {\n await runEslintFix(jsFiles, config, {isUpdate: false});\n }\n if (config.codePrefix) {\n await prependCodePrefix(config, jsFiles, config.codePrefix);\n }\n\n let postProcessCommitMsg =\n `decaffeinate: Run post-processing cleanups on ${shortDescription}`;\n console.log(`Generating the third commit: ${postProcessCommitMsg}...`);\n await git().raw(['add', '-f', ...thirdCommitModifiedFiles]);\n await makeCommit(postProcessCommitMsg);\n\n console.log(`Successfully ran decaffeinate on ${pluralize(coffeeFiles.length, 'file')}.`);\n console.log('You should now fix lint issues in any affected files.');\n console.log('All CoffeeScript files were backed up as .original.coffee files that you can use for comparison.');\n console.log('You can run \"bulk-decaffeinate clean\" to remove those files.');\n console.log('To allow git to properly track file history, you should NOT squash the generated commits together.');\n}\n\nasync function assertGitWorktreeClean() {\n let status = await git().status();\n if (status.files.length > status.not_added.length) {\n throw new CLIError(`\\\nYou have modifications to your git worktree.\nPlease revert or commit them before running convert.`);\n } else if (status.not_added.length > 0) {\n console.log(`\\\nWarning: the following untracked files are present in your repository:\n${status.not_added.join('\\n')}\nProceeding anyway.\n`);\n }\n}\n\nfunction getShortDescription(coffeeFiles) {\n let firstFile = basename(coffeeFiles[0]);\n if (coffeeFiles.length === 1) {\n return firstFile;\n } else {\n return `${firstFile} and ${pluralize(coffeeFiles.length - 1, 'other file')}`;\n }\n}\n","import { exec } from 'mz/child_process';\nimport git from 'simple-git/promise';\n\nimport CLIError from './util/CLIError';\n\n/**\n * The land option \"packages\" a set of commits into a single merge commit that\n * can be pushed. Splitting the decaffeinate work up into different commits\n * allows git to properly track file history when a file is changed from\n * CoffeeScript to JavaScript.\n *\n * A typical use case is that the merge commit will include 4 commits: the three\n * auto-generated decaffeinate commits and a follow-up commit to fix lint\n * errors. Unlike the auto-generated decaffeinate commits, the merge commit is\n * created with the default author name.\n */\nexport default async function land(config) {\n let remote = config.landConfig && config.landConfig.remote;\n let upstreamBranch = config.landConfig && config.landConfig.upstreamBranch;\n let phabricatorAware = config.landConfig && config.landConfig.phabricatorAware;\n if (!remote) {\n console.log('No remote was specified. Defaulting to origin.');\n remote = 'origin';\n }\n if (!upstreamBranch) {\n console.log('No upstreamBranch was specified. Defaulting to master.');\n upstreamBranch = 'master';\n }\n let remoteBranch = `${remote}/${upstreamBranch}`;\n console.log(`Running fetch for ${remote}.`);\n await git().fetch([remote]);\n\n let commits = await getCommits(config);\n console.log(`Found ${commits.length} commits to use.`);\n\n let differentialRevisionLine = phabricatorAware ? await getDifferentialRevisionLine(commits) : null;\n\n await git().checkout(remoteBranch);\n for (let commit of commits) {\n console.log(`Cherry-picking \"${commit.message}\"`);\n await git().raw(['cherry-pick', commit.hash]);\n let status = await git().status();\n if (status.conflicted.length > 0) {\n throw new CLIError(`\\\nThe cherry pick had conflicts.\nPlease rebase your changes and retry \"bulk-decaffeinate land\"`);\n }\n let message = await getCommitMessage(commit.hash);\n if (phabricatorAware) {\n if (!message.includes('Differential Revision')) {\n message += `\\n\\n${differentialRevisionLine}`;\n await git().commit(message, ['--amend']);\n }\n }\n }\n\n console.log(`Creating merge commit on ${remoteBranch}`);\n let cherryPickHeadCommit = (await git().revparse(['HEAD'])).trim();\n await git().checkout(remoteBranch);\n\n let mergeMessage = `Merge decaffeinate changes into ${remoteBranch}`;\n if (phabricatorAware) {\n mergeMessage += `\\n\\n${differentialRevisionLine}`;\n }\n await git().mergeFromTo(cherryPickHeadCommit, 'HEAD', ['--no-ff']);\n await git().commit(mergeMessage, ['--amend']);\n if (phabricatorAware) {\n console.log('Pulling commit message from Phabricator.');\n await exec('arc amend');\n }\n console.log('');\n console.log('Done. Please verify that the git history looks right.');\n console.log('You can push your changes with a command like this:');\n console.log(`git push ${remote} HEAD:${upstreamBranch}`);\n console.log('If you get a conflict, you should re-run \"bulk-decaffeinate land\".');\n}\n\nasync function getCommits(config) {\n let explicitBase = null;\n if (config.landBase) {\n explicitBase = (await git().revparse([config.landBase])).trim();\n }\n\n let allCommits;\n try {\n allCommits = (await git().log({from: 'HEAD', to: 'HEAD~20'})).all;\n } catch (e) {\n allCommits = (await git().log({from: 'HEAD'})).all;\n }\n\n let commits = [];\n let hasSeenDecaffeinateCommit = false;\n\n for (let commit of allCommits) {\n let isDecaffeinateCommit = commit.author_name === 'decaffeinate';\n if (explicitBase !== null) {\n if (explicitBase === commit.hash) {\n return commits;\n }\n } else {\n if (hasSeenDecaffeinateCommit && !isDecaffeinateCommit) {\n return commits;\n }\n }\n if (!hasSeenDecaffeinateCommit && isDecaffeinateCommit) {\n hasSeenDecaffeinateCommit = true;\n }\n commits.unshift(commit);\n }\n throw new CLIError(`\\\nSearched 20 commits without finding a set of commits to use. Make sure you have\ncommits with the \"decaffeinate\" author in your recent git history, and that the\nfirst of those commits is the first commit that you would like to land.`);\n}\n\nasync function getDifferentialRevisionLine(commits) {\n let resultLine = null;\n for (let commit of commits) {\n let commitMessage = await getCommitMessage(commit.hash);\n for (let line of commitMessage.split('\\n')) {\n if (line.startsWith('Differential Revision')) {\n if (resultLine === null || resultLine === line) {\n resultLine = line;\n } else {\n throw new CLIError(`\\\nFound multiple different \"Differential Revision\" lines in the matched commits.\nPlease set your git HEAD so that only one Phabricator code review is included.`);\n }\n }\n }\n }\n if (resultLine === null) {\n throw new CLIError(`\nExpected to find a \"Differential Revision\" line in at least one commit.`);\n }\n return resultLine;\n}\n\nasync function getCommitMessage(commitHash) {\n return await git().show(['-s', '--format=%B', commitHash]);\n}\n","import { readFile, writeFile } from 'fs-promise';\n\nimport runWithProgressBar from '../runner/runWithProgressBar';\nimport { HEADER_COMMENT_LINES } from './runEslintFix';\n\nexport default async function removeAutogeneratedHeader(config, jsFiles) {\n await runWithProgressBar(\n config,\n 'Removing any existing autogenerated headers...',\n jsFiles,\n removeHeadersFromFile);\n}\n\nasync function removeHeadersFromFile(path) {\n let contents = await readFile(path);\n let newContents = removeHeadersFromCode(contents);\n if (newContents !== contents) {\n await writeFile(path, newContents);\n }\n}\n\nexport function removeHeadersFromCode(code) {\n let lines = code.toString().split('\\n');\n\n let resultLines = [];\n for (let i = 0; i < lines.length; i++) {\n let line = lines[i];\n // Remove lines exactly matching a line we auto-generate.\n if (Object.values(HEADER_COMMENT_LINES).includes(line)) {\n continue;\n }\n\n // Remove regions of lines exactly matching our eslint-disable format.\n if (line === '/* eslint-disable') {\n let j = i + 1;\n let foundMatch = false;\n while (j < lines.length) {\n if (lines[j] === '*/') {\n foundMatch = true;\n break;\n }\n if (!lines[j].startsWith(' ') || !lines[j].endsWith(',')) {\n break;\n }\n j++;\n }\n if (foundMatch) {\n // Skip forward to j, the \"*/\" line, so the next considered line will be\n // the one after.\n i = j;\n continue;\n }\n }\n\n // Everything else gets added to the file.\n resultLines.push(line);\n }\n return resultLines.join('\\n');\n}\n","import getFilesToProcess from './config/getFilesToProcess';\nimport removeAutogeneratedHeader from './modernize/removeAutogeneratedHeader';\nimport runEslintFix from './modernize/runEslintFix';\nimport runFixImports from './modernize/runFixImports';\nimport runJscodeshiftScripts from './modernize/runJscodeshiftScripts';\nimport makeCLIFn from './runner/makeCLIFn';\nimport runWithProgressBar from './runner/runWithProgressBar';\nimport { JS_FILE_RECOGNIZER } from './util/FilePaths';\nimport pluralize from './util/pluralize';\n\nexport default async function modernizeJS(config) {\n let {decaffeinateArgs = [], decaffeinatePath} = config;\n\n let jsFiles = await getFilesToProcess(config, JS_FILE_RECOGNIZER);\n if (jsFiles.length === 0) {\n console.log('There were no JavaScript files to convert.');\n return;\n }\n\n await removeAutogeneratedHeader(config, jsFiles);\n await runWithProgressBar(\n config,\n 'Running decaffeinate --modernize-js on all files...',\n jsFiles,\n makeCLIFn(path => `${decaffeinatePath} --modernize-js ${decaffeinateArgs.join(' ')} ${path}`)\n );\n if (config.jscodeshiftScripts) {\n await runJscodeshiftScripts(jsFiles, config);\n }\n if (config.fixImportsConfig) {\n await runFixImports(jsFiles, config);\n }\n if (!config.skipEslintFix) {\n await runEslintFix(jsFiles, config, {isUpdate: true});\n }\n\n console.log(`Successfully modernized ${pluralize(jsFiles.length, 'file')}.`);\n console.log('You should now fix lint issues in any affected files.');\n}\n","import { exists, readFile } from 'mz/fs';\nimport readline from 'mz/readline';\nimport opn from 'opn';\n\nexport default async function viewErrors() {\n if (!(await exists('decaffeinate-results.json'))) {\n console.log(\n 'decaffeinate-results.json file not found. Please run the \"check\" command first.');\n return;\n }\n\n let resultsJson = await readFile('decaffeinate-results.json');\n let results = JSON.parse(resultsJson);\n let filesToOpen = results.filter(r => r.error !== null).map(r => r.path);\n if (filesToOpen.length === 0) {\n console.log('No failures were found!');\n return;\n }\n\n if (filesToOpen.length > 10) {\n let rl = readline.createInterface(process.stdin, process.stdout);\n let answer = await rl.question(\n `This will open ${filesToOpen.length} browser tabs. Do you want to proceed? [y/N] `);\n rl.close();\n if (!answer.toLowerCase().startsWith('y')) {\n return;\n }\n }\n for (let path of filesToOpen) {\n await openInRepl(path);\n }\n}\n\nasync function openInRepl(path) {\n let fileContents = await readFile(path);\n let encodedFile = encodeURIComponent(fileContents);\n let url = `http://decaffeinate-project.org/repl/#?evaluate=false&stage=full&code=${encodedFile}`;\n await opn(url, {wait: false});\n}\n","import 'babel-polyfill';\nimport commander from 'commander';\n\nimport check from './check';\nimport clean from './clean';\nimport resolveConfig from './config/resolveConfig';\nimport convert from './convert';\nimport land from './land';\nimport modernizeJS from './modernizeJS';\nimport CLIError from './util/CLIError';\nimport viewErrors from './viewErrors';\n\nexport default function () {\n let command = null;\n commander\n .arguments('<command>')\n .description(`Run decaffeinate on a set of files.\n\n Commands:\n check: Try decaffeinate on the specified files and generate a report of which files can be\n converted. By default, all .coffee files in the current directory are used.\n convert: Run decaffeinate on the specified files and generate git commits for the transition.\n view-errors: Open failures from the most recent run in an online repl.\n clean: Delete all files ending with .original.coffee in the current working directory or any\n of its subdirectories.\n land: Create a merge commit with all commits generated by bulk-decaffeinate.`)\n .action(commandArg => command = commandArg)\n .option('-c, --config [path]',\n `The config file to use. This arg may be specified multiple\n times. If unspecified, files like bulk-decaffeinate.config.js will\n be discovered and used.`,\n (arg, args) => {args.push(arg); return args;}, [])\n .option('-f, --file [path]',\n `An absolute or relative path to decaffeinate. This arg may be\n specified multiple times.`,\n