Skip to content

Commit e3c94d4

Browse files
committed
Merge pull request #10 from css-modules/pure-mode
added pure mode
2 parents a16c07d + d021ed2 commit e3c94d4

File tree

2 files changed

+94
-11
lines changed

2 files changed

+94
-11
lines changed

index.js

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,23 @@ function localizeNode(node, context) {
3030
switch(node.type) {
3131
case "selectors":
3232
var resultingGlobal;
33+
context.locals = true;
3334
newNodes = node.nodes.map(function(n) {
34-
var nContext = { global: context.global, lastWasSpacing: true };
35+
var nContext = {
36+
global: context.global,
37+
lastWasSpacing: true,
38+
locals: false
39+
};
3540
n = localizeNode(n, nContext);
3641
if(typeof resultingGlobal === "undefined") {
3742
resultingGlobal = nContext.global;
3843
} else if(resultingGlobal !== nContext.global) {
3944
throw new Error("Inconsistent rule global/local result in rule '" +
4045
Tokenizer.stringify(node) + "' (multiple selectors must result in the same mode for the rule)");
4146
}
47+
if(!nContext.locals) {
48+
context.locals = false;
49+
}
4250
return n;
4351
});
4452
context.global = resultingGlobal;
@@ -82,30 +90,47 @@ function localizeNode(node, context) {
8290
if(context.inside) {
8391
throw new Error("A :" + node.name + "(...) is not allowed inside of a :" + context.inside + "(...)");
8492
}
85-
subContext = { global: (node.name === "global"), inside: node.name };
93+
subContext = {
94+
global: (node.name === "global"),
95+
inside: node.name,
96+
locals: false
97+
};
8698
node = node.nodes.map(function(n) {
8799
return localizeNode(n, subContext);
88100
});
89101
// don't leak spacing
90102
node[0].before = undefined;
91103
node[node.length - 1].after = undefined;
104+
if(subContext.locals) {
105+
context.locals = true;
106+
}
92107
} else {
93-
subContext = { global: context.global, inside: context.inside, lastWasSpacing: true };
108+
subContext = {
109+
global: context.global,
110+
inside: context.inside,
111+
lastWasSpacing: true,
112+
locals: false
113+
};
94114
newNodes = node.nodes.map(function(n) {
95115
return localizeNode(n, subContext);
96116
});
97117
node = Object.create(node);
98118
node.nodes = normalizeNodeArray(newNodes);
119+
if(subContext.locals) {
120+
context.locals = true;
121+
}
99122
}
100123
break;
101124

125+
case "id":
102126
case "class":
103127
if(!context.global) {
104128
node = {
105129
type: "nested-pseudo-class",
106130
name: "local",
107131
nodes: [node]
108132
};
133+
context.locals = true;
109134
}
110135
break;
111136
}
@@ -125,33 +150,44 @@ function localizeDecl(decl) {
125150

126151
module.exports = postcss.plugin('postcss-modules-local-by-default', function (options) {
127152
if(options && options.mode) {
128-
if(options.mode !== "global" && options.mode !== "local") {
129-
throw new Error("options.mode must be either 'global' or 'local' (default 'local')");
153+
if(options.mode !== "global" && options.mode !== "local" && options.mode !== "pure") {
154+
throw new Error("options.mode must be either 'global', 'local' or 'pure' (default 'local')");
130155
}
131156
}
157+
var pureMode = options && options.mode === "pure";
158+
var globalMode = options && options.mode === "global";
132159
return function(css) {
133-
var initialGlobal = options && options.mode === "global";
134160
css.eachAtRule(function(atrule) {
135161
if(/keyframes$/.test(atrule.name)) {
136162
var globalMatch = /^\s*:global\s*\((.+)\)\s*$/.exec(atrule.params);
137163
var localMatch = /^\s*:local\s*\((.+)\)\s*$/.exec(atrule.params);
138164
if(globalMatch) {
165+
if(pureMode) {
166+
throw new Error("@keyframes :global(...) is not allowed in pure mode");
167+
}
139168
atrule.params = globalMatch[1];
140169
} else if(localMatch) {
141170
atrule.params = localMatch[0];
142-
} else if(!initialGlobal) {
171+
} else if(!globalMode) {
143172
atrule.params = ":local(" + atrule.params + ")";
144173
}
145174
}
146175
});
147176
css.eachRule(function(rule) {
148177
var selector = Tokenizer.parse(rule.selector);
149-
var context = { global: initialGlobal };
150-
selector = localizeNode(selector, context);
178+
var context = {
179+
global: globalMode,
180+
locals: false
181+
};
182+
var newSelector = localizeNode(selector, context);
183+
if(pureMode && !context.locals) {
184+
throw new Error("Selector '" + Tokenizer.stringify(selector) + "' is not pure " +
185+
"(pure selectors must contain at least one local class or id)");
186+
}
151187
if(!context.global) {
152188
rule.nodes.forEach(localizeDecl);
153189
}
154-
rule.selector = Tokenizer.stringify(selector);
190+
rule.selector = Tokenizer.stringify(newSelector);
155191
});
156192
};
157193
});

test.js

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ var tests = [
99
input: '.foobar {}',
1010
expected: ':local(.foobar) {}'
1111
},
12+
{
13+
should: 'scope ids',
14+
input: '#foobar {}',
15+
expected: ':local(#foobar) {}'
16+
},
1217
{
1318
should: 'scope multiple selectors',
1419
input: '.foo, .baz {}',
@@ -235,7 +240,7 @@ var tests = [
235240
should: 'throw on invalid mode',
236241
input: '',
237242
options: { mode: "???" },
238-
error: /'global' or 'local'/
243+
error: /'global', 'local' or 'pure'/
239244
},
240245
{
241246
should: 'throw on inconsistent selector result',
@@ -271,6 +276,48 @@ var tests = [
271276
should: 'throw on incorrect spacing with broad :local',
272277
input: '.foo:local .bar {}',
273278
error: /Missing whitespace before :local/
279+
},
280+
{
281+
should: 'throw on not pure selector (global class)',
282+
input: ':global(.foo) {}',
283+
options: { mode: "pure" },
284+
error: /':global\(\.foo\)' is not pure/
285+
},
286+
{
287+
should: 'compile in pure mode',
288+
input: ':global(.foo).bar, [type="radio"] ~ .label, :not(.foo), #bar {}',
289+
options: { mode: "pure" },
290+
expected: '.foo:local(.bar), [type="radio"] ~ :local(.label), :not(:local(.foo)), :local(#bar) {}'
291+
},
292+
{
293+
should: 'throw on not pure selector (with multiple 1)',
294+
input: '.foo, :global(.bar) {}',
295+
options: { mode: "pure" },
296+
error: /'.foo, :global\(\.bar\)' is not pure/
297+
},
298+
{
299+
should: 'throw on not pure selector (with multiple 2)',
300+
input: ':global(.bar), .foo {}',
301+
options: { mode: "pure" },
302+
error: /':global\(\.bar\), .foo' is not pure/
303+
},
304+
{
305+
should: 'throw on not pure selector (element)',
306+
input: 'input {}',
307+
options: { mode: "pure" },
308+
error: /'input' is not pure/
309+
},
310+
{
311+
should: 'throw on not pure selector (attribute)',
312+
input: '[type="radio"] {}',
313+
options: { mode: "pure" },
314+
error: /'\[type="radio"\]' is not pure/
315+
},
316+
{
317+
should: 'throw on not pure keyframes',
318+
input: '@keyframes :global(foo) {}',
319+
options: { mode: "pure" },
320+
error: /@keyframes :global\(\.\.\.\) is not allowed in pure mode/
274321
}
275322
];
276323

0 commit comments

Comments
 (0)