From b6fbcbc362df4192ed809e80ba5c2ffee0ceb3b7 Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Mon, 11 Nov 2024 18:36:45 +0300 Subject: [PATCH 1/2] test: added --- src/index.js | 31 ++++- test/index.test.js | 326 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 355 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index ee556a8..d5162d2 100644 --- a/src/index.js +++ b/src/index.js @@ -46,6 +46,8 @@ function normalizeNodeArray(nodes) { return array; } +const isPureSelectorSymbol = Symbol("is-pure-selector"); + function localizeNode(rule, mode, localAliasMap) { const transform = (node, context) => { if (context.ignoreNextSpacing && !isSpacing(node)) { @@ -253,7 +255,7 @@ function localizeNode(rule, mode, localAliasMap) { } case "nesting": { if (node.value === "&") { - context.hasLocals = true; + context.hasLocals = rule.parent[isPureSelectorSymbol]; } } } @@ -502,6 +504,18 @@ function localizeDeclaration(declaration, context) { } } +const isPureSelector = (context, rule) => { + if (!rule.parent || rule.type === "root") { + return !context.hasPureGlobals; + } + + if (rule.type === "rule" && rule[isPureSelectorSymbol]) { + return rule[isPureSelectorSymbol] || isPureSelector(context, rule.parent); + } + + return !context.hasPureGlobals || isPureSelector(context, rule.parent); +}; + module.exports = (options = {}) => { if ( options && @@ -652,8 +666,17 @@ module.exports = (options = {}) => { context.localAliasMap = localAliasMap; const ignoreComment = pureMode ? getIgnoreComment(rule) : undefined; + const isNotPure = pureMode && !isPureSelector(context, rule); - if (pureMode && context.hasPureGlobals && !ignoreComment) { + if ( + isNotPure && + !ignoreComment && + (rule.nodes.length > 0 + ? !rule.nodes.every( + (item) => item.type === "rule" || item.type === "atrule" + ) + : true) + ) { throw rule.error( 'Selector "' + rule.selector + @@ -664,6 +687,10 @@ module.exports = (options = {}) => { ignoreComment.remove(); } + if (pureMode) { + rule[isPureSelectorSymbol] = !isNotPure; + } + rule.selector = context.selector; // Less-syntax mixins parse as rules with no nodes diff --git a/test/index.test.js b/test/index.test.js index 86af0da..b9cac59 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -920,6 +920,14 @@ const tests = [ options: { mode: "pure" }, expected: ":local(.foo) { &:hover { a_value: some-value; } }", }, + { + name: "consider & statements as pure #2", + input: + ".foo { @media screen and (min-width: 900px) { &:hover { a_value: some-value; } } }", + options: { mode: "pure" }, + expected: + ":local(.foo) { @media screen and (min-width: 900px) { &:hover { a_value: some-value; } } }", + }, { name: "consider global inside local as pure", input: ".foo button { a_value: some-value; }", @@ -1291,6 +1299,324 @@ const tests = [ } }`, }, + { + name: "css nesting #3", + input: ".foo { span { a_value: some-value; } }", + options: { mode: "pure" }, + expected: ":local(.foo) { span { a_value: some-value; } }", + }, + { + name: "css nesting (unfolded) #3", + input: ".foo span { a_value: some-value }", + options: { mode: "pure" }, + expected: ":local(.foo) span { a_value: some-value }", + }, + { + name: "css nesting #4", + input: ".foo { span { a { a_value: some-value; } } }", + options: { mode: "pure" }, + expected: ":local(.foo) { span { a { a_value: some-value; } } }", + }, + { + name: "css nesting (unfolded) #4", + input: ".foo span a { a_value: some-value }", + options: { mode: "pure" }, + expected: ":local(.foo) span a { a_value: some-value }", + }, + { + name: "css nesting #5", + input: "html { .foo { a_value: some-value; } }", + options: { mode: "pure" }, + expected: "html { :local(.foo) { a_value: some-value; } }", + }, + { + name: "css nesting (unfolded) #5", + input: "html .foo { a_value: some-value }", + options: { mode: "pure" }, + expected: "html :local(.foo) { a_value: some-value }", + }, + { + name: "css nesting #6", + input: + "html { @media screen and (min-width: 900px) { .foo { a_value: some-value; } } }", + options: { mode: "pure" }, + expected: + "html { @media screen and (min-width: 900px) { :local(.foo) { a_value: some-value; } } }", + }, + { + name: "css nesting (unfolded) #6", + input: + "@media screen and (min-width: 900px) { html .foo { a_value: some-value } }", + options: { mode: "pure" }, + expected: + "@media screen and (min-width: 900px) { html :local(.foo) { a_value: some-value } }", + }, + { + name: "css nesting #7", + input: + "html { .foo { a_value: some-value; } .bar { a_value: some-value; } }", + options: { mode: "pure" }, + expected: + "html { :local(.foo) { a_value: some-value; } :local(.bar) { a_value: some-value; } }", + }, + { + name: "css nesting (unfolded) #7", + input: "html .foo, html .bar { a_value: some-value }", + options: { mode: "pure" }, + expected: "html :local(.foo), html :local(.bar) { a_value: some-value }", + }, + { + name: "css nesting #8", + input: + ".class { @media screen and (min-width: 900px) { & > span { a_value: some-value; } } }", + options: { mode: "pure" }, + expected: + ":local(.class) { @media screen and (min-width: 900px) { & > span { a_value: some-value; } } }", + }, + { + name: "css nesting (unfolded) #8", + input: + "@media screen and (min-width: 900px) { .class > span { a_value: some-value } }", + options: { mode: "pure" }, + expected: + "@media screen and (min-width: 900px) { :local(.class) > span { a_value: some-value } }", + }, + { + name: "css nesting #9", + input: + "html { @media screen and (min-width: 900px) { & > .class { a_value: some-value; } } }", + options: { mode: "pure" }, + expected: + "html { @media screen and (min-width: 900px) { & > :local(.class) { a_value: some-value; } } }", + }, + { + name: "css nesting (unfolded) #9", + input: + "@media screen and (min-width: 900px) { html > .class { a_value: some-value } }", + options: { mode: "pure" }, + expected: + "@media screen and (min-width: 900px) { html > :local(.class) { a_value: some-value } }", + }, + { + name: "css nesting #10", + input: + ".class { @media screen and (min-width: 900px) { & { a_value: some-value; } } }", + options: { mode: "pure" }, + expected: + ":local(.class) { @media screen and (min-width: 900px) { & { a_value: some-value; } } }", + }, + { + name: "css nesting (unfolded) #10", + input: + "@media screen and (min-width: 900px) { .class { a_value: some-value } }", + options: { mode: "pure" }, + expected: + "@media screen and (min-width: 900px) { :local(.class) { a_value: some-value } }", + }, + { + name: "css nesting #11", + input: "html { .foo { span { a_value: some-value; } } }", + options: { mode: "pure" }, + expected: "html { :local(.foo) { span { a_value: some-value; } } }", + }, + { + name: "css nesting (unfolded) #11", + input: "html .foo span { a_value: some-value }", + options: { mode: "pure" }, + expected: "html :local(.foo) span { a_value: some-value }", + }, + { + name: "css nesting #12", + input: "html { button { .foo { div { span { a_value: some-value; } } } } }", + options: { mode: "pure" }, + expected: + "html { button { :local(.foo) { div { span { a_value: some-value; } } } } }", + }, + { + name: "css nesting #13", + input: ".foo { button { div { div { span { a_value: some-value; } } } } }", + options: { mode: "pure" }, + expected: + ":local(.foo) { button { div { div { span { a_value: some-value; } } } } }", + }, + { + name: "css nesting #14", + input: "html { button { div { div { .foo { a_value: some-value; } } } } }", + options: { mode: "pure" }, + expected: + "html { button { div { div { :local(.foo) { a_value: some-value; } } } } }", + }, + { + name: "css nesting #15", + input: + "html { button { @media screen and (min-width: 900px) { .foo { div { span { a_value: some-value; } } } } } }", + options: { mode: "pure" }, + expected: + "html { button { @media screen and (min-width: 900px) { :local(.foo) { div { span { a_value: some-value; } } } } } }", + }, + { + name: "css nesting #16", + input: "html { .foo { a_value: some-value; } }", + options: { mode: "pure" }, + expected: "html { :local(.foo) { a_value: some-value; } }", + }, + { + name: "css nesting #17", + input: ".foo { div { a_value: some-value; } }", + options: { mode: "pure" }, + expected: ":local(.foo) { div { a_value: some-value; } }", + }, + { + name: "css nesting #18", + input: + "@media screen and (min-width: 900px) { html { .foo { a_value: some-value; } } }", + options: { mode: "pure" }, + expected: + "@media screen and (min-width: 900px) { html { :local(.foo) { a_value: some-value; } } }", + }, + { + name: "css nesting #19", + input: + "html { @media screen and (min-width: 900px) { .foo { a_value: some-value; } } }", + options: { mode: "pure" }, + expected: + "html { @media screen and (min-width: 900px) { :local(.foo) { a_value: some-value; } } }", + }, + { + name: "css nesting #20", + input: + "html { .foo { @media screen and (min-width: 900px) { a_value: some-value; } } }", + options: { mode: "pure" }, + expected: + "html { :local(.foo) { @media screen and (min-width: 900px) { a_value: some-value; } } }", + }, + { + name: "css nesting #21", + input: + "@media screen and (min-width: 900px) { .foo { div { a_value: some-value; } } }", + options: { mode: "pure" }, + expected: + "@media screen and (min-width: 900px) { :local(.foo) { div { a_value: some-value; } } }", + }, + { + name: "css nesting #22", + input: + ".foo { @media screen and (min-width: 900px) { div { a_value: some-value; } } }", + options: { mode: "pure" }, + expected: + ":local(.foo) { @media screen and (min-width: 900px) { div { a_value: some-value; } } }", + }, + { + name: "css nesting #23", + input: + ".foo { div { @media screen and (min-width: 900px) { a_value: some-value; } } }", + options: { mode: "pure" }, + expected: + ":local(.foo) { div { @media screen and (min-width: 900px) { a_value: some-value; } } }", + }, + { + name: "css nesting - throw on mixed parents", + input: ".foo, html { span { a_value: some-value; } }", + options: { mode: "pure" }, + error: /is not pure/, + }, + { + name: "css nesting - throw on &", + input: "html { & > span { a_value: some-value; } }", + options: { mode: "pure" }, + error: /is not pure/, + }, + { + name: "css nesting - throw on & #2", + input: "html { button { & > span { a_value: some-value; } } }", + options: { mode: "pure" }, + error: /is not pure/, + }, + { + name: "css nesting - throw on & #3", + input: + "html { @media screen and (min-width: 900px) { & > span { a_value: some-value; } } }", + options: { mode: "pure" }, + error: /is not pure/, + }, + { + name: "css nesting - throw on & #4", + input: "html { button { div { div { & { a_value: some-value; } } } } }", + options: { mode: "pure" }, + error: /is not pure/, + }, + { + name: "css nesting - throw", + input: "html { button { div { div { div { a_value: some-value; } } } } }", + options: { mode: "pure" }, + error: /is not pure/, + }, + { + name: "css nesting - throw #2", + input: "html { button { div { div { div { } } } } }", + options: { mode: "pure" }, + error: /is not pure/, + }, + { + name: "css nesting - throw #3", + input: + "html { button { @media screen and (min-width: 900px) { div { div { div { } } } } } }", + options: { mode: "pure" }, + error: /is not pure/, + }, + { + name: "css nesting - throw #4", + input: + "@media screen and (min-width: 900px) { html { button { div { div { div { } } } } } }", + options: { mode: "pure" }, + error: /is not pure/, + }, + { + name: "css nesting - throw #5", + input: + "html { button { div { div { div { @media screen and (min-width: 900px) { color: red } } } } } }", + options: { mode: "pure" }, + error: /is not pure/, + }, + { + name: "css nesting - throw on global styles with a local selector", + input: `html { a_value: some-value; .foo { a_value: some-value; } }`, + options: { mode: "pure" }, + error: /is not pure/, + }, + { + name: "css nesting - throw on global styles with a local selector #2", + input: `html { .foo { a_value: some-value; } a_value: some-value; }`, + options: { mode: "pure" }, + error: /is not pure/, + }, + { + name: "css nesting - throw on global styles with a local selector #3", + input: ` +html { + .foo { a_value: some-value; } + button { + color: red; + .bar { a_value: some-value; } + } +}`, + options: { mode: "pure" }, + error: /is not pure/, + }, + { + name: "css nesting - throw on global styles with a local selector #4", + input: ` +html { + @media screen and (min-width: 900px) { + button { + color: red; + .bar { a_value: some-value; } + } + } +}`, + options: { mode: "pure" }, + error: /is not pure/, + }, /* Bug in postcss-selector-parser { From 036a0693a6ccf4ea072ffb41e488a313cc57bd42 Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Mon, 11 Nov 2024 23:49:11 +0300 Subject: [PATCH 2/2] fix: css nesting and pure mode --- src/index.js | 20 ++++++++++++++------ test/index.test.js | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/index.js b/src/index.js index d5162d2..0b93b0c 100644 --- a/src/index.js +++ b/src/index.js @@ -516,6 +516,18 @@ const isPureSelector = (context, rule) => { return !context.hasPureGlobals || isPureSelector(context, rule.parent); }; +const isNodeWithoutDeclarations = (rule) => { + if (rule.nodes.length > 0) { + return !rule.nodes.every( + (item) => + item.type === "rule" || + (item.type === "atrule" && !isNodeWithoutDeclarations(item)) + ); + } + + return true; +}; + module.exports = (options = {}) => { if ( options && @@ -670,12 +682,8 @@ module.exports = (options = {}) => { if ( isNotPure && - !ignoreComment && - (rule.nodes.length > 0 - ? !rule.nodes.every( - (item) => item.type === "rule" || item.type === "atrule" - ) - : true) + isNodeWithoutDeclarations(rule) && + !ignoreComment ) { throw rule.error( 'Selector "' + diff --git a/test/index.test.js b/test/index.test.js index b9cac59..d33854f 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1574,7 +1574,42 @@ const tests = [ { name: "css nesting - throw #5", input: - "html { button { div { div { div { @media screen and (min-width: 900px) { color: red } } } } } }", + "html { div { @media screen and (min-width: 900px) { color: red } } }", + options: { mode: "pure" }, + error: /is not pure/, + }, + { + name: "css nesting - throw #6", + input: + "html { div { @media screen and (min-width: 900px) { @media screen and (min-width: 900px) { color: red } } } }", + options: { mode: "pure" }, + error: /is not pure/, + }, + { + name: "css nesting - throw #7", + input: + "html { div { @media screen and (min-width: 900px) { .a { } @media screen and (min-width: 900px) { color: red } } } }", + options: { mode: "pure" }, + error: /is not pure/, + }, + { + name: "css nesting - throw #7", + input: + "html { div { @media screen and (min-width: 900px) { .a { a_value: some-value; } @media screen and (min-width: 900px) { color: red } } } }", + options: { mode: "pure" }, + error: /is not pure/, + }, + { + name: "css nesting - throw #8", + input: ` +@media screen and (min-width: 900px) { + .a { a_value: some-value; } + @media screen and (min-width: 900px) { + div { + color: red + } + } +}`, options: { mode: "pure" }, error: /is not pure/, },