Skip to content

Commit f63b453

Browse files
Add tailwindcss/nesting plugin (#4673)
* add nesting plugin * rename @tailwindcss/nesting to tailwindcss/nesting * ignore the built `nesting` plugin * add a postcss7 compat version * include `nesting` plugin when publishing * add `build-plugins` script This will allow us to keep the plugins in their dedicated folders + tests + postcss7 compatibility files. However, when we copy over the plugins to the root. For example `plugins/nesting/` -> `nesting/` we skip files like `.test.js` and `.postcss7.js`. * build plugins when running `prepublishOnly` * improve compat mode We will use a glob so that we can move all *.postcss7.* files to just *.* likewise we will also backup to *.* to *.postcss8.* for restoring purposes. Concrete example: - Current state: - index.js // PostCSS 8 implementation - index.postcss7.js // PostCSS 7 implementation - Run "compat" - index.js // PostCSS 7 implementation - index.postcss7.js // PostCSS 7 implementation - index.postcss8.js // PostCSS 8 implementation (Backup of original) - Run "compat:restore" - index.js // PostCSS 8 implementation - index.postcss7.js // PostCSS 7 implementation - X index.postcss8.js // PostCSS 8 implementation (Removed) * Update README.md * ensure we `npm install` before publishing Co-authored-by: Adam Wathan <[email protected]>
1 parent 243e881 commit f63b453

File tree

9 files changed

+373
-43
lines changed

9 files changed

+373
-43
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,8 @@ index.html
1010
yarn.lock
1111
yarn-error.log
1212

13+
# "External" plugins
14+
/nesting
15+
1316
# Perf related files
1417
isolate*.log

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"babelify": "babel src --out-dir lib --copy-files",
2323
"postbabelify": "ncc build lib/cli-peer-dependencies.js -o peers",
2424
"rebuild-fixtures": "npm run babelify && babel-node scripts/rebuildFixtures.js",
25-
"prepublishOnly": "npm run babelify && babel-node scripts/build.js",
25+
"prepublishOnly": "npm install --force && npm run babelify && babel-node scripts/build.js && node scripts/build-plugins.js",
2626
"style": "eslint .",
2727
"test": "cross-env TAILWIND_MODE=build jest",
2828
"test:integrations": "npm run test --prefix ./integrations",
@@ -38,6 +38,7 @@
3838
"peers/*",
3939
"scripts/*.js",
4040
"stubs/*.stub.js",
41+
"nesting/*",
4142
"*.css",
4243
"*.js"
4344
],

