From e5d6a4704f2427cdf3baa0a1c941dd797c513993 Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Fri, 5 Jan 2018 21:09:51 +1100 Subject: [PATCH] Automatically load plugins from package.json (#3624) * Automatically load plugins from package.json * Fix build by adding json extension * Fixup misuse of options argument * Rewire graceful-fs to fs * Document graceful-fs issue * Alias graceful-fs to fs in Rollup --- jest.config.js | 7 +++ package.json | 1 + scripts/build/rollup.bin.config.js | 11 +++- scripts/build/rollup.index.config.js | 9 ++- src/common/load-plugins.js | 56 ++++++++++++++----- src/common/support.js | 12 +++- src/language-markdown/embed.js | 5 +- src/main/options.js | 7 ++- tests_config/fs.js | 3 + .../__tests__/plugin-extensions.js | 12 ++++ .../__tests__/plugin-resolution.js | 21 +++++++ tests_integration/plugins/automatic/file.txt | 1 + .../@prettier/plugin-foo/index.js | 23 ++++++++ .../node_modules/prettier-plugin-bar/index.js | 23 ++++++++ .../plugins/automatic/package.json | 8 +++ tests_integration/plugins/extensions/file.foo | 1 + .../plugins/extensions/plugin.js | 25 +++++++++ yarn.lock | 41 ++++++++++++++ 18 files changed, 243 insertions(+), 23 deletions(-) create mode 100644 tests_config/fs.js create mode 100644 tests_integration/__tests__/plugin-extensions.js create mode 100644 tests_integration/__tests__/plugin-resolution.js create mode 100644 tests_integration/plugins/automatic/file.txt create mode 100644 tests_integration/plugins/automatic/node_modules/@prettier/plugin-foo/index.js create mode 100644 tests_integration/plugins/automatic/node_modules/prettier-plugin-bar/index.js create mode 100644 tests_integration/plugins/automatic/package.json create mode 100644 tests_integration/plugins/extensions/file.foo create mode 100644 tests_integration/plugins/extensions/plugin.js diff --git a/jest.config.js b/jest.config.js index bcfffd31..c774d0b4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -14,5 +14,12 @@ module.exports = { "/src/clean-ast.js", "/src/deprecated.js" ], + moduleNameMapper: { + // Jest wires `fs` to `graceful-fs`, which causes a memory leak when + // `graceful-fs` does `require('fs')`. + // Ref: https://github.com/facebook/jest/issues/2179#issuecomment-355231418 + // If this is removed, see also rollup.bin.config.js and rollup.index.config.js. + "graceful-fs": "/tests_config/fs.js" + }, transform: {} }; diff --git a/package.json b/package.json index 312f286a..a324c695 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "postcss-scss": "1.0.2", "postcss-selector-parser": "2.2.3", "postcss-values-parser": "1.3.1", + "read-pkg-up": "3.0.0", "remark-frontmatter": "1.1.0", "remark-parse": "4.0.0", "resolve": "1.5.0", diff --git a/scripts/build/rollup.bin.config.js b/scripts/build/rollup.bin.config.js index 1844407e..ded68afb 100644 --- a/scripts/build/rollup.bin.config.js +++ b/scripts/build/rollup.bin.config.js @@ -11,9 +11,16 @@ export default Object.assign(baseConfig, { format: "cjs", banner: "#!/usr/bin/env node", plugins: [ - replace({ "#!/usr/bin/env node": "" }), + replace({ + "#!/usr/bin/env node": "", + // See comment in jest.config.js + "require('graceful-fs')": "require('fs')" + }), json(), - resolve({ preferBuiltins: true }), + resolve({ + preferBuiltins: true, + extensions: [".js", ".json"] + }), commonjs() ], external: [ diff --git a/scripts/build/rollup.index.config.js b/scripts/build/rollup.index.config.js index 80a8f9c1..74c821bf 100644 --- a/scripts/build/rollup.index.config.js +++ b/scripts/build/rollup.index.config.js @@ -17,10 +17,15 @@ export default Object.assign(baseConfig, { format: "cjs", plugins: [ replace({ - "process.env.NODE_ENV": JSON.stringify("production") + "process.env.NODE_ENV": JSON.stringify("production"), + // See comment in jest.config.js + "require('graceful-fs')": "require('fs')" }), json(), - resolve({ preferBuiltins: true }), + resolve({ + preferBuiltins: true, + extensions: [".js", ".json"] + }), commonjs() ], external, diff --git a/src/common/load-plugins.js b/src/common/load-plugins.js index 59e73637..581e5470 100644 --- a/src/common/load-plugins.js +++ b/src/common/load-plugins.js @@ -1,9 +1,10 @@ "use strict"; const resolve = require("resolve"); +const readPkgUp = require("read-pkg-up"); -function loadPlugins(options) { - options = Object.assign({ plugins: [] }, options); +function loadPlugins(plugins) { + plugins = plugins || []; const internalPlugins = [ require("../language-js"), @@ -13,20 +14,47 @@ function loadPlugins(options) { require("../language-markdown"), require("../language-html"), require("../language-vue") - ].filter(plugin => { - return options.plugins.indexOf(plugin) < 0; - }); + ]; - const externalPlugins = options.plugins.map(plugin => { - if (typeof plugin !== "string") { - return plugin; + const externalPlugins = plugins + .concat( + getPluginsFromPackage( + readPkgUp.sync({ + normalize: false + }).pkg + ) + ) + .map(plugin => { + if (typeof plugin !== "string") { + return plugin; + } + + const pluginPath = resolve.sync(plugin, { basedir: process.cwd() }); + return eval("require")(pluginPath); + }); + + return deduplicate(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 deduplicate(items) { + const uniqItems = []; + for (const item of items) { + if (uniqItems.indexOf(item) < 0) { + uniqItems.push(item); } - - const pluginPath = resolve.sync(plugin, { basedir: process.cwd() }); - return eval("require")(pluginPath); - }); - - return internalPlugins.concat(externalPlugins); + } + return uniqItems; } module.exports = loadPlugins; diff --git a/src/common/support.js b/src/common/support.js index c153a410..b7ca81d8 100644 --- a/src/common/support.js +++ b/src/common/support.js @@ -162,13 +162,21 @@ const supportOptions = { }; function getSupportInfo(version, opts) { - opts = opts || {}; + opts = Object.assign( + { + plugins: [], + pluginsLoaded: false, + showUnreleased: false, + showDeprecated: false + }, + opts + ); if (!version) { version = currentVersion; } - const plugins = loadPlugins(); + const plugins = opts.pluginsLoaded ? opts.plugins : loadPlugins(opts.plugins); const options = util .arrayify( diff --git a/src/language-markdown/embed.js b/src/language-markdown/embed.js index 85fdc360..6cd10123 100644 --- a/src/language-markdown/embed.js +++ b/src/language-markdown/embed.js @@ -31,7 +31,10 @@ function embed(path, print, textToDoc, options) { return null; function getParserName(lang) { - const supportInfo = support.getSupportInfo(undefined, options); + const supportInfo = support.getSupportInfo(undefined, { + plugins: options.plugins, + pluginsLoaded: true + }); const language = supportInfo.languages.find( language => language.name.toLowerCase() === lang || diff --git a/src/main/options.js b/src/main/options.js index c52ceea5..292386c2 100644 --- a/src/main/options.js +++ b/src/main/options.js @@ -44,7 +44,7 @@ function normalize(options) { const normalized = Object.assign({}, options || {}); const filepath = normalized.filepath; - normalized.plugins = loadPlugins(normalized); + normalized.plugins = loadPlugins(normalized.plugins); if ( filepath && @@ -54,7 +54,10 @@ function normalize(options) { const extension = path.extname(filepath); const filename = path.basename(filepath).toLowerCase(); - const language = getSupportInfo(null, normalized).languages.find( + const language = getSupportInfo(null, { + plugins: normalized.plugins, + pluginsLoaded: true + }).languages.find( language => typeof language.since === "string" && (language.extensions.indexOf(extension) > -1 || diff --git a/tests_config/fs.js b/tests_config/fs.js new file mode 100644 index 00000000..7ffea821 --- /dev/null +++ b/tests_config/fs.js @@ -0,0 +1,3 @@ +"use strict"; + +module.exports = require("fs"); diff --git a/tests_integration/__tests__/plugin-extensions.js b/tests_integration/__tests__/plugin-extensions.js new file mode 100644 index 00000000..df39fcb4 --- /dev/null +++ b/tests_integration/__tests__/plugin-extensions.js @@ -0,0 +1,12 @@ +"use strict"; + +const runPrettier = require("../runPrettier"); + +describe("uses 'extensions' from languages to determine parser", () => { + runPrettier("plugins/extensions", ["*.foo", "--plugin=./plugin"]).test({ + stdout: "!contents\n", + stderr: "", + status: 0, + write: [] + }); +}); diff --git a/tests_integration/__tests__/plugin-resolution.js b/tests_integration/__tests__/plugin-resolution.js new file mode 100644 index 00000000..2358f651 --- /dev/null +++ b/tests_integration/__tests__/plugin-resolution.js @@ -0,0 +1,21 @@ +"use strict"; + +const runPrettier = require("../runPrettier"); + +describe("automatically loads 'prettier-plugin-*' from package.json devDependencies", () => { + runPrettier("plugins/automatic", ["file.txt", "--parser=foo"]).test({ + stdout: "foo+contents\n", + stderr: "", + status: 0, + write: [] + }); +}); + +describe("automatically loads '@prettier/plugin-*' from package.json dependencies", () => { + runPrettier("plugins/automatic", ["file.txt", "--parser=bar"]).test({ + stdout: "bar+contents\n", + stderr: "", + status: 0, + write: [] + }); +}); diff --git a/tests_integration/plugins/automatic/file.txt b/tests_integration/plugins/automatic/file.txt new file mode 100644 index 00000000..12f00e90 --- /dev/null +++ b/tests_integration/plugins/automatic/file.txt @@ -0,0 +1 @@ +contents diff --git a/tests_integration/plugins/automatic/node_modules/@prettier/plugin-foo/index.js b/tests_integration/plugins/automatic/node_modules/@prettier/plugin-foo/index.js new file mode 100644 index 00000000..080b0e6c --- /dev/null +++ b/tests_integration/plugins/automatic/node_modules/@prettier/plugin-foo/index.js @@ -0,0 +1,23 @@ +"use strict"; + +const concat = require("../../../../../../src/doc").builders.concat; + +module.exports = { + languages: [ + { + name: "foo", + parsers: ["foo"] + } + ], + parsers: { + foo: { + parse: text => ({ text }), + astFormat: "foo" + } + }, + printers: { + foo: { + print: path => concat(["foo+", path.getValue().text]) + } + } +}; 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 new file mode 100644 index 00000000..b878e8e3 --- /dev/null +++ b/tests_integration/plugins/automatic/node_modules/prettier-plugin-bar/index.js @@ -0,0 +1,23 @@ +"use strict"; + +const concat = require("../../../../../src/doc").builders.concat; + +module.exports = { + languages: [ + { + name: "bar", + parsers: ["bar"] + } + ], + parsers: { + bar: { + parse: text => ({ text }), + astFormat: "bar" + } + }, + printers: { + bar: { + print: path => concat(["bar+", path.getValue().text]) + } + } +}; diff --git a/tests_integration/plugins/automatic/package.json b/tests_integration/plugins/automatic/package.json new file mode 100644 index 00000000..52178d8b --- /dev/null +++ b/tests_integration/plugins/automatic/package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "@prettier/plugin-foo": "*" + }, + "devDependencies": { + "prettier-plugin-bar": "*" + } +} diff --git a/tests_integration/plugins/extensions/file.foo b/tests_integration/plugins/extensions/file.foo new file mode 100644 index 00000000..12f00e90 --- /dev/null +++ b/tests_integration/plugins/extensions/file.foo @@ -0,0 +1 @@ +contents diff --git a/tests_integration/plugins/extensions/plugin.js b/tests_integration/plugins/extensions/plugin.js new file mode 100644 index 00000000..39d17d44 --- /dev/null +++ b/tests_integration/plugins/extensions/plugin.js @@ -0,0 +1,25 @@ +"use strict"; + +const concat = require("../../../src/doc").builders.concat; + +module.exports = { + languages: [ + { + name: "foo", + parsers: ["foo-parser"], + extensions: [".foo"], + since: "1.0.0" + } + ], + parsers: { + "foo-parser": { + parse: text => ({ text }), + astFormat: "foo-ast" + } + }, + printers: { + "foo-ast": { + print: path => concat(["!", path.getValue().text]) + } + } +}; diff --git a/yarn.lock b/yarn.lock index eff17b2e..f9f20404 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2825,6 +2825,10 @@ 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@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" @@ -2920,6 +2924,15 @@ 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" @@ -3388,6 +3401,13 @@ 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" @@ -3448,6 +3468,12 @@ 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" @@ -3681,6 +3707,13 @@ 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" @@ -3711,6 +3744,14 @@ 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"