Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to eliminate unused classes from DOM #32

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
43 changes: 30 additions & 13 deletions src/index.js
Original file line number Diff line number Diff line change
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, node.spaces);
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
referenced class name "otherClassName" in composes not found
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
exportEmptyLocals: false,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:local(.className) {
composes: otherClassName;
}
34 changes: 34 additions & 0 deletions test/test-cases/options-exportEmptyLocals-false/expected.css
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
exportEmptyLocals: false,
};
29 changes: 29 additions & 0 deletions test/test-cases/options-exportEmptyLocals-false/source.css
Original file line number Diff line number Diff line change
@@ -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 */
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add couple tests:

  • comment(s) inside block
  • comment(s) inside selector before/after
  • invalid composes (i.e. not reference)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Do you separate test cases or should everything go in these existing files?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add them here

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've now force-pushed changes. I had to add the invalid composes in a separate directory, but the other tests were added to the original files. Not entirely sure if I got all your suggestions for tests correctly, but please have a look and let me know.

Some comments are removed, but that's what the current code does as well and I've made the tests reflect that. Since I added the true or "default" tests in a separate commit before the other changes, you can check out that commit and run yarn test to confirm it if you wish. You can also see that only the :export blocks differ:

diff test/test-cases/options-exportEmptyLocals-{true,false}/expected.css

Also, I should mention that I had overlooked the fact that comments can go inside the declaration blocks, so it was good that you pointed that out and I've updated the code accordingly. Let me know if there are any other special cases that I'm unaware of.


: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;
}
37 changes: 37 additions & 0 deletions test/test-cases/options-exportEmptyLocals-true/expected.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/* 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__layer2A _input__layer1A;
layer1B: _input__layer1B;
layer2B: _input__layer2B _input__layer1B;
layer3: _input__layer3 _input__layer2A _input__layer1A _input__layer2B _input__layer1B;
foo: _input__foo;
bar: _input__bar;
baz: _input__baz;
qux: _input__qux;
}
29 changes: 29 additions & 0 deletions test/test-cases/options-exportEmptyLocals-true/source.css
Original file line number Diff line number Diff line change
@@ -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;
}