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 testingmaster
parent
a9520d31b1
commit
c4e5463514
486
bin/prettier.js
486
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 <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));
|
||||
|
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue