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 <adam.wathan@gmail.com>
This commit is contained in:
parent
243e8814d8
commit
f63b4531f5
9 changed files with 404 additions and 74 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -10,5 +10,8 @@ index.html
|
|||
yarn.lock
|
||||
yarn-error.log
|
||||
|
||||
# "External" plugins
|
||||
/nesting
|
||||
|
||||
# Perf related files
|
||||
isolate*.log
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
@ -38,6 +38,7 @@
|
|||
"peers/*",
|
||||
"scripts/*.js",
|
||||
"stubs/*.stub.js",
|
||||
"nesting/*",
|
||||
"*.css",
|
||||
"*.js"
|
||||
],
|
||||
|
|
|
|||
42
plugins/nesting/README.md
Normal file
42
plugins/nesting/README.md
Normal file
|
|
@ -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
plugins/nesting/index.js
Normal file
12
plugins/nesting/index.js
Normal file
|
|
@ -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
plugins/nesting/index.postcss7.js
Normal file
4
plugins/nesting/index.postcss7.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
let postcss = require('postcss')
|
||||
let nesting = require('./plugin')
|
||||
|
||||
module.exports = postcss.plugin('tailwindcss/nesting', nesting)
|
||||
176
plugins/nesting/index.test.js
Normal file
176
plugins/nesting/index.test.js
Normal file
|
|
@ -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
plugins/nesting/plugin.js
Normal file
41
plugins/nesting/plugin.js
Normal file
|
|
@ -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
scripts/build-plugins.js
Normal file
47
scripts/build-plugins.js
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,80 @@
|
|||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const merge = require('lodash/merge')
|
||||
let fs = require('fs')
|
||||
let path = require('path')
|
||||
let merge = require('lodash/merge')
|
||||
let fastGlob = require('fast-glob')
|
||||
|
||||
let postcss7 = fastGlob.sync(['./**/*.postcss7.*']).filter((file) => !file.startsWith('lib/'))
|
||||
let postcss8 = fastGlob.sync(['./**/*.postcss8.*']).filter((file) => !file.startsWith('lib/'))
|
||||
|
||||
if (process.argv.includes('--prepare')) {
|
||||
if (postcss8.length > 0) {
|
||||
console.error('\n\n[ABORT] Already in PostCSS 7 compatibility mode!\n\n')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
let mainPackageJson = require('../package.json')
|
||||
let compatPackageJson = require('../package.postcss7.json')
|
||||
|
||||
// Use postcss7 files
|
||||
for (let file of postcss7) {
|
||||
let bareFile = file.replace('.postcss7', '')
|
||||
let postcss8File = file.replace('.postcss7', '.postcss8')
|
||||
|
||||
// Backup
|
||||
copy(fromRootPath(bareFile), fromRootPath(postcss8File))
|
||||
|
||||
// Swap
|
||||
copy(fromRootPath(file), fromRootPath(bareFile))
|
||||
}
|
||||
|
||||
// Deep merge package.json contents
|
||||
let packageJson = merge({}, mainPackageJson, compatPackageJson)
|
||||
|
||||
// Remove peerDependencies
|
||||
delete packageJson.peerDependencies
|
||||
|
||||
// Cleanup devDependencies
|
||||
for (let key in packageJson.devDependencies) {
|
||||
if (key.includes('postcss')) delete packageJson.devDependencies[key]
|
||||
}
|
||||
|
||||
// Use new name
|
||||
packageJson.name = '@tailwindcss/postcss7-compat'
|
||||
|
||||
// Make sure you can publish
|
||||
packageJson.publishConfig = { access: 'public' }
|
||||
|
||||
// Write package.json with the new contents
|
||||
fs.writeFileSync(fromRootPath('package.json'), JSON.stringify(packageJson, null, 2), 'utf8')
|
||||
|
||||
// Print some useful information to make publishing easy
|
||||
console.log()
|
||||
console.log('You can safely publish `tailwindcss` in PostCSS 7 compatibility mode:\n')
|
||||
console.log()
|
||||
} else if (process.argv.includes('--restore')) {
|
||||
if (postcss8.length === 0) {
|
||||
console.error('\n\n[ABORT] Already in latest PostCSS mode!\n\n')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Use postcss8 files
|
||||
for (let file of postcss8) {
|
||||
let bareFile = file.replace('.postcss8', '')
|
||||
|
||||
// Restore
|
||||
copy(fromRootPath(file), fromRootPath(bareFile))
|
||||
|
||||
// Remove
|
||||
fs.unlinkSync(fromRootPath(file))
|
||||
}
|
||||
|
||||
// Done
|
||||
console.log()
|
||||
console.log('Restored from PostCSS 7 mode to latest PostCSS mode!')
|
||||
console.log()
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
function fromRootPath(...paths) {
|
||||
return path.resolve(process.cwd(), ...paths)
|
||||
|
|
@ -9,73 +83,3 @@ function fromRootPath(...paths) {
|
|||
function copy(fromPath, toPath) {
|
||||
fs.copyFileSync(fromPath, toPath)
|
||||
}
|
||||
|
||||
if (process.argv.includes('--prepare')) {
|
||||
if (
|
||||
fs.existsSync(fromRootPath('package.postcss8.json')) ||
|
||||
fs.existsSync(fromRootPath('src', 'index.postcss8.js'))
|
||||
) {
|
||||
console.error('\n\n[ABORT] Already in PostCSS 7 compatibility mode!\n\n')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const mainPackageJson = require('../package.json')
|
||||
const compatPackageJson = require('../package.postcss7.json')
|
||||
|
||||
// 1. Backup original package.json file
|
||||
copy(fromRootPath('package.json'), fromRootPath('package.postcss8.json'))
|
||||
|
||||
// 2. Backup src/index.js file
|
||||
copy(fromRootPath('src', 'index.js'), fromRootPath('src', 'index.postcss8.js'))
|
||||
|
||||
// 3. Use the PostCSS 7 compat file
|
||||
copy(fromRootPath('src', 'index.postcss7.js'), fromRootPath('src', 'index.js'))
|
||||
|
||||
// 4. Deep merge package.json contents
|
||||
const packageJson = merge({}, mainPackageJson, compatPackageJson)
|
||||
|
||||
// 5. Remove peerDependencies
|
||||
delete packageJson.peerDependencies
|
||||
|
||||
// 6. Cleanup devDependencies
|
||||
for (let key in packageJson.devDependencies) {
|
||||
if (key.includes('postcss')) delete packageJson.devDependencies[key]
|
||||
}
|
||||
|
||||
// 7. Use new name
|
||||
packageJson.name = '@tailwindcss/postcss7-compat'
|
||||
|
||||
// 8. Make sure you can publish
|
||||
packageJson.publishConfig = { access: 'public' }
|
||||
|
||||
// 9. Write package.json with the new contents
|
||||
fs.writeFileSync(fromRootPath('package.json'), JSON.stringify(packageJson, null, 2), 'utf8')
|
||||
|
||||
// 10. Print some useful information to make publishing easy
|
||||
console.log()
|
||||
console.log('You can safely publish `tailwindcss` in PostCSS 7 compatibility mode:\n')
|
||||
console.log()
|
||||
} else if (process.argv.includes('--restore')) {
|
||||
if (
|
||||
!fs.existsSync(fromRootPath('package.postcss8.json')) ||
|
||||
!fs.existsSync(fromRootPath('src', 'index.postcss8.js'))
|
||||
) {
|
||||
console.error('\n\n[ABORT] Already in latest PostCSS mode!\n\n')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// 1. Restore original package.json file
|
||||
copy(fromRootPath('package.postcss8.json'), fromRootPath('package.json'))
|
||||
|
||||
// 2. Restore src/index.js file
|
||||
copy(fromRootPath('src', 'index.postcss8.js'), fromRootPath('src', 'index.js'))
|
||||
|
||||
// 3. Cleanup PostCSS 8 related files
|
||||
fs.unlinkSync(fromRootPath('package.postcss8.json'))
|
||||
fs.unlinkSync(fromRootPath('src', 'index.postcss8.js'))
|
||||
|
||||
// 4. Done
|
||||
console.log()
|
||||
console.log('Restored from PostCSS 7 mode to latest PostCSS mode!')
|
||||
console.log()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue