Skip to content

add pure ignore comment for CSS Modules #80

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

Merged
merged 3 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
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 avoid extra loops when mode is not pure - better perf


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
Loading