Skip to content

Commit 475d39e

Browse files
committed
new :local syntax
added experimental :extend and from syntax splitted tests into multiple files
1 parent 69947fc commit 475d39e

File tree

8 files changed

+526
-166
lines changed

8 files changed

+526
-166
lines changed

README.md

+70-15
Original file line numberDiff line numberDiff line change
@@ -60,46 +60,101 @@ The result is:
6060

6161
### Local scope
6262

63-
(experimental)
64-
65-
By default CSS exports all class names into a global selector scope. This is a feature which tries to offer a local selector scope.
63+
By default CSS exports all class names into a global selector scope. This is a feature which offer a local selector scope.
6664

67-
The syntax `.local[className]` can be used to declare `className` in the local scope. The local identifiers are exported by the module.
65+
The syntax `:local(.className)` can be used to declare `className` in the local scope. The local identifiers are exported by the module.
6866

6967
It does it by replacing the selectors by unique identifiers. The choosen unique identifiers are exported by the module.
7068

7169
Example:
7270

7371
``` css
74-
.local[className] { background: red; }
75-
#local[someId] { background: green; }
76-
.local[className] .local[subClass] { color: green; }
77-
#local[someId] .local[subClass] { color: blue; }
72+
:local(.className) { background: red; }
73+
:local(#someId) { background: green; }
74+
:local(.className .subClass) { color: green; }
75+
:local(#someId .subClass) { color: blue; }
7876
```
7977

8078
is transformed to
8179

8280
``` css
83-
.ze24205081ae540afa51bd4cce768e8b7 { background: red; }
84-
#zdf12049771f7fc796a63a3945da3a66d { background: green; }
85-
.ze24205081ae540afa51bd4cce768e8b7 .z9f634213cd27594c1a13d18554d47a8c { color: green; }
86-
#zdf12049771f7fc796a63a3945da3a66d .z9f634213cd27594c1a13d18554d47a8c { color: blue; }
81+
._23_aKvs-b8bW2Vg3fwHozO { background: red; }
82+
#_1j3LM6lKkKzRIt19ImYVnD { background: green; }
83+
._23_aKvs-b8bW2Vg3fwHozO ._13LGdX8RMStbBE9w-t0gZ1 { color: green; }
84+
#_1j3LM6lKkKzRIt19ImYVnD ._13LGdX8RMStbBE9w-t0gZ1 { color: blue; }
8785
```
8886

8987
and the identifiers are exported:
9088

9189
``` js
9290
exports.locals = {
93-
className: "ze24205081ae540afa51bd4cce768e8b7",
94-
someId: "zdf12049771f7fc796a63a3945da3a66d",
95-
subClass: "z9f634213cd27594c1a13d18554d47a8c"
91+
className: "_23_aKvs-b8bW2Vg3fwHozO",
92+
someId: "_1j3LM6lKkKzRIt19ImYVnD",
93+
subClass: "_13LGdX8RMStbBE9w-t0gZ1"
9694
}
9795
```
9896

9997
You can configure the generated ident with the `localIdentName` query parameter (default `[hash:base64]`). Example: `css-loader?localIdentName=[path][name]---[local]---[hash:base64:5]` for easier debugging.
10098

10199
Note: For prerendering with extract-text-webpack-plugin you should use `css-loader/locals` instead of `style-loader!css-loader` in the prerendering bundle. It doesn't embed CSS but only exports the identifier mappings.
102100