plugins/nesting/README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# tailwindcss/nesting
2+
3+
This is a PostCSS plugin that wraps [postcss-nested](https://github.com/postcss/postcss-nested) or [postcss-nesting](https://github.com/jonathantneal/postcss-nesting) and acts as a compatibility layer to make sure your nesting plugin of choice properly understands Tailwind's custom syntax like `@apply` and `@screen`.
4+
5+
Add it to your PostCSS configuration, somewhere before Tailwind itself:
6+
7+
```js
8+
// postcss.config.js
9+
module.exports = {
10+
plugins: [
11+
require('postcss-import'),
12+
require('tailwindcss/nesting'),
13+
require('tailwindcss'),
14+
require('autoprefixer'),
15+
]
16+
}
17+
```
18+
19+
By default, it uses the [postcss-nested](https://github.com/postcss/postcss-nested) plugin under the hood, which uses a Sass-like syntax and is the plugin that powers nesting support in the [Tailwind CSS plugin API](https://tailwindcss.com/docs/plugins#css-in-js-syntax).
20+
21+
If you'd rather use [postcss-nesting](https://github.com/jonathantneal/postcss-nesting) (which is based on the work-in-progress [CSS Nesting](https://drafts.csswg.org/css-nesting-1/) specification), first install the plugin alongside:
22+
23+
```shell
24+
npm install postcss-nesting
25+
```
26+
27+
Then pass the plugin itself as an argument to `tailwindcss/nesting` in your PostCSS configuration:
28+
29+
```js
30+
// postcss.config.js
31+
module.exports = {
32+
plugins: [
33+
require('postcss-import'),
34+
require('tailwindcss/nesting')(require('postcss-nesting')),
35+
require('tailwindcss'),
36+
require('autoprefixer'),
37+
]
38+
}
39+
```
40+
41+
This can also be helpful if for whatever reason you need to use a very specific version of `postcss-nested` and want to override the version we bundle with `tailwindcss/nesting` itself.
42+

plugins/nesting/index.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
let nesting = require('./plugin')
2+
3+
module.exports = (opts) => {
4+
return {
5+
postcssPlugin: 'tailwindcss/nesting',
6+
Once(root, { result }) {
7+
return nesting(opts)(root, result)
8+
},
9+
}
10+
}
11+
12+
module.exports.postcss = true

plugins/nesting/index.postcss7.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
let postcss = require('postcss')
2+
let nesting = require('./plugin')
3+
4+
module.exports = postcss.plugin('tailwindcss/nesting', nesting)

plugins/nesting/index.test.js

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
let postcss = require('postcss')
2+
let postcssNested = require('postcss-nested')
3+
let plugin = require('.')
4+
5+
it('should be possible to load a custom nesting plugin', async () => {
6+
let input = css`
7+
.foo {
8+
color: black;
9+
@screen md {
10+
color: blue;
11+
}
12+
}
13+
`
14+
15+
expect(
16+
await run(input, function (root) {
17+
root.walkRules((rule) => {
18+
rule.selector += '-modified'
19+
})
20+
})
21+
).toMatchCss(css`
22+
.foo-modified {
23+
color: black;
24+
@media screen(md) {
25+
color: blue;
26+
}
27+
}
28+
`)
29+
})
30+
31+
it('should be possible to load a custom nesting plugin by name (string) instead', async () => {
32+
let input = css`
33+
.foo {
34+
color: black;
35+
@screen md {
36+
color: blue;
37+
}
38+
}
39+
`
40+
41+
expect(await run(input, 'postcss-nested')).toMatchCss(css`
42+
.foo {
43+
color: black;
44+
}
45+
46+
@media screen(md) {
47+
.foo {
48+
color: blue;
49+
}
50+
}
51+
`)
52+
})
53+
54+
it('should default to the bundled postcss-nested plugin (no options)', async () => {
55+
let input = css`
56+
.foo {
57+
color: black;
58+
@screen md {
59+
color: blue;
60+
}
61+
}
62+
`
63+
64+
expect(await run(input)).toMatchCss(css`
65+
.foo {
66+
color: black;
67+
}
68+
69+
@media screen(md) {
70+
.foo {
71+
color: blue;
72+
}
73+
}
74+
`)
75+
})
76+
77+
it('should default to the bundled postcss-nested plugin (empty ooptions)', async () => {
78+
let input = css`
79+
.foo {
80+
color: black;
81+
@screen md {
82+
color: blue;
83+
}
84+
}
85+
`
86+
87+
expect(await run(input, {})).toMatchCss(css`
88+
.foo {
89+
color: black;
90+
}
91+
92+
@media screen(md) {
93+
.foo {
94+
color: blue;
95+
}
96+
}
97+
`)
98+
})
99+
100+
test('@screen rules are replaced with media queries', async () => {
101+
let input = css`
102+
.foo {
103+
color: black;
104+
@screen md {
105+
color: blue;
106+
}
107+
}
108+
`
109+
110+
expect(await run(input, postcssNested)).toMatchCss(css`
111+
.foo {
112+
color: black;
113+
}
114+
115+
@media screen(md) {
116+
.foo {
117+
color: blue;
118+
}
119+
}
120+
`)
121+
})
122+
123+
test('@screen rules can work with `@apply`', async () => {
124+
let input = css`
125+
.foo {
126+
@apply bg-black;
127+
@screen md {
128+
@apply bg-blue-500;
129+
}
130+
}
131+
`
132+
133+
expect(await run(input, postcssNested)).toMatchCss(css`
134+
.foo {
135+
@apply bg-black;
136+
}
137+
138+
@media screen(md) {
139+
.foo {
140+
@apply bg-blue-500;
141+
}
142+
}
143+
`)
144+
})
145+
146+
// ---
147+
148+
function indentRecursive(node, indent = 0) {
149+
node.each &&
150+
node.each((child, i) => {
151+
if (!child.raws.before || child.raws.before.includes('\n')) {
152+
child.raws.before = `\n${node.type !== 'rule' && i > 0 ? '\n' : ''}${' '.repeat(indent)}`
153+
}
154+
child.raws.after = `\n${' '.repeat(indent)}`
155+
indentRecursive(child, indent + 1)
156+
})
157+
}
158+
159+
function formatNodes(root) {
160+
indentRecursive(root)
161+
if (root.first) {
162+
root.first.raws.before = ''
163+
}
164+
}
165+
166+
async function run(input, options) {
167+
return (
168+
await postcss([options === undefined ? plugin : plugin(options), formatNodes]).process(input, {
169+
from: undefined,
170+
})
171+
).toString()
172+
}
173+
174+
function css(templates) {
175+
return templates.join('')
176+
}

plugins/nesting/plugin.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
let postcss = require('postcss')
2+
let postcssNested = require('postcss-nested')
3+
4+
module.exports = function nesting(opts = postcssNested) {
5+
return (root, result) => {
6+
root.walkAtRules('screen', (rule) => {
7+
rule.name = 'media'
8+
rule.params = `screen(${rule.params})`
9+
})
10+
11+
root.walkAtRules('apply', (rule) => {
12+
rule.before(postcss.decl({ prop: '__apply', value: rule.params }))
13+
rule.remove()
14+
})
15+
16+
let plugin = (() => {
17+
if (typeof opts === 'function') {
18+
return opts
19+
}
20+
21+
if (typeof opts === 'string') {
22+
return require(opts)
23+
}
24+
25+
if (Object.keys(opts).length <= 0) {
26+
return postcssNested
27+
}
28+
29+
throw new Error('tailwindcss/nesting should be loaded with a nesting plugin.')
30+
})()
31+
32+
postcss([plugin]).process(root, result.opts).sync()
33+
34+
root.walkDecls('__apply', (decl) => {
35+
decl.before(postcss.atRule({ name: 'apply', params: decl.value }))
36+
decl.remove()
37+
})
38+
39+
return root
40+
}
41+
}

scripts/build-plugins.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
let fs = require('fs')
2+
let path = require('path')
3+
4+
let plugins = fs.readdirSync(fromRootPath('plugins'))
5+
6+
for (let plugin of plugins) {
7+
// Cleanup
8+
let pluginDest = fromRootPath(plugin)
9+
if (fs.existsSync(pluginDest)) {
10+
fs.rmdirSync(pluginDest, { recursive: true })
11+
}
12+
13+
// Copy plugin over
14+
copyFolder(fromRootPath('plugins', plugin), pluginDest, (file) => {
15+
// Ignore test files
16+
if (file.endsWith('.test.js')) return false
17+
// Ignore postcss7 files
18+
if (file.endsWith('.postcss7.js')) return false
19+
// Ignore postcss8 files
20+
if (file.endsWith('.postcss8.js')) return false
21+
22+
return true
23+
})
24+
}
25+
26+
// ---
27+
28+
function fromRootPath(...paths) {
29+
return path.resolve(process.cwd(), ...paths)
30+
}
31+
32+
function copy(fromPath, toPath) {
33+
fs.mkdirSync(path.dirname(toPath), { recursive: true }) // Ensure folder exists
34+
fs.copyFileSync(fromPath, toPath)
35+
}
36+
37+
function copyFolder(fromPath, toPath, shouldCopy = () => true) {
38+
let stats = fs.statSync(fromPath)
39+
if (stats.isDirectory()) {
40+
let filesAndFolders = fs.readdirSync(fromPath)
41+
for (let file of filesAndFolders) {
42+
copyFolder(path.resolve(fromPath, file), path.resolve(toPath, file), shouldCopy)
43+
}
44+
} else if (shouldCopy(fromPath)) {
45+
copy(fromPath, toPath)
46+
}
47+
}

0 commit comments

Comments
 (0)