Skip to content

Commit

Permalink
Fix plugin API in globally installed Prettier and introduce optional …
Browse files Browse the repository at this point in the history
…--plugin-search-dir (prettier#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
  • Loading branch information
kachkaev authored and ikatyang committed May 9, 2018
1 parent 8cf5914 commit 7345a38
Show file tree
Hide file tree
Showing 15 changed files with 247 additions and 105 deletions.
19 changes: 12 additions & 7 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
28 changes: 19 additions & 9 deletions src/cli/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down Expand Up @@ -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;
}
Expand All @@ -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(
Expand All @@ -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) {
Expand All @@ -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);
Expand Down
107 changes: 73 additions & 34 deletions src/common/load-plugins.js
Original file line number Diff line number Diff line change
@@ -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"),
Expand All @@ -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), []);

const externalPlugins = uniqBy(
externalManualLoadPluginInfos.concat(externalAutoLoadPluginInfos),
"requirePath"
).map(externalPluginInfo =>
Object.assign(
{ name: externalPluginInfo.name },
eval("require")(externalPluginInfo.requirePath)
)
);

return internalPlugins.concat(externalPlugins);
}

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-")
function findPluginsInNodeModules(nodeModulesDir) {
const pluginPackageJsonPaths = globby.sync(
["prettier-plugin-*/package.json", "@prettier/plugin-*/package.json"],
{ cwd: nodeModulesDir }
);
return pluginPackageJsonPaths.map(path.dirname);
}

function deduplicate(items) {
const uniqItems = [];
for (const item of items) {
if (uniqItems.indexOf(item) < 0) {
uniqItems.push(item);
}
function isDirectory(dir) {
try {
return fs.statSync(dir).isDirectory();
} catch (e) {
return false;
}
return uniqItems;
}

module.exports = loadPlugins;
4 changes: 3 additions & 1 deletion src/common/third-party.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
15 changes: 15 additions & 0 deletions src/main/core-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
25 changes: 25 additions & 0 deletions tests_integration/__tests__/__snapshots__/early-exit.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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`] = `
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion tests_integration/__tests__/early-exit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
Expand Down
Loading

0 comments on commit 7345a38

Please sign in to comment.