From 38119276608ef14821797cfc0242b3c7dead69af Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Mon, 11 Nov 2024 23:53:32 +0300 Subject: [PATCH] fix: css nesting and pure mode --- src/index.js | 39 ++++- test/index.test.js | 361 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 398 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index ee556a8..0b93b0c 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,30 @@ 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); +}; + +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 && @@ -652,8 +678,13 @@ 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 && + isNodeWithoutDeclarations(rule) && + !ignoreComment + ) { throw rule.error( 'Selector "' + rule.selector + @@ -664,6 +695,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..d33854f 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,359 @@ 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 { 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/, + }, + { + 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 {