postcss-nested 5.0.6 → 6.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 -111
  2. package/index.d.ts +9 -2
  3. package/index.js +231 -87
  4. package/package.json +2 -2
package/README.md CHANGED
@@ -81,118 +81,10 @@ Alternatives:
81
81
  [`postcss-current-selector`]: https://github.com/komlev/postcss-current-selector
82
82
  [`postcss-nested-ancestors`]: https://github.com/toomuchdesign/postcss-nested-ancestors
83
83
  [`postcss-nested-props`]: https://github.com/jedmao/postcss-nested-props
84
- [`postcss-nesting`]: https://github.com/jonathantneal/postcss-nesting
84
+ [`postcss-nesting`]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-nesting
85
85
  [CSSWG draft]: https://drafts.csswg.org/css-nesting-1/
86
86
  [PostCSS]: https://github.com/postcss/postcss
87
87
 
88
88
 
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`.
89
+ ## Docs
90
+ Read **[full docs](https://github.com/postcss/postcss-nested#readme)** on GitHub.
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,159 @@ 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(['media', 'supports', 'layer'], opts.bubble)
129
272
  let atruleChilds = createFnAtruleChilds(bubble)
130
273
  let unwrap = atruleNames(
131
274
  [
@@ -137,11 +280,20 @@ module.exports = (opts = {}) => {
137
280
  ],
138
281
  opts.unwrap
139
282
  )
283
+ let rootRuleName = (opts.rootRuleName || 'at-root').replace(/^@/, '')
140
284
  let preserveEmpty = opts.preserveEmpty
141
285
 
142
286
  return {
143
287
  postcssPlugin: 'postcss-nested',
144
- Rule (rule, { Rule }) {
288
+
289
+ Once(root) {
290
+ root.walkAtRules(rootRuleName, node => {
291
+ normalizeRootRule(node)
292
+ root[hasRootRule] = true
293
+ })
294
+ },
295
+
296
+ Rule(rule) {
145
297
  let unwrapped = false
146
298
  let after = rule
147
299
  let copyDeclarations = false
@@ -150,48 +302,33 @@ module.exports = (opts = {}) => {
150
302
  rule.each(child => {
151
303
  if (child.type === 'rule') {
152
304
  if (declarations.length) {
153
- after = pickDeclarations(rule.selector, declarations, after, Rule)
305
+ after = pickDeclarations(rule.selector, declarations, after)
154
306
  declarations = []
155
307
  }
156
308
 
157
309
  copyDeclarations = true
158
310
  unwrapped = true
159
- child.selectors = selectors(rule, child)
160
- after = pickComment(child.prev(), after)
161
- after.after(child)
162
- after = child
311
+ child.selectors = mergeSelectors(rule, child)
312
+ after = breakOut(child, after)
163
313
  } else if (child.type === 'atrule') {
164
314
  if (declarations.length) {
165
- after = pickDeclarations(rule.selector, declarations, after, Rule)
315
+ after = pickDeclarations(rule.selector, declarations, after)
166
316
  declarations = []
167
317
  }
168
-
169
- if (child.name === 'at-root') {
318
+ if (child.name === rootRuleName) {
170
319
  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()
320
+ atruleChilds(rule, child, true, child[rootRuleMergeSel])
321
+ after = breakOut(child, after)
181
322
  } else if (bubble[child.name]) {
182
323
  copyDeclarations = true
183
324
  unwrapped = true
184
325
  atruleChilds(rule, child, true)
185
- after = pickComment(child.prev(), after)
186
- after.after(child)
187
- after = child
326
+ after = breakOut(child, after)
188
327
  } else if (unwrap[child.name]) {
189
328
  copyDeclarations = true
190
329
  unwrapped = true
191
330
  atruleChilds(rule, child, false)
192
- after = pickComment(child.prev(), after)
193
- after.after(child)
194
- after = child
331
+ after = breakOut(child, after)
195
332
  } else if (copyDeclarations) {
196
333
  declarations.push(child)
197
334
  }
@@ -201,13 +338,20 @@ module.exports = (opts = {}) => {
201
338
  })
202
339
 
203
340
  if (declarations.length) {
204
- after = pickDeclarations(rule.selector, declarations, after, Rule)
341
+ after = pickDeclarations(rule.selector, declarations, after)
205
342
  }
206
343
 
207
344
  if (unwrapped && preserveEmpty !== true) {
208
345
  rule.raws.semicolon = true
209
346
  if (rule.nodes.length === 0) rule.remove()
210
347
  }
348
+ },
349
+
350
+ RootExit(root) {
351
+ if (root[hasRootRule]) {
352
+ root.walkAtRules(rootRuleName, unwrapRootRule)
353
+ root[hasRootRule] = false
354
+ }
211
355
  }
212
356
  }
213
357
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postcss-nested",
3
- "version": "5.0.6",
3
+ "version": "6.0.0",
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.10"
27
27
  }
28
28
  }