From 0d342b16dabf58c499da4e13310fdfa5c05badd9 Mon Sep 17 00:00:00 2001 From: Alexey Lavinsky Date: Thu, 10 Jun 2021 11:15:10 -0700 Subject: [PATCH] feat: allow `String` value for the "implementation" option --- README.md | 29 ++++++++- src/index.js | 14 ++++- src/options.json | 9 ++- src/utils.js | 22 +++++++ .../__snapshots__/implementation.test.js.snap | 63 +++++++++++++++++++ test/__snapshots__/loader.test.js.snap | 8 +-- .../validate-options.test.js.snap | 38 ++++++----- test/implementation.test.js | 22 +++++++ test/validate-options.test.js | 4 +- 9 files changed, 185 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 321c507b..c2207791 100644 --- a/README.md +++ b/README.md @@ -597,12 +597,14 @@ module.exports = { ### `implementation` -Type: `Function` +Type: `Function | String` The special `implementation` option determines which implementation of PostCSS to use. Overrides the locally installed `peerDependency` version of `postcss`. **This option is only really useful for downstream tooling authors to ease the PostCSS 7-to-8 transition.** +#### Function + **webpack.config.js** ```js @@ -626,6 +628,31 @@ module.exports = { }; ``` +#### String + +**webpack.config.js** + +```js +module.exports = { + module: { + rules: [ + { + test: /\.css$/i, + use: [ + { loader: "style-loader" }, + { loader: "css-loader" }, + { + loader: "postcss-loader", + options: { implementation: require.resolve("postcss") }, + }, + { loader: "sass-loader" }, + ], + }, + ], + }, +}; +``` + ## Examples ### SugarSS diff --git a/src/index.js b/src/index.js index 3a7f2a30..4e4d6e30 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,5 @@ import path from "path"; -import postcss from "postcss"; import { satisfies } from "semver"; import postcssPackage from "postcss/package.json"; @@ -14,6 +13,7 @@ import { normalizeSourceMap, normalizeSourceMapAfterPostcss, findPackageJSONDir, + getPostcssImplementation, } from "./utils"; let hasExplicitDependencyOnPostCSS = false; @@ -40,7 +40,17 @@ export default async function loader(content, sourceMap, meta) { ? true : options.postcssOptions.config; - const postcssFactory = options.implementation || postcss; + const postcssFactory = getPostcssImplementation(this, options.implementation); + + if (!postcssFactory) { + callback( + new Error( + `The Postcss implementation "${options.implementation}" not found` + ) + ); + + return; + } let loadedConfig; diff --git a/src/options.json b/src/options.json index 3c2a6ef8..6a3220d3 100644 --- a/src/options.json +++ b/src/options.json @@ -39,7 +39,14 @@ }, "implementation": { "description": "The implementation of postcss to use, instead of the locally installed version (https://github.com/postcss/postcss-loader#implementation)", - "instanceof": "Function" + "anyOf": [ + { + "type": "string" + }, + { + "instanceof": "Function" + } + ] } }, "additionalProperties": false diff --git a/src/utils.js b/src/utils.js index 0e5d9214..03cebe57 100644 --- a/src/utils.js +++ b/src/utils.js @@ -433,6 +433,27 @@ function findPackageJSONDir(cwd, statSync) { return dir; } +function getPostcssImplementation(loaderContext, implementation) { + let resolvedImplementation = implementation; + + if (!implementation || typeof implementation === "string") { + const postcssImplPkg = implementation || "postcss"; + + try { + // eslint-disable-next-line import/no-dynamic-require, global-require + resolvedImplementation = require(postcssImplPkg); + } catch (error) { + loaderContext.emitError(error); + + // eslint-disable-next-line consistent-return + return; + } + } + + // eslint-disable-next-line consistent-return + return resolvedImplementation; +} + export { loadConfig, getPostcssOptions, @@ -440,4 +461,5 @@ export { normalizeSourceMap, normalizeSourceMapAfterPostcss, findPackageJSONDir, + getPostcssImplementation, }; diff --git a/test/__snapshots__/implementation.test.js.snap b/test/__snapshots__/implementation.test.js.snap index fb74cc52..f27b0afc 100644 --- a/test/__snapshots__/implementation.test.js.snap +++ b/test/__snapshots__/implementation.test.js.snap @@ -1,5 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`"implementation" option should throw error when unresolved package: errors 1`] = ` +Array [ + "ModuleBuildError: Module build failed (from \`replaced original path\`): +Error: The Postcss implementation \\"unresolved\\" not found + at Object.loader (/src/index.js:47:7)", + "ModuleError: Module Error (from \`replaced original path\`): +(Emitted value instead of an instance of Error) Error: Cannot find module 'unresolved' from 'src/utils.js'", +] +`; + +exports[`"implementation" option should throw error when unresolved package: warnings 1`] = `Array []`; + exports[`"implementation" option should work with a custom instance of PostCSS: css 1`] = ` "a { color: black; @@ -50,3 +62,54 @@ a { exports[`"implementation" option should work with a custom instance of PostCSS: errors 1`] = `Array []`; exports[`"implementation" option should work with a custom instance of PostCSS: warnings 1`] = `Array []`; + +exports[`"implementation" option should work with implementation is string: css 1`] = ` +"a { + color: black; +} + +a { + color: red; +} + +a { + color: green; +} + +a { + color: blue; +} + +.class { + -x-border-color: blue blue *; + -x-color: * #fafafa; +} + +.class-foo { + -z-border-color: blue blue *; + -z-color: * #fafafa; +} + +.phone { + &_title { + width: 500px; + + @media (max-width: 500px) { + width: auto; + } + + body.is_dark & { + color: white; + } + } + + img { + display: block; + } +} +" +`; + +exports[`"implementation" option should work with implementation is string: errors 1`] = `Array []`; + +exports[`"implementation" option should work with implementation is string: warnings 1`] = `Array []`; diff --git a/test/__snapshots__/loader.test.js.snap b/test/__snapshots__/loader.test.js.snap index c90c167a..48790a2d 100644 --- a/test/__snapshots__/loader.test.js.snap +++ b/test/__snapshots__/loader.test.js.snap @@ -5,7 +5,7 @@ Array [ "ModuleBuildError: Module build failed (from \`replaced original path\`): Error: Something went wrong. at Processor.process (/test/loader.test.js:216:26) - at Object.loader (/src/index.js:106:30)", + at Object.loader (/src/index.js:116:30)", ] `; @@ -21,7 +21,7 @@ Array [ "ModuleBuildError: Module build failed (from \`replaced original path\`): Error: Something went wrong. at Processor.process (/test/loader.test.js:300:26) - at Object.loader (/src/index.js:106:30)", + at Object.loader (/src/index.js:116:30)", ] `; @@ -32,7 +32,7 @@ Array [ "ModuleBuildError: Module build failed (from \`replaced original path\`): Error: Something went wrong. at Processor.process (/test/loader.test.js:245:26) - at Object.loader (/src/index.js:106:30)", + at Object.loader (/src/index.js:116:30)", ] `; @@ -43,7 +43,7 @@ Array [ "ModuleBuildError: Module build failed (from \`replaced original path\`): Error: Something went wrong. at Processor.process (/test/loader.test.js:274:26) - at Object.loader (/src/index.js:106:30)", + at Object.loader (/src/index.js:116:30)", ] `; diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap index adc6357f..848127c4 100644 --- a/test/__snapshots__/validate-options.test.js.snap +++ b/test/__snapshots__/validate-options.test.js.snap @@ -38,32 +38,42 @@ exports[`validate options should throw an error on the "execute" option with "te exports[`validate options should throw an error on the "implementation" option with "/test/" value 1`] = ` "Invalid options object. PostCSS Loader has been initialized using an options object that does not match the API schema. - - options.implementation should be an instance of function. - -> The implementation of postcss to use, instead of the locally installed version (https://github.com/postcss/postcss-loader#implementation)" + - options.implementation should be one of these: + string | function + -> The implementation of postcss to use, instead of the locally installed version (https://github.com/postcss/postcss-loader#implementation) + Details: + * options.implementation should be a string. + * options.implementation should be an instance of function." `; exports[`validate options should throw an error on the "implementation" option with "[]" value 1`] = ` "Invalid options object. PostCSS Loader has been initialized using an options object that does not match the API schema. - - options.implementation should be an instance of function. - -> The implementation of postcss to use, instead of the locally installed version (https://github.com/postcss/postcss-loader#implementation)" + - options.implementation should be one of these: + string | function + -> The implementation of postcss to use, instead of the locally installed version (https://github.com/postcss/postcss-loader#implementation) + Details: + * options.implementation should be a string. + * options.implementation should be an instance of function." `; exports[`validate options should throw an error on the "implementation" option with "{}" value 1`] = ` "Invalid options object. PostCSS Loader has been initialized using an options object that does not match the API schema. - - options.implementation should be an instance of function. - -> The implementation of postcss to use, instead of the locally installed version (https://github.com/postcss/postcss-loader#implementation)" + - options.implementation should be one of these: + string | function + -> The implementation of postcss to use, instead of the locally installed version (https://github.com/postcss/postcss-loader#implementation) + Details: + * options.implementation should be a string. + * options.implementation should be an instance of function." `; exports[`validate options should throw an error on the "implementation" option with "1" value 1`] = ` "Invalid options object. PostCSS Loader has been initialized using an options object that does not match the API schema. - - options.implementation should be an instance of function. - -> The implementation of postcss to use, instead of the locally installed version (https://github.com/postcss/postcss-loader#implementation)" -`; - -exports[`validate options should throw an error on the "implementation" option with "something" value 1`] = ` -"Invalid options object. PostCSS Loader has been initialized using an options object that does not match the API schema. - - options.implementation should be an instance of function. - -> The implementation of postcss to use, instead of the locally installed version (https://github.com/postcss/postcss-loader#implementation)" + - options.implementation should be one of these: + string | function + -> The implementation of postcss to use, instead of the locally installed version (https://github.com/postcss/postcss-loader#implementation) + Details: + * options.implementation should be a string. + * options.implementation should be an instance of function." `; exports[`validate options should throw an error on the "postcssOptions" option with "{"config":[]}" value 1`] = ` diff --git a/test/implementation.test.js b/test/implementation.test.js index 57832cc3..f1378c66 100644 --- a/test/implementation.test.js +++ b/test/implementation.test.js @@ -9,6 +9,28 @@ import { } from "./helpers"; describe('"implementation" option', () => { + it("should work with implementation is string", async () => { + const compiler = getCompiler("./css/index.js", { + implementation: require.resolve("postcss"), + }); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle("style.css", stats); + + expect(codeFromBundle.css).toMatchSnapshot("css"); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + }); + + it("should throw error when unresolved package", async () => { + const compiler = getCompiler("./css/index.js", { + implementation: "unresolved", + }); + const stats = await compile(compiler); + + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + }); + it("should work with a custom instance of PostCSS", async () => { const spy = jest.fn(postcss); const compiler = getCompiler("./css/index.js", { diff --git a/test/validate-options.test.js b/test/validate-options.test.js index 7b6c68ea..8122f5bf 100644 --- a/test/validate-options.test.js +++ b/test/validate-options.test.js @@ -51,8 +51,8 @@ describe("validate options", () => { failure: [1, /test/, [], {}, "something"], }, implementation: { - success: [require("postcss")], - failure: [1, /test/, [], {}, "something"], + success: [require("postcss"), "postcss"], + failure: [1, /test/, [], {}], }, };