Allow plugins to override default options (#3991)

* refactor(cli): defer default value applying

* Allow plugins to override default options

* Move "defaultOptions" to top level of plugin

* Simplify implementation

* Attach plugin name

* Add pluginOptions to cli help

* Update snapshots

* Code review (immutable style)

* Add test for help output

* Use snapshot test, fix Object.assign

* Refactor to immutable style

* Add test case for automatic plugin resolution

* Add tests for applying and overriding default opts

* Remove "since" option

* Only set defaults for CLI args when no pluginDefaults are present

* Revert workaround, rebase to #4045

* Add basic documentation for `options` and `defaultOptions`
master
Christian Zosel 2018-02-27 14:20:02 +01:00 committed by Lucas Azzola
parent 23f032f348
commit d05a29da05
16 changed files with 263 additions and 35 deletions

View File

@ -46,7 +46,13 @@ If the plugin is unable to be found automatically, you can load them with:
## Developing Plugins
Prettier plugins are regular JavaScript modules with three exports, `languages`, `parsers` and `printers`.
Prettier plugins are regular JavaScript modules with five exports:
* `languages`
* `parsers`
* `printers`
* `options`
* `defaultOptions`
### `languages`
@ -149,6 +155,33 @@ function embed(
If you don't want to switch to a different parser, simply return `null` or `undefined`.
### `options`
`options` is an object containing the custom options your plugin supports.
Example:
```js
options: {
openingBraceNewLine: {
type: "boolean",
category: "Global",
default: true,
description: "Move open brace for code blocks onto new line."
}
}
```
### `defaultOptions`
If your plugin requires different default values for some of prettier's core options, you can specify them in `defaultOptions`:
```
defaultOptions: {
tabWidth: 4
}
```
### Utility functions
A `util` module from prettier core is considered a private API and is not meant to be consumed by plugins. Instead, the `util-shared` module provides the following limited set of utility functions for plugins:

35
src/cli/minimist.js Normal file
View File

@ -0,0 +1,35 @@
"use strict";
const minimist = require("minimist");
const PLACEHOLDER = null;
/**
* unspecified boolean flag without default value is parsed as `undefined` instead of `false`
*/
module.exports = function(args, options) {
const boolean = options.boolean || [];
const defaults = options.default || {};
const booleanWithoutDefault = boolean.filter(key => !(key in defaults));
const newDefaults = Object.assign(
{},
defaults,
booleanWithoutDefault.reduce(
(reduced, key) => Object.assign(reduced, { [key]: PLACEHOLDER }),
{}
)
);
const parsed = minimist(
args,
Object.assign({}, options, { default: newDefaults })
);
return Object.keys(parsed).reduce((reduced, key) => {
if (parsed[key] !== PLACEHOLDER) {
reduced[key] = parsed[key];
}
return reduced;
}, {});
};

View File

@ -3,7 +3,6 @@
const path = require("path");
const camelCase = require("camelcase");
const dashify = require("dashify");
const minimist = require("minimist");
const fs = require("fs");
const globby = require("globby");
const ignore = require("ignore");
@ -11,6 +10,7 @@ const chalk = require("chalk");
const readline = require("readline");
const leven = require("leven");
const minimist = require("./minimist");
const prettier = require("../../index");
const cleanAST = require("../common/clean-ast").cleanAST;
const errors = require("../common/errors");
@ -228,11 +228,7 @@ function parseArgsToOptions(context, overrideDefaults) {
Object.assign({
string: minimistOptions.string,
boolean: minimistOptions.boolean,
default: Object.assign(
{},
cliifyOptions(context.apiDefaultOptions, apiDetailedOptionMap),
cliifyOptions(overrideDefaults, apiDetailedOptionMap)
)
default: cliifyOptions(overrideDefaults, apiDetailedOptionMap)
})
),
context.detailedOptions,
@ -305,7 +301,7 @@ function createIgnorer(context) {
}
function eachFilename(context, patterns, callback) {
const ignoreNodeModules = context.argv["with-node-modules"] === false;
const ignoreNodeModules = context.argv["with-node-modules"] !== true;
if (ignoreNodeModules) {
patterns = patterns.concat(["!**/node_modules/**", "!./node_modules/**"]);
}
@ -613,7 +609,16 @@ function createDetailedUsage(context, optionName) {
? `\n\nDefault: ${createDefaultValueDisplay(optionDefaultValue)}`
: "";
return `${header}${description}${choices}${defaults}`;
const pluginDefaults =
option.pluginDefaults && Object.keys(option.pluginDefaults).length
? `\nPlugin defaults:${Object.keys(option.pluginDefaults).map(
key =>
`\n* ${key}: ${createDefaultValueDisplay(
option.pluginDefaults[key]
)}`
)}`
: "";
return `${header}${description}${choices}${defaults}${pluginDefaults}`;
}
function getOptionDefaultValue(context, optionName) {
@ -743,6 +748,7 @@ function createMinimistOptions(detailedOptions) {
.map(option => option.name),
default: detailedOptions
.filter(option => !option.deprecated)
.filter(option => !option.forwardToApi || option.name === "plugin")
.filter(option => option.default !== undefined)
.reduce(
(current, option) =>

View File

@ -30,7 +30,7 @@ function loadPlugins(plugins) {
}
const pluginPath = resolve.sync(plugin, { basedir: process.cwd() });
return eval("require")(pluginPath);
return Object.assign({ name: plugin }, eval("require")(pluginPath));
});
return deduplicate(internalPlugins.concat(externalPlugins));

View File

@ -249,6 +249,16 @@ function getSupportInfo(version, opts) {
}
return newOption;
})
.map(option => {
const filteredPlugins = plugins.filter(
plugin => plugin.defaultOptions && plugin.defaultOptions[option.name]
);
const pluginDefaults = filteredPlugins.reduce((reduced, plugin) => {
reduced[plugin.name] = plugin.defaultOptions[option.name];
return reduced;
}, {});
return Object.assign(option, { pluginDefaults });
});
const usePostCssParser = semver.lt(version, "1.7.1");

19
src/main/get-plugin.js Normal file
View File

@ -0,0 +1,19 @@
"use strict";
function getPlugin(options) {
const astFormat = options.astFormat;
if (!astFormat) {
throw new Error("getPlugin() requires astFormat to be set");
}
const printerPlugin = options.plugins.find(
plugin => plugin.printers[astFormat]
);
if (!printerPlugin) {
throw new Error(`Couldn't find plugin for AST format "${astFormat}"`);
}
return printerPlugin;
}
module.exports = getPlugin;

View File

@ -1,21 +0,0 @@
"use strict";
function getPrinter(options) {
const astFormat = options.astFormat;
if (!astFormat) {
throw new Error("getPrinter() requires astFormat to be set");
}
const printerPlugin = options.plugins.find(
plugin => plugin.printers[astFormat]
);
if (!printerPlugin) {
throw new Error(
`Couldn't find printer plugin for AST format "${astFormat}"`
);
}
return printerPlugin.printers[astFormat];
}
module.exports = getPrinter;

View File

@ -5,7 +5,7 @@ const getSupportInfo = require("../common/support").getSupportInfo;
const normalizer = require("./options-normalizer");
const loadPlugins = require("../common/load-plugins");
const resolveParser = require("./parser").resolveParser;
const getPrinter = require("./get-printer");
const getPlugin = require("./get-plugin");
const hiddenDefaults = {
astFormat: "estree",
@ -53,11 +53,27 @@ function normalize(options, opts) {
rawOptions.astFormat = parser.astFormat;
rawOptions.locEnd = parser.locEnd;
rawOptions.locStart = parser.locStart;
rawOptions.printer = getPrinter(rawOptions);
const plugin = getPlugin(rawOptions);
rawOptions.printer = plugin.printers[rawOptions.astFormat];
Object.keys(defaults).forEach(k => {
const pluginDefaults = supportOptions
.filter(
optionInfo =>
optionInfo.pluginDefaults && optionInfo.pluginDefaults[plugin.name]
)
.reduce(
(reduced, optionInfo) =>
Object.assign(reduced, {
[optionInfo.name]: optionInfo.pluginDefaults[plugin.name]
}),
{}
);
const mixedDefaults = Object.assign({}, defaults, pluginDefaults);
Object.keys(mixedDefaults).forEach(k => {
if (rawOptions[k] == null) {
rawOptions[k] = defaults[k];
rawOptions[k] = mixedDefaults[k];
}
});

View File

@ -514,6 +514,36 @@ exports[`show detailed usage with --help write (stdout) 1`] = `
exports[`show detailed usage with --help write (write) 1`] = `Array []`;
exports[`show detailed usage with plugin options (automatic resolution) (stderr) 1`] = `""`;
exports[`show detailed usage with plugin options (automatic resolution) (stdout) 1`] = `
"--tab-width <int>
Number of spaces per indentation level.
Default: 2
Plugin defaults:
* prettier-plugin-bar: 4
"
`;
exports[`show detailed usage with plugin options (automatic resolution) (write) 1`] = `Array []`;
exports[`show detailed usage with plugin options (manual resolution) (stderr) 1`] = `""`;
exports[`show detailed usage with plugin options (manual resolution) (stdout) 1`] = `
"--tab-width <int>
Number of spaces per indentation level.
Default: 2
Plugin defaults:
* ../plugins/automatic/node_modules/prettier-plugin-bar: 4
"
`;
exports[`show detailed usage with plugin options (manual resolution) (write) 1`] = `Array []`;
exports[`show usage with --help (stderr) 1`] = `""`;
exports[`show usage with --help (stdout) 1`] = `

View File

@ -661,6 +661,7 @@ exports[`CLI --support-info (stdout) 1`] = `
\\"description\\":
\\"Include parentheses around a sole arrow function parameter.\\",
\\"name\\": \\"arrowParens\\",
\\"pluginDefaults\\": {},
\\"since\\": \\"1.9.0\\",
\\"type\\": \\"choice\\"
},
@ -670,6 +671,7 @@ exports[`CLI --support-info (stdout) 1`] = `
\\"description\\": \\"Print spaces between brackets.\\",
\\"name\\": \\"bracketSpacing\\",
\\"oppositeDescription\\": \\"Do not print spaces between brackets.\\",
\\"pluginDefaults\\": {},
\\"since\\": \\"0.0.0\\",
\\"type\\": \\"boolean\\"
},
@ -679,6 +681,7 @@ exports[`CLI --support-info (stdout) 1`] = `
\\"description\\":
\\"Print (to stderr) where a cursor at the given position would move to after formatting.\\\\nThis option cannot be used with --range-start and --range-end.\\",
\\"name\\": \\"cursorOffset\\",
\\"pluginDefaults\\": {},
\\"range\\": { \\"end\\": null, \\"start\\": -1, \\"step\\": 1 },
\\"since\\": \\"1.4.0\\",
\\"type\\": \\"int\\"
@ -688,6 +691,7 @@ exports[`CLI --support-info (stdout) 1`] = `
\\"description\\":
\\"Specify the input filepath. This will be used to do parser inference.\\",
\\"name\\": \\"filepath\\",
\\"pluginDefaults\\": {},
\\"since\\": \\"1.4.0\\",
\\"type\\": \\"path\\"
},
@ -697,6 +701,7 @@ exports[`CLI --support-info (stdout) 1`] = `
\\"description\\":
\\"Insert @format pragma into file's first docblock comment.\\",
\\"name\\": \\"insertPragma\\",
\\"pluginDefaults\\": {},
\\"since\\": \\"1.8.0\\",
\\"type\\": \\"boolean\\"
},
@ -705,6 +710,7 @@ exports[`CLI --support-info (stdout) 1`] = `
\\"default\\": false,
\\"description\\": \\"Put > on the last line instead of at a new line.\\",
\\"name\\": \\"jsxBracketSameLine\\",
\\"pluginDefaults\\": {},
\\"since\\": \\"0.17.0\\",
\\"type\\": \\"boolean\\"
},
@ -729,6 +735,7 @@ exports[`CLI --support-info (stdout) 1`] = `
\\"default\\": \\"babylon\\",
\\"description\\": \\"Which parser to use.\\",
\\"name\\": \\"parser\\",
\\"pluginDefaults\\": {},
\\"since\\": \\"0.0.10\\",
\\"type\\": \\"choice\\"
},
@ -739,6 +746,7 @@ exports[`CLI --support-info (stdout) 1`] = `
\\"description\\":
\\"Add a plugin. Multiple plugins can be passed as separate \`--plugin\`s.\\",
\\"name\\": \\"plugins\\",
\\"pluginDefaults\\": {},
\\"since\\": \\"1.10.0\\",
\\"type\\": \\"path\\"
},
@ -747,6 +755,7 @@ exports[`CLI --support-info (stdout) 1`] = `
\\"default\\": 80,
\\"description\\": \\"The line length where Prettier will try wrap.\\",
\\"name\\": \\"printWidth\\",
\\"pluginDefaults\\": {},
\\"range\\": { \\"end\\": null, \\"start\\": 0, \\"step\\": 1 },
\\"since\\": \\"0.0.0\\",
\\"type\\": \\"int\\"
@ -773,6 +782,7 @@ exports[`CLI --support-info (stdout) 1`] = `
\\"default\\": \\"preserve\\",
\\"description\\": \\"How to wrap prose. (markdown)\\",
\\"name\\": \\"proseWrap\\",
\\"pluginDefaults\\": {},
\\"since\\": \\"1.8.2\\",
\\"type\\": \\"choice\\"
},
@ -782,6 +792,7 @@ exports[`CLI --support-info (stdout) 1`] = `
\\"description\\":
\\"Format code ending at a given character offset (exclusive).\\\\nThe range will extend forwards to the end of the selected statement.\\\\nThis option cannot be used with --cursor-offset.\\",
\\"name\\": \\"rangeEnd\\",
\\"pluginDefaults\\": {},
\\"range\\": { \\"end\\": null, \\"start\\": 0, \\"step\\": 1 },
\\"since\\": \\"1.4.0\\",
\\"type\\": \\"int\\"
@ -792,6 +803,7 @@ exports[`CLI --support-info (stdout) 1`] = `
\\"description\\":
\\"Format code starting at a given character offset.\\\\nThe range will extend backwards to the start of the first line containing the selected statement.\\\\nThis option cannot be used with --cursor-offset.\\",
\\"name\\": \\"rangeStart\\",
\\"pluginDefaults\\": {},
\\"range\\": { \\"end\\": null, \\"start\\": 0, \\"step\\": 1 },
\\"since\\": \\"1.4.0\\",
\\"type\\": \\"int\\"
@ -802,6 +814,7 @@ exports[`CLI --support-info (stdout) 1`] = `
\\"description\\":
\\"Require either '@prettier' or '@format' to be present in the file's first docblock comment\\\\nin order for it to be formatted.\\",
\\"name\\": \\"requirePragma\\",
\\"pluginDefaults\\": {},
\\"since\\": \\"1.7.0\\",
\\"type\\": \\"boolean\\"
},
@ -812,6 +825,7 @@ exports[`CLI --support-info (stdout) 1`] = `
\\"name\\": \\"semi\\",
\\"oppositeDescription\\":
\\"Do not print semicolons, except at the beginning of lines which may need them.\\",
\\"pluginDefaults\\": {},
\\"since\\": \\"1.0.0\\",
\\"type\\": \\"boolean\\"
},
@ -820,6 +834,7 @@ exports[`CLI --support-info (stdout) 1`] = `
\\"default\\": false,
\\"description\\": \\"Use single quotes instead of double quotes.\\",
\\"name\\": \\"singleQuote\\",
\\"pluginDefaults\\": {},
\\"since\\": \\"0.0.0\\",
\\"type\\": \\"boolean\\"
},
@ -828,6 +843,7 @@ exports[`CLI --support-info (stdout) 1`] = `
\\"default\\": 2,
\\"description\\": \\"Number of spaces per indentation level.\\",
\\"name\\": \\"tabWidth\\",
\\"pluginDefaults\\": {},
\\"range\\": { \\"end\\": null, \\"start\\": 0, \\"step\\": 1 },
\\"type\\": \\"int\\"
},
@ -849,6 +865,7 @@ exports[`CLI --support-info (stdout) 1`] = `
\\"default\\": \\"none\\",
\\"description\\": \\"Print trailing commas wherever possible when multi-line.\\",
\\"name\\": \\"trailingComma\\",
\\"pluginDefaults\\": {},
\\"since\\": \\"0.0.0\\",
\\"type\\": \\"choice\\"
},
@ -857,6 +874,7 @@ exports[`CLI --support-info (stdout) 1`] = `
\\"default\\": false,
\\"description\\": \\"Indent with tabs instead of spaces.\\",
\\"name\\": \\"useTabs\\",
\\"pluginDefaults\\": {},
\\"since\\": \\"1.0.0\\",
\\"type\\": \\"boolean\\"
}

