Skip to content

Commit af64d71

Browse files
Prevent nesting plugin from breaking other plugins (#7563)
* Prevent nesting plugin from breaking other plugins This uses a private API but it’s the only solution we have right now. It’s guarded to hopefully be less breaking if the API disappears. * Update changelog
1 parent 9effea5 commit af64d71

File tree

4 files changed

+135
-6
lines changed

4 files changed

+135
-6
lines changed

CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
- Nothing yet!
10+
- Prevent nesting plugin from breaking other plugins ([#7563](https://github.com/tailwindlabs/tailwindcss/pull/7563))
1111

1212
## [3.0.23] - 2022-02-16
1313

src/postcss-plugins/nesting/plugin.js

+36
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,42 @@ export function nesting(opts = postcssNested) {
3939
decl.remove()
4040
})
4141

42+
/**
43+
* Use a private PostCSS API to remove the "clean" flag from the entire AST.
44+
* This is done because running process() on the AST will set the "clean"
45+
* flag on all nodes, which we don't want.
46+
*
47+
* This causes downstream plugins using the visitor API to be skipped.
48+
*
49+
* This is guarded because the PostCSS API is not public
50+
* and may change in future versions of PostCSS.
51+
*
52+
* See https://github.com/postcss/postcss/issues/1712 for more details
53+
*
54+
* @param {import('postcss').Node} node
55+
*/
56+
function markDirty(node) {
57+
if (!('markDirty' in node)) {
58+
return
59+
}
60+
61+
// Traverse the tree down to the leaf nodes
62+
if (node.nodes) {
63+
node.nodes.forEach((n) => markDirty(n))
64+
}
65+
66+
// If it's a leaf node mark it as dirty
67+
// We do this here because marking a node as dirty
68+
// will walk up the tree and mark all parents as dirty
69+
// resulting in a lot of unnecessary work if we did this
70+
// for every single node
71+
if (!node.nodes) {
72+
node.markDirty()
73+
}
74+
}
75+
76+
markDirty(root)
77+
4278
return root
4379
}
4480
}

tests/postcss-plugins/nesting/index.test.js

+56-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import postcss from 'postcss'
22
import postcssNested from 'postcss-nested'
33
import plugin from '../../../src/postcss-plugins/nesting'
4+
import { visitorSpyPlugin } from './plugins.js'
45

56
it('should be possible to load a custom nesting plugin', async () => {
67
let input = css`
@@ -166,6 +167,46 @@ test('@screen rules can work with `@apply`', async () => {
166167
`)
167168
})
168169

170+
test('nesting does not break downstream plugin visitors', async () => {
171+
let input = css`
172+
.foo {
173+
color: black;
174+
}
175+
@suppoerts (color: blue) {
176+
.foo {
177+
color: blue;
178+
}
179+
}
180+
/* Comment */
181+
`
182+
183+
let spyPlugin = visitorSpyPlugin()
184+
185+
let plugins = [plugin(postcssNested), spyPlugin.plugin]
186+
187+
let result = await run(input, plugins)
188+
189+
expect(result).toMatchCss(css`
190+
.foo {
191+
color: black;
192+
}
193+
@suppoerts (color: blue) {
194+
.foo {
195+
color: blue;
196+
}
197+
}
198+
/* Comment */
199+
`)
200+
201+
expect(spyPlugin.spies.Once).toHaveBeenCalled()
202+
expect(spyPlugin.spies.OnceExit).toHaveBeenCalled()
203+
expect(spyPlugin.spies.Root).toHaveBeenCalled()
204+
expect(spyPlugin.spies.Rule).toHaveBeenCalled()
205+
expect(spyPlugin.spies.AtRule).toHaveBeenCalled()
206+
expect(spyPlugin.spies.Comment).toHaveBeenCalled()
207+
expect(spyPlugin.spies.Declaration).toHaveBeenCalled()
208+
})
209+
169210
// ---
170211

171212
function indentRecursive(node, indent = 0) {
@@ -187,11 +228,21 @@ function formatNodes(root) {
187228
}
188229

189230
async function run(input, options) {
190-
return (
191-
await postcss([options === undefined ? plugin : plugin(options), formatNodes]).process(input, {
192-
from: undefined,
193-
})
194-
).toString()
231+
let plugins = []
232+
233+
if (Array.isArray(options)) {
234+
plugins = options
235+
} else {
236+
plugins.push(options === undefined ? plugin : plugin(options))
237+
}
238+
239+
plugins.push(formatNodes)
240+
241+
let result = await postcss(plugins).process(input, {
242+
from: undefined,
243+
})
244+
245+
return result.toString()
195246
}
196247

197248
function css(templates) {
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
export function visitorSpyPlugin() {
2+
let Once = jest.fn()
3+
let OnceExit = jest.fn()
4+
let Root = jest.fn()
5+
let AtRule = jest.fn()
6+
let Rule = jest.fn()
7+
let Comment = jest.fn()
8+
let Declaration = jest.fn()
9+
10+
let plugin = Object.assign(
11+
function () {
12+
return {
13+
postcssPlugin: 'visitor-test',
14+
15+
// These work fine
16+
Once,
17+
OnceExit,
18+
19+
// These break
20+
Root,
21+
Rule,
22+
AtRule,
23+
Declaration,
24+
Comment,
25+
}
26+
},
27+
{ postcss: true }
28+
)
29+
30+
return {
31+
plugin,
32+
spies: {
33+
Once,
34+
OnceExit,
35+
Root,
36+
AtRule,
37+
Rule,
38+
Comment,
39+
Declaration,
40+
},
41+
}
42+
}

0 commit comments

Comments
 (0)