Fix plugin API in globally installed Prettier and introduce optional --plugin-search-dir (#4192)

* Fix plugin API in globally installed Prettier and introduce optional --plugin-search-dir

* Use find-parent-dir instead of find-up and test autoloading (with mocked fn)

* Add two test cases where --plugin-search-dir is not .

* Do not mutate pluginSearchDirs argument in load-plugins.js

* Do not test automatic plugin resolution as mocking of "find-parent-dir" does not work due to rollup

* Document --plugin-search-dir / pluginSearchDirs and improve spacing

* Address @ikatyang's review comments

* Fix require path for third-party

* Undo alphabetic sorting of third-party scripts
master
Alexander Kachkaev 2018-05-09 12:17:12 +01:00 committed by Ika
parent 8cf591447c
commit 7345a38e64
15 changed files with 248 additions and 106 deletions

View File

@ -7,31 +7,36 @@ title: Plugins (Beta)
> The plugin API is in a **beta** state as of Prettier 1.10 and the API may change in the next release!
Plugins are ways of adding new languages to Prettier. Prettier's own implementations of all languages are expressed using the plugin API. The core `prettier` package contains JavaScript and other web-focussed languages built in. For additional languages you'll need to install a plugin.
Plugins are ways of adding new languages to Prettier. Prettier's own implementations of all languages are expressed using the plugin API. The core `prettier` package contains JavaScript and other web-focused languages built in. For additional languages you'll need to install a plugin.
## Using Plugins
Plugins are automatically loaded if you have them installed in your `package.json`. Prettier plugin package names must start with `@prettier/plugin-` or `prettier-plugin-` to be registered.
Plugins are automatically loaded if you have them installed in the same `node_modules` directory where `prettier` is located. Plugin package names must start with `@prettier/plugin-` or `prettier-plugin-` to be registered.
If the plugin is unable to be found automatically, you can load them with:
When plugins cannot be found automatically, you can load them with:
* The [CLI](./cli.md), via the `--plugin` flag:
* The [CLI](./cli.md), via the `--plugin` and `--plugin-search-dir`:
```bash
prettier --write main.foo --plugin=./foo-plugin
prettier --write main.foo --plugin-search-dir=./dir-with-plugins --plugin=./foo-plugin
```
> Tip: You can pass multiple `--plugin` flags.
> Tip: You can set `--plugin` or `--plugin-search-dir` options multiple times.
* Or the [API](./api.md), via the `plugins` field:
* Or the [API](./api.md), via the `plugins` and `pluginSearchDirs` options:
```js
prettier.format("code", {
parser: "foo",
pluginSearchDirs: ["./dir-with-plugins"],
plugins: ["./foo-plugin"]
});
```
Prettier expects each of `pluginSearchDirs` to contain `node_modules` subdirectory, where `@prettier/plugin-*` and `prettier-plugin-*` will be searched. For instance, this can be your project directory or the location of global npm modules.
Providing at least one path to `--plugin-search-dir`/`pluginSearchDirs` turns off plugin autoloading in the default directory (i.e. `node_modules` above `prettier` binary).
## Official Plugins
* [`@prettier/plugin-python`](https://github.com/prettier/plugin-python)

View File

@ -17,7 +17,7 @@ function withPlugins(fn) {
const args = Array.from(arguments);
const opts = args[1] || {};
args[1] = Object.assign({}, opts, {
plugins: loadPlugins(opts.plugins)
plugins: loadPlugins(opts.plugins, opts.pluginSearchDirs)
});
return fn.apply(null, args);
};

View File

@ -29,6 +29,7 @@
"emoji-regex": "6.5.1",
"escape-string-regexp": "1.0.5",
"esutils": "2.0.2",
"find-parent-dir": "0.3.0",
"find-project-root": "1.1.1",
"flow-parser": "0.70",
"get-stream": "3.0.0",
@ -39,6 +40,7 @@
"jest-docblock": "22.2.2",
"json-stable-stringify": "1.0.1",
"leven": "2.1.0",
"lodash.uniqby": "4.7.0",
"mem": "1.1.0",
"minimatch": "3.0.4",
"minimist": "1.2.0",
@ -48,7 +50,6 @@
"postcss-scss": "1.0.5",
"postcss-selector-parser": "2.2.3",
"postcss-values-parser": "1.5.0",
"read-pkg-up": "3.0.0",
"remark-frontmatter": "1.1.0",
"remark-parse": "5.0.0",
"resolve": "1.5.0",

View File

@ -746,7 +746,12 @@ function createMinimistOptions(detailedOptions) {
.map(option => option.name),
default: detailedOptions
.filter(option => !option.deprecated)
.filter(option => !option.forwardToApi || option.name === "plugin")
.filter(
option =>
!option.forwardToApi ||
option.name === "plugin" ||
option.name === "plugin-search-dir"
)
.filter(option => option.default !== undefined)
.reduce(
(current, option) =>
@ -809,11 +814,15 @@ function createContext(args) {
const context = { args };
updateContextArgv(context);
normalizeContextArgv(context, ["loglevel", "plugin"]);
normalizeContextArgv(context, ["loglevel", "plugin", "plugin-search-dir"]);
context.logger = createLogger(context.argv["loglevel"]);
updateContextArgv(context, context.argv["plugin"]);
updateContextArgv(
context,
context.argv["plugin"],
context.argv["plugin-search-dir"]
);
return context;
}
@ -823,12 +832,13 @@ function initContext(context) {
normalizeContextArgv(context);
}
function updateContextOptions(context, plugins) {
function updateContextOptions(context, plugins, pluginSearchDirs) {
const supportOptions = prettier.getSupportInfo(null, {
showDeprecated: true,
showUnreleased: true,
showInternal: true,
plugins
plugins,
pluginSearchDirs
}).options;
const detailedOptionMap = normalizeDetailedOptionMap(
@ -851,12 +861,12 @@ function updateContextOptions(context, plugins) {
context.apiDefaultOptions = apiDefaultOptions;
}
function pushContextPlugins(context, plugins) {
function pushContextPlugins(context, plugins, pluginSearchDirs) {
context._supportOptions = context.supportOptions;
context._detailedOptions = context.detailedOptions;
context._detailedOptionMap = context.detailedOptionMap;
context._apiDefaultOptions = context.apiDefaultOptions;
updateContextOptions(context, plugins);
updateContextOptions(context, plugins, pluginSearchDirs);
}
function popContextPlugins(context) {
@ -866,8 +876,8 @@ function popContextPlugins(context) {
context.apiDefaultOptions = context._apiDefaultOptions;
}
function updateContextArgv(context, plugins) {
pushContextPlugins(context, plugins);
function updateContextArgv(context, plugins, pluginSearchDirs) {
pushContextPlugins(context, plugins, pluginSearchDirs);
const minimistOptions = createMinimistOptions(context.detailedOptions);
const argv = minimist(context.args, minimistOptions);

View File

@ -1,10 +1,30 @@
"use strict";
const uniqBy = require("lodash.uniqby");
const fs = require("fs");
const globby = require("globby");
const path = require("path");
const resolve = require("resolve");
const readPkgUp = require("read-pkg-up");
const thirdParty = require("./third-party");
function loadPlugins(plugins) {
plugins = plugins || [];
function loadPlugins(plugins, pluginSearchDirs) {
if (!plugins) {
plugins = [];
}
if (!pluginSearchDirs) {
pluginSearchDirs = [];
}
// unless pluginSearchDirs are provided, auto-load plugins from node_modules that are parent to Prettier
if (!pluginSearchDirs.length) {
const autoLoadDir = thirdParty.findParentDir(
thirdParty.findParentDir(__dirname, "prettier"),
"node_modules"
);
if (autoLoadDir) {
pluginSearchDirs = [autoLoadDir];
}
}
const internalPlugins = [
require("../language-js"),
@ -16,45 +36,64 @@ function loadPlugins(plugins) {
require("../language-vue")
];
const externalPlugins = plugins
.concat(
getPluginsFromPackage(
readPkgUp.sync({
normalize: false
}).pkg
)
)
.map(plugin => {
if (typeof plugin !== "string") {
return plugin;
const externalManualLoadPluginInfos = plugins.map(pluginName => ({
name: pluginName,
requirePath: resolve.sync(pluginName, { basedir: process.cwd() })
}));
const externalAutoLoadPluginInfos = pluginSearchDirs
.map(pluginSearchDir => {
const resolvedPluginSearchDir = path.resolve(
process.cwd(),
pluginSearchDir
);
if (!isDirectory(pluginSearchDir)) {
throw new Error(
`${pluginSearchDir} does not exist or is not a directory`
);
}
const pluginPath = resolve.sync(plugin, { basedir: process.cwd() });
return Object.assign({ name: plugin }, eval("require")(pluginPath));
});
const nodeModulesDir = path.resolve(
resolvedPluginSearchDir,
"node_modules"
);
return deduplicate(internalPlugins.concat(externalPlugins));
}
return findPluginsInNodeModules(nodeModulesDir).map(pluginName => ({
name: pluginName,
requirePath: resolve.sync(pluginName, {
basedir: resolvedPluginSearchDir
})
}));
})
.reduce((a, b) => a.concat(b), []);
function getPluginsFromPackage(pkg) {
if (!pkg) {
return [];
}
const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
return Object.keys(deps).filter(
dep =>
dep.startsWith("prettier-plugin-") || dep.startsWith("@prettier/plugin-")
const externalPlugins = uniqBy(
externalManualLoadPluginInfos.concat(externalAutoLoadPluginInfos),
"requirePath"
).map(externalPluginInfo =>
Object.assign(
{ name: externalPluginInfo.name },
eval("require")(externalPluginInfo.requirePath)
)
);
return internalPlugins.concat(externalPlugins);
}
function deduplicate(items) {
const uniqItems = [];
for (const item of items) {
if (uniqItems.indexOf(item) < 0) {
uniqItems.push(item);
}
function findPluginsInNodeModules(nodeModulesDir) {
const pluginPackageJsonPaths = globby.sync(
["prettier-plugin-*/package.json", "@prettier/plugin-*/package.json"],
{ cwd: nodeModulesDir }
);
return pluginPackageJsonPaths.map(path.dirname);
}
function isDirectory(dir) {
try {
return fs.statSync(dir).isDirectory();
} catch (e) {
return false;
}
return uniqItems;
}
module.exports = loadPlugins;

View File

@ -2,8 +2,10 @@
const getStream = require("get-stream");
const cosmiconfig = require("cosmiconfig");
const findParentDir = require("find-parent-dir").sync;
module.exports = {
getStream,
cosmiconfig
cosmiconfig,
findParentDir
};

View File

@ -122,6 +122,21 @@ const options = {
cliName: "plugin",
cliCategory: CATEGORY_CONFIG
},
pluginSearchDirs: {
since: "1.13.0",
type: "path",
array: true,
default: [{ value: [] }],
category: CATEGORY_GLOBAL,
description: dedent`
Custom directory that contains prettier plugins in node_modules subdirectory.
Overrides default behavior when plugins are searched relatively to the location of Prettier.
Multiple values are accepted.
`,
exception: value => typeof value === "string" || typeof value === "object",
cliName: "plugin-search-dir",
cliCategory: CATEGORY_CONFIG
},
printWidth: {
since: "0.0.0",
category: CATEGORY_GLOBAL,

View File

@ -302,6 +302,21 @@ Default: []
exports[`show detailed usage with --help plugin (write) 1`] = `Array []`;
exports[`show detailed usage with --help plugin-search-dir (stderr) 1`] = `""`;
exports[`show detailed usage with --help plugin-search-dir (stdout) 1`] = `
"--plugin-search-dir <path>
Custom directory that contains prettier plugins in node_modules subdirectory.
Overrides default behavior when plugins are searched relatively to the location of Prettier.
Multiple values are accepted.
Default: []
"
`;
exports[`show detailed usage with --help plugin-search-dir (write) 1`] = `Array []`;
exports[`show detailed usage with --help print-width (stderr) 1`] = `""`;
exports[`show detailed usage with --help print-width (stdout) 1`] = `
@ -599,6 +614,11 @@ Config options:
Defaults to .prettierignore.
--plugin <path> Add a plugin. Multiple plugins can be passed as separate \`--plugin\`s.
Defaults to [].
--plugin-search-dir <path>
Custom directory that contains prettier plugins in node_modules subdirectory.
Overrides default behavior when plugins are searched relatively to the location of Prettier.
Multiple values are accepted.
Defaults to [].
--with-node-modules Process files inside 'node_modules' directory.
Editor options:
@ -742,6 +762,11 @@ Config options:
Defaults to .prettierignore.
--plugin <path> Add a plugin. Multiple plugins can be passed as separate \`--plugin\`s.
Defaults to [].
--plugin-search-dir <path>
Custom directory that contains prettier plugins in node_modules subdirectory.
Overrides default behavior when plugins are searched relatively to the location of Prettier.
Multiple values are accepted.
Defaults to [].
--with-node-modules Process files inside 'node_modules' directory.
Editor options:

View File

@ -29,7 +29,8 @@ describe(`show detailed usage with plugin options (automatic resolution)`, () =>
runPrettier("plugins/automatic", [
"--help",
"tab-width",
"--parser=bar"
"--parser=bar",
`--plugin-search-dir=.`
]).test({
status: 0
});

View File

@ -3,7 +3,7 @@
const runPrettier = require("../runPrettier");
const EOL = require("os").EOL;
describe("automatically loads 'prettier-plugin-*' from package.json devDependencies", () => {
describe("automatically loads 'prettier-plugin-*'", () => {
runPrettier("plugins/automatic", ["file.txt", "--parser=foo"]).test({
stdout: "foo+contents" + EOL,
stderr: "",
@ -12,7 +12,7 @@ describe("automatically loads 'prettier-plugin-*' from package.json devDependenc
});
});
describe("automatically loads '@prettier/plugin-*' from package.json dependencies", () => {
describe("automatically loads '@prettier/plugin-*'", () => {
runPrettier("plugins/automatic", ["file.txt", "--parser=bar"]).test({
stdout: "bar+contents" + EOL,
stderr: "",
@ -20,3 +20,82 @@ describe("automatically loads '@prettier/plugin-*' from package.json dependencie
write: []
});
});
describe("automatically loads 'prettier-plugin-*' from --plugin-search-dir (same as autoload dir)", () => {
runPrettier("plugins/automatic", [
"file.txt",
"--parser=foo",
`--plugin-search-dir=.`
]).test({
stdout: "foo+contents" + EOL,
stderr: "",
status: 0,
write: []
});
});
describe("automatically loads '@prettier/plugin-*' from --plugin-search-dir (same as autoload dir)", () => {
runPrettier("plugins/automatic", [
"file.txt",
"--parser=bar",
`--plugin-search-dir=.`
]).test({
stdout: "bar+contents" + EOL,
stderr: "",
status: 0,
write: []
});
});
describe("automatically loads 'prettier-plugin-*' from --plugin-search-dir (different to autoload dir)", () => {
runPrettier("plugins", [
"automatic/file.txt",
"--parser=foo",
`--plugin-search-dir=automatic`
]).test({
stdout: "foo+contents" + EOL,
stderr: "",
status: 0,
write: []
});
});
describe("automatically loads '@prettier/plugin-*' from --plugin-search-dir (different to autoload dir)", () => {
runPrettier("plugins", [
"automatic/file.txt",
"--parser=bar",
`--plugin-search-dir=automatic`
]).test({
stdout: "bar+contents" + EOL,
stderr: "",
status: 0,
write: []
});
});
describe("does not crash when --plugin-search-dir does not contain node_modules", () => {
runPrettier("plugins/extensions", [
"file.foo",
"--plugin=./plugin",
`--plugin-search-dir=.`
]).test({
stdout: "!contents" + EOL,
stderr: "",
status: 0,
write: []
});
});
describe("crashes when one of --plugin-search-dir does not exist", () => {
runPrettier("plugins/automatic", [
"file.txt",
"--parser=foo",
`--plugin-search-dir=non-existing-dir`,
`--plugin-search-dir=.`
]).test({
stdout: "",
stderr: "non-existing-dir does not exist or is not a directory",
status: 1,
write: []
});
});

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -1,8 +1 @@
{
"dependencies": {
"@prettier/plugin-foo": "*"
},
"devDependencies": {
"prettier-plugin-bar": "*"
}
}
{}

View File

@ -82,6 +82,9 @@ function runPrettier(dir, args, options) {
Object.assign({}, options, { stopDir: __dirname })
)
);
jest
.spyOn(require(thirdParty), "findParentDir")
.mockImplementation(() => process.cwd());
try {
require(prettierCli);

View File

@ -1935,6 +1935,10 @@ fill-range@^2.1.0:
repeat-element "^1.1.2"
repeat-string "^1.5.2"
find-parent-dir@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.0.tgz#33c44b429ab2b2f0646299c5f9f718f376ff8d54"
find-project-root@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/find-project-root/-/find-project-root-1.1.1.tgz#d242727a2d904725df5714f23dfdcdedda0b6ef8"
@ -3000,10 +3004,6 @@ json-loader@^0.5.4:
version "0.5.4"
resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.4.tgz#8baa1365a632f58a3c46d20175fc6002c96e37de"
json-parse-better-errors@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.1.tgz#50183cd1b2d25275de069e9e71b467ac9eab973a"
json-schema-traverse@^0.3.0:
version "0.3.1"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
@ -3109,15 +3109,6 @@ load-json-file@^2.0.0:
pify "^2.0.0"
strip-bom "^3.0.0"
load-json-file@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
dependencies:
graceful-fs "^4.1.2"
parse-json "^4.0.0"
pify "^3.0.0"
strip-bom "^3.0.0"
loader-runner@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
@ -3142,6 +3133,10 @@ lodash.unescape@4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c"
lodash.uniqby@4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302"
lodash@^4.14.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
@ -3587,13 +3582,6 @@ parse-json@^3.0.0:
dependencies:
error-ex "^1.3.1"
parse-json@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
dependencies:
error-ex "^1.3.1"
json-parse-better-errors "^1.0.1"
parse5@3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
@ -3648,12 +3636,6 @@ path-type@^2.0.0:
dependencies:
pify "^2.0.0"
path-type@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
dependencies:
pify "^3.0.0"
pbkdf2@^3.0.3:
version "3.0.12"
resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.12.tgz#be36785c5067ea48d806ff923288c5f750b6b8a2"
@ -3894,13 +3876,6 @@ rc@^1.1.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
read-pkg-up@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07"
dependencies:
find-up "^2.0.0"
read-pkg "^3.0.0"
read-pkg-up@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
@ -3931,14 +3906,6 @@ read-pkg@^2.0.0:
normalize-package-data "^2.3.2"
path-type "^2.0.0"
read-pkg@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
dependencies:
load-json-file "^4.0.0"
normalize-package-data "^2.3.2"
path-type "^3.0.0"
readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.6:
version "2.2.11"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.11.tgz#0796b31f8d7688007ff0b93a8088d34aa17c0f72"