diff --git a/src/index.js b/src/index.js index 9a2f2b5..9adcbae 100644 --- a/src/index.js +++ b/src/index.js @@ -85,13 +85,19 @@ const plugin = (options = {}) => { const generateExportEntry = (options && options.generateExportEntry) || plugin.generateExportEntry; const exportGlobals = options && options.exportGlobals; + const exportEmptyLocals = + !options || + (typeof options.exportEmptyLocals === "undefined" || + options.exportEmptyLocals === null + ? true + : options.exportEmptyLocals); return { postcssPlugin: "postcss-modules-scope", Once(root, { rule }) { const exports = Object.create(null); - function exportScopedName(name, rawName) { + function exportScopedName(name, rawName, includeSelfReference) { const scopedName = generateScopedName( rawName ? rawName : name, root.source.input.from, @@ -107,30 +113,32 @@ const plugin = (options = {}) => { exports[key] = exports[key] || []; - if (exports[key].indexOf(value) < 0) { + if (includeSelfReference && exports[key].indexOf(value) < 0) { exports[key].push(value); } return scopedName; } - function localizeNode(node) { + function localizeNode(node, exportSelfReference) { switch (node.type) { case "selector": - node.nodes = node.map(localizeNode); + node.nodes = node.map((n) => localizeNode(n, exportSelfReference)); return node; case "class": return selectorParser.className({ value: exportScopedName( node.value, - node.raws && node.raws.value ? node.raws.value : null + node.raws && node.raws.value ? node.raws.value : null, + exportSelfReference ), }); case "id": { return selectorParser.id({ value: exportScopedName( node.value, - node.raws && node.raws.value ? node.raws.value : null + node.raws && node.raws.value ? node.raws.value : null, + exportSelfReference ), }); } @@ -141,7 +149,7 @@ const plugin = (options = {}) => { ); } - function traverseNode(node) { + function traverseNode(node, exportSelfReference) { switch (node.type) { case "pseudo": if (node.value === ":local") { @@ -149,7 +157,7 @@ const plugin = (options = {}) => { throw new Error('Unexpected comma (",") in :local block'); } - const selector = localizeNode(node.first); + const selector = localizeNode(node.first, exportSelfReference); // move the spaces that were around the psuedo selector to the first // non-container node selector.first.spaces = node.spaces; @@ -172,7 +180,7 @@ const plugin = (options = {}) => { /* falls through */ case "root": case "selector": { - node.each(traverseNode); + node.each((n) => traverseNode(n, exportSelfReference)); break; } case "id": @@ -197,8 +205,14 @@ const plugin = (options = {}) => { // Find any :local selectors root.walkRules((rule) => { let parsedSelector = selectorParser().astSync(rule); + const containsOwnDeclarations = rule.nodes.some( + (node) => node.prop !== "composes" && node.prop !== "compose-with" + ); - rule.selector = traverseNode(parsedSelector.clone()).toString(); + rule.selector = traverseNode( + parsedSelector.clone(), + exportEmptyLocals || containsOwnDeclarations + ).toString(); rule.walkDecls(/composes|compose-with/i, (decl) => { const localNames = getSingleLocalNamesForComposes(parsedSelector); @@ -249,7 +263,7 @@ const plugin = (options = {}) => { const input = localMatch.input; const matchPattern = localMatch[0]; const matchVal = localMatch[1]; - const newVal = exportScopedName(matchVal); + const newVal = exportScopedName(matchVal, undefined, true); result = input.replace(matchPattern, newVal); } else { @@ -274,11 +288,13 @@ const plugin = (options = {}) => { return; } - atRule.params = exportScopedName(localMatch[1]); + atRule.params = exportScopedName(localMatch[1], undefined, true); }); // If we found any :locals, insert an :export rule - const exportedNames = Object.keys(exports); + const exportedNames = Object.keys(exports).filter( + (exportedName) => exports[exportedName].length !== 0 + ); if (exportedNames.length > 0) { const exportRule = rule({ selector: ":export" }); diff --git a/test/test-cases/options-exportEmptyLocals-false/expected.css b/test/test-cases/options-exportEmptyLocals-false/expected.css new file mode 100644 index 0000000..ce29cf2 --- /dev/null +++ b/test/test-cases/options-exportEmptyLocals-false/expected.css @@ -0,0 +1,23 @@ +._input__layer1A { + color: red; +} + +._input__layer2A { +} + +._input__layer1B { +} + +._input__layer2B { + background: blue; +} + +._input__layer3 { +} + +:export { + layer1A: _input__layer1A; + layer2A: _input__layer1A; + layer2B: _input__layer2B; + layer3: _input__layer1A _input__layer2B; +} diff --git a/test/test-cases/options-exportEmptyLocals-false/options.js b/test/test-cases/options-exportEmptyLocals-false/options.js new file mode 100644 index 0000000..f64076a --- /dev/null +++ b/test/test-cases/options-exportEmptyLocals-false/options.js @@ -0,0 +1,3 @@ +module.exports = { + exportEmptyLocals: false, +}; diff --git a/test/test-cases/options-exportEmptyLocals-false/source.css b/test/test-cases/options-exportEmptyLocals-false/source.css new file mode 100644 index 0000000..beee0e5 --- /dev/null +++ b/test/test-cases/options-exportEmptyLocals-false/source.css @@ -0,0 +1,20 @@ +:local(.layer1A) { + color: red; +} + +:local(.layer2A) { + composes: layer1A; +} + +:local(.layer1B) { +} + +:local(.layer2B) { + background: blue; + composes: layer1B; +} + +:local(.layer3) { + composes: layer2A; + composes: layer2B; +}