As webpack doesn't provide a way to run several loaders for the same file [1], in order to produce many themed css style files from the same compilation, some hack has to be used.
This was tested using:
extract-text-webpack-plugin
:3.0.2
webpack
:3.10.0
node-sass
:4.7.2
sass-loader
:6.0.6
style-loader
:0.19.1
css-loader
:0.28.9
The idea behind:
- Use SASS's variables in order to provide differences between themes (each theme has got it's own varaibles definition file)
- For each theme, generate a valid ExtractPlugin configuration (putting the sass loader with it's themed variable definition)
- In order to process a file several times, we'll use multi-loader's approach (ie.: we'll duplicate imports inlining all the loader configuration)
- Inlinning loader configuration can't be done if the configuration is too complex [2], luckily webpack stores configuration in the compilation's RuleSet under the property
references
. These references use an id which is called ident. We'll use the AddIdentPlugin in order to add the configuration to the RuleSet.
- Multiple importing leads to a unused anonymous object in the js file
- Breaking
style-loader
- Hackish use of webpack's internals
// webpack.config.js
// ... imports
const AddIdentPlugin = require('./AddIdentPlugin');
/** Reads variables particular to the theme.
@param {String} theme - name of the theme
@return {String} Theme's variables
*/
const readThemedData = theme => { ... };
/** Builds webpack's style configuration
@param {String} theme - name of the theme
@return {Object} theme configuration - {
{WebpackPlugin} plugin: ExtractPlugin instance
{WebpackLoader} loader: ExtractPlugin loader
}
*/
const buildTheme = theme => {
const plugin = new ExtractTextPlugin({
filename: `styles.${theme}.[name].[contenthash].css`,
allChunks: true
});
const loader = plugin.extract({
fallback: 'style-loader',
use: [
{
loader: 'css-loader',
options: {
modules: true,
importLoaders: 2,
}
},
'postcss-loader',
{
loader: 'sass-loader',
options: {
data: readThemedData(theme)
}
}
]
});
return {
plugin,
loader
};
};
const themes = ['first-theme', 'another-nice-theme'];
const themesConf = themes.map(theme => buildTheme(theme));
module.exports = {
// I'll ommit not relevant webpack configuration
module: {
rules: [
// ...
{
test: /\.scss$/,
loader: path.resolve('./multi-loader.js'),
options: {
loaders: themesConf.map(t => t.loader)
}
}
]
},
plugins: [
// ...
...themesConf.map(t => t.plugin),
new AddIdentPlugin({ loaders: themesConf.reduce((all, l) => all.concat(l.loader), []) })
]
};
[1] Webpack doesn't allow to run different loaders pahts for the same file, because in webpack's point of view, this doesnt' have sense. Loaders are used to transform the specified file's code and assined to the variable, if several loader paths are executed what will be the valued assigned to the variable?
[2] With complex configuration, i'm saying not only to not serializable conf (ie: object that i cannot transform with JSON.stringify/parse
) but also complex and long objects.
Thank you! That was really helpful