From ff90dd62cdfc51b4ca6cd755a65d3e2adc51657a Mon Sep 17 00:00:00 2001 From: Evilebot Tnawi Date: Fri, 14 Dec 2018 15:34:27 +0300 Subject: [PATCH] feat: support auto resolving `dart-sass` Now you don't need setup `implementation: require('sass')`, just add `sass` to your `package.json` and install dependencies and `sass-loader` automatically load `sass`. Beware situation when `node-sass` and `sass` was installed, by default `sass-loader` loads `node-sass`, to avoid this situation use `implementation` option. --- README.md | 34 ++++++++++ lib/loader.js | 22 ++++++- test/index.test.js | 147 +++++++++++++++++++++++++++++------------- test/scss/simple.scss | 7 ++ 4 files changed, 162 insertions(+), 48 deletions(-) create mode 100644 test/scss/simple.scss diff --git a/README.md b/README.md index 94a69d65..56e76651 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,40 @@ module.exports = { See [the Node Sass documentation](https://github.com/sass/node-sass/blob/master/README.md#options) for all available Sass options. +By default the loader resolve the implementation based on your dependencies. +Just add required implementation to `package.json` +(`node-sass` or `sass` package) and install dependencies. + +Example where the `sass-loader` loader uses the `sass` (`dart-sass`) implementation: + +**package.json** + +```json +{ + "devDependencies": { + "sass-loader": "*", + "sass": "*" + } +} +``` + +Example where the `sass-loader` loader uses the `node-sass` implementation: + +**package.json** + +```json +{ + "devDependencies": { + "sass-loader": "*", + "node-sass": "*" + } +} +``` + +Beware the situation +when `node-sass` and `sass` was installed, by default the `sass-loader` +prefers `node-sass`, to avoid this situation use the `implementation` option. + The special `implementation` option determines which implementation of Sass to use. It takes either a [Node Sass][] or a [Dart Sass][] module. For example, to use Dart Sass, you'd pass: diff --git a/lib/loader.js b/lib/loader.js index d548f555..dea681b1 100644 --- a/lib/loader.js +++ b/lib/loader.js @@ -13,7 +13,7 @@ const normalizeOptions = require('./normalizeOptions'); let nodeSassJobQueue = null; /** - * The sass-loader makes node-sass available to webpack modules. + * The sass-loader makes node-sass and dart-sass available to webpack modules. * * @this {LoaderContext} * @param {string} content @@ -58,7 +58,7 @@ function sassLoader(content) { const render = getRenderFuncFromSassImpl( // eslint-disable-next-line import/no-extraneous-dependencies, global-require - options.implementation || require('node-sass') + options.implementation || getDefaultSassImpl() ); render(options, (err, result) => { @@ -157,4 +157,22 @@ function getRenderFuncFromSassImpl(module) { throw new Error(`Unknown Sass implementation "${implementation}".`); } +function getDefaultSassImpl() { + let sassImplPkg = 'node-sass'; + + try { + require.resolve('node-sass'); + } catch (error) { + try { + require.resolve('sass'); + sassImplPkg = 'sass'; + } catch (ignoreError) { + sassImplPkg = 'node-sass'; + } + } + + // eslint-disable-next-line import/no-dynamic-require, global-require + return require(sassImplPkg); +} + module.exports = sassLoader; diff --git a/test/index.test.js b/test/index.test.js index 262984aa..cfc7da0d 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -50,6 +50,52 @@ Object.defineProperty(loaderContextMock, 'options', { implementations.forEach((implementation) => { const [implementationName] = implementation.info.split('\t'); + function readCss(ext, id) { + return fs + .readFileSync( + path.join(__dirname, ext, 'spec', implementationName, `${id}.css`), + 'utf8' + ) + .replace(CR, ''); + } + + function runWebpack(baseConfig, loaderOptions, done) { + const webpackConfig = merge( + { + mode: 'development', + output: { + path: path.join(__dirname, 'output'), + filename: 'bundle.js', + libraryTarget: 'commonjs2', + }, + module: { + rules: [ + { + test: /\.s[ac]ss$/, + use: [ + { loader: 'raw-loader' }, + { + loader: pathToSassLoader, + options: merge({ implementation }, loaderOptions), + }, + ], + }, + ], + }, + }, + baseConfig + ); + + webpack(webpackConfig, (webpackErr, stats) => { + const err = + webpackErr || + (stats.hasErrors() && stats.compilation.errors[0]) || + (stats.hasWarnings() && stats.compilation.warnings[0]); + + done(err || null); + }); + } + describe(implementationName, () => { syntaxStyles.forEach((ext) => { function execTest(testId, loaderOptions, webpackOptions) { @@ -405,7 +451,7 @@ implementations.forEach((implementation) => { } ); }); - it('should not swallow errors when trying to load node-sass', (done) => { + it('should not swallow errors when trying to load sass implementation', (done) => { mockRequire.reRequire(pathToSassLoader); // eslint-disable-next-line global-require const module = require('module'); @@ -414,7 +460,7 @@ implementations.forEach((implementation) => { // eslint-disable-next-line no-underscore-dangle module._resolveFilename = function _resolveFilename(filename) { - if (!filename.match(/node-sass/)) { + if (!filename.match(/^(node-sass|sass)$/)) { // eslint-disable-next-line prefer-rest-params return originalResolve.apply(this, arguments); } @@ -520,54 +566,63 @@ implementations.forEach((implementation) => { } ); }); - }); - }); - function readCss(ext, id) { - return fs - .readFileSync( - path.join(__dirname, ext, 'spec', implementationName, `${id}.css`), - 'utf8' - ) - .replace(CR, ''); - } - - function runWebpack(baseConfig, loaderOptions, done) { - const webpackConfig = merge( - { - mode: 'development', - output: { - path: path.join(__dirname, 'output'), - filename: 'bundle.js', - libraryTarget: 'commonjs2', - }, - module: { - rules: [ - { - test: /\.s[ac]ss$/, - use: [ - { loader: 'raw-loader' }, - { - loader: pathToSassLoader, - options: merge({ implementation }, loaderOptions), - }, - ], - }, - ], - }, - }, - baseConfig - ); + const [implName] = implementation.info.trim().split(/\s/); + + it(`should load ${implName}`, (done) => { + mockRequire.reRequire(pathToSassLoader); + // eslint-disable-next-line global-require + const module = require('module'); + // eslint-disable-next-line no-underscore-dangle + const originalResolve = module._resolveFilename; + + // eslint-disable-next-line no-underscore-dangle + module._resolveFilename = function _resolveFilename(filename) { + if (implName === 'node-sass' && filename.match(/^sass$/)) { + const err = new Error('Some error'); - webpack(webpackConfig, (webpackErr, stats) => { - const err = - webpackErr || - (stats.hasErrors() && stats.compilation.errors[0]) || - (stats.hasWarnings() && stats.compilation.warnings[0]); + err.code = 'MODULE_NOT_FOUND'; - done(err || null); + throw err; + } + + if (implName === 'dart-sass' && filename.match(/^node-sass$/)) { + const err = new Error('Some error'); + + err.code = 'MODULE_NOT_FOUND'; + + throw err; + } + + // eslint-disable-next-line prefer-rest-params + return originalResolve.apply(this, arguments); + }; + + const pathToFile = path.resolve(__dirname, './scss/simple.scss'); + + runWebpack( + { + entry: pathToFile, + }, + { implementation: null }, + (err) => { + // eslint-disable-next-line no-underscore-dangle + module._resolveFilename = originalResolve; + + if (implName === 'node-sass') { + mockRequire.reRequire('node-sass'); + } + + if (implName === 'dart-sass') { + mockRequire.reRequire('sass'); + } + + done(err); + } + ); + }); }); - } + }); }); }); diff --git a/test/scss/simple.scss b/test/scss/simple.scss new file mode 100644 index 00000000..b8bf7120 --- /dev/null +++ b/test/scss/simple.scss @@ -0,0 +1,7 @@ +$font-stack: Helvetica, sans-serif; +$primary-color: #333; + +body { + font: 100% $font-stack; + color: $primary-color; +}