diff --git a/README.md b/README.md index d4cf84f5..7c75f053 100644 --- a/README.md +++ b/README.md @@ -172,7 +172,50 @@ module.exports = { Note that when using `sass` (`Dart Sass`), **synchronous compilation is twice as fast as asynchronous compilation** by default, due to the overhead of asynchronous callbacks. To avoid this overhead, you can use the [fibers](https://www.npmjs.com/package/fibers) package to call asynchronous importers from the synchronous code path. -To enable this, pass the `Fiber` class to the `sassOptions.fiber` option: +We automatically inject the [`fibers`](https://github.com/laverdet/node-fibers) package (setup `sassOptions.fiber`) if is possible (i.e. you need install the [`fibers`](https://github.com/laverdet/node-fibers) package). + +**package.json** + +```json +{ + "devDependencies": { + "sass-loader": "^7.2.0", + "sass": "^1.22.10", + "fibers": "^4.0.1" + } +} +``` + +You can disable automatically inject the [`fibers`](https://github.com/laverdet/node-fibers) package pass the `false` value for the `sassOptions.fiber` option. + +**webpack.config.js** + +```js +module.exports = { + module: { + rules: [ + { + test: /\.s[ac]ss$/i, + use: [ + 'style-loader', + 'css-loader', + { + loader: 'sass-loader', + options: { + implementation: require('sass'), + sassOptions: { + fiber: false, + }, + }, + }, + ], + }, + ], + }, +}; +``` + +Also you can pass own the `fiber` value using this code: **webpack.config.js** diff --git a/src/getSassOptions.js b/src/getSassOptions.js index 631b7b07..9d6d388b 100644 --- a/src/getSassOptions.js +++ b/src/getSassOptions.js @@ -16,12 +16,13 @@ function isProductionLikeMode(loaderContext) { /** * Derives the sass options from the loader context and normalizes its values with sane defaults. * - * @param {LoaderContext} loaderContext + * @param {object} loaderContext * @param {object} loaderOptions * @param {string} content + * @param {object} implementation * @returns {Object} */ -function getSassOptions(loaderContext, loaderOptions, content) { +function getSassOptions(loaderContext, loaderOptions, content, implementation) { const options = cloneDeep( loaderOptions.sassOptions ? typeof loaderOptions.sassOptions === 'function' @@ -30,6 +31,33 @@ function getSassOptions(loaderContext, loaderOptions, content) { : {} ); + const isDartSass = implementation.info.includes('dart-sass'); + + if (isDartSass) { + const shouldTryToResolveFibers = !options.fiber && options.fiber !== false; + + if (shouldTryToResolveFibers) { + let fibers; + + try { + fibers = require.resolve('fibers'); + } catch (_error) { + // Nothing + } + + if (fibers) { + // eslint-disable-next-line global-require, import/no-dynamic-require + options.fiber = require(fibers); + } + } else if (options.fiber === false) { + // Don't pass the `fiber` option for `sass` (`Dart Sass`) + delete options.fiber; + } + } else { + // Don't pass the `fiber` option for `node-sass` + delete options.fiber; + } + options.data = loaderOptions.prependData ? typeof loaderOptions.prependData === 'function' ? loaderOptions.prependData(loaderContext) + os.EOL + content diff --git a/src/index.js b/src/index.js index 1498eeb5..bf0d69d1 100644 --- a/src/index.js +++ b/src/index.js @@ -13,7 +13,7 @@ import SassError from './SassError'; /** * The sass-loader makes node-sass and dart-sass available to webpack modules. * - * @this {LoaderContext} + * @this {object} * @param {string} content */ function loader(content) { @@ -32,7 +32,7 @@ function loader(content) { this.addDependency(path.normalize(file)); }; - const sassOptions = getSassOptions(this, options, content); + const sassOptions = getSassOptions(this, options, content, implementation); const shouldUseWebpackImporter = typeof options.webpackImporter === 'boolean' diff --git a/test/__snapshots__/sassOptions-option.test.js.snap b/test/__snapshots__/sassOptions-option.test.js.snap index 384f5d6c..50c9b980 100644 --- a/test/__snapshots__/sassOptions-option.test.js.snap +++ b/test/__snapshots__/sassOptions-option.test.js.snap @@ -1,5 +1,381 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`sassOptions option should don't use the "fibers" package when the "fiber" option is "false" (dart-sass) (sass): css 1`] = ` +"@charset \\"UTF-8\\"; +body { + font: 100% Helvetica, sans-serif; + color: #333; +} + +nav ul { + margin: 0; + padding: 0; + list-style: none; +} +nav li { + display: inline-block; +} +nav a { + display: block; + padding: 6px 12px; + text-decoration: none; +} + +.box { + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + -ms-border-radius: 10px; + border-radius: 10px; +} + +.message, .warning, .error, .success { + border: 1px solid #ccc; + padding: 10px; + color: #333; +} + +.success { + border-color: green; +} + +.error { + border-color: red; +} + +.warning { + border-color: yellow; +} + +.foo:before { + content: \\"\\"; +} + +.bar:before { + content: \\"∑\\"; +}" +`; + +exports[`sassOptions option should don't use the "fibers" package when the "fiber" option is "false" (dart-sass) (sass): errors 1`] = `Array []`; + +exports[`sassOptions option should don't use the "fibers" package when the "fiber" option is "false" (dart-sass) (sass): warnings 1`] = `Array []`; + +exports[`sassOptions option should don't use the "fibers" package when the "fiber" option is "false" (dart-sass) (scss): css 1`] = ` +"@charset \\"UTF-8\\"; +body { + font: 100% Helvetica, sans-serif; + color: #333; +} + +nav ul { + margin: 0; + padding: 0; + list-style: none; +} +nav li { + display: inline-block; +} +nav a { + display: block; + padding: 6px 12px; + text-decoration: none; +} + +.box { + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + -ms-border-radius: 10px; + border-radius: 10px; +} + +.foo:before { + content: \\"\\"; +} + +.bar:before { + content: \\"∑\\"; +}" +`; + +exports[`sassOptions option should don't use the "fibers" package when the "fiber" option is "false" (dart-sass) (scss): errors 1`] = `Array []`; + +exports[`sassOptions option should don't use the "fibers" package when the "fiber" option is "false" (dart-sass) (scss): warnings 1`] = `Array []`; + +exports[`sassOptions option should don't use the "fibers" package when the "fiber" option is "false" (node-sass) (sass): css 1`] = ` +"@charset \\"UTF-8\\"; +body { + font: 100% Helvetica, sans-serif; + color: #333; } + +nav ul { + margin: 0; + padding: 0; + list-style: none; } + +nav li { + display: inline-block; } + +nav a { + display: block; + padding: 6px 12px; + text-decoration: none; } + +.box { + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + -ms-border-radius: 10px; + border-radius: 10px; } + +.message, .success, .error, .warning { + border: 1px solid #ccc; + padding: 10px; + color: #333; } + +.success { + border-color: green; } + +.error { + border-color: red; } + +.warning { + border-color: yellow; } + +.foo:before { + content: \\"\\"; } + +.bar:before { + content: \\"∑\\"; } +" +`; + +exports[`sassOptions option should don't use the "fibers" package when the "fiber" option is "false" (node-sass) (sass): errors 1`] = `Array []`; + +exports[`sassOptions option should don't use the "fibers" package when the "fiber" option is "false" (node-sass) (sass): warnings 1`] = `Array []`; + +exports[`sassOptions option should don't use the "fibers" package when the "fiber" option is "false" (node-sass) (scss): css 1`] = ` +"@charset \\"UTF-8\\"; +body { + font: 100% Helvetica, sans-serif; + color: #333; } + +nav ul { + margin: 0; + padding: 0; + list-style: none; } + +nav li { + display: inline-block; } + +nav a { + display: block; + padding: 6px 12px; + text-decoration: none; } + +.box { + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + -ms-border-radius: 10px; + border-radius: 10px; } + +.foo:before { + content: \\"\\"; } + +.bar:before { + content: \\"∑\\"; } +" +`; + +exports[`sassOptions option should don't use the "fibers" package when the "fiber" option is "false" (node-sass) (scss): errors 1`] = `Array []`; + +exports[`sassOptions option should don't use the "fibers" package when the "fiber" option is "false" (node-sass) (scss): warnings 1`] = `Array []`; + +exports[`sassOptions option should use the "fibers" package if it is possible (dart-sass) (sass): css 1`] = ` +"@charset \\"UTF-8\\"; +body { + font: 100% Helvetica, sans-serif; + color: #333; +} + +nav ul { + margin: 0; + padding: 0; + list-style: none; +} +nav li { + display: inline-block; +} +nav a { + display: block; + padding: 6px 12px; + text-decoration: none; +} + +.box { + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + -ms-border-radius: 10px; + border-radius: 10px; +} + +.message, .warning, .error, .success { + border: 1px solid #ccc; + padding: 10px; + color: #333; +} + +.success { + border-color: green; +} + +.error { + border-color: red; +} + +.warning { + border-color: yellow; +} + +.foo:before { + content: \\"\\"; +} + +.bar:before { + content: \\"∑\\"; +}" +`; + +exports[`sassOptions option should use the "fibers" package if it is possible (dart-sass) (sass): errors 1`] = `Array []`; + +exports[`sassOptions option should use the "fibers" package if it is possible (dart-sass) (sass): warnings 1`] = `Array []`; + +exports[`sassOptions option should use the "fibers" package if it is possible (dart-sass) (scss): css 1`] = ` +"@charset \\"UTF-8\\"; +body { + font: 100% Helvetica, sans-serif; + color: #333; +} + +nav ul { + margin: 0; + padding: 0; + list-style: none; +} +nav li { + display: inline-block; +} +nav a { + display: block; + padding: 6px 12px; + text-decoration: none; +} + +.box { + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + -ms-border-radius: 10px; + border-radius: 10px; +} + +.foo:before { + content: \\"\\"; +} + +.bar:before { + content: \\"∑\\"; +}" +`; + +exports[`sassOptions option should use the "fibers" package if it is possible (dart-sass) (scss): errors 1`] = `Array []`; + +exports[`sassOptions option should use the "fibers" package if it is possible (dart-sass) (scss): warnings 1`] = `Array []`; + +exports[`sassOptions option should use the "fibers" package if it is possible (node-sass) (sass): css 1`] = ` +"@charset \\"UTF-8\\"; +body { + font: 100% Helvetica, sans-serif; + color: #333; } + +nav ul { + margin: 0; + padding: 0; + list-style: none; } + +nav li { + display: inline-block; } + +nav a { + display: block; + padding: 6px 12px; + text-decoration: none; } + +.box { + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + -ms-border-radius: 10px; + border-radius: 10px; } + +.message, .success, .error, .warning { + border: 1px solid #ccc; + padding: 10px; + color: #333; } + +.success { + border-color: green; } + +.error { + border-color: red; } + +.warning { + border-color: yellow; } + +.foo:before { + content: \\"\\"; } + +.bar:before { + content: \\"∑\\"; } +" +`; + +exports[`sassOptions option should use the "fibers" package if it is possible (node-sass) (sass): errors 1`] = `Array []`; + +exports[`sassOptions option should use the "fibers" package if it is possible (node-sass) (sass): warnings 1`] = `Array []`; + +exports[`sassOptions option should use the "fibers" package if it is possible (node-sass) (scss): css 1`] = ` +"@charset \\"UTF-8\\"; +body { + font: 100% Helvetica, sans-serif; + color: #333; } + +nav ul { + margin: 0; + padding: 0; + list-style: none; } + +nav li { + display: inline-block; } + +nav a { + display: block; + padding: 6px 12px; + text-decoration: none; } + +.box { + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + -ms-border-radius: 10px; + border-radius: 10px; } + +.foo:before { + content: \\"\\"; } + +.bar:before { + content: \\"∑\\"; } +" +`; + +exports[`sassOptions option should use the "fibers" package if it is possible (node-sass) (scss): errors 1`] = `Array []`; + +exports[`sassOptions option should use the "fibers" package if it is possible (node-sass) (scss): warnings 1`] = `Array []`; + exports[`sassOptions option should work when the option is empty "Object" (dart-sass) (sass): css 1`] = ` "@charset \\"UTF-8\\"; body { diff --git a/test/implementation-option.test.js b/test/implementation-option.test.js index 40142b0b..29d4efd2 100644 --- a/test/implementation-option.test.js +++ b/test/implementation-option.test.js @@ -1,5 +1,6 @@ import nodeSass from 'node-sass'; import dartSass from 'sass'; +import Fiber from 'fibers'; import { compile, @@ -13,6 +14,8 @@ const implementations = [nodeSass, dartSass]; describe('implementation option', () => { beforeEach(() => { + // The `sass` (`Dart Sass`) package modify the `Function` prototype, but the `jest` lose a prototype + Object.setPrototypeOf(Fiber, Function.prototype); jest.clearAllMocks(); }); diff --git a/test/loader.test.js b/test/loader.test.js index c32756d0..97cb650b 100644 --- a/test/loader.test.js +++ b/test/loader.test.js @@ -2,6 +2,7 @@ import path from 'path'; import nodeSass from 'node-sass'; import dartSass from 'sass'; +import Fiber from 'fibers'; import { compile, @@ -16,6 +17,11 @@ const implementations = [nodeSass, dartSass]; const syntaxStyles = ['scss', 'sass']; describe('loader', () => { + beforeEach(() => { + // 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'); diff --git a/test/prependData-option.test.js b/test/prependData-option.test.js index d52ac044..14f490c4 100644 --- a/test/prependData-option.test.js +++ b/test/prependData-option.test.js @@ -1,5 +1,6 @@ import nodeSass from 'node-sass'; import dartSass from 'sass'; +import Fiber from 'fibers'; import { compile, @@ -13,6 +14,11 @@ const implementations = [nodeSass, dartSass]; const syntaxStyles = ['scss', 'sass']; describe('prependData option', () => { + beforeEach(() => { + // 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'); diff --git a/test/sassOptions-option.test.js b/test/sassOptions-option.test.js index 874e5a58..2e461714 100644 --- a/test/sassOptions-option.test.js +++ b/test/sassOptions-option.test.js @@ -161,13 +161,17 @@ describe('sassOptions option', () => { }); it(`should work with the "fiber" option (${implementationName}) (${syntax})`, async () => { + const dartSassSpy = jest.spyOn(dartSass, 'render'); const testId = getTestId('language', syntax); const options = { implementation: getImplementationByName(implementationName), sassOptions: {}, }; - if (semver.satisfies(process.version, '>= 10')) { + if ( + implementationName === 'dart-sass' && + semver.satisfies(process.version, '>= 10') + ) { // eslint-disable-next-line global-require options.sassOptions.fiber = Fiber; } @@ -176,10 +180,73 @@ describe('sassOptions option', () => { const codeFromBundle = getCodeFromBundle(stats); const codeFromSass = getCodeFromSass(testId, options); + if ( + implementationName === 'dart-sass' && + semver.satisfies(process.version, '>= 10') + ) { + expect(dartSassSpy.mock.calls[0][0]).toHaveProperty('fiber'); + } + + expect(codeFromBundle.css).toBe(codeFromSass.css); + expect(codeFromBundle.css).toMatchSnapshot('css'); + expect(stats.compilation.warnings).toMatchSnapshot('warnings'); + expect(stats.compilation.errors).toMatchSnapshot('errors'); + + dartSassSpy.mockRestore(); + }); + + it(`should use the "fibers" package if it is possible (${implementationName}) (${syntax})`, async () => { + const dartSassSpy = jest.spyOn(dartSass, 'render'); + const testId = getTestId('language', syntax); + const options = { + implementation: getImplementationByName(implementationName), + sassOptions: {}, + }; + + const stats = await compile(testId, { loader: { options } }); + const codeFromBundle = getCodeFromBundle(stats); + const codeFromSass = getCodeFromSass(testId, options); + + if ( + implementationName === 'dart-sass' && + semver.satisfies(process.version, '>= 10') + ) { + expect(dartSassSpy.mock.calls[0][0]).toHaveProperty('fiber'); + } + + expect(codeFromBundle.css).toBe(codeFromSass.css); + expect(codeFromBundle.css).toMatchSnapshot('css'); + expect(stats.compilation.warnings).toMatchSnapshot('warnings'); + expect(stats.compilation.errors).toMatchSnapshot('errors'); + + dartSassSpy.mockRestore(); + }); + + it(`should don't use the "fibers" package when the "fiber" option is "false" (${implementationName}) (${syntax})`, async () => { + const dartSassSpy = jest.spyOn(dartSass, 'render'); + const testId = getTestId('language', syntax); + const options = { + implementation: getImplementationByName(implementationName), + sassOptions: { fiber: false }, + }; + + const stats = await compile(testId, { loader: { options } }); + const codeFromBundle = getCodeFromBundle(stats); + const codeFromSass = getCodeFromSass(testId, options); + + if ( + implementationName === 'dart-sass' && + semver.satisfies(process.version, '>= 10') + ) { + expect(dartSassSpy.mock.calls[0][0]).not.toHaveProperty('fiber'); + } + expect(codeFromBundle.css).toBe(codeFromSass.css); expect(codeFromBundle.css).toMatchSnapshot('css'); expect(stats.compilation.warnings).toMatchSnapshot('warnings'); expect(stats.compilation.errors).toMatchSnapshot('errors'); + + dartSassSpy.mockRestore(); }); }); }); diff --git a/test/sourceMap-options.test.js b/test/sourceMap-options.test.js index 63093db6..9a9e6efe 100644 --- a/test/sourceMap-options.test.js +++ b/test/sourceMap-options.test.js @@ -3,6 +3,7 @@ import path from 'path'; import nodeSass from 'node-sass'; import dartSass from 'sass'; +import Fiber from 'fibers'; import { compile, @@ -15,6 +16,11 @@ const implementations = [nodeSass, dartSass]; const syntaxStyles = ['scss', 'sass']; describe('sourceMap option', () => { + beforeEach(() => { + // The `sass` (`Dart Sass`) package modify the `Function` prototype, but the `jest` lose a prototype + Object.setPrototypeOf(Fiber, Function.prototype); + }); + implementations.forEach((implementation) => { syntaxStyles.forEach((syntax) => { const [implementationName] = implementation.info.split('\t'); diff --git a/test/validate-options.test.js b/test/validate-options.test.js index bf11c32d..9d7b73eb 100644 --- a/test/validate-options.test.js +++ b/test/validate-options.test.js @@ -1,5 +1,12 @@ +import Fiber from 'fibers'; + import loader from '../src/cjs'; +beforeEach(() => { + // The `sass` (`Dart Sass`) package modify the `Function` prototype, but the `jest` lose a prototype + Object.setPrototypeOf(Fiber, Function.prototype); +}); + it('validate options', () => { const validate = (options) => loader.call( diff --git a/test/webpackImporter-options.test.js b/test/webpackImporter-options.test.js index db2e9ee9..3d6043c0 100644 --- a/test/webpackImporter-options.test.js +++ b/test/webpackImporter-options.test.js @@ -1,5 +1,6 @@ import nodeSass from 'node-sass'; import dartSass from 'sass'; +import Fiber from 'fibers'; import { compile, @@ -13,6 +14,11 @@ const implementations = [nodeSass, dartSass]; const syntaxStyles = ['scss', 'sass']; describe('webpackImporter option', () => { + beforeEach(() => { + // The `sass` (`Dart Sass`) package modify the `Function` prototype, but the `jest` lose a prototype + Object.setPrototypeOf(Fiber, Function.prototype); + }); + implementations.forEach((implementation) => { syntaxStyles.forEach((syntax) => { const [implementationName] = implementation.info.split('\t');