[WIP] Unify JIT and AOT code paths (#4188)

* WIP

* WIP

* Finish combining JIT and AOT plugins

Still lots of clean up that can be done in some of the more complex ones, but at least it's one file per plugin now.

* Remove unused import

* Fix AOT generation bugs

* Move corePlugins/index.js to corePlugins.js

* Convert JIT files to ESM

* Move tests

* Reorder core plugins to match JIT order

* Update AOT apply tests

* Unify utils

* Combine plugin lists to one single source of truth

* Finish resolving merge conflicts, fix tests

Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
This commit is contained in:
Robin Malfait 2021-04-30 16:58:40 +02:00 • committed by GitHub
parent 54aa5be7ce
commit bfc61625d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
340 changed files with 428688 additions and 430807 deletions

1405
tests/applyAtRule.test.js Normal file

File diff suppressed because it is too large Load diff

19
tests/cli.compile.test.js Normal file
View file

@ -0,0 +1,19 @@
import path from 'path'
import autoprefixer from 'autoprefixer'
import tailwind from '../src'
import compile from '../src/cli/compile'
describe('cli compile', () => {
const inputFile = path.resolve(__dirname, 'fixtures/tailwind-input.css')
const outputFile = 'output.css'
const plugins = [tailwind(), autoprefixer]
it('compiles CSS file', () => {
return compile({ inputFile, outputFile, plugins }).then((result) => {
expect(result.css).toContain('.example')
expect(result.css).toContain('-webkit-background-clip')
})
})
})

122
tests/cli.test.js Normal file
View file

@ -0,0 +1,122 @@
import path from 'path'
import cli from '../src/cli/main'
import * as constants from '../src/constants'
import * as utils from '../src/cli/utils'
import runInTempDirectory from '../jest/runInTempDirectory'
describe('cli', () => {
const inputCssPath = path.resolve(__dirname, 'fixtures/tailwind-input.css')
const customConfigPath = path.resolve(__dirname, 'fixtures/custom-config.js')
const esmPackageJsonPath = path.resolve(__dirname, 'fixtures/esm-package.json')
const defaultConfigFixture = utils.readFile(constants.defaultConfigStubFile)
const simpleConfigFixture = utils.readFile(constants.simpleConfigStubFile)
const defaultPostCssConfigFixture = utils.readFile(constants.defaultPostCssConfigStubFile)
beforeEach(() => {
console.log = jest.fn()
process.stdout.write = jest.fn()
})
describe('init', () => {
it('creates a Tailwind config file', () => {
return runInTempDirectory(() => {
return cli(['init']).then(() => {
expect(utils.readFile(constants.defaultConfigFile)).toEqual(simpleConfigFixture)
})
})
})
it('creates a Tailwind config file and a postcss.config.js file', () => {
return runInTempDirectory(() => {
return cli(['init', '-p']).then(() => {
expect(utils.readFile(constants.defaultConfigFile)).toEqual(simpleConfigFixture)
expect(utils.readFile(constants.defaultPostCssConfigFile)).toEqual(
defaultPostCssConfigFixture
)
})
})
})
it('creates a full Tailwind config file', () => {
return runInTempDirectory(() => {
return cli(['init', '--full']).then(() => {
expect(utils.readFile(constants.defaultConfigFile)).toEqual(
defaultConfigFixture.replace('../colors', 'tailwindcss/colors')
)
})
})
})
it('creates a .cjs Tailwind config file inside of an ESM project', () => {
return runInTempDirectory(() => {
utils.writeFile('package.json', utils.readFile(esmPackageJsonPath))
return cli(['init']).then(() => {
expect(utils.readFile(constants.cjsConfigFile)).toEqual(simpleConfigFixture)
})
})
})
it('creates a .cjs Tailwind config file and a postcss.config.cjs file inside of an ESM project', () => {
return runInTempDirectory(() => {
utils.writeFile('package.json', utils.readFile(esmPackageJsonPath))
return cli(['init', '-p']).then(() => {
expect(utils.readFile(constants.cjsConfigFile)).toEqual(simpleConfigFixture)
expect(utils.readFile(constants.cjsPostCssConfigFile)).toEqual(
defaultPostCssConfigFixture
)
})
})
})
it('creates a Tailwind config file in a custom location', () => {
return runInTempDirectory(() => {
return cli(['init', 'custom.js']).then(() => {
expect(utils.exists('custom.js')).toEqual(true)
})
})
})
})
describe('build', () => {
it('compiles CSS file using an input css file', () => {
return cli(['build', inputCssPath]).then(() => {
expect(process.stdout.write.mock.calls[0][0]).toContain('.example')
})
})
it('compiles CSS file without an input css file', () => {
return cli(['build']).then(() => {
expect(process.stdout.write.mock.calls[0][0]).toContain('normalize.css') // base
expect(process.stdout.write.mock.calls[0][0]).toContain('.container') // components
expect(process.stdout.write.mock.calls[0][0]).toContain('.mx-auto') // utilities
})
})
it('compiles CSS file using custom configuration', () => {
return cli(['build', inputCssPath, '--config', customConfigPath]).then(() => {
expect(process.stdout.write.mock.calls[0][0]).toContain('400px')
})
})
it('creates compiled CSS file', () => {
return runInTempDirectory(() => {
return cli(['build', inputCssPath, '--output', 'output.css']).then(() => {
expect(utils.readFile('output.css')).toContain('.example')
})
})
})
it('compiles CSS file with autoprefixer', () => {
return cli(['build', inputCssPath]).then(() => {
expect(process.stdout.write.mock.calls[0][0]).toContain('-webkit-background-clip')
})
})
it('compiles CSS file without autoprefixer', () => {
return cli(['build', inputCssPath, '--no-autoprefixer']).then(() => {
expect(process.stdout.write.mock.calls[0][0]).not.toContain('-webkit-background-clip')
})
})
})
})

73
tests/cli.utils.test.js Normal file
View file

@ -0,0 +1,73 @@
import * as utils from '../src/cli/utils'
describe('cli utils', () => {
describe('parseCliParams', () => {
it('parses CLI parameters', () => {
const result = utils.parseCliParams(['a', 'b', '-c', 'd'])
expect(result).toEqual(['a', 'b'])
})
})
describe('parseCliOptions', () => {
it('parses CLI options', () => {
const result = utils.parseCliOptions(['a', '-b', 'c'], { test: ['b'] })
expect(result).toEqual({ test: ['c'] })
})
it('parses multiple types of options', () => {
const result = utils.parseCliOptions(['a', '-b', 'c', '--test', 'd', '-test', 'e'], {
test: ['test', 'b'],
})
expect(result).toEqual({ test: ['c', 'd', 'e'] })
})
it('ignores unknown options', () => {
const result = utils.parseCliOptions(['a', '-b', 'c'], {})
expect(result).toEqual({})
})
it('maps options', () => {
const result = utils.parseCliOptions(['a', '-b', 'c', '-d', 'e'], { test: ['b', 'd'] })
expect(result).toEqual({ test: ['c', 'e'] })
})
it('parses undefined options', () => {
const result = utils.parseCliOptions(['a'], { test: ['b'] })
expect(result).toEqual({ test: undefined })
})
it('parses flags', () => {
const result = utils.parseCliOptions(['a', '-b'], { test: ['b'] })
expect(result).toEqual({ test: [] })
})
it('accepts multiple values per option', () => {
const result = utils.parseCliOptions(['a', '-b', 'c', 'd', '-e', 'f', '-g', 'h'], {
test: ['b', 'g'],
})
expect(result).toEqual({ test: ['c', 'd', 'h'] })
})
})
describe('getSimplePath', () => {
it('strips leading ./', () => {
const result = utils.getSimplePath('./test')
expect(result).toEqual('test')
})
it('returns unchanged path if it does not begin with ./', () => {
const result = utils.getSimplePath('../test')
expect(result).toEqual('../test')
})
})
})

View file

@ -0,0 +1,30 @@
import configurePlugins from '../src/util/configurePlugins'
test('setting a plugin to false removes it', () => {
const plugins = ['fontSize', 'display', 'backgroundPosition']
const configuredPlugins = configurePlugins(
{
display: false,
},
plugins
)
expect(configuredPlugins).toEqual(['fontSize', 'backgroundPosition'])
})
test('passing only false removes all plugins', () => {
const plugins = ['fontSize', 'display', 'backgroundPosition']
const configuredPlugins = configurePlugins(false, plugins)
expect(configuredPlugins).toEqual([])
})
test('passing an array whitelists plugins', () => {
const plugins = ['fontSize', 'display', 'backgroundPosition']
const configuredPlugins = configurePlugins(['display'], plugins)
expect(configuredPlugins).toEqual(['display'])
})

View file

@ -0,0 +1,336 @@
import _ from 'lodash'
import postcss from 'postcss'
import processPlugins from '../src/util/processPlugins'
import container from '../src/plugins/container'
function css(nodes) {
return postcss.root({ nodes }).toString()
}
function config(overrides) {
return _.defaultsDeep(overrides, {
theme: {
screens: {
sm: '576px',
md: '768px',
lg: '992px',
xl: '1200px',
},
},
prefix: '',
})
}
test('options are not required', () => {
const { components } = processPlugins([container()], config())
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 }
}
}
}
`)
})
test('screens can be passed explicitly', () => {
const { components } = processPlugins(
[container()],
config({
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 }
}
}
}
`)
})
test('screens are ordered ascending by min-width', () => {
const { components } = processPlugins(
[container()],
config({
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 }
}
}
}
`)
})
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' },
},
},
},
})
)
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 }
}
}
}
`)
})
test('the container can be centered by default', () => {
const { components } = processPlugins(
[container()],
config({
theme: {
container: {
center: true,
},
},
})
)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.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 }
}
}
}
`)
})
test('horizontal padding can be included by default', () => {
const { components } = processPlugins(
[container()],
config({
theme: {
container: {
padding: '2rem',
},
},
})
)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.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 }
}
}
}
`)
})
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',
},
},
},
})
)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.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
}
}
}
}
`)
})
test('setting all options at once', () => {
const { components } = processPlugins(
[container()],
config({
theme: {
container: {
screens: ['400px', '500px'],
center: true,
padding: '2rem',
},
},
})
)
expect(css(components)).toMatchCss(`
@layer components {
@variants {
.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 }
}
}
}
`)
})
test('container can use variants', () => {
const { components } = processPlugins(
[container()],
config({
theme: {
container: {
screens: ['400px', '500px'],
},
},
variants: {
container: ['responsive', 'hover'],
},
})
)
expect(css(components)).toMatchCss(`
@layer components {
@variants responsive, hover {
.container {
width: 100%
}
@media (min-width: 400px) {
.container {
max-width: 400px
}
}
@media (min-width: 500px) {
.container {
max-width: 500px
}
}
}
}
`)
})

562
tests/customConfig.test.js Normal file
View file

@ -0,0 +1,562 @@
import fs from 'fs'
import path from 'path'
import postcss from 'postcss'
import tailwind from '../src/index'
import { cjsConfigFile, defaultConfigFile } from '../src/constants'
import inTempDirectory from '../jest/runInTempDirectory'
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;
}
}
`,
{ 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({
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('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;
}
}
`,
{ 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 = {
theme: {
screens: {
mobile: '400px',
},
},
}`
)
return postcss([tailwind])
.process(
`
@responsive {
.foo {
color: blue;
}
}
`,
{ from: undefined }
)
.then((result) => {
expect(result.css).toMatchCss(`
.foo {
color: blue;
}
@media (min-width: 400px) {
.mobile\\:foo {
color: blue;
}
}
`)
})
})
})
test('tailwind.config.js is picked up by default', () => {
return inTempDirectory(() => {
fs.writeFileSync(
path.resolve(defaultConfigFile),
`module.exports = {
theme: {
screens: {
mobile: '400px',
},
},
}`
)
return postcss([tailwind])
.process(
`
@responsive {
.foo {
color: blue;
}
}
`,
{ from: undefined }
)
.then((result) => {
expect(result.css).toMatchCss(`
.foo {
color: blue;
}
@media (min-width: 400px) {
.mobile\\:foo {
color: blue;
}
}
`)
})
})
})
test('tailwind.config.cjs is picked up by default when passing an empty object', () => {
return inTempDirectory(() => {
fs.writeFileSync(
path.resolve(cjsConfigFile),
`module.exports = {
theme: {
screens: {
mobile: '400px',
},
},
}`
)
return postcss([tailwind({})])
.process(
`
@responsive {
.foo {
color: blue;
}
}
`,
{ from: undefined }
)
.then((result) => {
expect(result.css).toMatchCss(`
.foo {
color: blue;
}
@media (min-width: 400px) {
.mobile\\:foo {
color: blue;
}
}
`)
})
})
})
test('tailwind.config.js is picked up by default when passing an empty object', () => {
return inTempDirectory(() => {
fs.writeFileSync(
path.resolve(defaultConfigFile),
`module.exports = {
theme: {
screens: {
mobile: '400px',
},
},
}`
)
return postcss([tailwind({})])
.process(
`
@responsive {
.foo {
color: blue;
}
}
`,
{ 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: [] },
},
],
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;
}
`
expect(result.css).toMatchCss(expected)
})
})
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;
}
`
expect(result.css).toMatchCss(expected)
})
})
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: [] },
},
],
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;
}
`
expect(result.css).toMatchCss(expected)
})
})
test('presets can have their own 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'],
},
],
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;
}
`
expect(result.css).toMatchCss(expected)
})
})
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'],
},
],
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;
}
`
expect(result.css).toMatchCss(expected)
})
})

358
tests/darkMode.test.js Normal file
View file

@ -0,0 +1,358 @@
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

@ -0,0 +1,11 @@
import config from '../defaultConfig.js'
import configStub from '../stubs/defaultConfig.stub.js'
test('the default config matches the stub', () => {
expect(config).toEqual(configStub)
})
test('modifying the default config does not affect the stub', () => {
config.theme = {}
expect(config).not.toEqual(configStub)
})

View file

@ -0,0 +1,11 @@
import theme from '../defaultTheme.js'
import configStub from '../stubs/defaultConfig.stub.js'
test('the default theme matches the stub', () => {
expect(theme).toEqual(configStub.theme)
})
test('modifying the default theme does not affect the stub', () => {
theme.colors = {}
expect(theme).not.toEqual(configStub.theme)
})

View file

@ -0,0 +1,5 @@
import escapeClassName from '../src/util/escapeClassName'
test('invalid characters are escaped', () => {
expect(escapeClassName('w:_$-1/2')).toEqual('w\\:_\\$-1\\/2')
})

7
tests/fixtures/custom-config.js vendored Normal file
View file

@ -0,0 +1,7 @@
module.exports = {
theme: {
screens: {
mobile: '400px',
},
},
}

3
tests/fixtures/esm-package.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"type": "module"
}

44
tests/fixtures/purge-example.html vendored Normal file
View file

@ -0,0 +1,44 @@
<!-- Basic HTML -->
<html>
<head>
<title>My Page</title>
</head>
<body id="tailwind">
<main>
<div class="bg-red-500 md:bg-blue-300 w-1/2"></div>
</main>
</body>
</html>
<!-- Vue dynamic classes -->
<span :class="{ block: enabled, 'md:flow-root': !enabled }"></span>
<!-- JSX with template strings -->
<script>
function Component() {
return <div class={`h-screen`}></div>
}
</script>
<!-- Custom classes with really weird characters -->
<div class="min-h-(screen-4) bg-black! font-%#$@ w-(1/2+8)"></div>
<!-- Pug -->
span.inline-grid.grid-cols-3(class="px-1.5")
.col-span-2
Hello
.col-span-1.text-center
World!
<!-- Haml -->
.flow-root
.text-green-700.bg-green-100
.text-left= content
%samp.font-mono{:data-foo => "bar"}= output
.col-span-4[aria-hidden=true]
.tracking-tight#headline
<!-- JSON -->
{
"helloThere": "Hello there, <span class=\"whitespace-nowrap\">Mr. Jones</span>"
}

View file

@ -0,0 +1,12 @@
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@responsive {
.example {
@apply font-bold;
color: theme('colors.red.500');
}
}

View file

@ -0,0 +1,11 @@
@responsive {
.example {
color: red;
}
}
@tailwind screens;
.john {
content: "wick";
}

12
tests/fixtures/tailwind-input.css vendored Normal file
View file

@ -0,0 +1,12 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@responsive {
.example {
@apply font-bold;
color: theme('colors.red.500');
}
}

175412
tests/fixtures/tailwind-output-flagged.css vendored Normal file

File diff suppressed because it is too large Load diff

175412
tests/fixtures/tailwind-output-important.css vendored Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,37 @@
.example {
color: red;
}
@media (min-width: 640px) {
.sm\:example {
color: red;
}
}
@media (min-width: 768px) {
.md\:example {
color: red;
}
}
@media (min-width: 1024px) {
.lg\:example {
color: red;
}
}
@media (min-width: 1280px) {
.xl\:example {
color: red;
}
}
@media (min-width: 1536px) {
.\32xl\:example {
color: red;
}
}
.john {
content: "wick";
}

175412
tests/fixtures/tailwind-output.css vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,81 @@
import flattenColorPalette from '../src/util/flattenColorPalette'
test('it flattens nested color objects', () => {
expect(
flattenColorPalette({
purple: 'purple',
white: {
25: 'rgba(255,255,255,.25)',
50: 'rgba(255,255,255,.5)',
75: 'rgba(255,255,255,.75)',
DEFAULT: '#fff',
},
red: {
1: 'rgb(33,0,0)',
2: 'rgb(67,0,0)',
3: 'rgb(100,0,0)',
},
green: {
1: 'rgb(0,33,0)',
2: 'rgb(0,67,0)',
3: 'rgb(0,100,0)',
},
blue: {
1: 'rgb(0,0,33)',
2: 'rgb(0,0,67)',
3: 'rgb(0,0,100)',
},
})
).toEqual({
purple: 'purple',
'white-25': 'rgba(255,255,255,.25)',
'white-50': 'rgba(255,255,255,.5)',
'white-75': 'rgba(255,255,255,.75)',
white: '#fff',
'red-1': 'rgb(33,0,0)',
'red-2': 'rgb(67,0,0)',
'red-3': 'rgb(100,0,0)',
'green-1': 'rgb(0,33,0)',
'green-2': 'rgb(0,67,0)',
'green-3': 'rgb(0,100,0)',
'blue-1': 'rgb(0,0,33)',
'blue-2': 'rgb(0,0,67)',
'blue-3': 'rgb(0,0,100)',
})
})
test('it flattens deeply nested color objects', () => {
expect(
flattenColorPalette({
primary: 'purple',
secondary: {
DEFAULT: 'blue',
hover: 'cyan',
focus: 'red',
},
button: {
primary: {
DEFAULT: 'magenta',
hover: 'green',
focus: {
DEFAULT: 'yellow',
variant: 'orange',
},
},
},
})
).toEqual({
primary: 'purple',
secondary: 'blue',
'secondary-hover': 'cyan',
'secondary-focus': 'red',
'button-primary': 'magenta',
'button-primary-hover': 'green',
'button-primary-focus': 'yellow',
'button-primary-focus-variant': 'orange',
})
})
test('it handles empty objects', () => {
expect(flattenColorPalette({})).toEqual({})
})

341
tests/jit/apply.test.css Normal file
View file

@ -0,0 +1,341 @@
* {
--tw-shadow: 0 0 #0000;
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgba(59, 130, 246, 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
}
.basic-example {
border-radius: 0.375rem;
--tw-bg-opacity: 1;
background-color: rgba(59, 130, 246, var(--tw-bg-opacity));
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.class-order {
padding: 2rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
padding-top: 1.75rem;
padding-bottom: 1.75rem;
padding-top: 1rem;
padding-right: 0.25rem;
}
.with-additional-properties {
font-weight: 500;
text-align: right;
}
.variants {
font-weight: 600;
}
.variants:hover {
font-weight: 700;
}
.variants:focus {
font-weight: 500;
}
@media (min-width: 1024px) {
.variants {
font-weight: 300;
}
}
@media (min-width: 1280px) {
.variants:focus {
font-weight: 900;
}
}
.only-variants:hover {
font-weight: 700;
}
.only-variants:focus {
font-weight: 500;
}
@media (min-width: 1024px) {
.only-variants {
font-weight: 300;
}
}
@media (min-width: 1280px) {
.only-variants:focus {
font-weight: 900;
}
}
.group:hover .apply-group-variant {
text-align: center;
}
@media (min-width: 1024px) {
.group:hover .apply-group-variant {
text-align: left;
}
}
.dark .apply-dark-variant {
text-align: center;
}
.dark .apply-dark-variant:hover {
text-align: right;
}
@media (min-width: 1024px) {
.dark .apply-dark-variant {
text-align: left;
}
}
.apply-custom-utility {
custom: stuff;
}
.apply-custom-utility:hover {
custom: stuff;
}
@media (min-width: 1024px) {
.apply-custom-utility {
custom: stuff;
}
}
@media (min-width: 1280px) {
.apply-custom-utility:focus {
custom: stuff;
}
}
.multiple,
.selectors {
border-radius: 0.375rem;
--tw-bg-opacity: 1;
background-color: rgba(59, 130, 246, var(--tw-bg-opacity));
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.multiple-variants:hover,
.selectors-variants:hover {
text-align: center;
}
.multiple-variants:active,
.selectors-variants:active {
text-align: right;
}
@media (min-width: 1024px) {
.multiple-variants:focus,
.selectors-variants:focus {
text-align: left;
}
}
.group:hover .multiple-group,
.group:hover .selectors-group {
text-align: center;
}
@media (min-width: 1024px) {
.group:hover .multiple-group,
.group:hover .selectors-group {
text-align: left;
}
}
/* TODO: This works but the generated CSS is unnecessarily verbose. */
.complex-utilities {
--tw-ordinal: var(--tw-empty, /*!*/ /*!*/);
--tw-slashed-zero: var(--tw-empty, /*!*/ /*!*/);
--tw-numeric-figure: var(--tw-empty, /*!*/ /*!*/);
--tw-numeric-spacing: var(--tw-empty, /*!*/ /*!*/);
--tw-numeric-fraction: var(--tw-empty, /*!*/ /*!*/);
font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure)
var(--tw-numeric-spacing) var(--tw-numeric-fraction);
--tw-ordinal: var(--tw-empty, /*!*/ /*!*/);
--tw-slashed-zero: var(--tw-empty, /*!*/ /*!*/);
--tw-numeric-figure: var(--tw-empty, /*!*/ /*!*/);
--tw-numeric-spacing: var(--tw-empty, /*!*/ /*!*/);
--tw-numeric-fraction: var(--tw-empty, /*!*/ /*!*/);
font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure)
var(--tw-numeric-spacing) var(--tw-numeric-fraction);
--tw-ordinal: ordinal;
--tw-numeric-spacing: tabular-nums;
--tw-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.complex-utilities:hover {
--tw-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.complex-utilities:focus {
--tw-ordinal: var(--tw-empty, /*!*/ /*!*/);
--tw-slashed-zero: var(--tw-empty, /*!*/ /*!*/);
--tw-numeric-figure: var(--tw-empty, /*!*/ /*!*/);
--tw-numeric-spacing: var(--tw-empty, /*!*/ /*!*/);
--tw-numeric-fraction: var(--tw-empty, /*!*/ /*!*/);
font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure)
var(--tw-numeric-spacing) var(--tw-numeric-fraction);
--tw-numeric-fraction: diagonal-fractions;
}
.basic-nesting-parent {
.basic-nesting-child {
font-weight: 700;
}
.basic-nesting-child:hover {
font-weight: 400;
}
}
.use-base-only-a {
font-weight: 700;
}
.use-dependant-only-b {
font-weight: 700;
font-weight: 400;
}
.btn {
border-radius: 0.25rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 1rem;
padding-right: 1rem;
font-weight: 700;
}
.btn-blue {
border-radius: 0.25rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 1rem;
padding-right: 1rem;
font-weight: 700;
--tw-bg-opacity: 1;
background-color: rgba(59, 130, 246, var(--tw-bg-opacity));
--tw-text-opacity: 1;
color: rgba(255, 255, 255, var(--tw-text-opacity));
}
.btn-blue:hover {
--tw-bg-opacity: 1;
background-color: rgba(29, 78, 216, var(--tw-bg-opacity));
}
.recursive-apply-a {
font-weight: 900;
}
@media (min-width: 640px) {
.recursive-apply-a {
font-weight: 100;
}
}
.recursive-apply-b {
font-weight: 900;
}
@media (min-width: 640px) {
.recursive-apply-b {
font-weight: 100;
}
}
.recursive-apply-b {
font-weight: 600;
}
@media (min-width: 768px) {
.recursive-apply-b {
font-weight: 200;
}
}
.recursive-apply-c {
font-weight: 900;
}
@media (min-width: 640px) {
.recursive-apply-c {
font-weight: 100;
}
}
.recursive-apply-c {
font-weight: 600;
}
@media (min-width: 768px) {
.recursive-apply-c {
font-weight: 200;
}
}
.recursive-apply-c {
font-weight: 700;
}
@media (min-width: 1024px) {
.recursive-apply-c {
font-weight: 300;
}
}
.use-with-other-properties-base {
color: green;
font-weight: 700;
}
.use-with-other-properties-component {
color: green;
font-weight: 700;
}
.add-sibling-properties {
padding: 2rem;
padding-left: 1rem;
padding-right: 1rem;
}
.add-sibling-properties:hover {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
@media (min-width: 1024px) {
.add-sibling-properties {
padding-left: 2.5rem;
padding-right: 2.5rem;
}
}
@media (min-width: 1280px) {
.add-sibling-properties:focus {
padding-left: 0.25rem;
padding-right: 0.25rem;
}
}
.add-sibling-properties {
padding-top: 3px;
color: green;
font-weight: 700;
}
h1 {
font-size: 1.5rem;
line-height: 2rem;
}
@media (min-width: 640px) {
h1 {
font-size: 1.875rem;
line-height: 2.25rem;
}
}
@media (min-width: 1024px) {
h1 {
font-size: 1.5rem;
line-height: 2rem;
}
}
h2 {
font-size: 1.5rem;
line-height: 2rem;
}
@media (min-width: 1024px) {
h2 {
font-size: 1.5rem;
line-height: 2rem;
}
}
@media (min-width: 640px) {
h2 {
font-size: 1.5rem;
line-height: 2rem;
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.foo {
animation: spin 1s linear infinite;
}
@keyframes pulse {
50% {
opacity: 0.5;
}
}
.bar {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite !important;
}

39
tests/jit/apply.test.html Normal file
View file

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
<link rel="stylesheet" href="./tailwind.css" />
</head>
<body>
<div class="basic-example"></div>
<div class="class-order"></div>
<div class="with-additional-properties"></div>
<div class="variants"></div>
<div class="only-variants"></div>
<div class="apply-group-variant"></div>
<div class="apply-dark-variant"></div>
<div class="apply-custom-utility"></div>
<div class="multiple selectors"></div>
<div class="multiple-variants selectors-variants"></div>
<div class="multiple-group selectors-group"></div>
<div class="complex-utilities"></div>
<div class="basic-nesting-parent">
<div class="basic-nesting-child"></div>
</div>
<div class="use-base-only-a"></div>
<div class="use-dependant-only-b"></div>
<div class="btn"></div>
<div class="btn-blue"></div>
<div class="recursive-apply-a"></div>
<div class="recursive-apply-b"></div>
<div class="recursive-apply-c"></div>
<div class="use-with-other-properties-base use-with-other-properties-component"></div>
<div class="add-sibling-properties"></div>
<div class="a b"></div>
<div class="foo"></div>
<div class="bar"></div>
</body>
</html>

219
tests/jit/apply.test.js Normal file
View file

@ -0,0 +1,219 @@
import postcss from 'postcss'
import fs from 'fs'
import path from 'path'
import tailwind from '../../src/jit/index.js'
function run(input, config = {}) {
const { currentTestName } = expect.getState()
return postcss(tailwind(config)).process(input, {
from: `${path.resolve(__filename)}?test=${currentTestName}`,
})
}
test('@apply', () => {
let config = {
darkMode: 'class',
mode: 'jit',
purge: [path.resolve(__dirname, './apply.test.html')],
corePlugins: { preflight: false },
theme: {},
plugins: [],
}
let css = `
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.basic-example {
@apply px-4 py-2 bg-blue-500 rounded-md;
}
.class-order {
@apply pt-4 pr-1 px-3 py-7 p-8;
}
.with-additional-properties {
font-weight: 500;
@apply text-right;
}
.variants {
@apply xl:focus:font-black hover:font-bold lg:font-light focus:font-medium font-semibold;
}
.only-variants {
@apply xl:focus:font-black hover:font-bold lg:font-light focus:font-medium;
}
.apply-group-variant {
@apply group-hover:text-center lg:group-hover:text-left;
}
.apply-dark-variant {
@apply dark:text-center dark:hover:text-right lg:dark:text-left;
}
.apply-custom-utility {
@apply custom-util hover:custom-util lg:custom-util xl:focus:custom-util;
}
.multiple, .selectors {
@apply px-4 py-2 bg-blue-500 rounded-md;
}
.multiple-variants, .selectors-variants {
@apply hover:text-center active:text-right lg:focus:text-left;
}
.multiple-group, .selectors-group {
@apply group-hover:text-center lg:group-hover:text-left;
}
/* TODO: This works but the generated CSS is unnecessarily verbose. */
.complex-utilities {
@apply ordinal tabular-nums focus:diagonal-fractions shadow-lg hover:shadow-xl;
}
.basic-nesting-parent {
.basic-nesting-child {
@apply font-bold hover:font-normal;
}
}
.use-base-only-a {
@apply font-bold;
}
.use-base-only-b {
@apply use-base-only-a font-normal;
}
.use-dependant-only-a {
@apply font-bold;
}
.use-dependant-only-b {
@apply use-dependant-only-a font-normal;
}
.btn {
@apply font-bold py-2 px-4 rounded;
}
.btn-blue {
@apply btn bg-blue-500 hover:bg-blue-700 text-white;
}
.recursive-apply-a {
@apply font-black sm:font-thin;
}
.recursive-apply-b {
@apply recursive-apply-a font-semibold md:font-extralight;
}
.recursive-apply-c {
@apply recursive-apply-b font-bold lg:font-light;
}
.use-with-other-properties-base {
color: green;
@apply font-bold;
}
.use-with-other-properties-component {
@apply use-with-other-properties-base;
}
.add-sibling-properties {
padding: 2rem;
@apply px-4 hover:px-2 lg:px-10 xl:focus:px-1;
padding-top: 3px;
@apply use-with-other-properties-base;
}
h1 {
@apply text-2xl lg:text-2xl sm:text-3xl;
}
h2 {
@apply text-2xl;
@apply lg:text-2xl;
@apply sm:text-2xl;
}
}
@layer utilities {
.custom-util {
custom: stuff;
}
.foo {
@apply animate-spin;
}
.bar {
@apply animate-pulse !important;
}
}
`
return run(css, config).then((result) => {
let expectedPath = path.resolve(__dirname, './apply.test.css')
let expected = fs.readFileSync(expectedPath, 'utf8')
expect(result.css).toMatchFormattedCss(expected)
})
})
test('@apply error with unknown utility', async () => {
let config = {
darkMode: 'class',
purge: [path.resolve(__dirname, './apply.test.html')],
corePlugins: { preflight: false },
plugins: [],
}
let css = `
@tailwind components;
@tailwind utilities;
@layer components {
.foo {
@apply a-utility-that-does-not-exist;
}
}
`
await expect(run(css, config)).rejects.toThrowError('class does not exist')
})
test('@apply error with nested @screen', async () => {
let config = {
darkMode: 'class',
purge: [path.resolve(__dirname, './apply.test.html')],
corePlugins: { preflight: false },
plugins: [],
}
let css = `
@tailwind components;
@tailwind utilities;
@layer components {
.foo {
@screen md {
@apply text-black;
}
}
}
`
await expect(run(css, config)).rejects.toThrowError(
'@apply is not supported within nested at-rules like @screen'
)
})
test('@apply error with nested @anyatrulehere', async () => {
let config = {
darkMode: 'class',
purge: [path.resolve(__dirname, './apply.test.html')],
corePlugins: { preflight: false },
plugins: [],
}
let css = `
@tailwind components;
@tailwind utilities;
@layer components {
.foo {
@genie {
@apply text-black;
}
}
}
`
await expect(run(css, config)).rejects.toThrowError(
'@apply is not supported within nested at-rules like @genie'
)
})

View file

@ -0,0 +1,369 @@
* {
--tw-shadow: 0 0 #0000;
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgba(59, 130, 246, 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
}
.inset-\[11px\] {
top: 11px;
right: 11px;
bottom: 11px;
left: 11px;
}
.col-\[7\] {
grid-column: 7;
}
.col-start-\[7\] {
grid-column-start: 7;
}
.col-end-\[7\] {
grid-column-end: 7;
}
.row-\[7\] {
grid-row: 7;
}
.row-start-\[7\] {
grid-row-start: 7;
}
.row-end-\[7\] {
grid-row-end: 7;
}
.m-\[7px\] {
margin: 7px;
}
.mx-\[7px\] {
margin-left: 7px;
margin-right: 7px;
}
.my-\[7px\] {
margin-top: 7px;
margin-bottom: 7px;
}
.mt-\[7px\] {
margin-top: 7px;
}
.mr-\[7px\] {
margin-right: 7px;
}
.mb-\[7px\] {
margin-bottom: 7px;
}
.ml-\[7px\] {
margin-left: 7px;
}
.mt-\[clamp\(30px\2c 100px\)\] {
margin-top: clamp(30px, 100px);
}
.h-\[3\.23rem\] {
height: 3.23rem;
}
.h-\[calc\(100\%\+1rem\)\] {
height: calc(100% + 1rem);
}
.h-\[var\(--height\)\] {
height: var(--height);
}
.max-h-\[3\.23rem\] {
max-height: 3.23rem;
}
.max-h-\[calc\(100\%\+1rem\)\] {
max-height: calc(100% + 1rem);
}
.max-h-\[var\(--height\)\] {
max-height: var(--height);
}
.min-h-\[3\.23rem\] {
min-height: 3.23rem;
}
.min-h-\[calc\(100\%\+1rem\)\] {
min-height: calc(100% + 1rem);
}
.min-h-\[var\(--height\)\] {
min-height: var(--height);
}
.w-\[3\.23rem\] {
width: 3.23rem;
}
.w-\[calc\(100\%\+1rem\)\] {
width: calc(100% + 1rem);
}
.w-\[calc\(var\(--10-10px\2c calc\(-20px-\(-30px--40px\)\)\)-50px\)\] {
width: calc(var(--10-10px, calc(-20px - (-30px - -40px))) - 50px);
}
.w-\[var\(--width\)\] {
width: var(--width);
}
.w-\[var\(--width\2c calc\(100\%\+1rem\)\)\] {
width: var(--width, calc(100% + 1rem));
}
.w-\[calc\(100\%\/3-1rem\*2\)\] {
width: calc(100% / 3 - 1rem * 2);
}
.min-w-\[3\.23rem\] {
min-width: 3.23rem;
}
.min-w-\[calc\(100\%\+1rem\)\] {
min-width: calc(100% + 1rem);
}
.min-w-\[var\(--width\)\] {
min-width: var(--width);
}
.max-w-\[3\.23rem\] {
max-width: 3.23rem;
}
.max-w-\[calc\(100\%\+1rem\)\] {
max-width: calc(100% + 1rem);
}
.max-w-\[var\(--width\)\] {
max-width: var(--width);
}
.rotate-\[23deg\] {
--tw-rotate: 23deg;
}
.rotate-\[2\.3rad\] {
--tw-rotate: 2.3rad;
}
.rotate-\[401grad\] {
--tw-rotate: 401grad;
}
.rotate-\[1\.5turn\] {
--tw-rotate: 1.5turn;
}
.skew-x-\[3px\] {
--tw-skew-x: 3px;
}
.skew-y-\[3px\] {
--tw-skew-y: 3px;
}
.grid-cols-\[200px\2c repeat\(auto-fill\2c minmax\(15\%\2c 100px\)\)\2c 300px\] {
grid-template-columns: 200px repeat(auto-fill, minmax(15%, 100px)) 300px;
}
.grid-rows-\[200px\2c repeat\(auto-fill\2c minmax\(15\%\2c 100px\)\)\2c 300px\] {
grid-template-rows: 200px repeat(auto-fill, minmax(15%, 100px)) 300px;
}
.space-x-\[20cm\] > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(20cm * var(--tw-space-x-reverse));
margin-left: calc(20cm * calc(1 - var(--tw-space-x-reverse)));
}
.space-x-\[calc\(20\%-1cm\)\] > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(calc(20% - 1cm) * var(--tw-space-x-reverse));
margin-left: calc(calc(20% - 1cm) * calc(1 - var(--tw-space-x-reverse)));
}
.rounded-\[11px\] {
border-radius: 11px;
}
.rounded-t-\[var\(--radius\)\] {
border-top-left-radius: var(--radius);
border-top-right-radius: var(--radius);
}
.rounded-r-\[var\(--radius\)\] {
border-top-right-radius: var(--radius);
border-bottom-right-radius: var(--radius);
}
.rounded-b-\[var\(--radius\)\] {
border-bottom-right-radius: var(--radius);
border-bottom-left-radius: var(--radius);
}
.rounded-l-\[var\(--radius\)\] {
border-top-left-radius: var(--radius);
border-bottom-left-radius: var(--radius);
}
.rounded-tr-\[var\(--radius\)\] {
border-top-right-radius: var(--radius);
}
.rounded-br-\[var\(--radius\)\] {
border-bottom-right-radius: var(--radius);
}
.rounded-bl-\[var\(--radius\)\] {
border-bottom-left-radius: var(--radius);
}
.rounded-tl-\[var\(--radius\)\] {
border-top-left-radius: var(--radius);
}
.border-\[2\.5px\] {
border-width: 2.5px;
}
.border-\[\#f00\] {
--tw-border-opacity: 1;
border-color: rgba(255, 0, 0, var(--tw-border-opacity));
}
.bg-\[\#0f0\] {
--tw-bg-opacity: 1;
background-color: rgba(0, 255, 0, var(--tw-bg-opacity));
}
.bg-\[\#ff0000\] {
--tw-bg-opacity: 1;
background-color: rgba(255, 0, 0, var(--tw-bg-opacity));
}
.bg-\[\#0000ffcc\] {
background-color: #0000ffcc;
}
.bg-\[rgb\(123\2c 123\2c 123\)\] {
--tw-bg-opacity: 1;
background-color: rgba(123, 123, 123, var(--tw-bg-opacity));
}
.bg-\[rgba\(123\2c 123\2c 123\2c 0\.5\)\] {
background-color: rgba(123, 123, 123, 0.5);
}
.bg-\[hsl\(0\2c 100\%\2c 50\%\)\] {
--tw-bg-opacity: 1;
background-color: hsla(0, 100%, 50%, var(--tw-bg-opacity));
}
.bg-\[hsla\(0\2c 100\%\2c 50\%\2c 0\.3\)\] {
background-color: hsla(0, 100%, 50%, 0.3);
}
.bg-opacity-\[0\.11\] {
--tw-bg-opacity: 0.11;
}
.bg-opacity-\[var\(--value\)\] {
--tw-bg-opacity: var(--value);
}
.from-\[\#da5b66\] {
--tw-gradient-from: #da5b66;
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(218, 91, 102, 0));
}
.from-\[var\(--color\)\] {
--tw-gradient-from: var(--color);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(255, 255, 255, 0));
}
.via-\[\#da5b66\] {
--tw-gradient-stops: var(--tw-gradient-from), #da5b66,
var(--tw-gradient-to, rgba(218, 91, 102, 0));
}
.via-\[var\(--color\)\] {
--tw-gradient-stops: var(--tw-gradient-from), var(--color),
var(--tw-gradient-to, rgba(255, 255, 255, 0));
}
.to-\[\#da5b66\] {
--tw-gradient-to: #da5b66;
}
.to-\[var\(--color\)\] {
--tw-gradient-to: var(--color);
}
.fill-\[\#da5b66\] {
fill: #da5b66;
}
.fill-\[var\(--color\)\] {
fill: var(--color);
}
.stroke-\[\#da5b66\] {
stroke: #da5b66;
}
.p-\[var\(--app-padding\)\] {
padding: var(--app-padding);
}
.text-\[2\.23rem\] {
font-size: 2.23rem;
}
.leading-\[var\(--leading\)\] {
line-height: var(--leading);
}
.tracking-\[var\(--tracking\)\] {
letter-spacing: var(--tracking);
}
.placeholder-\[var\(--placeholder\)\]::placeholder {
color: var(--placeholder);
}
.placeholder-opacity-\[var\(--placeholder-opacity\)\]::placeholder {
--tw-placeholder-opacity: var(--placeholder-opacity);
}
.opacity-\[var\(--opacity\)\] {
opacity: var(--opacity);
}
.outline-\[var\(--outline\)\] {
outline: var(--outline);
outline-offset: 0;
}
.ring-\[10px\] {
--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(10px + var(--tw-ring-offset-width))
var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.ring-\[\#76ad65\] {
--tw-ring-opacity: 1;
--tw-ring-color: rgba(118, 173, 101, var(--tw-ring-opacity));
}
.ring-opacity-\[var\(--ring-opacity\)\] {
--tw-ring-opacity: var(--ring-opacity);
}
.ring-offset-\[19rem\] {
--tw-ring-offset-width: 19rem;
}
.ring-offset-\[\#76ad65\] {
--tw-ring-offset-color: #76ad65;
}
.ring-offset-\[\#ad672f\] {
--tw-ring-offset-color: #ad672f;
}
.blur-\[15px\] {
--tw-blur: blur(15px);
}
.brightness-\[300\%\] {
--tw-brightness: brightness(300%);
}
.contrast-\[2\.4\] {
--tw-contrast: contrast(2.4);
}
.grayscale-\[0\.55\] {
--tw-grayscale: grayscale(0.55);
}
.hue-rotate-\[0\.8turn\] {
--tw-hue-rotate: hue-rotate(0.8turn);
}
.invert-\[0\.75\] {
--tw-invert: invert(0.75);
}
.saturate-\[180\%\] {
--tw-saturate: saturate(180%);
}
.sepia-\[0\.2\] {
--tw-sepia: sepia(0.2);
}
.backdrop-blur-\[11px\] {
--tw-backdrop-blur: blur(11px);
}
.backdrop-brightness-\[1\.23\] {
--tw-backdrop-brightness: brightness(1.23);
}
.backdrop-contrast-\[0\.87\] {
--tw-backdrop-contrast: contrast(0.87);
}
.backdrop-grayscale-\[0\.42\] {
--tw-backdrop-grayscale: grayscale(0.42);
}
.backdrop-hue-rotate-\[1\.57rad\] {
--tw-backdrop-hue-rotate: hue-rotate(1.57rad);
}
.backdrop-invert-\[0\.66\] {
--tw-backdrop-invert: invert(0.66);
}
.backdrop-opacity-\[22\%\] {
--tw-backdrop-opacity: opacity(22%);
}
.backdrop-saturate-\[144\%\] {
--tw-backdrop-saturate: saturate(144%);
}
.backdrop-sepia-\[0\.38\] {
--tw-backdrop-sepia: sepia(0.38);
}
.delay-\[var\(--delay\)\] {
transition-delay: var(--delay);
}
.duration-\[2s\] {
transition-duration: 2s;
}
.duration-\[var\(--app-duration\)\] {
transition-duration: var(--app-duration);
}
@media (min-width: 1024px) {
.lg\:grid-cols-\[200px\2c repeat\(auto-fill\2c minmax\(15\%\2c 100px\)\)\2c 300px\] {
grid-template-columns: 200px repeat(auto-fill, minmax(15%, 100px)) 300px;
}
}

View file

@ -0,0 +1,109 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
<link rel="stylesheet" href="./tailwind.css" />
</head>
<body>
<div class="bg-[#0f0] bg-[#ff0000] bg-[#0000ffcc]"></div>
<div class="bg-[rgb(123,123,123)] bg-[rgba(123,123,123,0.5)]"></div>
<div class="bg-[hsl(0,100%,50%)] bg-[hsla(0,100%,50%,0.3)]"></div>
<div class="bg-opacity-[0.11]"></div>
<div class="bg-opacity-[var(--value)]"></div>
<div class="border-[#f00]"></div>
<div class="border-[2.5px]"></div>
<div class="w-[3.23rem]"></div>
<div class="w-[calc(100%+1rem)]"></div>
<div class="w-[calc(var(--10-10px,calc(-20px-(-30px--40px)))-50px)]"></div>
<div class="w-[var(--width)]"></div>
<div class="w-[var(--width,calc(100%+1rem))]"></div>
<div class="w-[calc(100%/3-1rem*2)]"></div>
<div class="min-w-[3.23rem]"></div>
<div class="min-w-[calc(100%+1rem)]"></div>
<div class="min-w-[var(--width)]"></div>
<div class="max-w-[3.23rem]"></div>
<div class="max-w-[calc(100%+1rem)]"></div>
<div class="max-w-[var(--width)]"></div>
<div class="h-[3.23rem]"></div>
<div class="h-[calc(100%+1rem)]"></div>
<div class="h-[var(--height)]"></div>
<div class="min-h-[3.23rem]"></div>
<div class="min-h-[calc(100%+1rem)]"></div>
<div class="min-h-[var(--height)]"></div>
<div class="max-h-[3.23rem]"></div>
<div class="max-h-[calc(100%+1rem)]"></div>
<div class="max-h-[var(--height)]"></div>
<div class="space-x-[20cm]"></div>
<div class="space-x-[calc(20%-1cm)]"></div>
<div class="grid-cols-[200px,repeat(auto-fill,minmax(15%,100px)),300px]"></div>
<div class="lg:grid-cols-[200px,repeat(auto-fill,minmax(15%,100px)),300px]"></div>
<div class="grid-rows-[200px,repeat(auto-fill,minmax(15%,100px)),300px]"></div>
<div class="col-[7]"></div>
<div class="col-end-[7]"></div>
<div class="col-start-[7]"></div>
<div class="row-[7]"></div>
<div class="row-end-[7]"></div>
<div class="row-start-[7]"></div>
<div class="rotate-[23deg] rotate-[2.3rad] rotate-[401grad] rotate-[1.5turn]"></div>
<div class="skew-x-[3px]"></div>
<div class="skew-y-[3px]"></div>
<div class="text-[2.23rem]"></div>
<div class="duration-[2s]"></div>
<div class="m-[7px]"></div>
<div class="mx-[7px]"></div>
<div class="my-[7px]"></div>
<div class="mt-[7px]"></div>
<div class="mr-[7px]"></div>
<div class="mb-[7px]"></div>
<div class="ml-[7px]"></div>
<div class="mt-[clamp(30px,100px)]"></div>
<div class="rounded-[11px]"></div>
<div
class="rounded-t-[var(--radius)] rounded-r-[var(--radius)] rounded-b-[var(--radius)] rounded-l-[var(--radius)]"
></div>
<div
class="rounded-tr-[var(--radius)] rounded-br-[var(--radius)] rounded-bl-[var(--radius)] rounded-tl-[var(--radius)]"
></div>
<div class="duration-[var(--app-duration)]"></div>
<div class="p-[var(--app-padding)]"></div>
<div class="inset-[11px]"></div>
<div class="blur-[15px]"></div>
<div class="brightness-[300%]"></div>
<div class="contrast-[2.4]"></div>
<div class="grayscale-[0.55]"></div>
<div class="hue-rotate-[0.8turn]"></div>
<div class="invert-[0.75]"></div>
<div class="saturate-[180%]"></div>
<div class="sepia-[0.2]"></div>
<div class="backdrop-blur-[11px]"></div>
<div class="backdrop-brightness-[1.23]"></div>
<div class="backdrop-contrast-[0.87]"></div>
<div class="backdrop-grayscale-[0.42]"></div>
<div class="backdrop-hue-rotate-[1.57rad]"></div>
<div class="backdrop-invert-[0.66]"></div>
<div class="backdrop-opacity-[22%]"></div>
<div class="backdrop-saturate-[144%]"></div>
<div class="backdrop-sepia-[0.38]"></div>
<div class="from-[#da5b66] via-[#da5b66] to-[#da5b66]"></div>
<div class="from-[var(--color)] via-[var(--color)] to-[var(--color)]"></div>
<div class="fill-[#da5b66]"></div>
<div class="fill-[var(--color)]"></div>
<div class="stroke-[#da5b66]"></div>
<div class="leading-[var(--leading)]"></div>
<div class="tracking-[var(--tracking)]"></div>
<div class="placeholder-[var(--placeholder)]"></div>
<div class="placeholder-opacity-[var(--placeholder-opacity)]"></div>
<div class="opacity-[var(--opacity)]"></div>
<div class="outline-[var(--outline)]"></div>
<div class="ring-[#76ad65]"></div>
<div class="ring-offset-[#76ad65]"></div>
<div class="ring-[10px]"></div>
<div class="ring-offset-[#ad672f]"></div>
<div class="ring-offset-[19rem]"></div>
<div class="ring-opacity-[var(--ring-opacity)]"></div>
<div class="delay-[var(--delay)]"></div>
</body>
</html>

View file

@ -0,0 +1,31 @@
import postcss from 'postcss'
import fs from 'fs'
import path from 'path'
import tailwind from '../../src/jit/index.js'
function run(input, config = {}) {
return postcss(tailwind(config)).process(input, { from: path.resolve(__filename) })
}
test('arbitrary values', () => {
let config = {
mode: 'jit',
purge: [path.resolve(__dirname, './arbitrary-values.test.html')],
corePlugins: { preflight: false },
theme: {},
plugins: [],
}
let css = `
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(css, config).then((result) => {
let expectedPath = path.resolve(__dirname, './arbitrary-values.test.css')
let expected = fs.readFileSync(expectedPath, 'utf8')
expect(result.css).toMatchFormattedCss(expected)
})
})

