diff --git a/package-lock.json b/package-lock.json index 0973f707..df2d40c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6196,9 +6196,9 @@ } }, "enhanced-resolve": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.2.0.tgz", - "integrity": "sha512-S7eiFb/erugyd1rLb6mQ3Vuq+EXHv5cpCkNqqIkYkBgN2QdFnyCZzFBleqwGEx4lgNGYij81BWnCrFNK7vxvjQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz", + "integrity": "sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ==", "dev": true, "requires": { "graceful-fs": "^4.1.2", diff --git a/package.json b/package.json index e7455913..ed2746d2 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/importer.js b/src/importer.js new file mode 100644 index 00000000..5312d377 --- /dev/null +++ b/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); + }); + }; +} diff --git a/test/__snapshots__/importer.test.js.snap b/test/__snapshots__/importer.test.js.snap new file mode 100644 index 00000000..6bda69c9 --- /dev/null +++ b/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; +}" +`; diff --git a/test/importer.test.js b/test/importer.test.js new file mode 100644 index 00000000..cbd739d4 --- /dev/null +++ b/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); + } + ); + }); +});