From 90c16fe06ec4cf5cfd1c50e794e4af9e224e5d3d Mon Sep 17 00:00:00 2001 From: Jan Nicklas Date: Thu, 31 Oct 2024 21:17:42 +0100 Subject: [PATCH 1/3] add pure ignore comment for css modules --- README.md | 15 +++++ src/index.js | 28 ++++++++- test/index.test.js | 141 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 181 insertions(+), 3 deletions(-) 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..899d6ae 100644 --- a/src/index.js +++ b/src/index.js @@ -4,8 +4,26 @@ 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 hasIgnoreComment(node) { + if (!node.parent) { + return false; + } + const indexInParent = node.parent.index(node); + for (let i = indexInParent - 1; i >= 0; i--) { + const prevNode = node.parent.nodes[i]; + if (prevNode.type === "comment") { + return prevNode.text.trimStart().startsWith(IGNORE_MARKER); + } else { + break; + } + } + return false; +} + function normalizeNodeArray(nodes) { const array = []; @@ -524,7 +542,7 @@ module.exports = (options = {}) => { let globalKeyframes = globalMode; if (globalMatch) { - if (pureMode) { + if (pureMode && !hasIgnoreComment(atRule)) { throw atRule.error( "@keyframes :global(...) is not allowed in pure mode" ); @@ -564,7 +582,11 @@ module.exports = (options = {}) => { context.options = options; context.localAliasMap = localAliasMap; - if (pureMode && context.hasPureGlobals) { + if ( + pureMode && + context.hasPureGlobals && + !hasIgnoreComment(atRule) + ) { throw atRule.error( 'Selector in at-rule"' + selector + @@ -615,7 +637,7 @@ module.exports = (options = {}) => { context.options = options; context.localAliasMap = localAliasMap; - if (pureMode && context.hasPureGlobals) { + if (pureMode && context.hasPureGlobals && !hasIgnoreComment(rule)) { throw rule.error( 'Selector "' + rule.selector + diff --git a/test/index.test.js b/test/index.test.js index 874634b..2e9bd68 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -944,6 +944,147 @@ 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: `/* cssmodules-pure-ignore */ + .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: `/* cssmodules-pure-ignore - needed for third party integration */ + #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: `/* cssmodules-pure-ignore */ + .foo { + /* cssmodules-pure-ignore */ + .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: `/* cssmodules-pure-ignore */ + ::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: `/* cssmodules-pure-ignore */ + @keyframes fadeOut { + from { opacity: 1; } + to { opacity: 0; } + }`, + }, + { + 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) { + /* cssmodules-pure-ignore */ + .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: `/* cssmodules-pure-ignore */ + .foo { color: blue; } + :local(.local) { color: green; } + /* cssmodules-pure-ignore */ + .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: `/* cssmodules-pure-ignore */ + .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: `/* cssmodules-pure-ignore */ + .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: `/* cssmodules-pure-ignore */ + .foo::before, + .foo::after { + content: ''; + }`, + }, { name: "css nesting", input: ` From 73e1c624313a711394bf35c227841d7eb44d95f9 Mon Sep 17 00:00:00 2001 From: Jan Nicklas Date: Fri, 1 Nov 2024 13:02:01 +0100 Subject: [PATCH 2/3] remove comments --- src/index.js | 32 ++++++++++++++++++-------------- test/index.test.js | 37 +++++++++++++++++-------------------- 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/src/index.js b/src/index.js index 899d6ae..4c0d48b 100644 --- a/src/index.js +++ b/src/index.js @@ -8,20 +8,18 @@ const IGNORE_MARKER = "cssmodules-pure-ignore"; const isSpacing = (node) => node.type === "combinator" && node.value === " "; -function hasIgnoreComment(node) { - if (!node.parent) { - return false; - } - const indexInParent = node.parent.index(node); +function getIgnoreComment(node) { + const indexInParent = node.parent ? node.parent.index(node) : -1; for (let i = indexInParent - 1; i >= 0; i--) { const prevNode = node.parent.nodes[i]; if (prevNode.type === "comment") { - return prevNode.text.trimStart().startsWith(IGNORE_MARKER); + if (prevNode.text.trimStart().startsWith(IGNORE_MARKER)) { + return prevNode; + } } else { break; } } - return false; } function normalizeNodeArray(nodes) { @@ -531,6 +529,7 @@ module.exports = (options = {}) => { }); root.walkAtRules((atRule) => { + const ignoreComment = getIgnoreComment(atRule); if (/keyframes$/i.test(atRule.name)) { const globalMatch = /^\s*:global\s*\((.+)\)\s*$/.exec( atRule.params @@ -542,7 +541,7 @@ module.exports = (options = {}) => { let globalKeyframes = globalMode; if (globalMatch) { - if (pureMode && !hasIgnoreComment(atRule)) { + if (pureMode && !ignoreComment) { throw atRule.error( "@keyframes :global(...) is not allowed in pure mode" ); @@ -582,11 +581,7 @@ module.exports = (options = {}) => { context.options = options; context.localAliasMap = localAliasMap; - if ( - pureMode && - context.hasPureGlobals && - !hasIgnoreComment(atRule) - ) { + if (pureMode && context.hasPureGlobals && ignoreComment) { throw atRule.error( 'Selector in at-rule"' + selector + @@ -620,9 +615,14 @@ module.exports = (options = {}) => { } }); } + + if (ignoreComment) { + ignoreComment.remove(); + } }); root.walkRules((rule) => { + const ignoreComment = getIgnoreComment(rule); if ( rule.parent && rule.parent.type === "atrule" && @@ -637,7 +637,7 @@ module.exports = (options = {}) => { context.options = options; context.localAliasMap = localAliasMap; - if (pureMode && context.hasPureGlobals && !hasIgnoreComment(rule)) { + if (pureMode && context.hasPureGlobals && !ignoreComment) { throw rule.error( 'Selector "' + rule.selector + @@ -654,6 +654,10 @@ module.exports = (options = {}) => { localizeDeclaration(declaration, context) ); } + + if (ignoreComment) { + ignoreComment.remove(); + } }); }, }; diff --git a/test/index.test.js b/test/index.test.js index 2e9bd68..02daad4 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -949,7 +949,15 @@ const tests = [ options: { mode: "pure" }, input: `/* cssmodules-pure-ignore */ :global(.foo) { color: blue; }`, - expected: `/* cssmodules-pure-ignore */ + 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; }`, }, { @@ -957,8 +965,7 @@ const tests = [ options: { mode: "pure" }, input: `/* cssmodules-pure-ignore - needed for third party integration */ :global(#foo) { color: blue; }`, - expected: `/* cssmodules-pure-ignore - needed for third party integration */ - #foo { color: blue; }`, + expected: `#foo { color: blue; }`, }, { name: "should not affect rules after the ignored block", @@ -985,9 +992,7 @@ const tests = [ /* cssmodules-pure-ignore */ :global(.bar) { color: blue; } }`, - expected: `/* cssmodules-pure-ignore */ - .foo { - /* cssmodules-pure-ignore */ + expected: `.foo { .bar { color: blue; } }`, }, @@ -998,8 +1003,7 @@ const tests = [ ::view-transition-group(modal) { animation-duration: 300ms; }`, - expected: `/* cssmodules-pure-ignore */ - ::view-transition-group(modal) { + expected: `::view-transition-group(modal) { animation-duration: 300ms; }`, }, @@ -1011,8 +1015,7 @@ const tests = [ from { opacity: 1; } to { opacity: 0; } }`, - expected: `/* cssmodules-pure-ignore */ - @keyframes fadeOut { + expected: `@keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }`, @@ -1025,7 +1028,6 @@ const tests = [ :global(.foo) { color: blue; } }`, expected: `@media (min-width: 768px) { - /* cssmodules-pure-ignore */ .foo { color: blue; } }`, }, @@ -1037,10 +1039,8 @@ const tests = [ .local { color: green; } /* cssmodules-pure-ignore */ :global(.bar) { color: red; }`, - expected: `/* cssmodules-pure-ignore */ - .foo { color: blue; } + expected: `.foo { color: blue; } :local(.local) { color: green; } - /* cssmodules-pure-ignore */ .bar { color: red; }`, }, { @@ -1050,8 +1050,7 @@ const tests = [ :global(.foo):hover > :global(.bar) + :global(.baz) { color: blue; }`, - expected: `/* cssmodules-pure-ignore */ - .foo:hover > .bar + .baz { + expected: `.foo:hover > .bar + .baz { color: blue; }`, }, @@ -1064,8 +1063,7 @@ const tests = [ :global(.baz) { color: blue; }`, - expected: `/* cssmodules-pure-ignore */ - .foo, + expected: `.foo, .bar, .baz { color: blue; @@ -1079,8 +1077,7 @@ const tests = [ :global(.foo)::after { content: ''; }`, - expected: `/* cssmodules-pure-ignore */ - .foo::before, + expected: `.foo::before, .foo::after { content: ''; }`, From 971abcd3fb71345ef0d0e27bce1a7eb251e4ca2f Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Tue, 5 Nov 2024 19:20:02 +0300 Subject: [PATCH 3/3] refactor: logic --- src/index.js | 46 +++++++++++++++--------- test/index.test.js | 90 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 16 deletions(-) diff --git a/src/index.js b/src/index.js index 4c0d48b..ee556a8 100644 --- a/src/index.js +++ b/src/index.js @@ -9,7 +9,12 @@ const IGNORE_MARKER = "cssmodules-pure-ignore"; const isSpacing = (node) => node.type === "combinator" && node.value === " "; function getIgnoreComment(node) { - const indexInParent = node.parent ? node.parent.index(node) : -1; + 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") { @@ -529,7 +534,6 @@ module.exports = (options = {}) => { }); root.walkAtRules((atRule) => { - const ignoreComment = getIgnoreComment(atRule); if (/keyframes$/i.test(atRule.name)) { const globalMatch = /^\s*:global\s*\((.+)\)\s*$/.exec( atRule.params @@ -541,11 +545,18 @@ module.exports = (options = {}) => { let globalKeyframes = globalMode; if (globalMatch) { - if (pureMode && !ignoreComment) { - throw atRule.error( - "@keyframes :global(...) is not allowed in pure mode" - ); + if (pureMode) { + 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) { @@ -568,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) => { @@ -581,7 +600,7 @@ module.exports = (options = {}) => { context.options = options; context.localAliasMap = localAliasMap; - if (pureMode && context.hasPureGlobals && ignoreComment) { + if (pureMode && context.hasPureGlobals && !ignoreComment) { throw atRule.error( 'Selector in at-rule"' + selector + @@ -615,14 +634,9 @@ module.exports = (options = {}) => { } }); } - - if (ignoreComment) { - ignoreComment.remove(); - } }); root.walkRules((rule) => { - const ignoreComment = getIgnoreComment(rule); if ( rule.parent && rule.parent.type === "atrule" && @@ -637,6 +651,8 @@ module.exports = (options = {}) => { context.options = options; context.localAliasMap = localAliasMap; + const ignoreComment = pureMode ? getIgnoreComment(rule) : undefined; + if (pureMode && context.hasPureGlobals && !ignoreComment) { throw rule.error( 'Selector "' + @@ -644,6 +660,8 @@ module.exports = (options = {}) => { '" is not pure ' + "(pure selectors must contain at least one local class or id)" ); + } else if (ignoreComment) { + ignoreComment.remove(); } rule.selector = context.selector; @@ -654,10 +672,6 @@ module.exports = (options = {}) => { localizeDeclaration(declaration, context) ); } - - if (ignoreComment) { - ignoreComment.remove(); - } }); }, }; diff --git a/test/index.test.js b/test/index.test.js index 02daad4..86af0da 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -960,6 +960,57 @@ const tests = [ 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" }, @@ -1020,6 +1071,45 @@ const tests = [ 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" },