diff --git a/README.md b/README.md index 5501230..842f9b0 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,21 @@ Declarations (mode `local`, by default): ``` +## 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 diff --git a/src/index.js b/src/index.js index 5bad19a..ee556a8 100644 --- a/src/index.js +++ b/src/index.js @@ -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 = []; @@ -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) { @@ -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) => { @@ -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 + @@ -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; diff --git a/test/index.test.js b/test/index.test.js index 874634b..86af0da 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -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: `