Skip to content

Commit

Permalink
feat: add pure ignore comment for CSS Modules (#80)
Browse files Browse the repository at this point in the history
  • Loading branch information
jantimon authored Nov 5, 2024
1 parent fde62d7 commit 9d07eeb
Show file tree
Hide file tree
Showing 3 changed files with 288 additions and 5 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ Declarations (mode `local`, by default):
```
<!-- prettier-ignore-end -->

## Pure Mode

In pure mode, all selectors must contain at least one local class or id
selector

To ignore this rule for a specific selector, add the following comment in front
of the selector:

```css
/* cssmodules-pure-ignore */
:global(#modal-backdrop) {
...;
}
```

## Building

```bash
Expand Down
50 changes: 45 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,29 @@ const selectorParser = require("postcss-selector-parser");
const valueParser = require("postcss-value-parser");
const { extractICSS } = require("icss-utils");

const IGNORE_MARKER = "cssmodules-pure-ignore";

const isSpacing = (node) => node.type === "combinator" && node.value === " ";

function getIgnoreComment(node) {
if (!node.parent) {
return;
}

const indexInParent = node.parent.index(node);

for (let i = indexInParent - 1; i >= 0; i--) {
const prevNode = node.parent.nodes[i];
if (prevNode.type === "comment") {
if (prevNode.text.trimStart().startsWith(IGNORE_MARKER)) {
return prevNode;
}
} else {
break;
}
}
}

function normalizeNodeArray(nodes) {
const array = [];

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

if (globalMatch) {
if (pureMode) {
throw atRule.error(
"@keyframes :global(...) is not allowed in pure mode"
);
const ignoreComment = getIgnoreComment(atRule);

if (!ignoreComment) {
throw atRule.error(
"@keyframes :global(...) is not allowed in pure mode"
);
} else {
ignoreComment.remove();
}
}

atRule.params = globalMatch[1];
globalKeyframes = true;
} else if (localMatch) {
Expand All @@ -551,6 +579,14 @@ module.exports = (options = {}) => {
});
} else if (/scope$/i.test(atRule.name)) {
if (atRule.params) {
const ignoreComment = pureMode
? getIgnoreComment(atRule)
: undefined;

if (ignoreComment) {
ignoreComment.remove();
}

atRule.params = atRule.params
.split("to")
.map((item) => {
Expand All @@ -564,7 +600,7 @@ module.exports = (options = {}) => {
context.options = options;
context.localAliasMap = localAliasMap;

if (pureMode && context.hasPureGlobals) {
if (pureMode && context.hasPureGlobals && !ignoreComment) {
throw atRule.error(
'Selector in at-rule"' +
selector +
Expand Down Expand Up @@ -615,13 +651,17 @@ module.exports = (options = {}) => {
context.options = options;
context.localAliasMap = localAliasMap;

if (pureMode && context.hasPureGlobals) {
const ignoreComment = pureMode ? getIgnoreComment(rule) : undefined;

if (pureMode && context.hasPureGlobals && !ignoreComment) {
throw rule.error(
'Selector "' +
rule.selector +
'" is not pure ' +
"(pure selectors must contain at least one local class or id)"
);
} else if (ignoreComment) {
ignoreComment.remove();
}

rule.selector = context.selector;
Expand Down
228 changes: 228 additions & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,234 @@ const tests = [
options: { mode: "pure" },
error: /is not pure/,
},
{
name: "should suppress errors for global selectors after ignore comment",
options: { mode: "pure" },
input: `/* cssmodules-pure-ignore */
:global(.foo) { color: blue; }`,
expected: `.foo { color: blue; }`,
},
{
name: "should suppress errors for global selectors after ignore comment #2",
options: { mode: "pure" },
input: `/* cssmodules-pure-ignore */
/* another comment */
:global(.foo) { color: blue; }`,
expected: `/* another comment */
.foo { color: blue; }`,
},
{
name: "should suppress errors for global selectors after ignore comment #3",
options: { mode: "pure" },
input: `/* another comment */
/* cssmodules-pure-ignore */
:global(.foo) { color: blue; }`,
expected: `/* another comment */
.foo { color: blue; }`,
},
{
name: "should suppress errors for global selectors after ignore comment #4",
options: { mode: "pure" },
input: `/* cssmodules-pure-ignore */ /* another comment */
:global(.foo) { color: blue; }`,
expected: `/* another comment */
.foo { color: blue; }`,
},
{
name: "should suppress errors for global selectors after ignore comment #5",
options: { mode: "pure" },
input: `/* another comment */ /* cssmodules-pure-ignore */
:global(.foo) { color: blue; }`,
expected: `/* another comment */
.foo { color: blue; }`,
},
{
name: "should suppress errors for global selectors after ignore comment #6",
options: { mode: "pure" },
input: `.foo { /* cssmodules-pure-ignore */ :global(.bar) { color: blue }; }`,
expected: `:local(.foo) { .bar { color: blue }; }`,
},
{
name: "should suppress errors for global selectors after ignore comment #7",
options: { mode: "pure" },
input: `/* cssmodules-pure-ignore */ :global(.foo) { /* cssmodules-pure-ignore */ :global(.bar) { color: blue } }`,
expected: `.foo { .bar { color: blue } }`,
},
{
name: "should suppress errors for global selectors after ignore comment #8",
options: { mode: "pure" },
input: `/* cssmodules-pure-ignore */ :global(.foo) { color: blue; }`,
expected: `.foo { color: blue; }`,
},
{
name: "should suppress errors for global selectors after ignore comment #9",
options: { mode: "pure" },
input: `/*
cssmodules-pure-ignore
*/ :global(.foo) { color: blue; }`,
expected: `.foo { color: blue; }`,
},
{
name: "should allow additional text in ignore comment",
options: { mode: "pure" },
input: `/* cssmodules-pure-ignore - needed for third party integration */
:global(#foo) { color: blue; }`,
expected: `#foo { color: blue; }`,
},
{
name: "should not affect rules after the ignored block",
options: { mode: "pure" },
input: `/* cssmodules-pure-ignore */
:global(.foo) { color: blue; }
:global(.bar) { color: red; }`,
error: /is not pure/,
},
{
name: "should work with nested global selectors in ignored block",
options: { mode: "pure" },
input: `/* cssmodules-pure-ignore */
:global(.foo) {
:global(.bar) { color: blue; }
}`,
error: /is not pure/,
},
{
name: "should work with ignored nested global selectors in ignored block",
options: { mode: "pure" },
input: `/* cssmodules-pure-ignore */
:global(.foo) {
/* cssmodules-pure-ignore */
:global(.bar) { color: blue; }
}`,
expected: `.foo {
.bar { color: blue; }
}`,
},
{
name: "should work with view transitions in ignored block",
options: { mode: "pure" },
input: `/* cssmodules-pure-ignore */
::view-transition-group(modal) {
animation-duration: 300ms;
}`,
expected: `::view-transition-group(modal) {
animation-duration: 300ms;
}`,
},
{
name: "should work with keyframes in ignored block",
options: { mode: "pure" },
input: `/* cssmodules-pure-ignore */
@keyframes :global(fadeOut) {
from { opacity: 1; }
to { opacity: 0; }
}`,
expected: `@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}`,
},
{
name: "should work with scope in ignored block",
options: { mode: "pure" },
input: `
/* cssmodules-pure-ignore */
@scope (:global(.foo)) to (:global(.bar)) {
.article-footer {
border: 5px solid black;
}
}
`,
expected: `
@scope (.foo) to (.bar) {
:local(.article-footer) {
border: 5px solid black;
}
}
`,
},
{
name: "should work with scope in ignored block #2",
options: { mode: "pure" },
input: `
/* cssmodules-pure-ignore */
@scope (:global(.foo))
to (:global(.bar)) {
.article-footer {
border: 5px solid black;
}
}
`,
expected: `
@scope (.foo) to (.bar) {
:local(.article-footer) {
border: 5px solid black;
}
}
`,
},
{
name: "should work in media queries",
options: { mode: "pure" },
input: `@media (min-width: 768px) {
/* cssmodules-pure-ignore */
:global(.foo) { color: blue; }
}`,
expected: `@media (min-width: 768px) {
.foo { color: blue; }
}`,
},
{
name: "should handle multiple ignore comments",
options: { mode: "pure" },
input: `/* cssmodules-pure-ignore */
:global(.foo) { color: blue; }
.local { color: green; }
/* cssmodules-pure-ignore */
:global(.bar) { color: red; }`,
expected: `.foo { color: blue; }
:local(.local) { color: green; }
.bar { color: red; }`,
},
{
name: "should work with complex selectors in ignored block",
options: { mode: "pure" },
input: `/* cssmodules-pure-ignore */
:global(.foo):hover > :global(.bar) + :global(.baz) {
color: blue;
}`,
expected: `.foo:hover > .bar + .baz {
color: blue;
}`,
},
{
name: "should work with multiple selectors in ignored block",
options: { mode: "pure" },
input: `/* cssmodules-pure-ignore */
:global(.foo),
:global(.bar),
:global(.baz) {
color: blue;
}`,
expected: `.foo,
.bar,
.baz {
color: blue;
}`,
},
{
name: "should work with pseudo-elements in ignored block",
options: { mode: "pure" },
input: `/* cssmodules-pure-ignore */
:global(.foo)::before,
:global(.foo)::after {
content: '';
}`,
expected: `.foo::before,
.foo::after {
content: '';
}`,
},
{
name: "css nesting",
input: `
Expand Down

0 comments on commit 9d07eeb

Please sign in to comment.