diff --git a/bin/prettier.js b/bin/prettier.js index 983ab272..79fe9b23 100755 --- a/bin/prettier.js +++ b/bin/prettier.js @@ -2,488 +2,4 @@ "use strict"; -const chalk = require("chalk"); -const dashify = require("dashify"); -const fs = require("fs"); -const getStream = require("get-stream"); -const globby = require("globby"); -const minimist = require("minimist"); -const path = require("path"); -const readline = require("readline"); -const ignore = require("ignore"); - -const prettier = eval("require")("../index"); -const cleanAST = require("../src/clean-ast").cleanAST; -const resolver = require("../src/resolve-config"); - -const args = process.argv.slice(2); - -const booleanOptionNames = [ - "use-tabs", - "semi", - "single-quote", - "bracket-spacing", - "jsx-bracket-same-line", - // Deprecated in 0.0.10 - "flow-parser" -]; -const stringOptionNames = [ - "print-width", - "tab-width", - "parser", - "trailing-comma" -]; - -const argv = minimist(args, { - boolean: [ - "write", - "stdin", - // The supports-color package (a sub sub dependency) looks directly at - // `process.argv` for `--no-color` and such-like options. The reason it is - // listed here is to avoid "Ignored unknown option: --no-color" warnings. - // See https://github.com/chalk/supports-color/#info for more information. - "color", - "list-different", - "help", - "version", - "debug-print-doc", - "debug-check", - "with-node-modules" - ].concat(booleanOptionNames), - string: [ - "cursor-offset", - "range-start", - "range-end", - "stdin-filepath", - "config", - "find-config-path", - "ignore-path" - ].concat(stringOptionNames), - default: { - color: true, - "ignore-path": ".prettierignore" - }, - alias: { - help: "h", - version: "v", - "list-different": "l" - }, - unknown: param => { - if (param.startsWith("-")) { - console.warn("Ignored unknown option: " + param + "\n"); - return false; - } - } -}); - -if (argv["version"]) { - console.log(prettier.version); - process.exit(0); -} - -const filepatterns = argv["_"]; -const write = argv["write"]; -const stdin = argv["stdin"] || (!filepatterns.length && !process.stdin.isTTY); -const ignoreNodeModules = argv["with-node-modules"] === false; -const ignoreNodeModulesGlobs = ["!**/node_modules/**", "!./node_modules/**"]; -const ignorePath = argv["ignore-path"]; -const globOptions = { - dot: true -}; - -if (write && argv["debug-check"]) { - console.error("Cannot use --write and --debug-check together."); - process.exit(1); -} - -if (argv["find-config-path"] && filepatterns.length) { - console.error("Cannot use --find-config-path with multiple files"); - process.exit(1); -} - -function getOptionsForFile(filePath) { - const options = - argv["config"] === false ? null : resolver.resolveConfig.sync(filePath); - - try { - const parsedArgs = minimist(args, { - boolean: booleanOptionNames, - string: stringOptionNames, - default: Object.assign( - { - semi: true, - "bracket-spacing": true, - parser: "babylon" - }, - dashifyObject(options) - ) - }); - return getOptions(Object.assign({}, argv, parsedArgs)); - } catch (error) { - console.error("Invalid configuration file:", error.toString()); - process.exit(2); - } -} - -function getOptions(argv) { - return { - cursorOffset: getIntOption(argv, "cursor-offset"), - rangeStart: getIntOption(argv, "range-start"), - rangeEnd: getIntOption(argv, "range-end"), - useTabs: argv["use-tabs"], - semi: argv["semi"], - printWidth: getIntOption(argv, "print-width"), - tabWidth: getIntOption(argv, "tab-width"), - bracketSpacing: argv["bracket-spacing"], - singleQuote: argv["single-quote"], - jsxBracketSameLine: argv["jsx-bracket-same-line"], - filepath: argv["stdin-filepath"], - trailingComma: getTrailingComma(argv), - parser: getParserOption(argv) - }; -} - -function getParserOption(argv) { - const value = argv.parser; - - if (value === undefined) { - return value; - } - - // For backward compatibility. Deprecated in 0.0.10 - if (argv["flow-parser"]) { - console.warn("`--flow-parser` is deprecated. Use `--parser flow` instead."); - return "flow"; - } - - return value; -} - -function getIntOption(argv, optionName) { - const value = argv[optionName]; - - if (value === undefined) { - return value; - } - - if (/^\d+$/.test(value)) { - return Number(value); - } - - console.error( - "Invalid --" + - optionName + - " value. Expected an integer, but received: " + - JSON.stringify(value) - ); - process.exit(1); -} - -function getTrailingComma(argv) { - switch (argv["trailing-comma"]) { - case undefined: - case "none": - return "none"; - case "": - console.warn( - "Warning: `--trailing-comma` was used without an argument. This is deprecated. " + - 'Specify "none", "es5", or "all".' - ); - return "es5"; - case "es5": - return "es5"; - case "all": - return "all"; - default: - throw new Error("Invalid option for --trailing-comma"); - } -} - -function dashifyObject(object) { - return Object.keys(object || {}).reduce((output, key) => { - output[dashify(key)] = object[key]; - return output; - }, {}); -} - -function diff(a, b) { - return require("diff").createTwoFilesPatch("", "", a, b, "", "", { - context: 2 - }); -} - -function format(input, opt) { - if (argv["debug-print-doc"]) { - const doc = prettier.__debug.printToDoc(input, opt); - return { formatted: prettier.__debug.formatDoc(doc) }; - } - - if (argv["debug-check"]) { - const pp = prettier.format(input, opt); - const pppp = prettier.format(pp, opt); - if (pp !== pppp) { - throw "prettier(input) !== prettier(prettier(input))\n" + diff(pp, pppp); - } else { - const ast = cleanAST(prettier.__debug.parse(input, opt)); - const past = cleanAST(prettier.__debug.parse(pp, opt)); - - if (ast !== past) { - const MAX_AST_SIZE = 2097152; // 2MB - const astDiff = - ast.length > MAX_AST_SIZE || past.length > MAX_AST_SIZE - ? "AST diff too large to render" - : diff(ast, past); - throw "ast(input) !== ast(prettier(input))\n" + - astDiff + - "\n" + - diff(input, pp); - } - } - return { formatted: opt.filepath || "(stdin)\n" }; - } - - return prettier.formatWithCursor(input, opt); -} - -function handleError(filename, e) { - const isParseError = Boolean(e && e.loc); - const isValidationError = /Validation Error/.test(e && e.message); - - // For parse errors and validation errors, we only want to show the error - // message formatted in a nice way. `String(e)` takes care of that. Other - // (unexpected) errors are passed as-is as a separate argument to - // `console.error`. That includes the stack trace (if any), and shows a nice - // `util.inspect` of throws things that aren't `Error` objects. (The Flow - // parser has mistakenly thrown arrays sometimes.) - if (isParseError) { - console.error(filename + ": " + String(e)); - } else if (isValidationError) { - console.error(String(e)); - // If validation fails for one file, it will fail for all of them. - process.exit(1); - } else { - console.error(filename + ":", e.stack || e); - } - - // Don't exit the process if one file failed - process.exitCode = 2; -} - -if ( - argv["help"] || - (!filepatterns.length && !stdin && !argv["find-config-path"]) -) { - console.log( - "Usage: prettier [opts] [filename ...]\n\n" + - "Available options:\n" + - " --write Edit the file in-place. (Beware!)\n" + - " --list-different or -l Print filenames of files that are different from Prettier formatting.\n" + - " --config Path to a prettier configuration file (.prettierrc, package.json, prettier.config.js).\n" + - " --no-config Do not look for a configuration file.\n" + - " --find-config-path \n" + - " Finds and prints the path to a configuration file for a given input file.\n" + - " --ignore-path Path to a file containing patterns that describe files to ignore.\n" + - " Defaults to ./.prettierignore.\n" + - " --stdin Read input from stdin.\n" + - " --stdin-filepath Path to the file used to read from stdin.\n" + - " --print-width Specify the length of line that the printer will wrap on. Defaults to 80.\n" + - " --tab-width Specify the number of spaces per indentation-level. Defaults to 2.\n" + - " --use-tabs Indent lines with tabs instead of spaces.\n" + - " --no-semi Do not print semicolons, except at the beginning of lines which may need them.\n" + - " --single-quote Use single quotes instead of double quotes.\n" + - " --no-bracket-spacing Do not print spaces between brackets.\n" + - " --jsx-bracket-same-line Put > on the last line instead of at a new line.\n" + - " --trailing-comma \n" + - " Print trailing commas wherever possible when multi-line. Defaults to none.\n" + - " --parser \n" + - " Specify which parse to use. Defaults to babylon.\n" + - " --cursor-offset Print (to stderr) where a cursor at the given position would move to after formatting.\n" + - " This option cannot be used with --range-start and --range-end\n" + - " --range-start Format code starting at a given character offset.\n" + - " The range will extend backwards to the start of the first line containing the selected statement.\n" + - " This option cannot be used with --cursor-offset.\n" + - " Defaults to 0.\n" + - " --range-end Format code ending at a given character offset (exclusive).\n" + - " The range will extend forwards to the end of the selected statement.\n" + - " This option cannot be used with --cursor-offset.\n" + - " Defaults to Infinity.\n" + - " --no-color Do not colorize error messages.\n" + - " --with-node-modules Process files inside `node_modules` directory.\n" + - " --version or -v Print Prettier version.\n" + - "\n" - ); - process.exit(argv["help"] ? 0 : 1); -} - -if (argv["find-config-path"]) { - resolveConfig(argv["find-config-path"]); -} else if (stdin) { - getStream(process.stdin).then(input => { - const options = getOptionsForFile(process.cwd()); - - if (listDifferent(input, options, "(stdin)")) { - return; - } - - try { - writeOutput(format(input, options), options); - } catch (e) { - handleError("stdin", e); - } - }); -} else { - eachFilename(filepatterns, (filename, options) => { - if (write) { - // Don't use `console.log` here since we need to replace this line. - process.stdout.write(filename); - } - - let input; - try { - input = fs.readFileSync(filename, "utf8"); - } catch (e) { - // Add newline to split errors from filename line. - process.stdout.write("\n"); - - console.error("Unable to read file: " + filename + "\n" + e); - // Don't exit the process if one file failed - process.exitCode = 2; - return; - } - - listDifferent(input, options, filename); - - const start = Date.now(); - - let result; - let output; - - try { - result = format( - input, - Object.assign({}, options, { filepath: filename }) - ); - output = result.formatted; - } catch (e) { - // Add newline to split errors from filename line. - process.stdout.write("\n"); - - handleError(filename, e); - return; - } - - if (write) { - // Remove previously printed filename to log it with duration. - readline.clearLine(process.stdout, 0); - readline.cursorTo(process.stdout, 0, null); - - // Don't write the file if it won't change in order not to invalidate - // mtime based caches. - if (output === input) { - if (!argv["list-different"]) { - console.log(chalk.grey("%s %dms"), filename, Date.now() - start); - } - } else { - if (argv["list-different"]) { - console.log(filename); - } else { - console.log("%s %dms", filename, Date.now() - start); - } - - try { - fs.writeFileSync(filename, output, "utf8"); - } catch (err) { - console.error("Unable to write file: " + filename + "\n" + err); - // Don't exit the process if one file failed - process.exitCode = 2; - } - } - } else if (argv["debug-check"]) { - if (output) { - console.log(output); - } else { - process.exitCode = 2; - } - } else if (!argv["list-different"]) { - writeOutput(result, options); - } - }); -} - -function listDifferent(input, options, filename) { - if (!argv["list-different"]) { - return; - } - - options = Object.assign({}, options, { filepath: filename }); - - if (!prettier.check(input, options)) { - if (!write) { - console.log(filename); - } - process.exitCode = 1; - } - - return true; -} - -function resolveConfig(filePath) { - const configFile = resolver.resolveConfigFile.sync(filePath); - if (configFile) { - console.log(path.relative(process.cwd(), configFile)); - } else { - process.exitCode = 1; - } -} - -function writeOutput(result, options) { - // Don't use `console.log` here since it adds an extra newline at the end. - process.stdout.write(result.formatted); - - if (options.cursorOffset) { - process.stderr.write(result.cursorOffset + "\n"); - } -} - -function eachFilename(patterns, callback) { - // The ignorer will be used to filter file paths after the glob is checked, - // before any files are actually read - const ignoreFilePath = path.resolve(ignorePath); - let ignoreText = ""; - - try { - ignoreText = fs.readFileSync(ignoreFilePath, "utf8"); - } catch (readError) { - if (readError.code !== "ENOENT") { - console.error(`Unable to read ${ignoreFilePath}:`, readError); - process.exit(2); - } - } - - const ignorer = ignore().add(ignoreText); - - if (ignoreNodeModules) { - patterns = patterns.concat(ignoreNodeModulesGlobs); - } - - return globby(patterns, globOptions) - .then(filePaths => { - if (filePaths.length === 0) { - console.error( - "No matching files. Patterns tried: " + patterns.join(" ") - ); - process.exitCode = 2; - return; - } - ignorer - .filter(filePaths) - .forEach(filePath => callback(filePath, getOptionsForFile(filePath))); - }) - .catch(err => { - console.error( - "Unable to expand glob patterns: " + patterns.join(" ") + "\n" + err - ); - // Don't exit the process if one pattern failed - process.exitCode = 2; - }); -} +require("../src/cli").run(process.argv.slice(2)); diff --git a/src/cli-constant.js b/src/cli-constant.js new file mode 100644 index 00000000..c2464a53 --- /dev/null +++ b/src/cli-constant.js @@ -0,0 +1,108 @@ +"use strict"; + +const booleanOptionNames = [ + "use-tabs", + "semi", + "single-quote", + "bracket-spacing", + "jsx-bracket-same-line", + // Deprecated in 0.0.10 + "flow-parser" +]; + +const stringOptionNames = [ + "print-width", + "tab-width", + "parser", + "trailing-comma" +]; + +const options = { + boolean: [ + "write", + "stdin", + // The supports-color package (a sub sub dependency) looks directly at + // `process.argv` for `--no-color` and such-like options. The reason it is + // listed here is to avoid "Ignored unknown option: --no-color" warnings. + // See https://github.com/chalk/supports-color/#info for more information. + "color", + "list-different", + "help", + "version", + "debug-print-doc", + "debug-check", + "with-node-modules" + ].concat(booleanOptionNames), + string: [ + "cursor-offset", + "range-start", + "range-end", + "stdin-filepath", + "config", + "find-config-path", + "ignore-path" + ].concat(stringOptionNames), + default: { + color: true, + "ignore-path": ".prettierignore" + }, + alias: { + help: "h", + version: "v", + "list-different": "l" + }, + unknown: param => { + if (param.startsWith("-")) { + console.warn(`Ignored unknown option: ${param}\n`); + return false; + } + } +}; + +const usage = ` +Usage: prettier [opts] [filename ...] + +Available options: + --write Edit the file in-place. (Beware!) + --list-different or -l Print filenames of files that are different from Prettier formatting. + --config Path to a prettier configuration file (.prettierrc, package.json, prettier.config.js). + --no-config Do not look for a configuration file. + --find-config-path + Finds and prints the path to a configuration file for a given input file. + --ignore-path Path to a file containing patterns that describe files to ignore. + Defaults to ./.prettierignore. + --stdin Read input from stdin. + --stdin-filepath Path to the file used to read from stdin. + --print-width Specify the length of line that the printer will wrap on. Defaults to 80. + --tab-width Specify the number of spaces per indentation-level. Defaults to 2. + --use-tabs Indent lines with tabs instead of spaces. + --no-semi Do not print semicolons, except at the beginning of lines which may need them. + --single-quote Use single quotes instead of double quotes. + --no-bracket-spacing Do not print spaces between brackets. + --jsx-bracket-same-line Put > on the last line instead of at a new line. + --trailing-comma + Print trailing commas wherever possible when multi-line. Defaults to none. + --parser + Specify which parse to use. Defaults to babylon. + --cursor-offset Print (to stderr) where a cursor at the given position would move to after formatting. + This option cannot be used with --range-start and --range-end + --range-start Format code starting at a given character offset. + The range will extend backwards to the start of the first line containing the selected statement. + This option cannot be used with --cursor-offset. + Defaults to 0. + --range-end Format code ending at a given character offset (exclusive). + The range will extend forwards to the end of the selected statement. + This option cannot be used with --cursor-offset. + Defaults to Infinity. + --no-color Do not colorize error messages. + --with-node-modules Process files inside 'node_modules' directory. + --version or -v Print Prettier version. + +`.slice(1); // remove leading line break + +module.exports = { + booleanOptionNames, + stringOptionNames, + options, + usage +}; diff --git a/src/cli-util.js b/src/cli-util.js new file mode 100644 index 00000000..6fba345d --- /dev/null +++ b/src/cli-util.js @@ -0,0 +1,364 @@ +"use strict"; + +const path = require("path"); +const dashify = require("dashify"); +const minimist = require("minimist"); +const getStream = require("get-stream"); +const fs = require("fs"); +const globby = require("globby"); +const ignore = require("ignore"); +const chalk = require("chalk"); +const readline = require("readline"); + +const prettier = eval("require")("../index"); +const cleanAST = require("./clean-ast").cleanAST; +const resolver = require("./resolve-config"); +const constant = require("./cli-constant"); + +function getOptions(argv) { + return { + cursorOffset: getIntOption(argv, "cursor-offset"), + rangeStart: getIntOption(argv, "range-start"), + rangeEnd: getIntOption(argv, "range-end"), + useTabs: argv["use-tabs"], + semi: argv["semi"], + printWidth: getIntOption(argv, "print-width"), + tabWidth: getIntOption(argv, "tab-width"), + bracketSpacing: argv["bracket-spacing"], + singleQuote: argv["single-quote"], + jsxBracketSameLine: argv["jsx-bracket-same-line"], + filepath: argv["stdin-filepath"], + trailingComma: getTrailingComma(argv), + parser: getParserOption(argv) + }; +} + +function getParserOption(argv) { + const value = argv.parser; + + if (value === undefined) { + return value; + } + + // For backward compatibility. Deprecated in 0.0.10 + if (argv["flow-parser"]) { + console.warn("`--flow-parser` is deprecated. Use `--parser flow` instead."); + return "flow"; + } + + return value; +} + +function getIntOption(argv, optionName) { + const value = argv[optionName]; + + if (value === undefined) { + return value; + } + + if (/^\d+$/.test(value)) { + return Number(value); + } + + console.error( + "Invalid --" + + optionName + + " value. Expected an integer, but received: " + + JSON.stringify(value) + ); + process.exit(1); +} + +function getTrailingComma(argv) { + switch (argv["trailing-comma"]) { + case undefined: + case "none": + return "none"; + case "": + console.warn( + "Warning: `--trailing-comma` was used without an argument. This is deprecated. " + + 'Specify "none", "es5", or "all".' + ); + return "es5"; + case "es5": + return "es5"; + case "all": + return "all"; + default: + throw new Error("Invalid option for --trailing-comma"); + } +} + +function dashifyObject(object) { + return Object.keys(object || {}).reduce((output, key) => { + output[dashify(key)] = object[key]; + return output; + }, {}); +} + +function diff(a, b) { + return require("diff").createTwoFilesPatch("", "", a, b, "", "", { + context: 2 + }); +} + +function handleError(filename, e) { + const isParseError = Boolean(e && e.loc); + const isValidationError = /Validation Error/.test(e && e.message); + + // For parse errors and validation errors, we only want to show the error + // message formatted in a nice way. `String(e)` takes care of that. Other + // (unexpected) errors are passed as-is as a separate argument to + // `console.error`. That includes the stack trace (if any), and shows a nice + // `util.inspect` of throws things that aren't `Error` objects. (The Flow + // parser has mistakenly thrown arrays sometimes.) + if (isParseError) { + console.error(`${filename}: ${String(e)}`); + } else if (isValidationError) { + console.error(String(e)); + // If validation fails for one file, it will fail for all of them. + process.exit(1); + } else { + console.error(filename + ":", e.stack || e); + } + + // Don't exit the process if one file failed + process.exitCode = 2; +} + +function resolveConfig(filePath) { + const configFile = resolver.resolveConfigFile.sync(filePath); + if (configFile) { + console.log(path.relative(process.cwd(), configFile)); + } else { + process.exitCode = 1; + } +} + +function writeOutput(result, options) { + // Don't use `console.log` here since it adds an extra newline at the end. + process.stdout.write(result.formatted); + + if (options.cursorOffset) { + process.stderr.write(result.cursorOffset + "\n"); + } +} + +function listDifferent(argv, input, options, filename) { + if (!argv["list-different"]) { + return; + } + + options = Object.assign({}, options, { filepath: filename }); + + if (!prettier.check(input, options)) { + if (!argv["write"]) { + console.log(filename); + } + process.exitCode = 1; + } + + return true; +} + +function format(argv, input, opt) { + if (argv["debug-print-doc"]) { + const doc = prettier.__debug.printToDoc(input, opt); + return { formatted: prettier.__debug.formatDoc(doc) }; + } + + if (argv["debug-check"]) { + const pp = prettier.format(input, opt); + const pppp = prettier.format(pp, opt); + if (pp !== pppp) { + throw "prettier(input) !== prettier(prettier(input))\n" + diff(pp, pppp); + } else { + const ast = cleanAST(prettier.__debug.parse(input, opt)); + const past = cleanAST(prettier.__debug.parse(pp, opt)); + + if (ast !== past) { + const MAX_AST_SIZE = 2097152; // 2MB + const astDiff = + ast.length > MAX_AST_SIZE || past.length > MAX_AST_SIZE + ? "AST diff too large to render" + : diff(ast, past); + throw "ast(input) !== ast(prettier(input))\n" + + astDiff + + "\n" + + diff(input, pp); + } + } + return { formatted: opt.filepath || "(stdin)\n" }; + } + + return prettier.formatWithCursor(input, opt); +} + +function getOptionsForFile(argv, filePath) { + const options = + argv["config"] === false ? null : resolver.resolveConfig.sync(filePath); + + try { + const parsedArgs = minimist(argv.__args, { + boolean: constant.booleanOptionNames, + string: constant.stringOptionNames, + default: Object.assign( + { + semi: true, + "bracket-spacing": true, + parser: "babylon" + }, + dashifyObject(options) + ) + }); + return getOptions(Object.assign({}, argv, parsedArgs)); + } catch (error) { + console.error("Invalid configuration:", error.toString()); + process.exit(2); + } +} + +function formatStdin(argv) { + getStream(process.stdin).then(input => { + const options = getOptionsForFile(argv, process.cwd()); + + if (listDifferent(argv, input, options, "(stdin)")) { + return; + } + + try { + writeOutput(format(argv, input, options), options); + } catch (e) { + handleError("stdin", e); + } + }); +} + +function eachFilename(argv, patterns, callback) { + const ignoreNodeModules = argv["with-node-modules"] === false; + // The ignorer will be used to filter file paths after the glob is checked, + // before any files are actually read + const ignoreFilePath = path.resolve(argv["ignore-path"]); + let ignoreText = ""; + + try { + ignoreText = fs.readFileSync(ignoreFilePath, "utf8"); + } catch (readError) { + if (readError.code !== "ENOENT") { + console.error(`Unable to read ${ignoreFilePath}:`, readError); + process.exit(2); + } + } + + const ignorer = ignore().add(ignoreText); + + if (ignoreNodeModules) { + patterns = patterns.concat(["!**/node_modules/**", "!./node_modules/**"]); + } + + const filePaths = globby.sync(patterns, { dot: true }); + try { + if (filePaths.length === 0) { + console.error(`No matching files. Patterns tried: ${patterns.join(" ")}`); + process.exitCode = 2; + return; + } + ignorer + .filter(filePaths) + .forEach(filePath => + callback(filePath, getOptionsForFile(argv, filePath)) + ); + } catch (err) { + console.error( + `Unable to expand glob patterns: ${patterns.join(" ")}\n${err}` + ); + // Don't exit the process if one pattern failed + process.exitCode = 2; + } +} + +function formatFiles(argv, filepatterns) { + eachFilename(argv, filepatterns, (filename, options) => { + if (argv["write"]) { + // Don't use `console.log` here since we need to replace this line. + process.stdout.write(filename); + } + + let input; + try { + input = fs.readFileSync(filename, "utf8"); + } catch (e) { + // Add newline to split errors from filename line. + process.stdout.write("\n"); + + console.error(`Unable to read file: ${filename}\n${e}`); + // Don't exit the process if one file failed + process.exitCode = 2; + return; + } + + listDifferent(argv, input, options, filename); + + const start = Date.now(); + + let result; + let output; + + try { + result = format( + argv, + input, + Object.assign({}, options, { filepath: filename }) + ); + output = result.formatted; + } catch (e) { + // Add newline to split errors from filename line. + process.stdout.write("\n"); + + handleError(filename, e); + return; + } + + if (argv["write"]) { + // Remove previously printed filename to log it with duration. + readline.clearLine(process.stdout, 0); + readline.cursorTo(process.stdout, 0, null); + + // Don't write the file if it won't change in order not to invalidate + // mtime based caches. + if (output === input) { + if (!argv["list-different"]) { + console.log(chalk.grey("%s %dms"), filename, Date.now() - start); + } + } else { + if (argv["list-different"]) { + console.log(filename); + } else { + console.log("%s %dms", filename, Date.now() - start); + } + + try { + fs.writeFileSync(filename, output, "utf8"); + } catch (err) { + console.error(`Unable to write file: ${filename}\n${err}`); + // Don't exit the process if one file failed + process.exitCode = 2; + } + } + } else if (argv["debug-check"]) { + if (output) { + console.log(output); + } else { + process.exitCode = 2; + } + } else if (!argv["list-different"]) { + writeOutput(result, options); + } + }); +} + +module.exports = { + resolveConfig, + formatStdin, + formatFiles +}; diff --git a/src/cli.js b/src/cli.js new file mode 100644 index 00000000..0f1ae78b --- /dev/null +++ b/src/cli.js @@ -0,0 +1,50 @@ +"use strict"; + +const minimist = require("minimist"); + +const prettier = eval("require")("../index"); +const constant = require("./cli-constant"); +const util = require("./cli-util"); + +function run(args) { + const argv = minimist(args, constant.options); + argv.__args = args; + + if (argv["version"]) { + console.log(prettier.version); + process.exit(0); + } + + const filepatterns = argv["_"]; + const stdin = argv["stdin"] || (!filepatterns.length && !process.stdin.isTTY); + + if (argv["write"] && argv["debug-check"]) { + console.error("Cannot use --write and --debug-check together."); + process.exit(1); + } + + if (argv["find-config-path"] && filepatterns.length) { + console.error("Cannot use --find-config-path with multiple files"); + process.exit(1); + } + + if ( + argv["help"] || + (!filepatterns.length && !stdin && !argv["find-config-path"]) + ) { + console.log(constant.usage); + process.exit(argv["help"] ? 0 : 1); + } + + if (argv["find-config-path"]) { + util.resolveConfig(argv["find-config-path"]); + } else if (stdin) { + util.formatStdin(argv); + } else { + util.formatFiles(argv, filepatterns); + } +} + +module.exports = { + run +}; diff --git a/tests_integration/__tests__/multiple-patterns.js b/tests_integration/__tests__/multiple-patterns.js index a76603b7..06016ba4 100644 --- a/tests_integration/__tests__/multiple-patterns.js +++ b/tests_integration/__tests__/multiple-patterns.js @@ -71,7 +71,6 @@ test("multiple patterns by with ignore pattern, doesn't ignore node_modules with test("no errors on empty patterns", () => { const result = runPrettier("cli/multiple-patterns"); - expect(result.status).toEqual(0); }); diff --git a/tests_integration/runPrettier.js b/tests_integration/runPrettier.js index e1329c33..5646dc91 100644 --- a/tests_integration/runPrettier.js +++ b/tests_integration/runPrettier.js @@ -1,34 +1,94 @@ -/* - * runPrettier – spawns `prettier` process. - * Adopted from Jest's integration tests suite. - */ "use strict"; const path = require("path"); -const spawnSync = require("cross-spawn").sync; -const PRETTIER_PATH = path.resolve(__dirname, "../bin/prettier.js"); - -// return the result of the spawned process: -// [ 'status', 'signal', 'output', 'pid', 'stdout', 'stderr', -// 'envPairs', 'options', 'args', 'file' ] function runPrettier(dir, args, options) { - const isRelative = dir[0] !== "/"; + let status; + let stdout = ""; + let stderr = ""; - if (isRelative) { - dir = path.resolve(__dirname, dir); + const spiedProcessExit = jest.spyOn(process, "exit"); + spiedProcessExit.mockImplementation(exitCode => { + if (status === undefined) { + status = exitCode || 0; + } + }); + + const spiedStdoutWrite = jest.spyOn(process.stdout, "write"); + spiedStdoutWrite.mockImplementation(text => { + if (status === undefined) { + stdout += text; + } + }); + + const spiedStderrWrite = jest.spyOn(process.stderr, "write"); + spiedStderrWrite.mockImplementation(text => { + if (status === undefined) { + stderr += text; + } + }); + + const spiedConsoleLog = jest.spyOn(console, "log"); + spiedConsoleLog.mockImplementation(text => { + if (status === undefined) { + stdout += text + "\n"; + } + }); + + const spiedConsoleWarn = jest.spyOn(console, "warn"); + spiedConsoleWarn.mockImplementation(text => { + if (status === undefined) { + stderr += text + "\n"; + } + }); + + const spiedConsoleError = jest.spyOn(console, "error"); + spiedConsoleError.mockImplementation(text => { + if (status === undefined) { + stderr += text + "\n"; + } + }); + + const originalCwd = process.cwd(); + const originalIsTTY = process.stdin.isTTY; + const originalArgv = process.argv; + const originalExitCode = process.exitCode; + + process.chdir(normalizeDir(dir)); + process.stdin.isTTY = false; + process.argv = ["path/to/node", "path/to/prettier/bin"].concat(args || []); + + jest.resetModules(); + jest.setMock("get-stream", () => ({ + then: handler => handler((options && options.input) || "") + })); + + try { + require("../bin/prettier"); + status = status || process.exitCode || 0; + } catch (error) { + stderr += error.message; + status = 1; + } finally { + process.exitCode = originalExitCode; + process.stdin.isTTY = originalIsTTY; + process.argv = originalArgv; + process.chdir(originalCwd); + + spiedProcessExit.mockRestore(); + spiedStdoutWrite.mockRestore(); + spiedStderrWrite.mockRestore(); + spiedConsoleLog.mockRestore(); + spiedConsoleWarn.mockRestore(); + spiedConsoleError.mockRestore(); } - const result = spawnSync( - PRETTIER_PATH, - args || [], - Object.assign({}, options, { cwd: dir }) - ); + return { status, stdout, stderr }; +} - result.stdout = result.stdout && result.stdout.toString(); - result.stderr = result.stderr && result.stderr.toString(); - - return result; +function normalizeDir(dir) { + const isRelative = dir[0] !== "/"; + return isRelative ? path.resolve(__dirname, dir) : dir; } module.exports = runPrettier;