postcss-nested 8.0.1 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +3 -0
  2. package/index.d.ts +28 -32
  3. package/index.js +90 -13
  4. package/package.json +2 -2
package/README.md CHANGED
@@ -81,3 +81,6 @@ Alternatives:
81
81
  [`postcss-nesting`]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-nesting
82
82
  [CSSWG draft]: https://drafts.csswg.org/css-nesting-1/
83
83
  [PostCSS]: https://github.com/postcss/postcss
84
+
85
+ ## Docs
86
+ Read full docs **[here](https://github.com/postcss/postcss-nested#readme)**.
package/index.d.ts CHANGED
@@ -3,39 +3,35 @@
3
3
 
4
4
  import type { PluginCreator } from 'postcss'
5
5
 
6
- declare namespace nested {
7
- interface Options {
8
- /**
9
- * By default, plugin will bubble only `@media`, `@supports` and `@layer`
10
- * at-rules. Use this option to add your custom at-rules to this list.
11
- */
12
- bubble?: string[]
13
-
14
- /**
15
- * By default, plugin will unwrap only `@font-face`, `@keyframes`,
16
- * and `@document` at-rules. You can add your custom at-rules
17
- * to this list by this option.
18
- */
19
- unwrap?: string[]
20
-
21
- /**
22
- * By default, plugin will strip out any empty selector generated
23
- * by intermediate nesting levels. You can set this option to `true`
24
- * to preserve them.
25
- */
26
- preserveEmpty?: boolean
27
-
28
- /**
29
- * The plugin supports the SCSS custom at-rule `@at-root` which breaks
30
- * rule blocks out of their nested position. If you want, you can choose
31
- * a new custom name for this rule in your code.
32
- */
33
- rootRuleName?: string
34
- }
35
-
36
- type Nested = PluginCreator<Options>
6
+ export interface Options {
7
+ /**
8
+ * By default, plugin will bubble only `@media`, `@supports` and `@layer`
9
+ * at-rules. Use this option to add your custom at-rules to this list.
10
+ */
11
+ bubble?: string[]
12
+
13
+ /**
14
+ * By default, plugin will unwrap only `@font-face`, `@keyframes`,
15
+ * and `@document` at-rules. You can add your custom at-rules
16
+ * to this list by this option.
17
+ */
18
+ unwrap?: string[]
19
+
20
+ /**
21
+ * By default, plugin will strip out any empty selector generated
22
+ * by intermediate nesting levels. You can set this option to `true`
23
+ * to preserve them.
24
+ */
25
+ preserveEmpty?: boolean
26
+
27
+ /**
28
+ * The plugin supports the SCSS custom at-rule `@at-root` which breaks
29
+ * rule blocks out of their nested position. If you want, you can choose
30
+ * a new custom name for this rule in your code.
31
+ */
32
+ rootRuleName?: string
37
33
  }
38
34
 
39
- declare let nested: nested.Nested
35
+ declare let nested: PluginCreator<Options>
40
36
 
41
37
  export default nested
package/index.js CHANGED
@@ -20,6 +20,18 @@ function parse(rawSelector, rule) {
20
20
  return nodes.at(0)
21
21
  }
22
22
 
23
+ /**
24
+ * Replaces one "&" token with the parent selector
25
+ */
26
+ function replaceAmp(node, parent) {
27
+ let clonedParent = parent.clone({})
28
+ if (node.value !== '&') {
29
+ node.replaceWith(parse(node.value.replace('&', clonedParent.toString())))
30
+ } else {
31
+ node.replaceWith(clonedParent)
32
+ }
33
+ }
34
+
23
35
  /**
24
36
  * Replaces the "&" token in a node's selector with the parent selector
25
37
  * similar to what SCSS does.
@@ -30,14 +42,7 @@ function interpolateAmpInSelector(nodes, parent) {
30
42
  let replaced = false
31
43
  nodes.each(node => {
32
44
  if (node.type === 'nesting') {
33
- let clonedParent = parent.clone({})
34
- if (node.value !== '&') {
35
- node.replaceWith(
36
- parse(node.value.replace('&', clonedParent.toString()))
37
- )
38
- } else {
39
- node.replaceWith(clonedParent)
40
- }
45
+ replaceAmp(node, parent)
41
46
  replaced = true
42
47
  } else if ('nodes' in node && node.nodes) {
43
48
  if (interpolateAmpInSelector(node, parent)) {
@@ -48,20 +53,92 @@ function interpolateAmpInSelector(nodes, parent) {
48
53
  return replaced
49
54
  }
50
55
 
56
+ /**
57
+ * Pseudo-classes which take a selector list
58
+ */
59
+ const SELECTOR_LISTS = new Set([':has', ':is', ':not', ':where'])
60
+
61
+ /**
62
+ * Replaces "&" inside :not() with the whole parent selector list, like Sass.
63
+ * A copy for each parent selector would turn "neither .a nor .b"
64
+ * into "not .a or not .b".
65
+ *
66
+ * Mutates the nodes list
67
+ */
68
+ function replaceAmpsInNot(nodes, parents, negated = false) {
69
+ let replaced = false
70
+ nodes.each(node => {
71
+ if (node.type !== 'pseudo') {
72
+ return
73
+ }
74
+ let name = node.value.toLowerCase()
75
+ let inside = negated || name === ':not'
76
+ node.each(selector => {
77
+ if (replaceAmpsInNot(selector, parents, inside)) {
78
+ replaced = true
79
+ }
80
+ let amps = selector.nodes.filter(i => i.type === 'nesting')
81
+ if (!inside || !SELECTOR_LISTS.has(name) || amps.length === 0) {
82
+ return
83
+ }
84
+ let combos = [[]]
85
+ for (let i = 0; i < amps.length; i++) {
86
+ combos = combos.flatMap(combo => parents.map(p => [...combo, p]))
87
+ }
88
+ let list = combos.map(combo => {
89
+ let copy = selector.clone()
90
+ copy.nodes
91
+ .filter(i => i.type === 'nesting')
92
+ .forEach((amp, i) => replaceAmp(amp, combo[i]))
93
+ return copy.toString().trim()
94
+ })
95
+ let space = node.index(selector) > 0 ? ' ' : ''
96
+ selector.replaceWith(...parser().astSync(space + list.join(', ')).nodes)
97
+ replaced = true
98
+ })
99
+ })
100
+ return replaced
101
+ }
102
+
51
103
  /**
52
104
  * Combines parent and child selectors, in a SCSS-like way
53
105
  */
54
106
  function mergeSelectors(parent, child) {
55
107
  let merged = []
56
- for (let sel of parent.selectors) {
57
- let parentNode = parse(sel, parent)
58
-
108
+ let parents = parent.selectors.map(sel => parse(sel, parent))
109
+ for (let parentNode of parents) {
59
110
  for (let selector of child.selectors) {
60
111
  if (!selector) {
61
112
  continue
62
113
  }
63
114
  let node = parse(selector, child)
64
- let replaced = interpolateAmpInSelector(node, parentNode)
115
+ let replacedInNot = replaceAmpsInNot(node, parents)
116
+ let ampsLeft = false
117
+ node.walkNesting(() => {
118
+ ampsLeft = true
119
+ })
120
+ // Without "&" outside :not(), every parent selector gives the same result
121
+ if (replacedInNot && !ampsLeft && parentNode !== parents[0]) {
122
+ continue
123
+ }
124
+ let amps = node.nodes.filter(i => i.type === 'nesting')
125
+ if (amps.length > 1) {
126
+ // Like in Sass, every "&" after the first one takes each parent selector
127
+ let combos = [[parentNode]]
128
+ for (let i = 1; i < amps.length; i++) {
129
+ combos = combos.flatMap(combo => parents.map(p => [...combo, p]))
130
+ }
131
+ for (let combo of combos) {
132
+ let copy = node.clone()
133
+ copy.nodes
134
+ .filter(i => i.type === 'nesting')
135
+ .forEach((amp, i) => replaceAmp(amp, combo[i]))
136
+ interpolateAmpInSelector(copy, parentNode)
137
+ merged.push(copy.toString())
138
+ }
139
+ continue
140
+ }
141
+ let replaced = interpolateAmpInSelector(node, parentNode) || replacedInNot
65
142
  if (!replaced) {
66
143
  node.prepend(parser.combinator({ value: ' ' }))
67
144
  node.prepend(parentNode.clone({}))
@@ -149,7 +226,7 @@ function pickAndClearDeclarations(
149
226
 
150
227
  function atruleNames(defaults, custom = '') {
151
228
  let names = defaults.concat(custom)
152
- let list = {}
229
+ let list = Object.create(null)
153
230
  for (let name of names) {
154
231
  list[name.replace(/^@/, '')] = true
155
232
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postcss-nested",
3
- "version": "8.0.1",
3
+ "version": "9.0.0",
4
4
  "description": "PostCSS plugin to unwrap nested rules like how Sass does it",
5
5
  "keywords": [
6
6
  "css",
@@ -28,7 +28,7 @@
28
28
  "./package.json": "./package.json"
29
29
  },
30
30
  "dependencies": {
31
- "postcss-selector-parser": "^7.1.4"
31
+ "postcss-selector-parser": "^7.1.6"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "postcss": "^8.2.14"