diff --git a/docs/plugins.md b/docs/plugins.md index 9c465ddc..fda3f599 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -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: diff --git a/src/cli/minimist.js b/src/cli/minimist.js new file mode 100644 index 00000000..aa62b68a --- /dev/null +++ b/src/cli/minimist.js @@ -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; + }, {}); +}; diff --git a/src/cli/util.js b/src/cli/util.js index a21531d8..4ec80b64 100644 --- a/src/cli/util.js +++ b/src/cli/util.js @@ -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) => diff --git a/src/common/load-plugins.js b/src/common/load-plugins.js index 581e5470..b4899784 100644 --- a/src/common/load-plugins.js +++ b/src/common/load-plugins.js @@ -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)); diff --git a/src/common/support.js b/src/common/support.js index 1127ef45..1ebee558 100644 --- a/src/common/support.js +++ b/src/common/support.js @@ -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"); diff --git a/src/main/get-plugin.js b/src/main/get-plugin.js new file mode 100644 index 00000000..17e4b7bd --- /dev/null +++ b/src/main/get-plugin.js @@ -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; diff --git a/src/main/get-printer.js b/src/main/get-printer.js deleted file mode 100644 index cba1b100..00000000 --- a/src/main/get-printer.js +++ /dev/null @@ -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; diff --git a/src/main/options.js b/src/main/options.js index a6b037a9..d285bfe7 100644 --- a/src/main/options.js +++ b/src/main/options.js @@ -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]; } }); diff --git a/tests_integration/__tests__/__snapshots__/early-exit.js.snap b/tests_integration/__tests__/__snapshots__/early-exit.js.snap index e602e308..bbe6abca 100644 --- a/tests_integration/__tests__/__snapshots__/early-exit.js.snap +++ b/tests_integration/__tests__/__snapshots__/early-exit.js.snap @@ -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 + + 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 + + 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`] = ` diff --git a/tests_integration/__tests__/__snapshots__/support-info.js.snap b/tests_integration/__tests__/__snapshots__/support-info.js.snap index 96555659..9d13a454 100644 --- a/tests_integration/__tests__/__snapshots__/support-info.js.snap +++ b/tests_integration/__tests__/__snapshots__/support-info.js.snap @@ -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\\" } diff --git a/tests_integration/__tests__/early-exit.js b/tests_integration/__tests__/early-exit.js index 0832bdcc..25b3609d 100644 --- a/tests_integration/__tests__/early-exit.js +++ b/tests_integration/__tests__/early-exit.js @@ -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( diff --git a/tests_integration/__tests__/plugin-default-options.js b/tests_integration/__tests__/plugin-default-options.js new file mode 100644 index 00000000..2a9bad9b --- /dev/null +++ b/tests_integration/__tests__/plugin-default-options.js @@ -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: [] + }); +}); diff --git a/tests_integration/plugins/automatic/node_modules/prettier-plugin-bar/index.js b/tests_integration/plugins/automatic/node_modules/prettier-plugin-bar/index.js index b878e8e3..9b4f1157 100644 --- a/tests_integration/plugins/automatic/node_modules/prettier-plugin-bar/index.js +++ b/tests_integration/plugins/automatic/node_modules/prettier-plugin-bar/index.js @@ -19,5 +19,8 @@ module.exports = { bar: { print: path => concat(["bar+", path.getValue().text]) } + }, + defaultOptions: { + tabWidth: 4 } }; diff --git a/tests_integration/plugins/defaultOptions/.config.json.swp b/tests_integration/plugins/defaultOptions/.config.json.swp new file mode 100644 index 00000000..5a1ea1a9 Binary files /dev/null and b/tests_integration/plugins/defaultOptions/.config.json.swp differ diff --git a/tests_integration/plugins/defaultOptions/config.json b/tests_integration/plugins/defaultOptions/config.json new file mode 100644 index 00000000..37ed1cfa --- /dev/null +++ b/tests_integration/plugins/defaultOptions/config.json @@ -0,0 +1,3 @@ +{ + "plugins": ["./plugin"] +} diff --git a/tests_integration/plugins/defaultOptions/plugin.js b/tests_integration/plugins/defaultOptions/plugin.js new file mode 100644 index 00000000..ba094ac7 --- /dev/null +++ b/tests_integration/plugins/defaultOptions/plugin.js @@ -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 + } + } +};