View File

@ -26,6 +26,27 @@ describe(`show detailed usage with --help l (alias)`, () => {
});
});
describe(`show detailed usage with plugin options (automatic resolution)`, () => {
runPrettier("plugins/automatic", [
"--help",
"tab-width",
"--parser=bar"
]).test({
status: 0
});
});
describe(`show detailed usage with plugin options (manual resolution)`, () => {
runPrettier("cli", [
"--help",
"tab-width",
"--plugin=../plugins/automatic/node_modules/prettier-plugin-bar",
"--parser=bar"
]).test({
status: 0
});
});
commonUtil
.arrayify(
Object.assign(

View File

@ -0,0 +1,29 @@
"use strict";
const runPrettier = require("../runPrettier");
describe("plugin default options should work", () => {
runPrettier(
"plugins/defaultOptions",
["--stdin-filepath", "example.foo", "--plugin=./plugin"],
{ input: "hello-world" }
).test({
stdout: "tabWidth:8",
stderr: "",
status: 0,
write: []
});
});
describe("overriding plugin default options should work", () => {
runPrettier(
"plugins/defaultOptions",
["--stdin-filepath", "example.foo", "--plugin=./plugin", "--tab-width=4"],
{ input: "hello-world" }
).test({
stdout: "tabWidth:4",
stderr: "",
status: 0,
write: []
});
});

View File

@ -19,5 +19,8 @@ module.exports = {
bar: {
print: path => concat(["bar+", path.getValue().text])
}
},
defaultOptions: {
tabWidth: 4
}
};

View File

@ -0,0 +1,3 @@
{
"plugins": ["./plugin"]
}

View File

@ -0,0 +1,26 @@
"use strict";
module.exports = {
languages: [
{
name: "foo",
parsers: ["foo-parser"],
extensions: [".foo"]
}
],
defaultOptions: {
tabWidth: 8
},
parsers: {
"foo-parser": {
parse: text => ({ text }),
astFormat: "foo-ast"
}
},
printers: {
"foo-ast": {
print: (path, options) =>
options.tabWidth ? `tabWidth:${options.tabWidth}` : path.getValue().text
}
}
};