Skip to content

Commit

Permalink
Add tailwindcss/nesting plugin (#4673)
Browse files Browse the repository at this point in the history
* 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 <adam.wathan@gmail.com>
  • Loading branch information
RobinMalfait and adamwathan committed Jun 17, 2021
1 parent 243e881 commit f63b453
Show file tree
Hide file tree
Showing 9 changed files with 373 additions and 43 deletions.
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)
}
}

0 comments on commit f63b453

Please sign in to comment.