From ba7e8efe032c1d76138b93ef8211054ed2067001 Mon Sep 17 00:00:00 2001 From: Jan Nicklas Date: Wed, 11 Dec 2024 15:44:20 +0100 Subject: [PATCH] feat: added support a `cssmodules-pure-no-check` comment --- README.md | 16 +++++- src/index.js | 36 ++++++++++---- test/index.test.js | 120 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f12e634..f60ac07 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ Declarations (mode `local`, by default): 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 +To ignore this rule for a specific selector, add the a `/* cssmodules-pure-ignore */` comment in front of the selector: ```css @@ -69,6 +69,20 @@ of the selector: } ``` +or by adding a `/* cssmodules-pure-no-check */` comment at the top of a file to disable this check for the whole file: + +```css +/* cssmodules-pure-no-check */ + +:global(#modal-backdrop) { + ...; +} + +:global(#my-id) { + ...; +} +``` + ## Building ```bash diff --git a/src/index.js b/src/index.js index 0b93b0c..ba0c4f7 100644 --- a/src/index.js +++ b/src/index.js @@ -4,21 +4,32 @@ const selectorParser = require("postcss-selector-parser"); const valueParser = require("postcss-value-parser"); const { extractICSS } = require("icss-utils"); -const IGNORE_MARKER = "cssmodules-pure-ignore"; +const IGNORE_FILE_MARKER = "cssmodules-pure-no-check"; +const IGNORE_NEXT_LINE_MARKER = "cssmodules-pure-ignore"; const isSpacing = (node) => node.type === "combinator" && node.value === " "; +const isPureCheckDisabled = (root) => { + for (const node of root.nodes) { + if (node.type !== "comment") { + return false; + } + if (node.text.trim().startsWith(IGNORE_FILE_MARKER)) { + return true; + } + } + return false; +}; + 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)) { + if (prevNode.text.trimStart().startsWith(IGNORE_NEXT_LINE_MARKER)) { return prevNode; } } else { @@ -552,6 +563,7 @@ module.exports = (options = {}) => { return { Once(root) { const { icssImports } = extractICSS(root, false); + const enforcePureMode = pureMode && !isPureCheckDisabled(root); Object.keys(icssImports).forEach((key) => { Object.keys(icssImports[key]).forEach((prop) => { @@ -571,9 +583,8 @@ module.exports = (options = {}) => { let globalKeyframes = globalMode; if (globalMatch) { - if (pureMode) { + if (enforcePureMode) { const ignoreComment = getIgnoreComment(atRule); - if (!ignoreComment) { throw atRule.error( "@keyframes :global(...) is not allowed in pure mode" @@ -582,7 +593,6 @@ module.exports = (options = {}) => { ignoreComment.remove(); } } - atRule.params = globalMatch[1]; globalKeyframes = true; } else if (localMatch) { @@ -626,7 +636,11 @@ module.exports = (options = {}) => { context.options = options; context.localAliasMap = localAliasMap; - if (pureMode && context.hasPureGlobals && !ignoreComment) { + if ( + enforcePureMode && + context.hasPureGlobals && + !ignoreComment + ) { throw atRule.error( 'Selector in at-rule"' + selector + @@ -677,8 +691,10 @@ module.exports = (options = {}) => { context.options = options; context.localAliasMap = localAliasMap; - const ignoreComment = pureMode ? getIgnoreComment(rule) : undefined; - const isNotPure = pureMode && !isPureSelector(context, rule); + const ignoreComment = enforcePureMode + ? getIgnoreComment(rule) + : undefined; + const isNotPure = enforcePureMode && !isPureSelector(context, rule); if ( isNotPure && diff --git a/test/index.test.js b/test/index.test.js index d33854f..ed4bb0f 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1180,6 +1180,126 @@ const tests = [ content: ''; }`, }, + { + name: "should disable pure mode checks for entire file with no-check comment", + options: { mode: "pure" }, + input: `/* cssmodules-pure-no-check */ + :global(.foo) { border: 1px solid #e2e8f0 } + :global(.bar) { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1) } + :global(.baz) { background: #4299e1 }`, + expected: `/* cssmodules-pure-no-check */ + .foo { border: 1px solid #e2e8f0 } + .bar { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1) } + .baz { background: #4299e1 }`, + }, + { + name: "should disable pure mode checks for nested selectors", + options: { mode: "pure" }, + input: `/* cssmodules-pure-no-check */ + :global(.foo) { + &:hover { border-color: #cbd5e0 } + & :global(.bar) { color: blue } + }`, + expected: `/* cssmodules-pure-no-check */ + .foo { + &:hover { border-color: #cbd5e0 } + & .bar { color: blue } + }`, + }, + { + name: "should ignore no-check comment if not at root level", + options: { mode: "pure" }, + input: `:global(.bar) { color: blue } + /* cssmodules-pure-no-check */`, + error: /is not pure/, + }, + { + name: "should ignore no-check comment if not at root level #2", + options: { mode: "pure" }, + input: `/* Some file description */ + .class { color: red; } + /* cssmodules-pure-no-check */ + :global(.foo) { color: blue }`, + error: /is not pure/, + }, + { + name: "should allow other comments before no-check comment", + options: { mode: "pure" }, + input: `/* Some file description */ + /* cssmodules-pure-no-check */ + :global(.foo) { color: blue }`, + expected: `/* Some file description */ + /* cssmodules-pure-no-check */ + .foo { color: blue }`, + }, + { + name: "should disable pure mode checks for deep nested selectors", + options: { mode: "pure" }, + input: `/* cssmodules-pure-no-check */ + :global(.foo) { max-width: 600px } + :global(.bar) { background: #fafafa } + :global(.baz) { + :global(.foobar) { + &::-webkit-scrollbar { width: 8px } + } + }`, + expected: `/* cssmodules-pure-no-check */ + .foo { max-width: 600px } + .bar { background: #fafafa } + .baz { + .foobar { + &::-webkit-scrollbar { width: 8px } + } + }`, + }, + { + name: "should work with keyframes when no-check is enabled", + options: { mode: "pure" }, + input: `/* cssmodules-pure-no-check */ + @keyframes :global(fadeIn) { + from { opacity: 0 } + to { opacity: 1 } + } + :global(.animate) { animation: global(fadeIn) 0.3s }`, + expected: `/* cssmodules-pure-no-check */ + @keyframes fadeIn { + from { opacity: 0 } + to { opacity: 1 } + } + .animate { animation: fadeIn 0.3s }`, + }, + { + name: "should allow multiline no-check comment", + options: { mode: "pure" }, + input: `/* + cssmodules-pure-no-check + */ + :global(.foo) { color: blue }`, + expected: `/* + cssmodules-pure-no-check + */ + .foo { color: blue }`, + }, + { + name: "should allow additional text in no-check comment", + options: { mode: "pure" }, + input: `/* cssmodules-pure-no-check - needed for styling third-party components */ + :global(.foo) { color: blue }`, + expected: `/* cssmodules-pure-no-check - needed for styling third-party components */ + .foo { color: blue }`, + }, + { + name: "should work with media queries when no-check is enabled", + options: { mode: "pure" }, + input: `/* cssmodules-pure-no-check */ + @media (max-width: 768px) { + :global(.foo) { position: fixed } + }`, + expected: `/* cssmodules-pure-no-check */ + @media (max-width: 768px) { + .foo { position: fixed } + }`, + }, { name: "css nesting", input: `