From 6980095a97819a816fb8418d8252b4ee7779eec8 Mon Sep 17 00:00:00 2001 From: Alexey Lavinsky Date: Wed, 9 Sep 2020 15:07:34 +0300 Subject: [PATCH] feat: add loader options validation (#234) --- src/index.js | 17 ++-- src/options.json | 8 +- src/utils.js | 9 -- .../validate-options.test.js.snap | 90 +++++++++++++++++++ test/validate-options.test.js | 74 +++++++++++++++ 5 files changed, 182 insertions(+), 16 deletions(-) create mode 100644 test/__snapshots__/validate-options.test.js.snap create mode 100644 test/validate-options.test.js diff --git a/src/index.js b/src/index.js index ede5afd..96c1771 100644 --- a/src/index.js +++ b/src/index.js @@ -4,18 +4,23 @@ import stylus from 'stylus'; import clone from 'clone'; +import { getOptions } from 'loader-utils'; +import validateOptions from 'schema-utils'; + +import schema from './options.json'; import createEvaluator from './evaluator'; -import { getOptions, isObject, castArray } from './utils'; +import { isObject, castArray } from './utils'; import resolver from './lib/resolver'; export default async function stylusLoader(source) { - const callback = this.async(); + const options = getOptions(this); - // get options passed to loader - const loaderOptions = getOptions(this); + validateOptions(schema, options, { + name: 'Stylus Loader', + baseDataPath: 'options', + }); - // clone loader options to avoid modifying this.query - const options = loaderOptions ? { ...loaderOptions } : {}; + const callback = this.async(); const stylusOptions = clone(options.stylusOptions) || {}; diff --git a/src/options.json b/src/options.json index 066dc78..667b953 100644 --- a/src/options.json +++ b/src/options.json @@ -1,7 +1,13 @@ { "type": "object", "properties": { - "name": { + "stylusOptions": { + "description": "Options to pass through to `Stylus` (https://github.com/webpack-contrib/stylus-loader#stylusoptions).", + "type": "object", + "additionalProperties": true + }, + "sourceMap": { + "description": "Enables/Disables generation of source maps (https://github.com/webpack-contrib/stylus-loader#sourcemap).", "type": "boolean" } }, diff --git a/src/utils.js b/src/utils.js index 5bfc7a7..1fb05fe 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,12 +1,3 @@ -export function getOptions(context) { - if (typeof context.getOptions === 'function') { - return context.getOptions(); - } - - // eslint-disable-next-line global-require - return require('loader-utils').getOptions(context); -} - export function isObject(value) { return typeof value === 'object' && value !== null; } diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap new file mode 100644 index 0000000..ae2837b --- /dev/null +++ b/test/__snapshots__/validate-options.test.js.snap @@ -0,0 +1,90 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +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. + -> Enables/Disables generation of source maps (https://github.com/webpack-contrib/stylus-loader#sourcemap)." +`; + +exports[`validate options should throw an error on the "stylusOptions" option with "[]" value 1`] = ` +"Invalid options object. Stylus Loader has been initialized using an options object that does not match the API schema. + - options.stylusOptions should be an object: + object { … } + -> Options to pass through to \`Stylus\` (https://github.com/webpack-contrib/stylus-loader#stylusoptions)." +`; + +exports[`validate options should throw an error on the "stylusOptions" 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.stylusOptions should be an object: + object { … } + -> Options to pass through to \`Stylus\` (https://github.com/webpack-contrib/stylus-loader#stylusoptions)." +`; + +exports[`validate options should throw an error on the "stylusOptions" 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.stylusOptions should be an object: + object { … } + -> Options to pass through to \`Stylus\` (https://github.com/webpack-contrib/stylus-loader#stylusoptions)." +`; + +exports[`validate options should throw an error on the "stylusOptions" 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.stylusOptions should be an object: + object { … } + -> Options to pass through to \`Stylus\` (https://github.com/webpack-contrib/stylus-loader#stylusoptions)." +`; + +exports[`validate options should throw an error on the "stylusOptions" 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.stylusOptions should be an object: + object { … } + -> Options to pass through to \`Stylus\` (https://github.com/webpack-contrib/stylus-loader#stylusoptions)." +`; + +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? }" +`; + +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? }" +`; + +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? }" +`; + +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? }" +`; + +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? }" +`; + +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? }" +`; + +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? }" +`; + +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? }" +`; diff --git a/test/validate-options.test.js b/test/validate-options.test.js new file mode 100644 index 0000000..504a29f --- /dev/null +++ b/test/validate-options.test.js @@ -0,0 +1,74 @@ +import { getCompiler, compile } from './helpers/index'; + +describe('validate options', () => { + const tests = { + stylusOptions: { + success: [ + {}, + { resolveCss: true }, + { includeCSS: false }, + { + define: { + $development: process.env.NODE_ENV === 'development', + }, + }, + ], + failure: [1, true, false, 'test', []], + }, + sourceMap: { + success: [true, false], + failure: ['string'], + }, + unknown: { + success: [], + failure: [1, true, false, 'test', /test/, [], {}, { foo: 'bar' }], + }, + }; + + function stringifyValue(value) { + if ( + Array.isArray(value) || + (value && typeof value === 'object' && value.constructor === Object) + ) { + return JSON.stringify(value); + } + + return value; + } + + async function createTestCase(key, value, type) { + it(`should ${ + type === 'success' ? 'successfully validate' : 'throw an error on' + } the "${key}" option with "${stringifyValue(value)}" value`, async () => { + const compiler = getCompiler('./basic.styl', { + [key]: value, + }); + let stats; + + try { + stats = await compile(compiler); + } finally { + if (type === 'success') { + expect(stats.hasErrors()).toBe(false); + } else if (type === 'failure') { + const { + compilation: { errors }, + } = stats; + + expect(errors).toHaveLength(1); + expect(() => { + throw new Error(errors[0].error.message); + }).toThrowErrorMatchingSnapshot(); + } + } + }); + } + + for (const [key, values] of Object.entries(tests)) { + for (const type of Object.keys(values)) { + for (const value of values[type]) { + createTestCase(key, value, type); + } + } + } +});