Skip to content

Commit 91954d8

Browse files
committed
feat: add option to omit locals with empty declaration blocks
fixes css-modules/css-modules#127, fixes css-modules/css-modules#269
1 parent 677df7f commit 91954d8

File tree

7 files changed

+103
-13
lines changed

7 files changed

+103
-13
lines changed

src/index.js

+30-13
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,19 @@ const plugin = (options = {}) => {
8585
const generateExportEntry =
8686
(options && options.generateExportEntry) || plugin.generateExportEntry;
8787
const exportGlobals = options && options.exportGlobals;
88+
const exportEmptyLocals =
89+
!options ||
90+
(typeof options.exportEmptyLocals === "undefined" ||
91+
options.exportEmptyLocals === null
92+
? true
93+
: options.exportEmptyLocals);
8894

8995
return {
9096
postcssPlugin: "postcss-modules-scope",
9197
Once(root, { rule }) {
9298
const exports = Object.create(null);
9399

94-
function exportScopedName(name, rawName) {
100+
function exportScopedName(name, rawName, includeSelfReference) {
95101
const scopedName = generateScopedName(
96102
rawName ? rawName : name,
97103
root.source.input.from,
@@ -107,30 +113,32 @@ const plugin = (options = {}) => {
107113

108114
exports[key] = exports[key] || [];
109115

110-
if (exports[key].indexOf(value) < 0) {
116+
if (includeSelfReference && exports[key].indexOf(value) < 0) {
111117
exports[key].push(value);
112118
}
113119

114120
return scopedName;
115121
}
116122

117-
function localizeNode(node) {
123+
function localizeNode(node, exportSelfReference) {
118124
switch (node.type) {
119125
case "selector":
120-
node.nodes = node.map(localizeNode);
126+
node.nodes = node.map((n) => localizeNode(n, exportSelfReference));
121127
return node;
122128
case "class":
123129
return selectorParser.className({
124130
value: exportScopedName(
125131
node.value,
126-
node.raws && node.raws.value ? node.raws.value : null
132+
node.raws && node.raws.value ? node.raws.value : null,
133+
exportSelfReference
127134
),
128135
});
129136
case "id": {
130137
return selectorParser.id({
131138
value: exportScopedName(
132139
node.value,
133-
node.raws && node.raws.value ? node.raws.value : null
140+
node.raws && node.raws.value ? node.raws.value : null,
141+
exportSelfReference
134142
),
135143
});
136144
}
@@ -141,15 +149,15 @@ const plugin = (options = {}) => {
141149
);
142150
}
143151

144-
function traverseNode(node) {
152+
function traverseNode(node, exportSelfReference) {
145153
switch (node.type) {
146154
case "pseudo":
147155
if (node.value === ":local") {
148156
if (node.nodes.length !== 1) {
149157
throw new Error('Unexpected comma (",") in :local block');
150158
}
151159

152-
const selector = localizeNode(node.first);
160+
const selector = localizeNode(node.first, exportSelfReference);
153161
// move the spaces that were around the psuedo selector to the first
154162
// non-container node
155163
selector.first.spaces = node.spaces;
@@ -172,7 +180,7 @@ const plugin = (options = {}) => {
172180
/* falls through */
173181
case "root":
174182
case "selector": {
175-
node.each(traverseNode);
183+
node.each((n) => traverseNode(n, exportSelfReference));
176184
break;
177185
}
178186
case "id":
@@ -197,8 +205,15 @@ const plugin = (options = {}) => {
197205
// Find any :local selectors
198206
root.walkRules((rule) => {
199207
let parsedSelector = selectorParser().astSync(rule);
208+
const containsOwnDeclarations = rule.nodes.some(
209+
(node) =>
210+
node.type !== "comment" && !/^compose(s|-with)$/i.test(node.prop)
211+
);
200212

201-
rule.selector = traverseNode(parsedSelector.clone()).toString();
213+
rule.selector = traverseNode(
214+
parsedSelector.clone(),
215+
exportEmptyLocals || containsOwnDeclarations
216+
).toString();
202217

203218
rule.walkDecls(/composes|compose-with/i, (decl) => {
204219
const localNames = getSingleLocalNamesForComposes(parsedSelector);
@@ -249,7 +264,7 @@ const plugin = (options = {}) => {
249264
const input = localMatch.input;
250265
const matchPattern = localMatch[0];
251266
const matchVal = localMatch[1];
252-
const newVal = exportScopedName(matchVal);
267+
const newVal = exportScopedName(matchVal, undefined, true);
253268

254269
result = input.replace(matchPattern, newVal);
255270
} else {
@@ -274,11 +289,13 @@ const plugin = (options = {}) => {
274289
return;
275290
}
276291

277-
atRule.params = exportScopedName(localMatch[1]);
292+
atRule.params = exportScopedName(localMatch[1], undefined, true);
278293
});
279294

280295
// If we found any :locals, insert an :export rule
281-
const exportedNames = Object.keys(exports);
296+
const exportedNames = Object.keys(exports).filter(
297+
(exportedName) => exports[exportedName].length !== 0
298+
);
282299

283300
if (exportedNames.length > 0) {
284301
const exportRule = rule({ selector: ":export" });
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
referenced class name "otherClassName" in composes not found
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
exportEmptyLocals: false,
3+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:local(.className) {
2+
composes: otherClassName;
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/* leaf node with contents */
2+
._input__layer1A {
3+
color: red;
4+
}
5+
6+
._input__layer2A /* doesn't add anything new */ {
7+
}
8+
9+
._input__layer1B {
10+
/* totally empty, except for this comment */
11+
}
12+
13+
._input__layer2B {
14+
background: blue;
15+
}
16+
17+
._input__layer3 {
18+
}
19+
20+
._input__foo > ._input__bar {
21+
}
22+
23+
._input__baz > ._input__qux {
24+
font-style: italic;
25+
}
26+
27+
:export {
28+
layer1A: _input__layer1A;
29+
layer2A: _input__layer1A;
30+
layer2B: _input__layer2B;
31+
layer3: _input__layer1A _input__layer2B;
32+
baz: _input__baz;
33+
qux: _input__qux;
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
exportEmptyLocals: false,
3+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/* leaf node with contents */
2+
:local(.layer1A) {
3+
color: red;
4+
}
5+
6+
:local(.layer2A) /* doesn't add anything new */ {
7+
composes: layer1A;
8+
}
9+
10+
:local(.layer1B) {
11+
/* totally empty, except for this comment */
12+
}
13+
14+
:local(.layer2B) {
15+
background: blue;
16+
composes: layer1B;
17+
}
18+
19+
:local(.layer3) {
20+
composes: layer2A;
21+
composes: layer2B;
22+
}
23+
24+
:local(.foo) /* empty */ > :local(.bar) {
25+
}
26+
27+
:local(.baz) /* non-empty */ > :local(.qux) {
28+
font-style: italic;
29+
}

0 commit comments

Comments
 (0)