postcss-nested 5.0.6 → 6.0.1

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 +5 -117
  2. package/index.d.ts +9 -2
  3. package/index.js +234 -87
  4. package/package.json +2 -2
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  title="Philosopher’s stone, logo of PostCSS"
5
5
  src="https://postcss.org/logo-leftp.svg">
6
6
 
7
- [PostCSS] plugin to unwrap nested rules like how Sass does it.
7
+ [PostCSS] plugin to unwrap nested rules closer to Sass syntax.
8
8
 
9
9
  ```css
10
10
  .phone {
@@ -59,8 +59,6 @@ html {
59
59
 
60
60
  Related plugins:
61
61
 
62
- * Use [`postcss-atroot`] for `@at-root` at-rule to move nested child
63
- to the CSS root.
64
62
  * Use [`postcss-current-selector`] **after** this plugin if you want
65
63
  to use current selector in properties or variables values.
66
64
  * Use [`postcss-nested-ancestors`] **before** this plugin if you want
@@ -68,8 +66,7 @@ Related plugins:
68
66
 
69
67
  Alternatives:
70
68
 
71
- * See also [`postcss-nesting`], which implements [CSSWG draft]
72
- (requires the `&` and introduces `@nest`).
69
+ * See also [`postcss-nesting`], which implements [CSSWG draft].
73
70
  * [`postcss-nested-props`] for nested properties like `font-size`.
74
71
 
75
72
  <a href="https://evilmartians.com/?utm_source=postcss-nested">
@@ -77,122 +74,13 @@ Alternatives:
77
74
  alt="Sponsored by Evil Martians" width="236" height="54">
78
75
  </a>
79
76
 
80
- [`postcss-atroot`]: https://github.com/OEvgeny/postcss-atroot
81
77
  [`postcss-current-selector`]: https://github.com/komlev/postcss-current-selector
82
78
  [`postcss-nested-ancestors`]: https://github.com/toomuchdesign/postcss-nested-ancestors
83
79
  [`postcss-nested-props`]: https://github.com/jedmao/postcss-nested-props
84
- [`postcss-nesting`]: https://github.com/jonathantneal/postcss-nesting
80
+ [`postcss-nesting`]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-nesting
85
81
  [CSSWG draft]: https://drafts.csswg.org/css-nesting-1/
86
82
  [PostCSS]: https://github.com/postcss/postcss
87
83
 
88
84
 
89
- ## Usage
90
-
91
- **Step 1:** Install plugin:
92
-
93
- ```sh
94
- npm install --save-dev postcss postcss-nested
95
- ```
96
-
97
- **Step 2:** Check your project for existing PostCSS config: `postcss.config.js`
98
- in the project root, `"postcss"` section in `package.json`
99
- or `postcss` in bundle config.
100
-
101
- If you do not use PostCSS, add it according to [official docs]
102
- and set this plugin in settings.
103
-
104
- **Step 3:** Add the plugin to plugins list:
105
-
106
- ```diff
107
- module.exports = {
108
- plugins: [
109
- + require('postcss-nested'),
110
- require('autoprefixer')
111
- ]
112
- }
113
- ```
114
-
115
- [official docs]: https://github.com/postcss/postcss#usage
116
-
117
-
118
- ## Options
119
-
120
- ### `bubble`
121
-
122
- By default, plugin will bubble only `@media` and `@supports` at-rules.
123
- You can add your custom at-rules to this list by `bubble` option:
124
-
125
- ```js
126
- postcss([ require('postcss-nested')({ bubble: ['phone'] }) ])
127
- ```
128
-
129
- ```css
130
- /* input */
131
- a {
132
- color: white;
133
- @phone {
134
- color: black;
135
- }
136
- }
137
- /* output */
138
- a {
139
- color: white;
140
- }
141
- @phone {
142
- a {
143
- color: black;
144
- }
145
- }
146
- ```
147
-
148
-
149
- ### `unwrap`
150
-
151
- By default, plugin will unwrap only `@font-face`, `@keyframes` and `@document`
152
- at-rules. You can add your custom at-rules to this list by `unwrap` option:
153
-
154
- ```js
155
- postcss([ require('postcss-nested')({ unwrap: ['phone'] }) ])
156
- ```
157
-
158
- ```css
159
- /* input */
160
- a {
161
- color: white;
162
- @phone {
163
- color: black;
164
- }
165
- }
166
- /* output */
167
- a {
168
- color: white;
169
- }
170
- @phone {
171
- color: black;
172
- }
173
- ```
174
-
175
-
176
- ### `preserveEmpty`
177
-
178
- By default, plugin will strip out any empty selector generated by intermediate
179
- nesting levels. You can set `preserveEmpty` to `true` to preserve them.
180
-
181
- ```css
182
- .a {
183
- .b {
184
- color: black;
185
- }
186
- }
187
- ```
188
-
189
- Will be compiled to:
190
-
191
- ```css
192
- .a { }
193
- .a .b {
194
- color: black;
195
- }
196
- ```
197
-
198
- This is especially useful if you want to export the empty classes with `postcss-modules`.
85
+ ## Docs
86
+ Read full docs **[here](https://github.com/postcss/postcss-nested#readme)**.
package/index.d.ts CHANGED
@@ -6,8 +6,8 @@ import { PluginCreator } from 'postcss'
6
6
  declare namespace nested {
7
7
  interface Options {
8
8
  /**
9
- * By default, plugin will bubble only `@media` and `@supports` at-rules.
10
- * You can add your custom at-rules to this list by this option.
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
11
  */
12
12
  bubble?: string[]
13
13
 
@@ -24,6 +24,13 @@ declare namespace nested {
24
24
  * to preserve them.
25
25
  */
26
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
27
34
  }
28
35
 
29
36
  type Nested = PluginCreator<Options>
package/index.js CHANGED
@@ -1,14 +1,17 @@
1
+ const { Rule, AtRule } = require('postcss')
1
2
  let parser = require('postcss-selector-parser')
2
3
 
3
- function parse (str, rule) {
4
+ /**
5
+ * Run a selector string through postcss-selector-parser
6
+ */
7
+ function parse(rawSelector, rule) {
4
8
  let nodes
5
- let saver = parser(parsed => {
6
- nodes = parsed
7
- })
8
9
  try {
9
- saver.processSync(str)
10
+ parser(parsed => {
11
+ nodes = parsed
12
+ }).processSync(rawSelector)
10
13
  } catch (e) {
11
- if (str.includes(':')) {
14
+ if (rawSelector.includes(':')) {
12
15
  throw rule ? rule.error('Missed semicolon') : e
13
16
  } else {
14
17
  throw rule ? rule.error(e.message) : e
@@ -17,19 +20,27 @@ function parse (str, rule) {
17
20
  return nodes.at(0)
18
21
  }
19
22
 
20
- function replace (nodes, parent) {
23
+ /**
24
+ * Replaces the "&" token in a node's selector with the parent selector
25
+ * similar to what SCSS does.
26
+ *
27
+ * Mutates the nodes list
28
+ */
29
+ function interpolateAmpInSelector(nodes, parent) {
21
30
  let replaced = false
22
- nodes.each(i => {
23
- if (i.type === 'nesting') {
24
- let clonedParent = parent.clone()
25
- if (i.value !== '&') {
26
- i.replaceWith(parse(i.value.replace('&', clonedParent.toString())))
31
+ nodes.each(node => {
32
+ if (node.type === 'nesting') {
33
+ let clonedParent = parent.clone({})
34
+ if (node.value !== '&') {
35
+ node.replaceWith(
36
+ parse(node.value.replace('&', clonedParent.toString()))
37
+ )
27
38
  } else {
28
- i.replaceWith(clonedParent)
39
+ node.replaceWith(clonedParent)
29
40
  }
30
41
  replaced = true
31
- } else if (i.nodes) {
32
- if (replace(i, parent)) {
42
+ } else if ('nodes' in node && node.nodes) {
43
+ if (interpolateAmpInSelector(node, parent)) {
33
44
  replaced = true
34
45
  }
35
46
  }
@@ -37,51 +48,60 @@ function replace (nodes, parent) {
37
48
  return replaced
38
49
  }
39
50
 
40
- function selectors (parent, child) {
41
- let result = []
42
- parent.selectors.forEach(i => {
43
- let parentNode = parse(i, parent)
44
-
45
- child.selectors.forEach(j => {
46
- if (j.length) {
47
- let node = parse(j, child)
48
- let replaced = replace(node, parentNode)
49
- if (!replaced) {
50
- node.prepend(parser.combinator({ value: ' ' }))
51
- node.prepend(parentNode.clone())
52
- }
53
- result.push(node.toString())
51
+ /**
52
+ * Combines parent and child selectors, in a SCSS-like way
53
+ */
54
+ function mergeSelectors(parent, child) {
55
+ let merged = []
56
+ parent.selectors.forEach(sel => {
57
+ let parentNode = parse(sel, parent)
58
+
59
+ child.selectors.forEach(selector => {
60
+ if (!selector) {
61
+ return
62
+ }
63
+ let node = parse(selector, child)
64
+ let replaced = interpolateAmpInSelector(node, parentNode)
65
+ if (!replaced) {
66
+ node.prepend(parser.combinator({ value: ' ' }))
67
+ node.prepend(parentNode.clone({}))
54
68
  }
69
+ merged.push(node.toString())
55
70
  })
56
71
  })
57
- return result
72
+ return merged
58
73
  }
59
74
 
60
- function pickComment (comment, after) {
61
- if (comment && comment.type === 'comment') {
62
- after.after(comment)
63
- return comment
64
- } else {
65
- return after
75
+ /**
76
+ * Move a child and its preceeding comment(s) to after "after"
77
+ */
78
+ function breakOut(child, after) {
79
+ let prev = child.prev()
80
+ after.after(child)
81
+ while (prev && prev.type === 'comment') {
82
+ let nextPrev = prev.prev()
83
+ after.after(prev)
84
+ prev = nextPrev
66
85
  }
86
+ return child
67
87
  }
68
88
 
69
- function createFnAtruleChilds (bubble) {
70
- return function atruleChilds (rule, atrule, bubbling) {
89
+ function createFnAtruleChilds(bubble) {
90
+ return function atruleChilds(rule, atrule, bubbling, mergeSels = bubbling) {
71
91
  let children = []
72
92
  atrule.each(child => {
73
- if (child.type === 'comment') {
74
- children.push(child)
75
- } else if (child.type === 'decl') {
76
- children.push(child)
77
- } else if (child.type === 'rule' && bubbling) {
78
- child.selectors = selectors(rule, child)
79
- } else if (child.type === 'atrule') {
80
- if (child.nodes && bubble[child.name]) {
81
- atruleChilds(rule, child, true)
82
- } else {
93
+ if (child.type === 'rule' && bubbling) {
94
+ if (mergeSels) {
95
+ child.selectors = mergeSelectors(rule, child)
96
+ }
97
+ } else if (child.type === 'atrule' && child.nodes) {
98
+ if (bubble[child.name]) {
99
+ atruleChilds(rule, child, mergeSels)
100
+ } else if (atrule[rootRuleMergeSel] !== false) {
83
101
  children.push(child)
84
102
  }
103
+ } else {
104
+ children.push(child)
85
105
  }
86
106
  })
87
107
  if (bubbling) {
@@ -96,36 +116,162 @@ function createFnAtruleChilds (bubble) {
96
116
  }
97
117
  }
98
118
 
99
- function pickDeclarations (selector, declarations, after, Rule) {
119
+ function pickDeclarations(selector, declarations, after) {
100
120
  let parent = new Rule({
101
121
  selector,
102
122
  nodes: []
103
123
  })
104
-
105
- for (let declaration of declarations) {
106
- parent.append(declaration)
107
- }
108
-
124
+ parent.append(declarations)
109
125
  after.after(parent)
110
126
  return parent
111
127
  }
112
128
 
113
- function atruleNames (defaults, custom) {
129
+ function atruleNames(defaults, custom) {
114
130
  let list = {}
115
- for (let i of defaults) {
116
- list[i] = true
131
+ for (let name of defaults) {
132
+ list[name] = true
117
133
  }
118
134
  if (custom) {
119
- for (let i of custom) {
120
- let name = i.replace(/^@/, '')
121
- list[name] = true
135
+ for (let name of custom) {
136
+ list[name.replace(/^@/, '')] = true
122
137
  }
123
138
  }
124
139
  return list
125
140
  }
126
141
 
142
+ function parseRootRuleParams(params) {
143
+ params = params.trim()
144
+ let braceBlock = params.match(/^\((.*)\)$/)
145
+ if (!braceBlock) {
146
+ return { type: 'basic', selector: params }
147
+ }
148
+ let bits = braceBlock[1].match(/^(with(?:out)?):(.+)$/)
149
+ if (bits) {
150
+ let allowlist = bits[1] === 'with'
151
+ let rules = Object.fromEntries(
152
+ bits[2]
153
+ .trim()
154
+ .split(/\s+/)
155
+ .map(name => [name, true])
156
+ )
157
+ if (allowlist && rules.all) {
158
+ return { type: 'noop' }
159
+ }
160
+ let escapes = rule => !!rules[rule]
161
+ if (rules.all) {
162
+ escapes = () => true
163
+ } else if (allowlist) {
164
+ escapes = rule => (rule === 'all' ? false : !rules[rule])
165
+ }
166
+
167
+ return {
168
+ type: 'withrules',
169
+ escapes
170
+ }
171
+ }
172
+ // Unrecognized brace block
173
+ return { type: 'unknown' }
174
+ }
175
+
176
+ function getAncestorRules(leaf) {
177
+ let lineage = []
178
+ let parent = leaf.parent
179
+
180
+ while (parent && parent instanceof AtRule) {
181
+ lineage.push(parent)
182
+ parent = parent.parent
183
+ }
184
+ return lineage
185
+ }
186
+
187
+ function unwrapRootRule(rule) {
188
+ let escapes = rule[rootRuleEscapes]
189
+
190
+ if (!escapes) {
191
+ rule.after(rule.nodes)
192
+ } else {
193
+ let nodes = rule.nodes
194
+
195
+ let topEscaped
196
+ let topEscapedIdx = -1
197
+ let breakoutLeaf
198
+ let breakoutRoot
199
+ let clone
200
+
201
+ let lineage = getAncestorRules(rule)
202
+ lineage.forEach((parent, i) => {
203
+ if (escapes(parent.name)) {
204
+ topEscaped = parent
205
+ topEscapedIdx = i
206
+ breakoutRoot = clone
207
+ } else {
208
+ let oldClone = clone
209
+ clone = parent.clone({ nodes: [] })
210
+ oldClone && clone.append(oldClone)
211
+ breakoutLeaf = breakoutLeaf || clone
212
+ }
213
+ })
214
+
215
+ if (!topEscaped) {
216
+ rule.after(nodes)
217
+ } else if (!breakoutRoot) {
218
+ topEscaped.after(nodes)
219
+ } else {
220
+ let leaf = breakoutLeaf
221
+ leaf.append(nodes)
222
+ topEscaped.after(breakoutRoot)
223
+ }
224
+
225
+ if (rule.next() && topEscaped) {
226
+ let restRoot
227
+ lineage.slice(0, topEscapedIdx + 1).forEach((parent, i, arr) => {
228
+ let oldRoot = restRoot
229
+ restRoot = parent.clone({ nodes: [] })
230
+ oldRoot && restRoot.append(oldRoot)
231
+
232
+ let nextSibs = []
233
+ let _child = arr[i - 1] || rule
234
+ let next = _child.next()
235
+ while (next) {
236
+ nextSibs.push(next)
237
+ next = next.next()
238
+ }
239
+ restRoot.append(nextSibs)
240
+ })
241
+ restRoot && (breakoutRoot || nodes[nodes.length - 1]).after(restRoot)
242
+ }
243
+ }
244
+
245
+ rule.remove()
246
+ }
247
+
248
+ const rootRuleMergeSel = Symbol('rootRuleMergeSel')
249
+ const rootRuleEscapes = Symbol('rootRuleEscapes')
250
+
251
+ function normalizeRootRule(rule) {
252
+ let { params } = rule
253
+ let { type, selector, escapes } = parseRootRuleParams(params)
254
+ if (type === 'unknown') {
255
+ throw rule.error(
256
+ `Unknown @${rule.name} parameter ${JSON.stringify(params)}`
257
+ )
258
+ }
259
+ if (type === 'basic' && selector) {
260
+ let selectorBlock = new Rule({ selector, nodes: rule.nodes })
261
+ rule.removeAll()
262
+ rule.append(selectorBlock)
263
+ }
264
+ rule[rootRuleEscapes] = escapes
265
+ rule[rootRuleMergeSel] = escapes ? !escapes('all') : type === 'noop'
266
+ }
267
+
268
+ const hasRootRule = Symbol('hasRootRule')
269
+
127
270
  module.exports = (opts = {}) => {
128
- let bubble = atruleNames(['media', 'supports'], opts.bubble)
271
+ let bubble = atruleNames(
272
+ ['media', 'supports', 'layer', 'container'],
273
+ opts.bubble
274
+ )
129
275
  let atruleChilds = createFnAtruleChilds(bubble)
130
276
  let unwrap = atruleNames(
131
277
  [
@@ -137,11 +283,20 @@ module.exports = (opts = {}) => {
137
283
  ],
138
284
  opts.unwrap
139
285
  )
286
+ let rootRuleName = (opts.rootRuleName || 'at-root').replace(/^@/, '')
140
287
  let preserveEmpty = opts.preserveEmpty
141
288
 
142
289
  return {
143
290
  postcssPlugin: 'postcss-nested',
144
- Rule (rule, { Rule }) {
291
+
292
+ Once(root) {
293
+ root.walkAtRules(rootRuleName, node => {
294
+ normalizeRootRule(node)
295
+ root[hasRootRule] = true
296
+ })
297
+ },
298
+
299
+ Rule(rule) {
145
300
  let unwrapped = false
146
301
  let after = rule
147
302
  let copyDeclarations = false
@@ -150,48 +305,33 @@ module.exports = (opts = {}) => {
150
305
  rule.each(child => {
151
306
  if (child.type === 'rule') {
152
307
  if (declarations.length) {
153
- after = pickDeclarations(rule.selector, declarations, after, Rule)
308
+ after = pickDeclarations(rule.selector, declarations, after)
154
309
  declarations = []
155
310
  }
156
311
 
157
312
  copyDeclarations = true
158
313
  unwrapped = true
159
- child.selectors = selectors(rule, child)
160
- after = pickComment(child.prev(), after)
161
- after.after(child)
162
- after = child
314
+ child.selectors = mergeSelectors(rule, child)
315
+ after = breakOut(child, after)
163
316
  } else if (child.type === 'atrule') {
164
317
  if (declarations.length) {
165
- after = pickDeclarations(rule.selector, declarations, after, Rule)
318
+ after = pickDeclarations(rule.selector, declarations, after)
166
319
  declarations = []
167
320
  }
168
-
169
- if (child.name === 'at-root') {
321
+ if (child.name === rootRuleName) {
170
322
  unwrapped = true
171
- atruleChilds(rule, child, false)
172
-
173
- let nodes = child.nodes
174
- if (child.params) {
175
- nodes = new Rule({ selector: child.params, nodes })
176
- }
177
-
178
- after.after(nodes)
179
- after = nodes
180
- child.remove()
323
+ atruleChilds(rule, child, true, child[rootRuleMergeSel])
324
+ after = breakOut(child, after)
181
325
  } else if (bubble[child.name]) {
182
326
  copyDeclarations = true
183
327
  unwrapped = true
184
328
  atruleChilds(rule, child, true)
185
- after = pickComment(child.prev(), after)
186
- after.after(child)
187
- after = child
329
+ after = breakOut(child, after)
188
330
  } else if (unwrap[child.name]) {
189
331
  copyDeclarations = true
190
332
  unwrapped = true
191
333
  atruleChilds(rule, child, false)
192
- after = pickComment(child.prev(), after)
193
- after.after(child)
194
- after = child
334
+ after = breakOut(child, after)
195
335
  } else if (copyDeclarations) {
196
336
  declarations.push(child)
197
337
  }
@@ -201,13 +341,20 @@ module.exports = (opts = {}) => {
201
341
  })
202
342
 
203
343
  if (declarations.length) {
204
- after = pickDeclarations(rule.selector, declarations, after, Rule)
344
+ after = pickDeclarations(rule.selector, declarations, after)
205
345
  }
206
346
 
207
347
  if (unwrapped && preserveEmpty !== true) {
208
348
  rule.raws.semicolon = true
209
349
  if (rule.nodes.length === 0) rule.remove()
210
350
  }
351
+ },
352
+
353
+ RootExit(root) {
354
+ if (root[hasRootRule]) {
355
+ root.walkAtRules(rootRuleName, unwrapRootRule)
356
+ root[hasRootRule] = false
357
+ }
211
358
  }
212
359
  }
213
360
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postcss-nested",
3
- "version": "5.0.6",
3
+ "version": "6.0.1",
4
4
  "description": "PostCSS plugin to unwrap nested rules like how Sass does it",
5
5
  "keywords": [
6
6
  "postcss",
@@ -23,6 +23,6 @@
23
23
  "postcss": "^8.2.14"
24
24
  },
25
25
  "dependencies": {
26
- "postcss-selector-parser": "^6.0.6"
26
+ "postcss-selector-parser": "^6.0.11"
27
27
  }
28
28
  }