Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tailwindcss/nesting plugin #4673

Merged
merged 10 commits into from Jun 17, 2021
3 changes: 3 additions & 0 deletions .gitignore
Expand Up @@ -10,5 +10,8 @@ index.html
yarn.lock
yarn-error.log

# "External" plugins
/nesting

# Perf related files
isolate*.log
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -22,7 +22,7 @@
"babelify": "babel src --out-dir lib --copy-files",
"postbabelify": "ncc build lib/cli-peer-dependencies.js -o peers",
"rebuild-fixtures": "npm run babelify && babel-node scripts/rebuildFixtures.js",
"prepublishOnly": "npm run babelify && babel-node scripts/build.js",
"prepublishOnly": "npm install --force && npm run babelify && babel-node scripts/build.js && node scripts/build-plugins.js",
"style": "eslint .",
"test": "cross-env TAILWIND_MODE=build jest",
"test:integrations": "npm run test --prefix ./integrations",
Expand All @@ -38,6 +38,7 @@
"peers/*",
"scripts/*.js",
"stubs/*.stub.js",
"nesting/*",
"*.css",
"*.js"
],
Expand Down
42 changes: 42 additions & 0 deletions plugins/nesting/README.md
@@ -0,0 +1,42 @@
# tailwindcss/nesting

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`.

Add it to your PostCSS configuration, somewhere before Tailwind itself:

```js
// postcss.config.js
module.exports = {
plugins: [
require('postcss-import'),
require('tailwindcss/nesting'),
require('tailwindcss'),
require('autoprefixer'),
]
}
```

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).

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:

```shell
npm install postcss-nesting
```

Then pass the plugin itself as an argument to `tailwindcss/nesting` in your PostCSS configuration:

```js
// postcss.config.js
module.exports = {
plugins: [
require('postcss-import'),
require('tailwindcss/nesting')(require('postcss-nesting')),
require('tailwindcss'),
require('autoprefixer'),
]
}
```

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.

12 changes: 12 additions & 0 deletions plugins/nesting/index.js
@@ -0,0 +1,12 @@
let nesting = require('./plugin')

module.exports = (opts) => {
return {
postcssPlugin: 'tailwindcss/nesting',
Once(root, { result }) {
return nesting(opts)(root, result)
},
}
}

module.exports.postcss = true
4 changes: 4 additions & 0 deletions plugins/nesting/index.postcss7.js
@@ -0,0 +1,4 @@
let postcss = require('postcss')
let nesting = require('./plugin')

module.exports = postcss.plugin('tailwindcss/nesting', nesting)
176 changes: 176 additions & 0 deletions plugins/nesting/index.test.js
@@ -0,0 +1,176 @@
let postcss = require('postcss')
let postcssNested = require('postcss-nested')
let plugin = require('.')

it('should be possible to load a custom nesting plugin', async () => {
let input = css`
.foo {
color: black;
@screen md {
color: blue;
}
}
`

expect(
await run(input, function (root) {
root.walkRules((rule) => {
rule.selector += '-modified'
})
})
).toMatchCss(css`
.foo-modified {
color: black;
@media screen(md) {
color: blue;
}
}
`)
})

it('should be possible to load a custom nesting plugin by name (string) instead', async () => {
let input = css`
.foo {
color: black;
@screen md {
color: blue;
}
}
`

expect(await run(input, 'postcss-nested')).toMatchCss(css`
.foo {
color: black;
}

@media screen(md) {
.foo {
color: blue;
}
}
`)
})

it('should default to the bundled postcss-nested plugin (no options)', async () => {
let input = css`
.foo {
color: black;
@screen md {
color: blue;
}
}
`

expect(await run(input)).toMatchCss(css`
.foo {
color: black;
}

@media screen(md) {
.foo {
color: blue;
}
}
`)
})

it('should default to the bundled postcss-nested plugin (empty ooptions)', async () => {
let input = css`
.foo {
color: black;
@screen md {
color: blue;
}
}
`

expect(await run(input, {})).toMatchCss(css`
.foo {
color: black;
}

