Skip to content

Commit ff90dd6

Browse files
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.
1 parent f524223 commit ff90dd6

File tree

4 files changed

+162
-48
lines changed

4 files changed

+162
-48
lines changed

README.md

+34
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,40 @@ module.exports = {
8787

8888
See [the Node Sass documentation](https://github.com/sass/node-sass/blob/master/README.md#options) for all available Sass options.
8989

90+
By default the loader resolve the implementation based on your dependencies.
91+
Just add required implementation to `package.json`
92+
(`node-sass` or `sass` package) and install dependencies.
93+
94+
Example where the `sass-loader` loader uses the `sass` (`dart-sass`) implementation:
95+
96+
**package.json**
97+
98+
```json
99+
{
100+
"devDependencies": {
101+
"sass-loader": "*",
102+
"sass": "*"
103+
}
104+
}
105+
```
106+
107+
Example where the `sass-loader` loader uses the `node-sass` implementation:
108+
109+
**package.json**
110+
111+
```json
112+
{
113+
"devDependencies": {
114+
"sass-loader": "*",
115+
"node-sass": "*"
116+
}
117+
}
118+
```
119+
120+
Beware the situation
121+
when `node-sass` and `sass` was installed, by default the `sass-loader`
122+
prefers `node-sass`, to avoid this situation use the `implementation` option.
123+
90124
The special `implementation` option determines which implementation of Sass to
91125
use. It takes either a [Node Sass][] or a [Dart Sass][] module. For example, to
92126
use Dart Sass, you'd pass:

lib/loader.js

+20-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const normalizeOptions = require('./normalizeOptions');
1313
let nodeSassJobQueue = null;
1414

1515
/**
16-
* The sass-loader makes node-sass available to webpack modules.
16+
* The sass-loader makes node-sass and dart-sass available to webpack modules.
1717
*
1818
* @this {LoaderContext}
1919
* @param {string} content
@@ -58,7 +58,7 @@ function sassLoader(content) {
5858

5959
const render = getRenderFuncFromSassImpl(
6060
// eslint-disable-next-line import/no-extraneous-dependencies, global-require
61-
options.implementation || require('node-sass')
61+
options.implementation || getDefaultSassImpl()
6262
);
6363

6464
render(options, (err, result) => {
@@ -157,4 +157,22 @@ function getRenderFuncFromSassImpl(module) {
157157
throw new Error(`Unknown Sass implementation "${implementation}".`);
158158
}
159159

160+
function getDefaultSassImpl() {
161+
let sassImplPkg = 'node-sass';
162+
163+
try {
164+
require.resolve('node-sass');
165+
} catch (error) {
166+
try {
167+
require.resolve('sass');
168+
sassImplPkg = 'sass';
169+
} catch (ignoreError) {
170+
sassImplPkg = 'node-sass';
171+
}
172+
}
173+
174+
// eslint-disable-next-line import/no-dynamic-require, global-require
175+
return require(sassImplPkg);
176+
}
177+
160178
module.exports = sassLoader;

test/index.test.js

+101-46
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,52 @@ Object.defineProperty(loaderContextMock, 'options', {
5050
implementations.forEach((implementation) => {
5151
const [implementationName] = implementation.info.split('\t');
5252

53+
function readCss(ext, id) {
54+
return fs
55+
.readFileSync(
56+
path.join(__dirname, ext, 'spec', implementationName, `${id}.css`),
57+
'utf8'
58+
)
59+
.replace(CR, '');
60+
}
61+
62+
function runWebpack(baseConfig, loaderOptions, done) {
63+
const webpackConfig = merge(
64+
{
65+
mode: 'development',
66+
output: {
67+
path: path.join(__dirname, 'output'),
68+
filename: 'bundle.js',
69+
libraryTarget: 'commonjs2',
70+
},
71+
module: {
72+
rules: [
73+
{
74+
test: /\.s[ac]ss$/,
75+
use: [
76+
{ loader: 'raw-loader' },
77+
{
78+
loader: pathToSassLoader,
79+
options: merge({ implementation }, loaderOptions),
80+
},
81+
],
82+
},
83+
],
84+
},
85+
},
86+
baseConfig
87+
);
88+
89+
webpack(webpackConfig, (webpackErr, stats) => {
90+
const err =
91+
webpackErr ||
92+
(stats.hasErrors() && stats.compilation.errors[0]) ||
93+
(stats.hasWarnings() && stats.compilation.warnings[0]);
94+
95+
done(err || null);
96+
});
97+
}
98+
5399
describe(implementationName, () => {
54100
syntaxStyles.forEach((ext) => {
55101
function execTest(testId, loaderOptions, webpackOptions) {
@@ -405,7 +451,7 @@ implementations.forEach((implementation) => {
405451
}
406452
);
407453
});
408-
it('should not swallow errors when trying to load node-sass', (done) => {
454+
it('should not swallow errors when trying to load sass implementation', (done) => {
409455
mockRequire.reRequire(pathToSassLoader);
410456
// eslint-disable-next-line global-require
411457
const module = require('module');
@@ -414,7 +460,7 @@ implementations.forEach((implementation) => {
414460

415461
// eslint-disable-next-line no-underscore-dangle
416462
module._resolveFilename = function _resolveFilename(filename) {
417-
if (!filename.match(/node-sass/)) {
463+
if (!filename.match(/^(node-sass|sass)$/)) {
418464
// eslint-disable-next-line prefer-rest-params
419465
return originalResolve.apply(this, arguments);
420466
}
@@ -520,54 +566,63 @@ implementations.forEach((implementation) => {
520566
}
521567
);
522568
});
523-
});
524-
});
525569

526-
function readCss(ext, id) {
527-
return fs
528-
.readFileSync(
529-
path.join(__dirname, ext, 'spec', implementationName, `${id}.css`),
530-
'utf8'
531-
)
532-
.replace(CR, '');
533-
}
534-
535-
function runWebpack(baseConfig, loaderOptions, done) {
536-
const webpackConfig = merge(
537-
{
538-
mode: 'development',
539-
output: {
540-
path: path.join(__dirname, 'output'),
541-
filename: 'bundle.js',
542-
libraryTarget: 'commonjs2',
543-
},
544-
module: {
545-
rules: [
546-
{
547-
test: /\.s[ac]ss$/,
548-
use: [
549-
{ loader: 'raw-loader' },
550-
{
551-
loader: pathToSassLoader,
552-
options: merge({ implementation }, loaderOptions),
553-
},
554-
],
555-
},
556-
],
557-
},
558-
},
559-
baseConfig
560-
);
570+
const [implName] = implementation.info.trim().split(/\s/);
571+
572+
it(`should load ${implName}`, (done) => {
573+
mockRequire.reRequire(pathToSassLoader);
574+
// eslint-disable-next-line global-require
575+
const module = require('module');
576+
// eslint-disable-next-line no-underscore-dangle
577+
const originalResolve = module._resolveFilename;
578+
579+
// eslint-disable-next-line no-underscore-dangle
580+
module._resolveFilename = function _resolveFilename(filename) {
581+
if (implName === 'node-sass' && filename.match(/^sass$/)) {
582+
const err = new Error('Some error');
561583

562-
webpack(webpackConfig, (webpackErr, stats) => {
563-
const err =
564-
webpackErr ||
565-
(stats.hasErrors() && stats.compilation.errors[0]) ||
566-
(stats.hasWarnings() && stats.compilation.warnings[0]);
584+
err.code = 'MODULE_NOT_FOUND';
567585

568-
done(err || null);
586+
throw err;
587+
}
588+
589+
if (implName === 'dart-sass' && filename.match(/^node-sass$/)) {
590+
const err = new Error('Some error');
591+
592+
err.code = 'MODULE_NOT_FOUND';
593+
594+
throw err;
595+
}
596+
597+
// eslint-disable-next-line prefer-rest-params
598+
return originalResolve.apply(this, arguments);
599+
};
600+
601+
const pathToFile = path.resolve(__dirname, './scss/simple.scss');
602+
603+
runWebpack(
604+
{
605+
entry: pathToFile,
606+
},
607+
{ implementation: null },
608+
(err) => {
609+
// eslint-disable-next-line no-underscore-dangle
610+
module._resolveFilename = originalResolve;
611+
612+
if (implName === 'node-sass') {
613+
mockRequire.reRequire('node-sass');
614+
}
615+
616+
if (implName === 'dart-sass') {
617+
mockRequire.reRequire('sass');
618+
}
619+
620+
done(err);
621+
}
622+
);
623+
});
569624
});
570-
}
625+
});
571626
});
572627
});
573628

test/scss/simple.scss

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
$font-stack: Helvetica, sans-serif;
2+
$primary-color: #333;
3+
4+
body {
5+
font: 100% $font-stack;
6+
color: $primary-color;
7+
}

0 commit comments

Comments
 (0)