Skip to content

Commit

Permalink
feat: add option to omit locals with empty declaration blocks
Browse files Browse the repository at this point in the history
  • Loading branch information
sjoqvist committed Jun 9, 2021
1 parent 740c851 commit 9c23834
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 13 deletions.
43 changes: 30 additions & 13 deletions src/index.js
Expand Up @@ -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,
Expand All @@ -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
),
});
}
Expand All @@ -141,15 +149,15 @@ const plugin = (options = {}) => {
);
}

function traverseNode(node) {
function traverseNode(node, exportSelfReference) {
switch (node.type) {
case "pseudo":
if (node.value === ":local") {
if (node.nodes.length !== 1) {
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;
Expand All @@ -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":
Expand All @@ -197,8 +205,15 @@ const plugin = (options = {}) => {
// Find any :local selectors
root.walkRules((rule) => {
let parsedSelector = selectorParser().astSync(rule);
const containsOwnDeclarations = rule.nodes.some(
(node) =>
node.type !== "comment" && !/^compose(s|-with)$/i.test(node.prop)
);

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);
Expand Down Expand Up @@ -249,7 +264,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 {
Expand All @@ -274,11 +289,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" });
Expand Down
@@ -0,0 +1 @@
referenced class name "otherClassName" in composes not found
@@ -0,0 +1,3 @@
module.exports = {
exportEmptyLocals: false,
};
@@ -0,0 +1,3 @@
:local(.className) {
composes: otherClassName;
}
34 changes: 34 additions & 0 deletions test/test-cases/options-exportEmptyLocals-false/expected.css
@@ -0,0 +1,34 @@
/* leaf node with contents */
._input__layer1A {
color: red;
}

._input__layer2A /* doesn't add anything new */ {
}

._input__layer1B {
/* totally empty, except for this comment */
}

._input__layer2B {
background: blue;
}

._input__layer3 {
}

._input__foo > ._input__bar {
}

._input__baz > ._input__qux {
font-style: italic;
}

:export {
layer1A: _input__layer1A;
layer2A: _input__layer1A;
layer2B: _input__layer2B;
layer3: _input__layer1A _input__layer2B;
baz: _input__baz;
qux: _input__qux;
}
3 changes: 3 additions & 0 deletions test/test-cases/options-exportEmptyLocals-false/options.js
@@ -0,0 +1,3 @@
module.exports = {
exportEmptyLocals: false,
};
29 changes: 29 additions & 0 deletions test/test-cases/options-exportEmptyLocals-false/source.css
@@ -0,0 +1,29 @@
/* leaf node with contents */
:local(.layer1A) {
color: red;
}

:local(.layer2A) /* doesn't add anything new */ {
composes: layer1A;
}

:local(.layer1B) {
/* totally empty, except for this comment */
}

:local(.layer2B) {
background: blue;
composes: layer1B;
}

:local(.layer3) {
composes: layer2A;
composes: layer2B;
}

:local(.foo) /* empty */ > :local(.bar) {
}

:local(.baz) /* non-empty */ > :local(.qux) {
font-style: italic;
}

0 comments on commit 9c23834

Please sign in to comment.