Skip to content

Commit 9d07eeb

Browse files
authored
feat: add pure ignore comment for CSS Modules (#80)
1 parent fde62d7 commit 9d07eeb

File tree

3 files changed

+288
-5
lines changed

3 files changed

+288
-5
lines changed

README.md

+15
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,21 @@ Declarations (mode `local`, by default):
5454
```
5555
<!-- prettier-ignore-end -->
5656

57+
## Pure Mode
58+
59+
In pure mode, all selectors must contain at least one local class or id
60+
selector
61+
62+
To ignore this rule for a specific selector, add the following comment in front
63+
of the selector:
64+
65+
```css
66+
/* cssmodules-pure-ignore */
67+
:global(#modal-backdrop) {
68+
...;
69+
}
70+
```
71+
5772
## Building
5873

5974
```bash

src/index.js

+45-5
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,29 @@ const selectorParser = require("postcss-selector-parser");
44
const valueParser = require("postcss-value-parser");
55
const { extractICSS } = require("icss-utils");
66

7+
const IGNORE_MARKER = "cssmodules-pure-ignore";
8+
79
const isSpacing = (node) => node.type === "combinator" && node.value === " ";
810

11+
function getIgnoreComment(node) {
12+
if (!node.parent) {
13+
return;
14+
}
15+
16+
const indexInParent = node.parent.index(node);
17+
18+
for (let i = indexInParent - 1; i >= 0; i--) {
19+
const prevNode = node.parent.nodes[i];
20+
if (prevNode.type === "comment") {
21+
if (prevNode.text.trimStart().startsWith(IGNORE_MARKER)) {
22+
return prevNode;
23+
}
24+
} else {
25+
break;
26+
}
27+
}
28+
}
29+
930
function normalizeNodeArray(nodes) {
1031
const array = [];
1132

@@ -525,10 +546,17 @@ module.exports = (options = {}) => {
525546

526547
if (globalMatch) {
527548
if (pureMode) {
528-
throw atRule.error(
529-
"@keyframes :global(...) is not allowed in pure mode"
530-
);
549+
const ignoreComment = getIgnoreComment(atRule);
550+
551+
if (!ignoreComment) {
552+
throw atRule.error(
553+
"@keyframes :global(...) is not allowed in pure mode"
554+
);
555+
} else {
556+
ignoreComment.remove();
557+
}
531558
}
559+
532560
atRule.params = globalMatch[1];
533561
globalKeyframes = true;
534562
} else if (localMatch) {
@@ -551,6 +579,14 @@ module.exports = (options = {}) => {
551579
});
552580
} else if (/scope$/i.test(atRule.name)) {
553581
if (atRule.params) {
582+
const ignoreComment = pureMode
583+
? getIgnoreComment(atRule)
584+
: undefined;
585+
586+
if (ignoreComment) {
587+
ignoreComment.remove();
588+
}
589+
554590
atRule.params = atRule.params
555591
.split("to")
556592
.map((item) => {
@@ -564,7 +600,7 @@ module.exports = (options = {}) => {
564600
context.options = options;
565601
context.localAliasMap = localAliasMap;
566602

567-
if (pureMode && context.hasPureGlobals) {
603+
if (pureMode && context.hasPureGlobals && !ignoreComment) {
568604
throw atRule.error(
569605
'Selector in at-rule"' +
570606
selector +
@@ -615,13 +651,17 @@ module.exports = (options = {}) => {
615651
context.options = options;
616652
context.localAliasMap = localAliasMap;
617653

618-
if (pureMode && context.hasPureGlobals) {
654+
const ignoreComment = pureMode ? getIgnoreComment(rule) : undefined;
655+
656+
if (pureMode && context.hasPureGlobals && !ignoreComment) {
619657
throw rule.error(
620658
'Selector "' +
621659
rule.selector +
622660
'" is not pure ' +
623661
"(pure selectors must contain at least one local class or id)"
624662
);
663+
} else if (ignoreComment) {
664+
ignoreComment.remove();
625665
}
626666

627667
rule.selector = context.selector;

test/index.test.js

+228
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,234 @@ const tests = [
944944
options: { mode: "pure" },
945945
error: /is not pure/,
946946
},
947+
{
948+
name: "should suppress errors for global selectors after ignore comment",
949+
options: { mode: "pure" },
950+
input: `/* cssmodules-pure-ignore */
951+
:global(.foo) { color: blue; }`,
952+
expected: `.foo { color: blue; }`,
953+
},
954+
{
955+
name: "should suppress errors for global selectors after ignore comment #2",
956+
options: { mode: "pure" },
957+
input: `/* cssmodules-pure-ignore */
958+
/* another comment */
959+
:global(.foo) { color: blue; }`,
960+
expected: `/* another comment */
961+
.foo { color: blue; }`,
962+
},
963+
{
964+
name: "should suppress errors for global selectors after ignore comment #3",
965+
options: { mode: "pure" },
966+
input: `/* another comment */
967+
/* cssmodules-pure-ignore */
968+
:global(.foo) { color: blue; }`,
969+
expected: `/* another comment */
970+
.foo { color: blue; }`,
971+
},
972+
{
973+
name: "should suppress errors for global selectors after ignore comment #4",
974+
options: { mode: "pure" },
975+
input: `/* cssmodules-pure-ignore */ /* another comment */
976+
:global(.foo) { color: blue; }`,
977+
expected: `/* another comment */
978+
.foo { color: blue; }`,
979+
},
980+
{
981+
name: "should suppress errors for global selectors after ignore comment #5",
982+
options: { mode: "pure" },
983+
input: `/* another comment */ /* cssmodules-pure-ignore */
984+
:global(.foo) { color: blue; }`,
985+
expected: `/* another comment */
986+
.foo { color: blue; }`,
987+
},
988+
{
989+
name: "should suppress errors for global selectors after ignore comment #6",
990+
options: { mode: "pure" },
991+
input: `.foo { /* cssmodules-pure-ignore */ :global(.bar) { color: blue }; }`,
992+
expected: `:local(.foo) { .bar { color: blue }; }`,
993+
},
994+
{
995+
name: "should suppress errors for global selectors after ignore comment #7",
996+
options: { mode: "pure" },
997+
input: `/* cssmodules-pure-ignore */ :global(.foo) { /* cssmodules-pure-ignore */ :global(.bar) { color: blue } }`,
998+
expected: `.foo { .bar { color: blue } }`,
999+
},
1000+
{
1001+
name: "should suppress errors for global selectors after ignore comment #8",
1002+
options: { mode: "pure" },
1003+
input: `/* cssmodules-pure-ignore */ :global(.foo) { color: blue; }`,
1004+
expected: `.foo { color: blue; }`,
1005+
},
1006+
{
1007+
name: "should suppress errors for global selectors after ignore comment #9",
1008+
options: { mode: "pure" },
1009+
input: `/*
1010+
cssmodules-pure-ignore
1011+
*/ :global(.foo) { color: blue; }`,
1012+
expected: `.foo { color: blue; }`,
1013+
},
1014+
{
1015+
name: "should allow additional text in ignore comment",
1016+
options: { mode: "pure" },
1017+
input: `/* cssmodules-pure-ignore - needed for third party integration */
1018+
:global(#foo) { color: blue; }`,
1019+
expected: `#foo { color: blue; }`,
1020+
},
1021+
{
1022+
name: "should not affect rules after the ignored block",
1023+
options: { mode: "pure" },
1024+
input: `/* cssmodules-pure-ignore */
1025+
:global(.foo) { color: blue; }
1026+
:global(.bar) { color: red; }`,
1027+
error: /is not pure/,
1028+
},
1029+
{
1030+
name: "should work with nested global selectors in ignored block",
1031+
options: { mode: "pure" },
1032+
input: `/* cssmodules-pure-ignore */
1033+
:global(.foo) {
1034+
:global(.bar) { color: blue; }
1035+
}`,
1036+
error: /is not pure/,
1037+
},
1038+
{
1039+
name: "should work with ignored nested global selectors in ignored block",
1040+
options: { mode: "pure" },
1041+
input: `/* cssmodules-pure-ignore */
1042+
:global(.foo) {
1043+
/* cssmodules-pure-ignore */
1044+
:global(.bar) { color: blue; }
1045+
}`,
1046+
expected: `.foo {
1047+
.bar { color: blue; }
1048+
}`,
1049+
},
1050+
{
1051+
name: "should work with view transitions in ignored block",
1052+
options: { mode: "pure" },
1053+
input: `/* cssmodules-pure-ignore */
1054+
::view-transition-group(modal) {
1055+
animation-duration: 300ms;
1056+
}`,
1057+
expected: `::view-transition-group(modal) {
1058+
animation-duration: 300ms;
1059+
}`,
1060+
},
1061+
{
1062+
name: "should work with keyframes in ignored block",
1063+
options: { mode: "pure" },
1064+
input: `/* cssmodules-pure-ignore */
1065+
@keyframes :global(fadeOut) {
1066+
from { opacity: 1; }
1067+
to { opacity: 0; }
1068+
}`,
1069+
expected: `@keyframes fadeOut {
1070+
from { opacity: 1; }
1071+
to { opacity: 0; }
1072+
}`,
1073+
},
1074+
{
1075+
name: "should work with scope in ignored block",
1076+
options: { mode: "pure" },
1077+
input: `
1078+
/* cssmodules-pure-ignore */
1079+
@scope (:global(.foo)) to (:global(.bar)) {
1080+
.article-footer {
1081+
border: 5px solid black;
1082+
}
1083+
}
1084+
`,
1085+
expected: `
1086+
@scope (.foo) to (.bar) {
1087+
:local(.article-footer) {
1088+
border: 5px solid black;
1089+
}
1090+
}
1091+
`,
1092+
},
1093+
{
1094+
name: "should work with scope in ignored block #2",
1095+
options: { mode: "pure" },
1096+
input: `
1097+
/* cssmodules-pure-ignore */
1098+
@scope (:global(.foo))
1099+
to (:global(.bar)) {
1100+
.article-footer {
1101+
border: 5px solid black;
1102+
}
1103+
}
1104+
`,
1105+
expected: `
1106+
@scope (.foo) to (.bar) {
1107+
:local(.article-footer) {
1108+
border: 5px solid black;
1109+
}
1110+
}
1111+
`,
1112+
},
1113+
{
1114+
name: "should work in media queries",
1115+
options: { mode: "pure" },
1116+
input: `@media (min-width: 768px) {
1117+
/* cssmodules-pure-ignore */
1118+
:global(.foo) { color: blue; }
1119+
}`,
1120+
expected: `@media (min-width: 768px) {
1121+
.foo { color: blue; }
1122+
}`,
1123+
},
1124+
{
1125+
name: "should handle multiple ignore comments",
1126+
options: { mode: "pure" },
1127+
input: `/* cssmodules-pure-ignore */
1128+
:global(.foo) { color: blue; }
1129+
.local { color: green; }
1130+
/* cssmodules-pure-ignore */
1131+
:global(.bar) { color: red; }`,
1132+
expected: `.foo { color: blue; }
1133+
:local(.local) { color: green; }
1134+
.bar { color: red; }`,
1135+
},
1136+
{
1137+
name: "should work with complex selectors in ignored block",
1138+
options: { mode: "pure" },
1139+
input: `/* cssmodules-pure-ignore */
1140+
:global(.foo):hover > :global(.bar) + :global(.baz) {
1141+
color: blue;
1142+
}`,
1143+
expected: `.foo:hover > .bar + .baz {
1144+
color: blue;
1145+
}`,
1146+
},
1147+
{
1148+
name: "should work with multiple selectors in ignored block",
1149+
options: { mode: "pure" },
1150+
input: `/* cssmodules-pure-ignore */
1151+
:global(.foo),
1152+
:global(.bar),
1153+
:global(.baz) {
1154+
color: blue;
1155+
}`,
1156+
expected: `.foo,
1157+
.bar,
1158+
.baz {
1159+
color: blue;
1160+
}`,
1161+
},
1162+
{
1163+
name: "should work with pseudo-elements in ignored block",
1164+
options: { mode: "pure" },
1165+
input: `/* cssmodules-pure-ignore */
1166+
:global(.foo)::before,
1167+
:global(.foo)::after {
1168+
content: '';
1169+
}`,
1170+
expected: `.foo::before,
1171+
.foo::after {
1172+
content: '';
1173+
}`,
1174+
},
9471175
{
9481176
name: "css nesting",
9491177
input: `

0 commit comments

Comments
 (0)