From a2045ce9a4351f55fa7c04ac8398559052b9bc9b Mon Sep 17 00:00:00 2001 From: Adam DiCarlo Date: Tue, 10 Nov 2015 16:25:53 -0800 Subject: [PATCH 1/2] Provide loaderContext to importers. This allows importers to resolve import paths using Webpack's API and add dependencies to the build. --- README.md | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ index.js | 2 +- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7f14c016..4f1e6bf4 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,78 @@ webpack provides an [advanced mechanism to resolve files](http://webpack.github. It's important to only prepend it with `~`, because `~/` resolves to the home-directory. webpack needs to distinguish between `bootstrap` and `~bootstrap` because CSS- and Sass-files have no special syntax for importing relative files. Writing `@import "file"` is the same as `@import "./file";` +#### Advanced: Using a custom node-sass importer with sass-loader + +Though sass-loader implements a custom importer, you can use your own as well. Include your importer function in your sassLoader options, like this: + +```javascript +var sass = require('node-sass') +module.exports = { + ... + module: { + loaders: [{ + test: /\.scss$/, + loaders: ['style', 'css', 'sass'] + }] + } + sassLoader: { + importer: specialImporter + } +}; +``` + +Writing an importer that can resolve properly is a bit involved, but is possible because sass-loader (as of version 3.1.3) gives your importer access to sass-loader's own `this` value via `this.options.loaderContext`, so that you can access Webpack's loader API. + +Here's an example implementation: + +```javascript +var loaderUtils = require('loader-utils'); +var sass = require('node-sass'); + +function specialImporter (url, fileContext, done) { + if (!shouldThisFileBeHandledByMyImporter(url)) { + // Return sass.NULL in order to declare you wish to "pass" on this url and + // let other importers handle it. Be careful, as this doesn't work correctly in + // an environment with multiple copies of node-sass (e.g., if your importer is + // inside of a symlinked package). This can manifest as a strange Sass error + // like "No such mixin foo", when `foo` is @import'd from another Sass file. + return sass.NULL; + } + + // Let's run the URL through webpack's resolvers. Sass-loader includes the + // dirname of the entry point file (the initially require()'d scss file) in + // includePaths as the last entry. + var includePaths = this.options.includePaths.split(':'); + + // If we're given fileContext (and it's not 'stdin'), then fileContext is the + // path of the file doing the import--i.e., the import we're processing is the + // result of an @import from within Sass, rather than of a require() from + // within JS. + var workingDir = fileContext && fileContext !== 'stdin' + ? path.dirname(fileContext) + : includePaths[includePaths.length - 1]; + var filePath = loaderUtils.urlToRequest(url, workingDir); + if (filePath[0] === '.') { + filePath = path.resolve(workingDir, filePath); + } + + var loaderContext = this.options.loaderContext; + var resolve = loaderContext.resolve.bind(loaderContext); + + resolve(workingDir, filePath, function resolveCallback (err, filename) { + if (err) done(err); + + // Tell Webpack about the file being part of the build. + this.options.loaderContext.addDependency(filename); + + // Return the result or error via the `done` function: + done({ contents: '/* Resulting Sass code goes here */' }); + // If there's an error, send an error object or the error you caught: + // done(new Error('Helpful error message about the file')) + }.bind(this)) +} +``` + ### Problems with `url(...)` Since Sass/[libsass](https://github.com/sass/libsass) does not provide [url rewriting](https://github.com/sass/libsass/issues/532), all linked assets must be relative to the output. diff --git a/index.js b/index.js index 7d5a424d..7f64c2f6 100644 --- a/index.js +++ b/index.js @@ -381,5 +381,5 @@ function getLoaderConfig(loaderContext) { delete query.config; - return assign({}, config, query); + return assign({ loaderContext: loaderContext }, config, query); } From 3fc5e23d687a9fb12e39221c44dfa08bfc770858 Mon Sep 17 00:00:00 2001 From: Adam DiCarlo Date: Fri, 27 Nov 2015 22:16:00 -0800 Subject: [PATCH 2/2] README: Rewrite custom importer section, adding use case --- README.md | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 4f1e6bf4..966b845c 100644 --- a/README.md +++ b/README.md @@ -99,12 +99,18 @@ webpack provides an [advanced mechanism to resolve files](http://webpack.github. It's important to only prepend it with `~`, because `~/` resolves to the home-directory. webpack needs to distinguish between `bootstrap` and `~bootstrap` because CSS- and Sass-files have no special syntax for importing relative files. Writing `@import "file"` is the same as `@import "./file";` -#### Advanced: Using a custom node-sass importer with sass-loader +#### Advanced: Doing wild things with a custom node-sass importer -Though sass-loader implements a custom importer, you can use your own as well. Include your importer function in your sassLoader options, like this: +node-sass's `importer` API allows you to define custom handlers for Sass's `@import` directive. While sass-loader defines its own importer callback to integrate `@import` with webpack's resolve mechanism, it will also pass along your own importer callback to node-sass. This allows your code a chance to "steal" the import and handle it, or pass on it and let sass-loader handle it as normal. + +Why would you want to do this? Well, maybe you want your Sass to be able to `@import` something other than Sass or normal stylesheets... like Stylus, or LESS. You could write an importer that checks for the appropriate file extension, and invokes another compiler, replacing the `@import` with compiled CSS before it gets to node-sass. Or you could even transpile it to Sass. + +Luckily, sass-loader won't get in your way if you want to do this. sass-loader will pass along your importer to node-sass and (as of 3.1.3) allow you to access webpack's loader API via `this.options.loaderContext`. For now, you'll have to handle the path resolution logic yourself, though, since path resolution and actual processing of imports are tightly coupled together. + +Include your importer function in your sassLoader options, like this: ```javascript -var sass = require('node-sass') +var sass = require('node-sass'); module.exports = { ... module: { @@ -112,16 +118,13 @@ module.exports = { test: /\.scss$/, loaders: ['style', 'css', 'sass'] }] - } + }, sassLoader: { - importer: specialImporter + importer: stylusImporter } }; ``` - -Writing an importer that can resolve properly is a bit involved, but is possible because sass-loader (as of version 3.1.3) gives your importer access to sass-loader's own `this` value via `this.options.loaderContext`, so that you can access Webpack's loader API. - -Here's an example implementation: +And then, use loader-util's and webpack's resolve in your importer: ```javascript var loaderUtils = require('loader-utils'); @@ -166,8 +169,8 @@ function specialImporter (url, fileContext, done) { // Return the result or error via the `done` function: done({ contents: '/* Resulting Sass code goes here */' }); // If there's an error, send an error object or the error you caught: - // done(new Error('Helpful error message about the file')) - }.bind(this)) + // done(new Error('Helpful error message about the file')); + }.bind(this)); } ```