Skip to content

Commit dec5b56

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 af015f1 commit dec5b56

File tree

4 files changed

+75
-13
lines changed

4 files changed

+75
-13
lines changed

src/index.js

+29-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,14 @@ 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) => node.prop !== "composes" && node.prop !== "compose-with"
210+
);
200211

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

203217
rule.walkDecls(/composes|compose-with/i, (decl) => {
204218
const localNames = getSingleLocalNamesForComposes(parsedSelector);
@@ -249,7 +263,7 @@ const plugin = (options = {}) => {
249263
const input = localMatch.input;
250264
const matchPattern = localMatch[0];
251265
const matchVal = localMatch[1];
252-
const newVal = exportScopedName(matchVal);
266+
const newVal = exportScopedName(matchVal, undefined, true);
253267

254268
result = input.replace(matchPattern, newVal);
255269
} else {
@@ -274,11 +288,13 @@ const plugin = (options = {}) => {
274288
return;
275289
}
276290

277-
atRule.params = exportScopedName(localMatch[1]);
291+
atRule.params = exportScopedName(localMatch[1], undefined, true);
278292
});
279293

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

283299
if (exportedNames.length > 0) {
284300
const exportRule = rule({ selector: ":export" });
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
._input__layer1A {
2+
color: red;
3+
}
4+
5+
._input__layer2A {
6+
}
7+
8+
._input__layer1B {
9+
}
10+
11+
._input__layer2B {
12+
background: blue;
13+
}
14+
15+
._input__layer3 {
16+
}
17+
18+
:export {
19+
layer1A: _input__layer1A;
20+
layer2A: _input__layer1A;
21+
layer2B: _input__layer2B;
22+
layer3: _input__layer1A _input__layer2B;
23+
}
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,20 @@
1+
:local(.layer1A) {
2+
color: red;
3+
}
4+
5+
:local(.layer2A) {
6+
composes: layer1A;
7+
}
8+
9+
:local(.layer1B) {
10+
}
11+
12+
:local(.layer2B) {
13+
background: blue;
14+
composes: layer1B;
15+
}
16+
17+
:local(.layer3) {
18+
composes: layer2A;
19+
composes: layer2B;
20+
}

0 commit comments

Comments
 (0)