1017 lines
29 KiB
JavaScript
1017 lines
29 KiB
JavaScript
"use strict";
|
|
|
|
const path = require("path");
|
|
const camelCase = require("camelcase");
|
|
const dashify = require("dashify");
|
|
const fs = require("fs");
|
|
const globby = require("globby");
|
|
const chalk = require("chalk");
|
|
const readline = require("readline");
|
|
const stringify = require("json-stable-stringify");
|
|
|
|
const minimist = require("./minimist");
|
|
const prettier = require("../../index");
|
|
const createIgnorer = require("../common/create-ignorer");
|
|
const errors = require("../common/errors");
|
|
const constant = require("./constant");
|
|
const coreOptions = require("../main/core-options");
|
|
const optionsModule = require("../main/options");
|
|
const optionsNormalizer = require("../main/options-normalizer");
|
|
const thirdParty = require("../common/third-party");
|
|
const arrayify = require("../utils/arrayify");
|
|
const isTTY = require("../utils/is-tty");
|
|
|
|
const OPTION_USAGE_THRESHOLD = 25;
|
|
const CHOICE_USAGE_MARGIN = 3;
|
|
const CHOICE_USAGE_INDENTATION = 2;
|
|
|
|
function getOptions(argv, detailedOptions) {
|
|
return detailedOptions
|
|
.filter(option => option.forwardToApi)
|
|
.reduce(
|
|
(current, option) =>
|
|
Object.assign(current, {
|
|
[option.forwardToApi]: argv[option.name]
|
|
}),
|
|
{}
|
|
);
|
|
}
|
|
|
|
function cliifyOptions(object, apiDetailedOptionMap) {
|
|
return Object.keys(object || {}).reduce((output, key) => {
|
|
const apiOption = apiDetailedOptionMap[key];
|
|
const cliKey = apiOption ? apiOption.name : key;
|
|
|
|
output[dashify(cliKey)] = object[key];
|
|
return output;
|
|
}, {});
|
|
}
|
|
|
|
function diff(a, b) {
|
|
return require("diff").createTwoFilesPatch("", "", a, b, "", "", {
|
|
context: 2
|
|
});
|
|
}
|
|
|
|
function handleError(context, filename, error) {
|
|
if (error instanceof errors.UndefinedParserError) {
|
|
if (context.argv["write"] && isTTY()) {
|
|
readline.clearLine(process.stdout, 0);
|
|
readline.cursorTo(process.stdout, 0, null);
|
|
}
|
|
if (!context.argv["check"] && !context.argv["list-different"]) {
|
|
process.exitCode = 2;
|
|
}
|
|
context.logger.error(error.message);
|
|
return;
|
|
}
|
|
|
|
if (context.argv["write"]) {
|
|
// Add newline to split errors from filename line.
|
|
process.stdout.write("\n");
|
|
}
|
|
|
|
const isParseError = Boolean(error && error.loc);
|
|
const isValidationError = /Validation Error/.test(error && error.message);
|
|
|
|
// For parse errors and validation errors, we only want to show the error
|
|
// message formatted in a nice way. `String(error)` 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) {
|
|
context.logger.error(`${filename}: ${String(error)}`);
|
|
} else if (isValidationError || error instanceof errors.ConfigError) {
|
|
context.logger.error(String(error));
|
|
// If validation fails for one file, it will fail for all of them.
|
|
process.exit(1);
|
|
} else if (error instanceof errors.DebugError) {
|
|
context.logger.error(`${filename}: ${error.message}`);
|
|
} else {
|
|
context.logger.error(filename + ": " + (error.stack || error));
|
|
}
|
|
|
|
// Don't exit the process if one file failed
|
|
process.exitCode = 2;
|
|
}
|
|
|
|
function logResolvedConfigPathOrDie(context) {
|
|
const configFile = prettier.resolveConfigFile.sync(
|
|
context.argv["find-config-path"]
|
|
);
|
|
if (configFile) {
|
|
context.logger.log(path.relative(process.cwd(), configFile));
|
|
} else {
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function logFileInfoOrDie(context) {
|
|
const options = {
|
|
ignorePath: context.argv["ignore-path"],
|
|
withNodeModules: context.argv["with-node-modules"],
|
|
plugins: context.argv["plugin"],
|
|
pluginSearchDirs: context.argv["plugin-search-dir"]
|
|
};
|
|
context.logger.log(
|
|
prettier.format(
|
|
stringify(prettier.getFileInfo.sync(context.argv["file-info"], options)),
|
|
{ parser: "json" }
|
|
)
|
|
);
|
|
}
|
|
|
|
function writeOutput(context, result, options) {
|
|
// Don't use `console.log` here since it adds an extra newline at the end.
|
|
process.stdout.write(
|
|
context.argv["debug-check"] ? result.filepath : result.formatted
|
|
);
|
|
|
|
if (options && options.cursorOffset >= 0) {
|
|
process.stderr.write(result.cursorOffset + "\n");
|
|
}
|
|
}
|
|
|
|
function listDifferent(context, input, options, filename) {
|
|
if (!context.argv["check"] && !context.argv["list-different"]) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (!options.filepath && !options.parser) {
|
|
throw new errors.UndefinedParserError(
|
|
"No parser and no file path given, couldn't infer a parser."
|
|
);
|
|
}
|
|
if (!prettier.check(input, options)) {
|
|
if (!context.argv["write"]) {
|
|
context.logger.log(filename);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
context.logger.error(error.message);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function format(context, input, opt) {
|
|
if (!opt.parser && !opt.filepath) {
|
|
throw new errors.UndefinedParserError(
|
|
"No parser and no file path given, couldn't infer a parser."
|
|
);
|
|
}
|
|
|
|
if (context.argv["debug-print-doc"]) {
|
|
const doc = prettier.__debug.printToDoc(input, opt);
|
|
return { formatted: prettier.__debug.formatDoc(doc) };
|
|
}
|
|
|
|
if (context.argv["debug-check"]) {
|
|
const pp = prettier.format(input, opt);
|
|
const pppp = prettier.format(pp, opt);
|
|
if (pp !== pppp) {
|
|
throw new errors.DebugError(
|
|
"prettier(input) !== prettier(prettier(input))\n" + diff(pp, pppp)
|
|
);
|
|
} else {
|
|
const stringify = obj => JSON.stringify(obj, null, 2);
|
|
const ast = stringify(
|
|
prettier.__debug.parse(input, opt, /* massage */ true).ast
|
|
);
|
|
const past = stringify(
|
|
prettier.__debug.parse(pp, opt, /* massage */ true).ast
|
|
);
|
|
|
|
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 new errors.DebugError(
|
|
"ast(input) !== ast(prettier(input))\n" +
|
|
astDiff +
|
|
"\n" +
|
|
diff(input, pp)
|
|
);
|
|
}
|
|
}
|
|
return { formatted: pp, filepath: opt.filepath || "(stdin)\n" };
|
|
}
|
|
|
|
/* istanbul ignore if */
|
|
if (context.argv["debug-benchmark"]) {
|
|
let benchmark;
|
|
try {
|
|
benchmark = eval("require")("benchmark");
|
|
} catch (err) {
|
|
context.logger.debug(
|
|
"'--debug-benchmark' requires the 'benchmark' package to be installed."
|
|
);
|
|
process.exit(2);
|
|
}
|
|
context.logger.debug(
|
|
"'--debug-benchmark' option found, measuring formatWithCursor with 'benchmark' module."
|
|
);
|
|
const suite = new benchmark.Suite();
|
|
suite
|
|
.add("format", () => {
|
|
prettier.formatWithCursor(input, opt);
|
|
})
|
|
.on("cycle", event => {
|
|
const results = {
|
|
benchmark: String(event.target),
|
|
hz: event.target.hz,
|
|
ms: event.target.times.cycle * 1000
|
|
};
|
|
context.logger.debug(
|
|
"'--debug-benchmark' measurements for formatWithCursor: " +
|
|
JSON.stringify(results, null, 2)
|
|
);
|
|
})
|
|
.run({ async: false });
|
|
} else if (context.argv["debug-repeat"] > 0) {
|
|
const repeat = context.argv["debug-repeat"];
|
|
context.logger.debug(
|
|
"'--debug-repeat' option found, running formatWithCursor " +
|
|
repeat +
|
|
" times."
|
|
);
|
|
// should be using `performance.now()`, but only `Date` is cross-platform enough
|
|
const now = Date.now ? () => Date.now() : () => +new Date();
|
|
let totalMs = 0;
|
|
for (let i = 0; i < repeat; ++i) {
|
|
const startMs = now();
|
|
prettier.formatWithCursor(input, opt);
|
|
totalMs += now() - startMs;
|
|
}
|
|
const averageMs = totalMs / repeat;
|
|
const results = {
|
|
repeat,
|
|
hz: 1000 / averageMs,
|
|
ms: averageMs
|
|
};
|
|
context.logger.debug(
|
|
"'--debug-repeat' measurements for formatWithCursor: " +
|
|
JSON.stringify(results, null, 2)
|
|
);
|
|
}
|
|
|
|
return prettier.formatWithCursor(input, opt);
|
|
}
|
|
|
|
function getOptionsOrDie(context, filePath) {
|
|
try {
|
|
if (context.argv["config"] === false) {
|
|
context.logger.debug(
|
|
"'--no-config' option found, skip loading config file."
|
|
);
|
|
return null;
|
|
}
|
|
|
|
context.logger.debug(
|
|
context.argv["config"]
|
|
? `load config file from '${context.argv["config"]}'`
|
|
: `resolve config from '${filePath}'`
|
|
);
|
|
|
|
const options = prettier.resolveConfig.sync(filePath, {
|
|
editorconfig: context.argv["editorconfig"],
|
|
config: context.argv["config"]
|
|
});
|
|
|
|
context.logger.debug("loaded options `" + JSON.stringify(options) + "`");
|
|
return options;
|
|
} catch (error) {
|
|
context.logger.error("Invalid configuration file: " + error.message);
|
|
process.exit(2);
|
|
}
|
|
}
|
|
|
|
function getOptionsForFile(context, filepath) {
|
|
const options = getOptionsOrDie(context, filepath);
|
|
|
|
const hasPlugins = options && options.plugins;
|
|
if (hasPlugins) {
|
|
pushContextPlugins(context, options.plugins);
|
|
}
|
|
|
|
const appliedOptions = Object.assign(
|
|
{ filepath },
|
|
applyConfigPrecedence(
|
|
context,
|
|
options &&
|
|
optionsNormalizer.normalizeApiOptions(options, context.supportOptions, {
|
|
logger: context.logger
|
|
})
|
|
)
|
|
);
|
|
|
|
context.logger.debug(
|
|
`applied config-precedence (${context.argv["config-precedence"]}): ` +
|
|
`${JSON.stringify(appliedOptions)}`
|
|
);
|
|
|
|
if (hasPlugins) {
|
|
popContextPlugins(context);
|
|
}
|
|
|
|
return appliedOptions;
|
|
}
|
|
|
|
function parseArgsToOptions(context, overrideDefaults) {
|
|
const minimistOptions = createMinimistOptions(context.detailedOptions);
|
|
const apiDetailedOptionMap = createApiDetailedOptionMap(
|
|
context.detailedOptions
|
|
);
|
|
return getOptions(
|
|
optionsNormalizer.normalizeCliOptions(
|
|
minimist(
|
|
context.args,
|
|
Object.assign({
|
|
string: minimistOptions.string,
|
|
boolean: minimistOptions.boolean,
|
|
default: cliifyOptions(overrideDefaults, apiDetailedOptionMap)
|
|
})
|
|
),
|
|
context.detailedOptions,
|
|
{ logger: false }
|
|
),
|
|
context.detailedOptions
|
|
);
|
|
}
|
|
|
|
function applyConfigPrecedence(context, options) {
|
|
try {
|
|
switch (context.argv["config-precedence"]) {
|
|
case "cli-override":
|
|
return parseArgsToOptions(context, options);
|
|
case "file-override":
|
|
return Object.assign({}, parseArgsToOptions(context), options);
|
|
case "prefer-file":
|
|
return options || parseArgsToOptions(context);
|
|
}
|
|
} catch (error) {
|
|
context.logger.error(error.toString());
|
|
process.exit(2);
|
|
}
|
|
}
|
|
|
|
function formatStdin(context) {
|
|
const filepath = context.argv["stdin-filepath"]
|
|
? path.resolve(process.cwd(), context.argv["stdin-filepath"])
|
|
: process.cwd();
|
|
|
|
const ignorer = createIgnorerFromContextOrDie(context);
|
|
const relativeFilepath = path.relative(process.cwd(), filepath);
|
|
|
|
thirdParty.getStream(process.stdin).then(input => {
|
|
if (relativeFilepath && ignorer.filter([relativeFilepath]).length === 0) {
|
|
writeOutput(context, { formatted: input });
|
|
return;
|
|
}
|
|
|
|
const options = getOptionsForFile(context, filepath);
|
|
|
|
try {
|
|
if (listDifferent(context, input, options, "(stdin)")) {
|
|
return;
|
|
}
|
|
|
|
writeOutput(context, format(context, input, options), options);
|
|
} catch (error) {
|
|
handleError(context, relativeFilepath || "stdin", error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function createIgnorerFromContextOrDie(context) {
|
|
try {
|
|
return createIgnorer.sync(
|
|
context.argv["ignore-path"],
|
|
context.argv["with-node-modules"]
|
|
);
|
|
} catch (e) {
|
|
context.logger.error(e.message);
|
|
process.exit(2);
|
|
}
|
|
}
|
|
|
|
function eachFilename(context, patterns, callback) {
|
|
// The '!./' globs are due to https://github.com/prettier/prettier/issues/2110
|
|
const ignoreNodeModules = context.argv["with-node-modules"] !== true;
|
|
if (ignoreNodeModules) {
|
|
patterns = patterns.concat(["!**/node_modules/**", "!./node_modules/**"]);
|
|
}
|
|
patterns = patterns.concat(["!**/.{git,svn,hg}/**", "!./.{git,svn,hg}/**"]);
|
|
|
|
try {
|
|
const filePaths = globby
|
|
.sync(patterns, { dot: true, nodir: true })
|
|
.map(filePath => path.relative(process.cwd(), filePath));
|
|
|
|
if (filePaths.length === 0) {
|
|
context.logger.error(
|
|
`No matching files. Patterns tried: ${patterns.join(" ")}`
|
|
);
|
|
process.exitCode = 2;
|
|
return;
|
|
}
|
|
filePaths.forEach(filePath => callback(filePath));
|
|
} catch (error) {
|
|
context.logger.error(
|
|
`Unable to expand glob patterns: ${patterns.join(" ")}\n${error.message}`
|
|
);
|
|
// Don't exit the process if one pattern failed
|
|
process.exitCode = 2;
|
|
}
|
|
}
|
|
|
|
function formatFiles(context) {
|
|
// The ignorer will be used to filter file paths after the glob is checked,
|
|
// before any files are actually written
|
|
const ignorer = createIgnorerFromContextOrDie(context);
|
|
|
|
let numberOfUnformattedFilesFound = 0;
|
|
|
|
if (context.argv["check"]) {
|
|
context.logger.log("Checking formatting...");
|
|
}
|
|
|
|
eachFilename(context, context.filePatterns, filename => {
|
|
const fileIgnored = ignorer.filter([filename]).length === 0;
|
|
if (
|
|
fileIgnored &&
|
|
(context.argv["debug-check"] ||
|
|
context.argv["write"] ||
|
|
context.argv["check"] ||
|
|
context.argv["list-different"])
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const options = Object.assign(getOptionsForFile(context, filename), {
|
|
filepath: filename
|
|
});
|
|
|
|
if (isTTY()) {
|
|
// Don't use `console.log` here since we need to replace this line.
|
|
context.logger.log(filename, { newline: false });
|
|
}
|
|
|
|
let input;
|
|
try {
|
|
input = fs.readFileSync(filename, "utf8");
|
|
} catch (error) {
|
|
// Add newline to split errors from filename line.
|
|
context.logger.log("");
|
|
|
|
context.logger.error(
|
|
`Unable to read file: ${filename}\n${error.message}`
|
|
);
|
|
// Don't exit the process if one file failed
|
|
process.exitCode = 2;
|
|
return;
|
|
}
|
|
|
|
if (fileIgnored) {
|
|
writeOutput(context, { formatted: input }, options);
|
|
return;
|
|
}
|
|
|
|
const start = Date.now();
|
|
|
|
let result;
|
|
let output;
|
|
|
|
try {
|
|
result = format(
|
|
context,
|
|
input,
|
|
Object.assign({}, options, { filepath: filename })
|
|
);
|
|
output = result.formatted;
|
|
} catch (error) {
|
|
handleError(context, filename, error);
|
|
return;
|
|
}
|
|
|
|
const isDifferent = output !== input;
|
|
|
|
if (isTTY()) {
|
|
// Remove previously printed filename to log it with duration.
|
|
readline.clearLine(process.stdout, 0);
|
|
readline.cursorTo(process.stdout, 0, null);
|
|
}
|
|
|
|
if (context.argv["write"]) {
|
|
// Don't write the file if it won't change in order not to invalidate
|
|
// mtime based caches.
|
|
if (isDifferent) {
|
|
if (!context.argv["check"] && !context.argv["list-different"]) {
|
|
context.logger.log(`${filename} ${Date.now() - start}ms`);
|
|
}
|
|
|
|
try {
|
|
fs.writeFileSync(filename, output, "utf8");
|
|
} catch (error) {
|
|
context.logger.error(
|
|
`Unable to write file: ${filename}\n${error.message}`
|
|
);
|
|
// Don't exit the process if one file failed
|
|
process.exitCode = 2;
|
|
}
|
|
} else if (!context.argv["check"] && !context.argv["list-different"]) {
|
|
context.logger.log(`${chalk.grey(filename)} ${Date.now() - start}ms`);
|
|
}
|
|
} else if (context.argv["debug-check"]) {
|
|
if (result.filepath) {
|
|
context.logger.log(result.filepath);
|
|
} else {
|
|
process.exitCode = 2;
|
|
}
|
|
} else if (!context.argv["check"] && !context.argv["list-different"]) {
|
|
writeOutput(context, result, options);
|
|
}
|
|
|
|
if (
|
|
(context.argv["check"] || context.argv["list-different"]) &&
|
|
isDifferent
|
|
) {
|
|
context.logger.log(filename);
|
|
numberOfUnformattedFilesFound += 1;
|
|
}
|
|
});
|
|
|
|
// Print check summary based on expected exit code
|
|
if (context.argv["check"]) {
|
|
context.logger.log(
|
|
numberOfUnformattedFilesFound === 0
|
|
? "All matched files use Prettier code style!"
|
|
: context.argv["write"]
|
|
? "Code style issues fixed in the above file(s)."
|
|
: "Code style issues found in the above file(s). Forgot to run Prettier?"
|
|
);
|
|
}
|
|
|
|
// Ensure non-zero exitCode when using --check/list-different is not combined with --write
|
|
if (
|
|
(context.argv["check"] || context.argv["list-different"]) &&
|
|
numberOfUnformattedFilesFound > 0 &&
|
|
!process.exitCode &&
|
|
!context.argv["write"]
|
|
) {
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
function getOptionsWithOpposites(options) {
|
|
// Add --no-foo after --foo.
|
|
const optionsWithOpposites = options.map(option => [
|
|
option.description ? option : null,
|
|
option.oppositeDescription
|
|
? Object.assign({}, option, {
|
|
name: `no-${option.name}`,
|
|
type: "boolean",
|
|
description: option.oppositeDescription
|
|
})
|
|
: null
|
|
]);
|
|
return flattenArray(optionsWithOpposites).filter(Boolean);
|
|
}
|
|
|
|
function createUsage(context) {
|
|
const options = getOptionsWithOpposites(context.detailedOptions).filter(
|
|
// remove unnecessary option (e.g. `semi`, `color`, etc.), which is only used for --help <flag>
|
|
option =>
|
|
!(
|
|
option.type === "boolean" &&
|
|
option.oppositeDescription &&
|
|
!option.name.startsWith("no-")
|
|
)
|
|
);
|
|
|
|
const groupedOptions = groupBy(options, option => option.category);
|
|
|
|
const firstCategories = constant.categoryOrder.slice(0, -1);
|
|
const lastCategories = constant.categoryOrder.slice(-1);
|
|
const restCategories = Object.keys(groupedOptions).filter(
|
|
category =>
|
|
firstCategories.indexOf(category) === -1 &&
|
|
lastCategories.indexOf(category) === -1
|
|
);
|
|
const allCategories = firstCategories.concat(restCategories, lastCategories);
|
|
|
|
const optionsUsage = allCategories.map(category => {
|
|
const categoryOptions = groupedOptions[category]
|
|
.map(option => createOptionUsage(context, option, OPTION_USAGE_THRESHOLD))
|
|
.join("\n");
|
|
return `${category} options:\n\n${indent(categoryOptions, 2)}`;
|
|
});
|
|
|
|
return [constant.usageSummary].concat(optionsUsage, [""]).join("\n\n");
|
|
}
|
|
|
|
function createOptionUsage(context, option, threshold) {
|
|
const header = createOptionUsageHeader(option);
|
|
const optionDefaultValue = getOptionDefaultValue(context, option.name);
|
|
return createOptionUsageRow(
|
|
header,
|
|
`${option.description}${
|
|
optionDefaultValue === undefined
|
|
? ""
|
|
: `\nDefaults to ${createDefaultValueDisplay(optionDefaultValue)}.`
|
|
}`,
|
|
threshold
|
|
);
|
|
}
|
|
|
|
function createDefaultValueDisplay(value) {
|
|
return Array.isArray(value)
|
|
? `[${value.map(createDefaultValueDisplay).join(", ")}]`
|
|
: value;
|
|
}
|
|
|
|
function createOptionUsageHeader(option) {
|
|
const name = `--${option.name}`;
|
|
const alias = option.alias ? `-${option.alias},` : null;
|
|
const type = createOptionUsageType(option);
|
|
return [alias, name, type].filter(Boolean).join(" ");
|
|
}
|
|
|
|
function createOptionUsageRow(header, content, threshold) {
|
|
const separator =
|
|
header.length >= threshold
|
|
? `\n${" ".repeat(threshold)}`
|
|
: " ".repeat(threshold - header.length);
|
|
|
|
const description = content.replace(/\n/g, `\n${" ".repeat(threshold)}`);
|
|
|
|
return `${header}${separator}${description}`;
|
|
}
|
|
|
|
function createOptionUsageType(option) {
|
|
switch (option.type) {
|
|
case "boolean":
|
|
return null;
|
|
case "choice":
|
|
return `<${option.choices
|
|
.filter(choice => choice.since !== null)
|
|
.filter(choice => !choice.deprecated)
|
|
.map(choice => choice.value)
|
|
.join("|")}>`;
|
|
default:
|
|
return `<${option.type}>`;
|
|
}
|
|
}
|
|
|
|
function flattenArray(array) {
|
|
return [].concat.apply([], array);
|
|
}
|
|
|
|
function createChoiceUsages(choices, margin, indentation) {
|
|
const activeChoices = choices.filter(
|
|
choice => !choice.deprecated && choice.since !== null
|
|
);
|
|
const threshold =
|
|
activeChoices
|
|
.map(choice => choice.value.length)
|
|
.reduce((current, length) => Math.max(current, length), 0) + margin;
|
|
return activeChoices.map(choice =>
|
|
indent(
|
|
createOptionUsageRow(choice.value, choice.description, threshold),
|
|
indentation
|
|
)
|
|
);
|
|
}
|
|
|
|
function createDetailedUsage(context, flag) {
|
|
const option = getOptionsWithOpposites(context.detailedOptions).find(
|
|
option => option.name === flag || option.alias === flag
|
|
);
|
|
|
|
const header = createOptionUsageHeader(option);
|
|
const description = `\n\n${indent(option.description, 2)}`;
|
|
|
|
const choices =
|
|
option.type !== "choice"
|
|
? ""
|
|
: `\n\nValid options:\n\n${createChoiceUsages(
|
|
option.choices,
|
|
CHOICE_USAGE_MARGIN,
|
|
CHOICE_USAGE_INDENTATION
|
|
).join("\n")}`;
|
|
|
|
const optionDefaultValue = getOptionDefaultValue(context, option.name);
|
|
const defaults =
|
|
optionDefaultValue !== undefined
|
|
? `\n\nDefault: ${createDefaultValueDisplay(optionDefaultValue)}`
|
|
: "";
|
|
|
|
const pluginDefaults =
|
|
option.pluginDefaults && Object.keys(option.pluginDefaults).length
|
|
? `\nPlugin defaults:${Object.keys(option.pluginDefaults).map(
|
|
key =>
|
|
`\n* ${key}: ${createDefaultValueDisplay(
|
|
option.pluginDefaults[key]
|
|
)}`
|
|
)}`
|
|
: "";
|
|
return `${header}${description}${choices}${defaults}${pluginDefaults}`;
|
|
}
|
|
|
|
function getOptionDefaultValue(context, optionName) {
|
|
// --no-option
|
|
if (!(optionName in context.detailedOptionMap)) {
|
|
return undefined;
|
|
}
|
|
|
|
const option = context.detailedOptionMap[optionName];
|
|
|
|
if (option.default !== undefined) {
|
|
return option.default;
|
|
}
|
|
|
|
const optionCamelName = camelCase(optionName);
|
|
if (optionCamelName in context.apiDefaultOptions) {
|
|
return context.apiDefaultOptions[optionCamelName];
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function indent(str, spaces) {
|
|
return str.replace(/^/gm, " ".repeat(spaces));
|
|
}
|
|
|
|
function groupBy(array, getKey) {
|
|
return array.reduce((obj, item) => {
|
|
const key = getKey(item);
|
|
const previousItems = key in obj ? obj[key] : [];
|
|
return Object.assign({}, obj, { [key]: previousItems.concat(item) });
|
|
}, Object.create(null));
|
|
}
|
|
|
|
function pick(object, keys) {
|
|
return !keys
|
|
? object
|
|
: keys.reduce(
|
|
(reduced, key) => Object.assign(reduced, { [key]: object[key] }),
|
|
{}
|
|
);
|
|
}
|
|
|
|
function createLogger(logLevel) {
|
|
return {
|
|
warn: createLogFunc("warn", "yellow"),
|
|
error: createLogFunc("error", "red"),
|
|
debug: createLogFunc("debug", "blue"),
|
|
log: createLogFunc("log")
|
|
};
|
|
|
|
function createLogFunc(loggerName, color) {
|
|
if (!shouldLog(loggerName)) {
|
|
return () => {};
|
|
}
|
|
|
|
const prefix = color ? `[${chalk[color](loggerName)}] ` : "";
|
|
return function(message, opts) {
|
|
opts = Object.assign({ newline: true }, opts);
|
|
const stream = process[loggerName === "log" ? "stdout" : "stderr"];
|
|
stream.write(message.replace(/^/gm, prefix) + (opts.newline ? "\n" : ""));
|
|
};
|
|
}
|
|
|
|
function shouldLog(loggerName) {
|
|
switch (logLevel) {
|
|
case "silent":
|
|
return false;
|
|
default:
|
|
return true;
|
|
case "debug":
|
|
if (loggerName === "debug") {
|
|
return true;
|
|
}
|
|
// fall through
|
|
case "log":
|
|
if (loggerName === "log") {
|
|
return true;
|
|
}
|
|
// fall through
|
|
case "warn":
|
|
if (loggerName === "warn") {
|
|
return true;
|
|
}
|
|
// fall through
|
|
case "error":
|
|
return loggerName === "error";
|
|
}
|
|
}
|
|
}
|
|
|
|
function normalizeDetailedOption(name, option) {
|
|
return Object.assign({ category: coreOptions.CATEGORY_OTHER }, option, {
|
|
choices:
|
|
option.choices &&
|
|
option.choices.map(choice => {
|
|
const newChoice = Object.assign(
|
|
{ description: "", deprecated: false },
|
|
typeof choice === "object" ? choice : { value: choice }
|
|
);
|
|
if (newChoice.value === true) {
|
|
newChoice.value = ""; // backward compatibility for original boolean option
|
|
}
|
|
return newChoice;
|
|
})
|
|
});
|
|
}
|
|
|
|
function normalizeDetailedOptionMap(detailedOptionMap) {
|
|
return Object.keys(detailedOptionMap)
|
|
.sort()
|
|
.reduce((normalized, name) => {
|
|
const option = detailedOptionMap[name];
|
|
return Object.assign(normalized, {
|
|
[name]: normalizeDetailedOption(name, option)
|
|
});
|
|
}, {});
|
|
}
|
|
|
|
function createMinimistOptions(detailedOptions) {
|
|
return {
|
|
// we use vnopts' AliasSchema to handle aliases for better error messages
|
|
alias: {},
|
|
boolean: detailedOptions
|
|
.filter(option => option.type === "boolean")
|
|
.map(option => [option.name].concat(option.alias || []))
|
|
.reduce((a, b) => a.concat(b)),
|
|
string: detailedOptions
|
|
.filter(option => option.type !== "boolean")
|
|
.map(option => [option.name].concat(option.alias || []))
|
|
.reduce((a, b) => a.concat(b)),
|
|
default: detailedOptions
|
|
.filter(option => !option.deprecated)
|
|
.filter(
|
|
option =>
|
|
!option.forwardToApi ||
|
|
option.name === "plugin" ||
|
|
option.name === "plugin-search-dir"
|
|
)
|
|
.filter(option => option.default !== undefined)
|
|
.reduce(
|
|
(current, option) =>
|
|
Object.assign({ [option.name]: option.default }, current),
|
|
{}
|
|
)
|
|
};
|
|
}
|
|
|
|
function createApiDetailedOptionMap(detailedOptions) {
|
|
return detailedOptions.reduce(
|
|
(current, option) =>
|
|
option.forwardToApi && option.forwardToApi !== option.name
|
|
? Object.assign(current, { [option.forwardToApi]: option })
|
|
: current,
|
|
{}
|
|
);
|
|
}
|
|
|
|
function createDetailedOptionMap(supportOptions) {
|
|
return supportOptions.reduce((reduced, option) => {
|
|
const newOption = Object.assign({}, option, {
|
|
name: option.cliName || dashify(option.name),
|
|
description: option.cliDescription || option.description,
|
|
category: option.cliCategory || coreOptions.CATEGORY_FORMAT,
|
|
forwardToApi: option.name
|
|
});
|
|
|
|
if (option.deprecated) {
|
|
delete newOption.forwardToApi;
|
|
delete newOption.description;
|
|
delete newOption.oppositeDescription;
|
|
newOption.deprecated = true;
|
|
}
|
|
|
|
return Object.assign(reduced, { [newOption.name]: newOption });
|
|
}, {});
|
|
}
|
|
|
|
//-----------------------------context-util-start-------------------------------
|
|
/**
|
|
* @typedef {Object} Context
|
|
* @property logger
|
|
* @property args
|
|
* @property argv
|
|
* @property filePatterns
|
|
* @property supportOptions
|
|
* @property detailedOptions
|
|
* @property detailedOptionMap
|
|
* @property apiDefaultOptions
|
|
*/
|
|
function createContext(args) {
|
|
const context = { args };
|
|
|
|
updateContextArgv(context);
|
|
normalizeContextArgv(context, ["loglevel", "plugin", "plugin-search-dir"]);
|
|
|
|
context.logger = createLogger(context.argv["loglevel"]);
|
|
|
|
updateContextArgv(
|
|
context,
|
|
context.argv["plugin"],
|
|
context.argv["plugin-search-dir"]
|
|
);
|
|
|
|
return context;
|
|
}
|
|
|
|
function initContext(context) {
|
|
// split into 2 step so that we could wrap this in a `try..catch` in cli/index.js
|
|
normalizeContextArgv(context);
|
|
}
|
|
|
|
function updateContextOptions(context, plugins, pluginSearchDirs) {
|
|
const supportOptions = prettier.getSupportInfo(null, {
|
|
showDeprecated: true,
|
|
showUnreleased: true,
|
|
showInternal: true,
|
|
plugins,
|
|
pluginSearchDirs
|
|
}).options;
|
|
|
|
const detailedOptionMap = normalizeDetailedOptionMap(
|
|
Object.assign({}, createDetailedOptionMap(supportOptions), constant.options)
|
|
);
|
|
|
|
const detailedOptions = arrayify(detailedOptionMap, "name");
|
|
|
|
const apiDefaultOptions = supportOptions
|
|
.filter(optionInfo => !optionInfo.deprecated)
|
|
.reduce(
|
|
(reduced, optionInfo) =>
|
|
Object.assign(reduced, { [optionInfo.name]: optionInfo.default }),
|
|
Object.assign({}, optionsModule.hiddenDefaults)
|
|
);
|
|
|
|
context.supportOptions = supportOptions;
|
|
context.detailedOptions = detailedOptions;
|
|
context.detailedOptionMap = detailedOptionMap;
|
|
context.apiDefaultOptions = apiDefaultOptions;
|
|
}
|
|
|
|
function pushContextPlugins(context, plugins, pluginSearchDirs) {
|
|
context._supportOptions = context.supportOptions;
|
|
context._detailedOptions = context.detailedOptions;
|
|
context._detailedOptionMap = context.detailedOptionMap;
|
|
context._apiDefaultOptions = context.apiDefaultOptions;
|
|
updateContextOptions(context, plugins, pluginSearchDirs);
|
|
}
|
|
|
|
function popContextPlugins(context) {
|
|
context.supportOptions = context._supportOptions;
|
|
context.detailedOptions = context._detailedOptions;
|
|
context.detailedOptionMap = context._detailedOptionMap;
|
|
context.apiDefaultOptions = context._apiDefaultOptions;
|
|
}
|
|
|
|
function updateContextArgv(context, plugins, pluginSearchDirs) {
|
|
pushContextPlugins(context, plugins, pluginSearchDirs);
|
|
|
|
const minimistOptions = createMinimistOptions(context.detailedOptions);
|
|
const argv = minimist(context.args, minimistOptions);
|
|
|
|
context.argv = argv;
|
|
context.filePatterns = argv["_"];
|
|
}
|
|
|
|
function normalizeContextArgv(context, keys) {
|
|
const detailedOptions = !keys
|
|
? context.detailedOptions
|
|
: context.detailedOptions.filter(
|
|
option => keys.indexOf(option.name) !== -1
|
|
);
|
|
const argv = !keys ? context.argv : pick(context.argv, keys);
|
|
|
|
context.argv = optionsNormalizer.normalizeCliOptions(argv, detailedOptions, {
|
|
logger: context.logger
|
|
});
|
|
}
|
|
//------------------------------context-util-end--------------------------------
|
|
|
|
module.exports = {
|
|
createContext,
|
|
createDetailedOptionMap,
|
|
createDetailedUsage,
|
|
createUsage,
|
|
format,
|
|
formatFiles,
|
|
formatStdin,
|
|
initContext,
|
|
logResolvedConfigPathOrDie,
|
|
logFileInfoOrDie,
|
|
normalizeDetailedOptionMap
|
|
};
|