Skip to content

Commit

Permalink
feat: support auto resolving dart-sass
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
evilebottnawi authored Dec 14, 2018
1 parent f524223 commit ff90dd6
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 48 deletions.
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
22 changes: 20 additions & 2 deletions lib/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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;
147 changes: 101 additions & 46 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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');
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
}
);
});
});
}
});
});
});

Expand Down
7 changes: 7 additions & 0 deletions test/scss/simple.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
$font-stack: Helvetica, sans-serif;
$primary-color: #333;

body {
font: 100% $font-stack;
color: $primary-color;
}

0 comments on commit ff90dd6

Please sign in to comment.