prettierx
Version:
prettierX - a less opinionated fork of the Prettier code formatter
198 lines (172 loc) • 5.45 kB
JavaScript
;
const fs = require("fs");
const path = require("path");
const readlines = require("n-readlines");
const { UndefinedParserError } = require("../common/errors");
const { getSupportInfo } = require("../main/support");
const normalizer = require("./options-normalizer");
const { resolveParser } = require("./parser");
const hiddenDefaults = {
astFormat: "estree",
printer: {},
originalText: undefined,
locStart: null,
locEnd: null,
};
// Copy options and fill in default values.
function normalize(options, opts = {}) {
const rawOptions = { ...options };
const supportOptions = getSupportInfo({
plugins: options.plugins,
showUnreleased: true,
showDeprecated: true,
}).options;
const defaults = {
...hiddenDefaults,
...Object.fromEntries(
supportOptions
.filter((optionInfo) => optionInfo.default !== undefined)
.map((option) => [option.name, option.default])
),
};
if (!rawOptions.parser) {
if (!rawOptions.filepath) {
const logger = opts.logger || console;
logger.warn(
"No parser and no filepath given, using 'babel' the parser now " +
"but this will throw an error in the future. " +
"Please specify a parser or a filepath so one can be inferred."
);
rawOptions.parser = "babel";
} else {
rawOptions.parser = inferParser(rawOptions.filepath, rawOptions.plugins);
if (!rawOptions.parser) {
throw new UndefinedParserError(
`No parser could be inferred for file: ${rawOptions.filepath}`
);
}
}
}
const parser = resolveParser(
normalizer.normalizeApiOptions(
rawOptions,
[supportOptions.find((x) => x.name === "parser")],
{ passThrough: true, logger: false }
)
);
rawOptions.astFormat = parser.astFormat;
rawOptions.locEnd = parser.locEnd;
rawOptions.locStart = parser.locStart;
const plugin = getPlugin(rawOptions);
rawOptions.printer = plugin.printers[rawOptions.astFormat];
const pluginDefaults = Object.fromEntries(
supportOptions
.filter(
(optionInfo) =>
optionInfo.pluginDefaults &&
optionInfo.pluginDefaults[plugin.name] !== undefined
)
.map((optionInfo) => [
optionInfo.name,
optionInfo.pluginDefaults[plugin.name],
])
);
const mixedDefaults = { ...defaults, ...pluginDefaults };
for (const [k, value] of Object.entries(mixedDefaults)) {
if (rawOptions[k] === null || rawOptions[k] === undefined) {
rawOptions[k] = value;
}
}
if (rawOptions.parser === "json") {
rawOptions.trailingComma = "none";
}
return normalizer.normalizeApiOptions(rawOptions, supportOptions, {
passThrough: Object.keys(hiddenDefaults),
...opts,
});
}
function getPlugin(options) {
const { astFormat } = options;
// TODO: test this with plugins
/* istanbul ignore next */
if (!astFormat) {
throw new Error("getPlugin() requires astFormat to be set");
}
const printerPlugin = options.plugins.find(
(plugin) => plugin.printers && plugin.printers[astFormat]
);
// TODO: test this with plugins
/* istanbul ignore next */
if (!printerPlugin) {
throw new Error(`Couldn't find plugin for AST format "${astFormat}"`);
}
return printerPlugin;
}
function getInterpreter(filepath) {
/* istanbul ignore next */
if (typeof filepath !== "string") {
return "";
}
let fd;
try {
fd = fs.openSync(filepath, "r");
} catch {
// istanbul ignore next
return "";
}
try {
const liner = new readlines(fd);
const firstLine = liner.next().toString("utf8");
// #!/bin/env node, #!/usr/bin/env node
const m1 = firstLine.match(/^#!\/(?:usr\/)?bin\/env\s+(\S+)/);
if (m1) {
return m1[1];
}
// #!/bin/node, #!/usr/bin/node, #!/usr/local/bin/node
const m2 = firstLine.match(/^#!\/(?:usr\/(?:local\/)?)?bin\/(\S+)/);
if (m2) {
return m2[1];
}
return "";
} catch {
// There are some weird cases where paths are missing, causing Jest
// failures. It's unclear what these correspond to in the real world.
/* istanbul ignore next */
return "";
} finally {
try {
// There are some weird cases where paths are missing, causing Jest
// failures. It's unclear what these correspond to in the real world.
fs.closeSync(fd);
} catch {
// nop
}
}
}
function inferParser(filepath, plugins) {
const filename = path.basename(filepath).toLowerCase();
const languages = getSupportInfo({ plugins }).languages.filter(
(language) => language.since !== null
);
// If the file has no extension, we can try to infer the language from the
// interpreter in the shebang line, if any; but since this requires FS access,
// do it last.
let language = languages.find(
(language) =>
(language.extensions &&
language.extensions.some((extension) =>
filename.endsWith(extension)
)) ||
(language.filenames &&
language.filenames.some((name) => name.toLowerCase() === filename))
);
if (!language && !filename.includes(".")) {
const interpreter = getInterpreter(filepath);
language = languages.find(
(language) =>
language.interpreters && language.interpreters.includes(interpreter)
);
}
return language && language.parsers[0];
}
module.exports = { normalize, hiddenDefaults, inferParser };