From b1187196de0755417f12d81aa72ea2629d946371 Mon Sep 17 00:00:00 2001 From: Alexey Lavinsky Date: Thu, 10 Jun 2021 08:26:54 -0700 Subject: [PATCH] feat: allow to use `String` value for the `implementation option --- README.md | 59 +++++++++++++++++++ src/index.js | 13 +++- src/options.json | 11 ++++ src/utils.js | 22 +++++++ .../implementation-option.test.js.snap | 42 +++++++++++++ .../validate-options.test.js.snap | 56 +++++++++++++++--- test/implementation-option.test.js | 58 ++++++++++++++++++ test/validate-options.test.js | 5 ++ 8 files changed, 255 insertions(+), 11 deletions(-) create mode 100644 test/__snapshots__/implementation-option.test.js.snap create mode 100644 test/implementation-option.test.js diff --git a/README.md b/README.md index 2b11cb9..41425e4 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ And run `webpack` via your preferred method. | **[`sourceMap`](#sourcemap)** | `{Boolean}` | `compiler.devtool` | Enables/Disables generation of source maps. | | **[`webpackImporter`](#webpackimporter)** | `{Boolean}` | `true` | Enables/Disables the default Webpack importer. | | **[`additionalData`](#additionalData)** | `{String\|Function}` | `undefined` | Prepends/Appends `Stylus` code to the actual entry file. | +| **[`implementation`](#implementation)** | `{String\|Function}` | `stylus` | Setup Stylus implementation to use. | ### `stylusOptions` @@ -398,6 +399,64 @@ module.exports = { }; ``` +### `implementation` + +Type: `Function | String` + +The special `implementation` option determines which implementation of Stylus to use. Overrides the locally installed `peerDependency` version of `stylus`. + +#### Function + +**webpack.config.js** + +```js +module.exports = { + module: { + rules: [ + { + test: /\.styl/i, + use: [ + "style-loader", + "css-loader", + { + loader: "stylus-loader", + options: { + implementation: require("stylus"), + }, + }, + ], + }, + ], + }, +}; +``` + +#### String + +**webpack.config.js** + +```js +module.exports = { + module: { + rules: [ + { + test: /\.styl/i, + use: [ + "style-loader", + "css-loader", + { + loader: "stylus-loader", + options: { + implementation: require.resolve("stylus"), + }, + }, + ], + }, + ], + }, +}; +``` + ## Examples ### Normal usage diff --git a/src/index.js b/src/index.js index 3ee692c..b087ea5 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,5 @@ import path from "path"; -import stylus from "stylus"; - import schema from "./options.json"; import { getStylusOptions, @@ -9,11 +7,20 @@ import { urlResolver, readFile, normalizeSourceMap, + getStylusImplementation, } from "./utils"; export default async function stylusLoader(source) { const options = this.getOptions(schema); const callback = this.async(); + const implementation = getStylusImplementation(this, options.implementation); + + if (!implementation) { + callback(); + + return; + } + let data = source; if (typeof options.additionalData !== "undefined") { @@ -24,7 +31,7 @@ export default async function stylusLoader(source) { } const stylusOptions = getStylusOptions(this, options); - const styl = stylus(data, stylusOptions); + const styl = implementation(data, stylusOptions); // include regular CSS on @import if (stylusOptions.includeCSS) { diff --git a/src/options.json b/src/options.json index cd8b1d6..63a57a2 100644 --- a/src/options.json +++ b/src/options.json @@ -2,6 +2,17 @@ "title": "Stylus Loader options", "type": "object", "properties": { + "implementation": { + "description": "The implementation of the `Stylus` to be used (https://github.com/webpack-contrib/stylus-loade#implementation).", + "anyOf": [ + { + "type": "string" + }, + { + "instanceof": "Function" + } + ] + }, "stylusOptions": { "description": "Options to pass through to `Stylus` (https://github.com/webpack-contrib/stylus-loader#stylusoptions).", "anyOf": [ diff --git a/src/utils.js b/src/utils.js index ab4e46e..2ce5b22 100644 --- a/src/utils.js +++ b/src/utils.js @@ -54,6 +54,27 @@ function getStylusOptions(loaderContext, loaderOptions) { return stylusOptions; } +function getStylusImplementation(loaderContext, implementation) { + let resolvedImplementation = implementation; + + if (!implementation || typeof implementation === "string") { + const stylusImplPkg = implementation || "stylus"; + + try { + // eslint-disable-next-line import/no-dynamic-require, global-require + resolvedImplementation = require(stylusImplPkg); + } catch (error) { + loaderContext.emitError(error); + + // eslint-disable-next-line consistent-return + return; + } + } + + // eslint-disable-next-line consistent-return + return resolvedImplementation; +} + function getPossibleRequests(loaderContext, filename) { let request = filename; @@ -710,4 +731,5 @@ export { resolveFilename, readFile, normalizeSourceMap, + getStylusImplementation, }; diff --git a/test/__snapshots__/implementation-option.test.js.snap b/test/__snapshots__/implementation-option.test.js.snap new file mode 100644 index 0000000..c43feac --- /dev/null +++ b/test/__snapshots__/implementation-option.test.js.snap @@ -0,0 +1,42 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`implementation option should throw error when unresolved package: errors 1`] = ` +Array [ + "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 when implementation option is string: css 1`] = ` +"body { + font: 12px Helvetica, Arial, sans-serif; +} +a.button { + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +" +`; + +exports[`implementation option should work when implementation option is string: errors 1`] = `Array []`; + +exports[`implementation option should work when implementation option is string: warnings 1`] = `Array []`; + +exports[`implementation option should work: css 1`] = ` +"body { + font: 12px Helvetica, Arial, sans-serif; +} +a.button { + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +" +`; + +exports[`implementation option should work: errors 1`] = `Array []`; + +exports[`implementation option should work: warnings 1`] = `Array []`; diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap index 98ef345..9367263 100644 --- a/test/__snapshots__/validate-options.test.js.snap +++ b/test/__snapshots__/validate-options.test.js.snap @@ -60,6 +60,46 @@ exports[`validate options should throw an error on the "additionalData" option w * options.additionalData should be an instance of function." `; +exports[`validate options should throw an error on the "implementation" option with "[]" value 1`] = ` +"Invalid options object. Stylus Loader has been initialized using an options object that does not match the API schema. + - options.implementation should be one of these: + string | function + -> The implementation of the \`Stylus\` to be used (https://github.com/webpack-contrib/stylus-loade#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. Stylus Loader has been initialized using an options object that does not match the API schema. + - options.implementation should be one of these: + string | function + -> The implementation of the \`Stylus\` to be used (https://github.com/webpack-contrib/stylus-loade#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 "false" value 1`] = ` +"Invalid options object. Stylus Loader has been initialized using an options object that does not match the API schema. + - options.implementation should be one of these: + string | function + -> The implementation of the \`Stylus\` to be used (https://github.com/webpack-contrib/stylus-loade#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 "true" value 1`] = ` +"Invalid options object. Stylus Loader has been initialized using an options object that does not match the API schema. + - options.implementation should be one of these: + string | function + -> The implementation of the \`Stylus\` to be used (https://github.com/webpack-contrib/stylus-loade#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 "sourceMap" option with "string" value 1`] = ` "Invalid options object. Stylus Loader has been initialized using an options object that does not match the API schema. - options.sourceMap should be a boolean. @@ -124,49 +164,49 @@ exports[`validate options should throw an error on the "stylusOptions" option wi exports[`validate options should throw an error on the "unknown" option with "/test/" value 1`] = ` "Invalid options object. Stylus Loader has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { stylusOptions?, sourceMap?, webpackImporter?, additionalData? }" + object { implementation?, stylusOptions?, sourceMap?, webpackImporter?, additionalData? }" `; exports[`validate options should throw an error on the "unknown" option with "[]" value 1`] = ` "Invalid options object. Stylus Loader has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { stylusOptions?, sourceMap?, webpackImporter?, additionalData? }" + object { implementation?, stylusOptions?, sourceMap?, webpackImporter?, additionalData? }" `; exports[`validate options should throw an error on the "unknown" option with "{"foo":"bar"}" value 1`] = ` "Invalid options object. Stylus Loader has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { stylusOptions?, sourceMap?, webpackImporter?, additionalData? }" + object { implementation?, stylusOptions?, sourceMap?, webpackImporter?, additionalData? }" `; exports[`validate options should throw an error on the "unknown" option with "{}" value 1`] = ` "Invalid options object. Stylus Loader has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { stylusOptions?, sourceMap?, webpackImporter?, additionalData? }" + object { implementation?, stylusOptions?, sourceMap?, webpackImporter?, additionalData? }" `; exports[`validate options should throw an error on the "unknown" option with "1" value 1`] = ` "Invalid options object. Stylus Loader has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { stylusOptions?, sourceMap?, webpackImporter?, additionalData? }" + object { implementation?, stylusOptions?, sourceMap?, webpackImporter?, additionalData? }" `; exports[`validate options should throw an error on the "unknown" option with "false" value 1`] = ` "Invalid options object. Stylus Loader has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { stylusOptions?, sourceMap?, webpackImporter?, additionalData? }" + object { implementation?, stylusOptions?, sourceMap?, webpackImporter?, additionalData? }" `; exports[`validate options should throw an error on the "unknown" option with "test" value 1`] = ` "Invalid options object. Stylus Loader has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { stylusOptions?, sourceMap?, webpackImporter?, additionalData? }" + object { implementation?, stylusOptions?, sourceMap?, webpackImporter?, additionalData? }" `; exports[`validate options should throw an error on the "unknown" option with "true" value 1`] = ` "Invalid options object. Stylus Loader has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { stylusOptions?, sourceMap?, webpackImporter?, additionalData? }" + object { implementation?, stylusOptions?, sourceMap?, webpackImporter?, additionalData? }" `; exports[`validate options should throw an error on the "webpackImporter" option with "string" value 1`] = ` diff --git a/test/implementation-option.test.js b/test/implementation-option.test.js new file mode 100644 index 0000000..7365969 --- /dev/null +++ b/test/implementation-option.test.js @@ -0,0 +1,58 @@ +/** + * @jest-environment node + */ + +import { + compile, + getCodeFromBundle, + getCodeFromStylus, + getCompiler, + getErrors, + getWarnings, +} from "./helpers"; + +jest.setTimeout(30000); + +describe("implementation option", () => { + it("should work", async () => { + const testId = "./basic.styl"; + const compiler = getCompiler(testId, { + // eslint-disable-next-line global-require + implementation: require("stylus"), + }); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + const codeFromStylus = await getCodeFromStylus(testId); + + expect(codeFromBundle.css).toBe(codeFromStylus.css); + expect(codeFromBundle.css).toMatchSnapshot("css"); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + }); + + it("should work when implementation option is string", async () => { + const testId = "./basic.styl"; + const compiler = getCompiler(testId, { + implementation: require.resolve("stylus"), + }); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + const codeFromStylus = await getCodeFromStylus(testId); + + expect(codeFromBundle.css).toBe(codeFromStylus.css); + expect(codeFromBundle.css).toMatchSnapshot("css"); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + }); + + it("should throw error when unresolved package", async () => { + const testId = "./basic.styl"; + const compiler = getCompiler(testId, { + implementation: "unresolved", + }); + const stats = await compile(compiler); + + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + }); +}); diff --git a/test/validate-options.test.js b/test/validate-options.test.js index 7bd5cad..5a48cba 100644 --- a/test/validate-options.test.js +++ b/test/validate-options.test.js @@ -38,6 +38,11 @@ describe("validate options", () => { success: ["color = coral", () => "bg = coral"], failure: [1, true, false, /test/, [], {}], }, + implementation: { + // eslint-disable-next-line global-require + success: [require("stylus"), "stylus"], + failure: [true, false, {}, []], + }, unknown: { success: [], failure: [1, true, false, "test", /test/, [], {}, { foo: "bar" }],