prettierx
Version:
prettierX - a less opinionated fork of the Prettier code formatter
436 lines (380 loc) • 12.8 kB
JavaScript
;
const { promises: fs } = require("fs");
const path = require("path");
const chalk = require("chalk");
// eslint-disable-next-line no-restricted-modules
const prettier = require("../index");
// eslint-disable-next-line no-restricted-modules
const { getStdin } = require("../common/third-party");
const { createIgnorer, errors } = require("./prettier-internal");
const { expandPatterns, fixWindowsSlashes } = require("./expand-patterns");
const { getOptionsForFile } = require("./option");
const isTTY = require("./is-tty");
function diff(a, b) {
return require("diff").createTwoFilesPatch("", "", a, b, "", "", {
context: 2,
});
}
function handleError(context, filename, error, printedFilename) {
if (error instanceof errors.UndefinedParserError) {
// Can't test on CI, `isTTY()` is always false, see ./is-tty.js
/* istanbul ignore next */
if (
(context.argv.write || context.argv["ignore-unknown"]) &&
printedFilename
) {
printedFilename.clear();
}
if (context.argv["ignore-unknown"]) {
return;
}
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 = /^Invalid \S+ value\./.test(error && error.message);
if (isParseError) {
// `invalid.js: SyntaxError: Unexpected token (1:1)`.
context.logger.error(`${filename}: ${String(error)}`);
} else if (isValidationError || error instanceof errors.ConfigError) {
// `Invalid printWidth value. Expected an integer, but received 0.5.`
context.logger.error(error.message);
// If validation fails for one file, it will fail for all of them.
process.exit(1);
} else if (error instanceof errors.DebugError) {
// `invalid.js: Some debug error message`
context.logger.error(`${filename}: ${error.message}`);
} else {
// `invalid.js: Error: Some unexpected error\n[stack trace]`
/* istanbul ignore next */
context.logger.error(filename + ": " + (error.stack || error));
}
// Don't exit the process if one file failed
process.exitCode = 2;
}
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) + "\n" };
}
if (context.argv["debug-print-comments"]) {
return {
formatted: prettier.format(
JSON.stringify(prettier.formatWithCursor(input, opt).comments || []),
{ parser: "json" }
),
};
}
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
);
/* istanbul ignore next */
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 next */
if (context.argv["debug-benchmark"]) {
let benchmark;
try {
// eslint-disable-next-line import/no-extraneous-dependencies
benchmark = require("benchmark");
} catch {
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."
);
let totalMs = 0;
for (let i = 0; i < repeat; ++i) {
// should be using `performance.now()`, but only `Date` is cross-platform enough
const startMs = Date.now();
prettier.formatWithCursor(input, opt);
totalMs += Date.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);
}
async function createIgnorerFromContextOrDie(context) {
try {
return await createIgnorer(
context.argv["ignore-path"],
context.argv["with-node-modules"]
);
} catch (e) {
context.logger.error(e.message);
process.exit(2);
}
}
async function formatStdin(context) {
const filepath = context.argv["stdin-filepath"]
? path.resolve(process.cwd(), context.argv["stdin-filepath"])
: process.cwd();
const ignorer = await createIgnorerFromContextOrDie(context);
// If there's an ignore-path set, the filename must be relative to the
// ignore path, not the current working directory.
const relativeFilepath = context.argv["ignore-path"]
? path.relative(path.dirname(context.argv["ignore-path"]), filepath)
: path.relative(process.cwd(), filepath);
try {
const input = await getStdin();
if (
relativeFilepath &&
ignorer.ignores(fixWindowsSlashes(relativeFilepath))
) {
writeOutput(context, { formatted: input });
return;
}
const options = await getOptionsForFile(context, filepath);
if (listDifferent(context, input, options, "(stdin)")) {
return;
}
writeOutput(context, format(context, input, options), options);
} catch (error) {
handleError(context, relativeFilepath || "stdin", error);
}
}
async 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 = await createIgnorerFromContextOrDie(context);
let numberOfUnformattedFilesFound = 0;
if (context.argv.check) {
context.logger.log("Checking formatting...");
}
for await (const pathOrError of expandPatterns(context)) {
if (typeof pathOrError === "object") {
context.logger.error(pathOrError.error);
// Don't exit, but set the exit code to 2
process.exitCode = 2;
continue;
}
const filename = pathOrError;
// If there's an ignore-path set, the filename must be relative to the
// ignore path, not the current working directory.
const ignoreFilename = context.argv["ignore-path"]
? path.relative(path.dirname(context.argv["ignore-path"]), filename)
: filename;
const fileIgnored = ignorer.ignores(fixWindowsSlashes(ignoreFilename));
if (
fileIgnored &&
(context.argv["debug-check"] ||
context.argv.write ||
context.argv.check ||
context.argv["list-different"])
) {
continue;
}
const options = {
...(await getOptionsForFile(context, filename)),
filepath: filename,
};
let printedFilename;
if (isTTY()) {
printedFilename = context.logger.log(filename, {
newline: false,
clearable: true,
});
}
let input;
try {
input = await fs.readFile(filename, "utf8");
} catch (error) {
// Add newline to split errors from filename line.
/* istanbul ignore next */
context.logger.log("");
/* istanbul ignore next */
context.logger.error(
`Unable to read file: ${filename}\n${error.message}`
);
// Don't exit the process if one file failed
/* istanbul ignore next */
process.exitCode = 2;
/* istanbul ignore next */
continue;
}
if (fileIgnored) {
writeOutput(context, { formatted: input }, options);
continue;
}
const start = Date.now();
let result;
let output;
try {
result = format(context, input, options);
output = result.formatted;
} catch (error) {
handleError(context, filename, error, printedFilename);
continue;
}
const isDifferent = output !== input;
if (printedFilename) {
// Remove previously printed filename to log it with duration.
printedFilename.clear();
}
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 {
await fs.writeFile(filename, output, "utf8");
} catch (error) {
/* istanbul ignore next */
context.logger.error(
`Unable to write file: ${filename}\n${error.message}`
);
// Don't exit the process if one file failed
/* istanbul ignore next */
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"]) {
/* istanbul ignore else */
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 (isDifferent) {
if (context.argv.check) {
context.logger.warn(filename);
} else if (context.argv["list-different"]) {
context.logger.log(filename);
}
numberOfUnformattedFilesFound += 1;
}
}
// Print check summary based on expected exit code
if (context.argv.check) {
if (numberOfUnformattedFilesFound === 0) {
context.logger.log("All matched files use Prettier code style!");
} else {
context.logger.warn(
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;
}
}
module.exports = { format, formatStdin, formatFiles };