101+
### Inheriting
102+
103+
(experimental)
104+
105+
When declaring a local class name you can inherit from another local class name.
106+
107+
``` css
108+
:local(.className) {
109+
background: red;
110+
color: yellow;
111+
}
112+
113+
:local(.subClass):extends(.className) {
114+
background: blue;
115+
}
116+
```
117+
118+
This doesn't result in any change to the CSS itself but exports multiple class names:
119+
120+
``` js
121+
exports.locals = {
122+
className: "_23_aKvs-b8bW2Vg3fwHozO",
123+
subClass: "_13LGdX8RMStbBE9w-t0gZ1 _23_aKvs-b8bW2Vg3fwHozO"
124+
}
125+
```
126+
127+
and CSS is transformed to:
128+
129+
``` css
130+
._23_aKvs-b8bW2Vg3fwHozO {
131+
background: red;
132+
color: yellow;
133+
}
134+
135+
._13LGdX8RMStbBE9w-t0gZ1 {
136+
background: blue;
137+
}
138+
```
139+
140+
### Importing local class names
141+
142+
(experimental)
143+
144+
To import a local class name from another module:
145+
146+
``` css
147+
:local(.continueButton):extends(.button from "library/button.css") {
148+
background: red;
149+
}
150+
```
151+
152+
``` css
153+
:local(.nameEdit):extends(.edit.highlight from "./edit.css") {
154+
background: red;
155+
}
156+
```
157+
103158
### SourceMaps
104159

105160
To include SourceMaps set the `sourceMap` query param.

lib/loader.js