View file

@ -0,0 +1,757 @@
* {
--tw-shadow: 0 0 #0000;
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgba(59, 130, 246, 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
}
.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;
}
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.pointer-events-none {
pointer-events: none;
}
.invisible {
visibility: hidden;
}
.absolute {
position: absolute;
}
.inset-0 {
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
}
.inset-y-4 {
top: 1rem;
bottom: 1rem;
}
.inset-x-2 {
left: 0.5rem;
right: 0.5rem;
}
.top-6 {
top: 1.5rem;
}
.right-8 {
right: 2rem;
}
.bottom-12 {
bottom: 3rem;
}
.left-16 {
left: 4rem;
}
.isolate {
isolation: isolate;
}
.isolation-auto {
isolation: auto;
}
.z-30 {
z-index: 30;
}
.order-last {
order: 9999;
}
.order-2 {
order: 2;
}
.col-span-3 {
grid-column: span 3 / span 3;
}
.col-start-1 {
grid-column-start: 1;
}
.col-end-4 {
grid-column-end: 4;
}
.row-span-2 {
grid-row: span 2 / span 2;
}
.row-start-3 {
grid-row-start: 3;
}
.row-end-5 {
grid-row-end: 5;
}
.float-right {
float: right;
}
.clear-left {
clear: left;
}
.m-4 {
margin: 1rem;
}
.my-2 {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.mt-0 {
margin-top: 0px;
}
.mr-1 {
margin-right: 0.25rem;
}
.mb-3 {
margin-bottom: 0.75rem;
}
.ml-4 {
margin-left: 1rem;
}
.box-border {
box-sizing: border-box;
}
.inline-grid {
display: inline-grid;
}
.hidden {
display: none;
}
.h-16 {
height: 4rem;
}
.max-h-screen {
max-height: 100vh;
}
.min-h-0 {
min-height: 0px;
}
.w-12 {
width: 3rem;
}
.min-w-min {
min-width: min-content;
}
.max-w-full {
max-width: 100%;
}
.flex-1 {
flex: 1 1 0%;
}
.flex-shrink {
flex-shrink: 1;
}
.flex-shrink-0 {
flex-shrink: 0;
}
.flex-grow {
flex-grow: 1;
}
.flex-grow-0 {
flex-grow: 0;
}
.table-fixed {
table-layout: fixed;
}
.border-collapse {
border-collapse: collapse;
}
.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));
}
.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));
}
.origin-top-right {
transform-origin: top right;
}
.translate-x-5 {
--tw-translate-x: 1.25rem;
}
.-translate-x-4 {
--tw-translate-x: -1rem;
}
.translate-y-6 {
--tw-translate-y: 1.5rem;
}
.-translate-x-3 {
--tw-translate-x: -0.75rem;
}
.rotate-3 {
--tw-rotate: 3deg;
}
.skew-y-12 {
--tw-skew-y: 12deg;
}
.skew-x-12 {
--tw-skew-x: 12deg;
}
.scale-95 {
--tw-scale-x: 0.95;
--tw-scale-y: 0.95;
}
.animate-none {
animation: none;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
.cursor-pointer {
cursor: pointer;
}
.select-none {
user-select: none;
}
.resize-none {
resize: none;
}
.list-inside {
list-style-position: inside;
}
.list-disc {
list-style-type: disc;
}
.appearance-none {
appearance: none;
}
.auto-cols-min {
grid-auto-columns: min-content;
}
.grid-flow-row {
grid-auto-flow: row;
}
.auto-rows-max {
grid-auto-rows: max-content;
}
.grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.grid-rows-3 {
grid-template-rows: repeat(3, minmax(0, 1fr));
}
.flex-row-reverse {
flex-direction: row-reverse;
}
.flex-wrap {
flex-wrap: wrap;
}
.place-content-start {
place-content: start;
}
.place-items-end {
place-items: end;
}
.content-center {
align-content: center;
}
.items-start {
align-items: flex-start;
}
.justify-center {
justify-content: center;
}
.justify-items-end {
justify-items: end;
}
.gap-4 {
gap: 1rem;
}
.gap-x-2 {
column-gap: 0.5rem;
}
.gap-y-3 {
row-gap: 0.75rem;
}
.space-x-4 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(1rem * var(--tw-space-x-reverse));
margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-y-3 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
}
.space-y-reverse > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 1;
}
.space-x-reverse > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 1;
}
.divide-x-2 > :not([hidden]) ~ :not([hidden]) {
--tw-divide-x-reverse: 0;
border-right-width: calc(2px * var(--tw-divide-x-reverse));
border-left-width: calc(2px * calc(1 - var(--tw-divide-x-reverse)));
}
.divide-y-4 > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(4px * calc(1 - var(--tw-divide-y-reverse)));
border-bottom-width: calc(4px * var(--tw-divide-y-reverse));
}
.divide-x-0 > :not([hidden]) ~ :not([hidden]) {
--tw-divide-x-reverse: 0;
border-right-width: calc(0px * var(--tw-divide-x-reverse));
border-left-width: calc(0px * calc(1 - var(--tw-divide-x-reverse)));
}
.divide-y-0 > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(0px * calc(1 - var(--tw-divide-y-reverse)));
border-bottom-width: calc(0px * var(--tw-divide-y-reverse));
}
.divide-dotted > :not([hidden]) ~ :not([hidden]) {
border-style: dotted;
}
.divide-gray-200 > :not([hidden]) ~ :not([hidden]) {
--tw-divide-opacity: 1;
border-color: rgba(229, 231, 235, var(--tw-divide-opacity));
}
.divide-opacity-50 > :not([hidden]) ~ :not([hidden]) {
--tw-divide-opacity: 0.5;
}
.place-self-center {
place-self: center;
}
.self-end {
align-self: flex-end;
}
.justify-self-start {
justify-self: start;
}
.overflow-hidden {
overflow: hidden;
}
.overscroll-contain {
overscroll-behavior: contain;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.overflow-ellipsis {
text-overflow: ellipsis;
}
.whitespace-nowrap {
white-space: nowrap;
}
.break-words {
overflow-wrap: break-word;
}
.rounded-md {
border-radius: 0.375rem;
}
.border {
border-width: 1px;
}
.border-2 {
border-width: 2px;
}
.border-solid {
border-style: solid;
}
.border-black {
--tw-border-opacity: 1;
border-color: rgba(0, 0, 0, var(--tw-border-opacity));
}
.border-opacity-10 {
--tw-border-opacity: 0.1;
}
.bg-green-500 {
--tw-bg-opacity: 1;
background-color: rgba(16, 185, 129, var(--tw-bg-opacity));
}
.bg-opacity-20 {
--tw-bg-opacity: 0.2;
}
.bg-gradient-to-r {
background-image: linear-gradient(to right, var(--tw-gradient-stops));
}
.from-red-300 {
--tw-gradient-from: #fca5a5;
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(252, 165, 165, 0));
}
.via-purple-200 {
--tw-gradient-stops: var(--tw-gradient-from), #ddd6fe,
var(--tw-gradient-to, rgba(221, 214, 254, 0));
}
.to-blue-400 {
--tw-gradient-to: #60a5fa;
}
.decoration-slice {
box-decoration-break: slice;
}
.decoration-clone {
box-decoration-break: clone;
}
.bg-cover {
background-size: cover;
}
.bg-local {
background-attachment: local;
}
.bg-clip-border {
background-clip: border-box;
}
.bg-top {
background-position: top;
}
.bg-no-repeat {
background-repeat: no-repeat;
}
.bg-origin-border {
background-origin: border-box;
}
.bg-origin-padding {
background-origin: padding-box;
}
.bg-origin-content {
background-origin: content-box;
}
.fill-current {
fill: currentColor;
}
.stroke-current {
stroke: currentColor;
}
.stroke-2 {
stroke-width: 2;
}
.object-cover {
object-fit: cover;
}
.object-bottom {
object-position: bottom;
}
.p-4 {
padding: 1rem;
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.px-3 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.pt-1 {
padding-top: 0.25rem;
}
.pr-2 {
padding-right: 0.5rem;
}
.pb-3 {
padding-bottom: 0.75rem;
}
.pl-4 {
padding-left: 1rem;
}
.text-center {
text-align: center;
}
.align-middle {
vertical-align: middle;
}
.font-sans {
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
}
.text-2xl {
font-size: 1.5rem;
line-height: 2rem;
}
.font-medium {
font-weight: 500;
}
.uppercase {
text-transform: uppercase;
}
.not-italic {
font-style: normal;
}
.ordinal,
.slashed-zero,
.lining-nums,
.oldstyle-nums,
.proportional-nums,
.tabular-nums,
.diagonal-fractions,
.stacked-fractions {
--tw-ordinal: var(--tw-empty, /*!*/ /*!*/);
--tw-slashed-zero: var(--tw-empty, /*!*/ /*!*/);
--tw-numeric-figure: var(--tw-empty, /*!*/ /*!*/);
--tw-numeric-spacing: var(--tw-empty, /*!*/ /*!*/);
--tw-numeric-fraction: var(--tw-empty, /*!*/ /*!*/);
font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure)
var(--tw-numeric-spacing) var(--tw-numeric-fraction);
}
.tabular-nums {
--tw-numeric-spacing: tabular-nums;
}
.ordinal {
--tw-ordinal: ordinal;
}
.diagonal-fractions {
--tw-numeric-fraction: diagonal-fractions;
}
.leading-relaxed {
line-height: 1.625;
}
.leading-5 {
line-height: 1.25rem;
}
.tracking-tight {
letter-spacing: -0.025em;
}
.text-indigo-500 {
--tw-text-opacity: 1;
color: rgba(99, 102, 241, var(--tw-text-opacity));
}
.text-opacity-10 {
--tw-text-opacity: 0.1;
}
.underline {
text-decoration: underline;
}
.antialiased {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.placeholder-green-300::placeholder {
--tw-placeholder-opacity: 1;
color: rgba(110, 231, 183, var(--tw-placeholder-opacity));
}
.placeholder-opacity-60::placeholder {
--tw-placeholder-opacity: 0.6;
}
.opacity-90 {
opacity: 0.9;
}
.bg-blend-darken {
background-blend-mode: darken;
}
.bg-blend-difference {
background-blend-mode: difference;
}
.mix-blend-multiply {
mix-blend-mode: multiply;
}
.mix-blend-saturation {
mix-blend-mode: saturation;
}
.shadow {
--tw-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.shadow-lg {
--tw-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.outline-none {
outline: 2px solid transparent;
outline-offset: 2px;
}
.outline-black {
outline: 2px dotted black;
outline-offset: 2px;
}
.ring {
--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(3px + var(--tw-ring-offset-width))
var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.ring-4 {
--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(4px + var(--tw-ring-offset-width))
var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.ring-white {
--tw-ring-opacity: 1;
--tw-ring-color: rgba(255, 255, 255, var(--tw-ring-opacity));
}
.ring-opacity-40 {
--tw-ring-opacity: 0.4;
}
.ring-offset-2 {
--tw-ring-offset-width: 2px;
}
.ring-offset-blue-300 {
--tw-ring-offset-color: #93c5fd;
}
.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);
}
.filter-none {
filter: none;
}
.blur-md {
--tw-blur: blur(12px);
}
.brightness-150 {
--tw-brightness: brightness(1.5);
}
.contrast-50 {
--tw-contrast: contrast(0.5);
}
.drop-shadow-md {
--tw-drop-shadow: drop-shadow(0 4px 3px rgba(0, 0, 0, 0.07))
drop-shadow(0 2px 2px rgba(0, 0, 0, 0.06));
}
.grayscale {
--tw-grayscale: grayscale(100%);
}
.hue-rotate-60 {
--tw-hue-rotate: hue-rotate(60deg);
}
.invert {
--tw-invert: invert(100%);
}
.saturate-200 {
--tw-saturate: saturate(2);
}
.sepia {
--tw-sepia: sepia(100%);
}
.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);
}
.backdrop-filter-none {
backdrop-filter: none;
}
.backdrop-blur-lg {
--tw-backdrop-blur: blur(16px);
}
.backdrop-brightness-50 {
--tw-backdrop-brightness: brightness(0.5);
}
.backdrop-contrast-0 {
--tw-backdrop-contrast: contrast(0);
}
.backdrop-grayscale {
--tw-backdrop-grayscale: grayscale(100%);
}
.backdrop-hue-rotate-90 {
--tw-backdrop-hue-rotate: hue-rotate(90deg);
}
.backdrop-invert {
--tw-backdrop-invert: invert(100%);
}
.backdrop-opacity-75 {
--tw-backdrop-opacity: opacity(0.75);
}
.backdrop-saturate-150 {
--tw-backdrop-saturate: saturate(1.5);
}
.backdrop-sepia {
--tw-backdrop-sepia: sepia(100%);
}
.transition {
transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow,
transform, filter, backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.transition-all {
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.delay-300 {
transition-delay: 300ms;
}
.duration-200 {
transition-duration: 200ms;
}
.ease-in-out {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

View file

@ -0,0 +1,147 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
<link rel="stylesheet" href="./tailwind.css" />
</head>
<body>
<div class="sr-only"></div>
<div class="content-center"></div>
<div class="items-start"></div>
<div class="self-end"></div>
<div class="animate-none"></div>
<div class="animate-spin"></div>
<div class="appearance-none"></div>
<div class="bg-local"></div>
<div class="bg-clip-border"></div>
<div class="bg-green-500"></div>
<div class="bg-gradient-to-r"></div>
<div class="bg-opacity-20"></div>
<div class="bg-top"></div>
<div class="bg-no-repeat"></div>
<div class="bg-cover"></div>
<div class="bg-origin-border bg-origin-padding bg-origin-content"></div>
<div class="border-collapse"></div>
<div class="border-black"></div>
<div class="border-opacity-10"></div>
<div class="rounded-md"></div>
<div class="border-solid"></div>
<div class="border"></div>
<div class="border-2"></div>
<div class="shadow"></div>
<div class="shadow-md"></div>
<div class="shadow-lg"></div>
<div class="decoration-clone decoration-slice"></div>
<div class="box-border"></div>
<div class="clear-left"></div>
<div class="container"></div>
<div class="cursor-pointer"></div>
<div class="hidden inline-grid"></div>
<div class="divide-gray-200"></div>
<div class="divide-opacity-50"></div>
<div class="divide-dotted"></div>
<div class="divide-x-2 divide-y-4 divide-x-0 divide-y-0"></div>
<div class="fill-current"></div>
<div class="flex-1"></div>
<div class="flex-row-reverse"></div>
<div class="flex-grow"></div>
<div class="flex-grow-0"></div>
<div class="flex-shrink"></div>
<div class="flex-shrink-0"></div>
<div class="flex-wrap"></div>
<div class="float-right"></div>
<div class="font-sans"></div>
<div class="text-2xl"></div>
<div class="antialiased"></div>
<div class="not-italic"></div>
<div class="tabular-nums ordinal diagonal-fractions"></div>
<div class="font-medium"></div>
<div class="gap-x-2 gap-y-3 gap-4"></div>
<div class="from-red-300 via-purple-200 to-blue-400"></div>
<div class="auto-cols-min"></div>
<div class="grid-flow-row"></div>
<div class="auto-rows-max"></div>
<div class="col-span-3"></div>
<div class="col-start-1"></div>
<div class="col-end-4"></div>
<div class="row-span-2"></div>
<div class="row-start-3"></div>
<div class="row-end-5"></div>
<div class="grid-cols-4"></div>
<div class="grid-rows-3"></div>
<div class="h-16"></div>
<div class="inset-0 inset-y-4 inset-x-2 top-6 right-8 bottom-12 left-16"></div>
<div class="isolate isolation-auto"></div>
<div class="justify-center"></div>
<div class="justify-items-end"></div>
<div class="justify-self-start"></div>
<div class="tracking-tight"></div>
<div class="leading-relaxed leading-5"></div>
<div class="list-inside"></div>
<div class="list-disc"></div>
<div class="m-4 my-2 mx-auto mt-0 mr-1 mb-3 ml-4"></div>
<div class="max-h-screen"></div>
<div class="max-w-full"></div>
<div class="min-h-0"></div>
<div class="min-w-min"></div>
<div class="object-cover"></div>
<div class="object-bottom"></div>
<div class="opacity-90"></div>
<div class="bg-blend-darken bg-blend-difference"></div>
<div class="mix-blend-multiply mix-blend-saturation"></div>
<div class="order-last order-2"></div>
<div class="outline-none outline-black"></div>
<div class="overflow-hidden"></div>
<div class="overscroll-contain"></div>
<div class="p-4 py-2 px-3 pt-1 pr-2 pb-3 pl-4"></div>
<div class="place-content-start"></div>
<div class="placeholder-green-300"></div>
<div class="placeholder-opacity-60"></div>
<div class="place-items-end"></div>
<div class="place-self-center"></div>
<div class="pointer-events-none"></div>
<div class="absolute"></div>
<div class="resize-none"></div>
<div class="ring-white"></div>
<div class="ring-offset-blue-300"></div>
<div class="ring-offset-2"></div>
<div class="ring-opacity-40"></div>
<div class="ring ring-4"></div>
<div
class="filter filter-none blur-md brightness-150 contrast-50 drop-shadow-md grayscale hue-rotate-60 invert saturate-200 sepia"
></div>
<div
class="backdrop-filter backdrop-filter-none backdrop-blur-lg backdrop-brightness-50 backdrop-contrast-0 backdrop-grayscale backdrop-hue-rotate-90 backdrop-invert backdrop-opacity-75 backdrop-saturate-150 backdrop-sepia"
></div>
<div class="rotate-3"></div>
<div class="scale-95"></div>
<div class="skew-y-12 skew-x-12"></div>
<div class="space-x-4 space-y-3 space-x-reverse space-y-reverse"></div>
<div class="stroke-current"></div>
<div class="stroke-2"></div>
<div class="table-fixed"></div>
<div class="text-center"></div>
<div class="text-indigo-500"></div>
<div class="underline"></div>
<div class="text-opacity-10"></div>
<div class="overflow-ellipsis truncate"></div>
<div class="uppercase"></div>
<div class="transform transform-gpu"></div>
<div class="origin-top-right"></div>
<div class="delay-300"></div>
<div class="duration-200"></div>
<div class="transition transition-all"></div>
<div class="ease-in-out"></div>
<div class="translate-x-5 -translate-x-4 translate-y-6 -translate-x-3"></div>
<div class="select-none"></div>
<div class="align-middle"></div>
<div class="invisible"></div>
<div class="whitespace-nowrap"></div>
<div class="w-12"></div>
<div class="break-words"></div>
<div class="z-30"></div>
</body>
</html>

View file

@ -0,0 +1,33 @@
import postcss from 'postcss'
import fs from 'fs'
import path from 'path'
import tailwind from '../../src/jit/index.js'
function run(input, config = {}) {
return postcss(tailwind(config)).process(input, {
from: path.resolve(__filename),
})
}
test('basic usage', () => {
let config = {
mode: 'jit',
purge: [path.resolve(__dirname, './basic-usage.test.html')],
corePlugins: { preflight: false },
theme: {},
plugins: [],
}
let css = `
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(css, config).then((result) => {
let expectedPath = path.resolve(__dirname, './basic-usage.test.css')
let expected = fs.readFileSync(expectedPath, 'utf8')
expect(result.css).toMatchFormattedCss(expected)
})
})

View file

@ -0,0 +1,56 @@
* {
--tw-shadow: 0 0 #0000;
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgba(59, 130, 246, 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
}
@font-face {
font-family: 'Poppins';
src: url('/fonts/Poppins.woff2') format('woff2'), url('/fonts/Poppins.woff') format('woff');
}
@font-face {
font-family: 'Proxima Nova';
src: url('/fonts/ProximaNova.woff2') format('woff2'),
url('/fonts/ProximaNova.woff') format('woff');
}
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter.woff2') format('woff2'), url('/fonts/Inter.woff') format('woff');
}
@font-face {
font-family: 'Gilroy';
src: url('/fonts/Gilroy.woff2') format('woff2'), url('/fonts/Gilroy.woff') format('woff');
}
@page {
margin: 1cm;
}
.font-bold {
font-weight: 700;
}
@media (min-width: 640px) {
.sm\:text-center {
text-align: center;
}
.sm\:font-bold {
font-weight: 700;
}
}
@media (min-width: 768px) {
.md\:text-center {
text-align: center;
}
.md\:font-bold {
font-weight: 700;
}
}
@media (min-width: 1024px) {
.lg\:text-center {
text-align: center;
}
.lg\:font-bold {
font-weight: 700;
}
}

View file

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
<link rel="stylesheet" href="./tailwind.css" />
</head>
<body>
<div class="custom-component"></div>
<div class="sm:custom-component"></div>
<div class="md:custom-component"></div>
<div class="lg:custom-component"></div>
<div class="font-bold"></div>
<div class="sm:font-bold"></div>
<div class="sm:text-center"></div>
<div class="md:font-bold"></div>
<div class="md:text-center"></div>
<div class="lg:font-bold"></div>
<div class="lg:text-center"></div>
</body>
</html>

View file

@ -0,0 +1,58 @@
import postcss from 'postcss'
import fs from 'fs'
import path from 'path'
import tailwind from '../../src/jit/index.js'
function run(input, config = {}) {
return postcss(tailwind(config)).process(input, {
from: path.resolve(__filename),
})
}
test('collapse adjacent rules', () => {
let config = {
mode: 'jit',
purge: [path.resolve(__dirname, './collapse-adjacent-rules.test.html')],
corePlugins: { preflight: false },
theme: {},
plugins: [],
}
let css = `
@tailwind base;
@font-face {
font-family: "Inter";
src: url("/fonts/Inter.woff2") format("woff2"),
url("/fonts/Inter.woff") format("woff");
}
@font-face {
font-family: "Gilroy";
src: url("/fonts/Gilroy.woff2") format("woff2"),
url("/fonts/Gilroy.woff") format("woff");
}
@page {
margin: 1cm;
}
@tailwind components;
@tailwind utilities;
@layer base {
@font-face {
font-family: "Poppins";
src: url("/fonts/Poppins.woff2") format("woff2"),
url("/fonts/Poppins.woff") format("woff");
}
@font-face {
font-family: "Proxima Nova";
src: url("/fonts/ProximaNova.woff2") format("woff2"),
url("/fonts/ProximaNova.woff") format("woff");
}
}
`
return run(css, config).then((result) => {
let expectedPath = path.resolve(__dirname, './collapse-adjacent-rules.test.css')
let expected = fs.readFileSync(expectedPath, 'utf8')
expect(result.css).toMatchFormattedCss(expected)
})
})

View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
<link rel="stylesheet" href="./tailwind.css" />
</head>
<body></body>
</html>

View file

@ -0,0 +1,17 @@
const path = require('path')
const process = require('child_process')
const { promisify } = require('util')
const exec = promisify(process.exec)
test.skip('a build re-uses the context across multiple files with the same config', async () => {
const filepath = path.resolve(__dirname, './context-reuse.worker.js')
const { stdout } = await exec(`node ${filepath}`)
const results = JSON.parse(stdout.trim())
expect(results[0].activeContexts).toBe(1)
expect(results[1].activeContexts).toBe(1)
expect(results[2].activeContexts).toBe(1)
expect(results[3].activeContexts).toBe(1)
})

View file

@ -0,0 +1,48 @@
const fs = require('fs')
const path = require('path')
const postcss = require('postcss')
const tailwind = require('../index.js')
const sharedState = require('../lib/sharedState.js')
const configPath = path.resolve(__dirname, './context-reuse.tailwind.config.js')
function run(input, config = {}, from = null) {
from = from || path.resolve(__filename)
return postcss(tailwind(config)).process(input, { from })
}
async function runTest() {
let messages = []
let config = {
darkMode: 'class',
mode: 'jit',
purge: [path.resolve(__dirname, './context-reuse.test.html')],
corePlugins: { preflight: false },
theme: {},
plugins: [],
}
let from = path.resolve(__filename)
fs.writeFileSync(configPath, `module.exports = ${JSON.stringify(config)};`)
let results = [
await run(`@tailwind utilities;`, configPath, `${from}?id=1`),
await run(`body { @apply bg-blue-400; }`, configPath, `${from}?id=2`),
await run(`body { @apply text-red-400; }`, configPath, `${from}?id=3`),
await run(`body { @apply mb-4; }`, configPath, `${from}?id=4`),
]
results.forEach(() => {
messages.push({
activeContexts: sharedState.contextSourcesMap.size,
})
})
process.stdout.write(JSON.stringify(messages))
}
runTest().finally(() => {
fs.unlinkSync(configPath)
})

View file

@ -0,0 +1,17 @@
* {
--tw-shadow: 0 0 #0000;
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgba(59, 130, 246, 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
}
.bg-white {
--tw-bg-opacity: 1;
background-color: rgba(255, 255, 255, var(--tw-bg-opacity));
}
.text-indigo-500 {
--tw-text-opacity: 1;
color: rgba(99, 102, 241, var(--tw-text-opacity));
}

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
<link rel="stylesheet" href="./tailwind.css" />
</head>
<body>
<div class="text-indigo-500 bg-white">hello world</div>
<span>text-red-500 shouldn't appear in the output</span>
</body>
</html>

View file

@ -0,0 +1,67 @@
import postcss from 'postcss'
import fs from 'fs'
import path from 'path'
function run(input, config = {}) {
jest.resetModules()
const tailwind = require('../../src/jit/index.js').default
return postcss(tailwind(config)).process(input, {
from: path.resolve(__filename),
})
}
function customExtractor(content) {
const matches = content.match(/class="([^"]+)"/)
return matches ? matches[1].split(/\s+/) : []
}
const css = `
@tailwind base;
@tailwind components;
@tailwind utilities;
`
const expectedPath = path.resolve(__dirname, './custom-extractors.test.css')
const expected = fs.readFileSync(expectedPath, 'utf8')
test('defaultExtractor', () => {
let config = {
mode: 'jit',
purge: {
content: [path.resolve(__dirname, './custom-extractors.test.html')],
options: {
defaultExtractor: customExtractor,
},
},
corePlugins: { preflight: false },
theme: {},
plugins: [],
}
return run(css, config).then((result) => {
expect(result.css).toMatchFormattedCss(expected)
})
})
test('extractors array', () => {
let config = {
purge: {
content: [path.resolve(__dirname, './custom-extractors.test.html')],
options: {
extractors: [
{
extractor: customExtractor,
extensions: ['html'],
},
],
},
},
mode: 'jit',
corePlugins: { preflight: false },
theme: {},
plugins: [],
}
return run(css, config).then((result) => {
expect(result.css).toMatchFormattedCss(expected)
})
})

View file

@ -0,0 +1,19 @@
.group_hover .group-hover_focus-within_text-left:focus-within {
text-align: left;
}
[dir='rtl'] .rtl_active_text-center:active {
text-align: center;
}
@media (prefers-reduced-motion: no-preference) {
.motion-safe_hover_text-center:hover {
text-align: center;
}
}
.dark .dark_focus_text-left:focus {
text-align: left;
}
@media (min-width: 768px) {
.md_hover_text-right:hover {
text-align: right;
}
}

View file

@ -0,0 +1,5 @@
<div class="md_hover_text-right"></div>
<div class="motion-safe_hover_text-center"></div>
<div class="dark_focus_text-left"></div>
<div class="group-hover_focus-within_text-left"></div>
<div class="rtl_active_text-center"></div>

View file

@ -0,0 +1,31 @@
import postcss from 'postcss'
import fs from 'fs'
import path from 'path'
import tailwind from '../../src/jit/index.js'
function run(input, config = {}) {
return postcss(tailwind(config)).process(input, {
from: path.resolve(__filename),
})
}
test('custom separator', () => {
let config = {
darkMode: 'class',
mode: 'jit',
purge: [path.resolve(__dirname, './custom-separator.test.html')],
separator: '_',
corePlugins: {},
theme: {},
plugins: [],
}
let css = `@tailwind utilities`
return run(css, config).then((result) => {
let expectedPath = path.resolve(__dirname, './custom-separator.test.css')
let expected = fs.readFileSync(expectedPath, 'utf8')
expect(result.css).toMatchFormattedCss(expected)
})
})

View file

@ -0,0 +1,52 @@
* {
--tw-shadow: 0 0 #0000;
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgba(59, 130, 246, 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
}
h1 {
font-size: 32px;
}
.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;
}
}
.mt-6 {
margin-top: 1.5rem;
}
.bg-black {
--tw-bg-opacity: 1;
background-color: rgba(0, 0, 0, var(--tw-bg-opacity));
}
@media (min-width: 768px) {
.md\:hover\:text-center:hover {
text-align: center;
}
}

View file

@ -0,0 +1,5 @@
<h1>Hello world!</h1>
<div class="container"></div>
<div class="mt-6"></div>
<div class="bg-black"></div>
<div class="md:hover:text-center"></div>

View file

@ -0,0 +1,41 @@
import postcss from 'postcss'
import fs from 'fs'
import path from 'path'
import tailwind from '../../src/jit/index.js'
function run(input, config = {}) {
return postcss(tailwind(config)).process(input, {
from: path.resolve(__filename),
})
}
test('using @import instead of @tailwind', () => {
let config = {
purge: [path.resolve(__dirname, './import-syntax.test.html')],
mode: 'jit',
corePlugins: { preflight: false },
theme: {},
plugins: [
function ({ addBase }) {
addBase({
h1: {
fontSize: '32px',
},
})
},
],
}
let css = `
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
`
return run(css, config).then((result) => {
let expectedPath = path.resolve(__dirname, './import-syntax.test.css')
let expected = fs.readFileSync(expectedPath, 'utf8')
expect(result.css).toMatchFormattedCss(expected)
})
})

View file

@ -0,0 +1,85 @@
* {
--tw-shadow: 0 0 #0000;
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgba(59, 130, 246, 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
}
.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;
}
}
.btn {
button: yes !important;
}
@font-face {
font-family: Inter;
}
@page {
margin: 1cm;
}
.custom-component {
font-weight: 700;
}
.custom-important-component {
text-align: center !important;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite !important;
}
.font-bold {
font-weight: 700 !important;
}
.custom-util {
button: no;
}
.group:hover .group-hover\:focus-within\:text-left:focus-within {
text-align: left !important;
}
[dir='rtl'] .rtl\:active\:text-center:active {
text-align: center !important;
}
@media (prefers-reduced-motion: no-preference) {
.motion-safe\:hover\:text-center:hover {
text-align: center !important;
}
}
.dark .dark\:focus\:text-left:focus {
text-align: left !important;
}
@media (min-width: 768px) {
.md\:hover\:text-right:hover {
text-align: right !important;
}
}

View file

@ -0,0 +1,12 @@
<div class="container"></div>
<div class="btn"></div>
<div class="animate-spin"></div>
<div class="custom-util"></div>
<div class="custom-component"></div>
<div class="custom-important-component"></div>
<div class="font-bold"></div>
<div class="md:hover:text-right"></div>
<div class="motion-safe:hover:text-center"></div>
<div class="dark:focus:text-left"></div>
<div class="group-hover:focus-within:text-left"></div>
<div class="rtl:active:text-center"></div>

View file

@ -0,0 +1,73 @@
import postcss from 'postcss'
import fs from 'fs'
import path from 'path'
import tailwind from '../../src/jit/index.js'
function run(input, config = {}) {
return postcss(tailwind(config)).process(input, {
from: path.resolve(__filename),
})
}
test('important boolean', () => {
let config = {
important: true,
darkMode: 'class',
mode: 'jit',
purge: [path.resolve(__dirname, './important-boolean.test.html')],
corePlugins: { preflight: false },
theme: {},
plugins: [
function ({ addComponents, addUtilities }) {
addComponents(
{
'.btn': {
button: 'yes',
},
},
{ respectImportant: true }
)
addComponents(
{
'@font-face': {
'font-family': 'Inter',
},
'@page': {
margin: '1cm',
},
},
{ respectImportant: true }
)
addUtilities(
{
'.custom-util': {
button: 'no',
},
},
{ respectImportant: false }
)
},
],
}
let css = `
@tailwind base;
@tailwind components;
@layer components {
.custom-component {
@apply font-bold;
}
.custom-important-component {
@apply text-center !important;
}
}
@tailwind utilities;
`
return run(css, config).then((result) => {
let expectedPath = path.resolve(__dirname, './important-boolean.test.css')
let expected = fs.readFileSync(expectedPath, 'utf8')
expect(result.css).toMatchFormattedCss(expected)
})
})

View file

@ -0,0 +1,53 @@
* {
--tw-shadow: 0 0 #0000;
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgba(59, 130, 246, 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
}
.\!tw-container {
width: 100% !important;
}
@media (min-width: 640px) {
.\!tw-container {
max-width: 640px !important;
}
}
@media (min-width: 768px) {
.\!tw-container {
max-width: 768px !important;
}
}
@media (min-width: 1024px) {
.\!tw-container {
max-width: 1024px !important;
}
}
@media (min-width: 1280px) {
.\!tw-container {
max-width: 1280px !important;
}
}
@media (min-width: 1536px) {
.\!tw-container {
max-width: 1536px !important;
}
}
.\!tw-font-bold {
font-weight: 700 !important;
}
.hover\:\!tw-text-center:hover {
text-align: center !important;
}
@media (min-width: 1024px) {
.lg\:\!tw-opacity-50 {
opacity: 0.5 !important;
}
}
@media (min-width: 1280px) {
.xl\:focus\:disabled\:\!tw-float-right:focus:disabled {
float: right !important;
}
}

View file

@ -0,0 +1,5 @@
<div class="!tw-container"></div>
<div class="!tw-font-bold"></div>
<div class="hover:!tw-text-center"></div>
<div class="lg:!tw-opacity-50"></div>
<div class="xl:focus:disabled:!tw-float-right"></div>

View file

@ -0,0 +1,36 @@
import postcss from 'postcss'
import fs from 'fs'
import path from 'path'
import tailwind from '../../src/jit/index.js'
function run(input, config = {}) {
return postcss(tailwind(config)).process(input, {
from: path.resolve(__filename),
})
}
test('important modifier with prefix', () => {
let config = {
important: false,
prefix: 'tw-',
darkMode: 'class',
mode: 'jit',
purge: [path.resolve(__dirname, './important-modifier-prefix.test.html')],
corePlugins: { preflight: false },
theme: {},
plugins: [],
}
let css = `
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(css, config).then((result) => {
let expectedPath = path.resolve(__dirname, './important-modifier-prefix.test.css')
let expected = fs.readFileSync(expectedPath, 'utf8')
expect(result.css).toMatchFormattedCss(expected)
})
})

View file

@ -0,0 +1,53 @@
* {
--tw-shadow: 0 0 #0000;
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgba(59, 130, 246, 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
}
.\!container {
width: 100% !important;
}
@media (min-width: 640px) {
.\!container {
max-width: 640px !important;
}
}
@media (min-width: 768px) {
.\!container {
max-width: 768px !important;
}
}
@media (min-width: 1024px) {
.\!container {
max-width: 1024px !important;
}
}
@media (min-width: 1280px) {
.\!container {
max-width: 1280px !important;
}
}
@media (min-width: 1536px) {
.\!container {
max-width: 1536px !important;
}
}
.\!font-bold {
font-weight: 700 !important;
}
.hover\:\!text-center:hover {
text-align: center !important;
}
@media (min-width: 1024px) {
.lg\:\!opacity-50 {
opacity: 0.5 !important;
}
}
@media (min-width: 1280px) {
.xl\:focus\:disabled\:\!float-right:focus:disabled {
float: right !important;
}
}

View file

@ -0,0 +1,5 @@
<div class="!container"></div>
<div class="!font-bold"></div>
<div class="hover:!text-center"></div>
<div class="lg:!opacity-50"></div>
<div class="xl:focus:disabled:!float-right"></div>

View file

@ -0,0 +1,35 @@
import postcss from 'postcss'
import fs from 'fs'
import path from 'path'
import tailwind from '../../src/jit/index.js'
function run(input, config = {}) {
return postcss(tailwind(config)).process(input, {
from: path.resolve(__filename),
})
}
test('important modifier', () => {
let config = {
important: false,
darkMode: 'class',
mode: 'jit',
purge: [path.resolve(__dirname, './important-modifier.test.html')],
corePlugins: { preflight: false },
theme: {},
plugins: [],
}
let css = `
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(css, config).then((result) => {
let expectedPath = path.resolve(__dirname, './important-modifier.test.css')
let expected = fs.readFileSync(expectedPath, 'utf8')
expect(result.css).toMatchFormattedCss(expected)
})
})

View file

@ -0,0 +1,85 @@
* {
--tw-shadow: 0 0 #0000;
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgba(59, 130, 246, 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
}
.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;
}
}
#app .btn {
button: yes;
}
@font-face {
font-family: Inter;
}
@page {
margin: 1cm;
}
.custom-component {
font-weight: 700;
}
.custom-important-component {
text-align: center !important;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
#app .animate-spin {
animation: spin 1s linear infinite;
}
#app .font-bold {
font-weight: 700;
}
.custom-util {
button: no;
}
#app .group:hover .group-hover\:focus-within\:text-left:focus-within {
text-align: left;
}
#app [dir='rtl'] .rtl\:active\:text-center:active {
text-align: center;
}
@media (prefers-reduced-motion: no-preference) {
#app .motion-safe\:hover\:text-center:hover {
text-align: center;
}
}
#app .dark .dark\:focus\:text-left:focus {
text-align: left;
}
@media (min-width: 768px) {
#app .md\:hover\:text-right:hover {
text-align: right;
}
}

View file

@ -0,0 +1,12 @@
<div class="container"></div>
<div class="btn"></div>
<div class="animate-spin"></div>
<div class="custom-util"></div>
<div class="custom-component"></div>
<div class="custom-important-component"></div>
<div class="font-bold"></div>
<div class="md:hover:text-right"></div>
<div class="motion-safe:hover:text-center"></div>
<div class="dark:focus:text-left"></div>
<div class="group-hover:focus-within:text-left"></div>
<div class="rtl:active:text-center"></div>

View file

@ -0,0 +1,73 @@
import postcss from 'postcss'
import fs from 'fs'
import path from 'path'
import tailwind from '../../src/jit/index.js'
function run(input, config = {}) {
return postcss(tailwind(config)).process(input, {
from: path.resolve(__filename),
})
}
test('important selector', () => {
let config = {
important: '#app',
darkMode: 'class',
mode: 'jit',
purge: [path.resolve(__dirname, './important-selector.test.html')],
corePlugins: { preflight: false },
theme: {},
plugins: [
function ({ addComponents, addUtilities }) {
addComponents(
{
'.btn': {
button: 'yes',
},
},
{ respectImportant: true }
)
addComponents(
{
'@font-face': {
'font-family': 'Inter',
},
'@page': {
margin: '1cm',
},
},
{ respectImportant: true }
)
addUtilities(
{
'.custom-util': {
button: 'no',
},
},
{ respectImportant: false }
)
},
],
}
let css = `
@tailwind base;
@tailwind components;
@layer components {
.custom-component {
@apply font-bold;
}
.custom-important-component {
@apply text-center !important;
}
}
@tailwind utilities;
`
return run(css, config).then((result) => {
let expectedPath = path.resolve(__dirname, './important-selector.test.css')
let expected = fs.readFileSync(expectedPath, 'utf8')
expect(result.css).toMatchFormattedCss(expected)
})
})

View file

@ -0,0 +1,568 @@
.theme-test {
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
color: #3b82f6;
}
@media (min-width: 1024px) {
.screen-test {
color: purple;
}
}
.apply-1 {
margin-top: 1.5rem;
}
.apply-2 {
margin-top: 1.5rem;
}
.apply-test {
margin-top: 1.5rem;
--tw-bg-opacity: 1;
background-color: rgba(236, 72, 153, var(--tw-bg-opacity));
}
.apply-test:hover {
font-weight: 700;
}
.apply-test:focus:hover {
font-weight: 700;
}
@media (min-width: 640px) {
.apply-test {
--tw-bg-opacity: 1;
background-color: rgba(16, 185, 129, var(--tw-bg-opacity));
}
.apply-test:focus:nth-child(even) {
--tw-bg-opacity: 1;
background-color: rgba(251, 207, 232, var(--tw-bg-opacity));
}
}
.apply-components {
width: 100%;
}
@media (min-width: 640px) {
.apply-components {
max-width: 640px;
}
}
@media (min-width: 768px) {
.apply-components {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.apply-components {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.apply-components {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.apply-components {
max-width: 1536px;
}
}
.apply-components {
margin-left: auto;
margin-right: auto;
}
.drop-empty-rules:hover {
font-weight: 700;
}
.group:hover .apply-group {
font-weight: 700;
}
.dark .apply-dark-mode {
font-weight: 700;
}
.apply-with-existing:hover {
font-weight: 400;
}
@media (min-width: 640px) {
.apply-with-existing:hover {
--tw-bg-opacity: 1;
background-color: rgba(16, 185, 129, var(--tw-bg-opacity));
}
}
.multiple,
.selectors {
font-weight: 700;
}
.group:hover .multiple,
.group:hover .selectors {
font-weight: 400;
}
.list > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(1rem * var(--tw-space-x-reverse));
margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
}
.nested {
.example {
font-weight: 700;
}
.example:hover {
font-weight: 400;
}
}
.apply-order-a {
margin: 1.25rem;
margin-top: 1.5rem;
}
.apply-order-b {
margin: 1.25rem;
margin-top: 1.5rem;
}
.dark .group:hover .apply-dark-group-example-a {
--tw-bg-opacity: 1;
background-color: rgba(16, 185, 129, var(--tw-bg-opacity));
}
@media (min-width: 640px) {
@media (prefers-reduced-motion: no-preference) {
.group:active .crazy-example:focus {
opacity: 0.1;
}
}
}
* {
--tw-shadow: 0 0 #0000;
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgba(59, 130, 246, 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
}
h1 {
font-size: 1.5rem;
font-weight: 700;
}
h1:first-child {
margin-top: 0px;
}
div {
background: #654321;
}
.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;
}
}
.aspect-w-1,
.aspect-w-2,
.aspect-w-3,
.aspect-w-4,
.aspect-w-5,
.aspect-w-6,
.aspect-w-7,
.aspect-w-8,
.aspect-w-9,
.aspect-w-10,
.aspect-w-11,
.aspect-w-12,
.aspect-w-13,
.aspect-w-14,
.aspect-w-15,
.aspect-w-16 {
position: relative;
padding-bottom: calc(var(--tw-aspect-h) / var(--tw-aspect-w) * 100%);
}
.aspect-w-1 > *,
.aspect-w-2 > *,
.aspect-w-3 > *,
.aspect-w-4 > *,
.aspect-w-5 > *,
.aspect-w-6 > *,
.aspect-w-7 > *,
.aspect-w-8 > *,
.aspect-w-9 > *,
.aspect-w-10 > *,
.aspect-w-11 > *,
.aspect-w-12 > *,
.aspect-w-13 > *,
.aspect-w-14 > *,
.aspect-w-15 > *,
.aspect-w-16 > * {
position: absolute;
height: 100%;
width: 100%;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.aspect-w-1 {
--tw-aspect-w: 1;
}
.aspect-w-3 {
--tw-aspect-w: 3;
}
.aspect-h-2 {
--tw-aspect-h: 2;
}
.aspect-h-4 {
--tw-aspect-h: 4;
}
.test-apply-font-variant {
--tw-ordinal: var(--tw-empty, /*!*/ /*!*/);
--tw-slashed-zero: var(--tw-empty, /*!*/ /*!*/);
--tw-numeric-figure: var(--tw-empty, /*!*/ /*!*/);
--tw-numeric-spacing: var(--tw-empty, /*!*/ /*!*/);
--tw-numeric-fraction: var(--tw-empty, /*!*/ /*!*/);
font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure)
var(--tw-numeric-spacing) var(--tw-numeric-fraction);
--tw-ordinal: var(--tw-empty, /*!*/ /*!*/);
--tw-slashed-zero: var(--tw-empty, /*!*/ /*!*/);
--tw-numeric-figure: var(--tw-empty, /*!*/ /*!*/);
--tw-numeric-spacing: var(--tw-empty, /*!*/ /*!*/);
--tw-numeric-fraction: var(--tw-empty, /*!*/ /*!*/);
font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure)
var(--tw-numeric-spacing) var(--tw-numeric-fraction);
--tw-ordinal: ordinal;
--tw-numeric-spacing: tabular-nums;
}
.custom-component {
background: #123456;
}
* {
padding: 5px;
}
.foo .bg-black {
appearance: none;
}
.mt-6 {
margin-top: 1.5rem;
}
.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));
}
.scale-50 {
--tw-scale-x: 0.5;
--tw-scale-y: 0.5;
}
.grid-cols-\[200px\2c repeat\(auto-fill\2c minmax\(15\%\2c 100px\)\)\2c 300px\] {
grid-template-columns: 200px repeat(auto-fill, minmax(15%, 100px)) 300px;
}
.bg-black {
--tw-bg-opacity: 1;
background-color: rgba(0, 0, 0, var(--tw-bg-opacity));
}
.bg-green-500 {
--tw-bg-opacity: 1;
background-color: rgba(16, 185, 129, var(--tw-bg-opacity));
}
.bg-opacity-50 {
--tw-bg-opacity: 0.5;
}
.bg-gradient-to-r {
background-image: linear-gradient(to right, var(--tw-gradient-stops));
}
.bg-hero--home-1 {
background-image: url('/images/homepage-1.jpg');
}
.from-foo {
--tw-gradient-from: #bada55;
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(186, 218, 85, 0));
}
.text-center {
text-align: center;
}
.font-medium {
font-weight: 500;
}
.shadow-sm {
--tw-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.magic-none {
magic: none;
}
.magic-tons {
magic: tons;
}
.custom-util {
background: #abcdef;
}
* {
margin: 10px;
}
.first\:pt-0:first-child {
padding-top: 0px;
}
.hover\:container:hover {
width: 100%;
}
@media (min-width: 640px) {
.hover\:container:hover {
max-width: 640px;
}
}
@media (min-width: 768px) {
.hover\:container:hover {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.hover\:container:hover {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.hover\:container:hover {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.hover\:container:hover {
max-width: 1536px;
}
}
.hover\:scale-75:hover {
--tw-scale-x: 0.75;
--tw-scale-y: 0.75;
}
.hover\:font-bold:hover {
font-weight: 700;
}
.hover\:shadow-lg:hover {
--tw-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.hover\:custom-util:hover {
background: #abcdef;
}
.focus\:font-normal:focus {
font-weight: 400;
}
.focus\:ring-2:focus {
--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(2px + var(--tw-ring-offset-width))
var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.focus\:ring-blue-500:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgba(59, 130, 246, var(--tw-ring-opacity));
}
.focus\:hover\:font-light:focus:hover {
font-weight: 300;
}
.disabled\:font-bold:disabled {
font-weight: 700;
}
.group:hover .group-hover\:opacity-100 {
opacity: 1;
}
.group:hover .group-hover\:custom-util {
background: #abcdef;
}
.group:active .group-active\:opacity-10 {
opacity: 0.1;
}
.foo\:custom-util {
background: #abcdef !important;
}
.foo\:hover\:custom-util:hover {
background: #abcdef !important;
}
@media (prefers-reduced-motion: no-preference) {
.motion-safe\:transition {
transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow,
transform, filter, backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.motion-safe\:custom-util {
background: #abcdef;
}
}
@media (prefers-reduced-motion: reduce) {
.motion-reduce\:transition {
transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow,
transform, filter, backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
}
.dark .dark\:custom-util {
background: #abcdef;
}
@media (min-width: 640px) {
.sm\:container {
width: 100%;
}
@media (min-width: 640px) {
.sm\:container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.sm\:container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.sm\:container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.sm\:container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.sm\:container {
max-width: 1536px;
}
}
.sm\:text-center {
text-align: center;
}
.sm\:ordinal,
.sm\:slashed-zero,
.sm\:lining-nums,
.sm\:oldstyle-nums,
.sm\:proportional-nums,
.sm\:tabular-nums,
.sm\:diagonal-fractions,
.sm\:stacked-fractions {
--tw-ordinal: var(--tw-empty, /*!*/ /*!*/);
--tw-slashed-zero: var(--tw-empty, /*!*/ /*!*/);
--tw-numeric-figure: var(--tw-empty, /*!*/ /*!*/);
--tw-numeric-spacing: var(--tw-empty, /*!*/ /*!*/);
--tw-numeric-fraction: var(--tw-empty, /*!*/ /*!*/);
font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure)
var(--tw-numeric-spacing) var(--tw-numeric-fraction);
}
.sm\:tabular-nums {
--tw-numeric-spacing: tabular-nums;
}
.sm\:custom-util {
background: #abcdef;
}
@media (prefers-reduced-motion: no-preference) {
.group:active .sm\:motion-safe\:group-active\:focus\:opacity-10:focus {
opacity: 0.1;
}
}
}
@media (min-width: 768px) {
.md\:container {
width: 100%;
}
@media (min-width: 640px) {
.md\:container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.md\:container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.md\:container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.md\:container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.md\:container {
max-width: 1536px;
}
}
.md\:text-center {
text-align: center;
}
.md\:opacity-50 {
opacity: 0.5;
}
.md\:shadow-sm {
--tw-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.md\:hover\:opacity-20:hover {
opacity: 0.2;
}
@media (prefers-reduced-motion: no-preference) {
.md\:motion-safe\:hover\:transition:hover {
transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow,
transform, filter, backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
}
@media (prefers-reduced-motion: no-preference) {
.dark .md\:dark\:motion-safe\:foo\:active\:custom-util:active {
background: #abcdef !important;
}
}
@media (min-width: 640px) {
.md\:sm\:text-center {
text-align: center;
}
}
}
@media (orientation: portrait) {
.portrait\:text-center {
text-align: center;
}
}
@media (min-width: 1280px) and (max-width: 1535px) {
.range\:text-right {
text-align: right;
}
}
@media (min-width: 640px) and (max-width: 767px), (max-width: 868px) {
.multi\:text-left {
text-align: left;
}
}

View file

@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
<link rel="stylesheet" href="./tailwind.css" />
</head>
<body>
<div class="portrait:text-center range:text-right multi:text-left"></div>
<div
class="container hover:container sm:container md:container text-center sm:text-center md:text-center"
></div>
<div class="grid-cols-[200px,repeat(auto-fill,minmax(15%,100px)),300px]"></div>
<div class="test-apply-font-variant"></div>
<div class="mt-6"></div>
<div class="bg-black"></div>
<div class="custom-util"></div>
<div class="hover:custom-util"></div>
<div class="group-hover:custom-util"></div>
<div class="foo:custom-util"></div>
<div class="foo:hover:custom-util"></div>
<div class="sm:custom-util"></div>
<div class="dark:custom-util"></div>
<div class="motion-safe:custom-util"></div>
<div class="md:dark:motion-safe:foo:active:custom-util"></div>
<div class="aspect-w-1 aspect-h-2"></div>
<div class="aspect-w-3 aspect-h-4"></div>
<div class="magic-none magic-tons"></div>
<div class="focus:font-normal"></div>
<div class="font-medium"></div>
<div class="bg-gradient-to-r from-foo"></div>
<div class="custom-component custom-util"></div>
<div class="bg-opacity-50"></div>
<div class="focus:ring-2 focus:ring-blue-500"></div>
<div class="hover:font-bold"></div>
<div class="disabled:font-bold"></div>
<div class="focus:hover:font-light"></div>
<div class="first:pt-0"></div>
<div class="container"></div>
<div class="bg-hero--home-1"></div>
<div class="group-hover:opacity-100"></div>
<div class="group-active:opacity-10"></div>
<div class="sm:motion-safe:group-active:focus:opacity-10"></div>
<div class="motion-safe:transition"></div>
<div class="motion-reduce:transition"></div>
<div class="md:motion-safe:hover:transition"></div>
<div class="md:sm:text-center shadow-sm md:shadow-sm"></div>
<div class="md:sm:text-center shadow-sm md:shadow-sm"></div>
<div class="bg-green-500 md:opacity-50 md:hover:opacity-20 sm:tabular-nums"></div>
<div class="text-center shadow-md hover:shadow-lg transform scale-50 hover:scale-75"></div>
</body>
</html>
<script>
defineComponent({
name: 'HelloWorld',
props: {
msg: {
type: String,
required: true,
},
things: Array /* PropType<string[]> */,
},
setup: () => {
const count = ref(0)
// Weird regex-looking stuff that once caused a stack overflow in candidatePermutations
const pattern = ' ]-[] '
return {
count,
stuff: [] /* string[] | undefined */,
}
},
})
</script>

View file

@ -0,0 +1,169 @@
import postcss from 'postcss'
import fs from 'fs'
import path from 'path'
import tailwind from '../../src/jit/index.js'
function run(input, config = {}) {
return postcss(tailwind(config)).process(input, {
from: path.resolve(__filename),
})
}
test('it works', () => {
let config = {
darkMode: 'class',
mode: 'jit',
purge: [path.resolve(__dirname, './kitchen-sink.test.html')],
corePlugins: { preflight: false },
theme: {
extend: {
screens: {
portrait: { raw: '(orientation: portrait)' },
range: { min: '1280px', max: '1535px' },
multi: [{ min: '640px', max: '767px' }, { max: '868px' }],
},
gradientColorStops: {
foo: '#bada55',
},
backgroundImage: {
'hero--home-1': "url('/images/homepage-1.jpg')",
},
},
},
plugins: [
require('@tailwindcss/aspect-ratio'),
function ({ addVariant }) {
addVariant(
'foo',
({ container }) => {
container.walkRules((rule) => {
rule.selector = `.foo\\:${rule.selector.slice(1)}`
rule.walkDecls((decl) => {
decl.important = true
})
})
},
{ before: 'sm' }
)
},
function ({ addUtilities, addBase, theme }) {
addBase({
h1: {
fontSize: theme('fontSize.2xl'),
fontWeight: theme('fontWeight.bold'),
'&:first-child': {
marginTop: '0px',
},
},
})
addUtilities(
{
'.magic-none': {
magic: 'none',
},
'.magic-tons': {
magic: 'tons',
},
},
['responsive', 'hover']
)
},
],
}
let css = `
@layer utilities {
.custom-util {
background: #abcdef;
}
* {
margin: 10px;
}
}
@layer components {
.test-apply-font-variant {
@apply ordinal tabular-nums;
}
.custom-component {
background: #123456;
}
* {
padding: 5px;
}
.foo .bg-black {
appearance: none;
}
}
@layer base {
div {
background: #654321;
}
}
.theme-test {
font-family: theme('fontFamily.sans');
color: theme('colors.blue.500');
}
@screen lg {
.screen-test {
color: purple;
}
}
.apply-1 {
@apply mt-6;
}
.apply-2 {
@apply mt-6;
}
.apply-test {
@apply mt-6 bg-pink-500 hover:font-bold focus:hover:font-bold sm:bg-green-500 sm:focus:even:bg-pink-200;
}
.apply-components {
@apply container mx-auto;
}
.drop-empty-rules {
@apply hover:font-bold;
}
.apply-group {
@apply group-hover:font-bold;
}
.apply-dark-mode {
@apply dark:font-bold;
}
.apply-with-existing:hover {
@apply font-normal sm:bg-green-500;
}
.multiple, .selectors {
@apply font-bold group-hover:font-normal;
}
.list {
@apply space-x-4;
}
.nested {
.example {
@apply font-bold hover:font-normal;
}
}
.apply-order-a {
@apply m-5 mt-6;
}
.apply-order-b {
@apply mt-6 m-5;
}
.apply-dark-group-example-a {
@apply dark:group-hover:bg-green-500;
}
.crazy-example {
@apply sm:motion-safe:group-active:focus:opacity-10;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(css, config).then((result) => {
let expectedPath = path.resolve(__dirname, './kitchen-sink.test.css')
let expected = fs.readFileSync(expectedPath, 'utf8')
expect(result.css).toMatchFormattedCss(expected)
})
})

View file

@ -0,0 +1,33 @@
.markdown > p {
margin-top: 12px;
}
.font-bold {
font-weight: 700;
}
.foo .foo\:markdown > p {
margin-top: 12px;
}
.foo .foo\:font-bold {
font-weight: 700;
}
.foo .foo\:visited\:markdown:visited > p {
margin-top: 12px;
}
.foo .foo\:hover\:font-bold:hover {
font-weight: 700;
}
@media (min-width: 640px) {
.foo .sm\:foo\:font-bold {
font-weight: 700;
}
}
@media (min-width: 768px) {
.foo .md\:foo\:focus\:font-bold:focus {
font-weight: 700;
}
}
@media (min-width: 1024px) {
.foo .lg\:foo\:disabled\:markdown:disabled > p {
margin-top: 12px;
}
}

View file

@ -0,0 +1,17 @@
<div class="font-bold"></div>
<div class="foo:font-bold"></div>
<div class="foo:hover:font-bold"></div>
<div class="sm:foo:font-bold"></div>
<div class="md:foo:focus:font-bold"></div>
<div class="markdown">
<p>Lorem ipsum dolor sit amet...</p>
</div>
<div class="foo:markdown">
<p>Lorem ipsum dolor sit amet...</p>
</div>
<div class="foo:visited:markdown">
<p>Lorem ipsum dolor sit amet...</p>
</div>
<div class="lg:foo:disabled:markdown">
<p>Lorem ipsum dolor sit amet...</p>
</div>

View file

@ -0,0 +1,53 @@
import postcss from 'postcss'
import fs from 'fs'
import path from 'path'
import tailwind from '../../src/jit/index.js'
import selectorParser from 'postcss-selector-parser'
function run(input, config = {}) {
return postcss(tailwind(config)).process(input, {
from: path.resolve(__filename),
})
}
test('modify selectors', () => {
let config = {
darkMode: 'class',
mode: 'jit',
purge: [path.resolve(__dirname, './modify-selectors.test.html')],
corePlugins: { preflight: false },
theme: {},
plugins: [
function ({ addVariant }) {
addVariant('foo', ({ modifySelectors, separator }) => {
modifySelectors(({ selector }) => {
return selectorParser((selectors) => {
selectors.walkClasses((classNode) => {
classNode.value = `foo${separator}${classNode.value}`
classNode.parent.insertBefore(classNode, selectorParser().astSync(`.foo `))
})
}).processSync(selector)
})
})
},
],
}
let css = `
@tailwind components;
@tailwind utilities;
@layer components {
.markdown > p {
margin-top: 12px;
}
}
`
return run(css, config).then((result) => {
let expectedPath = path.resolve(__dirname, './modify-selectors.test.css')
let expected = fs.readFileSync(expectedPath, 'utf8')
expect(result.css).toMatchFormattedCss(expected)
})
})

View file

@ -0,0 +1,3 @@
.bg-foo {
background-image: url("./foo.png");
}

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
<link rel="stylesheet" href="./tailwind.css" />
</head>
<body>
<div class="bg-foo"></div>
</body>
</html>

60
tests/jit/mutable.test.js Normal file
View file

@ -0,0 +1,60 @@
import postcss from 'postcss'
import fs from 'fs'
import path from 'path'
import tailwind from '../../src/jit/index.js'
function pluginThatMutatesRules() {
return (root) => {
root.walkRules((rule) => {
rule.nodes
.filter((node) => node.prop === 'background-image')
.forEach((node) => {
node.value = 'url("./bar.png")'
})
return rule
})
}
}
function run(input, config = {}) {
return postcss(tailwind(config)).process(input, {
from: path.resolve(__filename),
})
}
test.only('plugins mutating rules after tailwind doesnt break it', async () => {
let config = {
purge: [path.resolve(__dirname, './mutable.test.html')],
mode: 'jit',
theme: {
backgroundImage: {
foo: 'url("./foo.png")',
},
},
plugins: [],
}
let css = `@tailwind utilities;`
function checkResult(result) {
let expectedPath = path.resolve(__dirname, './mutable.test.css')
let expected = fs.readFileSync(expectedPath, 'utf8')
expect(result.css).toMatchFormattedCss(expected)
}
// Verify the first run produces the expected result
let firstRun = await run(css, config)
checkResult(firstRun)
// Outside of the context of tailwind jit more postcss plugins may operate on the AST:
// In this case we have a plugin that mutates rules directly
await postcss([pluginThatMutatesRules()]).process(firstRun, {
from: path.resolve(__filename),
})
// Verify subsequent runs don't produce mutated rules
let secondRun = await run(css, config)
checkResult(secondRun)
})

View file

@ -0,0 +1,24 @@
* {
--tw-shadow: 0 0 #0000;
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgba(59, 130, 246, 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
}
.divide-black > :not([hidden]) ~ :not([hidden]) {
border-color: #000;
}
.border-black {
border-color: #000;
}
.bg-black {
background-color: #000;
}
.text-black {
color: #000;
}
.placeholder-black::placeholder {
color: #000;
}

View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
<link rel="stylesheet" href="./tailwind.css" />
</head>
<body>
<div class="divide-black"></div>
<div class="border-black"></div>
<div class="bg-black"></div>
<div class="text-black"></div>
<div class="placeholder-black"></div>
</body>
</html>

39
tests/jit/opacity.test.js Normal file
View file

@ -0,0 +1,39 @@
import postcss from 'postcss'
import fs from 'fs'
import path from 'path'
import tailwind from '../../src/jit/index.js'
function run(input, config = {}) {
return postcss(tailwind(config)).process(input, { from: path.resolve(__filename) })
}
test('opacity', () => {
let config = {
mode: 'jit',
darkMode: 'class',
purge: [path.resolve(__dirname, './opacity.test.html')],
corePlugins: {
preflight: false,
backgroundOpacity: false,
borderOpacity: false,
divideOpacity: false,
placeholderOpacity: false,
textOpacity: false,
},
theme: {},
plugins: [],
}
let css = `
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(css, config).then((result) => {
let expectedPath = path.resolve(__dirname, './opacity.test.css')
let expected = fs.readFileSync(expectedPath, 'utf8')
expect(result.css).toMatchCss(expected)
})
})

View file

@ -0,0 +1,9 @@
.tw-ml-1 {
margin-left: 0.25rem;
}
.flex {
display: flex;
}
.tw-align-bottom {
vertical-align: bottom
}

View file

@ -0,0 +1,3 @@
<div class="flex"></div>
<div class="tw-align-bottom"></div>
<div class="tw-ml-1"></div>

View file

@ -0,0 +1,37 @@
import postcss from 'postcss'
import fs from 'fs'
import path from 'path'
import tailwind from '../../src/jit/index.js'
function run(input, config = {}) {
return postcss(tailwind(config)).process(input, {
from: path.resolve(__filename),
})
}
test('prefix fn', () => {
let config = {
prefix(selector) {
if (['.align-bottom', '.ml'].some((prefix) => selector.startsWith(prefix))) {
return 'tw-'
}
return ''
},
purge: [path.resolve(__dirname, './prefix.fn.test.html')],
mode: 'jit',
corePlugins: { preflight: false },
theme: {},
}
let css = `
@tailwind utilities;
`
return run(css, config).then((result) => {
let expectedPath = path.resolve(__dirname, './prefix.fn.test.css')
let expected = fs.readFileSync(expectedPath, 'utf8')
expect(result.css).toMatchFormattedCss(expected)
})
})

86
tests/jit/prefix.test.css Normal file
View file

@ -0,0 +1,86 @@
* {
--tw-shadow: 0 0 #0000;
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgba(59, 130, 246, 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
}
.tw-container {
width: 100%;
}
@media (min-width: 640px) {
.tw-container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.tw-container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.tw-container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.tw-container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.tw-container {
max-width: 1536px;
}
}
.tw-btn-prefix {
button: yes;
}
.btn-no-prefix {
button: yes;
}
.custom-component {
font-weight: 700;
}
.tw-dark .tw-group:hover .custom-component {
font-weight: 400;
}
.tw--ml-4 {
margin-left: -1rem;
}
.tw-font-bold {
font-weight: 700;
}
.tw-custom-util-prefix {
button: no;
}
.custom-util-no-prefix {
button: no;
}
.tw-group:hover .group-hover\:focus-within\:tw-text-left:focus-within {
text-align: left;
}
[dir='rtl'] .rtl\:active\:tw-text-center:active {
text-align: center;
}
@media (prefers-reduced-motion: no-preference) {
.motion-safe\:hover\:tw-text-center:hover {
text-align: center;
}
}
.tw-dark .dark\:focus\:tw-text-left:focus {
text-align: left;
}
@media (min-width: 768px) {
.md\:tw--ml-5 {
margin-left: -1.25rem;
}
.md\:hover\:tw--ml-6:hover {
margin-left: -1.5rem;
}
.md\:hover\:tw-text-right:hover {
text-align: right;
}
}

View file

@ -0,0 +1,17 @@
<div class="tw--ml-4"></div>
<div class="md:tw--ml-5"></div>
<div class="md:hover:tw--ml-6"></div>
<div class="tw-container"></div>
<div class="btn-no-prefix"></div>
<div class="tw-btn-prefix"></div>
<div class="tw-custom-util-prefix"></div>
<div class="custom-util-no-prefix"></div>
<div class="custom-component"></div>
<div class="tw-custom-component-prefix"></div>
<div class="custom-component-no-prefix"></div>
<div class="tw-font-bold"></div>
<div class="md:hover:tw-text-right"></div>
<div class="motion-safe:hover:tw-text-center"></div>
<div class="dark:focus:tw-text-left"></div>
<div class="group-hover:focus-within:tw-text-left"></div>
<div class="rtl:active:tw-text-center"></div>

74
tests/jit/prefix.test.js Normal file
View file

@ -0,0 +1,74 @@
import postcss from 'postcss'
import fs from 'fs'
import path from 'path'
import tailwind from '../../src/jit/index.js'
function run(input, config = {}) {
return postcss(tailwind(config)).process(input, {
from: path.resolve(__filename),
})
}
test('prefix', () => {
let config = {
prefix: 'tw-',
darkMode: 'class',
mode: 'jit',
purge: [path.resolve(__dirname, './prefix.test.html')],
corePlugins: { preflight: false },
theme: {},
plugins: [
function ({ addComponents, addUtilities }) {
addComponents({
'.btn-prefix': {
button: 'yes',
},
})
addComponents(
{
'.btn-no-prefix': {
button: 'yes',
},
},
{ respectPrefix: false }
)
addUtilities({
'.custom-util-prefix': {
button: 'no',
},
})
addUtilities(
{
'.custom-util-no-prefix': {
button: 'no',
},
},
{ respectPrefix: false }
)
},
],
}
let css = `
@tailwind base;
@tailwind components;
@layer components {
.custom-component {
@apply tw-font-bold dark:group-hover:tw-font-normal;
}
}
@tailwind utilities;
@layer utilites {
.custom-utility {
foo: bar;
}
}
`
return run(css, config).then((result) => {
let expectedPath = path.resolve(__dirname, './prefix.test.css')
let expected = fs.readFileSync(expectedPath, 'utf8')
expect(result.css).toMatchFormattedCss(expected)
})
})

View file

@ -0,0 +1,65 @@
* {
--tw-shadow: 0 0 #0000;
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgba(59, 130, 246, 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
}
.responsive-in-components {
color: blue;
}
.variants-in-components {
color: red;
}
.both-in-components {
color: green;
}
.responsive-in-utilities {
color: blue;
}
.variants-in-utilities {
color: red;
}
.both-in-utilities {
color: green;
}
.responsive-at-root {
color: white;
}
.variants-at-root {
color: orange;
}
.both-at-root {
color: pink;
}
@media (min-width: 768px) {
.md\:focus\:responsive-in-components:focus {
color: blue;
}
.md\:focus\:variants-in-components:focus {
color: red;
}
.md\:focus\:both-in-components:focus {
color: green;
}
.md\:focus\:responsive-in-utilities:focus {
color: blue;
}
.md\:focus\:variants-in-utilities:focus {
color: red;
}
.md\:focus\:both-in-utilities:focus {
color: green;
}
.md\:focus\:responsive-at-root:focus {
color: white;
}
.md\:focus\:variants-at-root:focus {
color: orange;
}
.md\:focus\:both-at-root:focus {
color: pink;
}
}

View file

@ -0,0 +1,18 @@
<div class="responsive-in-utilities"></div>
<div class="variants-in-utilities"></div>
<div class="both-in-utilities"></div>
<div class="responsive-at-root"></div>
<div class="variants-at-root"></div>
<div class="both-at-root"></div>
<div class="responsive-in-components"></div>
<div class="variants-in-components"></div>
<div class="both-in-components"></div>
<div class="md:focus:responsive-in-utilities"></div>
<div class="md:focus:variants-in-utilities"></div>
<div class="md:focus:both-in-utilities"></div>
<div class="md:focus:responsive-at-root"></div>
<div class="md:focus:variants-at-root"></div>
<div class="md:focus:both-at-root"></div>
<div class="md:focus:responsive-in-components"></div>
<div class="md:focus:variants-in-components"></div>
<div class="md:focus:both-in-components"></div>

View file

@ -0,0 +1,91 @@
import postcss from 'postcss'
import fs from 'fs'
import path from 'path'
import tailwind from '../../src/jit/index.js'
function run(input, config = {}) {
return postcss(tailwind(config)).process(input, {
from: path.resolve(__filename),
})
}
test('responsive and variants atrules', () => {
let config = {
purge: [path.resolve(__dirname, './responsive-and-variants-atrules.test.html')],
mode: 'jit',
corePlugins: { preflight: false },
theme: {},
plugins: [],
}
let css = `
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer utilities {
@responsive {
.responsive-in-utilities {
color: blue;
}
}
@variants {
.variants-in-utilities {
color: red;
}
}
@responsive {
@variants {
.both-in-utilities {
color: green;
}
}
}
}
@responsive {
.responsive-at-root {
color: white;
}
}
@variants {
.variants-at-root {
color: orange;
}
}
@responsive {
@variants {
.both-at-root {
color: pink;
}
}
}
@layer components {
@responsive {
.responsive-in-components {
color: blue;
}
}
@variants {
.variants-in-components {
color: red;
}
}
@responsive {
@variants {
.both-in-components {
color: green;
}
}
}
}
`
return run(css, config).then((result) => {
let expectedPath = path.resolve(__dirname, './responsive-and-variants-atrules.test.css')
let expected = fs.readFileSync(expectedPath, 'utf8')
expect(result.css).toMatchFormattedCss(expected)
})
})

View file

@ -0,0 +1,19 @@
* {
--tw-shadow: 0 0 #0000;
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgba(59, 130, 246, 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
}
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
}
@media (min-width: 1024px) {
.lg\:hover\:bg-blue-500:hover {
--tw-bg-opacity: 1;
background-color: rgba(59, 130, 246, var(--tw-bg-opacity));
}
}

View file

@ -0,0 +1,33 @@
import postcss from 'postcss'
import fs from 'fs'
import path from 'path'
import tailwind from '../../src/jit/index.js'
function run(input, config = {}) {
return postcss(tailwind(config)).process(input, {
from: path.resolve(__filename),
})
}
test('basic usage', () => {
let config = {
purge: [path.resolve(__dirname, './svelte-syntax.test.svelte')],
mode: 'jit',
corePlugins: { preflight: false },
theme: {},
plugins: [],
}
let css = `
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(css, config).then((result) => {
let expectedPath = path.resolve(__dirname, './svelte-syntax.test.css')
let expected = fs.readFileSync(expectedPath, 'utf8')
expect(result.css).toMatchFormattedCss(expected)
})
})

View file

@ -0,0 +1,11 @@
<script>
let current = 'foo'
</script>
<button class:lg:hover:bg-blue-500={current === 'foo'}>Click me</button>
<button
class:bg-red-500={current === 'foo'}
>
Click me
</button>

225
tests/jit/variants.test.css Normal file
View file

@ -0,0 +1,225 @@
* {
--tw-shadow: 0 0 #0000;
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgba(59, 130, 246, 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
}
.shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.first\:shadow-md:first-child {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.last\:shadow-md:last-child {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.odd\:shadow-md:nth-child(odd) {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.even\:shadow-md:nth-child(even) {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.visited\:shadow-md:visited {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.checked\:shadow-md:checked {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.focus-within\:shadow-md:focus-within {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.hover\:shadow-md:hover {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.focus\:shadow-md:focus {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.focus\:hover\:shadow-md:focus:hover {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.focus-visible\:shadow-md:focus-visible {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.active\:shadow-md:active {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.disabled\:shadow-md:disabled {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.group:first-child .group-first\:shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.group:last-child .group-last\:shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.group:nth-child(odd) .group-odd\:shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.group:nth-child(even) .group-even\:shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.group:visited .group-visited\:shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.group:checked .group-checked\:shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.group:focus-within .group-focus-within\:shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.group:hover .group-hover\:shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.group:focus .group-focus\:shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.group:focus-visible .group-focus-visible\:shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.group:active .group-active\:shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.group:disabled .group-disabled\:shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
[dir='ltr'] .ltr\:shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
[dir='rtl'] .rtl\:shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
@media (prefers-reduced-motion: no-preference) {
.motion-safe\:shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
}
@media (prefers-reduced-motion: reduce) {
.motion-reduce\:shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
}
.dark .dark\:shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
@media (min-width: 640px) {
.sm\:shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.sm\:active\:shadow-md:active {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
}
@media (min-width: 768px) {
.md\:shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.group:focus .md\:group-focus\:shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
}
@media (min-width: 1024px) {
.lg\:shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.dark .lg\:dark\:shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
}
@media (min-width: 1280px) {
.xl\:shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
}
@media (min-width: 1536px) {
.\32xl\:shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
@media (prefers-reduced-motion: no-preference) {
.dark .\32xl\:dark\:motion-safe\:focus-within\:shadow-md:focus-within {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
}
}

View file

@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
<link rel="stylesheet" href="./tailwind.css" />
</head>
<body>
<!-- Basic pseudo variants -->
<div class="shadow-md"></div>
<div class="hover:shadow-md"></div>
<div class="focus:shadow-md"></div>
<div class="disabled:shadow-md"></div>
<div class="active:shadow-md"></div>
<div class="first:shadow-md"></div>
<div class="last:shadow-md"></div>
<div class="even:shadow-md"></div>
<div class="odd:shadow-md"></div>
<div class="visited:shadow-md"></div>
<div class="checked:shadow-md"></div>
<div class="focus-within:shadow-md"></div>
<div class="focus-visible:shadow-md"></div>
<!-- Group variants -->
<div class="group-hover:shadow-md"></div>
<div class="group-focus:shadow-md"></div>
<div class="group-disabled:shadow-md"></div>
<div class="group-active:shadow-md"></div>
<div class="group-first:shadow-md"></div>
<div class="group-last:shadow-md"></div>
<div class="group-even:shadow-md"></div>
<div class="group-odd:shadow-md"></div>
<div class="group-odd:shadow-md"></div>
<div class="group-visited:shadow-md"></div>
<div class="group-checked:shadow-md"></div>
<div class="group-focus-within:shadow-md"></div>
<div class="group-focus-visible:shadow-md"></div>
<div class="group-focus-visible:shadow-md"></div>
<!-- Reduced motion variants -->
<div class="motion-safe:shadow-md"></div>
<div class="motion-reduce:shadow-md"></div>
<!-- Direction variants -->
<div class="rtl:shadow-md"></div>
<div class="ltr:shadow-md"></div>
<!-- Dark mode variants -->
<div class="dark:shadow-md"></div>
<!-- Screen variants -->
<div class="sm:shadow-md"></div>
<div class="md:shadow-md"></div>
<div class="lg:shadow-md"></div>
<div class="xl:shadow-md"></div>
<div class="2xl:shadow-md"></div>
<!-- Stacked variants -->
<div class="focus:hover:shadow-md"></div>
<div class="sm:active:shadow-md"></div>
<div class="md:group-focus:shadow-md"></div>
<div class="lg:dark:shadow-md"></div>
<div class="xl:dark:disabledshadow-md"></div>
<div class="2xl:dark:motion-safe:focus-within:shadow-md"></div>
</body>
</html>

View file

@ -0,0 +1,34 @@
import postcss from 'postcss'
import fs from 'fs'
import path from 'path'
import tailwind from '../../src/jit/index.js'
function run(input, config = {}) {
return postcss(tailwind(config)).process(input, {
from: path.resolve(__filename),
})
}
test('variants', () => {
let config = {
darkMode: 'class',
mode: 'jit',
purge: [path.resolve(__dirname, './variants.test.html')],
corePlugins: { preflight: false },
theme: {},
plugins: [],
}
let css = `
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(css, config).then((result) => {
let expectedPath = path.resolve(__dirname, './variants.test.css')
let expected = fs.readFileSync(expectedPath, 'utf8')
expect(result.css).toMatchFormattedCss(expected)
})
})

93
tests/layerAtRule.test.js Normal file
View file

@ -0,0 +1,93 @@
import postcss from 'postcss'
import tailwind from '../src/index'
function run(input, config = {}) {
return postcss([tailwind({ corePlugins: [], ...config })]).process(input, { from: undefined })
}
test('layers are grouped and inserted at the matching @tailwind rule', () => {
const input = `
@layer vanilla {
strong { font-weight: medium }
}
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.btn { background: blue }
}
@layer utilities {
.align-banana { text-align: banana }
}
@layer base {
h1 { font-weight: bold }
}
@layer components {
.card { border-radius: 12px }
}
@layer base {
p { font-weight: normal }
}
@layer utilities {
.align-sandwich { text-align: sandwich }
}
@layer chocolate {
a { text-decoration: underline }
}
`
const expected = `
@layer vanilla {
strong { font-weight: medium }
}
body { margin: 0 }
h1 { font-weight: bold }
p { font-weight: normal }
.input { background: white }
.btn { background: blue }
.card { border-radius: 12px }
.float-squirrel { float: squirrel }
.align-banana { text-align: banana }
.align-sandwich { text-align: sandwich }
@layer chocolate {
a { text-decoration: underline }
}
`
expect.assertions(2)
return run(input, {
plugins: [
function ({ addBase, addComponents, addUtilities }) {
addBase({
body: {
margin: 0,
},
})
addComponents({
'.input': { background: 'white' },
})
addUtilities({
'.float-squirrel': { float: 'squirrel' },
})
},
],
}).then((result) => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})

14
tests/negateValue.test.js Normal file
View file

@ -0,0 +1,14 @@
import negateValue from '../src/util/negateValue'
test('it negates numeric CSS values', () => {
expect(negateValue('5')).toEqual('-5')
expect(negateValue('10px')).toEqual('-10px')
expect(negateValue('18rem')).toEqual('-18rem')
expect(negateValue('-10')).toEqual('10')
expect(negateValue('-7ch')).toEqual('7ch')
})
test('it leaves keywords untouched', () => {
expect(negateValue('auto')).toEqual('auto')
expect(negateValue('cover')).toEqual('cover')
})

View file

@ -0,0 +1,228 @@
import parseAnimationValue from '../src/util/parseAnimationValue'
import { produce } from './util/produce'
describe('Tailwind Defaults', () => {
it.each([
[
'spin 1s linear infinite',
{ name: 'spin', duration: '1s', timingFunction: 'linear', iterationCount: 'infinite' },
],
[
'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
{
name: 'ping',
duration: '1s',
timingFunction: 'cubic-bezier(0, 0, 0.2, 1)',
iterationCount: 'infinite',
},
],
[
'pulse 2s cubic-bezier(0.4, 0, 0.6) infinite',
{
name: 'pulse',
duration: '2s',
timingFunction: 'cubic-bezier(0.4, 0, 0.6)',
iterationCount: 'infinite',
},
],
['bounce 1s infinite', { name: 'bounce', duration: '1s', iterationCount: 'infinite' }],
])('should be possible to parse: "%s"', (input, expected) => {
expect(parseAnimationValue(input)).toEqual(expected)
})
})
describe('MDN Examples', () => {
it.each([
[
'3s ease-in 1s 2 reverse both paused slidein',
{
delay: '1s',
direction: 'reverse',
duration: '3s',
fillMode: 'both',
iterationCount: '2',
name: 'slidein',
playState: 'paused',
timingFunction: 'ease-in',
},
],
[
'slidein 3s linear 1s',
{ delay: '1s', duration: '3s', name: 'slidein', timingFunction: 'linear' },
],
['slidein 3s', { duration: '3s', name: 'slidein' }],
])('should be possible to parse: "%s"', (input, expected) => {
expect(parseAnimationValue(input)).toEqual(expected)
})
})
describe('duration & delay', () => {
it.each([
// Positive seconds (integer)
['spin 1s 1s linear', { duration: '1s', delay: '1s' }],
['spin 2s 1s linear', { duration: '2s', delay: '1s' }],
['spin 1s 2s linear', { duration: '1s', delay: '2s' }],
// Negative seconds (integer)
['spin -1s -1s linear', { duration: '-1s', delay: '-1s' }],
['spin -2s -1s linear', { duration: '-2s', delay: '-1s' }],
['spin -1s -2s linear', { duration: '-1s', delay: '-2s' }],
// Positive seconds (float)
['spin 1.321s 1.321s linear', { duration: '1.321s', delay: '1.321s' }],
['spin 2.321s 1.321s linear', { duration: '2.321s', delay: '1.321s' }],
['spin 1.321s 2.321s linear', { duration: '1.321s', delay: '2.321s' }],
// Negative seconds (float)
['spin -1.321s -1.321s linear', { duration: '-1.321s', delay: '-1.321s' }],
['spin -2.321s -1.321s linear', { duration: '-2.321s', delay: '-1.321s' }],
['spin -1.321s -2.321s linear', { duration: '-1.321s', delay: '-2.321s' }],
// Positive milliseconds (integer)
['spin 100ms 100ms linear', { duration: '100ms', delay: '100ms' }],
['spin 200ms 100ms linear', { duration: '200ms', delay: '100ms' }],
['spin 100ms 200ms linear', { duration: '100ms', delay: '200ms' }],
// Negative milliseconds (integer)
['spin -100ms -100ms linear', { duration: '-100ms', delay: '-100ms' }],
['spin -200ms -100ms linear', { duration: '-200ms', delay: '-100ms' }],
['spin -100ms -200ms linear', { duration: '-100ms', delay: '-200ms' }],
// Positive milliseconds (float)
['spin 100.321ms 100.321ms linear', { duration: '100.321ms', delay: '100.321ms' }],
['spin 200.321ms 100.321ms linear', { duration: '200.321ms', delay: '100.321ms' }],
['spin 100.321ms 200.321ms linear', { duration: '100.321ms', delay: '200.321ms' }],
// Negative milliseconds (float)
['spin -100.321ms -100.321ms linear', { duration: '-100.321ms', delay: '-100.321ms' }],
['spin -200.321ms -100.321ms linear', { duration: '-200.321ms', delay: '-100.321ms' }],
['spin -100.321ms -200.321ms linear', { duration: '-100.321ms', delay: '-200.321ms' }],
])('should be possible to parse "%s" into %o', (input, { duration, delay }) => {
const parsed = parseAnimationValue(input)
expect(parsed.duration).toEqual(duration)
expect(parsed.delay).toEqual(delay)
})
})
describe('iteration count', () => {
it.each([
// Number
['1 spin 200s 100s linear', '1'],
['spin 2 200s 100s linear', '2'],
['spin 200s 3 100s linear', '3'],
['spin 200s 100s 4 linear', '4'],
['spin 200s 100s linear 5', '5'],
// Infinite
['infinite spin 200s 100s linear', 'infinite'],
['spin infinite 200s 100s linear', 'infinite'],
['spin 200s infinite 100s linear', 'infinite'],
['spin 200s 100s infinite linear', 'infinite'],
['spin 200s 100s linear infinite', 'infinite'],
])(
'should be possible to parse "%s" with an iteraction count of "%s"',
(input, iterationCount) => {
expect(parseAnimationValue(input).iterationCount).toEqual(iterationCount)
}
)
})
describe('iteration count', () => {
it.each([
// Number
['1 spin 200s 100s linear', '1'],
['spin 2 200s 100s linear', '2'],
['spin 200s 3 100s linear', '3'],
['spin 200s 100s 4 linear', '4'],
['spin 200s 100s linear 5', '5'],
['100 spin 200s 100s linear', '100'],
['spin 200 200s 100s linear', '200'],
['spin 200s 300 100s linear', '300'],
['spin 200s 100s 400 linear', '400'],
['spin 200s 100s linear 500', '500'],
// Infinite
['infinite spin 200s 100s linear', 'infinite'],
['spin infinite 200s 100s linear', 'infinite'],
['spin 200s infinite 100s linear', 'infinite'],
['spin 200s 100s infinite linear', 'infinite'],
['spin 200s 100s linear infinite', 'infinite'],
])(
'should be possible to parse "%s" with an iteraction count of "%s"',
(input, iterationCount) => {
expect(parseAnimationValue(input).iterationCount).toEqual(iterationCount)
}
)
})
describe('multiple animations', () => {
it('should be possible to parse multiple applications at once', () => {
const input = [
'spin 1s linear infinite',
'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
'pulse 2s cubic-bezier(0.4, 0, 0.6) infinite',
].join(',')
const parsed = parseAnimationValue(input)
expect(parsed).toHaveLength(3)
expect(parsed).toEqual([
{ name: 'spin', duration: '1s', timingFunction: 'linear', iterationCount: 'infinite' },
{
name: 'ping',
duration: '1s',
timingFunction: 'cubic-bezier(0, 0, 0.2, 1)',
iterationCount: 'infinite',
},
{
name: 'pulse',
duration: '2s',
timingFunction: 'cubic-bezier(0.4, 0, 0.6)',
iterationCount: 'infinite',
},
])
})
})
describe('randomized crazy big examples', () => {
function reOrder(input, offset = 0) {
return [...input.slice(offset), ...input.slice(0, offset)]
}
it.each(
produce((choose) => {
const direction = choose('normal', 'reverse', 'alternate', 'alternate-reverse')
const playState = choose('running', 'paused')
const fillMode = choose('none', 'forwards', 'backwards', 'both')
const iterationCount = choose('infinite', '1', '100')
const timingFunction = choose(
'linear',
'ease',
'ease-in',
'ease-out',
'ease-in-out',
'cubic-bezier(0, 0, 0.2, 1)',
'steps(4, end)'
)
const name = choose('animation-name-a', 'animation-name-b')
const inputArgs = [direction, playState, fillMode, iterationCount, timingFunction, name]
const orderOffset = choose(...Array(inputArgs.length).keys())
return [
// Input
reOrder(inputArgs, orderOffset).join(' '),
// Output
{
direction,
playState,
fillMode,
iterationCount,
timingFunction,
name,
},
]
})
)('should be possible to parse "%s"', (input, output) => {
expect(parseAnimationValue(input)).toEqual(output)
})
})

View file

@ -0,0 +1,302 @@
import parseObjectStyles from '../src/util/parseObjectStyles'
import postcss from 'postcss'
function css(nodes) {
return postcss.root({ nodes }).toString()
}
test('it parses simple single class definitions', () => {
const result = parseObjectStyles({
'.foobar': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
},
})
expect(css(result)).toMatchCss(`
.foobar {
background-color: red;
color: white;
padding: 1rem
}
`)
})
test('it parses multiple class definitions', () => {
const result = parseObjectStyles({
'.foo': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
},
'.bar': {
width: '200px',
height: '100px',
},
})
expect(css(result)).toMatchCss(`
.foo {
background-color: red;
color: white;
padding: 1rem
}
.bar {
width: 200px;
height: 100px
}
`)
})
test('it parses nested pseudo-selectors', () => {
const result = parseObjectStyles({
'.foo': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
':hover': {
backgroundColor: 'orange',
},
':focus': {
backgroundColor: 'blue',
},
},
})
expect(css(result)).toMatchCss(`
.foo {
background-color: red;
color: white;
padding: 1rem;
}
.foo:hover {
background-color: orange;
}
.foo:focus {
background-color: blue;
}
`)
})
test('it parses top-level media queries', () => {
const result = parseObjectStyles({
'@media (min-width: 200px)': {
'.foo': {
backgroundColor: 'orange',
},
},
})
expect(css(result)).toMatchCss(`
@media (min-width: 200px) {
.foo {
background-color: orange
}
}
`)
})
test('it parses nested media queries', () => {
const result = parseObjectStyles({
'.foo': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
'@media (min-width: 200px)': {
backgroundColor: 'orange',
},
},
})
expect(css(result)).toMatchCss(`
.foo {
background-color: red;
color: white;
padding: 1rem;
}
@media (min-width: 200px) {
.foo {
background-color: orange;
}
}
`)
})
test('it bubbles nested screen rules', () => {
const result = parseObjectStyles({
'.foo': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
'@screen sm': {
backgroundColor: 'orange',
},
},
})
expect(css(result)).toMatchCss(`
.foo {
background-color: red;
color: white;
padding: 1rem;
}
@screen sm {
.foo {
background-color: orange;
}
}
`)
})
test('it parses pseudo-selectors in nested media queries', () => {
const result = parseObjectStyles({
'.foo': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
':hover': {
'@media (min-width: 200px)': {
backgroundColor: 'orange',
},
},
},
})
expect(css(result)).toMatchCss(`
.foo {
background-color: red;
color: white;
padding: 1rem;
}
@media (min-width: 200px) {
.foo:hover {
background-color: orange;
}
}
`)
})
test('it parses descendant selectors', () => {
const result = parseObjectStyles({
'.foo': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
'.bar': {
backgroundColor: 'orange',
},
},
})
expect(css(result)).toMatchCss(`
.foo {
background-color: red;
color: white;
padding: 1rem;
}
.foo .bar {
background-color: orange;
}
`)
})
test('it parses nested multi-class selectors', () => {
const result = parseObjectStyles({
'.foo': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
'&.bar': {
backgroundColor: 'orange',
},
},
})
expect(css(result)).toMatchCss(`
.foo {
background-color: red;
color: white;
padding: 1rem;
}
.foo.bar {
background-color: orange;
}
`)
})
test('it parses nested multi-class selectors in media queries', () => {
const result = parseObjectStyles({
'.foo': {
backgroundColor: 'red',
color: 'white',
padding: '1rem',
'@media (min-width: 200px)': {
'&.bar': {
backgroundColor: 'orange',
},
},
},
})
expect(css(result)).toMatchCss(`
.foo {
background-color: red;
color: white;
padding: 1rem;
}
@media (min-width: 200px) {
.foo.bar {
background-color: orange;
}
}
`)
})
test('it strips empty selectors when nesting', () => {
const result = parseObjectStyles({
'.foo': {
'.bar': {
backgroundColor: 'orange',
},
},
})
expect(css(result)).toMatchCss(`
.foo .bar {
background-color: orange
}
`)
})
test('it can parse an array of styles', () => {
const result = parseObjectStyles([
{
'.foo': {
backgroundColor: 'orange',
},
},
{
'.bar': {
backgroundColor: 'red',
},
},
{
'.foo': {
backgroundColor: 'blue',
},
},
])
expect(css(result)).toMatchCss(`
.foo {
background-color: orange
}
.bar {
background-color: red
}
.foo {
background-color: blue
}
`)
})

View file

@ -0,0 +1,131 @@
import postcss from 'postcss'
import processPlugins from '../../src/util/processPlugins'
import plugin from '../../src/plugins/animation'
function css(nodes) {
return postcss.root({ nodes }).toString()
}
test('defining animation and keyframes', () => {
const config = {
theme: {
animation: {
none: 'none',
spin: 'spin 1s linear infinite',
ping: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
},
keyframes: {
spin: { to: { transform: 'rotate(360deg)' } },
ping: { '75%, 100%': { transform: 'scale(2)', opacity: '0' } },
},
},
variants: {
animation: [],
},
}
const { utilities } = processPlugins([plugin()], config)
expect(css(utilities)).toMatchCss(`
@layer utilities {
@variants {
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes ping {
75%, 100% { transform: scale(2); opacity: 0; }
}
}
}
@layer utilities {
@variants {
.animate-none { animation: none; }
.animate-spin { animation: spin 1s linear infinite; }
.animate-ping { animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite; }
}
}
`)
})
test('defining animation and keyframes with prefix', () => {
const config = {
prefix: 'tw-',
theme: {
animation: {
none: 'none',
spin: 'spin 1s linear infinite',
ping: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
},
keyframes: {
spin: { to: { transform: 'rotate(360deg)' } },
ping: { '75%, 100%': { transform: 'scale(2)', opacity: '0' } },
},
},
variants: {
animation: [],
},
}
const { utilities } = processPlugins([plugin()], config)
expect(css(utilities)).toMatchCss(`
@layer utilities {
@variants {
@keyframes tw-spin {
to { transform: rotate(360deg); }
}
@keyframes tw-ping {
75%, 100% { transform: scale(2); opacity: 0; }
}
}
}
@layer utilities {
@variants {
.tw-animate-none { animation: none; }
.tw-animate-spin { animation: tw-spin 1s linear infinite; }
.tw-animate-ping { animation: tw-ping 1s cubic-bezier(0, 0, 0.2, 1) infinite; }
}
}
`)
})
test('defining animation and keyframes with prefix (skip undefined animations)', () => {
const config = {
prefix: 'tw-',
theme: {
animation: {
none: 'none',
spin: 'spin 1s linear infinite',
ping: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
},
keyframes: {
spin: { to: { transform: 'rotate(360deg)' } },
},
},
variants: {
animation: [],
},
}
const { utilities } = processPlugins([plugin()], config)
expect(css(utilities)).toMatchCss(`
@layer utilities {
@variants {
@keyframes tw-spin {
to { transform: rotate(360deg); }
}
}
}
@layer utilities {
@variants {
.tw-animate-none { animation: none; }
.tw-animate-spin { animation: tw-spin 1s linear infinite; }
.tw-animate-ping { animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite; }
}
}
`)
})

View file

@ -0,0 +1,28 @@
import invokePlugin from '../util/invokePlugin'
import plugin from '../../src/plugins/backgroundColor'
test('defining color as a function', () => {
const config = {
theme: {
backgroundColor: {
black: ({ opacityVariable: _ }) => 'black',
},
},
variants: {
backgroundColor: [],
},
}
const { utilities } = invokePlugin(plugin(), config)
expect(utilities).toEqual([
[
{
'.bg-black': {
'background-color': 'black',
},
},
[],
],
])
})

View file

@ -0,0 +1,28 @@
import invokePlugin from '../util/invokePlugin'
import plugin from '../../src/plugins/borderColor'
test('defining color as a function', () => {
const config = {
theme: {
borderColor: {
black: ({ opacityVariable: _ }) => 'black',
},
},
variants: {
borderColor: [],
},
}
const { utilities } = invokePlugin(plugin(), config)
expect(utilities).toEqual([
[
{
'.border-black': {
'border-color': 'black',
},
},
[],
],
])
})

View file

@ -0,0 +1,79 @@
import _ from 'lodash'
import escapeClassName from '../../src/util/escapeClassName'
import plugin from '../../src/plugins/boxShadow'
test('box shadow can use DEFAULT keyword and negative prefix syntax', () => {
const addedUtilities = []
const config = {
theme: {
boxShadow: {
DEFAULT: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
'-': 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)',
'-md': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
},
},
variants: {
boxShadow: ['responsive'],
},
}
const getConfigValue = (path, defaultValue) => _.get(config, path, defaultValue)
const pluginApi = {
config: getConfigValue,
e: escapeClassName,
theme: (path, defaultValue) => getConfigValue(`theme.${path}`, defaultValue),
variants: (path, defaultValue) => {
if (_.isArray(config.variants)) {
return config.variants
}
return getConfigValue(`variants.${path}`, defaultValue)
},
addUtilities(utilities, options) {
addedUtilities.push({
utilities,
options,
})
},
}
plugin()(pluginApi)
expect(addedUtilities).toEqual([
{
utilities: {
'*': {
'--tw-shadow': '0 0 #0000',
},
},
options: { respectImportant: false },
},
{
utilities: {
'.shadow': {
'--tw-shadow': '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
'box-shadow':
'var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)',
},
'.shadow-md': {
'--tw-shadow': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
'box-shadow':
'var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)',
},
'.-shadow': {
'--tw-shadow': 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)',
'box-shadow':
'var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)',
},
'.-shadow-md': {
'--tw-shadow': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
'box-shadow':
'var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)',
},
},
options: ['responsive'],
},
])
})

View file

@ -0,0 +1,28 @@
import invokePlugin from '../util/invokePlugin'
import plugin from '../../src/plugins/divideColor'
test('defining color as a function', () => {
const config = {
theme: {
divideColor: {
black: ({ opacityVariable: _ }) => 'black',
},
},
variants: {
divideColor: [],
},
}
const { utilities } = invokePlugin(plugin(), config)
expect(utilities).toEqual([
[
{
'.divide-black > :not([hidden]) ~ :not([hidden])': {
'border-color': 'black',
},
},
[],
],
])
})

View file

@ -0,0 +1,74 @@
import invokePlugin from '../util/invokePlugin'
import plugin from '../../src/plugins/divideWidth'
test('generating divide width utilities', () => {
const config = {
theme: {
divideWidth: {
DEFAULT: '1px',
0: '0',
2: '2px',
4: '4px',
},
},
variants: {
divideWidth: ['responsive'],
},
}
const { utilities } = invokePlugin(plugin(), config)
expect(utilities).toEqual([
[
{
'.divide-y > :not([hidden]) ~ :not([hidden])': {
'--tw-divide-y-reverse': '0',
'border-top-width': 'calc(1px * calc(1 - var(--tw-divide-y-reverse)))',
'border-bottom-width': 'calc(1px * var(--tw-divide-y-reverse))',
},
'.divide-x > :not([hidden]) ~ :not([hidden])': {
'--tw-divide-x-reverse': '0',
'border-right-width': 'calc(1px * var(--tw-divide-x-reverse))',
'border-left-width': 'calc(1px * calc(1 - var(--tw-divide-x-reverse)))',
},
'.divide-y-0 > :not([hidden]) ~ :not([hidden])': {
'--tw-divide-y-reverse': '0',
'border-top-width': 'calc(0px * calc(1 - var(--tw-divide-y-reverse)))',
'border-bottom-width': 'calc(0px * var(--tw-divide-y-reverse))',
},
'.divide-x-0 > :not([hidden]) ~ :not([hidden])': {
'--tw-divide-x-reverse': '0',
'border-right-width': 'calc(0px * var(--tw-divide-x-reverse))',
'border-left-width': 'calc(0px * calc(1 - var(--tw-divide-x-reverse)))',
},
'.divide-y-2 > :not([hidden]) ~ :not([hidden])': {
'--tw-divide-y-reverse': '0',
'border-top-width': 'calc(2px * calc(1 - var(--tw-divide-y-reverse)))',
'border-bottom-width': 'calc(2px * var(--tw-divide-y-reverse))',
},
'.divide-x-2 > :not([hidden]) ~ :not([hidden])': {
'--tw-divide-x-reverse': '0',
'border-right-width': 'calc(2px * var(--tw-divide-x-reverse))',
'border-left-width': 'calc(2px * calc(1 - var(--tw-divide-x-reverse)))',
},
'.divide-y-4 > :not([hidden]) ~ :not([hidden])': {
'--tw-divide-y-reverse': '0',
'border-top-width': 'calc(4px * calc(1 - var(--tw-divide-y-reverse)))',
'border-bottom-width': 'calc(4px * var(--tw-divide-y-reverse))',
},
'.divide-x-4 > :not([hidden]) ~ :not([hidden])': {
'--tw-divide-x-reverse': '0',
'border-right-width': 'calc(4px * var(--tw-divide-x-reverse))',
'border-left-width': 'calc(4px * calc(1 - var(--tw-divide-x-reverse)))',
},
'.divide-y-reverse > :not([hidden]) ~ :not([hidden])': {
'--tw-divide-y-reverse': '1',
},
'.divide-x-reverse > :not([hidden]) ~ :not([hidden])': {
'--tw-divide-x-reverse': '1',
},
},
['responsive'],
],
])
})

View file

@ -0,0 +1,28 @@
import invokePlugin from '../util/invokePlugin'
import plugin from '../../src/plugins/fill'
test('defining color as a function', () => {
const config = {
theme: {
fill: {
black: ({ opacityVariable: _ }) => 'black',
},
},
variants: {
fill: [],
},
}
const { utilities } = invokePlugin(plugin(), config)
expect(utilities).toEqual([
[
{
'.fill-black': {
fill: 'black',
},
},
[],
],
])
})

View file

@ -0,0 +1,86 @@
import invokePlugin from '../util/invokePlugin'
import plugin from '../../src/plugins/fontSize'
test('font-size utilities can include a default line-height', () => {
const config = {
theme: {
fontSize: {
sm: '12px',
md: ['16px', '24px'],
lg: ['20px', '28px'],
},
},
variants: {
fontSize: ['responsive'],
},
}
const { utilities } = invokePlugin(plugin(), config)
expect(utilities).toEqual([
[
{
'.text-sm': { 'font-size': '12px' },
'.text-md': { 'font-size': '16px', 'line-height': '24px' },
'.text-lg': { 'font-size': '20px', 'line-height': '28px' },
},
['responsive'],
],
])
})
test('font-size utilities can include a default letter-spacing', () => {
const config = {
theme: {
fontSize: {
sm: '12px',
md: ['16px', { letterSpacing: '-0.01em' }],
lg: ['20px', { letterSpacing: '-0.02em' }],
},
},
variants: {
fontSize: ['responsive'],
},
}
const { utilities } = invokePlugin(plugin(), config)
expect(utilities).toEqual([
[
{
'.text-sm': { 'font-size': '12px' },
'.text-md': { 'font-size': '16px', 'letter-spacing': '-0.01em' },
'.text-lg': { 'font-size': '20px', 'letter-spacing': '-0.02em' },
},
['responsive'],
],
])
})
test('font-size utilities can include a default line-height and letter-spacing', () => {
const config = {
theme: {
fontSize: {
sm: '12px',
md: ['16px', { lineHeight: '24px', letterSpacing: '-0.01em' }],
lg: ['20px', { lineHeight: '28px', letterSpacing: '-0.02em' }],
},
},
variants: {
fontSize: ['responsive'],
},
}
const { utilities } = invokePlugin(plugin(), config)
expect(utilities).toEqual([
[
{
'.text-sm': { 'font-size': '12px' },
'.text-md': { 'font-size': '16px', 'line-height': '24px', 'letter-spacing': '-0.01em' },
'.text-lg': { 'font-size': '20px', 'line-height': '28px', 'letter-spacing': '-0.02em' },
},
['responsive'],
],
])
})

View file

@ -0,0 +1,82 @@
import postcss from 'postcss'
import tailwind from '../../src/index'
test('opacity variables are given to colors defined as closures', () => {
return postcss([
tailwind({
theme: {
colors: {
primary: ({ opacityVariable, opacityValue }) => {
if (opacityValue !== undefined) {
return `rgba(31,31,31,${opacityValue})`
}
if (opacityVariable !== undefined) {
return `rgba(31,31,31,var(${opacityVariable},1))`
}
return `rgb(31,31,31)`
},
secondary: 'hsl(10, 50%, 50%)',
},
opacity: {
50: '0.5',
},
},
variants: {
textColor: [],
textOpacity: [],
gradientColorStops: [],
},
corePlugins: ['textColor', 'textOpacity', 'gradientColorStops'],
}),
])
.process('@tailwind utilities', { from: undefined })
.then((result) => {
const expected = `
.text-primary {
--tw-text-opacity: 1;
color: rgba(31, 31, 31, var(--tw-text-opacity));
}
.text-secondary {
--tw-text-opacity: 1;
color: hsla(10, 50%, 50%, var(--tw-text-opacity));
}
.text-opacity-50 {
--tw-text-opacity: 0.5;
}
.from-primary {
--tw-gradient-from: rgb(31, 31, 31);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(31, 31, 31, 0));
}
.from-secondary {
--tw-gradient-from: hsl(10, 50%, 50%);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, hsla(10, 50%, 50%, 0));
}
.via-primary {
--tw-gradient-stops: var(--tw-gradient-from), rgb(31, 31, 31),
var(--tw-gradient-to, rgba(31, 31, 31, 0));
}
.via-secondary {
--tw-gradient-stops: var(--tw-gradient-from), hsl(10, 50%, 50%),
var(--tw-gradient-to, hsla(10, 50%, 50%, 0));
}
.to-primary {
--tw-gradient-to: rgb(31, 31, 31);
}
.to-secondary {
--tw-gradient-to: hsl(10, 50%, 50%);
}
`
expect(result.css).toMatchCss(expected)
})
})

View file

@ -0,0 +1,53 @@
import _ from 'lodash'
import escapeClassName from '../../src/util/escapeClassName'
import plugin from '../../src/plugins/letterSpacing'
test('letter spacing can use negative prefix syntax', () => {
const addedUtilities = []
const config = {
theme: {
letterSpacing: {
'-1': '-0.025em',
1: '0.025em',
},
},
variants: {
letterSpacing: ['responsive'],
},
}
const getConfigValue = (path, defaultValue) => _.get(config, path, defaultValue)
const pluginApi = {
config: getConfigValue,
e: escapeClassName,
theme: (path, defaultValue) => getConfigValue(`theme.${path}`, defaultValue),
variants: (path, defaultValue) => {
if (_.isArray(config.variants)) {
return config.variants
}
return getConfigValue(`variants.${path}`, defaultValue)
},
addUtilities(utilities, variants) {
addedUtilities.push({
utilities,
variants,
})
},
}
plugin()(pluginApi)
expect(addedUtilities).toEqual([
{
utilities: [
{
'.-tracking-1': { letterSpacing: '-0.025em' },
'.tracking-1': { letterSpacing: '0.025em' },
},
],
variants: ['responsive'],
},
])
})

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