Skip to content

Commit

Permalink
feat: export a decoupled version of the Sass importer
Browse files Browse the repository at this point in the history
This will be used by `vue-jest` and potentially other projects, to write
a Jest transform that can adequately mimic `sass-loader`'s behaviour.

Closes #873
  • Loading branch information
vvanpo committed Aug 2, 2020
1 parent 5e7bd57 commit 7005704
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 3 deletions.
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -77,6 +77,7 @@
"css-loader": "^3.6.0",
"del": "^5.1.0",
"del-cli": "^3.0.1",
"enhanced-resolve": "^4.3.0",
"eslint": "^7.3.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-import": "^2.21.2",
Expand Down
73 changes: 73 additions & 0 deletions src/importer.js
@@ -0,0 +1,73 @@
import { getSassImplementation, getWebpackResolver } from './utils';

/**
* A factory function for creating a Sass importer that uses `sass-loader`'s
* resolution rules.
*
* @see https://sass-lang.com/documentation/js-api#importer
*
* This is useful when attempting to mimic `sass-loader`'s behaviour in contexts
* that do not support Webpack. For example, it could be used to write a Jest
* transform for testing files with Sass imports.
*
* The resulting Sass importer is asynchronous, so it can only be used with
* `sass.render()` and not `renderSync()`.
*
* Example usage:
* ```js
* import sass from 'sass';
* import resolve from 'enhanced-resolve';
* import createImporter from 'sass-loader/dist/importer';
* import webpackConfig = './webpack.config';
*
* const { resolve: { alias } } = webpackConfig;
* const resolverFactory = (options) => resolve.create({ alias, ...options });
* const importer = createImporter(resolverFactory, sass);
*
* sass.render({
* file: 'input.scss',
* importer,
* }, (err, result) => {
* // ...
* });
* ```
*
* @param {Function} resolverFactory - A factory function for creating a Webpack
* resolver. The resulting `resolve` function should be compatible with the
* asynchronous resolve function supplied by [`enhanced-resolve`]{@link
* https://github.com/webpack/enhanced-resolve}. In all likelihood you'll want
* to pass `resolve.create()` from `enhanced-resolve`, or a wrapped copy of
* it.
* @param {Object} [implementation] - The imported Sass implementation, both
* `sass` (Dart Sass) and `node-sass` are supported. If no implementation is
* supplied, `sass` will be preferred if it's available.
* @param {string[]} [includePaths] - The list of include paths passed to Sass.
*
* @returns {Function}
*/
export default function createSassImporter(
resolverFactory,
implementation = null,
includePaths = []
) {
if (!implementation) {
// eslint-disable-next-line no-param-reassign
implementation = getSassImplementation();
}

const resolve = getWebpackResolver(
implementation,
resolverFactory,
includePaths
);

return (url, prev, done) => {
resolve(prev, url)
.then((result) => {
done({ file: result });
})
.catch(() => {
done(null);
});
};
}
95 changes: 95 additions & 0 deletions test/__snapshots__/importer.test.js.snap
@@ -0,0 +1,95 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`importer should resolve imports when passed to \`sass\` 1`] = `
"@charset \\"UTF-8\\";
/* @import another/module */
@import url(http://example.com/something/from/the/interwebs);
.another-sass-module {
background: hotpink;
}
/* @import another/underscore */
.underscore {
background: hotpink;
}
/* @import another/_underscore */
.underscore {
background: hotpink;
}
/* @import ~sass/underscore */
.underscore-sass {
background: hotpink;
}
/* @import ~sass/some.module */
.some-sass-module {
background: hotpink;
}
/* @import url(http://example.com/something/from/the/interwebs); */
/* scoped import @import language */
.scoped-import body {
font: 100% Helvetica, sans-serif;
color: #333;
}
.scoped-import nav ul {
margin: 0;
padding: 0;
list-style: none;
}
.scoped-import nav li {
display: inline-block;
}
.scoped-import nav a {
display: block;
padding: 6px 12px;
text-decoration: none;
}
.scoped-import .box {
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
-ms-border-radius: 10px;
border-radius: 10px;
}
.scoped-import .message, .scoped-import .warning, .scoped-import .error, .scoped-import .success {
border: 1px solid #ccc;
padding: 10px;
color: #333;
}
.scoped-import .success {
border-color: green;
}
.scoped-import .error {
border-color: red;
}
.scoped-import .warning {
border-color: yellow;
}
.scoped-import .foo:before {
content: \\"\\";
}
.scoped-import .bar:before {
content: \\"\\";
}
/* @import util */
.util {
color: hotpink;
}
/* @import ~module */
.module {
background: hotpink;
}
/* @import ~another */
.another-scss-module-from-node-modules {
background: hotpink;
}
a {
color: red;
}"
`;
22 changes: 22 additions & 0 deletions test/importer.test.js
@@ -0,0 +1,22 @@
import sass from 'sass';
import resolve from 'enhanced-resolve';

import createSassImporter from '../src/importer';

describe('importer', () => {
it('should resolve imports when passed to `sass`', (done) => {
const importer = createSassImporter(resolve.create, sass);

sass.render(
{
file: 'test/sass/imports.sass',
importer,
},
(err, result) => {
expect(result.css.toString()).toMatchSnapshot();

done(err);
}
);
});
});

0 comments on commit 7005704

Please sign in to comment.