refactor(cli): separate files and make it pure as possible (#2730)

* refactor(bin): resolveConfig

* refactor(bin): getOptionsForFile

* refactor(bin): remove `mapSeries`

* refactor: move content from bin to src/cli

* refactor: wrap content with function

* refactor: move constants to another file

* refactor: move utils to another file

* refactor: move functions

* refactor: extract functions

* refactor: remove unnecessary variable

* refactor: move `src/cli` to `src` with prefix `cli`

* refactor: use template literal

* refactor: remove unnecessary variable

* refactor: extract `listDifferent`

* refactor: extract `format`

* refactor: change error message `Invalid configuration:`

* refactor: bind args with argv

* refactor: extract `getOptionsForFile`

* refactor: extract `formatStdin`

* refactor: extract `eachFilename`

* refactor: extract `formatFiles`

* refactor: remove unnecessary export

* refactor: use `globby.sync()`

* refactor: add `runPrettier.sync()` helper

* refactor: fix linting

* refactor: mock `process.argv`

* chore: use `--forceExit` to avoid process hanging

* refactor: mock `get-stream`

* refactor: remove unnecessary stuff

* refactor: fix linting

* refactor: wrap `run` with `try..catch.finally`

* refactor: restore `process.exitCode` after testing
master
Ika 2017-09-03 17:21:06 +08:00 committed by Lucas Azzola
parent a9520d31b1
commit c4e5463514
6 changed files with 605 additions and 508 deletions

View File

@ -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 <path>\n" +
" Finds and prints the path to a configuration file for a given input file.\n" +
" --ignore-path <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 <int> Specify the length of line that the printer will wrap on. Defaults to 80.\n" +
" --tab-width <int> 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 <none|es5|all>\n" +
" Print trailing commas wherever possible when multi-line. Defaults to none.\n" +
" --parser <flow|babylon|typescript|postcss|json|graphql>\n" +
" Specify which parse to use. Defaults to babylon.\n" +
" --cursor-offset <int> 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 <int> 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 <int> 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));

108
src/cli-constant.js Normal file
View File

@ -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 <path>
Finds and prints the path to a configuration file for a given input file.
--ignore-path <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 <int> Specify the length of line that the printer will wrap on. Defaults to 80.
--tab-width <int> 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 <none|es5|all>
Print trailing commas wherever possible when multi-line. Defaults to none.
--parser <flow|babylon|typescript|postcss|json|graphql>
Specify which parse to use. Defaults to babylon.
--cursor-offset <int> 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 <int> 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 <int> 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
};

364
src/cli-util.js Normal file
View File

@ -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
};

50
src/cli.js Normal file
View File

@ -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
};

View File

@ -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);
});

View File

@ -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;