Implement prettier.getFileInfo() method and --file-info CLI option (#4341)

* Implement prettier.getFileInfo() method and --file-info CLI option

* Add empty line between functions in index.js

* Support --plugin-search-dirs / pluginSearchDirs() in --file-info / getFileInfo()

* Address review comments by @ikatyang
master
Alexander Kachkaev 2018-05-09 17:53:44 +01:00 committed by Ika
parent ee96e97c77
commit cc734753fc
13 changed files with 538 additions and 27 deletions

View File

@ -72,6 +72,23 @@ If `options.useCache` is `false`, all caching will be bypassed.
As you repeatedly call `resolveConfig`, the file system structure will be cached for performance. This function will clear the cache. Generally this is only needed for editor integrations that know that the file system has changed since the last format took place.
## `prettier.getFileInfo(filePath [, options])`
`getFileInfo` can be used by editor extensions to decide if a particular file needs to be formatted. This method returns a promise, which resolves to an object with the following properties:
```typescript
{
ignored: boolean,
inferredParser: string | null,
}
```
Setting `options.ignorePath` (`string`) and `options.withNodeModules` (`boolean`) influence the value of `ignored` (`false` by default).
Providing [plugin](./plugins.md) paths in `options.plugins` (`string[]`) helps extract `inferredParser` for files that are not supported by Prettier core.
Use `prettier.getFileInfo.sync(filePath [, options])` if you'd like to use sync version.
## `prettier.getSupportInfo([version])`
Returns an object representing the parsers, languages and file types Prettier supports.

View File

@ -4,6 +4,7 @@ const version = require("./package.json").version;
const core = require("./src/main/core");
const getSupportInfo = require("./src/main/support").getSupportInfo;
const getFileInfo = require("./src/common/get-file-info");
const sharedUtil = require("./src/common/util-shared");
const loadPlugins = require("./src/common/load-plugins");
@ -12,7 +13,7 @@ const config = require("./src/config/resolve-config");
const doc = require("./src/doc");
// Luckily `opts` is always the 2nd argument
function withPlugins(fn) {
function _withPlugins(fn) {
return function() {
const args = Array.from(arguments);
const opts = args[1] || {};
@ -23,6 +24,14 @@ function withPlugins(fn) {
};
}
function withPlugins(fn) {
const resultingFn = _withPlugins(fn);
if (fn.sync) {
resultingFn.sync = _withPlugins(fn.sync);
}
return resultingFn;
}
const formatWithCursor = withPlugins(core.formatWithCursor);
module.exports = {
@ -47,6 +56,7 @@ module.exports = {
resolveConfigFile: config.resolveConfigFile,
clearConfigCache: config.clearCache,
getFileInfo: withPlugins(getFileInfo),
getSupportInfo: withPlugins(getSupportInfo),
version,

View File

@ -131,6 +131,14 @@ const options = {
description:
"Find and print the path to a configuration file for the given input file."
},
"file-info": {
type: "path",
description: dedent`
Extract the following info (as JSON) for a given file path. Reported fields:
* ignored (boolean) - true if file path is filtered by --ignore-path
* inferredParser (string | null) - name of parser inferred from file path
`
},
help: {
type: "flag",
alias: "h",

View File

@ -22,6 +22,11 @@ function run(args) {
process.exit(1);
}
if (context.argv["file-info"] && context.filePatterns.length) {
context.logger.error("Cannot use --file-info with multiple files");
process.exit(1);
}
if (context.argv["version"]) {
context.logger.log(prettier.version);
process.exit(0);
@ -50,10 +55,9 @@ function run(args) {
context.argv["stdin"] || (!hasFilePatterns && !process.stdin.isTTY);
if (context.argv["find-config-path"]) {
util.logResolvedConfigPathOrDie(
context,
context.argv["find-config-path"]
);
util.logResolvedConfigPathOrDie(context);
} else if (context.argv["file-info"]) {
util.logFileInfoOrDie(context);
} else if (useStdin) {
util.formatStdin(context);
} else if (hasFilePatterns) {

View File

@ -5,13 +5,14 @@ const camelCase = require("camelcase");
const dashify = require("dashify");
const fs = require("fs");
const globby = require("globby");
const ignore = require("ignore");
const chalk = require("chalk");
const readline = require("readline");
const leven = require("leven");
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");
@ -76,8 +77,10 @@ function handleError(context, filename, error) {
process.exitCode = 2;
}
function logResolvedConfigPathOrDie(context, filePath) {
const configFile = prettier.resolveConfigFile.sync(filePath);
function logResolvedConfigPathOrDie(context) {
const configFile = prettier.resolveConfigFile.sync(
context.argv["find-config-path"]
);
if (configFile) {
context.logger.log(path.relative(process.cwd(), configFile));
} else {
@ -85,6 +88,23 @@ function logResolvedConfigPathOrDie(context, filePath) {
}
}
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(result, options) {
// Don't use `console.log` here since it adds an extra newline at the end.
process.stdout.write(result.formatted);
@ -255,7 +275,7 @@ function formatStdin(context) {
? path.resolve(process.cwd(), context.argv["stdin-filepath"])
: process.cwd();
const ignorer = createIgnorer(context);
const ignorer = createIgnorerFromContextOrDie(context);
const relativeFilepath = path.relative(process.cwd(), filepath);
thirdParty.getStream(process.stdin).then(input => {
@ -278,22 +298,16 @@ function formatStdin(context) {
});
}
function createIgnorer(context) {
const ignoreFilePath = path.resolve(context.argv["ignore-path"]);
let ignoreText = "";
function createIgnorerFromContextOrDie(context) {
try {
ignoreText = fs.readFileSync(ignoreFilePath, "utf8");
} catch (readError) {
if (readError.code !== "ENOENT") {
context.logger.error(
`Unable to read ${ignoreFilePath}: ` + readError.message
);
process.exit(2);
}
return createIgnorer(
context.argv["ignore-path"],
context.argv["with-node-modules"]
);
} catch (e) {
context.logger.error(e.message);
process.exit(2);
}
return ignore().add(ignoreText);
}
function eachFilename(context, patterns, callback) {
@ -329,7 +343,7 @@ function eachFilename(context, patterns, callback) {
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 = createIgnorer(context);
const ignorer = createIgnorerFromContextOrDie(context);
eachFilename(context, context.filePatterns, (filename, options) => {
const fileIgnored = ignorer.filter([filename]).length === 0;
@ -910,5 +924,6 @@ module.exports = {
formatStdin,
initContext,
logResolvedConfigPathOrDie,
logFileInfoOrDie,
normalizeDetailedOptionMap
};

View File

@ -0,0 +1,30 @@
"use strict";
const ignore = require("ignore");
const fs = require("fs");
const path = require("path");
function createIgnorer(ignorePath, withNodeModules) {
let ignoreText = "";
if (ignorePath) {
const resolvedIgnorePath = path.resolve(ignorePath);
try {
ignoreText = fs.readFileSync(resolvedIgnorePath, "utf8");
} catch (readError) {
if (readError.code !== "ENOENT") {
throw new Error(
`Unable to read ${resolvedIgnorePath}: ${readError.message}`
);
}
}
}
const ignorer = ignore().add(ignoreText);
if (!withNodeModules) {
ignorer.add("node_modules");
}
return ignorer;
}
module.exports = createIgnorer;

View File

@ -0,0 +1,40 @@
"use strict";
const createIgnorer = require("./create-ignorer");
const options = require("../main/options");
/**
* @param {string} filePath
* @param {{ ignorePath?: string, withNodeModules?: boolean, plugins: object }} opts
*
* Please note that prettier.getFileInfo() expects opts.plugins to be an array of paths,
* not an object. A transformation from this array to an object is automatically done
* internally by the method wrapper. See withPlugins() in index.js.
*/
function _getFileInfo(filePath, opts) {
let ignored = false;
const ignorer = createIgnorer(opts.ignorePath, opts.withNodeModules);
ignored = ignorer.ignores(filePath);
const inferredParser = options.inferParser(filePath, opts.plugins) || null;
return {
ignored,
inferredParser
};
}
// the method has been implemented as asynchronous to avoid possible breaking changes in future
function getFileInfo(filePath, opts) {
return new Promise((resolve, reject) => {
try {
resolve(_getFileInfo(filePath, opts));
} catch (e) {
reject(e);
}
});
}
getFileInfo.sync = _getFileInfo;
module.exports = getFileInfo;

View File

@ -118,7 +118,7 @@ function inferParser(filepath, plugins) {
}).languages.find(
language =>
language.since !== null &&
(language.extensions.indexOf(extension) > -1 ||
((language.extensions && language.extensions.indexOf(extension) > -1) ||
(language.filenames &&
language.filenames.find(name => name.toLowerCase() === filename)))
);
@ -126,4 +126,4 @@ function inferParser(filepath, plugins) {
return language && language.parsers[0];
}
module.exports = { normalize, hiddenDefaults };
module.exports = { normalize, hiddenDefaults, inferParser };

View File

@ -102,6 +102,19 @@ Default: true
exports[`show detailed usage with --help editorconfig (write) 1`] = `Array []`;
exports[`show detailed usage with --help file-info (stderr) 1`] = `""`;
exports[`show detailed usage with --help file-info (stdout) 1`] = `
"--file-info <path>
Extract the following info (as JSON) for a given file path. Reported fields:
* ignored (boolean) - true if file path is filtered by --ignore-path
* inferredParser (string | null) - name of parser inferred from file path
"
`;
exports[`show detailed usage with --help file-info (write) 1`] = `Array []`;
exports[`show detailed usage with --help find-config-path (stderr) 1`] = `""`;
exports[`show detailed usage with --help find-config-path (stdout) 1`] = `
@ -638,6 +651,9 @@ Editor options:
Other options:
--no-color Do not colorize error messages.
--file-info <path> Extract the following info (as JSON) for a given file path. Reported fields:
* ignored (boolean) - true if file path is filtered by --ignore-path
* inferredParser (string | null) - name of parser inferred from file path
-h, --help <flag> Show CLI usage, or details about the given flag.
Example: --help write
--insert-pragma Insert @format pragma into file's first docblock comment.
@ -786,6 +802,9 @@ Editor options:
Other options:
--no-color Do not colorize error messages.
--file-info <path> Extract the following info (as JSON) for a given file path. Reported fields:
* ignored (boolean) - true if file path is filtered by --ignore-path
* inferredParser (string | null) - name of parser inferred from file path
-h, --help <flag> Show CLI usage, or details about the given flag.
Example: --help write
--insert-pragma Insert @format pragma into file's first docblock comment.
@ -807,6 +826,15 @@ Other options:
exports[`throw error and show usage with something unexpected (write) 1`] = `Array []`;
exports[`throw error with --file-info + multiple files (stderr) 1`] = `
"[error] Cannot use --file-info with multiple files
"
`;
exports[`throw error with --file-info + multiple files (stdout) 1`] = `""`;
exports[`throw error with --file-info + multiple files (write) 1`] = `Array []`;
exports[`throw error with --find-config-path + multiple files (stderr) 1`] = `
"[error] Cannot use --find-config-path with multiple files
"

View File

@ -0,0 +1,121 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`extracts file-info for a file in not_node_modules (stderr) 1`] = `""`;
exports[`extracts file-info for a file in not_node_modules (stdout) 1`] = `
"{ \\"ignored\\": false, \\"inferredParser\\": \\"babylon\\" }
"
`;
exports[`extracts file-info for a file in not_node_modules (write) 1`] = `Array []`;
exports[`extracts file-info for a js file (stderr) 1`] = `""`;
exports[`extracts file-info for a js file (stdout) 1`] = `
"{ \\"ignored\\": false, \\"inferredParser\\": \\"babylon\\" }
"
`;
exports[`extracts file-info for a js file (write) 1`] = `Array []`;
exports[`extracts file-info for a known markdown file with no extension (stderr) 1`] = `""`;
exports[`extracts file-info for a known markdown file with no extension (stdout) 1`] = `
"{ \\"ignored\\": false, \\"inferredParser\\": \\"markdown\\" }
"
`;
exports[`extracts file-info for a known markdown file with no extension (write) 1`] = `Array []`;
exports[`extracts file-info for a markdown file (stderr) 1`] = `""`;
exports[`extracts file-info for a markdown file (stdout) 1`] = `
"{ \\"ignored\\": false, \\"inferredParser\\": \\"markdown\\" }
"
`;
exports[`extracts file-info for a markdown file (write) 1`] = `Array []`;
exports[`extracts file-info with ignored=false for a file in node_modules when --with-node-modules provided (stderr) 1`] = `""`;
exports[`extracts file-info with ignored=false for a file in node_modules when --with-node-modules provided (stdout) 1`] = `
"{ \\"ignored\\": false, \\"inferredParser\\": \\"babylon\\" }
"
`;
exports[`extracts file-info with ignored=false for a file in node_modules when --with-node-modules provided (write) 1`] = `Array []`;
exports[`extracts file-info with ignored=true for a file in .prettierignore (stderr) 1`] = `""`;
exports[`extracts file-info with ignored=true for a file in .prettierignore (stdout) 1`] = `
"{ \\"ignored\\": true, \\"inferredParser\\": \\"babylon\\" }
"
`;
exports[`extracts file-info with ignored=true for a file in .prettierignore (write) 1`] = `Array []`;
exports[`extracts file-info with ignored=true for a file in a hand-picked .prettierignore (stderr) 1`] = `""`;
exports[`extracts file-info with ignored=true for a file in a hand-picked .prettierignore (stdout) 1`] = `
"{ \\"ignored\\": true, \\"inferredParser\\": \\"babylon\\" }
"
`;
exports[`extracts file-info with ignored=true for a file in a hand-picked .prettierignore (write) 1`] = `Array []`;
exports[`extracts file-info with inferredParser=foo when a plugin is hand-picked (stderr) 1`] = `""`;
exports[`extracts file-info with inferredParser=foo when a plugin is hand-picked (stdout) 1`] = `
"{ \\"ignored\\": false, \\"inferredParser\\": \\"foo\\" }
"
`;
exports[`extracts file-info with inferredParser=foo when a plugin is hand-picked (write) 1`] = `Array []`;
exports[`extracts file-info with inferredParser=foo when plugins are autoloaded (stderr) 1`] = `""`;
exports[`extracts file-info with inferredParser=foo when plugins are autoloaded (stdout) 1`] = `
"{ \\"ignored\\": false, \\"inferredParser\\": \\"foo\\" }
"
`;
exports[`extracts file-info with inferredParser=foo when plugins are autoloaded (write) 1`] = `Array []`;
exports[`extracts file-info with inferredParser=foo when plugins are loaded with --plugin-search-dir (stderr) 1`] = `""`;
exports[`extracts file-info with inferredParser=foo when plugins are loaded with --plugin-search-dir (stdout) 1`] = `
"{ \\"ignored\\": false, \\"inferredParser\\": \\"foo\\" }
"
`;
exports[`extracts file-info with inferredParser=foo when plugins are loaded with --plugin-search-dir (write) 1`] = `Array []`;
exports[`extracts file-info with inferredParser=null for file.foo (stderr) 1`] = `""`;
exports[`extracts file-info with inferredParser=null for file.foo (stdout) 1`] = `
"{ \\"ignored\\": false, \\"inferredParser\\": null }
"
`;
exports[`extracts file-info with inferredParser=null for file.foo (write) 1`] = `Array []`;
exports[`extracts file-info with with ignored=true for a file in node_modules (stderr) 1`] = `""`;
exports[`extracts file-info with with ignored=true for a file in node_modules (stdout) 1`] = `
"{ \\"ignored\\": true, \\"inferredParser\\": \\"babylon\\" }
"
`;
exports[`extracts file-info with with ignored=true for a file in node_modules (write) 1`] = `Array []`;

View File

@ -99,6 +99,12 @@ describe("throw error with --find-config-path + multiple files", () => {
});
});
describe("throw error with --file-info + multiple files", () => {
runPrettier("cli", ["--file-info", "abc.js", "def.js"]).test({
status: 1
});
});
describe("throw error and show usage with something unexpected", () => {
runPrettier("cli", [], { isTTY: true }).test({
status: "non-zero"

View File

@ -0,0 +1,231 @@
"use strict";
const path = require("path");
const runPrettier = require("../runPrettier");
const prettier = require("../../tests_config/require_prettier");
expect.addSnapshotSerializer(require("../path-serializer"));
describe("extracts file-info for a js file", () => {
runPrettier("cli/", ["--file-info", "something.js"]).test({
status: 0
});
});
describe("extracts file-info for a markdown file", () => {
runPrettier("cli/", ["--file-info", "README.md"]).test({
status: 0
});
});
describe("extracts file-info for a known markdown file with no extension", () => {
runPrettier("cli/", ["--file-info", "README"]).test({
status: 0
});
});
describe("extracts file-info with ignored=true for a file in .prettierignore", () => {
runPrettier("cli/ignore-path/", ["--file-info", "regular-module.js"]).test({
status: 0
});
});
describe("extracts file-info with ignored=true for a file in a hand-picked .prettierignore", () => {
runPrettier("cli/", [
"--file-info",
"regular-module.js",
"--ignore-path=ignore-path/.prettierignore"
]).test({
status: 0
});
});
describe("extracts file-info for a file in not_node_modules", () => {
runPrettier("cli/with-node-modules/", [
"--file-info",
"not_node_modules/file.js"
]).test({
status: 0
});
});
describe("extracts file-info with with ignored=true for a file in node_modules", () => {
runPrettier("cli/with-node-modules/", [
"--file-info",
"node_modules/file.js"
]).test({
status: 0
});
});
describe("extracts file-info with ignored=false for a file in node_modules when --with-node-modules provided", () => {
runPrettier("cli/with-node-modules/", [
"--file-info",
"node_modules/file.js",
"--with-node-modules"
]).test({
status: 0
});
});
describe("extracts file-info with inferredParser=null for file.foo", () => {
runPrettier("cli/", ["--file-info", "file.foo"]).test({
status: 0
});
});
describe("extracts file-info with inferredParser=foo when plugins are autoloaded", () => {
runPrettier("plugins/automatic/", ["--file-info", "file.foo"]).test({
status: 0
});
});
describe("extracts file-info with inferredParser=foo when plugins are loaded with --plugin-search-dir", () => {
runPrettier("cli/", [
"--file-info",
"file.foo",
"--plugin-search-dir",
"../plugins/automatic"
]).test({
status: 0
});
});
describe("extracts file-info with inferredParser=foo when a plugin is hand-picked", () => {
runPrettier("cli/", [
"--file-info",
"file.foo",
"--plugin",
"../plugins/automatic/node_modules/@prettier/plugin-foo"
]).test({
status: 0
});
});
test("API getFileInfo with no args", () => {
expect(prettier.getFileInfo()).rejects.toThrow();
});
test("API getFileInfo.sync with no args", () => {
expect(() => prettier.getFileInfo.sync()).toThrow();
});
test("API getFileInfo with filepath only", () => {
expect(prettier.getFileInfo("README")).resolves.toMatchObject({
ignored: false,
inferredParser: "markdown"
});
});
test("API getFileInfo.sync with filepath only", () => {
expect(prettier.getFileInfo.sync("README")).toMatchObject({
ignored: false,
inferredParser: "markdown"
});
});
test("API getFileInfo with ignorePath", () => {
const file = path.resolve(
path.join(__dirname, "../cli/ignore-path/regular-module.js")
);
const ignorePath = path.resolve(
path.join(__dirname, "../cli/ignore-path/.prettierignore")
);
expect(prettier.getFileInfo(file)).resolves.toMatchObject({
ignored: false,
inferredParser: "babylon"
});
expect(
prettier.getFileInfo(file, {
ignorePath
})
).resolves.toMatchObject({
ignored: true,
inferredParser: "babylon"
});
});
test("API getFileInfo.sync with ignorePath", () => {
const file = path.resolve(
path.join(__dirname, "../cli/ignore-path/regular-module.js")
);
const ignorePath = path.resolve(
path.join(__dirname, "../cli/ignore-path/.prettierignore")
);
expect(prettier.getFileInfo.sync(file)).toMatchObject({
ignored: false,
inferredParser: "babylon"
});
expect(
prettier.getFileInfo.sync(file, {
ignorePath
})
).toMatchObject({
ignored: true,
inferredParser: "babylon"
});
});
test("API getFileInfo with withNodeModules", () => {
const file = path.resolve(
path.join(__dirname, "../cli/with-node-modules/node_modules/file.js")
);
expect(prettier.getFileInfo(file)).resolves.toMatchObject({
ignored: true,
inferredParser: "babylon"
});
expect(
prettier.getFileInfo(file, {
withNodeModules: true
})
).resolves.toMatchObject({
ignored: false,
inferredParser: "babylon"
});
});
test("API getFileInfo with plugins loaded using pluginSearchDir", () => {
const file = "file.foo";
const pluginsPath = path.resolve(
path.join(__dirname, "../plugins/automatic")
);
expect(prettier.getFileInfo(file)).resolves.toMatchObject({
ignored: false,
inferredParser: null
});
expect(
prettier.getFileInfo(file, {
pluginSearchDirs: [pluginsPath]
})
).resolves.toMatchObject({
ignored: false,
inferredParser: "foo"
});
});
test("API getFileInfo with hand-picked plugins", () => {
const file = "file.foo";
const pluginPath = path.resolve(
path.join(
__dirname,
"../plugins/automatic/node_modules/@prettier/plugin-foo"
)
);
expect(prettier.getFileInfo(file)).resolves.toMatchObject({
ignored: false,
inferredParser: null
});
expect(
prettier.getFileInfo(file, {
plugins: [pluginPath]
})
).resolves.toMatchObject({
ignored: false,
inferredParser: "foo"
});
});

View File

@ -6,7 +6,8 @@ module.exports = {
languages: [
{
name: "foo",
parsers: ["foo"]
parsers: ["foo"],
extensions: [".foo"]
}
],
parsers: {