postcss-modules-component-plugin v0.0.4
This module provides linkage between postcss-modules output and React (or other view component) import / require statements.
You will find this very useful if you wish to create component-oriented style modules that you can import directly from your JavaScript modules; and also easily share code with external libraries.
But be warned, this module is a dirty hack around various limitations in css-loader, postcss-modules, postcss-import and Webpack, and will probably go away once one of these tools / ecosystems evolves to a sufficient featureset. It was created out of frustration after many hours of head-banging — so if you're reading this thinking, "this sounds dumb...", then please open an issue explaining to me how to get these things working the right way.
What it does
The current problem with "the done thing" in Webpack-land is that css-loader does its module parsing at the end of the compilation process, leaving it unable (AFAIK) to make decisions about source files from their original locations. This is very annoying when you want to set default CSS scopes based on file location. Ideally you want a dependency graph that says:
MyComponent.js ──┐
┌─────────────┘
└──> ./MyComponent.scss (LOCAL) ──┐
┌─────────────────────────┘
│
├──> ${root}/src/framework.scss (LOCAL) ──┐
│ ┌─────────────────────────────────┘
│ │
│ ├──> ${root}/node_modules/some-lib/_variables.scss (GLOBAL)
│ │
│ ├──> ./_variables.scss (LOCAL)
│ │
│ └──> ${root}/node_modules/some-lib/_mixins.scss (GLOBAL)
│
│
└──> ${root}/node_modules/some-lib/ComponentBase.scss (GLOBAL)This module allows you to achieve this, so that a class of .currentElement in MyComponent.scss will compile to something like .MyComponent🇮🇳_currentElement_7KOWY while in ComponentBase.scss it will come out as simply .currentElement. All in a way you can access directly from your JavaScript view components.
Caveats
- If local components use the same class names as global components which have been previously loaded, these will come out global.
- Stylesheets which load other local styles will end up with the inner class names localised twice.
Usage
Simply make the following additions to your webpack config. Note the configuration is setup for use with an .scss file extension but you can import css, less, pcss or whatever you want so long as you have the rest of your postcss plugins configured to deal with them.
const loaderUtils = require('loader-utils');
const postCSSModuleComponents = require('postcss-modules-component-plugin');
// optional:
postCSSModuleComponents.setLocalModuleNameFormat('[name][emoji]_[localName]_[hash:base64:5]');
postCSSModuleComponents.setGlobalModulesWhitelist([
/\/node_modules\//, // <-- this is the default
/src\/views\/globalView\.scss$/,
]);
//...
const moduleLoaderPlugin = postcssModules({
generateScopedName: postCSSModuleComponents.scopedName,
getJSON: postCSSModuleComponents.writer,
});
//...
module.exports = {
//...
module: {
loaders: [
//...
{
test: /\.(scss)$/, // or whichever
exclude: [/\/node_modules/],
loaders: [
{ loader: 'style-loader' },
{ loader: postCSSModuleComponents.loader() },
{ loader: 'css-loader', query: { sourceMap: true, importLoaders: 1 } },
{ loader: 'postcss-loader' },
],
},
//...
]
},
//...
postcss: function(webpack) {
return {
plugins: [
// :IMPORTANT: must be first
partialImport({
extension: 'scss',
addDependencyTo: webpack,
plugins: [moduleLoaderPlugin],
}),
moduleLoaderPlugin,
//...
],
};
},
//...
};Note that moduleLoaderPlugin appears TWICE in the postcss plugin definition. This is very important- the plugin needs to run over modules before flattening partials, as well as over the final compiled module before continuing.
How it works
If you study the above diagram you'll notice that MyComponent.scss has to render and return the classes it defines as local by default, and those ComponentBase.scss defines as global. The only way this can be done is during the PostCSS compilation phase, specifically by postcss-modules within postcss-import's plugins within PostCSS. Phrew. And then we need another postcss-modules pass on top of all of that to send out the class names differently, because in the toplevel case we want to emit a mix of local and global classes.
In the plugin, we:
- Bind to the
getJSONfeature ofpostcss-modulesto retain the parsed classname output for each module. - Intervene before
postcss-importpicks up each file and mark all encountered class names as global if the filename matches one of our global whitelist regexes. If the filename doesn't define an automatic global context we return a localised class name. - Intervene again after
postcss-importhas merged all the@imported files together and, using the flags we picked up in step 2, emit either global or local classnames accordingly. - Bind to the webpack loader stack after
css-loaderhas generated its code in order to inject the cached JSON from step 1 into the final payload (which will be empty, sincecss-loaderisn't running in module mode.
Todo
- Attempts with multiple instances of
postcss-modulesplugin resulted in issues, if we can revisit with two separate instances may be able to solve the "local classnames can be shadowed by globals" problem.
Author
Made with love at everledger.io
(...and OK yes, some frustration :p)
License
MIT