+88-19
Original file line numberDiff line numberDiff line change
@@ -26,45 +26,116 @@ module.exports = function(content, map) {
2626

2727
var result = [];
2828

29+
// for importing CSS
30+
var loadersRequest = this.loaders.slice(
31+
this.loaderIndex,
32+
this.loaderIndex + 1 + importLoaders
33+
).map(function(x) { return x.request; }).join("!");
34+
var importUrlPrefix = "-!" + loadersRequest + "!";
35+
2936
var stuff = parseSource(content);
3037

3138
var replacer = new ReplaceMany();
39+
40+
// store already imported files
41+
var importedUrls = [];
42+
43+
// add @imports to result
3244
stuff.imports.forEach(function(imp) {
3345
replacer.replace(imp.start, imp.length, "");
3446
if(!loaderUtils.isUrlRequest(imp.url, root)) {
3547
result.push("exports.push([module.id, " +
3648
JSON.stringify("@import url(" + imp.url + ");") + ", " +
3749
JSON.stringify(imp.mediaQuery) + "]);");
3850
} else {
39-
var loadersRequest = this.loaders.slice(
40-
this.loaderIndex,
41-
this.loaderIndex + 1 + importLoaders
42-
).map(function(x) { return x.request; }).join("!");
43-
var importUrl = "-!" +
44-
loadersRequest + "!" +
45-
loaderUtils.urlToRequest(imp.url);
51+
var importUrl = importUrlPrefix + loaderUtils.urlToRequest(imp.url);
4652
result.push("exports.i(require(" + loaderUtils.stringifyRequest(this, importUrl) + "), " + JSON.stringify(imp.mediaQuery) + ");");
53+
if(!imp.mediaQuery)
54+
importedUrls.push(importUrl);
4755
}
4856
}, this);
49-
stuff.urls.forEach(function(url, idx) {
50-
replacer.replace(url.start, url.length, "__CSSLOADERURL_" + idx + "__");
51-
});
57+
58+
// replace url(...)
59+
if(query.url !== false) {
60+
stuff.urls.forEach(function(url, idx) {
61+
replacer.replace(url.start, url.length, "__CSSLOADERURL_" + idx + "__");
62+
});
63+
}
64+
65+
// replace :local()
5266
var locals = {};
67+
var localExtends = {};
5368
stuff.locals.forEach(function(local) {
5469
var ident;
55-
if(!locals[local.name]) {
56-
ident = getLocalIdent(this, localIdentName, local.name, {
70+
var name = local.name;
71+
if(!locals[name]) {
72+
ident = getLocalIdent(this, localIdentName, name, {
5773
regExp: localIdentRegExp
5874
});
59-
locals[local.name] = ident;
75+
locals[name] = ident;
6076
} else {
61-
ident = locals[local.name];
77+
ident = locals[name];
78+
}
79+
if(local.extends) {
80+
local.extends.forEach(function(extend) {
81+
if(!localExtends[name])
82+
localExtends[name] = [];
83+
localExtends[name].push(extend);
84+
});
6285
}
6386
replacer.replace(local.start, local.length, local.prefix + ident);
6487
}, this);
6588

89+
// remove stuff
90+
stuff.remove.forEach(function(rem) {
91+
replacer.replace(rem.start, rem.length, "");
92+
});
93+
94+
// pass errors from parser
95+
if(this.emitError) {
96+
stuff.errors.forEach(function(err) {
97+
this.emitError(err);
98+
}, this);
99+
}
100+
101+
// generate the locals
102+
var localKeys = Object.keys(locals);
103+
if(localKeys.length > 0) {
104+
var localLines = localKeys.map(function(key, idx) {
105+
var line = " " + JSON.stringify(key) + ": ";
106+
function addExtend(extend) {
107+
if(extend.from) {
108+
var importUrl = importUrlPrefix + extend.from;
109+
if(importedUrls.indexOf(importUrl) < 0) {
110+
result.push("exports.i(require(" + loaderUtils.stringifyRequest(this, importUrl) + "), \"\");");
111+
importedUrls.push(importUrl);
112+
}
113+
line += " + \" \" + require(" + loaderUtils.stringifyRequest(this, importUrl) + ").locals[" + JSON.stringify(extend.name) + "]";
114+
} else if(locals[extend.name]) {
115+
line += " + \" \" + " + JSON.stringify(locals[extend.name]);
116+
if(localExtends[extend.name]) {
117+
localExtends[extend.name].forEach(addExtend, this);
118+
}
119+
} else if(this.emitError) {
120+
this.emitError("Cannot extend from unknown class '" + extend.name + "'");
121+
}
122+
}
123+
line += JSON.stringify(locals[key]);
124+
if(localExtends[key]) {
125+
localExtends[key].forEach(addExtend, this);
126+
}
127+
if(idx !== localKeys.length - 1) line += ",";
128+
return line;
129+
}, this);
130+
result.push("exports.locals = {");
131+
result.push(localLines.join("\n"));
132+
result.push("};");
133+
}
134+
135+
// transform the CSS
66136
var cssContent = replacer.run(content);
67137

138+
// minimize CSS
68139
if(minimize) {
69140
var options = Object.create(query);
70141
if(query.sourceMap && map) {
@@ -81,8 +152,8 @@ module.exports = function(content, map) {
81152
return JSON.stringify(str).replace(/^"|"$/g, "");
82153
}
83154

155+
// replace url(...) in the generated code
84156
var css = JSON.stringify(cssContent);
85-
86157
var urlRegExp = /__CSSLOADERURL_[0-9]+__/g;
87158
css = css.replace(urlRegExp, function(str) {
88159
var match = /^__CSSLOADERURL_([0-9]+)__$/.exec(str);
@@ -106,6 +177,7 @@ module.exports = function(content, map) {
106177
return "\"+require(" + loaderUtils.stringifyRequest(this, loaderUtils.urlToRequest(url, root)) + ")+\"";
107178
}.bind(this));
108179

180+
// add a SourceMap
109181
if(query.sourceMap && !minimize) {
110182
var cssRequest = loaderUtils.getRemainingRequest(this);
111183
var request = loaderUtils.getCurrentRequest(this);
@@ -131,10 +203,7 @@ module.exports = function(content, map) {
131203
result.push("exports.push([module.id, " + css + ", \"\"]);");
132204
}
133205

134-
if(Object.keys(locals).length > 0) {
135-
result.push("exports.locals = " + JSON.stringify(locals) + ";");
136-
}
137-
206+
// embed runtime
138207
return "exports = module.exports = require(" + loaderUtils.stringifyRequest(this, require.resolve("./css-base.js")) + ")();\n" +
139208
result.join("\n");
140209
};

0 commit comments

Comments
 (0)