@media screen(md) {
.foo {
color: blue;
}
}
`)
})

test('@screen rules are replaced with media queries', async () => {
let input = css`
.foo {
color: black;
@screen md {
color: blue;
}
}
`

expect(await run(input, postcssNested)).toMatchCss(css`
.foo {
color: black;
}

@media screen(md) {
.foo {
color: blue;
}
}
`)
})

test('@screen rules can work with `@apply`', async () => {
let input = css`
.foo {
@apply bg-black;
@screen md {
@apply bg-blue-500;
}
}
`

expect(await run(input, postcssNested)).toMatchCss(css`
.foo {
@apply bg-black;
}

@media screen(md) {
.foo {
@apply bg-blue-500;
}
}
`)
})

// ---

function indentRecursive(node, indent = 0) {
node.each &&
node.each((child, i) => {
if (!child.raws.before || child.raws.before.includes('\n')) {
child.raws.before = `\n${node.type !== 'rule' && i > 0 ? '\n' : ''}${' '.repeat(indent)}`
}
child.raws.after = `\n${' '.repeat(indent)}`
indentRecursive(child, indent + 1)
})
}

function formatNodes(root) {
indentRecursive(root)
if (root.first) {
root.first.raws.before = ''
}
}

async function run(input, options) {
return (
await postcss([options === undefined ? plugin : plugin(options), formatNodes]).process(input, {
from: undefined,
})
).toString()
}

function css(templates) {
return templates.join('')
}
41 changes: 41 additions & 0 deletions plugins/nesting/plugin.js
@@ -0,0 +1,41 @@
let postcss = require('postcss')
let postcssNested = require('postcss-nested')

module.exports = function nesting(opts = postcssNested) {
return (root, result) => {
root.walkAtRules('screen', (rule) => {
rule.name = 'media'
rule.params = `screen(${rule.params})`
})

root.walkAtRules('apply', (rule) => {
rule.before(postcss.decl({ prop: '__apply', value: rule.params }))
rule.remove()
})

let plugin = (() => {
if (typeof opts === 'function') {
return opts
}

if (typeof opts === 'string') {
return require(opts)
}

if (Object.keys(opts).length <= 0) {
return postcssNested
}

throw new Error('tailwindcss/nesting should be loaded with a nesting plugin.')
})()

postcss([plugin]).process(root, result.opts).sync()

root.walkDecls('__apply', (decl) => {
decl.before(postcss.atRule({ name: 'apply', params: decl.value }))
decl.remove()
})

return root
}
}
47 changes: 47 additions & 0 deletions scripts/build-plugins.js
@@ -0,0 +1,47 @@
let fs = require('fs')
let path = require('path')

let plugins = fs.readdirSync(fromRootPath('plugins'))

for (let plugin of plugins) {
// Cleanup
let pluginDest = fromRootPath(plugin)
if (fs.existsSync(pluginDest)) {
fs.rmdirSync(pluginDest, { recursive: true })
}

// Copy plugin over
copyFolder(fromRootPath('plugins', plugin), pluginDest, (file) => {
// Ignore test files
if (file.endsWith('.test.js')) return false
// Ignore postcss7 files
if (file.endsWith('.postcss7.js')) return false
// Ignore postcss8 files
if (file.endsWith('.postcss8.js')) return false

return true
})
}

// ---

function fromRootPath(...paths) {
return path.resolve(process.cwd(), ...paths)
}

function copy(fromPath, toPath) {
fs.mkdirSync(path.dirname(toPath), { recursive: true }) // Ensure folder exists
fs.copyFileSync(fromPath, toPath)
}

function copyFolder(fromPath, toPath, shouldCopy = () => true) {
let stats = fs.statSync(fromPath)
if (stats.isDirectory()) {
let filesAndFolders = fs.readdirSync(fromPath)
for (let file of filesAndFolders) {
copyFolder(path.resolve(fromPath, file), path.resolve(toPath, file), shouldCopy)
}
} else if (shouldCopy(fromPath)) {
copy(fromPath, toPath)
}
}