diff --git a/README.md b/README.md index 063a43e0..4cc51d26 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ module.exports = { Type: `Object|Function` -Setups options for [Node Sass](https://github.com/sass/node-sass) or [Dart Sass](http://sass-lang.com/dart-sass). +Options for [Node Sass](https://github.com/sass/node-sass) or [Dart Sass](http://sass-lang.com/dart-sass) implementation. > ℹ️ The `indentedSyntax` option has `true` value for the `sass` extension. @@ -403,7 +403,7 @@ module.exports = { Type: `Boolean` Default: `true` -Allows to disable default `webpack` importer. +Enables/Disables default `webpack` importer. This can improve performance in some cases. Use it with caution because aliases and `@import` at-rules starts with `~` will not work, but you can pass own `importer` to solve this (see [`importer docs`](https://github.com/sass/node-sass#importer--v200---experimental)). @@ -476,6 +476,8 @@ module.exports = { ### Source maps +Enables/Disables generation of source maps. + To enable CSS source maps, you'll need to pass the `sourceMap` option to the sass-loader _and_ the css-loader. **webpack.config.js** diff --git a/package-lock.json b/package-lock.json index 036d0d8f..9baa661f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1716,6 +1716,17 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } } } }, @@ -1809,7 +1820,6 @@ "version": "6.10.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "dev": true, "requires": { "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", @@ -1826,8 +1836,7 @@ "ajv-keywords": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", - "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", - "dev": true + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==" }, "amdefine": { "version": "1.0.1", @@ -5108,8 +5117,7 @@ "fast-deep-equal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" }, "fast-glob": { "version": "3.0.4", @@ -5182,8 +5190,7 @@ "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, "fast-levenshtein": { "version": "2.0.6", @@ -8279,8 +8286,7 @@ "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -10675,8 +10681,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "q": { "version": "1.5.1", @@ -11480,13 +11485,11 @@ "dev": true }, "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.1.0.tgz", + "integrity": "sha512-g6SViEZAfGNrToD82ZPUjq52KUPDYc+fN5+g6Euo5mLokl/9Yx14z0Cu4RR1m55HtBXejO0sBt+qw79axN+Fiw==", "requires": { "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", "ajv-keywords": "^3.1.0" } }, @@ -12595,6 +12598,17 @@ "worker-farm": "^1.7.0" }, "dependencies": { + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -13044,7 +13058,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, "requires": { "punycode": "^2.1.0" } @@ -13272,6 +13285,17 @@ "esrecurse": "^4.1.0", "estraverse": "^4.1.1" } + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } } } }, @@ -13701,6 +13725,17 @@ "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", "dev": true }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", diff --git a/package.json b/package.json index 88514826..f6932bfb 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "clone-deep": "^4.0.1", "loader-utils": "^1.2.3", "neo-async": "^2.6.1", + "schema-utils": "^2.1.0", "semver": "^6.3.0" }, "devDependencies": { diff --git a/src/getSassOptions.js b/src/getSassOptions.js index 76f97dee..1fb4f584 100644 --- a/src/getSassOptions.js +++ b/src/getSassOptions.js @@ -25,7 +25,7 @@ function getSassOptions(loaderContext, loaderOptions, content) { const options = cloneDeep( loaderOptions.sassOptions ? typeof loaderOptions.sassOptions === 'function' - ? loaderOptions.sassOptions(loaderContext) + ? loaderOptions.sassOptions(loaderContext) || {} : loaderOptions.sassOptions : {} ); diff --git a/src/index.js b/src/index.js index 670298fe..21028b1f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,11 @@ import path from 'path'; +import validateOptions from 'schema-utils'; import async from 'neo-async'; import semver from 'semver'; import { getOptions } from 'loader-utils'; +import schema from './options.json'; import formatSassError from './formatSassError'; import webpackImporter from './webpackImporter'; import getSassOptions from './getSassOptions'; @@ -19,6 +21,11 @@ let nodeSassJobQueue = null; function loader(content) { const options = getOptions(this) || {}; + validateOptions(schema, options, { + name: 'Sass Loader', + baseDataPath: 'options', + }); + const callback = this.async(); const addNormalizedDependency = (file) => { // node-sass returns POSIX paths diff --git a/src/options.json b/src/options.json new file mode 100644 index 00000000..82f092b2 --- /dev/null +++ b/src/options.json @@ -0,0 +1,40 @@ +{ + "type": "object", + "properties": { + "implementation": { + "description": "The implementation of the sass to be used (https://github.com/webpack-contrib/sass-loader#implementation)." + }, + "sassOptions": { + "description": "Options for `node-sass` or `sass` (`Dart Sass`) implementation. (https://github.com/webpack-contrib/sass-loader#implementation).", + "anyOf": [ + { + "type": "object", + "additionalProperties": true + }, + { + "instanceof": "Function" + } + ] + }, + "prependData": { + "description": "Prepends `Sass`/`SCSS` code before the actual entry file (https://github.com/webpack-contrib/sass-loader#prependdata).", + "anyOf": [ + { + "type": "string" + }, + { + "instanceof": "Function" + } + ] + }, + "sourceMap": { + "description": "Enables/Disables generation of source maps (https://github.com/webpack-contrib/sass-loader#sourcemap).", + "type": "boolean" + }, + "webpackImporter": { + "description": "Enables/Disables default `webpack` importer (https://github.com/webpack-contrib/sass-loader#webpackimporter).", + "type": "boolean" + } + }, + "additionalProperties": false +} diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap new file mode 100644 index 00000000..2269113b --- /dev/null +++ b/test/__snapshots__/validate-options.test.js.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`validate options 1`] = ` +"Invalid options object. Sass Loader has been initialised using an options object that does not match the API schema. + - options.sassOptions should be one of these: + object { … } | function + -> Options for \`node-sass\` or \`sass\` (\`Dart Sass\`) implementation. (https://github.com/webpack-contrib/sass-loader#implementation). + Details: + * options.sassOptions should be an object: + object { … } + * options.sassOptions should be an instance of function." +`; + +exports[`validate options 2`] = ` +"Invalid options object. Sass Loader has been initialised using an options object that does not match the API schema. + - options.prependData should be one of these: + string | function + -> Prepends \`Sass\`/\`SCSS\` code before the actual entry file (https://github.com/webpack-contrib/sass-loader#prependdata). + Details: + * options.prependData should be a string. + * options.prependData should be an instance of function." +`; + +exports[`validate options 3`] = ` +"Invalid options object. Sass Loader has been initialised using an options object that does not match the API schema. + - options.webpackImporter should be a boolean. + -> Enables/Disables default \`webpack\` importer (https://github.com/webpack-contrib/sass-loader#webpackimporter)." +`; + +exports[`validate options 4`] = ` +"Invalid options object. Sass Loader has been initialised using an options object that does not match the API schema. + - options has an unknown property 'unknown'. These properties are valid: + object { implementation?, sassOptions?, prependData?, sourceMap?, webpackImporter? }" +`; diff --git a/test/validate-options.test.js b/test/validate-options.test.js new file mode 100644 index 00000000..7973982d --- /dev/null +++ b/test/validate-options.test.js @@ -0,0 +1,51 @@ +import loader from '../src/cjs'; + +it('validate options', () => { + const validate = (options) => + loader.call( + Object.assign( + {}, + { + query: options, + loaders: [], + resourcePath: 'file.scss', + getResolve: () => () => {}, + async: () => (error) => { + if (error) { + throw error; + } + }, + } + ), + 'a { color: red; }' + ); + + // eslint-disable-next-line global-require + // expect(() => validate({ implementation: require('node-sass') })).not.toThrow(); + // eslint-disable-next-line global-require + // expect(() => validate({ implementation: require('sass') })).not.toThrow(); + // expect(() => validate({ implementation: true })).not.toThrow(); + + expect(() => validate({ sassOptions: {} })).not.toThrow(); + expect(() => + validate({ + sassOptions: () => { + return {}; + }, + }) + ).not.toThrow(); + expect(() => validate({ sassOptions: () => {} })).not.toThrow(); + expect(() => validate({ sassOptions: true })).toThrowErrorMatchingSnapshot(); + + expect(() => validate({ prependData: '$color: red;' })).not.toThrow(); + expect(() => validate({ prependData: () => '$color: red;' })).not.toThrow(); + expect(() => validate({ prependData: true })).toThrowErrorMatchingSnapshot(); + + expect(() => validate({ webpackImporter: true })).not.toThrow(); + expect(() => validate({ webpackImporter: false })).not.toThrow(); + expect(() => + validate({ webpackImporter: 'unknown' }) + ).toThrowErrorMatchingSnapshot(); + + expect(() => validate({ unknown: 'unknown' })).toThrowErrorMatchingSnapshot(); +});