Skip to content

Commit 881e17c

Browse files
committed
renamed placeholders to locals
added new placeholder syntax
1 parent 0ba1ac7 commit 881e17c

9 files changed

+144
-113
lines changed

README.md

+38-28
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## installation
44

5-
`npm install css-loader`
5+
`npm install css-loader --save-dev`
66

77
## Usage
88

@@ -30,24 +30,51 @@ module.exports = {
3030
module: {
3131
loaders: [
3232
{ test: /\.css$/, loader: "style-loader!css-loader" },
33-
{ test: /\.png$/, loader: "url-loader?limit=100000&mimetype=image/png" },
33+
{ test: /\.png$/, loader: "url-loader?limit=100000" },
3434
{ test: /\.jpg$/, loader: "file-loader" }
3535
]
3636
}
3737
};
3838
```
3939

40-
### Placeholders
40+
### 'Root-relative' urls
41+
42+
For urls that start with a `/`, the default behavior is to not translate them:
43+
* `url(/image.png)` => `url(/image.png)`
44+
45+
If a `root` query parameter is set, however, it will be prepended to the url
46+
and then translated:
47+
48+
With a config like:
49+
50+
``` javascript
51+
loaders: [
52+
{ test: /\.css$/, loader: "style-loader!css-loader?root=." },
53+
...
54+
]
55+
```
56+
57+
The result is:
58+
59+
* `url(/image.png)` => `require("./image.png")`
60+
61+
### Local scope
4162

4263
(experimental)
4364

44-
Special selectors are automatically replaced with random identifiers, which are exported:
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.
66+
67+
The syntax `.local[className]` can be used to declare `className` in the local scope. The local identifiers are exported by the module.
68+
69+
It does it by replacing the selectors by unique identifiers. The choosen unique identifiers are exported by the module.
70+
71+
Example:
4572

4673
``` css
47-
.[className] { background: red; }
48-
#[someId] { background: green; }
49-
.[className] .[subClass] { color: green; }
50-
#[someId] .[subClass] { color: blue; }
74+
.local[className] { background: red; }
75+
#local[someId] { background: green; }
76+
.local[className] .local[subClass] { color: green; }
77+
#local[someId] .local[subClass] { color: blue; }
5178
```
5279

5380
is transformed to
@@ -62,33 +89,16 @@ is transformed to
6289
and the identifiers are exported:
6390

6491
``` js
65-
exports.placeholders = {
92+
exports.locals = {
6693
className: "ze24205081ae540afa51bd4cce768e8b7",
6794
someId: "zdf12049771f7fc796a63a3945da3a66d",
6895
subClass: "z9f634213cd27594c1a13d18554d47a8c"
6996
}
7097
```
7198

72-
### 'Root-relative' urls
73-
74-
For urls that start with a `/`, the default behavior is to not translate them:
75-
* `url(/image.png)` => `url(/image.png)`
76-
77-
If a `root` query parameter is set, however, it will be prepended to the url
78-
and then translated:
79-
80-
With a config like:
99+
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.
81100

82-
``` javascript
83-
loaders: [
84-
{ test: /\.css$/, loader: "style-loader!css-loader?root=." },
85-
...
86-
]
87-
```
88-
89-
The result is:
90-
91-
* `url(/image.png)` => `require("./image.png")`
101+
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.
92102

93103
### SourceMaps
94104

lib/getLocalIdent.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
MIT License http://www.opensource.org/licenses/mit-license.php
3+
Author Tobias Koppers @sokra
4+
*/
5+
var loaderUtils = require("loader-utils");
6+
module.exports = function getLocalIdent(loaderContext, localIdentName, localName, options) {
7+
var request = loaderContext.options && typeof loaderContext.options.context === "string" ?
8+
loaderUtils.stringifyRequest({ context: loaderContext.options.context }, loaderUtils.getRemainingRequest(loaderContext)) :
9+
loaderContext.request;
10+
options.content = localName + " " + request;
11+
options.context = loaderContext.options && typeof loaderContext.options.context === "string" ? loaderContext.options.context : loaderContext.context;
12+
localIdentName = localIdentName.replace(/\[local\]/gi, localName);
13+
var hash = loaderUtils.interpolateName(loaderContext, localIdentName, options);
14+
return hash.replace(/[^a-zA-Z0-9\-_]/g, "-").replace(/^([^a-zA-Z_])/, "_$1");
15+
};

lib/getPlaceholderIdent.js

-13
This file was deleted.

lib/loader.js

+14-10
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ var ReplaceMany = require("./ReplaceMany");
88
var loaderUtils = require("loader-utils");
99
var SourceListMap = require("source-list-map").SourceListMap;
1010
var CleanCSS = require("clean-css");
11-
var getPlaceholderIdent = require("./getPlaceholderIdent");
11+
var getLocalIdent = require("./getLocalIdent");
1212

1313
module.exports = function(content, map) {
1414
if(this.cacheable) this.cacheable();
@@ -17,6 +17,8 @@ module.exports = function(content, map) {
1717
var forceMinimize = query.minimize;
1818
var importLoaders = parseInt(query.importLoaders, 10) || 0;
1919
var minimize = typeof forceMinimize !== "undefined" ? !!forceMinimize : (this && this.minimize);
20+
var localIdentName = query.localIdentName || "[hash:base64]";
21+
var localIdentRegExp = query.localIdentRegExp;
2022

2123
if(typeof map !== "string") {
2224
map = JSON.stringify(map);
@@ -47,16 +49,18 @@ module.exports = function(content, map) {
4749
stuff.urls.forEach(function(url, idx) {
4850
replacer.replace(url.start, url.length, "__CSSLOADERURL_" + idx + "__");
4951
});
50-
var placeholders = {};
51-
stuff.placeholders.forEach(function(placeholder) {
52+
var locals = {};
53+
stuff.locals.forEach(function(local) {
5254
var ident;
53-
if(!placeholders[placeholder.name]) {
54-
ident = getPlaceholderIdent(this, placeholder.name);
55-
placeholders[placeholder.name] = ident;
55+
if(!locals[local.name]) {
56+
ident = getLocalIdent(this, localIdentName, local.name, {
57+
regExp: localIdentRegExp
58+
});
59+
locals[local.name] = ident;
5660
} else {
57-
ident = placeholders[placeholder.name];
61+
ident = locals[local.name];
5862
}
59-
replacer.replace(placeholder.start, placeholder.length, placeholder.prefix + ident);
63+
replacer.replace(local.start, local.length, local.prefix + ident);
6064
}, this);
6165

6266
var cssContent = replacer.run(content);
@@ -127,8 +131,8 @@ module.exports = function(content, map) {
127131
result.push("exports.push([module.id, " + css + ", \"\"]);");
128132
}
129133

130-
if(Object.keys(placeholders).length > 0) {
131-
result.push("exports.placeholders = " + JSON.stringify(placeholders) + ";");
134+
if(Object.keys(locals).length > 0) {
135+
result.push("exports.locals = " + JSON.stringify(locals) + ";");
132136
}
133137

134138
return "exports = module.exports = require(" + loaderUtils.stringifyRequest(this, require.resolve("./css-base.js")) + ")();\n" +

lib/localsLoader.js

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
MIT License http://www.opensource.org/licenses/mit-license.php
3+
Author Tobias Koppers @sokra
4+
*/
5+
var loaderUtils = require("loader-utils");
6+
var parseSource = require("./parseSource");
7+
var getLocalIdent = require("./getLocalIdent");
8+
9+
module.exports = function(content) {
10+
if(this.cacheable) this.cacheable();
11+
var query = loaderUtils.parseQuery(this.query);
12+
var localIdentName = query.localIdentName || "[hash:base64]";
13+
var localIdentRegExp = query.localIdentRegExp;
14+
15+
var stuff = parseSource(content);
16+
17+
var locals = {};
18+
stuff.locals.forEach(function(local) {
19+
if(!locals[local.name]) {
20+
locals[local.name] = getLocalIdent(this, localIdentName, local.name, {
21+
regExp: localIdentRegExp
22+
});
23+
}
24+
}, this);
25+
26+
return "module.exports = " + JSON.stringify(locals) + ";";
27+
};

lib/parseSource.js

+6-7
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ function urlMatch(match, textBeforeUrl, replacedText, url, index) {
1313
});
1414
}
1515

16-
function placeholderMatch(match, prefix, name, index) {
17-
this.placeholders.push({
16+
function localMatch(match, prefix, name, index) {
17+
this.locals.push({
1818
name: name,
1919
prefix: prefix,
2020
start: index,
@@ -47,10 +47,9 @@ var parser = new Parser({
4747
"(url\\s*\\()(\\s*'([^']*)'\\s*)\\)": urlMatch,
4848
"(url\\s*\\()(\\s*([^)]*)\\s*)\\)": urlMatch,
4949

50-
// placeholder
51-
"()\\{\\{([A-Za-z_0-9]+)\\}\\}": placeholderMatch,
52-
"(\\.)\\[([A-Za-z_0-9]+)\\]": placeholderMatch,
53-
"(#)\\[([A-Za-z_0-9]+)\\]": placeholderMatch
50+
// local
51+
"(\\.)local\\[([A-Za-z_0-9]+)\\]": localMatch,
52+
"(#)local\\[([A-Za-z_0-9]+)\\]": localMatch
5453
},
5554
comment: {
5655
"\\*/": "source"
@@ -61,7 +60,7 @@ module.exports = function parseSource(source) {
6160
var context = {
6261
imports: [],
6362
urls: [],
64-
placeholders: []
63+
locals: []
6564
};
6665
return parser.parse("source", source, context);
6766
};

lib/placeholdersLoader.js

-21
This file was deleted.

placeholders.js renamed to locals.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
MIT License http://www.opensource.org/licenses/mit-license.php
33
Author Tobias Koppers @sokra
44
*/
5-
module.exports = require("./lib/placeholdersLoader");
5+
module.exports = require("./lib/localsLoader");

test/urlTest.js

+43-33
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,32 @@
1-
var should = require("should");
2-
var path = require("path");
1+
/*globals describe it */
2+
3+
require("should");
34
var cssLoader = require("../index.js");
45
var vm = require("vm");
56

7+
function assetEvaluated(output, result, modules) {
8+
try {
9+
var fn = vm.runInThisContext("(function(module, exports, require) {" + output + "})", "testcase.js");
10+
var m = { exports: {}, id: 1 };
11+
fn(m, m.exports, function(module) {
12+
if(module === "./lib/css-base.js")
13+
return require("../lib/css-base");
14+
if(module.indexOf("-!loader!") === 0)
15+
module = module.substr(9);
16+
if(modules && modules[module])
17+
return modules[module];
18+
return "{" + module + "}";
19+
});
20+
} catch(e) {
21+
console.error(output);
22+
throw e;
23+
}
24+
delete m.exports.toString;
25+
delete m.exports.i;
26+
m.exports.should.be.eql(result);
27+
28+
}
29+
630
function test(name, input, result, query, modules) {
731
it(name, function() {
832
var output = cssLoader.call({
@@ -38,29 +62,6 @@ function testMinimize(name, input, result, query, modules) {
3862
});
3963
}
4064

41-
function assetEvaluated(output, result, modules) {
42-
try {
43-
var fn = vm.runInThisContext("(function(module, exports, require) {" + output + "})", "testcase.js");
44-
var m = { exports: {}, id: 1 };
45-
fn(m, m.exports, function(module) {
46-
if(module === "./lib/css-base.js")
47-
return require("../lib/css-base");
48-
if(module.indexOf("-!loader!") === 0)
49-
module = module.substr(9);
50-
if(modules && modules[module])
51-
return modules[module];
52-
return "{" + module + "}";
53-
});
54-
} catch(e) {
55-
console.error(output);
56-
throw e;
57-
}
58-
delete m.exports.toString;
59-
delete m.exports.i;
60-
m.exports.should.be.eql(result);
61-
62-
}
63-
6465
describe("url", function() {
6566
test("empty", "", [
6667
[1, "", ""]
@@ -142,18 +143,27 @@ describe("url", function() {
142143
test("media query", "@media (min-width: 500px) { body { background: url(image.png); } }", [
143144
[1, "@media (min-width: 500px) { body { background: url({./image.png}); } }", ""]
144145
]);
145-
test("placeholder", ".[className] { background: red; }\n#[someId] { background: green; }\n" +
146-
".[className] .[subClass] { color: green; }\n#[someId] .[subClass] { color: blue; }", function() { var r = [
147-
[1, ".z857c3103f06630f914262cbc4bce752f { background: red; }\n#z5a79ec8f696debd47ffff36ec4ae1eb8 { background: green; }\n" +
148-
".z857c3103f06630f914262cbc4bce752f .zaf1bf69321affd3c299f08aee1373fba { color: green; }\n#z5a79ec8f696debd47ffff36ec4ae1eb8 .zaf1bf69321affd3c299f08aee1373fba { color: blue; }", ""]
146+
test("locals", ".local[className] { background: red; }\n#local[someId] { background: green; }\n" +
147+
".local[className] .local[subClass] { color: green; }\n#local[someId] .local[subClass] { color: blue; }", function() { var r = [
148+
[1, "._23_aKvs-b8bW2Vg3fwHozO { background: red; }\n#_1j3LM6lKkKzRIt19ImYVnD { background: green; }\n" +
149+
"._23_aKvs-b8bW2Vg3fwHozO ._13LGdX8RMStbBE9w-t0gZ1 { color: green; }\n#_1j3LM6lKkKzRIt19ImYVnD ._13LGdX8RMStbBE9w-t0gZ1 { color: blue; }", ""]
149150
];
150-
r.placeholders = {
151-
className: "z857c3103f06630f914262cbc4bce752f",
152-
someId: "z5a79ec8f696debd47ffff36ec4ae1eb8",
153-
subClass: "zaf1bf69321affd3c299f08aee1373fba"
151+
r.locals = {
152+
className: "_23_aKvs-b8bW2Vg3fwHozO",
153+
someId: "_1j3LM6lKkKzRIt19ImYVnD",
154+
subClass: "_13LGdX8RMStbBE9w-t0gZ1"
154155
};
155156
return r;
156157
}());
158+
test("locals-format", ".local[test] { background: red; }",
159+
function() { var r = [
160+
[1, ".test-3tNsp { background: red; }", ""]
161+
];
162+
r.locals = {
163+
test: "test-3tNsp"
164+
};
165+
return r;
166+
}(), "?localIdentName=[local]-[hash:base64:5]");
157167
testMinimize("minimized simple", ".class { a: b c d; }", [
158168
[1, ".class{a:b c d}", ""]
159169
]);

0 commit comments

Comments
 (0)