diff --git a/README.md b/README.md index 7e15d9f5..d7605969 100644 --- a/README.md +++ b/README.md @@ -119,13 +119,14 @@ Thankfully there are a two solutions to this problem: ## Options -| Name | Type | Default | Description | -| :---------------------------------------: | :------------------: | :-------------------------------------: | :---------------------------------------------------------------- | -| **[`implementation`](#implementation)** | `{Object\|String}` | `sass` | Setup Sass implementation to use. | -| **[`sassOptions`](#sassoptions)** | `{Object\|Function}` | defaults values for Sass implementation | Options for Sass. | -| **[`sourceMap`](#sourcemap)** | `{Boolean}` | `compiler.devtool` | Enables/Disables generation of source maps. | -| **[`additionalData`](#additionaldata)** | `{String\|Function}` | `undefined` | Prepends/Appends `Sass`/`SCSS` code before the actual entry file. | -| **[`webpackImporter`](#webpackimporter)** | `{Boolean}` | `true` | Enables/Disables the default Webpack importer. | +| Name | Type | Default | Description | +| :-------------------------------------------: | :------------------: | :-------------------------------------: | :---------------------------------------------------------------- | +| **[`implementation`](#implementation)** | `{Object\|String}` | `sass` | Setup Sass implementation to use. | +| **[`sassOptions`](#sassoptions)** | `{Object\|Function}` | defaults values for Sass implementation | Options for Sass. | +| **[`sourceMap`](#sourcemap)** | `{Boolean}` | `compiler.devtool` | Enables/Disables generation of source maps. | +| **[`additionalData`](#additionaldata)** | `{String\|Function}` | `undefined` | Prepends/Appends `Sass`/`SCSS` code before the actual entry file. | +| **[`webpackImporter`](#webpackimporter)** | `{Boolean}` | `true` | Enables/Disables the default Webpack importer. | +| **[`warnRuleAsWarning`](#warnruleaswarning)** | `{Boolean}` | `false` | Treats the `@warn` rule as a webpack warning. | ### `implementation` @@ -604,6 +605,65 @@ module.exports = { }; ``` +### `warnRuleAsWarning` + +Type: `Boolean` +Default: `false` + +Treats the `@warn` rule as a webpack warning. + +> ℹ️ It will be `true` by default in the next major release. + +**style.scss** + +```scss +$known-prefixes: webkit, moz, ms, o; + +@mixin prefix($property, $value, $prefixes) { + @each $prefix in $prefixes { + @if not index($known-prefixes, $prefix) { + @warn "Unknown prefix #{$prefix}."; + } + + -#{$prefix}-#{$property}: $value; + } + #{$property}: $value; +} + +.tilt { + // Oops, we typo'd "webkit" as "wekbit"! + @include prefix(transform, rotate(15deg), wekbit ms); +} +``` + +The presented code will throw webpack warning instead logging. + +To ignore unnecessary warnings you can use the [ignoreWarnings](https://webpack.js.org/configuration/other-options/#ignorewarnings) option. + +**webpack.config.js** + +```js +module.exports = { + module: { + rules: [ + { + test: /\.s[ac]ss$/i, + use: [ + "style-loader", + "css-loader", + { + loader: "sass-loader", + options: { + warnRuleAsWarning: true, + }, + }, + ], + }, + ], + }, +}; +``` + ## Examples ### Extracts CSS into separate files diff --git a/src/SassError.js b/src/SassError.js index d7418ff9..836ebdc3 100644 --- a/src/SassError.js +++ b/src/SassError.js @@ -3,6 +3,7 @@ class SassError extends Error { super(); this.name = "SassError"; + // TODO remove me in the next major release this.originalSassError = sassError; this.loc = { line: sassError.line, diff --git a/src/SassWarning.js b/src/SassWarning.js new file mode 100644 index 00000000..bd84bf82 --- /dev/null +++ b/src/SassWarning.js @@ -0,0 +1,17 @@ +class SassWarning extends Error { + constructor(warning, options) { + super(warning); + + this.name = "SassWarning"; + this.hideStack = true; + + if (options.span) { + this.loc = { + line: options.span.start.line, + column: options.span.start.column, + }; + } + } +} + +export default SassWarning; diff --git a/src/options.json b/src/options.json index 4fe50039..ee597e35 100644 --- a/src/options.json +++ b/src/options.json @@ -48,6 +48,11 @@ "description": "Enables/Disables default `webpack` importer.", "link": "https://github.com/webpack-contrib/sass-loader#webpackimporter", "type": "boolean" + }, + "warnRuleAsWarning": { + "description": "Treats the '@warn' rule as a webpack warning.", + "link": "https://github.com/webpack-contrib/sass-loader#warnruleaswarning", + "type": "boolean" } }, "additionalProperties": false diff --git a/src/utils.js b/src/utils.js index 3b8de062..8ab27032 100644 --- a/src/utils.js +++ b/src/utils.js @@ -4,6 +4,8 @@ import path from "path"; import { klona } from "klona/full"; import async from "neo-async"; +import SassWarning from "./SassWarning"; + function getDefaultSassImplementation() { let sassImplPkg = "sass"; @@ -229,6 +231,9 @@ async function getSassOptions( } if (!options.logger) { + // TODO set me to `true` by default in the next major release + const needEmitWarning = loaderOptions.warnRuleAsWarning === true; + const logger = loaderContext.getLogger("sass-loader"); const formatSpan = (span) => `${span.url || "-"}:${span.start.line}:${span.start.column}: `; @@ -262,7 +267,13 @@ async function getSassOptions( builtMessage += `\n\n${loggerOptions.stack}`; } - logger.warn(builtMessage); + if (needEmitWarning) { + loaderContext.emitWarning( + new SassWarning(builtMessage, loggerOptions) + ); + } else { + logger.warn(builtMessage); + } }, }; } diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap index efc323db..ccc76270 100644 --- a/test/__snapshots__/validate-options.test.js.snap +++ b/test/__snapshots__/validate-options.test.js.snap @@ -69,49 +69,56 @@ exports[`validate options should throw an error on the "sourceMap" option with " exports[`validate options should throw an error on the "unknown" option with "/test/" value 1`] = ` "Invalid options object. Sass 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 { implementation?, sassOptions?, additionalData?, sourceMap?, webpackImporter? }" + object { implementation?, sassOptions?, additionalData?, sourceMap?, webpackImporter?, warnRuleAsWarning? }" `; exports[`validate options should throw an error on the "unknown" option with "[]" value 1`] = ` "Invalid options object. Sass 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 { implementation?, sassOptions?, additionalData?, sourceMap?, webpackImporter? }" + object { implementation?, sassOptions?, additionalData?, sourceMap?, webpackImporter?, warnRuleAsWarning? }" `; exports[`validate options should throw an error on the "unknown" option with "{"foo":"bar"}" value 1`] = ` "Invalid options object. Sass 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 { implementation?, sassOptions?, additionalData?, sourceMap?, webpackImporter? }" + object { implementation?, sassOptions?, additionalData?, sourceMap?, webpackImporter?, warnRuleAsWarning? }" `; exports[`validate options should throw an error on the "unknown" option with "{}" value 1`] = ` "Invalid options object. Sass 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 { implementation?, sassOptions?, additionalData?, sourceMap?, webpackImporter? }" + object { implementation?, sassOptions?, additionalData?, sourceMap?, webpackImporter?, warnRuleAsWarning? }" `; exports[`validate options should throw an error on the "unknown" option with "1" value 1`] = ` "Invalid options object. Sass 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 { implementation?, sassOptions?, additionalData?, sourceMap?, webpackImporter? }" + object { implementation?, sassOptions?, additionalData?, sourceMap?, webpackImporter?, warnRuleAsWarning? }" `; exports[`validate options should throw an error on the "unknown" option with "false" value 1`] = ` "Invalid options object. Sass 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 { implementation?, sassOptions?, additionalData?, sourceMap?, webpackImporter? }" + object { implementation?, sassOptions?, additionalData?, sourceMap?, webpackImporter?, warnRuleAsWarning? }" `; exports[`validate options should throw an error on the "unknown" option with "test" value 1`] = ` "Invalid options object. Sass 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 { implementation?, sassOptions?, additionalData?, sourceMap?, webpackImporter? }" + object { implementation?, sassOptions?, additionalData?, sourceMap?, webpackImporter?, warnRuleAsWarning? }" `; exports[`validate options should throw an error on the "unknown" option with "true" value 1`] = ` "Invalid options object. Sass 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 { implementation?, sassOptions?, additionalData?, sourceMap?, webpackImporter? }" + object { implementation?, sassOptions?, additionalData?, sourceMap?, webpackImporter?, warnRuleAsWarning? }" +`; + +exports[`validate options should throw an error on the "warnRuleAsWarning" option with "string" value 1`] = ` +"Invalid options object. Sass Loader has been initialized using an options object that does not match the API schema. + - options.warnRuleAsWarning should be a boolean. + -> Treats the '@warn' rule as a webpack warning. + -> Read more at https://github.com/webpack-contrib/sass-loader#warnruleaswarning" `; exports[`validate options should throw an error on the "webpackImporter" option with "string" value 1`] = ` diff --git a/test/__snapshots__/warnRuleAsWarning.test.js.snap b/test/__snapshots__/warnRuleAsWarning.test.js.snap new file mode 100644 index 00000000..cef26b23 --- /dev/null +++ b/test/__snapshots__/warnRuleAsWarning.test.js.snap @@ -0,0 +1,185 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`loader should not emit warning by default (dart-sass) (sass): css 1`] = ` +"a { + color: red; +}" +`; + +exports[`loader should not emit warning by default (dart-sass) (sass): errors 1`] = `Array []`; + +exports[`loader should not emit warning by default (dart-sass) (sass): logs 1`] = ` +Array [ + Array [ + Object { + "args": Array [ + "file:////sass/logging.sass:0:0: My debug message", + ], + "type": "debug", + }, + Object { + "args": Array [ + "My warning message + +test/sass/logging.sass 2:1 root stylesheet +", + ], + "type": "warn", + }, + ], +] +`; + +exports[`loader should not emit warning by default (dart-sass) (sass): warnings 1`] = `Array []`; + +exports[`loader should not emit warning by default (dart-sass) (scss): css 1`] = ` +"a { + color: red; +}" +`; + +exports[`loader should not emit warning by default (dart-sass) (scss): errors 1`] = `Array []`; + +exports[`loader should not emit warning by default (dart-sass) (scss): logs 1`] = ` +Array [ + Array [ + Object { + "args": Array [ + "file:////scss/logging.scss:0:0: My debug message", + ], + "type": "debug", + }, + Object { + "args": Array [ + "My warning message + +test/scss/logging.scss 2:1 root stylesheet +", + ], + "type": "warn", + }, + ], +] +`; + +exports[`loader should not emit warning by default (dart-sass) (scss): warnings 1`] = `Array []`; + +exports[`loader should not emit warning when 'false' used (dart-sass) (sass): css 1`] = ` +"a { + color: red; +}" +`; + +exports[`loader should not emit warning when 'false' used (dart-sass) (sass): errors 1`] = `Array []`; + +exports[`loader should not emit warning when 'false' used (dart-sass) (sass): logs 1`] = ` +Array [ + Array [ + Object { + "args": Array [ + "file:////sass/logging.sass:0:0: My debug message", + ], + "type": "debug", + }, + Object { + "args": Array [ + "My warning message + +test/sass/logging.sass 2:1 root stylesheet +", + ], + "type": "warn", + }, + ], +] +`; + +exports[`loader should not emit warning when 'false' used (dart-sass) (sass): warnings 1`] = `Array []`; + +exports[`loader should not emit warning when 'false' used (dart-sass) (scss): css 1`] = ` +"a { + color: red; +}" +`; + +exports[`loader should not emit warning when 'false' used (dart-sass) (scss): errors 1`] = `Array []`; + +exports[`loader should not emit warning when 'false' used (dart-sass) (scss): logs 1`] = ` +Array [ + Array [ + Object { + "args": Array [ + "file:////scss/logging.scss:0:0: My debug message", + ], + "type": "debug", + }, + Object { + "args": Array [ + "My warning message + +test/scss/logging.scss 2:1 root stylesheet +", + ], + "type": "warn", + }, + ], +] +`; + +exports[`loader should not emit warning when 'false' used (dart-sass) (scss): warnings 1`] = `Array []`; + +exports[`loader should not emit warning when 'true' used (dart-sass) (sass): css 1`] = ` +"a { + color: red; +}" +`; + +exports[`loader should not emit warning when 'true' used (dart-sass) (sass): errors 1`] = `Array []`; + +exports[`loader should not emit warning when 'true' used (dart-sass) (sass): logs 1`] = ` +Array [ + Array [ + Object { + "args": Array [ + "file:////sass/logging.sass:0:0: My debug message", + ], + "type": "debug", + }, + ], +] +`; + +exports[`loader should not emit warning when 'true' used (dart-sass) (sass): warnings 1`] = ` +Array [ + "ModuleWarning: Module Warning (from ../src/cjs.js): +My warning message", +] +`; + +exports[`loader should not emit warning when 'true' used (dart-sass) (scss): css 1`] = ` +"a { + color: red; +}" +`; + +exports[`loader should not emit warning when 'true' used (dart-sass) (scss): errors 1`] = `Array []`; + +exports[`loader should not emit warning when 'true' used (dart-sass) (scss): logs 1`] = ` +Array [ + Array [ + Object { + "args": Array [ + "file:////scss/logging.scss:0:0: My debug message", + ], + "type": "debug", + }, + ], +] +`; + +exports[`loader should not emit warning when 'true' used (dart-sass) (scss): warnings 1`] = ` +Array [ + "ModuleWarning: Module Warning (from ../src/cjs.js): +My warning message", +] +`; diff --git a/test/validate-options.test.js b/test/validate-options.test.js index 7f43e0eb..3fd7e02a 100644 --- a/test/validate-options.test.js +++ b/test/validate-options.test.js @@ -52,6 +52,10 @@ describe("validate options", () => { success: [true, false], failure: ["string"], }, + warnRuleAsWarning: { + success: [true, false], + failure: ["string"], + }, unknown: { success: [], failure: [1, true, false, "test", /test/, [], {}, { foo: "bar" }], diff --git a/test/warnRuleAsWarning.test.js b/test/warnRuleAsWarning.test.js new file mode 100644 index 00000000..2fff8a81 --- /dev/null +++ b/test/warnRuleAsWarning.test.js @@ -0,0 +1,151 @@ +import url from "url"; + +import dartSass from "sass"; + +import { isSupportedFibers } from "../src/utils"; + +import { + compile, + getCodeFromBundle, + getCodeFromSass, + getCompiler, + getErrors, + getImplementationByName, + getTestId, + getWarnings, +} from "./helpers"; + +jest.setTimeout(60000); + +let Fiber; +const implementations = [dartSass]; +const syntaxStyles = ["scss", "sass"]; + +describe("loader", () => { + beforeAll(async () => { + if (isSupportedFibers()) { + const { default: fibers } = await import("fibers"); + Fiber = fibers; + } + }); + + beforeEach(() => { + if (isSupportedFibers()) { + // The `sass` (`Dart Sass`) package modify the `Function` prototype, but the `jest` lose a prototype + Object.setPrototypeOf(Fiber, Function.prototype); + } + }); + + implementations.forEach((implementation) => { + const [implementationName] = implementation.info.split("\t"); + + syntaxStyles.forEach((syntax) => { + it(`should not emit warning by default (${implementationName}) (${syntax})`, async () => { + const testId = getTestId("logging", syntax); + const options = { + implementation: getImplementationByName(implementationName), + }; + const compiler = getCompiler(testId, { loader: { options } }); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + const codeFromSass = getCodeFromSass(testId, options); + const logs = []; + + for (const [name, value] of stats.compilation.logging) { + if (/sass-loader/.test(name)) { + logs.push( + value.map((item) => { + return { + type: item.type, + args: item.args.map((arg) => + arg + .replace(url.pathToFileURL(__dirname), "file:///") + .replace(/\\/g, "/") + ), + }; + }) + ); + } + } + + expect(codeFromBundle.css).toBe(codeFromSass.css); + expect(codeFromBundle.css).toMatchSnapshot("css"); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + expect(logs).toMatchSnapshot("logs"); + }); + + it(`should not emit warning when 'false' used (${implementationName}) (${syntax})`, async () => { + const testId = getTestId("logging", syntax); + const options = { + implementation: getImplementationByName(implementationName), + warnRuleAsWarning: false, + }; + const compiler = getCompiler(testId, { loader: { options } }); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + const codeFromSass = getCodeFromSass(testId, options); + const logs = []; + + for (const [name, value] of stats.compilation.logging) { + if (/sass-loader/.test(name)) { + logs.push( + value.map((item) => { + return { + type: item.type, + args: item.args.map((arg) => + arg + .replace(url.pathToFileURL(__dirname), "file:///") + .replace(/\\/g, "/") + ), + }; + }) + ); + } + } + + expect(codeFromBundle.css).toBe(codeFromSass.css); + expect(codeFromBundle.css).toMatchSnapshot("css"); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + expect(logs).toMatchSnapshot("logs"); + }); + + it(`should not emit warning when 'true' used (${implementationName}) (${syntax})`, async () => { + const testId = getTestId("logging", syntax); + const options = { + implementation: getImplementationByName(implementationName), + warnRuleAsWarning: true, + }; + const compiler = getCompiler(testId, { loader: { options } }); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + const codeFromSass = getCodeFromSass(testId, options); + const logs = []; + + for (const [name, value] of stats.compilation.logging) { + if (/sass-loader/.test(name)) { + logs.push( + value.map((item) => { + return { + type: item.type, + args: item.args.map((arg) => + arg + .replace(url.pathToFileURL(__dirname), "file:///") + .replace(/\\/g, "/") + ), + }; + }) + ); + } + } + + expect(codeFromBundle.css).toBe(codeFromSass.css); + expect(codeFromBundle.css).toMatchSnapshot("css"); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + expect(logs).toMatchSnapshot("logs"); + }); + }); + }); +});