Skip to content

Commit 837853e

Browse files
committed
feat: add compat modes
1 parent ee2efe7 commit 837853e

33 files changed

+3633
-164
lines changed

README.md

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
# `css-module-loader`
2+
3+
An implementation of webpack CSS module support independent of `css-loader`.
4+
5+
## Usage
6+
7+
```sh
8+
yarn add css-module-loader -D
9+
```
10+
11+
To configure, add `css-module-loader` _immediately_ after `css-loader`, disable
12+
any existing module options on `css-loader`.
13+
14+
**webpack.config.js**
15+
16+
```diff
17+
{
18+
...
19+
module: {
20+
rules: [
21+
{
22+
test: /\.module\.css$/
23+
use: [
24+
'style-loader',
25+
{
26+
loader: 'css-loader',
27+
options: {
28+
- modules: true
29+
+ importLoaders: 1,
30+
}
31+
},
32+
+ 'css-module-loader'
33+
]
34+
}
35+
]
36+
}
37+
}
38+
```
39+
40+
If you are using css modules with a preprocessor, make sure that `css-module-loader` comes before them. It should only receive CSS (plus CSS module syntax)
41+
42+
```diff
43+
test: /\.module\.css$/,
44+
use: [
45+
'style-loader',
46+
{
47+
loader: 'css-loader',
48+
options: {
49+
- importLoaders: 1
50+
+ importLoaders: 2
51+
}
52+
},
53+
+ 'css-module-loader',
54+
'sass-loader'
55+
]
56+
```
57+
58+
## Differences Between `css-loader`
59+
60+
There are two big differences between the existing support in
61+
`css-loader`.
62+
63+
The first is that it's not coupled to css-loader. By not being embedded in `css-loader`, users have more flexibility around upgrades, extensions and modifications. We can also update `css-module-loader` independently!
64+
65+
The second difference is that `css-module-loader` does not use the original
66+
css-modules implementation. Instead, it is built on top of [modular-css](https://m-css.com/). `modular-css` is an actively developed off shoot of the original css-modules. It has more features, less footguns, and better and broader tooling support.
67+
68+
### Why not use the "official" css-modules?
69+
70+
The original css module tooling is mostly abandoned at this point. The original
71+
authors have moved on to new things and much of the tooling is stuck in a "bug fix" only limbo. This hasn't been a severe problem because css-modules is very
72+
stable, and fairly simple, but there are a lot of sharp edges in the implementation, that prevents certain DX improvements, like more helpful errors.
73+
74+
On the other hand `modular-css` is actively maintained, as well as very stable. It addresses a bunch of the css-modules sharp edges (like ambigious scoping rules) and adds in a few quality of life features, like `:external` and module level `composes`. `modular-css` is also much easier to extend and hack on!
75+
76+
### Migrating from css-loader
77+
78+
In many cases the switch to `modular-css` should be transparent, many of the "breaking" differences are around less common usage patterns. Even still, `css-module-loader` includes a "compat" mode that should help ease the transition
79+
80+
Enable compat mode via a loader option:
81+
82+
```js
83+
{
84+
loader: 'css-module-loader',
85+
options: {
86+
compat: true
87+
}
88+
}
89+
```
90+
91+
Different compatibilty plugins can also be more finely enabled/disabled
92+
93+
```js
94+
{
95+
loader: 'css-module-loader',
96+
options: {
97+
compat: {
98+
scoping: false,
99+
valueAliasing: false,
100+
icssImports: true,
101+
icssExports: true,
102+
}
103+
}
104+
}
105+
```
106+
107+
The compatiblity plugins are outlined below:
108+
109+
#### `scoping`
110+
111+
ref: https://m-css.com/guide/#global
112+
113+
`css-modules` allows `:global` and `:local` scoping to nest, wheras `modular-css` only allows the `:global(.foo)` pseudo for marking a selector
114+
as global.
115+
116+
**When to enable:** If your code contains none-parameterized `:global` or `:local` pseudos like
117+
118+
```css
119+
.my-class :global .token {
120+
}
121+
```
122+
123+
**How to migrate:** Change to the parameterized form `:global(.token) {}`
124+
125+
#### `valueAliasing`
126+
127+
`css-modules` version of `@value`s allows for individually aliasing imported names:
128+
129+
```css
130+
@value primary as utilsPrimary from './utils.css';
131+
132+
.foo {
133+
color: utilsPrimary;
134+
}
135+
```
136+
137+
`modular-css` only (currently) allows importing by the exact name OR namespacing the entire import
138+
like: `@value * as utils from './utils.css';`
139+
140+
> Note: enabling this will also enable `icssImports` which is used to implement it.
141+
142+
**When to enable:** If you `@value` aliases, e.g.
143+
144+
```css
145+
@value primary as utilsPrimary from './utils.css';
146+
```
147+
148+
**How to migrate:** Switch to using `@value * as ns` to handle conflicts (see: https://m-css.com/guide/#namespaces)
149+
150+
```css
151+
@value * as utils from './utils.css';
152+
153+
.foo {
154+
color: utils.primary;
155+
}
156+
```
157+
158+
#### `icssExports`
159+
160+
A very niche feature of css-modules that you may not even know is possible.
161+
css-modules, contains a little IL (intermediary language) for defining imports and exports manually. Generally this is only used by tooling, as part of a compile step, but it is possible to write manually as well.
162+
163+
**When to enable:** If your code contains explicit `:export {}` rules
164+
165+
```css
166+
:export {
167+
myColor: red;
168+
navbarHeight: 4rem;
169+
}
170+
```
171+
172+
**How to migrate:** Switch to using `@value` (see: https://m-css.com/guide/#values)
173+
174+
#### `icssImports`
175+
176+
The pair to `icssExports`, allows importing and replacing local values via the ICSS
177+
import syntax
178+
179+
**When to enable:** If your code contains explicit `:import("./file.css") {}` rules
180+
181+
```css
182+
:import('./utils.css') {
183+
utilsPrimary: primary;
184+
}
185+
186+
.foo {
187+
color: utilsPrimary;
188+
}
189+
```
190+
191+
**How to migrate:** Switch to using `@value` and `@value * as ns` (see: https://m-css.com/guide/#values)
192+
193+
### Other notable (small) differences
194+
195+
These are things that aren't easy to write a compat plugin for and so may need
196+
direct migration.
197+
198+
- importing `@value` from other files can only import actual `@value`s not classes
199+
- `@value`s are not replaced in selectors. use `:external` to reference identifiers from other files

example/.eslintrc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"env": {
3+
"browser": true
4+
}
5+
}

example/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const styles = require('./styles/button.module.css');
2+
3+
document.body.innerHTML = `
4+
<div>
5+
<button class="${styles.foo}">button</p>
6+
</div>
7+
`;

example/styles/button.module.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
@value * as utils from './utils.module.css';
2+
3+
.foo {
4+
color: blue;
5+
background-color: utils.bgColor;
6+
}

example/styles/utils.module.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@value bgColor: palevioletred;

example/webpack.config.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const ExtractCSS = require('mini-css-extract-plugin');
2+
const { plugins } = require('webpack-atoms');
3+
4+
module.exports = {
5+
entry: './index.js',
6+
context: __dirname,
7+
output: {
8+
publicPath: '/',
9+
filename: 'main.js',
10+
path: `${__dirname}/build`,
11+
},
12+
devServer: {
13+
contentBase: './build',
14+
disableHostCheck: true,
15+
historyApiFallback: true,
16+
stats: 'minimal',
17+
},
18+
module: {
19+
rules: [
20+
{
21+
test: /\.module\.css/,
22+
use: [
23+
ExtractCSS.loader,
24+
{ loader: 'css-loader', options: { importLoaders: 1 } },
25+
{ loader: require.resolve('../lib/loader.js'), options: {} },
26+
],
27+
},
28+
],
29+
},
30+
plugins: [plugins.html(), new ExtractCSS()],
31+
};

lib/Utils.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const mapValues = require('lodash/mapValues');
2+
const findLast = require('lodash/findLast');
3+
4+
function getExports(file, messages, opts = {}) {
5+
const lastClsMsg = findLast(messages, 'classes');
6+
7+
return Object.assign(
8+
{},
9+
opts.exportValues ? mapValues(file.values, ({ value }) => value) : null,
10+
lastClsMsg.classes,
11+
...messages
12+
.filter(m => m.plugin && m.plugin.startsWith('modular-css-export'))
13+
.map(m => m.exports),
14+
);
15+
}
16+
17+
module.exports = {
18+
getExports,
19+
};

lib/loader.js

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
const path = require('path');
2+
13
const Processor = require('@modular-css/processor');
24
const { promisify } = require('util');
35
const { getOptions } = require('loader-utils');
@@ -6,6 +8,13 @@ const getLocalName = require('./getLocalName');
68
const PROCESSOR = Symbol('@modular-css processor');
79
const CACHE = Symbol('loadModule cache module');
810

11+
const compatPlugins = {
12+
scoping: require('./plugins/compat-scope-pseudos'),
13+
valueAliasing: require('./plugins/compat-value-aliasing'),
14+
icssImport: require('./plugins/compat-icss-import'),
15+
icssExport: require('./plugins/compat-icss-export'),
16+
};
17+
918
function getLoadFilePrefix(loaderContext) {
1019
// loads a file with all loaders configured after this one
1120
const loadersRequest = loaderContext.loaders
@@ -19,21 +28,41 @@ function getLoadFilePrefix(loaderContext) {
1928
function loader(src) {
2029
const { resourcePath, _compilation: compilation } = this;
2130
const cb = this.async();
31+
const fs = this._compilation.inputFileSystem;
2232

2333
const options = getOptions(this) || {};
2434

2535
const prefix = getLoadFilePrefix(this);
2636

37+
const resolver = (from, file) => {
38+
let resolved = file.replace(/^~/, '');
39+
if (!path.isAbsolute(resolved)) {
40+
resolved = path.resolve(path.dirname(from), file);
41+
}
42+
43+
try {
44+
fs.statSync(resolved);
45+
return resolved;
46+
} catch (err) {
47+
console.log(err);
48+
return false;
49+
}
50+
};
51+
2752
const loadFile = promisify((file, done) => {
53+
// console.log('LOAD FILE');
2854
if (compilation[CACHE].has(file)) {
2955
done(null, compilation[CACHE].get(file));
3056
return;
3157
}
3258

3359
this.loadModule(`${prefix}${file}`, (err, moduleSource) => {
34-
const content = JSON.parse(moduleSource.toString());
35-
// console.log('CACHE', file)
36-
compilation[CACHE].set(file, content);
60+
let content = '';
61+
if (moduleSource) {
62+
content = JSON.parse(moduleSource.toString());
63+
compilation[CACHE].set(file, content);
64+
}
65+
3766
done(err, content);
3867
});
3968
});
@@ -43,18 +72,41 @@ function loader(src) {
4372
}
4473

4574
if (!compilation[PROCESSOR]) {
46-
compilation[PROCESSOR] = new Processor({
75+
let compat = !options.compat ? [] : Object.entries(compatPlugins);
76+
77+
if (typeof options.compat === 'object') {
78+
const names = new Set(
79+
Object.entries(options.compat)
80+
.filter(e => e[1])
81+
.map(e => e[0]),
82+
);
83+
84+
compat = compat.filter(e => names.has(e[0]));
85+
}
86+
87+
compat = compat.map(([, plugin]) => plugin);
88+
89+
const mCssOptions = {
90+
exportGlobals: false,
4791
...options,
4892
loadFile,
4993
// this isn't run, b/c we don't combine css, but it'd be wrong
5094
// to do anyway since webpack handles it.
5195
rewrite: false,
96+
resolvers: [resolver, ...(options.resolvers || [])],
5297
namer: (filename, localName) =>
5398
getLocalName(filename, localName, this, options),
54-
processing: [require('./plugins/icss-import-export')]
55-
.concat(options.processing)
99+
before: compat
100+
.filter(p => p.phase === 'before')
101+
.concat(options.before)
56102
.filter(Boolean),
57-
});
103+
processing: []
104+
.concat(compat.filter(p => p.phase === 'processing'))
105+
.concat(require('./plugins/icss-import-export'), options.processing)
106+
.filter(Boolean),
107+
};
108+
109+
compilation[PROCESSOR] = new Processor(mCssOptions);
58110
}
59111

60112
const processor = compilation[PROCESSOR];

0 commit comments

Comments
 (0)