Skip to content

Commit 37c75f9

Browse files
committed
fix: Correctly handle with/without parameters on @at-root
1 parent 16c7282 commit 37c75f9

File tree

3 files changed

+194
-25
lines changed

3 files changed

+194
-25
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
33

44
## Upcoming...
55
* ... <!-- Add new lines here. -->
6+
* fix: Correctly handle `with`/`without` parameters on `@at-root`
67
* fix: Errors when handling sibling `@at-root` rule blocks
78
* fix: Move all preceeding comments with rule
89
* fix: `@layer` blocks should also bubble

index.js

Lines changed: 182 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// @ts-check
22
let parser = require('postcss-selector-parser')
33

4+
/** @typedef {import('postcss').Container} Container */
45
/** @typedef {import('postcss').ChildNode} ChildNode */
56
/** @typedef {import('postcss').Comment} Comment */
67
/** @typedef {import('postcss').Declaration} Declaration */
@@ -127,15 +128,18 @@ function createFnAtruleChilds (/** @type {RuleMap} */ bubble) {
127128
* @param {PostcssRule} rule
128129
* @param {AtRule} atrule
129130
* @param {boolean} bubbling
131+
* @param {boolean} [mergeSels]
130132
*/
131-
return function atruleChilds (rule, atrule, bubbling) {
133+
return function atruleChilds (rule, atrule, bubbling, mergeSels = bubbling) {
132134
/** @type {Array<ChildNode>} */
133135
let children = []
134136
atrule.each(child => {
135137
if (child.type === 'rule' && bubbling) {
136-
child.selectors = mergeSelectors(rule, child)
138+
if (mergeSels) {
139+
child.selectors = mergeSelectors(rule, child)
140+
}
137141
} else if (child.type === 'atrule' && child.nodes && bubble[child.name]) {
138-
atruleChilds(rule, child, true)
142+
atruleChilds(rule, child, mergeSels)
139143
} else {
140144
children.push(child)
141145
}
@@ -186,6 +190,165 @@ function atruleNames (defaults, custom) {
186190
return list
187191
}
188192

193+
/** @typedef {{ type: 'basic', selector?: string, escapeRule?: never }} AtRootBParams */
194+
/** @typedef {{ type: 'withrules', escapeRule: (rule: string) => boolean, selector?: never }} AtRootWParams */
195+
/** @typedef {{ type: 'unknown', selector?: never, escapeRule?: never }} AtRootUParams */
196+
/** @typedef {{ type: 'noop', selector?: never, escapeRule?: never }} AtRootNParams */
197+
/** @typedef {AtRootBParams | AtRootWParams | AtRootNParams | AtRootUParams} AtRootParams */
198+
199+
/** @type {(params: string) => AtRootParams } */
200+
function parseAtRootParams (params) {
201+
params = params.trim()
202+
let braceBlock = params.match(/^\((.*)\)$/)
203+
if (!braceBlock) {
204+
return { type: 'basic', selector: params }
205+
}
206+
let bits = braceBlock[1].match(/^(with(?:out)?):(.+)$/)
207+
if (bits) {
208+
let allowlist = bits[1] === 'with'
209+
/** @type {RuleMap} */
210+
let rules = Object.fromEntries(
211+
bits[2]
212+
.trim()
213+
.split(/\s+/)
214+
.map(name => [name, true])
215+
)
216+
if (allowlist && rules.all) {
217+
return { type: 'noop' }
218+
}
219+
return {
220+
type: 'withrules',
221+
escapeRule: rules.all
222+
? () => true
223+
: allowlist
224+
? rule => rule === 'all' ? false : !rules[rule]
225+
: rule => !!rules[rule]
226+
}
227+
}
228+
// Unrecognized brace block
229+
return { type: 'unknown' }
230+
}
231+
232+
/**
233+
* @param {AtRule} leaf
234+
* @returns {Array<AtRule>}
235+
*/
236+
function getAncestorRules (leaf) {
237+
/** @type {Array<AtRule>} */
238+
const lineage = []
239+
/** @type {Container<ChildNode> | ChildNode | Document | undefined} */
240+
let parent
241+
parent = leaf.parent
242+
243+
while (parent) {
244+
if (parent.type === 'atrule') {
245+
lineage.push(/** @type {AtRule} */(parent))
246+
}
247+
parent = parent.parent
248+
}
249+
return lineage
250+
}
251+
252+
253+
/**
254+
* @param {AtRule} at_root
255+
*/
256+
function handleAtRootWithRules (at_root) {
257+
const { type, escapeRule } = parseAtRootParams(at_root.params)
258+
if (type !== 'withrules') {
259+
throw at_root.error('This rule should have been handled during first pass.')
260+
}
261+
262+
const nodes = at_root.nodes
263+
264+
/** @type {AtRule | undefined} */
265+
let topEscaped
266+
let topEscapedIdx = -1
267+
/** @type {AtRule | undefined} */
268+
let breakoutLeaf
269+
/** @type {AtRule | undefined} */
270+
let breakoutRoot
271+
/** @type {AtRule | undefined} */
272+
let clone
273+
274+
const lineage = getAncestorRules(at_root)
275+
lineage.forEach((parent, i) => {
276+
if (escapeRule(parent.name)) {
277+
topEscaped = parent
278+
topEscapedIdx = i
279+
breakoutRoot = clone
280+
} else {
281+
const oldClone = clone
282+
clone = parent.clone({ nodes: [] })
283+
oldClone && clone.append(oldClone)
284+
breakoutLeaf = breakoutLeaf || clone
285+
}
286+
})
287+
288+
if (!topEscaped) {
289+
at_root.after(nodes)
290+
} else if (!breakoutRoot) {
291+
topEscaped.after(nodes)
292+
} else {
293+
const leaf = /** @type {AtRule} */ (breakoutLeaf)
294+
leaf.append(nodes)
295+
topEscaped.after(breakoutRoot)
296+
}
297+
298+
if (at_root.next() && topEscaped) {
299+
/** @type {AtRule | undefined} */
300+
let restRoot
301+
lineage.slice(0, topEscapedIdx +1).forEach((parent, i, arr) => {
302+
const oldRoot = restRoot
303+
restRoot = parent.clone({ nodes: [] })
304+
oldRoot && restRoot.append(oldRoot)
305+
306+
/** @type {Array<ChildNode>} */
307+
let nextSibs = []
308+
let _child = arr[i - 1] || at_root
309+
let next = _child.next()
310+
while (next) {
311+
nextSibs.push(next)
312+
next = next.next()
313+
}
314+
restRoot.append(nextSibs)
315+
})
316+
restRoot && (breakoutRoot || nodes[nodes.length - 1]).after(restRoot)
317+
}
318+
319+
at_root.remove()
320+
}
321+
322+
/**
323+
* @param {PostcssRule} rule
324+
* @param {AtRule} child
325+
* @param {ChildNode} after
326+
* @param {{ (rule: PostcssRule, atrule: AtRule, bubbling: boolean, mergeSels?: boolean): void; }} atruleChilds
327+
*/
328+
function handleAtRoot (rule, child, after, atruleChilds) {
329+
let { nodes, params } = child
330+
const { type, selector, escapeRule } = parseAtRootParams(params)
331+
if (type === 'withrules') {
332+
atruleChilds(rule, child, true, !escapeRule('all'))
333+
after = breakOut(child, after)
334+
} else if (type === 'unknown') {
335+
throw rule.error(`Unknown @at-root parameter ${JSON.stringify(params)}`)
336+
} else {
337+
if (selector) {
338+
// nodes = [new Rule({ selector: selector, nodes })]
339+
nodes = [rule.clone({ selector, nodes })]
340+
}
341+
atruleChilds(rule, child, true, type === 'noop')
342+
after.after(nodes)
343+
after = nodes[nodes.length - 1]
344+
child.remove()
345+
}
346+
return after
347+
}
348+
349+
350+
// ---------------------------------------------------------------------------
351+
189352
/** @type {import('./').Nested} */
190353
module.exports = (opts = {}) => {
191354
let bubble = atruleNames(['media', 'supports', 'layer'], opts.bubble)
@@ -202,8 +365,22 @@ module.exports = (opts = {}) => {
202365
)
203366
let preserveEmpty = opts.preserveEmpty
204367

368+
let hasRootRules = false
369+
205370
return {
206371
postcssPlugin: 'postcss-nested',
372+
373+
RootExit (root, { }) {
374+
if (hasRootRules) {
375+
root.walk((node) => {
376+
if (node.type === 'atrule' && node.name === 'at-root') {
377+
handleAtRootWithRules(node)
378+
}
379+
})
380+
hasRootRules = false
381+
}
382+
},
383+
207384
Rule (rule, { Rule }) {
208385
let unwrapped = false
209386
/** @type {ChildNode} */
@@ -228,19 +405,10 @@ module.exports = (opts = {}) => {
228405
after = pickDeclarations(rule.selector, declarations, after, Rule)
229406
declarations = []
230407
}
231-
232408
if (child.name === 'at-root') {
409+
hasRootRules = true
233410
unwrapped = true
234-
atruleChilds(rule, child, false)
235-
236-
let nodes = child.nodes
237-
if (child.params) {
238-
nodes = [new Rule({ selector: child.params, nodes: child.nodes })]
239-
}
240-
241-
after.after(nodes)
242-
after = nodes[nodes.length - 1]
243-
child.remove()
411+
after = handleAtRoot(rule, child, after, atruleChilds)
244412
} else if (bubble[child.name]) {
245413
copyDeclarations = true
246414
unwrapped = true

index.test.js

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ test('at-root unwraps nested media', () => {
111111
run('a { & {} @media x { @at-root { b { } } } }', 'a {} @media x { b {} }')
112112
})
113113

114-
test.skip('nested at-root with nested media', () => {
114+
test('nested at-root with nested media', () => {
115115
run(
116116
`a {
117117
& {}
@@ -135,7 +135,7 @@ test.skip('nested at-root with nested media', () => {
135135
)
136136
})
137137

138-
test.skip('at-root supports (without: all)', () => {
138+
test('at-root supports (without: all)', () => {
139139
run(
140140
`@media x {
141141
@supports (z:y) {
@@ -191,7 +191,7 @@ test.skip('at-root supports (without: all)', () => {
191191
)
192192
})
193193

194-
test.skip('at-root supports (with: all)', () => {
194+
test('at-root supports (with: all)', () => {
195195
run(
196196
`@media x {
197197
@supports (z:y) {
@@ -224,7 +224,7 @@ test.skip('at-root supports (with: all)', () => {
224224
)
225225
})
226226

227-
test.skip('at-root supports (without: foo)', () => {
227+
test('at-root supports (without: foo)', () => {
228228
run(
229229
`@media x {
230230
a {
@@ -241,7 +241,7 @@ test.skip('at-root supports (without: foo)', () => {
241241
)
242242
})
243243

244-
test.skip('at-root supports (without: foo) 2', () => {
244+
test('at-root supports (without: foo) 2', () => {
245245
run(
246246
`@supports (y:z) {
247247
@media x {
@@ -262,7 +262,7 @@ test.skip('at-root supports (without: foo) 2', () => {
262262
)
263263
})
264264

265-
test.skip('at-root supports (with: foo)', () => {
265+
test('at-root supports (with: foo)', () => {
266266
run(
267267
`@supports (y:z) {
268268
@media x {
@@ -283,7 +283,7 @@ test.skip('at-root supports (with: foo)', () => {
283283
)
284284
})
285285

286-
test.skip('at-root supports (without: foo) 3', () => {
286+
test('at-root supports (without: foo) 3', () => {
287287
run(
288288
`@supports (y:z) {
289289
@media x {
@@ -307,7 +307,7 @@ test.skip('at-root supports (without: foo) 3', () => {
307307
})
308308

309309

310-
test.skip('at-root supports (without: foo) 4', () => {
310+
test('at-root supports (without: foo) 4', () => {
311311
run(
312312
`@media x {
313313
@supports (y:z) {
@@ -328,7 +328,7 @@ test.skip('at-root supports (without: foo) 4', () => {
328328
)
329329
})
330330

331-
test.skip('at-root supports (without: foo) 5', () => {
331+
test('at-root supports (without: foo) 5', () => {
332332
run(
333333
`@media x {
334334
@supports (a:b) {
@@ -479,7 +479,7 @@ test('leaves nested @media blocks as is', () => {
479479
)
480480
})
481481

482-
test.skip('@at-root fully espacpes nested @media blocks', () => {
482+
test('@at-root fully espacpes nested @media blocks', () => {
483483
run(
484484
`a { x: 3 }
485485
a {
@@ -714,7 +714,7 @@ test('shows clear errors on other errors', () => {
714714
}, ':2:3: Unexpected')
715715
})
716716

717-
test.skip('errors on unknown @at-root parameters', () => {
717+
test('errors on unknown @at-root parameters', () => {
718718
let css = 'a {\n @at-root (wonky: "blah") {\n b {}\n }\n}'
719719
throws(() => {
720720
css = postcss([plugin]).process(css, { from: undefined }).css

0 commit comments

Comments
 (0)