Remove AOT (#5340)

* make `jit` mode the default when no mode is specified

* unify JIT and AOT codepaths

* ensure `Object.entries` on undefined doesn't break

It could be that sometimes you don't have values in your config (e.g.: `presets: []`), this in turn will break some plugins where we assume we have a value.

* drop AOT specific tests

These tests are all covered by JIT mode already and were AOT specific.

* simplify tests, and add a few

Some of the tests were written for AOT specifically, some were missing. We also updated the way we write those tests, essentially making Tailwind a blackbox, by testing against the final output.
Now that JIT mode is the default, this is super fast because we only generate what is used, instead of partially testing in a 3MB file or building it all, then purging.

* add some todo's to make sure we warn in a few cases

* make `darkMode: 'media'`, the default

This also includes moving dark mode tests to its own dedicated file.

* remove PostCSS 7 compat mode

* update CLI to be JIT-first

* fix integration tests

This is not a _real_ fix, but it does solve the broken test for now.

* warn when using @responsive or @variants

* remove the JIT preview warning

* remove AOT-only code paths

* remove all `mode: 'jit'` blocks

Also remove `variants: {}` since they are not useful in `JIT` mode
anymore.

* drop unused dependencies

* rename `purge` to `content`

* remove static CDN builds

* mark `--purge` as deprecated in the CLI

This will still work, but a warning will be printed and it won't show up
in the `--help` output.

* cleanup nesting plugin

We don't have to duplicate it anymore since there is no PostCSS 7
version anymore.

* make sure integration tests run in band

* cleanup folder structure

* make sure nesting folder is available

* simplify resolving of purge/content information
This commit is contained in:
Robin Malfait 2021-09-01 17:13:59 +02:00 committed by GitHub
parent 911e755056
commit 691ed02f63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
141 changed files with 1991 additions and 10609 deletions

5
.gitignore vendored
View File

@ -10,11 +10,8 @@ index.html
yarn.lock
yarn-error.log
# "External" plugins
/nesting
# Perf related files
isolate*.log
# Generated files
/src/corePluginList.js
/src/corePluginList.js

3
dist/.gitignore vendored
View File

@ -1,3 +0,0 @@
*
!.gitignore
!.npmignore

0
dist/.npmignore vendored
View File

View File

@ -5,7 +5,7 @@
"scripts": {
"build": "parcel build ./src/index.html --no-cache",
"dev": "parcel watch ./src/index.html --no-cache",
"test": "jest"
"test": "jest --runInBand"
},
"jest": {
"displayName": "parcel",

View File

@ -1,13 +1,8 @@
module.exports = {
purge: ['./src/index.html'],
mode: 'jit',
darkMode: false, // or 'media' or 'class'
content: ['./src/index.html'],
theme: {
extend: {},
},
variants: {
extend: {},
},
corePlugins: {
preflight: false,
},

View File

@ -127,9 +127,7 @@ describe.skip('watcher', () => {
'../tailwind.config.js',
javascript`
module.exports = {
purge: ['./src/index.html'],
mode: 'jit',
darkMode: false, // or 'media' or 'class'
content: ['./src/index.html'],
theme: {
extend: {
screens: {
@ -140,9 +138,6 @@ describe.skip('watcher', () => {
}
},
},
variants: {
extend: {},
},
corePlugins: {
preflight: false,
},

View File

@ -4,7 +4,7 @@
"version": "0.0.0",
"scripts": {
"build": "NODE_ENV=production postcss ./src/index.css -o ./dist/main.css",
"test": "jest"
"test": "jest --runInBand"
},
"jest": {
"displayName": "PostCSS CLI",

View File

@ -1,13 +1,8 @@
module.exports = {
purge: ['./src/index.html'],
mode: 'jit',
darkMode: false, // or 'media' or 'class'
content: ['./src/index.html'],
theme: {
extend: {},
},
variants: {
extend: {},
},
corePlugins: {
preflight: false,
},

View File

@ -109,9 +109,7 @@ describe('watcher', () => {
'../tailwind.config.js',
javascript`
module.exports = {
purge: ['./src/index.html'],
mode: 'jit',
darkMode: false, // or 'media' or 'class'
content: ['./src/index.html'],
theme: {
extend: {
screens: {
@ -122,9 +120,6 @@ describe('watcher', () => {
}
},
},
variants: {
extend: {},
},
corePlugins: {
preflight: false,
},

View File

@ -4,7 +4,7 @@
"version": "0.0.0",
"scripts": {
"build": "rollup -c",
"test": "jest"
"test": "jest --runInBand"
},
"jest": {
"displayName": "rollup.js",

View File

@ -1,13 +1,8 @@
module.exports = {
purge: ['./src/index.html'],
mode: 'jit',
darkMode: false, // or 'media' or 'class'
content: ['./src/index.html'],
theme: {
extend: {},
},
variants: {
extend: {},
},
corePlugins: {
preflight: false,
},

View File

@ -105,9 +105,7 @@ describe.each([{ TAILWIND_MODE: 'watch' }, { TAILWIND_MODE: undefined }])('watch
'../tailwind.config.js',
javascript`
module.exports = {
purge: ['./src/index.html'],
mode: 'jit',
darkMode: false, // or 'media' or 'class'
content: ['./src/index.html'],
theme: {
extend: {
screens: {
@ -118,9 +116,6 @@ describe.each([{ TAILWIND_MODE: 'watch' }, { TAILWIND_MODE: undefined }])('watch
}
},
},
variants: {
extend: {},
},
corePlugins: {
preflight: false,
},

View File

@ -1,13 +1,8 @@
module.exports = {
purge: ['./src/index.html'],
mode: 'jit',
darkMode: false, // or 'media' or 'class'
content: ['./src/index.html'],
theme: {
extend: {},
},
variants: {
extend: {},
},
corePlugins: {
preflight: false,
},

View File

@ -115,9 +115,7 @@ describe('Build command', () => {
let customConfig = `module.exports = ${JSON.stringify(
{
purge: ['./src/index.html'],
mode: 'jit',
darkMode: false, // or 'media' or 'class'
content: ['./src/index.html'],
theme: {
extend: {
fontWeight: {
@ -125,9 +123,6 @@ describe('Build command', () => {
},
},
},
variants: {
extend: {},
},
corePlugins: {
preflight: false,
},
@ -258,7 +253,7 @@ describe('Build command', () => {
expect(combined).toMatchInlineSnapshot(`
"
tailwindcss v2.2.8
tailwindcss v2.2.9
Usage:
tailwindcss build [options]
@ -267,8 +262,7 @@ describe('Build command', () => {
-i, --input Input file
-o, --output Output file
-w, --watch Watch for changes and rebuild as needed
--jit Build using JIT mode
--purge Content paths to use for removing unused classes
--content Content paths to use for removing unused classes
--postcss Load custom PostCSS configuration
-m, --minify Minify the output
-c, --config Path to a custom config file
@ -298,34 +292,6 @@ describe('Init command', () => {
expect((await readOutputFile('../full.config.js')).split('\n').length).toBeGreaterThan(50)
})
test('--jit', async () => {
cleanupFile('with-jit.config.js')
let { combined } = await $(`${EXECUTABLE} init with-jit.config.js --jit`)
expect(combined).toMatchInlineSnapshot(`
"
Created Tailwind CSS config file: with-jit.config.js
"
`)
expect(await readOutputFile('../with-jit.config.js')).toContain("mode: 'jit'")
})
test('--full, --jit', async () => {
cleanupFile('full-with-jit.config.js')
let { combined } = await $(`${EXECUTABLE} init full-with-jit.config.js --jit --full`)
expect(combined).toMatchInlineSnapshot(`
"
Created Tailwind CSS config file: full-with-jit.config.js
"
`)
expect(await readOutputFile('../full-with-jit.config.js')).toContain("mode: 'jit'")
})
test('--postcss', async () => {
expect(await fileExists('postcss.config.js')).toBe(true)
await removeFile('postcss.config.js')
@ -348,13 +314,12 @@ describe('Init command', () => {
expect(combined).toMatchInlineSnapshot(`
"
tailwindcss v2.2.8
tailwindcss v2.2.9
Usage:
tailwindcss init [options]
Options:
--jit Initialize for JIT mode
-f, --full Initialize a full \`tailwind.config.js\` file
-p, --postcss Initialize a \`postcss.config.js\` file
-h, --help Display usage information

View File

@ -32,19 +32,14 @@ describe('static build', () => {
'../tailwind.config.js',
javascript`
module.exports = {
purge: {
content: {
content: ['./src/index.html'],
safelist: ['bg-red-500','bg-red-600']
},
mode: 'jit',
darkMode: false, // or 'media' or 'class'
theme: {
extend: {
},
},
variants: {
extend: {},
},
corePlugins: {
preflight: false,
},
@ -212,9 +207,7 @@ describe('watcher', () => {
'../tailwind.config.js',
javascript`
module.exports = {
purge: ['./src/index.html'],
mode: 'jit',
darkMode: false, // or 'media' or 'class'
content: ['./src/index.html'],
theme: {
extend: {
screens: {
@ -225,9 +218,6 @@ describe('watcher', () => {
}
},
},
variants: {
extend: {},
},
corePlugins: {
preflight: false,
},

View File

@ -6,7 +6,7 @@
"browser": "./src/index.js",
"scripts": {
"build": "vite build",
"test": "jest"
"test": "jest --runInBand"
},
"jest": {
"displayName": "vite",

View File

@ -1,13 +1,8 @@
module.exports = {
purge: ['./index.html'],
mode: 'jit',
darkMode: false, // or 'media' or 'class'
content: ['./index.html'],
theme: {
extend: {},
},
variants: {
extend: {},
},
corePlugins: {
preflight: false,
},

View File

@ -132,9 +132,7 @@ describe('watcher', () => {
'tailwind.config.js',
javascript`
module.exports = {
purge: ['./index.html'],
mode: 'jit',
darkMode: false, // or 'media' or 'class'
content: ['./index.html'],
theme: {
extend: {
screens: {
@ -145,9 +143,6 @@ describe('watcher', () => {
}
},
},
variants: {
extend: {},
},
corePlugins: {
preflight: false,
},

View File

@ -7,7 +7,7 @@
"scripts": {
"build": "webpack --mode=production",
"dev": "webpack --mode=development --watch",
"test": "jest"
"test": "jest --runInBand"
},
"jest": {
"displayName": "webpack 4",

View File

@ -1,13 +1,8 @@
module.exports = {
purge: ['./src/index.html'],
mode: 'jit',
darkMode: false, // or 'media' or 'class'
content: ['./src/index.html'],
theme: {
extend: {},
},
variants: {
extend: {},
},
corePlugins: {
preflight: false,
},

View File

@ -103,9 +103,7 @@ describe.each([{ TAILWIND_MODE: 'watch' }, { TAILWIND_MODE: undefined }])('watch
'../tailwind.config.js',
javascript`
module.exports = {
purge: ['./src/index.html'],
mode: 'jit',
darkMode: false, // or 'media' or 'class'
content: ['./src/index.html'],
theme: {
extend: {
screens: {
@ -116,9 +114,6 @@ describe.each([{ TAILWIND_MODE: 'watch' }, { TAILWIND_MODE: undefined }])('watch
}
},
},
variants: {
extend: {},
},
corePlugins: {
preflight: false,
},

View File

@ -1,13 +1,8 @@
module.exports = {
purge: ['./src/index.html'],
mode: 'jit',
darkMode: false, // or 'media' or 'class'
content: ['./src/index.html'],
theme: {
extend: {},
},
variants: {
extend: {},
},
corePlugins: {
preflight: false,
},

View File

@ -103,9 +103,7 @@ describe.each([{ TAILWIND_MODE: 'watch' }, { TAILWIND_MODE: undefined }])('watch
'../tailwind.config.js',
javascript`
module.exports = {
purge: ['./src/index.html'],
mode: 'jit',
darkMode: false, // or 'media' or 'class'
content: ['./src/index.html'],
theme: {
extend: {
screens: {
@ -116,9 +114,6 @@ describe.each([{ TAILWIND_MODE: 'watch' }, { TAILWIND_MODE: undefined }])('watch
}
},
},
variants: {
extend: {},
},
corePlugins: {
preflight: false,
},
@ -234,19 +229,14 @@ describe.each([{ TAILWIND_MODE: 'watch' }, { TAILWIND_MODE: undefined }])('watch
'../tailwind.config.js',
javascript`
module.exports = {
purge: {
content: {
content: ['./src/index.html'],
safelist: ['bg-red-500','bg-red-600']
},
mode: 'jit',
darkMode: false, // or 'media' or 'class'
theme: {
extend: {
},
},
variants: {
extend: {},
},
corePlugins: {
preflight: false,
},

177
package-lock.json generated
View File

@ -9,7 +9,6 @@
"license": "MIT",
"dependencies": {
"arg": "^5.0.1",
"bytes": "^3.0.0",
"chalk": "^4.1.2",
"chokidar": "^3.5.2",
"color": "^4.0.1",
@ -18,14 +17,10 @@
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
"fast-glob": "^3.2.7",
"fs-extra": "^10.0.0",
"glob-parent": "^6.0.1",
"html-tags": "^3.1.0",
"is-glob": "^4.0.1",
"lodash": "^4.17.21",
"lodash.topath": "^4.5.2",
"modern-normalize": "^1.1.0",
"node-emoji": "^1.11.0",
"normalize-path": "^3.0.0",
"object-hash": "^2.2.0",
"postcss-js": "^3.0.3",
@ -33,8 +28,6 @@
"postcss-nested": "5.0.6",
"postcss-selector-parser": "^6.0.6",
"postcss-value-parser": "^4.1.0",
"pretty-hrtime": "^1.0.3",
"purgecss": "^4.0.3",
"quick-lru": "^5.1.1",
"reduce-css-calc": "^2.1.8",
"resolve": "^1.20.0",
@ -53,7 +46,6 @@
"@vercel/ncc": "^0.29.2",
"autoprefixer": "^10.3.3",
"babel-jest": "^27.0.6",
"clean-css": "5.1.4",
"cross-env": "^7.0.3",
"cssnano": "^5.0.8",
"eslint": "^7.32.0",
@ -3090,14 +3082,6 @@
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
"dev": true
},
"node_modules/bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/cache-base": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
@ -3409,27 +3393,6 @@
"node": ">=0.10.0"
}
},
"node_modules/clean-css": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.1.4.tgz",
"integrity": "sha512-e6JAuR0T2ahg7fOSv98Nxqh7mHWOac5TaCSgrr61h/6mkPLwlxX38hzob4h6IKj/UHlrrLXvAEjWqXlvi8r8lQ==",
"dev": true,
"dependencies": {
"source-map": "~0.6.0"
},
"engines": {
"node": ">= 10.0"
}
},
"node_modules/clean-css/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
@ -5094,19 +5057,6 @@
"node": ">=0.10.0"
}
},
"node_modules/fs-extra": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz",
"integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/fs-readdir-recursive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
@ -5297,7 +5247,8 @@
"node_modules/graceful-fs": {
"version": "4.2.6",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
"integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ=="
"integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==",
"dev": true
},
"node_modules/has": {
"version": "1.0.3",
@ -5411,14 +5362,6 @@
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true
},
"node_modules/html-tags": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz",
"integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==",
"engines": {
"node": ">=8"
}
},
"node_modules/http-proxy-agent": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz",
@ -7016,6 +6959,7 @@
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"dependencies": {
"universalify": "^2.0.0"
},
@ -7151,11 +7095,6 @@
"integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=",
"dev": true
},
"node_modules/lodash.topath": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz",
"integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak="
},
"node_modules/lodash.truncate": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
@ -7483,14 +7422,6 @@
"integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
"dev": true
},
"node_modules/node-emoji": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz",
"integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==",
"dependencies": {
"lodash": "^4.17.21"
}
},
"node_modules/node-environment-flags": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz",
@ -8686,6 +8617,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz",
"integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=",
"dev": true,
"engines": {
"node": ">= 0.8"
}
@ -8734,28 +8666,6 @@
"node": ">=6"
}
},
"node_modules/purgecss": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/purgecss/-/purgecss-4.0.3.tgz",
"integrity": "sha512-PYOIn5ibRIP34PBU9zohUcCI09c7drPJJtTDAc0Q6QlRz2/CHQ8ywGLdE7ZhxU2VTqB7p5wkvj5Qcm05Rz3Jmw==",
"dependencies": {
"commander": "^6.0.0",
"glob": "^7.0.0",
"postcss": "^8.2.1",
"postcss-selector-parser": "^6.0.2"
},
"bin": {
"purgecss": "bin/purgecss.js"
}
},
"node_modules/purgecss/node_modules/commander": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
"integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
"engines": {
"node": ">= 6"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -10119,6 +10029,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"dev": true,
"engines": {
"node": ">= 10.0.0"
}
@ -12744,11 +12655,6 @@
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
"dev": true
},
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"cache-base": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
@ -12988,23 +12894,6 @@
}
}
},
"clean-css": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.1.4.tgz",
"integrity": "sha512-e6JAuR0T2ahg7fOSv98Nxqh7mHWOac5TaCSgrr61h/6mkPLwlxX38hzob4h6IKj/UHlrrLXvAEjWqXlvi8r8lQ==",
"dev": true,
"requires": {
"source-map": "~0.6.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
@ -14272,16 +14161,6 @@
"map-cache": "^0.2.2"
}
},
"fs-extra": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz",
"integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==",
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
}
},
"fs-readdir-recursive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
@ -14416,7 +14295,8 @@
"graceful-fs": {
"version": "4.2.6",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
"integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ=="
"integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==",
"dev": true
},
"has": {
"version": "1.0.3",
@ -14502,11 +14382,6 @@
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true
},
"html-tags": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz",
"integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg=="
},
"http-proxy-agent": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz",
@ -15725,6 +15600,7 @@
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^2.0.0"
@ -15840,11 +15716,6 @@
"integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=",
"dev": true
},
"lodash.topath": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz",
"integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak="
},
"lodash.truncate": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
@ -16108,14 +15979,6 @@
"integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
"dev": true
},
"node-emoji": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz",
"integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==",
"requires": {
"lodash": "^4.17.21"
}
},
"node-environment-flags": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz",
@ -16927,7 +16790,8 @@
"pretty-hrtime": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz",
"integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE="
"integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=",
"dev": true
},
"process-nextick-args": {
"version": "2.0.1",
@ -16964,24 +16828,6 @@
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
"dev": true
},
"purgecss": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/purgecss/-/purgecss-4.0.3.tgz",
"integrity": "sha512-PYOIn5ibRIP34PBU9zohUcCI09c7drPJJtTDAc0Q6QlRz2/CHQ8ywGLdE7ZhxU2VTqB7p5wkvj5Qcm05Rz3Jmw==",
"requires": {
"commander": "^6.0.0",
"glob": "^7.0.0",
"postcss": "^8.2.1",
"postcss-selector-parser": "^6.0.2"
},
"dependencies": {
"commander": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
"integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA=="
}
}
},
"queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -18060,7 +17906,8 @@
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ=="
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"dev": true
},
"unset-value": {
"version": "1.0.0",

View File

@ -22,14 +22,12 @@
"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 install --force && npm run babelify && babel-node scripts/build.js && node scripts/build-plugins.js",
"prepublishOnly": "npm install --force && npm run babelify",
"style": "eslint .",
"test": "cross-env TAILWIND_MODE=build jest",
"test:integrations": "npm run test --prefix ./integrations",
"install:integrations": "node scripts/install-integrations.js",
"posttest": "npm run style",
"compat": "node scripts/compat.js --prepare",
"compat:restore": "node scripts/compat.js --restore",
"generate:plugin-list": "babel-node scripts/create-plugin-list.js"
},
"files": [
@ -52,7 +50,6 @@
"@vercel/ncc": "^0.29.2",
"autoprefixer": "^10.3.3",
"babel-jest": "^27.0.6",
"clean-css": "5.1.4",
"cross-env": "^7.0.3",
"cssnano": "^5.0.8",
"eslint": "^7.32.0",
@ -71,7 +68,6 @@
},
"dependencies": {
"arg": "^5.0.1",
"bytes": "^3.0.0",
"chalk": "^4.1.2",
"chokidar": "^3.5.2",
"color": "^4.0.1",
@ -80,14 +76,10 @@
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
"fast-glob": "^3.2.7",
"fs-extra": "^10.0.0",
"glob-parent": "^6.0.1",
"html-tags": "^3.1.0",
"is-glob": "^4.0.1",
"lodash": "^4.17.21",
"lodash.topath": "^4.5.2",
"modern-normalize": "^1.1.0",
"node-emoji": "^1.11.0",
"normalize-path": "^3.0.0",
"object-hash": "^2.2.0",
"postcss-js": "^3.0.3",
@ -95,8 +87,6 @@
"postcss-nested": "5.0.6",
"postcss-selector-parser": "^6.0.6",
"postcss-value-parser": "^4.1.0",
"pretty-hrtime": "^1.0.3",
"purgecss": "^4.0.3",
"quick-lru": "^5.1.1",
"reduce-css-calc": "^2.1.8",
"resolve": "^1.20.0",

View File

@ -1,13 +0,0 @@
{
"devDependencies": {
"cssnano": "^4"
},
"dependencies": {
"purgecss": "^4.0.3",
"autoprefixer": "^9",
"postcss": "^7",
"postcss-functions": "^3",
"postcss-js": "^2",
"postcss-nested": "^4"
}
}

View File

@ -1,4 +0,0 @@
let postcss = require('postcss')
let nesting = require('./plugin')
module.exports = postcss.plugin('tailwindcss/nesting', nesting)

View File

@ -1,47 +0,0 @@
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)
}
}

View File

@ -1,46 +0,0 @@
import fs from 'fs'
import postcss from 'postcss'
import tailwind from '..'
import CleanCSS from 'clean-css'
function buildDistFile(filename, config = {}, outFilename = filename) {
return new Promise((resolve, reject) => {
console.log(`Processing ./${filename}.css...`)
fs.readFile(`./${filename}.css`, (err, css) => {
if (err) throw err
return postcss([tailwind(config), require('autoprefixer')])
.process(css, {
from: `./${filename}.css`,
to: `./dist/${outFilename}.css`,
})
.then((result) => {
fs.writeFileSync(`./dist/${outFilename}.css`, result.css)
return result
})
.then((result) => {
const minified = new CleanCSS().minify(result.css)
fs.writeFileSync(`./dist/${outFilename}.min.css`, minified.styles)
})
.then(resolve)
.catch((error) => {
console.log(error)
reject()
})
})
})
}
console.info('Building Tailwind!')
Promise.all([
buildDistFile('base'),
buildDistFile('components'),
buildDistFile('utilities'),
buildDistFile('tailwind'),
buildDistFile('tailwind', { darkMode: 'class' }, 'tailwind-dark'),
buildDistFile('tailwind', { future: 'all', experimental: 'all' }, 'tailwind-experimental'),
]).then(() => {
console.log('Finished Building Tailwind!')
})

View File

@ -1,85 +0,0 @@
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)
}
function copy(fromPath, toPath) {
fs.copyFileSync(fromPath, toPath)
}

View File

@ -9,11 +9,11 @@ import fs from 'fs'
import postcssrc from 'postcss-load-config'
import { cosmiconfig } from 'cosmiconfig'
import loadPlugins from 'postcss-load-config/src/plugins' // Little bit scary, looking at private/internal API
import tailwindJit from './jit/processTailwindFeatures'
import tailwindAot from './processTailwindFeatures'
import tailwind from './processTailwindFeatures'
import resolveConfigInternal from '../resolveConfig'
import fastGlob from 'fast-glob'
import getModuleDependencies from './lib/getModuleDependencies'
import log from './util/log'
import packageJson from '../package.json'
let env = {
@ -86,7 +86,9 @@ function help({ message, usage, commands, options }) {
console.log()
console.log('Options:')
for (let { flags, description } of Object.values(groupedOptions)) {
for (let { flags, description, deprecated } of Object.values(groupedOptions)) {
if (deprecated) continue
if (flags.length === 1) {
console.log(
' '.repeat(indent + 4 /* 4 = "-i, ".length */),
@ -126,7 +128,6 @@ let commands = {
init: {
run: init,
args: {
'--jit': { type: Boolean, description: 'Initialize for JIT mode' },
'--full': { type: Boolean, description: 'Initialize a full `tailwind.config.js` file' },
'--postcss': { type: Boolean, description: 'Initialize a `postcss.config.js` file' },
'-f': '--full',
@ -139,8 +140,14 @@ let commands = {
'--input': { type: String, description: 'Input file' },
'--output': { type: String, description: 'Output file' },
'--watch': { type: Boolean, description: 'Watch for changes and rebuild as needed' },
'--jit': { type: Boolean, description: 'Build using JIT mode' },
'--purge': { type: String, description: 'Content paths to use for removing unused classes' },
'--content': {
type: String,
description: 'Content paths to use for removing unused classes',
},
'--purge': {
type: String,
deprecated: true,
},
'--postcss': {
type: oneOf(String, Boolean),
description: 'Load custom PostCSS configuration',
@ -306,15 +313,6 @@ function init() {
// Change colors import
stubFile = stubFile.replace('../colors', 'tailwindcss/colors')
// --jit mode
if (args['--jit']) {
// Add jit mode
stubFile = stubFile.replace('module.exports = {', "module.exports = {\n mode: 'jit',")
// Deleting variants
stubFile = stubFile.replace(/variants: {(.*)},\n /gs, '')
}
fs.writeFileSync(tailwindConfigLocation, stubFile, 'utf8')
messages.push(`Created Tailwind CSS config file: ${path.basename(tailwindConfigLocation)}`)
@ -420,39 +418,21 @@ async function build() {
let resolvedConfig = resolveConfigInternal(config)
if (args['--purge']) {
resolvedConfig.purge = {
enabled: true,
content: args['--purge'].split(/(?<!{[^}]+),/),
log.warn(['The `--purge` flag has been deprecated.', 'Please use `--content` instead.'])
if (!args['--content']) {
args['--content'] = ['--purge']
}
}
if (args['--jit']) {
resolvedConfig.mode = 'jit'
if (args['--content']) {
resolvedConfig.content = args['--content'].split(/(?<!{[^}]+),/)
}
return resolvedConfig
}
function extractContent(config) {
let content = Array.isArray(config.purge) ? config.purge : config.purge.content
return content.concat(
(config.purge?.safelist ?? []).map((content) => {
if (typeof content === 'string') {
return { raw: content, extension: 'html' }
}
if (content instanceof RegExp) {
throw new Error(
"Values inside 'purge.safelist' can only be of type 'string', found 'regex'."
)
}
throw new Error(
`Values inside 'purge.safelist' can only be of type 'string', found '${typeof content}'.`
)
})
)
return config.content.content.concat(config.content.safelist)
}
function extractFileGlobs(config) {
@ -473,7 +453,7 @@ async function build() {
function getChangedContent(config) {
let changedContent = []
// Resolve globs from the purge config
// Resolve globs from the content config
let globs = extractFileGlobs(config)
let files = fastGlob.sync(globs)
@ -496,26 +476,18 @@ async function build() {
let config = resolveConfig()
let changedContent = getChangedContent(config)
let tailwindPlugin =
config.mode === 'jit'
? () => {
return {
postcssPlugin: 'tailwindcss',
Once(root, { result }) {
tailwindJit(({ createContext }) => {
return () => {
return createContext(config, changedContent)
}
})(root, result)
},
let tailwindPlugin = () => {
return {
postcssPlugin: 'tailwindcss',
Once(root, { result }) {
tailwind(({ createContext }) => {
return () => {
return createContext(config, changedContent)
}
}
: () => {
return {
postcssPlugin: 'tailwindcss',
plugins: [tailwindAot(() => config, configPath)],
}
}
})(root, result)
},
}
}
tailwindPlugin.postcss = true
@ -637,39 +609,31 @@ async function build() {
async function rebuild(config) {
env.DEBUG && console.time('Finished in')
let tailwindPlugin =
config.mode === 'jit'
? () => {
return {
postcssPlugin: 'tailwindcss',
Once(root, { result }) {
env.DEBUG && console.time('Compiling CSS')
tailwindJit(({ createContext }) => {
console.error()
console.error('Rebuilding...')
let tailwindPlugin = () => {
return {
postcssPlugin: 'tailwindcss',
Once(root, { result }) {
env.DEBUG && console.time('Compiling CSS')
tailwind(({ createContext }) => {
console.error()
console.error('Rebuilding...')
return () => {
if (context !== null) {
context.changedContent = changedContent.splice(0)
return context
}
return () => {
if (context !== null) {
context.changedContent = changedContent.splice(0)
return context
}
env.DEBUG && console.time('Creating context')
context = createContext(config, changedContent.splice(0))
env.DEBUG && console.timeEnd('Creating context')
return context
}
})(root, result)
env.DEBUG && console.timeEnd('Compiling CSS')
},
env.DEBUG && console.time('Creating context')
context = createContext(config, changedContent.splice(0))
env.DEBUG && console.timeEnd('Creating context')
return context
}
}
: () => {
return {
postcssPlugin: 'tailwindcss',
plugins: [tailwindAot(() => config, configPath)],
}
}
})(root, result)
env.DEBUG && console.timeEnd('Compiling CSS')
},
}
}
tailwindPlugin.postcss = true

View File

@ -1,57 +1,335 @@
import * as plugins from './plugins/index.js'
import configurePlugins from './util/configurePlugins'
import postcss from 'postcss'
import * as corePlugins from './plugins'
import buildMediaQuery from './util/buildMediaQuery'
import prefixSelector from './util/prefixSelector'
import {
applyPseudoToMarker,
updateLastClasses,
updateAllClasses,
transformAllSelectors,
transformAllClasses,
transformLastClasses,
} from './util/pluginUtils'
import log from './util/log'
function move(items, item, befores) {
let lowestBefore = -1
export default {
pseudoElementVariants: function ({ config, addVariant }) {
addVariant(
'first-letter',
transformAllSelectors((selector) => {
return updateAllClasses(selector, (className, { withPseudo }) => {
return withPseudo(`first-letter${config('separator')}${className}`, '::first-letter')
})
})
)
for (let before of befores) {
let index = items.indexOf(before)
if (index >= 0 && (index < lowestBefore || lowestBefore === -1)) {
lowestBefore = index
addVariant(
'first-line',
transformAllSelectors((selector) => {
return updateAllClasses(selector, (className, { withPseudo }) => {
return withPseudo(`first-line${config('separator')}${className}`, '::first-line')
})
})
)
addVariant('marker', [
transformAllSelectors((selector) => {
let variantSelector = updateAllClasses(selector, (className) => {
return `marker${config('separator')}${className}`
})
return `${variantSelector} *::marker`
}),
transformAllSelectors((selector) => {
return updateAllClasses(selector, (className, { withPseudo }) => {
return withPseudo(`marker${config('separator')}${className}`, '::marker')
})
}),
])
addVariant('selection', [
transformAllSelectors((selector) => {
let variantSelector = updateAllClasses(selector, (className) => {
return `selection${config('separator')}${className}`
})
return `${variantSelector} *::selection`
}),
transformAllSelectors((selector) => {
return updateAllClasses(selector, (className, { withPseudo }) => {
return withPseudo(`selection${config('separator')}${className}`, '::selection')
})
}),
])
addVariant(
'before',
transformAllSelectors(
(selector) => {
return updateAllClasses(selector, (className, { withPseudo }) => {
return withPseudo(`before${config('separator')}${className}`, '::before')
})
},
{
withRule: (rule) => {
let foundContent = false
rule.walkDecls('content', () => {
foundContent = true
})
if (!foundContent) {
rule.prepend(postcss.decl({ prop: 'content', value: '""' }))
}
},
}
)
)
addVariant(
'after',
transformAllSelectors(
(selector) => {
return updateAllClasses(selector, (className, { withPseudo }) => {
return withPseudo(`after${config('separator')}${className}`, '::after')
})
},
{
withRule: (rule) => {
let foundContent = false
rule.walkDecls('content', () => {
foundContent = true
})
if (!foundContent) {
rule.prepend(postcss.decl({ prop: 'content', value: '""' }))
}
},
}
)
)
},
pseudoClassVariants: function ({ config, addVariant }) {
let pseudoVariants = [
// Positional
['first', 'first-child'],
['last', 'last-child'],
['only', 'only-child'],
['odd', 'nth-child(odd)'],
['even', 'nth-child(even)'],
'first-of-type',
'last-of-type',
'only-of-type',
// State
'visited',
'target',
// Forms
'default',
'checked',
'indeterminate',
'placeholder-shown',
'autofill',
'required',
'valid',
'invalid',
'in-range',
'out-of-range',
'read-only',
// Content
'empty',
// Interactive
'focus-within',
'hover',
'focus',
'focus-visible',
'active',
'disabled',
]
for (let variant of pseudoVariants) {
let [variantName, state] = Array.isArray(variant) ? variant : [variant, variant]
addVariant(
variantName,
transformAllClasses((className, { withPseudo }) => {
return withPseudo(`${variantName}${config('separator')}${className}`, `:${state}`)
})
)
}
}
if (items.indexOf(item) === -1 || lowestBefore === -1) {
return items
}
let groupMarker = prefixSelector(config('prefix'), '.group')
for (let variant of pseudoVariants) {
let [variantName, state] = Array.isArray(variant) ? variant : [variant, variant]
let groupVariantName = `group-${variantName}`
items = [...items]
let fromIndex = items.indexOf(item)
let toIndex = lowestBefore
items.splice(fromIndex, 1)
items.splice(toIndex, 0, item)
return items
}
export default function ({ corePlugins: corePluginConfig }) {
let pluginOrder = Object.keys(plugins)
pluginOrder = configurePlugins(corePluginConfig, pluginOrder)
pluginOrder = move(pluginOrder, 'transform', ['translate', 'rotate', 'skew', 'scale'])
pluginOrder = move(pluginOrder, 'filter', [
'blur',
'brightness',
'contrast',
'dropShadow',
'grayscale',
'hueRotate',
'invert',
'saturate',
'sepia',
])
pluginOrder = move(pluginOrder, 'backdropFilter', [
'backdropBlur',
'backdropBrightness',
'backdropContrast',
'backdropGrayscale',
'backdropHueRotate',
'backdropInvert',
'backdropOpacity',
'backdropSaturate',
'backdropSepia',
])
return pluginOrder.map((pluginName) => {
return plugins[pluginName]()
})
addVariant(
groupVariantName,
transformAllSelectors((selector) => {
let variantSelector = updateAllClasses(selector, (className) => {
if (`.${className}` === groupMarker) return className
return `${groupVariantName}${config('separator')}${className}`
})
if (variantSelector === selector) {
return null
}
return applyPseudoToMarker(
variantSelector,
groupMarker,
state,
(marker, selector) => `${marker} ${selector}`
)
})
)
}
let peerMarker = prefixSelector(config('prefix'), '.peer')
for (let variant of pseudoVariants) {
let [variantName, state] = Array.isArray(variant) ? variant : [variant, variant]
let peerVariantName = `peer-${variantName}`
addVariant(
peerVariantName,
transformAllSelectors((selector) => {
let variantSelector = updateAllClasses(selector, (className) => {
if (`.${className}` === peerMarker) return className
return `${peerVariantName}${config('separator')}${className}`
})
if (variantSelector === selector) {
return null
}
return applyPseudoToMarker(variantSelector, peerMarker, state, (marker, selector) =>
selector.trim().startsWith('~') ? `${marker}${selector}` : `${marker} ~ ${selector}`
)
})
)
}
},
directionVariants: function ({ config, addVariant }) {
addVariant(
'ltr',
transformAllSelectors(
(selector) =>
`[dir="ltr"] ${updateAllClasses(
selector,
(className) => `ltr${config('separator')}${className}`
)}`
)
)
addVariant(
'rtl',
transformAllSelectors(
(selector) =>
`[dir="rtl"] ${updateAllClasses(
selector,
(className) => `rtl${config('separator')}${className}`
)}`
)
)
},
reducedMotionVariants: function ({ config, addVariant }) {
addVariant(
'motion-safe',
transformLastClasses(
(className) => {
return `motion-safe${config('separator')}${className}`
},
{
wrap: () =>
postcss.atRule({
name: 'media',
params: '(prefers-reduced-motion: no-preference)',
}),
}
)
)
addVariant(
'motion-reduce',
transformLastClasses(
(className) => {
return `motion-reduce${config('separator')}${className}`
},
{
wrap: () =>
postcss.atRule({
name: 'media',
params: '(prefers-reduced-motion: reduce)',
}),
}
)
)
},
darkVariants: function ({ config, addVariant }) {
let mode = config('darkMode', 'media')
if (mode === false) {
mode = 'media'
log.warn([
'`darkMode` is set to `false` in your config.',
'This will behave just like the `media` value.',
])
}
if (mode === 'class') {
addVariant(
'dark',
transformAllSelectors((selector) => {
let variantSelector = updateLastClasses(selector, (className) => {
return `dark${config('separator')}${className}`
})
if (variantSelector === selector) {
return null
}
let darkSelector = prefixSelector(config('prefix'), `.dark`)
return `${darkSelector} ${variantSelector}`
})
)
} else if (mode === 'media') {
addVariant(
'dark',
transformLastClasses(
(className) => {
return `dark${config('separator')}${className}`
},
{
wrap: () =>
postcss.atRule({
name: 'media',
params: '(prefers-color-scheme: dark)',
}),
}
)
)
}
},
screenVariants: function ({ config, theme, addVariant }) {
for (let screen in theme('screens')) {
let size = theme('screens')[screen]
let query = buildMediaQuery(size)
addVariant(
screen,
transformLastClasses(
(className) => {
return `${screen}${config('separator')}${className}`
},
{ wrap: () => postcss.atRule({ name: 'media', params: query }) }
)
)
}
},
...Object.fromEntries(
Object.entries(corePlugins).map(([pluginName, plugin]) => {
return [pluginName, plugin()]
})
),
}

View File

@ -1,98 +1,33 @@
import path from 'path'
import fs from 'fs'
import _ from 'lodash'
import getModuleDependencies from './lib/getModuleDependencies'
import registerConfigAsDependency from './lib/registerConfigAsDependency'
import setupTrackingContext from './lib/setupTrackingContext'
import setupWatchingContext from './lib/setupWatchingContext'
import processTailwindFeatures from './processTailwindFeatures'
import formatCSS from './lib/formatCSS'
import resolveConfig from './util/resolveConfig'
import getAllConfigs from './util/getAllConfigs'
import { supportedConfigFiles } from './constants'
import defaultConfig from '../stubs/defaultConfig.stub.js'
import jitPlugins from './jit'
function resolveConfigPath(filePath) {
// require('tailwindcss')({ theme: ..., variants: ... })
if (_.isObject(filePath) && !_.has(filePath, 'config') && !_.isEmpty(filePath)) {
return undefined
}
// require('tailwindcss')({ config: 'custom-config.js' })
if (_.isObject(filePath) && _.has(filePath, 'config') && _.isString(filePath.config)) {
return path.resolve(filePath.config)
}
// require('tailwindcss')({ config: { theme: ..., variants: ... } })
if (_.isObject(filePath) && _.has(filePath, 'config') && _.isObject(filePath.config)) {
return undefined
}
// require('tailwindcss')('custom-config.js')
if (_.isString(filePath)) {
return path.resolve(filePath)
}
// require('tailwindcss')
for (const configFile of supportedConfigFiles) {
try {
const configPath = path.resolve(configFile)
fs.accessSync(configPath)
return configPath
} catch (err) {}
}
return undefined
}
const getConfigFunction = (config) => () => {
if (_.isUndefined(config)) {
return resolveConfig([
...getAllConfigs(defaultConfig),
{ corePlugins: { caretColor: false, content: false } },
])
}
// Skip this if Jest is running: https://github.com/facebook/jest/pull/9841#issuecomment-621417584
if (process.env.JEST_WORKER_ID === undefined) {
if (!_.isObject(config)) {
getModuleDependencies(config).forEach((mdl) => {
delete require.cache[require.resolve(mdl.file)]
})
}
}
const configObject = _.isObject(config) ? _.get(config, 'config', config) : require(config)
return resolveConfig([
...getAllConfigs(configObject),
{ corePlugins: { caretColor: false, content: false } },
])
}
module.exports = function tailwindcss(config) {
const resolvedConfigPath = resolveConfigPath(config)
const getConfig = getConfigFunction(resolvedConfigPath || config)
const mode = _.get(getConfig(), 'mode', 'aot')
if (mode === 'jit') {
return {
postcssPlugin: 'tailwindcss',
plugins: jitPlugins(config),
}
}
const plugins = []
if (!_.isUndefined(resolvedConfigPath)) {
plugins.push(registerConfigAsDependency(resolvedConfigPath))
}
import { env } from './lib/sharedState'
module.exports = function tailwindcss(configOrPath) {
return {
postcssPlugin: 'tailwindcss',
plugins: [...plugins, processTailwindFeatures(getConfig), formatCSS],
plugins: [
env.DEBUG &&
function (root) {
console.log('\n')
console.time('JIT TOTAL')
return root
},
function (root, result) {
let setupContext =
env.TAILWIND_MODE === 'watch'
? setupWatchingContext(configOrPath)
: setupTrackingContext(configOrPath)
processTailwindFeatures(setupContext)(root, result)
},
env.DEBUG &&
function (root) {
console.timeEnd('JIT TOTAL')
console.log('\n')
return root
},
].filter(Boolean),
}
}

View File

@ -1,87 +0,0 @@
import path from 'path'
import fs from 'fs'
import _ from 'lodash'
import postcss from 'postcss'
import getModuleDependencies from './lib/getModuleDependencies'
import registerConfigAsDependency from './lib/registerConfigAsDependency'
import processTailwindFeatures from './processTailwindFeatures'
import formatCSS from './lib/formatCSS'
import resolveConfig from './util/resolveConfig'
import getAllConfigs from './util/getAllConfigs'
import { supportedConfigFiles } from './constants'
import defaultConfig from '../stubs/defaultConfig.stub.js'
import jitPlugins from './jit'
function resolveConfigPath(filePath) {
// require('tailwindcss')({ theme: ..., variants: ... })
if (_.isObject(filePath) && !_.has(filePath, 'config') && !_.isEmpty(filePath)) {
return undefined
}
// require('tailwindcss')({ config: 'custom-config.js' })
if (_.isObject(filePath) && _.has(filePath, 'config') && _.isString(filePath.config)) {
return path.resolve(filePath.config)
}
// require('tailwindcss')({ config: { theme: ..., variants: ... } })
if (_.isObject(filePath) && _.has(filePath, 'config') && _.isObject(filePath.config)) {
return undefined
}
// require('tailwindcss')('custom-config.js')
if (_.isString(filePath)) {
return path.resolve(filePath)
}
// require('tailwindcss')
for (const configFile of supportedConfigFiles) {
try {
const configPath = path.resolve(configFile)
fs.accessSync(configPath)
return configPath
} catch (err) {}
}
return undefined
}
const getConfigFunction = (config) => () => {
if (_.isUndefined(config)) {
return resolveConfig([...getAllConfigs(defaultConfig)])
}
// Skip this if Jest is running: https://github.com/facebook/jest/pull/9841#issuecomment-621417584
if (process.env.JEST_WORKER_ID === undefined) {
if (!_.isObject(config)) {
getModuleDependencies(config).forEach((mdl) => {
delete require.cache[require.resolve(mdl.file)]
})
}
}
const configObject = _.isObject(config) ? _.get(config, 'config', config) : require(config)
return resolveConfig([...getAllConfigs(configObject)])
}
const plugin = postcss.plugin('tailwindcss', (config) => {
const resolvedConfigPath = resolveConfigPath(config)
const getConfig = getConfigFunction(resolvedConfigPath || config)
const mode = _.get(getConfig(), 'mode', 'aot')
if (mode === 'jit') {
return postcss(jitPlugins(config))
}
const plugins = []
if (!_.isUndefined(resolvedConfigPath)) {
plugins.push(registerConfigAsDependency(resolvedConfigPath))
}
return postcss([...plugins, processTailwindFeatures(getConfig), formatCSS])
})
module.exports = plugin

View File

@ -1,325 +0,0 @@
import postcss from 'postcss'
import * as corePlugins from '../plugins'
import buildMediaQuery from '../util/buildMediaQuery'
import prefixSelector from '../util/prefixSelector'
import {
applyPseudoToMarker,
updateLastClasses,
updateAllClasses,
transformAllSelectors,
transformAllClasses,
transformLastClasses,
} from '../util/pluginUtils'
export default {
pseudoElementVariants: function ({ config, addVariant }) {
addVariant(
'first-letter',
transformAllSelectors((selector) => {
return updateAllClasses(selector, (className, { withPseudo }) => {
return withPseudo(`first-letter${config('separator')}${className}`, '::first-letter')
})
})
)
addVariant(
'first-line',
transformAllSelectors((selector) => {
return updateAllClasses(selector, (className, { withPseudo }) => {
return withPseudo(`first-line${config('separator')}${className}`, '::first-line')
})
})
)
addVariant('marker', [
transformAllSelectors((selector) => {
let variantSelector = updateAllClasses(selector, (className) => {
return `marker${config('separator')}${className}`
})
return `${variantSelector} *::marker`
}),
transformAllSelectors((selector) => {
return updateAllClasses(selector, (className, { withPseudo }) => {
return withPseudo(`marker${config('separator')}${className}`, '::marker')
})
}),
])
addVariant('selection', [
transformAllSelectors((selector) => {
let variantSelector = updateAllClasses(selector, (className) => {
return `selection${config('separator')}${className}`
})
return `${variantSelector} *::selection`
}),
transformAllSelectors((selector) => {
return updateAllClasses(selector, (className, { withPseudo }) => {
return withPseudo(`selection${config('separator')}${className}`, '::selection')
})
}),
])
addVariant(
'before',
transformAllSelectors(
(selector) => {
return updateAllClasses(selector, (className, { withPseudo }) => {
return withPseudo(`before${config('separator')}${className}`, '::before')
})
},
{
withRule: (rule) => {
let foundContent = false
rule.walkDecls('content', () => {
foundContent = true
})
if (!foundContent) {
rule.prepend(postcss.decl({ prop: 'content', value: '""' }))
}
},
}
)
)
addVariant(
'after',
transformAllSelectors(
(selector) => {
return updateAllClasses(selector, (className, { withPseudo }) => {
return withPseudo(`after${config('separator')}${className}`, '::after')
})
},
{
withRule: (rule) => {
let foundContent = false
rule.walkDecls('content', () => {
foundContent = true
})
if (!foundContent) {
rule.prepend(postcss.decl({ prop: 'content', value: '""' }))
}
},
}
)
)
},
pseudoClassVariants: function ({ config, addVariant }) {
let pseudoVariants = [
// Positional
['first', 'first-child'],
['last', 'last-child'],
['only', 'only-child'],
['odd', 'nth-child(odd)'],
['even', 'nth-child(even)'],
'first-of-type',
'last-of-type',
'only-of-type',
// State
'visited',
'target',
// Forms
'default',
'checked',
'indeterminate',
'placeholder-shown',
'autofill',
'required',
'valid',
'invalid',
'in-range',
'out-of-range',
'read-only',
// Content
'empty',
// Interactive
'focus-within',
'hover',
'focus',
'focus-visible',
'active',
'disabled',
]
for (let variant of pseudoVariants) {
let [variantName, state] = Array.isArray(variant) ? variant : [variant, variant]
addVariant(
variantName,
transformAllClasses((className, { withPseudo }) => {
return withPseudo(`${variantName}${config('separator')}${className}`, `:${state}`)
})
)
}
let groupMarker = prefixSelector(config('prefix'), '.group')
for (let variant of pseudoVariants) {
let [variantName, state] = Array.isArray(variant) ? variant : [variant, variant]
let groupVariantName = `group-${variantName}`
addVariant(
groupVariantName,
transformAllSelectors((selector) => {
let variantSelector = updateAllClasses(selector, (className) => {
if (`.${className}` === groupMarker) return className
return `${groupVariantName}${config('separator')}${className}`
})
if (variantSelector === selector) {
return null
}
return applyPseudoToMarker(
variantSelector,
groupMarker,
state,
(marker, selector) => `${marker} ${selector}`
)
})
)
}
let peerMarker = prefixSelector(config('prefix'), '.peer')
for (let variant of pseudoVariants) {
let [variantName, state] = Array.isArray(variant) ? variant : [variant, variant]
let peerVariantName = `peer-${variantName}`
addVariant(
peerVariantName,
transformAllSelectors((selector) => {
let variantSelector = updateAllClasses(selector, (className) => {
if (`.${className}` === peerMarker) return className
return `${peerVariantName}${config('separator')}${className}`
})
if (variantSelector === selector) {
return null
}
return applyPseudoToMarker(variantSelector, peerMarker, state, (marker, selector) =>
selector.trim().startsWith('~') ? `${marker}${selector}` : `${marker} ~ ${selector}`
)
})
)
}
},
directionVariants: function ({ config, addVariant }) {
addVariant(
'ltr',
transformAllSelectors(
(selector) =>
`[dir="ltr"] ${updateAllClasses(
selector,
(className) => `ltr${config('separator')}${className}`
)}`
)
)
addVariant(
'rtl',
transformAllSelectors(
(selector) =>
`[dir="rtl"] ${updateAllClasses(
selector,
(className) => `rtl${config('separator')}${className}`
)}`
)
)
},
reducedMotionVariants: function ({ config, addVariant }) {
addVariant(
'motion-safe',
transformLastClasses(
(className) => {
return `motion-safe${config('separator')}${className}`
},
{
wrap: () =>
postcss.atRule({
name: 'media',
params: '(prefers-reduced-motion: no-preference)',
}),
}
)
)
addVariant(
'motion-reduce',
transformLastClasses(
(className) => {
return `motion-reduce${config('separator')}${className}`
},
{
wrap: () =>
postcss.atRule({
name: 'media',
params: '(prefers-reduced-motion: reduce)',
}),
}
)
)
},
darkVariants: function ({ config, addVariant }) {
if (config('darkMode') === 'class') {
addVariant(
'dark',
transformAllSelectors((selector) => {
let variantSelector = updateLastClasses(selector, (className) => {
return `dark${config('separator')}${className}`
})
if (variantSelector === selector) {
return null
}
let darkSelector = prefixSelector(config('prefix'), `.dark`)
return `${darkSelector} ${variantSelector}`
})
)
} else if (config('darkMode') === 'media') {
addVariant(
'dark',
transformLastClasses(
(className) => {
return `dark${config('separator')}${className}`
},
{
wrap: () =>
postcss.atRule({
name: 'media',
params: '(prefers-color-scheme: dark)',
}),
}
)
)
}
},
screenVariants: function ({ config, theme, addVariant }) {
for (let screen in theme('screens')) {
let size = theme('screens')[screen]
let query = buildMediaQuery(size)
addVariant(
screen,
transformLastClasses(
(className) => {
return `${screen}${config('separator')}${className}`
},
{ wrap: () => postcss.atRule({ name: 'media', params: query }) }
)
)
}
},
...Object.fromEntries(
Object.entries(corePlugins).map(([pluginName, plugin]) => {
return [pluginName, plugin()]
})
),
}

View File

@ -1,29 +0,0 @@
import setupTrackingContext from './lib/setupTrackingContext'
import setupWatchingContext from './lib/setupWatchingContext'
import { env } from './lib/sharedState'
import processTailwindFeatures from './processTailwindFeatures'
export default function (configOrPath = {}) {
return [
env.DEBUG &&
function (root) {
console.log('\n')
console.time('JIT TOTAL')
return root
},
function (root, result) {
let setupContext =
env.TAILWIND_MODE === 'watch'
? setupWatchingContext(configOrPath)
: setupTrackingContext(configOrPath)
processTailwindFeatures(setupContext)(root, result)
},
env.DEBUG &&
function (root) {
console.timeEnd('JIT TOTAL')
console.log('\n')
return root
},
].filter(Boolean)
}

View File

@ -1,52 +0,0 @@
import normalizeTailwindDirectives from './lib/normalizeTailwindDirectives'
import expandTailwindAtRules from './lib/expandTailwindAtRules'
import expandApplyAtRules from './lib/expandApplyAtRules'
import evaluateTailwindFunctions from '../lib/evaluateTailwindFunctions'
import substituteScreenAtRules from '../lib/substituteScreenAtRules'
import resolveDefaultsAtRules from './lib/resolveDefaultsAtRules'
import collapseAdjacentRules from './lib/collapseAdjacentRules'
import { createContext } from './lib/setupContextUtils'
import log from '../util/log'
let warned = false
export default function processTailwindFeatures(setupContext) {
return function (root, result) {
if (!warned) {
log.warn([
`You have enabled the JIT engine which is currently in preview.`,
'Preview features are not covered by semver, may introduce breaking changes, and can change at any time.',
])
warned = true
}
let tailwindDirectives = normalizeTailwindDirectives(root)
let context = setupContext({
tailwindDirectives,
registerDependency(dependency) {
result.messages.push({
plugin: 'tailwindcss',
parent: result.opts.from,
...dependency,
})
},
createContext(tailwindConfig, changedContent) {
return createContext(tailwindConfig, changedContent, tailwindDirectives, root)
},
})(root, result)
if (context.tailwindConfig.separator === '-') {
throw new Error(
"The '-' character cannot be used as a custom separator in JIT mode due to parsing ambiguity. Please use another character like '_' instead."
)
}
expandTailwindAtRules(context)(root, result)
expandApplyAtRules(context)(root, result)
evaluateTailwindFunctions(context)(root, result)
substituteScreenAtRules(context)(root, result)
resolveDefaultsAtRules(context)(root, result)
collapseAdjacentRules(context)(root, result)
}
}

View File

@ -1,19 +0,0 @@
export default function applyImportantConfiguration(_config) {
return function (css) {
css.walkRules((rule) => {
const important = rule.__tailwind ? rule.__tailwind.important : false
if (!important) {
return
}
if (typeof important === 'string') {
rule.selectors = rule.selectors.map((selector) => {
return `${rule.__tailwind.important} ${selector}`
})
} else {
rule.walkDecls((decl) => (decl.important = true))
}
})
}
}

View File

@ -1,18 +0,0 @@
import postcss from 'postcss'
export default function convertLayerAtRulesToControlComments() {
return function (css) {
css.walkAtRules('layer', (atRule) => {
const layer = atRule.params
if (!['base', 'components', 'utilities'].includes(layer)) {
return
}
atRule.before(postcss.comment({ text: `tailwind start ${layer}` }))
atRule.before(atRule.nodes)
atRule.before(postcss.comment({ text: `tailwind end ${layer}` }))
atRule.remove()
})
}
}

View File

@ -1,7 +1,7 @@
import postcss from 'postcss'
import { resolveMatches } from './generateRules'
import bigSign from '../../util/bigSign'
import escapeClassName from '../../util/escapeClassName'
import bigSign from '../util/bigSign'
import escapeClassName from '../util/escapeClassName'
function buildApplyCache(applyCandidates, context) {
for (let candidate of applyCandidates) {

View File

@ -1,7 +1,7 @@
import * as sharedState from './sharedState'
import { generateRules } from './generateRules'
import bigSign from '../../util/bigSign'
import cloneNodes from '../../util/cloneNodes'
import bigSign from '../util/bigSign'
import cloneNodes from '../util/cloneNodes'
let env = sharedState.env
let contentMatchCache = sharedState.contentMatchCache
@ -30,19 +30,18 @@ const builtInTransformers = {
}
function getExtractor(tailwindConfig, fileExtension) {
let extractors = (tailwindConfig && tailwindConfig.purge && tailwindConfig.purge.extract) || {}
const purgeOptions =
(tailwindConfig && tailwindConfig.purge && tailwindConfig.purge.options) || {}
let extractors = tailwindConfig.content.extract
let contentOptions = tailwindConfig.content.options
if (typeof extractors === 'function') {
extractors = {
DEFAULT: extractors,
}
}
if (purgeOptions.defaultExtractor) {
extractors.DEFAULT = purgeOptions.defaultExtractor
if (contentOptions.defaultExtractor) {
extractors.DEFAULT = contentOptions.defaultExtractor
}
for (let { extensions, extractor } of purgeOptions.extractors || []) {
for (let { extensions, extractor } of contentOptions.extractors || []) {
for (let extension of extensions) {
extractors[extension] = extractor
}
@ -57,8 +56,7 @@ function getExtractor(tailwindConfig, fileExtension) {
}
function getTransformer(tailwindConfig, fileExtension) {
let transformers =
(tailwindConfig && tailwindConfig.purge && tailwindConfig.purge.transform) || {}
let transformers = tailwindConfig.content.transform
if (typeof transformers === 'function') {
transformers = {

View File

@ -1,17 +0,0 @@
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)
})
}
export default function formatNodes(root) {
indentRecursive(root)
if (root.first) {
root.first.raws.before = ''
}
}

View File

@ -1,9 +1,9 @@
import postcss from 'postcss'
import selectorParser from 'postcss-selector-parser'
import parseObjectStyles from '../../util/parseObjectStyles'
import isPlainObject from '../../util/isPlainObject'
import prefixSelector from '../../util/prefixSelector'
import { updateAllClasses } from '../../util/pluginUtils'
import parseObjectStyles from '../util/parseObjectStyles'
import isPlainObject from '../util/isPlainObject'
import prefixSelector from '../util/prefixSelector'
import { updateAllClasses } from '../util/pluginUtils'
let classNameParser = selectorParser((selectors) => {
return selectors.first.filter(({ type }) => type === 'class').pop().value

View File

@ -1,3 +1,5 @@
import log from '../util/log'
export default function normalizeTailwindDirectives(root) {
let tailwindDirectives = new Set()
let layerDirectives = new Set()
@ -38,6 +40,11 @@ export default function normalizeTailwindDirectives(root) {
}
if (['layer', 'responsive', 'variants'].includes(atRule.name)) {
if (['responsive', 'variants'].includes(atRule.name)) {
log.warn([
`'@${atRule.name}' is deprecated, use '@layer utilities' or '@layer components' instead.`,
])
}
layerDirectives.add(atRule)
}
})

View File

@ -1,223 +0,0 @@
import _ from 'lodash'
import postcss from 'postcss'
import PurgeCSS, { defaultOptions, standardizeSafelist, mergeExtractorSelectors } from 'purgecss'
import log from '../util/log'
import htmlTags from 'html-tags'
import path from 'path'
import parseDependency from '../util/parseDependency'
import normalizePath from 'normalize-path'
function removeTailwindMarkers(css) {
css.walkAtRules('tailwind', (rule) => rule.remove())
css.walkComments((comment) => {
switch (comment.text.trim()) {
case 'tailwind start base':
case 'tailwind end base':
case 'tailwind start components':
case 'tailwind start utilities':
case 'tailwind end components':
case 'tailwind end utilities':
comment.remove()
break
default:
break
}
})
}
export function tailwindExtractor(content) {
// Capture as liberally as possible, including things like `h-(screen-1.5)`
const broadMatches = content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || []
const broadMatchesWithoutTrailingSlash = broadMatches.map((match) => _.trimEnd(match, '\\'))
// Capture classes within other delimiters like .block(class="w-1/2") in Pug
const innerMatches = content.match(/[^<>"'`\s.(){}[\]#=%]*[^<>"'`\s.(){}[\]#=%:]/g) || []
return broadMatches.concat(broadMatchesWithoutTrailingSlash).concat(innerMatches)
}
function getTransformer(config, fileExtension) {
let transformers = (config.purge && config.purge.transform) || {}
if (typeof transformers === 'function') {
transformers = {
DEFAULT: transformers,
}
}
return transformers[fileExtension] || transformers.DEFAULT || ((content) => content)
}
export default function purgeUnusedUtilities(config, configChanged, registerDependency) {
const purgeEnabled = _.get(
config,
'purge.enabled',
config.purge !== false && config.purge !== undefined && process.env.NODE_ENV === 'production'
)
if (!purgeEnabled) {
return removeTailwindMarkers
}
// Skip if `purge: []` since that's part of the default config
if (Array.isArray(config.purge) && config.purge.length === 0) {
if (configChanged) {
log.warn([
'Tailwind is not purging unused styles because no template paths have been provided.',
'If you have manually configured PurgeCSS outside of Tailwind or are deliberately not removing unused styles, set `purge: false` in your Tailwind config file to silence this warning.',
'https://tailwindcss.com/docs/controlling-file-size/#removing-unused-css',
])
}
return removeTailwindMarkers
}
const extractors = config.purge.extract || {}
const transformers = config.purge.transform || {}
let { defaultExtractor: originalDefaultExtractor, ...purgeOptions } = config.purge.options || {}
if (config.purge?.safelist && !purgeOptions.hasOwnProperty('safelist')) {
purgeOptions.safelist = config.purge.safelist
}
if (!originalDefaultExtractor) {
originalDefaultExtractor =
typeof extractors === 'function' ? extractors : extractors.DEFAULT || tailwindExtractor
}
const defaultExtractor = (content) => {
const preserved = originalDefaultExtractor(content)
if (_.get(config, 'purge.preserveHtmlElements', true)) {
preserved.push(...htmlTags)
}
return preserved
}
// If `extractors` is a function then we don't have any file-specific extractors,
// only a default one.
let fileSpecificExtractors = typeof extractors === 'function' ? {} : extractors
// PurgeCSS doesn't support "transformers," so we implement those using extractors.
// If we have a custom transformer for an extension, but not a matching extractor,
// then we need to create an extractor that we can augment later.
if (typeof transformers !== 'function') {
for (let [extension] of Object.entries(transformers)) {
if (!fileSpecificExtractors[extension]) {
fileSpecificExtractors[extension] = defaultExtractor
}
}
}
// Augment file-specific extractors by running the transformer before we extract classes.
fileSpecificExtractors = Object.entries(fileSpecificExtractors).map(([extension, extractor]) => {
return {
extensions: [extension],
extractor: (content) => {
const transformer = getTransformer(config, extension)
return extractor(transformer(content))
},
}
})
let content = (
Array.isArray(config.purge) ? config.purge : config.purge.content || purgeOptions.content || []
).map((item) => {
if (typeof item === 'string') {
return normalizePath(path.resolve(item))
}
return item
})
for (let fileOrGlob of content.filter((item) => typeof item === 'string')) {
registerDependency(parseDependency(fileOrGlob))
}
let hasLayers = false
const mode = _.get(config, 'purge.mode', 'layers')
return postcss([
function (css) {
if (!['all', 'layers'].includes(mode)) {
throw new Error('Purge `mode` must be one of `layers` or `all`.')
}
if (mode === 'all') {
return
}
const layers = _.get(config, 'purge.layers', ['base', 'components', 'utilities'])
css.walkComments((comment) => {
switch (comment.text.trim()) {
case `purgecss start ignore`:
comment.before(postcss.comment({ text: 'purgecss end ignore' }))
break
case `purgecss end ignore`:
comment.before(postcss.comment({ text: 'purgecss end ignore' }))
comment.text = 'purgecss start ignore'
break
default:
break
}
layers.forEach((layer) => {
switch (comment.text.trim()) {
case `tailwind start ${layer}`:
comment.text = 'purgecss end ignore'
hasLayers = true
break
case `tailwind end ${layer}`:
comment.text = 'purgecss start ignore'
break
default:
break
}
})
})
css.prepend(postcss.comment({ text: 'purgecss start ignore' }))
css.append(postcss.comment({ text: 'purgecss end ignore' }))
},
removeTailwindMarkers,
async function (css) {
if (mode === 'layers' && !hasLayers) {
return
}
const purgeCSS = new PurgeCSS()
purgeCSS.options = {
...defaultOptions,
defaultExtractor: (content) => {
const transformer = getTransformer(config)
return defaultExtractor(transformer(content))
},
extractors: fileSpecificExtractors,
...purgeOptions,
safelist: standardizeSafelist(purgeOptions.safelist),
}
if (purgeCSS.options.variables) {
purgeCSS.variablesStructure.safelist = purgeCSS.options.safelist.variables || []
}
const fileFormatContents = content.filter((o) => typeof o === 'string')
const rawFormatContents = content.filter((o) => typeof o === 'object')
const cssFileSelectors = await purgeCSS.extractSelectorsFromFiles(
fileFormatContents,
purgeCSS.options.extractors
)
const cssRawSelectors = await purgeCSS.extractSelectorsFromString(
rawFormatContents,
purgeCSS.options.extractors
)
const cssSelectors = mergeExtractorSelectors(cssFileSelectors, cssRawSelectors)
purgeCSS.walkThroughCSS(css, cssSelectors)
if (purgeCSS.options.fontFace) purgeCSS.removeUnusedFontFaces()
if (purgeCSS.options.keyframes) purgeCSS.removeUnusedKeyframes()
if (purgeCSS.options.variables) purgeCSS.removeUnusedCSSVariables()
},
])
}

View File

@ -1,18 +0,0 @@
import fs from 'fs'
import getModuleDependencies from './getModuleDependencies'
export default function (configFile) {
if (!fs.existsSync(configFile)) {
throw new Error(`Specified Tailwind config file "${configFile}" doesn't exist.`)
}
return function (css, opts) {
getModuleDependencies(configFile).forEach((mdl) => {
opts.messages.push({
type: 'dependency',
parent: css.source.input.file,
file: mdl.file,
})
})
}
}

View File

@ -4,14 +4,14 @@ import postcss from 'postcss'
import dlv from 'dlv'
import selectorParser from 'postcss-selector-parser'
import transformThemeValue from '../../util/transformThemeValue'
import parseObjectStyles from '../../util/parseObjectStyles'
import prefixSelector from '../../util/prefixSelector'
import isPlainObject from '../../util/isPlainObject'
import escapeClassName from '../../util/escapeClassName'
import nameClass from '../../util/nameClass'
import { coerceValue } from '../../util/pluginUtils'
import bigSign from '../../util/bigSign'
import transformThemeValue from '../util/transformThemeValue'
import parseObjectStyles from '../util/parseObjectStyles'
import prefixSelector from '../util/prefixSelector'
import isPlainObject from '../util/isPlainObject'
import escapeClassName from '../util/escapeClassName'
import nameClass from '../util/nameClass'
import { coerceValue } from '../util/pluginUtils'
import bigSign from '../util/bigSign'
import corePlugins from '../corePlugins'
import * as sharedState from './sharedState'
import { env } from './sharedState'

View File

@ -5,17 +5,17 @@ import fastGlob from 'fast-glob'
import LRU from 'quick-lru'
import normalizePath from 'normalize-path'
import hash from '../../util/hashConfig'
import getModuleDependencies from '../../lib/getModuleDependencies'
import hash from '../util/hashConfig'
import getModuleDependencies from '../lib/getModuleDependencies'
import resolveConfig from '../../../resolveConfig'
import resolveConfig from '../../resolveConfig'
import resolveConfigPath from '../../util/resolveConfigPath'
import resolveConfigPath from '../util/resolveConfigPath'
import { env } from './sharedState'
import { getContext, getFileModifiedMap } from './setupContextUtils'
import parseDependency from '../../util/parseDependency'
import parseDependency from '../util/parseDependency'
let configPathCache = new LRU({ maxSize: 100 })
@ -26,13 +26,9 @@ function getCandidateFiles(context, tailwindConfig) {
return candidateFilesCache.get(context)
}
let purgeContent = Array.isArray(tailwindConfig.purge)
? tailwindConfig.purge
: tailwindConfig.purge.content
let candidateFiles = purgeContent
let candidateFiles = tailwindConfig.content.content
.filter((item) => typeof item === 'string')
.map((purgePath) => normalizePath(path.resolve(purgePath)))
.map((contentPath) => normalizePath(path.resolve(contentPath)))
return candidateFilesCache.set(context, candidateFiles).get(context)
}
@ -81,29 +77,9 @@ function getTailwindConfig(configOrPath) {
}
function resolvedChangedContent(context, candidateFiles, fileModifiedMap) {
let changedContent = (
Array.isArray(context.tailwindConfig.purge)
? context.tailwindConfig.purge
: context.tailwindConfig.purge.content
)
let changedContent = context.tailwindConfig.content.content
.filter((item) => typeof item.raw === 'string')
.concat(
(context.tailwindConfig.purge?.safelist ?? []).map((content) => {
if (typeof content === 'string') {
return { raw: content, extension: 'html' }
}
if (content instanceof RegExp) {
throw new Error(
"Values inside 'purge.safelist' can only be of type 'string', found 'regex'."
)
}
throw new Error(
`Values inside 'purge.safelist' can only be of type 'string', found '${typeof content}'.`
)
})
)
.concat(context.tailwindConfig.content.safelist)
.map(({ raw, extension }) => ({ content: raw, extension }))
for (let changedFile of resolveChangedFiles(candidateFiles, fileModifiedMap)) {

View File

@ -7,11 +7,11 @@ import fastGlob from 'fast-glob'
import LRU from 'quick-lru'
import normalizePath from 'normalize-path'
import hash from '../../util/hashConfig'
import log from '../../util/log'
import getModuleDependencies from '../../lib/getModuleDependencies'
import resolveConfig from '../../../resolveConfig'
import resolveConfigPath from '../../util/resolveConfigPath'
import hash from '../util/hashConfig'
import log from '../util/log'
import getModuleDependencies from '../lib/getModuleDependencies'
import resolveConfig from '../../resolveConfig'
import resolveConfigPath from '../util/resolveConfigPath'
import { getContext } from './setupContextUtils'
// This is used to trigger rebuilds. Just updating the timestamp
@ -147,13 +147,9 @@ function getCandidateFiles(context, tailwindConfig) {
return candidateFilesCache.get(context)
}
let purgeContent = Array.isArray(tailwindConfig.purge)
? tailwindConfig.purge
: tailwindConfig.purge.content
let candidateFiles = purgeContent
let candidateFiles = tailwindConfig.content.content
.filter((item) => typeof item === 'string')
.map((purgePath) => normalizePath(path.resolve(purgePath)))
.map((contentPath) => normalizePath(path.resolve(contentPath)))
return candidateFilesCache.set(context, candidateFiles).get(context)
}
@ -189,29 +185,9 @@ function getTailwindConfig(configOrPath) {
}
function resolvedChangedContent(context, candidateFiles) {
let changedContent = (
Array.isArray(context.tailwindConfig.purge)
? context.tailwindConfig.purge
: context.tailwindConfig.purge.content
)
let changedContent = context.tailwindConfig.content.content
.filter((item) => typeof item.raw === 'string')
.concat(
(context.tailwindConfig.purge?.safelist ?? []).map((content) => {
if (typeof content === 'string') {
return { raw: content, extension: 'html' }
}
if (content instanceof RegExp) {
throw new Error(
"Values inside 'purge.safelist' can only be of type 'string', found 'regex'."
)
}
throw new Error(
`Values inside 'purge.safelist' can only be of type 'string', found '${typeof content}'.`
)
})
)
.concat(context.tailwindConfig.content.safelist)
.map(({ raw, extension }) => ({ content: raw, extension }))
for (let changedFile of resolveChangedFiles(context, candidateFiles)) {

View File

@ -1,409 +0,0 @@
import _ from 'lodash'
import selectorParser from 'postcss-selector-parser'
import postcss from 'postcss'
import didYouMean from 'didyoumean'
import substituteTailwindAtRules from './substituteTailwindAtRules'
import evaluateTailwindFunctions from './evaluateTailwindFunctions'
import substituteVariantsAtRules from './substituteVariantsAtRules'
import substituteResponsiveAtRules from './substituteResponsiveAtRules'
import convertLayerAtRulesToControlComments from './convertLayerAtRulesToControlComments'
import substituteScreenAtRules from './substituteScreenAtRules'
import prefixSelector from '../util/prefixSelector'
import { useMemo } from '../util/useMemo'
function hasAtRule(css, atRule, condition) {
let found = false
css.walkAtRules(
atRule,
condition === undefined
? () => {
found = true
return false
}
: (node) => {
if (condition(node)) {
found = true
return false
}
}
)
return found
}
function cloneWithoutChildren(node) {
if (node.type === 'atrule') {
return postcss.atRule({ name: node.name, params: node.params })
}
if (node.type === 'rule') {
return postcss.rule({ name: node.name, selectors: node.selectors })
}
const clone = node.clone()
clone.removeAll()
return clone
}
const tailwindApplyPlaceholder = selectorParser.attribute({
attribute: '__TAILWIND-APPLY-PLACEHOLDER__',
})
function generateRulesFromApply({ rule, utilityName: className, classPosition }, replaceWiths) {
const parser = selectorParser((selectors) => {
let i = 0
selectors.walkClasses((c) => {
if (classPosition === i++ && c.value === className) {
c.replaceWith(tailwindApplyPlaceholder)
}
})
})
const processedSelectors = _.flatMap(rule.selectors, (selector) => {
// You could argue we should make this replacement at the AST level, but if we believe
// the placeholder string is safe from collisions then it is safe to do this is a simple
// string replacement, and much, much faster.
return replaceWiths.map((replaceWith) =>
parser.processSync(selector).replace('[__TAILWIND-APPLY-PLACEHOLDER__]', replaceWith)
)
})
const cloned = rule.clone()
let current = cloned
let parent = rule.parent
while (parent && parent.type !== 'root') {
const parentClone = cloneWithoutChildren(parent)
parentClone.append(current)
current.parent = parentClone
current = parentClone
parent = parent.parent
}
cloned.selectors = processedSelectors
return current
}
const extractUtilityNamesParser = selectorParser((selectors) => {
let classes = []
selectors.walkClasses((c) => classes.push(c.value))
return classes
})
const extractUtilityNames = useMemo(
(selector) => extractUtilityNamesParser.transformSync(selector),
(selector) => selector
)
const cloneRuleWithParent = useMemo(
(rule) => rule.clone({ parent: rule.parent }),
(rule) => rule
)
function buildCssUtilityMap(css, startIndex) {
let index = startIndex
const utilityMap = {}
function handle(getRule, rule) {
const utilityNames = extractUtilityNames(rule.selector)
utilityNames.forEach((utilityName, i) => {
if (utilityMap[utilityName] === undefined) {
utilityMap[utilityName] = []
}
utilityMap[utilityName].push({
index,
utilityName,
classPosition: i,
...getRule(rule),
})
index++
})
}
// This is the end user's css. This might contain rules that we want to
// apply. We want immediate copies of everything in case that we have user
// defined classes that are recursively applied. Down below we are modifying
// the rules directly. We could do a better solution where we keep track of a
// dependency tree, but that is a bit more complex. Might revisit later,
// we'll see how this turns out!
css.walkRules(handle.bind(null, (rule) => ({ rule: cloneRuleWithParent(rule) })))
return utilityMap
}
const buildLookupTreeUtilityMap = useMemo(
(lookupTree) => {
let index = 0
const utilityMap = {}
function handle(getRule, rule) {
const utilityNames = extractUtilityNames(rule.selector)
utilityNames.forEach((utilityName, i) => {
if (utilityMap[utilityName] === undefined) {
utilityMap[utilityName] = []
}
utilityMap[utilityName].push({
index,
utilityName,
classPosition: i,
...getRule(rule),
})
index++
})
}
// Lookup tree is the big lookup tree, making the rule lazy allows us to save
// some memory because we don't need everything.
lookupTree.walkRules(
handle.bind(null, (rule) => ({
get rule() {
return cloneRuleWithParent(rule)
},
}))
)
return utilityMap
},
(tree) => tree
)
function mergeAdjacentRules(initialRule, rulesToInsert) {
let previousRule = initialRule
rulesToInsert.forEach((toInsert) => {
if (
toInsert.type === 'rule' &&
previousRule.type === 'rule' &&
toInsert.selector === previousRule.selector
) {
previousRule.append(toInsert.nodes)
} else if (
toInsert.type === 'atrule' &&
previousRule.type === 'atrule' &&
toInsert.params === previousRule.params
) {
const merged = mergeAdjacentRules(
previousRule.nodes[previousRule.nodes.length - 1],
toInsert.nodes
)
previousRule.append(merged)
} else {
previousRule = toInsert
}
toInsert.walk((n) => {
if (n.nodes && n.nodes.length === 0) {
n.remove()
}
})
})
return rulesToInsert.filter((r) => r.nodes.length > 0)
}
function makeExtractUtilityRules(css, lookupTree, config) {
const lookupTreeUtilityMap = buildLookupTreeUtilityMap(lookupTree)
const lookupTreeUtilityMapKeys = Object.keys(lookupTreeUtilityMap)
const utilityMap = buildCssUtilityMap(css, lookupTreeUtilityMapKeys.length)
function getUtility(utilityName) {
const utility = []
if (lookupTreeUtilityMap[utilityName]) {
utility.push(...lookupTreeUtilityMap[utilityName])
}
if (utilityMap[utilityName]) {
utility.push(...utilityMap[utilityName])
}
if (utility.length > 0) return utility
}
return function extractUtilityRules(utilityNames, rule) {
const combined = []
utilityNames.forEach((utilityName) => {
const utility = getUtility(utilityName)
if (utility === undefined) {
// Look for prefixed utility in case the user has goofed
const prefixedUtilityName = prefixSelector(config.prefix, `.${utilityName}`).slice(1)
const prefixedUtility = getUtility(prefixedUtilityName)
if (prefixedUtility !== undefined) {
throw rule.error(
`The \`${utilityName}\` class does not exist, but \`${prefixedUtilityName}\` does. Did you forget the prefix?`
)
}
const suggestedClass = didYouMean(
utilityName,
Object.keys(utilityMap).concat(lookupTreeUtilityMapKeys)
)
const suggestionMessage = suggestedClass ? `, but \`${suggestedClass}\` does` : ''
throw rule.error(
`The \`${utilityName}\` class does not exist${suggestionMessage}. If you're sure that \`${utilityName}\` exists, make sure that any \`@import\` statements are being properly processed before Tailwind CSS sees your CSS, as \`@apply\` can only be used for classes in the same CSS tree.`,
{ word: utilityName }
)
}
combined.push(...utility)
})
return combined.sort((a, b) => a.index - b.index)
}
}
function findParent(rule, predicate) {
let parent = rule.parent
while (parent) {
if (predicate(parent)) {
return parent
}
parent = parent.parent
}
throw new Error('No parent could be found')
}
function processApplyAtRules(css, lookupTree, config) {
const extractUtilityRules = makeExtractUtilityRules(css, lookupTree, config)
do {
css.walkAtRules('apply', (applyRule) => {
const parent = applyRule.parent // Direct parent
const nearestParentRule = findParent(applyRule, (r) => r.type === 'rule')
const currentUtilityNames = extractUtilityNames(nearestParentRule.selector)
const [importantEntries, applyUtilityNames, important = importantEntries.length > 0] =
_.partition(applyRule.params.split(/[\s\t\n]+/g), (n) => n === '!important')
if (_.intersection(applyUtilityNames, currentUtilityNames).length > 0) {
const currentUtilityName = _.intersection(applyUtilityNames, currentUtilityNames)[0]
throw parent.error(
`You cannot \`@apply\` the \`${currentUtilityName}\` utility here because it creates a circular dependency.`
)
}
// Extract any post-apply declarations and re-insert them after apply rules
const afterRule = parent.clone({ raws: {} })
afterRule.nodes = afterRule.nodes.slice(parent.index(applyRule) + 1)
parent.nodes = parent.nodes.slice(0, parent.index(applyRule) + 1)
// Sort applys to match CSS source order
const applys = extractUtilityRules(applyUtilityNames, applyRule)
// Get new rules with the utility portion of the selector replaced with the new selector
const rulesToInsert = []
applys.forEach(
nearestParentRule === parent
? (util) => rulesToInsert.push(generateRulesFromApply(util, parent.selectors))
: (util) => util.rule.nodes.forEach((n) => afterRule.append(n.clone()))
)
rulesToInsert.forEach((rule) => {
if (rule.type === 'atrule') {
rule.walkRules((rule) => {
rule.__tailwind = { ...rule.__tailwind, important }
})
} else {
rule.__tailwind = { ...rule.__tailwind, important }
}
})
const { nodes } = _.tap(postcss.root({ nodes: rulesToInsert }), (root) => {
root.walkDecls((d) => {
d.important = important
})
})
const mergedRules = mergeAdjacentRules(nearestParentRule, [...nodes, afterRule])
applyRule.remove()
parent.after(mergedRules)
// If the base rule has nothing in it (all applys were pseudo or responsive variants),
// remove the rule fuggit.
if (parent.nodes.length === 0) {
parent.remove()
}
})
// We already know that we have at least 1 @apply rule. Otherwise this
// function would not have been called. Therefore we can execute this code
// at least once. This also means that in the best case scenario we only
// call this 2 times, instead of 3 times.
// 1st time -> before we call this function
// 2nd time -> when we check if we have to do this loop again (because do {} while (check))
// .. instead of
// 1st time -> before we call this function
// 2nd time -> when we check the first time (because while (check) do {})
// 3rd time -> when we re-check to see if we should do this loop again
} while (hasAtRule(css, 'apply'))
return css
}
let defaultTailwindTree = new Map()
export default function substituteClassApplyAtRules(config, getProcessedPlugins, configChanged) {
return function (css) {
// We can stop already when we don't have any @apply rules. Vue users: you're welcome!
if (!hasAtRule(css, 'apply')) {
return css
}
let requiredTailwindAtRules = ['base', 'components', 'utilities']
if (
hasAtRule(css, 'tailwind', (node) => {
let idx = requiredTailwindAtRules.indexOf(node.params)
if (idx !== -1) requiredTailwindAtRules.splice(idx, 1)
if (requiredTailwindAtRules.length <= 0) return true
return false
})
) {
// Tree already contains all the at rules (requiredTailwindAtRules)
return processApplyAtRules(css, postcss.root(), config)
}
let lookupKey = requiredTailwindAtRules.join(',')
// We mutated the `requiredTailwindAtRules`, but when we hit this point in
// time, it means that we don't have all the atrules. The missing atrules
// are listed inside the requiredTailwindAtRules, which we can use to fill
// in the missing pieces.
//
// Important for <style> blocks in Vue components.
const generateLookupTree =
configChanged || !defaultTailwindTree.has(lookupKey)
? () => {
return postcss([
substituteTailwindAtRules(config, getProcessedPlugins()),
evaluateTailwindFunctions({ tailwindConfig: config }),
substituteVariantsAtRules(config, getProcessedPlugins()),
substituteResponsiveAtRules(config),
convertLayerAtRulesToControlComments(config),
substituteScreenAtRules({ tailwindConfig: config }),
])
.process(requiredTailwindAtRules.map((rule) => `@tailwind ${rule};`).join('\n'), {
from: __filename,
})
.then((result) => {
defaultTailwindTree.set(lookupKey, result)
return result
})
}
: () => Promise.resolve(defaultTailwindTree.get(lookupKey))
return generateLookupTree().then((result) => {
return processApplyAtRules(css, result.root, config)
})
}
}

View File

@ -1,88 +0,0 @@
import _ from 'lodash'
import postcss from 'postcss'
function updateSource(nodes, source) {
return _.tap(Array.isArray(nodes) ? postcss.root({ nodes }) : nodes, (tree) => {
tree.walk((node) => (node.source = source))
})
}
export default function (
_config,
{ base: pluginBase, components: pluginComponents, utilities: pluginUtilities }
) {
return function (css) {
css.walkAtRules('import', (atRule) => {
if (atRule.params === '"tailwindcss/base"' || atRule.params === "'tailwindcss/base'") {
atRule.name = 'tailwind'
atRule.params = 'base'
}
if (
atRule.params === '"tailwindcss/components"' ||
atRule.params === "'tailwindcss/components'"
) {
atRule.name = 'tailwind'
atRule.params = 'components'
}
if (
atRule.params === '"tailwindcss/utilities"' ||
atRule.params === "'tailwindcss/utilities'"
) {
atRule.name = 'tailwind'
atRule.params = 'utilities'
}
if (atRule.params === '"tailwindcss/screens"' || atRule.params === "'tailwindcss/screens'") {
atRule.name = 'tailwind'
atRule.params = 'screens'
}
})
let includesScreensExplicitly = false
const layers = {
base: [],
components: [],
utilities: [],
}
css.walkAtRules('layer', (atRule) => {
if (!['base', 'components', 'utilities'].includes(atRule.params)) {
return
}
layers[atRule.params].push(atRule)
})
css.walkAtRules('tailwind', (atRule) => {
if (atRule.params === 'preflight') {
// prettier-ignore
throw atRule.error("`@tailwind preflight` is not a valid at-rule in Tailwind v2.0, use `@tailwind base` instead.", { word: 'preflight' })
}
if (atRule.params === 'base') {
atRule.after(layers.base)
atRule.after(updateSource(pluginBase, atRule.source))
}
if (atRule.params === 'components') {
atRule.after(layers.components)
atRule.after(updateSource(pluginComponents, atRule.source))
}
if (atRule.params === 'utilities') {
atRule.after(layers.utilities)
atRule.after(updateSource(pluginUtilities, atRule.source))
}
if (atRule.params === 'screens') {
includesScreensExplicitly = true
}
})
if (!includesScreensExplicitly) {
css.append([postcss.atRule({ name: 'tailwind', params: 'screens' })])
}
}
}

View File

@ -1,201 +0,0 @@
import _ from 'lodash'
import postcss from 'postcss'
import selectorParser from 'postcss-selector-parser'
import generateVariantFunction from '../util/generateVariantFunction'
import prefixSelector from '../util/prefixSelector'
import buildSelectorVariant from '../util/buildSelectorVariant'
function generatePseudoClassVariant(pseudoClass, selectorPrefix = pseudoClass) {
return generateVariantFunction(({ modifySelectors, separator }) => {
const parser = selectorParser((selectors) => {
selectors.walkClasses((sel) => {
sel.value = `${selectorPrefix}${separator}${sel.value}`
sel.parent.insertAfter(sel, selectorParser.pseudo({ value: `:${pseudoClass}` }))
})
})
return modifySelectors(({ selector }) => parser.processSync(selector))
})
}
function ensureIncludesDefault(variants) {
return variants.includes('DEFAULT') ? variants : ['DEFAULT', ...variants]
}
const defaultVariantGenerators = (config) => ({
DEFAULT: generateVariantFunction(() => {}),
dark: generateVariantFunction(
({ container, separator, modifySelectors }) => {
if (config.darkMode === false) {
return postcss.root()
}
if (config.darkMode === 'media') {
const modified = modifySelectors(({ selector }) => {
return buildSelectorVariant(selector, 'dark', separator, (message) => {
throw container.error(message)
})
})
const mediaQuery = postcss.atRule({
name: 'media',
params: '(prefers-color-scheme: dark)',
})
mediaQuery.append(modified)
container.append(mediaQuery)
return container
}
if (config.darkMode === 'class') {
const modified = modifySelectors(({ selector }) => {
return buildSelectorVariant(selector, 'dark', separator, (message) => {
throw container.error(message)
})
})
modified.walkRules((rule) => {
rule.selectors = rule.selectors.map((selector) => {
return `${prefixSelector(config.prefix, '.dark')} ${selector}`
})
})
return modified
}
throw new Error("The `darkMode` config option must be either 'media' or 'class'.")
},
{ unstable_stack: true }
),
'motion-safe': generateVariantFunction(
({ container, separator, modifySelectors }) => {
const modified = modifySelectors(({ selector }) => {
return buildSelectorVariant(selector, 'motion-safe', separator, (message) => {
throw container.error(message)
})
})
const mediaQuery = postcss.atRule({
name: 'media',
params: '(prefers-reduced-motion: no-preference)',
})
mediaQuery.append(modified)
container.append(mediaQuery)
},
{ unstable_stack: true }
),
'motion-reduce': generateVariantFunction(
({ container, separator, modifySelectors }) => {
const modified = modifySelectors(({ selector }) => {
return buildSelectorVariant(selector, 'motion-reduce', separator, (message) => {
throw container.error(message)
})
})
const mediaQuery = postcss.atRule({
name: 'media',
params: '(prefers-reduced-motion: reduce)',
})
mediaQuery.append(modified)
container.append(mediaQuery)
},
{ unstable_stack: true }
),
'group-hover': generateVariantFunction(({ modifySelectors, separator }) => {
const parser = selectorParser((selectors) => {
selectors.walkClasses((sel) => {
sel.value = `group-hover${separator}${sel.value}`
sel.parent.insertBefore(
sel,
selectorParser().astSync(prefixSelector(config.prefix, '.group:hover '))
)
})
})
return modifySelectors(({ selector }) => parser.processSync(selector))
}),
'group-focus': generateVariantFunction(({ modifySelectors, separator }) => {
const parser = selectorParser((selectors) => {
selectors.walkClasses((sel) => {
sel.value = `group-focus${separator}${sel.value}`
sel.parent.insertBefore(
sel,
selectorParser().astSync(prefixSelector(config.prefix, '.group:focus '))
)
})
})
return modifySelectors(({ selector }) => parser.processSync(selector))
}),
hover: generatePseudoClassVariant('hover'),
'focus-within': generatePseudoClassVariant('focus-within'),
'focus-visible': generatePseudoClassVariant('focus-visible'),
'read-only': generatePseudoClassVariant('read-only'),
focus: generatePseudoClassVariant('focus'),
active: generatePseudoClassVariant('active'),
visited: generatePseudoClassVariant('visited'),
disabled: generatePseudoClassVariant('disabled'),
checked: generatePseudoClassVariant('checked'),
first: generatePseudoClassVariant('first-child', 'first'),
last: generatePseudoClassVariant('last-child', 'last'),
odd: generatePseudoClassVariant('nth-child(odd)', 'odd'),
even: generatePseudoClassVariant('nth-child(even)', 'even'),
empty: generatePseudoClassVariant('empty'),
})
function prependStackableVariants(atRule, variants, stackableVariants) {
if (!_.some(variants, (v) => stackableVariants.includes(v))) {
return variants
}
if (_.every(variants, (v) => stackableVariants.includes(v))) {
return variants
}
const variantsParent = postcss.atRule({
name: 'variants',
params: variants.filter((v) => stackableVariants.includes(v)).join(', '),
})
atRule.before(variantsParent)
variantsParent.append(atRule)
variants = _.without(variants, ...stackableVariants)
return variants
}
export default function (config, { variantGenerators: pluginVariantGenerators }) {
return function (css) {
const variantGenerators = {
...defaultVariantGenerators(config),
...pluginVariantGenerators,
}
const stackableVariants = Object.entries(variantGenerators)
.filter(([_variant, { options }]) => options.unstable_stack)
.map(([variant]) => variant)
let variantsFound = false
do {
variantsFound = false
css.walkAtRules('variants', (atRule) => {
variantsFound = true
let variants = postcss.list.comma(atRule.params).filter((variant) => variant !== '')
if (variants.includes('responsive')) {
const responsiveParent = postcss.atRule({ name: 'responsive' })
atRule.before(responsiveParent)
responsiveParent.append(atRule)
}
const remainingVariants = prependStackableVariants(atRule, variants, stackableVariants)
_.forEach(_.without(ensureIncludesDefault(remainingVariants), 'responsive'), (variant) => {
if (!variantGenerators[variant]) {
throw new Error(
`Your config mentions the "${variant}" variant, but "${variant}" doesn't appear to be a variant. Did you forget or misconfigure a plugin that supplies that variant?`
)
}
variantGenerators[variant].handler(atRule, config)
})
atRule.remove()
})
} while (variantsFound)
}
}

View File

@ -4,7 +4,7 @@ export default function () {
return function ({ matchUtilities, theme, variants, prefix }) {
let prefixName = (name) => prefix(`.${name}`).slice(1)
let keyframes = Object.fromEntries(
Object.entries(theme('keyframes') || {}).map(([key, value]) => {
Object.entries(theme('keyframes') ?? {}).map(([key, value]) => {
return [
key,
[

View File

@ -1,16 +1,12 @@
export default function () {
return function ({ config, matchUtilities, theme, variants }) {
return function ({ matchUtilities, theme, variants }) {
matchUtilities(
{
'backdrop-blur': (value) => {
return {
'--tw-backdrop-blur': `blur(${value})`,
...(config('mode') === 'jit'
? {
'@defaults backdrop-filter': {},
'backdrop-filter': 'var(--tw-backdrop-filter)',
}
: {}),
'@defaults backdrop-filter': {},
'backdrop-filter': 'var(--tw-backdrop-filter)',
}
},
},

View File

@ -1,16 +1,12 @@
export default function () {
return function ({ config, matchUtilities, theme, variants }) {
return function ({ matchUtilities, theme, variants }) {
matchUtilities(
{
'backdrop-brightness': (value) => {
return {
'--tw-backdrop-brightness': `brightness(${value})`,
...(config('mode') === 'jit'
? {
'@defaults backdrop-filter': {},
'backdrop-filter': 'var(--tw-backdrop-filter)',
}
: {}),
'@defaults backdrop-filter': {},
'backdrop-filter': 'var(--tw-backdrop-filter)',
}
},
},

View File

@ -1,16 +1,12 @@
export default function () {
return function ({ config, matchUtilities, theme, variants }) {
return function ({ matchUtilities, theme, variants }) {
matchUtilities(
{
'backdrop-contrast': (value) => {
return {
'--tw-backdrop-contrast': `contrast(${value})`,
...(config('mode') === 'jit'
? {
'@defaults backdrop-filter': {},
'backdrop-filter': 'var(--tw-backdrop-filter)',
}
: {}),
'@defaults backdrop-filter': {},
'backdrop-filter': 'var(--tw-backdrop-filter)',
}
},
},

View File

@ -1,69 +1,38 @@
export default function () {
return function ({ config, addBase, addUtilities, variants }) {
if (config('mode') === 'jit') {
addBase({
'@defaults backdrop-filter': {
'--tw-backdrop-blur': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-brightness': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-contrast': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-grayscale': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-hue-rotate': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-invert': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-opacity': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-saturate': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-sepia': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-filter': [
'var(--tw-backdrop-blur)',
'var(--tw-backdrop-brightness)',
'var(--tw-backdrop-contrast)',
'var(--tw-backdrop-grayscale)',
'var(--tw-backdrop-hue-rotate)',
'var(--tw-backdrop-invert)',
'var(--tw-backdrop-opacity)',
'var(--tw-backdrop-saturate)',
'var(--tw-backdrop-sepia)',
].join(' '),
return function ({ addBase, addUtilities, variants }) {
addBase({
'@defaults backdrop-filter': {
'--tw-backdrop-blur': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-brightness': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-contrast': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-grayscale': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-hue-rotate': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-invert': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-opacity': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-saturate': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-sepia': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-filter': [
'var(--tw-backdrop-blur)',
'var(--tw-backdrop-brightness)',
'var(--tw-backdrop-contrast)',
'var(--tw-backdrop-grayscale)',
'var(--tw-backdrop-hue-rotate)',
'var(--tw-backdrop-invert)',
'var(--tw-backdrop-opacity)',
'var(--tw-backdrop-saturate)',
'var(--tw-backdrop-sepia)',
].join(' '),
},
})
addUtilities(
{
'.backdrop-filter': {
'@defaults backdrop-filter': {},
'backdrop-filter': 'var(--tw-backdrop-filter)',
},
})
addUtilities(
{
'.backdrop-filter': {
'@defaults backdrop-filter': {},
'backdrop-filter': 'var(--tw-backdrop-filter)',
},
'.backdrop-filter-none': { 'backdrop-filter': 'none' },
},
variants('backdropFilter')
)
} else {
addUtilities(
{
'.backdrop-filter': {
'--tw-backdrop-blur': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-brightness': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-contrast': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-grayscale': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-hue-rotate': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-invert': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-opacity': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-saturate': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-backdrop-sepia': 'var(--tw-empty,/*!*/ /*!*/)',
'backdrop-filter': [
'var(--tw-backdrop-blur)',
'var(--tw-backdrop-brightness)',
'var(--tw-backdrop-contrast)',
'var(--tw-backdrop-grayscale)',
'var(--tw-backdrop-hue-rotate)',
'var(--tw-backdrop-invert)',
'var(--tw-backdrop-opacity)',
'var(--tw-backdrop-saturate)',
'var(--tw-backdrop-sepia)',
].join(' '),
},
'.backdrop-filter-none': { 'backdrop-filter': 'none' },
},
variants('backdropFilter')
)
}
'.backdrop-filter-none': { 'backdrop-filter': 'none' },
},
variants('backdropFilter')
)
}
}

View File

@ -1,16 +1,12 @@
export default function () {
return function ({ config, matchUtilities, theme, variants }) {
return function ({ matchUtilities, theme, variants }) {
matchUtilities(
{
'backdrop-grayscale': (value) => {
return {
'--tw-backdrop-grayscale': `grayscale(${value})`,
...(config('mode') === 'jit'
? {
'@defaults backdrop-filter': {},
'backdrop-filter': 'var(--tw-backdrop-filter)',
}
: {}),
'@defaults backdrop-filter': {},
'backdrop-filter': 'var(--tw-backdrop-filter)',
}
},
},

View File

@ -1,16 +1,12 @@
export default function () {
return function ({ config, matchUtilities, theme, variants }) {
return function ({ matchUtilities, theme, variants }) {
matchUtilities(
{
'backdrop-hue-rotate': (value) => {
return {
'--tw-backdrop-hue-rotate': `hue-rotate(${value})`,
...(config('mode') === 'jit'
? {
'@defaults backdrop-filter': {},
'backdrop-filter': 'var(--tw-backdrop-filter)',
}
: {}),
'@defaults backdrop-filter': {},
'backdrop-filter': 'var(--tw-backdrop-filter)',
}
},
},

View File

@ -1,16 +1,12 @@
export default function () {
return function ({ config, matchUtilities, theme, variants }) {
return function ({ matchUtilities, theme, variants }) {
matchUtilities(
{
'backdrop-invert': (value) => {
return {
'--tw-backdrop-invert': `invert(${value})`,
...(config('mode') === 'jit'
? {
'@defaults backdrop-filter': {},
'backdrop-filter': 'var(--tw-backdrop-filter)',
}
: {}),
'@defaults backdrop-filter': {},
'backdrop-filter': 'var(--tw-backdrop-filter)',
}
},
},

View File

@ -1,16 +1,12 @@
export default function () {
return function ({ config, matchUtilities, theme, variants }) {
return function ({ matchUtilities, theme, variants }) {
matchUtilities(
{
'backdrop-opacity': (value) => {
return {
'--tw-backdrop-opacity': `opacity(${value})`,
...(config('mode') === 'jit'
? {
'@defaults backdrop-filter': {},
'backdrop-filter': 'var(--tw-backdrop-filter)',
}
: {}),
'@defaults backdrop-filter': {},
'backdrop-filter': 'var(--tw-backdrop-filter)',
}
},
},

View File

@ -1,16 +1,12 @@
export default function () {
return function ({ config, matchUtilities, theme, variants }) {
return function ({ matchUtilities, theme, variants }) {
matchUtilities(
{
'backdrop-saturate': (value) => {
return {
'--tw-backdrop-saturate': `saturate(${value})`,
...(config('mode') === 'jit'
? {
'@defaults backdrop-filter': {},
'backdrop-filter': 'var(--tw-backdrop-filter)',
}
: {}),
'@defaults backdrop-filter': {},
'backdrop-filter': 'var(--tw-backdrop-filter)',
}
},
},

View File

@ -1,16 +1,12 @@
export default function () {
return function ({ config, matchUtilities, theme, variants }) {
return function ({ matchUtilities, theme, variants }) {
matchUtilities(
{
'backdrop-sepia': (value) => {
return {
'--tw-backdrop-sepia': `sepia(${value})`,
...(config('mode') === 'jit'
? {
'@defaults backdrop-filter': {},
'backdrop-filter': 'var(--tw-backdrop-filter)',
}
: {}),
'@defaults backdrop-filter': {},
'backdrop-filter': 'var(--tw-backdrop-filter)',
}
},
},

View File

@ -1,16 +1,12 @@
export default function () {
return function ({ config, matchUtilities, theme, variants }) {
return function ({ matchUtilities, theme, variants }) {
matchUtilities(
{
blur: (value) => {
return {
'--tw-blur': `blur(${value})`,
...(config('mode') === 'jit'
? {
'@defaults filter': {},
filter: 'var(--tw-filter)',
}
: {}),
'@defaults filter': {},
filter: 'var(--tw-filter)',
}
},
},

View File

@ -2,39 +2,21 @@ import flattenColorPalette from '../util/flattenColorPalette'
import withAlphaVariable from '../util/withAlphaVariable'
export default function () {
return function ({ config, addBase, matchUtilities, theme, variants, corePlugins }) {
if (config('mode') === 'jit') {
if (!corePlugins('borderOpacity')) {
addBase({
'@defaults border-width': {
'border-color': theme('borderColor.DEFAULT', 'currentColor'),
},
})
} else {
addBase({
'@defaults border-width': withAlphaVariable({
color: theme('borderColor.DEFAULT', 'currentColor'),
property: 'border-color',
variable: '--tw-border-opacity',
}),
})
}
return function ({ addBase, matchUtilities, theme, variants, corePlugins }) {
if (!corePlugins('borderOpacity')) {
addBase({
'@defaults border-width': {
'border-color': theme('borderColor.DEFAULT', 'currentColor'),
},
})
} else {
if (!corePlugins('borderOpacity')) {
addBase({
'*, ::before, ::after': {
'border-color': theme('borderColor.DEFAULT', 'currentColor'),
},
})
} else {
addBase({
'*, ::before, ::after': withAlphaVariable({
color: theme('borderColor.DEFAULT', 'currentColor'),
property: 'border-color',
variable: '--tw-border-opacity',
}),
})
}
addBase({
'@defaults border-width': withAlphaVariable({
color: theme('borderColor.DEFAULT', 'currentColor'),
property: 'border-color',
variable: '--tw-border-opacity',
}),
})
}
matchUtilities(
@ -60,70 +42,66 @@ export default function () {
}
)
if (config('mode') === 'jit') {
matchUtilities(
{
'border-t': (value) => {
if (!corePlugins('borderOpacity')) {
return {
'border-top-color': value,
}
matchUtilities(
{
'border-t': (value) => {
if (!corePlugins('borderOpacity')) {
return {
'border-top-color': value,
}
}
return withAlphaVariable({
color: value,
property: 'border-top-color',
variable: '--tw-border-opacity',
})
},
'border-r': (value) => {
if (!corePlugins('borderOpacity')) {
return {
'border-right-color': value,
}
}
return withAlphaVariable({
color: value,
property: 'border-right-color',
variable: '--tw-border-opacity',
})
},
'border-b': (value) => {
if (!corePlugins('borderOpacity')) {
return {
'border-bottom-color': value,
}
}
return withAlphaVariable({
color: value,
property: 'border-bottom-color',
variable: '--tw-border-opacity',
})
},
'border-l': (value) => {
if (!corePlugins('borderOpacity')) {
return {
'border-left-color': value,
}
}
return withAlphaVariable({
color: value,
property: 'border-left-color',
variable: '--tw-border-opacity',
})
},
return withAlphaVariable({
color: value,
property: 'border-top-color',
variable: '--tw-border-opacity',
})
},
{
values: (({ DEFAULT: _, ...colors }) => colors)(
flattenColorPalette(theme('borderColor'))
),
variants: variants('borderColor'),
type: 'color',
}
)
}
'border-r': (value) => {
if (!corePlugins('borderOpacity')) {
return {
'border-right-color': value,
}
}
return withAlphaVariable({
color: value,
property: 'border-right-color',
variable: '--tw-border-opacity',
})
},
'border-b': (value) => {
if (!corePlugins('borderOpacity')) {
return {
'border-bottom-color': value,
}
}
return withAlphaVariable({
color: value,
property: 'border-bottom-color',
variable: '--tw-border-opacity',
})
},
'border-l': (value) => {
if (!corePlugins('borderOpacity')) {
return {
'border-left-color': value,
}
}
return withAlphaVariable({
color: value,
property: 'border-left-color',
variable: '--tw-border-opacity',
})
},
},
{
values: (({ DEFAULT: _, ...colors }) => colors)(flattenColorPalette(theme('borderColor'))),
variants: variants('borderColor'),
type: 'color',
}
)
}
}

View File

@ -2,35 +2,17 @@ import { asLength } from '../util/pluginUtils'
import createUtilityPlugin from '../util/createUtilityPlugin'
export default function () {
return function (helpers) {
if (helpers.config('mode') === 'jit') {
createUtilityPlugin(
'borderWidth',
[
['border', [['@defaults border-width', {}], 'border-width']],
[
['border-t', [['@defaults border-width', {}], 'border-top-width']],
['border-r', [['@defaults border-width', {}], 'border-right-width']],
['border-b', [['@defaults border-width', {}], 'border-bottom-width']],
['border-l', [['@defaults border-width', {}], 'border-left-width']],
],
],
{ resolveArbitraryValue: asLength }
)(helpers)
} else {
createUtilityPlugin(
'borderWidth',
[
['border', ['border-width']],
[
['border-t', ['border-top-width']],
['border-r', ['border-right-width']],
['border-b', ['border-bottom-width']],
['border-l', ['border-left-width']],
],
],
{ resolveArbitraryValue: asLength }
)(helpers)
}
}
return createUtilityPlugin(
'borderWidth',
[
['border', [['@defaults border-width', {}], 'border-width']],
[
['border-t', [['@defaults border-width', {}], 'border-top-width']],
['border-r', [['@defaults border-width', {}], 'border-right-width']],
['border-b', [['@defaults border-width', {}], 'border-bottom-width']],
['border-l', [['@defaults border-width', {}], 'border-left-width']],
],
],
{ resolveArbitraryValue: asLength }
)
}

View File

@ -8,25 +8,14 @@ let defaultBoxShadow = [
].join(', ')
export default function () {
return function ({ config, matchUtilities, addBase, addUtilities, theme, variants }) {
if (config('mode') === 'jit') {
addBase({
'@defaults box-shadow': {
'--tw-ring-offset-shadow': '0 0 #0000',
'--tw-ring-shadow': '0 0 #0000',
'--tw-shadow': '0 0 #0000',
},
})
} else {
addUtilities(
{
'*, ::before, ::after': {
'--tw-shadow': '0 0 #0000',
},
},
{ respectImportant: false }
)
}
return function ({ matchUtilities, addBase, theme, variants }) {
addBase({
'@defaults box-shadow': {
'--tw-ring-offset-shadow': '0 0 #0000',
'--tw-ring-shadow': '0 0 #0000',
'--tw-shadow': '0 0 #0000',
},
})
matchUtilities(
{
@ -34,7 +23,7 @@ export default function () {
value = transformValue(value)
return {
...(config('mode') === 'jit' ? { '@defaults box-shadow': {} } : {}),
'@defaults box-shadow': {},
'--tw-shadow': value === 'none' ? '0 0 #0000' : value,
'box-shadow': defaultBoxShadow,
}

View File

@ -1,16 +1,12 @@
export default function () {
return function ({ config, matchUtilities, theme, variants }) {
return function ({ matchUtilities, theme, variants }) {
matchUtilities(
{
brightness: (value) => {
return {
'--tw-brightness': `brightness(${value})`,
...(config('mode') === 'jit'
? {
'@defaults filter': {},
filter: 'var(--tw-filter)',
}
: {}),
'@defaults filter': {},
filter: 'var(--tw-filter)',
}
},
},

View File

@ -1,16 +1,12 @@
export default function () {
return function ({ config, matchUtilities, theme, variants }) {
return function ({ matchUtilities, theme, variants }) {
matchUtilities(
{
contrast: (value) => {
return {
'--tw-contrast': `contrast(${value})`,
...(config('mode') === 'jit'
? {
'@defaults filter': {},
filter: 'var(--tw-filter)',
}
: {}),
'@defaults filter': {},
filter: 'var(--tw-filter)',
}
},
},

View File

@ -2,7 +2,7 @@ import _ from 'lodash'
import nameClass from '../util/nameClass'
export default function () {
return function ({ config, addUtilities, theme, variants }) {
return function ({ addUtilities, theme, variants }) {
const utilities = _.fromPairs(
_.map(theme('dropShadow'), (value, modifier) => {
return [
@ -11,12 +11,8 @@ export default function () {
'--tw-drop-shadow': Array.isArray(value)
? value.map((v) => `drop-shadow(${v})`).join(' ')
: `drop-shadow(${value})`,
...(config('mode') === 'jit'
? {
'@defaults filter': {},
filter: 'var(--tw-filter)',
}
: {}),
'@defaults filter': {},
filter: 'var(--tw-filter)',
},
]
})

View File

@ -1,66 +1,35 @@
export default function () {
return function ({ config, addBase, addUtilities, variants }) {
if (config('mode') === 'jit') {
addBase({
'@defaults filter': {
'--tw-blur': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-brightness': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-contrast': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-grayscale': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-hue-rotate': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-invert': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-saturate': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-sepia': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-drop-shadow': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-filter': [
'var(--tw-blur)',
'var(--tw-brightness)',
'var(--tw-contrast)',
'var(--tw-grayscale)',
'var(--tw-hue-rotate)',
'var(--tw-invert)',
'var(--tw-saturate)',
'var(--tw-sepia)',
'var(--tw-drop-shadow)',
].join(' '),
},
})
addUtilities(
{
'.filter': { '@defaults filter': {}, filter: 'var(--tw-filter)' },
'.filter-none': { filter: 'none' },
},
variants('filter')
)
} else {
addUtilities(
{
'.filter': {
'--tw-blur': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-brightness': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-contrast': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-grayscale': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-hue-rotate': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-invert': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-saturate': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-sepia': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-drop-shadow': 'var(--tw-empty,/*!*/ /*!*/)',
filter: [
'var(--tw-blur)',
'var(--tw-brightness)',
'var(--tw-contrast)',
'var(--tw-grayscale)',
'var(--tw-hue-rotate)',
'var(--tw-invert)',
'var(--tw-saturate)',
'var(--tw-sepia)',
'var(--tw-drop-shadow)',
].join(' '),
},
'.filter-none': { filter: 'none' },
},
variants('filter')
)
}
return function ({ addBase, addUtilities, variants }) {
addBase({
'@defaults filter': {
'--tw-blur': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-brightness': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-contrast': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-grayscale': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-hue-rotate': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-invert': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-saturate': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-sepia': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-drop-shadow': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-filter': [
'var(--tw-blur)',
'var(--tw-brightness)',
'var(--tw-contrast)',
'var(--tw-grayscale)',
'var(--tw-hue-rotate)',
'var(--tw-invert)',
'var(--tw-saturate)',
'var(--tw-sepia)',
'var(--tw-drop-shadow)',
].join(' '),
},
})
addUtilities(
{
'.filter': { '@defaults filter': {}, filter: 'var(--tw-filter)' },
'.filter-none': { filter: 'none' },
},
variants('filter')
)
}
}

View File

@ -1,16 +1,12 @@
export default function () {
return function ({ config, matchUtilities, theme, variants }) {
return function ({ matchUtilities, theme, variants }) {
matchUtilities(
{
grayscale: (value) => {
return {
'--tw-grayscale': `grayscale(${value})`,
...(config('mode') === 'jit'
? {
'@defaults filter': {},
filter: 'var(--tw-filter)',
}
: {}),
'@defaults filter': {},
filter: 'var(--tw-filter)',
}
},
},

View File

@ -1,16 +1,12 @@
export default function () {
return function ({ config, matchUtilities, theme, variants }) {
return function ({ matchUtilities, theme, variants }) {
matchUtilities(
{
'hue-rotate': (value) => {
return {
'--tw-hue-rotate': `hue-rotate(${value})`,
...(config('mode') === 'jit'
? {
'@defaults filter': {},
filter: 'var(--tw-filter)',
}
: {}),
'@defaults filter': {},
filter: 'var(--tw-filter)',
}
},
},

View File

@ -1,16 +1,12 @@
export default function () {
return function ({ config, matchUtilities, theme, variants }) {
return function ({ matchUtilities, theme, variants }) {
matchUtilities(
{
invert: (value) => {
return {
'--tw-invert': `invert(${value})`,
...(config('mode') === 'jit'
? {
'@defaults filter': {},
filter: 'var(--tw-filter)',
}
: {}),
'@defaults filter': {},
filter: 'var(--tw-filter)',
}
},
},

View File

@ -1,7 +1,7 @@
import { withAlphaValue } from '../util/withAlphaVariable'
export default function () {
return function ({ config, matchUtilities, addBase, addUtilities, theme, variants }) {
return function ({ matchUtilities, addBase, addUtilities, theme, variants }) {
let ringOpacityDefault = theme('ringOpacity.DEFAULT', '0.5')
let ringColorDefault = withAlphaValue(
theme('ringColor.DEFAULT'),
@ -9,39 +9,23 @@ export default function () {
`rgba(147, 197, 253, ${ringOpacityDefault})`
)
if (config('mode') === 'jit') {
addBase({
'@defaults ring-width': {
'--tw-ring-inset': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-ring-offset-width': theme('ringOffsetWidth.DEFAULT', '0px'),
'--tw-ring-offset-color': theme('ringOffsetColor.DEFAULT', '#fff'),
'--tw-ring-color': ringColorDefault,
'--tw-ring-offset-shadow': '0 0 #0000',
'--tw-ring-shadow': '0 0 #0000',
'--tw-shadow': '0 0 #0000',
},
})
} else {
addUtilities(
{
'*, ::before, ::after': {
'--tw-ring-inset': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-ring-offset-width': theme('ringOffsetWidth.DEFAULT', '0px'),
'--tw-ring-offset-color': theme('ringOffsetColor.DEFAULT', '#fff'),
'--tw-ring-color': ringColorDefault,
'--tw-ring-offset-shadow': '0 0 #0000',
'--tw-ring-shadow': '0 0 #0000',
},
},
{ respectImportant: false }
)
}
addBase({
'@defaults ring-width': {
'--tw-ring-inset': 'var(--tw-empty,/*!*/ /*!*/)',
'--tw-ring-offset-width': theme('ringOffsetWidth.DEFAULT', '0px'),
'--tw-ring-offset-color': theme('ringOffsetColor.DEFAULT', '#fff'),
'--tw-ring-color': ringColorDefault,
'--tw-ring-offset-shadow': '0 0 #0000',
'--tw-ring-shadow': '0 0 #0000',
'--tw-shadow': '0 0 #0000',
},
})
matchUtilities(
{
ring: (value) => {
return {
...(config('mode') === 'jit' ? { '@defaults ring-width': {} } : {}),
'@defaults ring-width': {},
'--tw-ring-offset-shadow': `var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)`,
'--tw-ring-shadow': `var(--tw-ring-inset) 0 0 0 calc(${value} + var(--tw-ring-offset-width)) var(--tw-ring-color)`,
'box-shadow': [
@ -62,7 +46,7 @@ export default function () {
addUtilities(
{
'.ring-inset': {
...(config('mode') === 'jit' ? { '@defaults ring-width': {} } : {}),
'@defaults ring-width': {},
'--tw-ring-inset': 'inset',
},
},

View File

@ -1,16 +1,7 @@
import createUtilityPlugin from '../util/createUtilityPlugin'
export default function () {
return function ({ config, ...rest }) {
if (config('mode') === 'jit') {
return createUtilityPlugin('rotate', [
[
'rotate',
[['@defaults transform', {}], '--tw-rotate', ['transform', 'var(--tw-transform)']],
],
])({ config, ...rest })
} else {
return createUtilityPlugin('rotate', [['rotate', ['--tw-rotate']]])({ config, ...rest })
}
}
return createUtilityPlugin('rotate', [
['rotate', [['@defaults transform', {}], '--tw-rotate', ['transform', 'var(--tw-transform)']]],
])
}

View File

@ -1,16 +1,12 @@
export default function () {
return function ({ config, matchUtilities, theme, variants }) {
return function ({ matchUtilities, theme, variants }) {
matchUtilities(
{
saturate: (value) => {
return {
'--tw-saturate': `saturate(${value})`,
...(config('mode') === 'jit'
? {
'@defaults filter': {},
filter: 'var(--tw-filter)',
}
: {}),
'@defaults filter': {},
filter: 'var(--tw-filter)',
}
},
},

View File

@ -1,37 +1,25 @@
import createUtilityPlugin from '../util/createUtilityPlugin'
export default function () {
return function ({ config, ...rest }) {
if (config('mode') === 'jit') {
return createUtilityPlugin('scale', [
[
'scale',
[
['@defaults transform', {}],
'--tw-scale-x',
'--tw-scale-y',
['transform', 'var(--tw-transform)'],
],
],
[
[
'scale-x',
[['@defaults transform', {}], '--tw-scale-x', ['transform', 'var(--tw-transform)']],
],
[
'scale-y',
[['@defaults transform', {}], '--tw-scale-y', ['transform', 'var(--tw-transform)']],
],
],
])({ config, ...rest })
} else {
return createUtilityPlugin('scale', [
['scale', ['--tw-scale-x', '--tw-scale-y']],
[
['scale-x', ['--tw-scale-x']],
['scale-y', ['--tw-scale-y']],
],
])({ config, ...rest })
}
}
return createUtilityPlugin('scale', [
[
'scale',
[
['@defaults transform', {}],
'--tw-scale-x',
'--tw-scale-y',
['transform', 'var(--tw-transform)'],
],
],
[
[
'scale-x',
[['@defaults transform', {}], '--tw-scale-x', ['transform', 'var(--tw-transform)']],
],
[
'scale-y',
[['@defaults transform', {}], '--tw-scale-y', ['transform', 'var(--tw-transform)']],
],
],
])
}

View File

@ -1,16 +1,12 @@
export default function () {
return function ({ config, matchUtilities, theme, variants }) {
return function ({ matchUtilities, theme, variants }) {
matchUtilities(
{
sepia: (value) => {
return {
'--tw-sepia': `sepia(${value})`,
...(config('mode') === 'jit'
? {
'@defaults filter': {},
filter: 'var(--tw-filter)',
}
: {}),
'@defaults filter': {},
filter: 'var(--tw-filter)',
}
},
},

View File

@ -1,27 +1,16 @@
import createUtilityPlugin from '../util/createUtilityPlugin'
export default function () {
return function ({ config, ...rest }) {
if (config('mode') === 'jit') {
return createUtilityPlugin('skew', [
[
[
'skew-x',
[['@defaults transform', {}], '--tw-skew-x', ['transform', 'var(--tw-transform)']],
],
[
'skew-y',
[['@defaults transform', {}], '--tw-skew-y', ['transform', 'var(--tw-transform)']],
],
],
])({ config, ...rest })
} else {
return createUtilityPlugin('skew', [
[
['skew-x', ['--tw-skew-x']],
['skew-y', ['--tw-skew-y']],
],
])({ config, ...rest })
}
}
return createUtilityPlugin('skew', [
[
[
'skew-x',
[['@defaults transform', {}], '--tw-skew-x', ['transform', 'var(--tw-transform)']],
],
[
'skew-y',
[['@defaults transform', {}], '--tw-skew-y', ['transform', 'var(--tw-transform)']],
],
],
])
}

View File

@ -1,15 +1,32 @@
export default function () {
return function ({ config, addBase, addUtilities, variants }) {
if (config('mode') === 'jit') {
addBase({
'@defaults transform': {
'--tw-translate-x': '0',
'--tw-translate-y': '0',
'--tw-rotate': '0',
'--tw-skew-x': '0',
'--tw-skew-y': '0',
'--tw-scale-x': '1',
'--tw-scale-y': '1',
return function ({ addBase, addUtilities, variants }) {
addBase({
'@defaults transform': {
'--tw-translate-x': '0',
'--tw-translate-y': '0',
'--tw-rotate': '0',
'--tw-skew-x': '0',
'--tw-skew-y': '0',
'--tw-scale-x': '1',
'--tw-scale-y': '1',
'--tw-transform': [
'translateX(var(--tw-translate-x))',
'translateY(var(--tw-translate-y))',
'rotate(var(--tw-rotate))',
'skewX(var(--tw-skew-x))',
'skewY(var(--tw-skew-y))',
'scaleX(var(--tw-scale-x))',
'scaleY(var(--tw-scale-y))',
].join(' '),
},
})
addUtilities(
{
'.transform': {
'@defaults transform': {},
transform: 'var(--tw-transform)',
},
'.transform-cpu': {
'--tw-transform': [
'translateX(var(--tw-translate-x))',
'translateY(var(--tw-translate-y))',
@ -20,80 +37,19 @@ export default function () {
'scaleY(var(--tw-scale-y))',
].join(' '),
},
})
addUtilities(
{
'.transform': {
'@defaults transform': {},
transform: 'var(--tw-transform)',
},
'.transform-cpu': {
'--tw-transform': [
'translateX(var(--tw-translate-x))',
'translateY(var(--tw-translate-y))',
'rotate(var(--tw-rotate))',
'skewX(var(--tw-skew-x))',
'skewY(var(--tw-skew-y))',
'scaleX(var(--tw-scale-x))',
'scaleY(var(--tw-scale-y))',
].join(' '),
},
'.transform-gpu': {
'--tw-transform': [
'translate3d(var(--tw-translate-x), var(--tw-translate-y), 0)',
'rotate(var(--tw-rotate))',
'skewX(var(--tw-skew-x))',
'skewY(var(--tw-skew-y))',
'scaleX(var(--tw-scale-x))',
'scaleY(var(--tw-scale-y))',
].join(' '),
},
'.transform-none': { transform: 'none' },
'.transform-gpu': {
'--tw-transform': [
'translate3d(var(--tw-translate-x), var(--tw-translate-y), 0)',
'rotate(var(--tw-rotate))',
'skewX(var(--tw-skew-x))',
'skewY(var(--tw-skew-y))',
'scaleX(var(--tw-scale-x))',
'scaleY(var(--tw-scale-y))',
].join(' '),
},
variants('transform')
)
} else {
addUtilities(
{
'.transform': {
'--tw-translate-x': '0',
'--tw-translate-y': '0',
'--tw-rotate': '0',
'--tw-skew-x': '0',
'--tw-skew-y': '0',
'--tw-scale-x': '1',
'--tw-scale-y': '1',
transform: [
'translateX(var(--tw-translate-x))',
'translateY(var(--tw-translate-y))',
'rotate(var(--tw-rotate))',
'skewX(var(--tw-skew-x))',
'skewY(var(--tw-skew-y))',
'scaleX(var(--tw-scale-x))',
'scaleY(var(--tw-scale-y))',
].join(' '),
},
'.transform-gpu': {
'--tw-translate-x': '0',
'--tw-translate-y': '0',
'--tw-rotate': '0',
'--tw-skew-x': '0',
'--tw-skew-y': '0',
'--tw-scale-x': '1',
'--tw-scale-y': '1',
transform: [
'translate3d(var(--tw-translate-x), var(--tw-translate-y), 0)',
'rotate(var(--tw-rotate))',
'skewX(var(--tw-skew-x))',
'skewY(var(--tw-skew-y))',
'scaleX(var(--tw-scale-x))',
'scaleY(var(--tw-scale-y))',
].join(' '),
},
'.transform-none': { transform: 'none' },
},
variants('transform')
)
}
'.transform-none': { transform: 'none' },
},
variants('transform')
)
}
}

View File

@ -1,27 +1,16 @@
import createUtilityPlugin from '../util/createUtilityPlugin'
export default function () {
return function ({ config, ...rest }) {
if (config('mode') === 'jit') {
return createUtilityPlugin('translate', [
[
[
'translate-x',
[['@defaults transform', {}], '--tw-translate-x', ['transform', 'var(--tw-transform)']],
],
[
'translate-y',
[['@defaults transform', {}], '--tw-translate-y', ['transform', 'var(--tw-transform)']],
],
],
])({ config, ...rest })
} else {
return createUtilityPlugin('translate', [
[
['translate-x', ['--tw-translate-x']],
['translate-y', ['--tw-translate-y']],
],
])({ config, ...rest })
}
}
return createUtilityPlugin('translate', [
[
[
'translate-x',
[['@defaults transform', {}], '--tw-translate-x', ['transform', 'var(--tw-transform)']],
],
[
'translate-y',
[['@defaults transform', {}], '--tw-translate-y', ['transform', 'var(--tw-transform)']],
],
],
])
}

View File

@ -1,79 +1,41 @@
import _ from 'lodash'
import postcss from 'postcss'
import substituteTailwindAtRules from './lib/substituteTailwindAtRules'
import normalizeTailwindDirectives from './lib/normalizeTailwindDirectives'
import expandTailwindAtRules from './lib/expandTailwindAtRules'
import expandApplyAtRules from './lib/expandApplyAtRules'
import evaluateTailwindFunctions from './lib/evaluateTailwindFunctions'
import substituteVariantsAtRules from './lib/substituteVariantsAtRules'
import substituteResponsiveAtRules from './lib/substituteResponsiveAtRules'
import convertLayerAtRulesToControlComments from './lib/convertLayerAtRulesToControlComments'
import substituteScreenAtRules from './lib/substituteScreenAtRules'
import substituteClassApplyAtRules from './lib/substituteClassApplyAtRules'
import applyImportantConfiguration from './lib/applyImportantConfiguration'
import purgeUnusedStyles from './lib/purgeUnusedStyles'
import resolveDefaultsAtRules from './lib/resolveDefaultsAtRules'
import collapseAdjacentRules from './lib/collapseAdjacentRules'
import { createContext } from './lib/setupContextUtils'
import corePlugins from './corePlugins'
import processPlugins from './util/processPlugins'
import cloneNodes from './util/cloneNodes'
import { issueFlagNotices } from './featureFlags.js'
export default function processTailwindFeatures(setupContext) {
return function (root, result) {
let tailwindDirectives = normalizeTailwindDirectives(root)
import hash from './util/hashConfig'
import log from './util/log'
import { shared } from './util/disposables'
let context = setupContext({
tailwindDirectives,
registerDependency(dependency) {
result.messages.push({
plugin: 'tailwindcss',
parent: result.opts.from,
...dependency,
})
},
createContext(tailwindConfig, changedContent) {
return createContext(tailwindConfig, changedContent, tailwindDirectives, root)
},
})(root, result)
let previousConfig = null
let processedPlugins = null
let getProcessedPlugins = null
export default function (getConfig) {
return function (css, result) {
const config = getConfig()
const configChanged = hash(previousConfig) !== hash(config)
previousConfig = config
if (configChanged) {
shared.dispose()
if (config.target) {
log.warn([
'The `target` feature has been removed in Tailwind CSS v2.0.',
'Please remove this option from your config file to silence this warning.',
])
}
issueFlagNotices(config)
processedPlugins = processPlugins(
[...corePlugins(config), ..._.get(config, 'plugins', [])],
config
if (context.tailwindConfig.separator === '-') {
throw new Error(
"The '-' character cannot be used as a custom separator in JIT mode due to parsing ambiguity. Please use another character like '_' instead."
)
getProcessedPlugins = function () {
return {
...processedPlugins,
base: cloneNodes(processedPlugins.base),
components: cloneNodes(processedPlugins.components),
utilities: cloneNodes(processedPlugins.utilities),
}
}
}
function registerDependency(dependency) {
result.messages.push({
plugin: 'tailwindcss',
parent: result.opts.from,
...dependency,
})
}
return postcss([
substituteTailwindAtRules(config, getProcessedPlugins()),
evaluateTailwindFunctions({ tailwindConfig: config }),
substituteVariantsAtRules(config, getProcessedPlugins()),
substituteResponsiveAtRules(config),
convertLayerAtRulesToControlComments(config),
substituteScreenAtRules({ tailwindConfig: config }),
substituteClassApplyAtRules(config, getProcessedPlugins, configChanged),
applyImportantConfiguration(config),
purgeUnusedStyles(config, configChanged, registerDependency),
]).process(css, { from: _.get(css, 'source.input.file') })
expandTailwindAtRules(context)(root, result)
expandApplyAtRules(context)(root, result)
evaluateTailwindFunctions(context)(root, result)
substituteScreenAtRules(context)(root, result)
resolveDefaultsAtRules(context)(root, result)
collapseAdjacentRules(context)(root, result)
}
}

View File

@ -36,7 +36,7 @@ export default function createUtilityPlugin(
{
values: filterDefault
? Object.fromEntries(
Object.entries(theme(themeKey) || {}).filter(([modifier]) => modifier !== 'DEFAULT')
Object.entries(theme(themeKey) ?? {}).filter(([modifier]) => modifier !== 'DEFAULT')
)
: theme(themeKey),
variants: variants(themeKey),

View File

@ -1,7 +1,7 @@
const flattenColorPalette = (colors) =>
Object.assign(
{},
...Object.entries(colors || {}).flatMap(([color, values]) =>
...Object.entries(colors ?? {}).flatMap(([color, values]) =>
typeof values == 'object'
? Object.entries(flattenColorPalette(values)).map(([number, hex]) => ({
[color + (number === 'DEFAULT' ? '' : `-${number}`)]: hex,

View File

@ -14,6 +14,7 @@ import corePluginList from '../corePluginList'
import configurePlugins from './configurePlugins'
import defaultConfig from '../../stubs/defaultConfig.stub'
import colors from '../../colors'
import log from './log'
const configUtils = {
colors,
@ -257,30 +258,86 @@ function resolvePluginLists(pluginLists) {
}
export default function resolveConfig(configs) {
const allConfigs = [
let allConfigs = [
...extractPluginConfigs(configs),
{
darkMode: false,
content: [],
prefix: '',
important: false,
separator: ':',
variantOrder: defaultConfig.variantOrder,
},
]
const { variantOrder } = allConfigs.find((c) => c.variantOrder)
let { variantOrder } = allConfigs.find((c) => c.variantOrder)
return defaults(
{
theme: resolveFunctionKeys(
mergeExtensions(mergeThemes(map(allConfigs, (t) => get(t, 'theme', {}))))
),
variants: resolveVariants(
allConfigs.map((c) => get(c, 'variants', {})),
variantOrder
),
corePlugins: resolveCorePlugins(allConfigs.map((c) => c.corePlugins)),
plugins: resolvePluginLists(configs.map((c) => get(c, 'plugins', []))),
},
...allConfigs
return normalizeConfig(
defaults(
{
theme: resolveFunctionKeys(
mergeExtensions(mergeThemes(map(allConfigs, (t) => get(t, 'theme', {}))))
),
variants: resolveVariants(
allConfigs.map((c) => get(c, 'variants', {})),
variantOrder
),
corePlugins: resolveCorePlugins(allConfigs.map((c) => c.corePlugins)),
plugins: resolvePluginLists(configs.map((c) => get(c, 'plugins', []))),
},
...allConfigs
)
)
}
let warnedAbout = new Set()
function normalizeConfig(config) {
if (!warnedAbout.has('purge-deprecation') && config.hasOwnProperty('purge')) {
log.warn([
'The `purge` option in your tailwind.config.js file has been deprecated.',
'Please rename this to `content` instead.',
])
warnedAbout.add('purge-deprecation')
}
config.content = {
content: (() => {
let { content, purge } = config
if (Array.isArray(content)) return content
if (Array.isArray(content?.content)) return content.content
if (Array.isArray(purge)) return purge
if (Array.isArray(purge?.content)) return purge.content
return []
})(),
safelist: (() => {
let { content, purge } = config
let [safelisKey, safelistPaths] = (() => {
if (Array.isArray(content?.safelist)) return ['content.safelist', content.safelist]
if (Array.isArray(purge?.safelist)) return ['purge.safelist', purge.safelist]
return [null, []]
})()
return safelistPaths.map((content) => {
if (typeof content === 'string') {
return { raw: content, extension: 'html' }
}
if (content instanceof RegExp) {
throw new Error(
`Values inside '${safelistKey}' can only be of type 'string', found 'regex'.`
)
}
throw new Error(
`Values inside '${safelisKey}' can only be of type 'string', found '${typeof content}'.`
)
})
})(),
extract: config.content?.extract || config.purge?.extract || {},
options: config.content?.options || config.purge?.options || {},
transform: config.content?.transform || config.purge?.transform || {},
}
return config
}

View File

@ -1,9 +1,9 @@
const colors = require('../colors')
module.exports = {
purge: [],
content: [],
presets: [],
darkMode: false, // or 'media' or 'class'
darkMode: 'media', // or 'class'
theme: {
screens: {
sm: '640px',
@ -822,150 +822,5 @@ module.exports = {
'active',
'disabled',
],
variants: {
accessibility: ['responsive', 'focus-within', 'focus'],
alignContent: ['responsive'],
alignItems: ['responsive'],
alignSelf: ['responsive'],
animation: ['responsive'],
appearance: ['responsive'],
backdropBlur: ['responsive'],
backdropBrightness: ['responsive'],
backdropContrast: ['responsive'],
backdropFilter: ['responsive'],
backdropGrayscale: ['responsive'],
backdropHueRotate: ['responsive'],
backdropInvert: ['responsive'],
backdropOpacity: ['responsive'],
backdropSaturate: ['responsive'],
backdropSepia: ['responsive'],
backgroundAttachment: ['responsive'],
backgroundBlendMode: ['responsive'],
backgroundClip: ['responsive'],
backgroundColor: ['responsive', 'dark', 'group-hover', 'focus-within', 'hover', 'focus'],
backgroundImage: ['responsive'],
backgroundOpacity: ['responsive', 'dark', 'group-hover', 'focus-within', 'hover', 'focus'],
backgroundPosition: ['responsive'],
backgroundRepeat: ['responsive'],
backgroundSize: ['responsive'],
backgroundOrigin: ['responsive'],
blur: ['responsive'],
borderCollapse: ['responsive'],
borderColor: ['responsive', 'dark', 'group-hover', 'focus-within', 'hover', 'focus'],
borderOpacity: ['responsive', 'dark', 'group-hover', 'focus-within', 'hover', 'focus'],
borderRadius: ['responsive'],
borderStyle: ['responsive'],
borderWidth: ['responsive'],
boxDecorationBreak: ['responsive'],
boxShadow: ['responsive', 'group-hover', 'focus-within', 'hover', 'focus'],
boxSizing: ['responsive'],
brightness: ['responsive'],
clear: ['responsive'],
container: ['responsive'],
contrast: ['responsive'],
cursor: ['responsive'],
display: ['responsive'],
divideColor: ['responsive', 'dark'],
divideOpacity: ['responsive', 'dark'],
divideStyle: ['responsive'],
divideWidth: ['responsive'],
dropShadow: ['responsive'],
fill: ['responsive'],
filter: ['responsive'],
flex: ['responsive'],
flexDirection: ['responsive'],
flexGrow: ['responsive'],
flexShrink: ['responsive'],
flexWrap: ['responsive'],
float: ['responsive'],
fontFamily: ['responsive'],
fontSize: ['responsive'],
fontSmoothing: ['responsive'],
fontStyle: ['responsive'],
fontVariantNumeric: ['responsive'],
fontWeight: ['responsive'],
gap: ['responsive'],
gradientColorStops: ['responsive', 'dark', 'hover', 'focus'],
grayscale: ['responsive'],
gridAutoColumns: ['responsive'],
gridAutoFlow: ['responsive'],
gridAutoRows: ['responsive'],
gridColumn: ['responsive'],
gridColumnEnd: ['responsive'],
gridColumnStart: ['responsive'],
gridRow: ['responsive'],
gridRowEnd: ['responsive'],
gridRowStart: ['responsive'],
gridTemplateColumns: ['responsive'],
gridTemplateRows: ['responsive'],
height: ['responsive'],
hueRotate: ['responsive'],
inset: ['responsive'],
invert: ['responsive'],
isolation: ['responsive'],
justifyContent: ['responsive'],
justifyItems: ['responsive'],
justifySelf: ['responsive'],
letterSpacing: ['responsive'],
lineHeight: ['responsive'],
listStylePosition: ['responsive'],
listStyleType: ['responsive'],
margin: ['responsive'],
maxHeight: ['responsive'],
maxWidth: ['responsive'],
minHeight: ['responsive'],
minWidth: ['responsive'],
mixBlendMode: ['responsive'],
objectFit: ['responsive'],
objectPosition: ['responsive'],
opacity: ['responsive', 'group-hover', 'focus-within', 'hover', 'focus'],
order: ['responsive'],
outline: ['responsive', 'focus-within', 'focus'],
overflow: ['responsive'],
overscrollBehavior: ['responsive'],
padding: ['responsive'],
placeContent: ['responsive'],
placeItems: ['responsive'],
placeSelf: ['responsive'],
placeholderColor: ['responsive', 'dark', 'focus'],
placeholderOpacity: ['responsive', 'dark', 'focus'],
pointerEvents: ['responsive'],
position: ['responsive'],
resize: ['responsive'],
ringColor: ['responsive', 'dark', 'focus-within', 'focus'],
ringOffsetColor: ['responsive', 'dark', 'focus-within', 'focus'],
ringOffsetWidth: ['responsive', 'focus-within', 'focus'],
ringOpacity: ['responsive', 'dark', 'focus-within', 'focus'],
ringWidth: ['responsive', 'focus-within', 'focus'],
rotate: ['responsive', 'hover', 'focus'],
saturate: ['responsive'],
scale: ['responsive', 'hover', 'focus'],
sepia: ['responsive'],
skew: ['responsive', 'hover', 'focus'],
space: ['responsive'],
stroke: ['responsive'],
strokeWidth: ['responsive'],
tableLayout: ['responsive'],
textAlign: ['responsive'],
textColor: ['responsive', 'dark', 'group-hover', 'focus-within', 'hover', 'focus'],
textDecoration: ['responsive', 'group-hover', 'focus-within', 'hover', 'focus'],
textOpacity: ['responsive', 'dark', 'group-hover', 'focus-within', 'hover', 'focus'],
textOverflow: ['responsive'],
textTransform: ['responsive'],
transform: ['responsive'],
transformOrigin: ['responsive'],
transitionDelay: ['responsive'],
transitionDuration: ['responsive'],
transitionProperty: ['responsive'],
transitionTimingFunction: ['responsive'],
translate: ['responsive', 'hover', 'focus'],
userSelect: ['responsive'],
verticalAlign: ['responsive'],
visibility: ['responsive'],
whitespace: ['responsive'],
width: ['responsive'],
wordBreak: ['responsive'],
zIndex: ['responsive', 'focus-within', 'focus'],
},
plugins: [],
}

View File

@ -1,11 +1,8 @@
module.exports = {
purge: [],
darkMode: false, // or 'media' or 'class'
content: [],
darkMode: 'media', // or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}

File diff suppressed because it is too large Load Diff

View File

@ -1,336 +1,400 @@
import _ from 'lodash'
import path from 'path'
import postcss from 'postcss'
import processPlugins from '../src/util/processPlugins'
import container from '../src/plugins/container'
function css(nodes) {
return postcss.root({ nodes }).toString()
}
import tailwind from '../src'
function config(overrides) {
return _.defaultsDeep(overrides, {
theme: {
screens: {
sm: '576px',
md: '768px',
lg: '992px',
xl: '1200px',
},
},
prefix: '',
function run(input, config = {}) {
return postcss(tailwind(config)).process(input, {
from: path.resolve(__filename),
})
}
test('options are not required', () => {
const { components } = processPlugins([container()], config())
function css(templates) {
return templates.join('')
}
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.container { width: 100% }
@media (min-width: 576px) {
.container { max-width: 576px }
}
@media (min-width: 768px) {
.container { max-width: 768px }
}
@media (min-width: 992px) {
.container { max-width: 992px }
}
@media (min-width: 1200px) {
.container { max-width: 1200px }
function html(templates) {
return templates.join('')
}
test('options are not required', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
}
let content = css`
@tailwind components;
`
return run(content, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
}
`)
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
`)
})
})
test('screens can be passed explicitly', () => {
const { components } = processPlugins(
[container()],
config({
theme: {
container: {
screens: ['400px', '500px'],
},
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
screens: ['400px', '500px'],
},
})
)
},
}
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.container { width: 100% }
@media (min-width: 400px) {
.container { max-width: 400px }
}
@media (min-width: 500px) {
.container { max-width: 500px }
let content = css`
@tailwind components;
`
return run(content, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
}
@media (min-width: 400px) {
.container {
max-width: 400px;
}
}
}
`)
@media (min-width: 500px) {
.container {
max-width: 500px;
}
}
`)
})
})
test('screens are ordered ascending by min-width', () => {
const { components } = processPlugins(
[container()],
config({
theme: {
container: {
screens: ['500px', '400px'],
},
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
screens: ['500px', '400px'],
},
})
)
},
}
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.container { width: 100% }
@media (min-width: 400px) {
.container { max-width: 400px }
}
@media (min-width: 500px) {
.container { max-width: 500px }
let content = css`
@tailwind components;
`
return run(content, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
}
@media (min-width: 400px) {
.container {
max-width: 400px;
}
}
}
`)
@media (min-width: 500px) {
.container {
max-width: 500px;
}
}
`)
})
})
test('screens are deduplicated by min-width', () => {
const { components } = processPlugins(
[container()],
config({
theme: {
container: {
screens: {
sm: '576px',
md: '768px',
'sm-only': { min: '576px', max: '767px' },
},
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
screens: {
sm: '576px',
md: '768px',
'sm-only': { min: '576px', max: '767px' },
},
},
})
)
},
}
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.container { width: 100% }
@media (min-width: 576px) {
.container { max-width: 576px }
}
@media (min-width: 768px) {
.container { max-width: 768px }
let content = css`
@tailwind components;
`
return run(content, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
}
@media (min-width: 576px) {
.container {
max-width: 576px;
}
}
}
`)
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
`)
})
})
test('the container can be centered by default', () => {
const { components } = processPlugins(
[container()],
config({
theme: {
container: {
center: true,
},
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
center: true,
},
})
)
},
}
expect(css(components)).toMatchCss(`
@layer components {
@variants {
let content = css`
@tailwind components;
`
return run(content, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
margin-right: auto;
margin-left: auto;
}
@media (min-width: 640px) {
.container {
width: 100%;
margin-right: auto;
margin-left: auto
}
@media (min-width: 576px) {
.container { max-width: 576px }
}
@media (min-width: 768px) {
.container { max-width: 768px }
}
@media (min-width: 992px) {
.container { max-width: 992px }
}
@media (min-width: 1200px) {
.container { max-width: 1200px }
max-width: 640px;
}
}
}
`)
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
`)
})
})
test('horizontal padding can be included by default', () => {
const { components } = processPlugins(
[container()],
config({
theme: {
container: {
padding: '2rem',
},
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
padding: '2rem',
},
})
)
},
}
expect(css(components)).toMatchCss(`
@layer components {
@variants {
let content = css`
@tailwind components;
`
return run(content, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
padding-right: 2rem;
padding-left: 2rem;
}
@media (min-width: 640px) {
.container {
width: 100%;
padding-right: 2rem;
padding-left: 2rem
}
@media (min-width: 576px) {
.container { max-width: 576px }
}
@media (min-width: 768px) {
.container { max-width: 768px }
}
@media (min-width: 992px) {
.container { max-width: 992px }
}
@media (min-width: 1200px) {
.container { max-width: 1200px }
max-width: 640px;
}
}
}
`)
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
`)
})
})
test('responsive horizontal padding can be included by default', () => {
const { components } = processPlugins(
[container()],
config({
theme: {
screens: {
sm: '576px',
md: { min: '768px' },
lg: { 'min-width': '992px' },
xl: { min: '1200px', max: '1600px' },
},
container: {
padding: {
DEFAULT: '1rem',
sm: '2rem',
lg: '4rem',
xl: '5rem',
},
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
screens: {
sm: '576px',
md: { min: '768px' },
lg: { 'min-width': '992px' },
xl: { min: '1200px', max: '1600px' },
},
container: {
padding: {
DEFAULT: '1rem',
sm: '2rem',
lg: '4rem',
xl: '5rem',
},
},
})
)
},
}
expect(css(components)).toMatchCss(`
@layer components {
@variants {
let content = css`
@tailwind components;
`
return run(content, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
padding-right: 1rem;
padding-left: 1rem;
}
@media (min-width: 576px) {
.container {
width: 100%;
padding-right: 1rem;
padding-left: 1rem
}
@media (min-width: 576px) {
.container {
max-width: 576px;
padding-right: 2rem;
padding-left: 2rem
}
}
@media (min-width: 768px) {
.container { max-width: 768px }
}
@media (min-width: 992px) {
.container {
max-width: 992px;
padding-right: 4rem;
padding-left: 4rem
}
}
@media (min-width: 1200px) {
.container {
max-width: 1200px;
padding-right: 5rem;
padding-left: 5rem
}
max-width: 576px;
padding-right: 2rem;
padding-left: 2rem;
}
}
}
`)
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 992px) {
.container {
max-width: 992px;
padding-right: 4rem;
padding-left: 4rem;
}
}
@media (min-width: 1200px) {
.container {
max-width: 1200px;
padding-right: 5rem;
padding-left: 5rem;
}
}
`)
})
})
test('setting all options at once', () => {
const { components } = processPlugins(
[container()],
config({
theme: {
container: {
screens: ['400px', '500px'],
center: true,
padding: '2rem',
},
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
screens: ['400px', '500px'],
center: true,
padding: '2rem',
},
})
)
},
}
expect(css(components)).toMatchCss(`
@layer components {
@variants {
let content = css`
@tailwind components;
`
return run(content, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
margin-right: auto;
margin-left: auto;
padding-right: 2rem;
padding-left: 2rem;
}
@media (min-width: 400px) {
.container {
width: 100%;
margin-right: auto;
margin-left: auto;
padding-right: 2rem;
padding-left: 2rem
}
@media (min-width: 400px) {
.container { max-width: 400px }
}
@media (min-width: 500px) {
.container { max-width: 500px }
max-width: 400px;
}
}
}
`)
@media (min-width: 500px) {
.container {
max-width: 500px;
}
}
`)
})
})
test('container can use variants', () => {
const { components } = processPlugins(
[container()],
config({
theme: {
container: {
screens: ['400px', '500px'],
},
let config = {
content: [{ raw: html`<div class="lg:hover:container"></div>` }],
theme: {
container: {
screens: ['400px', '500px'],
},
variants: {
container: ['responsive', 'hover'],
},
})
)
},
}
expect(css(components)).toMatchCss(`
@layer components {
@variants responsive, hover {
.container {
width: 100%
let content = css`
@tailwind components;
`
return run(content, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 1024px) {
.lg\\:hover\\:container:hover {
width: 100%;
}
@media (min-width: 400px) {
.container {
max-width: 400px
.lg\\:hover\\:container:hover {
max-width: 400px;
}
}
@media (min-width: 500px) {
.container {
max-width: 500px
.lg\\:hover\\:container:hover {
max-width: 500px;
}
}
}
}
`)
`)
})
})

View File

@ -5,139 +5,124 @@ import tailwind from '../src/index'
import { cjsConfigFile, defaultConfigFile } from '../src/constants'
import inTempDirectory from '../jest/runInTempDirectory'
// NOTE: If we ever want to abstract this logic, then we have to watch out
// because in most tests we default to an empty object here. However, in this
// tests we do want to check the difference between no config (undefined) and a
// config (empty object or full object).
function run(input, config /* Undefined is important in this case */) {
return postcss(tailwind(config)).process(input, {
from: path.resolve(__filename),
})
}
function css(templates) {
return templates.join('')
}
function html(templates) {
return templates.join('')
}
function javascript(templates) {
return templates.join('')
}
test('it uses the values from the custom config file', () => {
return postcss([tailwind(path.resolve(`${__dirname}/fixtures/custom-config.js`))])
.process(
`
@responsive {
.foo {
color: blue;
}
let config = require(path.resolve(`${__dirname}/fixtures/custom-config.js`))
let content = css`
@tailwind utilities;
`
return run(content, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\\:font-bold {
font-weight: 700;
}
`,
{ from: undefined }
)
.then((result) => {
const expected = `
.foo {
color: blue;
}
@media (min-width: 400px) {
.mobile\\:foo {
color: blue;
}
}
`
expect(result.css).toMatchCss(expected)
})
}
`)
})
})
test('custom config can be passed as an object', () => {
return postcss([
tailwind({
let config = {
content: [{ raw: html`<div class="mobile:font-bold"></div>` }],
theme: {
screens: {
mobile: '400px',
},
},
}
let content = css`
@tailwind utilities;
`
return run(content, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\\:font-bold {
font-weight: 700;
}
}
`)
})
})
test('custom config path can be passed using `config` property in an object', () => {
let config = {
config: path.resolve(`${__dirname}/fixtures/custom-config.js`),
}
let content = css`
@tailwind utilities;
`
return run(content, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\\:font-bold {
font-weight: 700;
}
}
`)
})
})
test('custom config can be passed under the `config` property', () => {
let config = {
config: {
content: [{ raw: html`<div class="mobile:font-bold"></div>` }],
theme: {
screens: {
mobile: '400px',
},
},
}),
])
.process(
`
@responsive {
.foo {
color: blue;
}
}
`,
{ from: undefined }
)
.then((result) => {
const expected = `
.foo {
color: blue;
}
@media (min-width: 400px) {
.mobile\\:foo {
color: blue;
}
}
`
},
}
expect(result.css).toMatchCss(expected)
})
})
let content = css`
@tailwind utilities;
`
test('custom config path can be passed using `config` property in an object', () => {
return postcss([tailwind({ config: path.resolve(`${__dirname}/fixtures/custom-config.js`) })])
.process(
`
@responsive {
.foo {
color: blue;
}
return run(content, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\\:font-bold {
font-weight: 700;
}
`,
{ from: undefined }
)
.then((result) => {
const expected = `
.foo {
color: blue;
}
@media (min-width: 400px) {
.mobile\\:foo {
color: blue;
}
}
`
expect(result.css).toMatchCss(expected)
})
})
test('custom config can be passed under the `config` property', () => {
return postcss([
tailwind({
config: {
theme: {
screens: {
mobile: '400px',
},
},
},
}),
])
.process(
`
@responsive {
.foo {
color: blue;
}
}
`,
{ from: undefined }
)
.then((result) => {
const expected = `
.foo {
color: blue;
}
@media (min-width: 400px) {
.mobile\\:foo {
color: blue;
}
}
`
expect(result.css).toMatchCss(expected)
})
}
`)
})
})
test('tailwind.config.cjs is picked up by default', () => {
return inTempDirectory(() => {
fs.writeFileSync(
path.resolve(cjsConfigFile),
`module.exports = {
javascript`module.exports = {
content: [{ raw: '<div class="mobile:font-bold"></div>' }],
theme: {
screens: {
mobile: '400px',
@ -146,29 +131,19 @@ test('tailwind.config.cjs is picked up by default', () => {
}`
)
return postcss([tailwind])
.process(
`
@responsive {
.foo {
color: blue;
}
let content = css`
@tailwind utilities;
`
return run(content).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\\:font-bold {
font-weight: 700;
}
`,
{ from: undefined }
)
.then((result) => {
expect(result.css).toMatchCss(`
.foo {
color: blue;
}
@media (min-width: 400px) {
.mobile\\:foo {
color: blue;
}
}
`)
})
}
`)
})
})
})
@ -176,7 +151,8 @@ test('tailwind.config.js is picked up by default', () => {
return inTempDirectory(() => {
fs.writeFileSync(
path.resolve(defaultConfigFile),
`module.exports = {
javascript`module.exports = {
content: [{ raw: '<div class="mobile:font-bold"></div>' }],
theme: {
screens: {
mobile: '400px',
@ -185,29 +161,19 @@ test('tailwind.config.js is picked up by default', () => {
}`
)
return postcss([tailwind])
.process(
`
@responsive {
.foo {
color: blue;
}
let content = css`
@tailwind utilities;
`
return run(content).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\\:font-bold {
font-weight: 700;
}
`,
{ from: undefined }
)
.then((result) => {
expect(result.css).toMatchCss(`
.foo {
color: blue;
}
@media (min-width: 400px) {
.mobile\\:foo {
color: blue;
}
}
`)
})
}
`)
})
})
})
@ -215,7 +181,8 @@ test('tailwind.config.cjs is picked up by default when passing an empty object',
return inTempDirectory(() => {
fs.writeFileSync(
path.resolve(cjsConfigFile),
`module.exports = {
javascript`module.exports = {
content: [{ raw: '<div class="mobile:font-bold"></div>' }],
theme: {
screens: {
mobile: '400px',
@ -224,29 +191,19 @@ test('tailwind.config.cjs is picked up by default when passing an empty object',
}`
)
return postcss([tailwind({})])
.process(
`
@responsive {
.foo {
color: blue;
}
let content = css`
@tailwind utilities;
`
return run(content, {}).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\\:font-bold {
font-weight: 700;
}
`,
{ from: undefined }
)
.then((result) => {
expect(result.css).toMatchCss(`
.foo {
color: blue;
}
@media (min-width: 400px) {
.mobile\\:foo {
color: blue;
}
}
`)
})
}
`)
})
})
})
@ -254,7 +211,8 @@ test('tailwind.config.js is picked up by default when passing an empty object',
return inTempDirectory(() => {
fs.writeFileSync(
path.resolve(defaultConfigFile),
`module.exports = {
javascript`module.exports = {
content: [{ raw: '<div class="mobile:font-bold"></div>' }],
theme: {
screens: {
mobile: '400px',
@ -263,300 +221,240 @@ test('tailwind.config.js is picked up by default when passing an empty object',
}`
)
return postcss([tailwind({})])
.process(
`
@responsive {
.foo {
color: blue;
}
let content = css`
@tailwind utilities;
`
return run(content, {}).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\\:font-bold {
font-weight: 700;
}
`,
{ from: undefined }
)
.then((result) => {
expect(result.css).toMatchCss(`
.foo {
color: blue;
}
@media (min-width: 400px) {
.mobile\\:foo {
color: blue;
}
}
`)
})
}
`)
})
})
})
test('the default config can be overridden using the presets key', () => {
return postcss([
tailwind({
presets: [
{
theme: {
extend: {
minHeight: {
24: '24px',
},
},
},
corePlugins: ['minHeight'],
variants: { minHeight: [] },
let config = {
content: [{ raw: html`<div class="min-h-0 min-h-primary min-h-secondary"></div>` }],
presets: [
{
theme: {
extend: { minHeight: { secondary: '24px' } },
},
],
theme: {
extend: { minHeight: { 48: '48px' } },
},
}),
])
.process(
`
@tailwind utilities
`,
{ from: undefined }
)
.then((result) => {
const expected = `
.min-h-0 {
min-height: 0px;
}
.min-h-24 {
min-height: 24px;
}
.min-h-48 {
min-height: 48px;
}
.min-h-full {
min-height: 100%;
}
.min-h-screen {
min-height: 100vh;
}
`
],
theme: {
extend: { minHeight: { primary: '48px' } },
},
}
expect(result.css).toMatchCss(expected)
})
let content = css`
@tailwind utilities;
`
return run(content, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.min-h-0 {
min-height: 0px;
}
.min-h-primary {
min-height: 48px;
}
.min-h-secondary {
min-height: 24px;
}
`)
})
})
test('presets can be functions', () => {
return postcss([
tailwind({
presets: [
() => ({
theme: {
extend: {
minHeight: {
24: '24px',
},
},
},
corePlugins: ['minHeight'],
variants: { minHeight: [] },
}),
],
theme: {
extend: { minHeight: { 48: '48px' } },
},
}),
])
.process(
`
@tailwind utilities
`,
{ from: undefined }
)
.then((result) => {
const expected = `
.min-h-0 {
min-height: 0px;
}
.min-h-24 {
min-height: 24px;
}
.min-h-48 {
min-height: 48px;
}
.min-h-full {
min-height: 100%;
}
.min-h-screen {
min-height: 100vh;
}
`
let config = {
content: [{ raw: html`<div class="min-h-0 min-h-primary min-h-secondary"></div>` }],
presets: [
() => ({
theme: {
extend: { minHeight: { secondary: '24px' } },
},
}),
],
theme: {
extend: { minHeight: { primary: '48px' } },
},
}
expect(result.css).toMatchCss(expected)
})
let content = css`
@tailwind utilities;
`
return run(content, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.min-h-0 {
min-height: 0px;
}
.min-h-primary {
min-height: 48px;
}
.min-h-secondary {
min-height: 24px;
}
`)
})
})
test('the default config can be removed by using an empty presets key in a preset', () => {
return postcss([
tailwind({
presets: [
{
presets: [],
theme: {
extend: {
minHeight: {
24: '24px',
},
},
},
corePlugins: ['minHeight'],
variants: { minHeight: [] },
let config = {
content: [{ raw: html`<div class="min-h-0 min-h-primary min-h-secondary"></div>` }],
presets: [
{
presets: [],
theme: {
extend: { minHeight: { secondary: '24px' } },
},
],
theme: {
extend: { minHeight: { 48: '48px' } },
},
}),
])
.process(
`
@tailwind utilities
`,
{ from: undefined }
)
.then((result) => {
const expected = `
.min-h-24 {
min-height: 24px;
}
.min-h-48 {
min-height: 48px;
}
`
],
theme: {
extend: { minHeight: { primary: '48px' } },
},
}
expect(result.css).toMatchCss(expected)
})
let content = css`
@tailwind utilities;
`
return run(content, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.min-h-primary {
min-height: 48px;
}
.min-h-secondary {
min-height: 24px;
}
`)
})
})
test('presets can have their own presets', () => {
return postcss([
tailwind({
presets: [
{
presets: [],
theme: {
colors: { red: '#dd0000' },
},
let config = {
content: [{ raw: html`<div class="bg-transparent bg-black bg-white bg-red"></div>` }],
presets: [
{
presets: [],
theme: {
colors: { red: '#dd0000' },
},
{
presets: [
{
presets: [],
theme: {
colors: {
transparent: 'transparent',
red: '#ff0000',
},
},
},
],
theme: {
extend: {
colors: {
black: 'black',
red: '#ee0000',
},
backgroundColor: (theme) => theme('colors'),
},
},
corePlugins: ['backgroundColor'],
},
],
theme: {
extend: { colors: { white: 'white' } },
},
}),
])
.process(
`
@tailwind utilities
`,
{ from: undefined }
)
.then((result) => {
const expected = `
.bg-transparent {
background-color: transparent;
}
.bg-red {
background-color: #ee0000;
}
.bg-black {
background-color: black;
}
.bg-white {
background-color: white;
}
`
{
presets: [
{
presets: [],
theme: {
colors: {
transparent: 'transparent',
red: '#ff0000',
},
},
},
],
theme: {
extend: {
colors: {
black: 'black',
red: '#ee0000',
},
backgroundColor: (theme) => theme('colors'),
},
},
corePlugins: ['backgroundColor'],
},
],
theme: {
extend: { colors: { white: 'white' } },
},
}
expect(result.css).toMatchCss(expected)
})
let content = css`
@tailwind utilities;
`
return run(content, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-transparent {
background-color: transparent;
}
.bg-black {
background-color: black;
}
.bg-white {
background-color: white;
}
.bg-red {
background-color: #ee0000;
}
`)
})
})
test('function presets can be mixed with object presets', () => {
return postcss([
tailwind({
presets: [
() => ({
presets: [],
theme: {
colors: { red: '#dd0000' },
},
}),
{
presets: [
() => ({
presets: [],
theme: {
colors: {
transparent: 'transparent',
red: '#ff0000',
},
},
}),
],
theme: {
extend: {
colors: {
black: 'black',
red: '#ee0000',
},
backgroundColor: (theme) => theme('colors'),
},
},
corePlugins: ['backgroundColor'],
let config = {
content: [{ raw: html`<div class="bg-transparent bg-black bg-white bg-red"></div>` }],
presets: [
() => ({
presets: [],
theme: {
colors: { red: '#dd0000' },
},
],
theme: {
extend: { colors: { white: 'white' } },
}),
{
presets: [
() => ({
presets: [],
theme: {
colors: {
transparent: 'transparent',
red: '#ff0000',
},
},
}),
],
theme: {
extend: {
colors: {
black: 'black',
red: '#ee0000',
},
backgroundColor: (theme) => theme('colors'),
},
},
corePlugins: ['backgroundColor'],
},
}),
])
.process(
`
@tailwind utilities
`,
{ from: undefined }
)
.then((result) => {
const expected = `
.bg-transparent {
background-color: transparent;
}
.bg-red {
background-color: #ee0000;
}
.bg-black {
background-color: black;
}
.bg-white {
background-color: white;
}
`
],
theme: {
extend: { colors: { white: 'white' } },
},
}
expect(result.css).toMatchCss(expected)
})
let content = css`
@tailwind utilities;
`
return run(content, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-transparent {
background-color: transparent;
}
.bg-black {
background-color: black;
}
.bg-white {
background-color: white;
}
.bg-red {
background-color: #ee0000;
}
`)
})
})

View File

@ -1,358 +0,0 @@
import postcss from 'postcss'
import tailwind from '../src/index'
import createPlugin from '../src/util/createPlugin'
function run(input, config = {}) {
return postcss([tailwind(config)]).process(input, { from: undefined })
}
test('user-defined dark mode variants do not stack', () => {
const input = `
@variants dark, hover {
.text-red {
color: red;
}
}
`
const expected = `
.text-red {
color: red;
}
.custom-dark .custom-dark\\:text-red {
color: red;
}
.hover\\:text-red:hover {
color: red;
}
`
const userPlugin = createPlugin(function ({ addVariant }) {
addVariant('dark', function ({ modifySelectors }) {
modifySelectors(function ({ className }) {
return `.custom-dark .custom-dark\\:${className}`
})
})
})
expect.assertions(2)
return postcss([tailwind({ plugins: [userPlugin] })])
.process(input, { from: undefined })
.then((result) => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
test('dark mode variants can be generated using the class strategy', () => {
const input = `
@variants dark {
.text-red {
color: red;
}
}
`
const expected = `
.text-red {
color: red;
}
@media (prefers-color-scheme: dark) {
.dark\\:text-red {
color: red;
}
}
`
expect.assertions(2)
return run(input, { darkMode: 'media' }).then((result) => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
test('dark mode variants can be generated even when the user has their own plugins array', () => {
const input = `
@variants dark {
.text-red {
color: red;
}
}
`
const expected = `
.text-red {
color: red;
}
@media (prefers-color-scheme: dark) {
.dark\\:text-red {
color: red;
}
}
`
expect.assertions(2)
return run(input, { darkMode: 'media', plugins: [] }).then((result) => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
test('dark mode variants can be generated using the class strategy', () => {
const input = `
@variants dark {
.text-red {
color: red;
}
}
`
const expected = `
.text-red {
color: red;
}
.dark .dark\\:text-red {
color: red;
}
`
expect.assertions(2)
return run(input, { darkMode: 'class' }).then((result) => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
test('dark mode variants can be disabled', () => {
const input = `
@variants dark {
.text-red {
color: red;
}
}
`
const expected = `
.text-red {
color: red;
}
`
expect.assertions(2)
return run(input, { dark: false }).then((result) => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
test('dark mode variants are disabled by default', () => {
const input = `
@variants dark {
.text-red {
color: red;
}
}
`
const expected = `
.text-red {
color: red;
}
`
expect.assertions(2)
return run(input).then((result) => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
test('dark mode variants stack with other variants', () => {
const input = `
@variants responsive, dark, hover, focus {
.text-red {
color: red;
}
}
`
const expected = `
.text-red {
color: red;
}
.hover\\:text-red:hover {
color: red;
}
.focus\\:text-red:focus {
color: red;
}
@media (prefers-color-scheme: dark) {
.dark\\:text-red {
color: red;
}
.dark\\:hover\\:text-red:hover {
color: red;
}
.dark\\:focus\\:text-red:focus {
color: red;
}
}
@media (min-width: 500px) {
.sm\\:text-red {
color: red;
}
.sm\\:hover\\:text-red:hover {
color: red;
}
.sm\\:focus\\:text-red:focus {
color: red;
}
@media (prefers-color-scheme: dark) {
.sm\\:dark\\:text-red {
color: red;
}
.sm\\:dark\\:hover\\:text-red:hover {
color: red;
}
.sm\\:dark\\:focus\\:text-red:focus {
color: red;
}
}
}
@media (min-width: 800px) {
.lg\\:text-red {
color: red;
}
.lg\\:hover\\:text-red:hover {
color: red;
}
.lg\\:focus\\:text-red:focus {
color: red;
}
@media (prefers-color-scheme: dark) {
.lg\\:dark\\:text-red {
color: red;
}
.lg\\:dark\\:hover\\:text-red:hover {
color: red;
}
.lg\\:dark\\:focus\\:text-red:focus {
color: red;
}
}
}
`
expect.assertions(2)
return run(input, { darkMode: 'media', theme: { screens: { sm: '500px', lg: '800px' } } }).then(
(result) => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
}
)
})
test('dark mode variants stack with other variants when using the class strategy', () => {
const input = `
@variants responsive, dark, group-hover, hover, focus {
.text-red {
color: red;
}
}
`
const expected = `
.text-red {
color: red;
}
.group:hover .group-hover\\:text-red {
color: red;
}
.hover\\:text-red:hover {
color: red;
}
.focus\\:text-red:focus {
color: red;
}
.dark .dark\\:text-red {
color: red;
}
.dark .group:hover .dark\\:group-hover\\:text-red {
color: red;
}
.dark .dark\\:hover\\:text-red:hover {
color: red;
}
.dark .dark\\:focus\\:text-red:focus {
color: red;
}
@media (min-width: 500px) {
.sm\\:text-red {
color: red;
}
.group:hover .sm\\:group-hover\\:text-red {
color: red;
}
.sm\\:hover\\:text-red:hover {
color: red;
}
.sm\\:focus\\:text-red:focus {
color: red;
}
.dark .sm\\:dark\\:text-red {
color: red;
}
.dark .group:hover .sm\\:dark\\:group-hover\\:text-red {
color: red;
}
.dark .sm\\:dark\\:hover\\:text-red:hover {
color: red;
}
.dark .sm\\:dark\\:focus\\:text-red:focus {
color: red;
}
}
@media (min-width: 800px) {
.lg\\:text-red {
color: red;
}
.group:hover .lg\\:group-hover\\:text-red {
color: red;
}
.lg\\:hover\\:text-red:hover {
color: red;
}
.lg\\:focus\\:text-red:focus {
color: red;
}
.dark .lg\\:dark\\:text-red {
color: red;
}
.dark .group:hover .lg\\:dark\\:group-hover\\:text-red {
color: red;
}
.dark .lg\\:dark\\:hover\\:text-red:hover {
color: red;
}
.dark .lg\\:dark\\:focus\\:text-red:focus {
color: red;
}
}
`
expect.assertions(2)
return run(input, { darkMode: 'class', theme: { screens: { sm: '500px', lg: '800px' } } }).then(
(result) => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
}
)
})

View File

@ -1,4 +1,5 @@
module.exports = {
content: [{ raw: '<div class="mobile:font-bold"></div>' }],
theme: {
screens: {
mobile: '400px',

View File

@ -1,5 +1,5 @@
module.exports = {
purge: ['./tests/fixtures/*.html'],
content: ['./tests/fixtures/*.html'],
theme: {
extend: {
colors: {

Some files were not shown because too many files have changed in this diff Show More