Merge engines (#11275)

* Simplify CI, make oxide engine the default (#11281)

* remove integration tests for the `stable` engine

* make `integration-tests-oxide` the default integration tests workflow

* remove unnecessary CACHE_PREFIX

* drop `ci-stable.yml` workflow

* make `CI — Oxide` the default

+ drop testing against 3.3 branch (since this is stable only)
+ remove unnecessary CACHE_PREFIX

* drop `release-insiders-stable.yml` workflow

* make `Release Insiders — Oxide` the default

+ change release channel to just `insiders` instead of `oxide-insders`

* drop 3.3 branch from integration tests

* change job name for insiders release

This makes it consistent with the other job names

* prep `release-oxide.yml` workflow to be the default workflow

* add Tailwind Play update

Currently commented out until we figure out how this will work exactly.

* use `env.VERSION` for the version we want to release

* drop `release-stable.yml` workflow

* make `release-oxide.yml` the default `release.yml` workflow

* inline "Calculate environment variables" step

* cache node_modules and cargo related files/folders in the release step

* always use the default CLI

* drop oxide specific implementation

* ensure we test `--postcss` in the CLI

Even for the Oxide engine

* always process `@import` rules

* remove `generalizedModifiers` flag

This was already enabled by default. This commits removes some of the
outstanding references to it.

* remove built-in peer dependencies for the CLI

* drop unused esbuild dependency

* fix type for `configBag`

* setup Lightning CSS for the CLI

* drop `cssnano` dependency

We only used this in the CLI and we now use Lighting CSS which has this
built in already. Therefore we can get rid of this as a dependency.

* ensure `imported.css` file exists in integration tests

* passthrough `options` to lightningcss

Ideally we can read this from `result.map` instead. However this doesn't
get filled in for whatever reason even though we use `map: {inline:true}`.

You can run the `tests/cli.test.js`, and more specifically the
`--postcss supports process options with custom config` test.

* Always process CSS with Lightning CSS

Some tests temporarily skipped. Autoprefixer removed from `postcss.config.js` stubs since vendor prefixing handled by Lightning.

* Remove code for normalizing `@import "tailwindcss/*"` statements

* ensure tailwind doesn't crash when a `tailwind.config.js` file is not present

* add `log.group` to group multiple messages together with different types

* remove engine specific checks in integration tests

* remove unused imports/variables

Make linting check happy

* support `content: ['auto']`

This will allow us to still use auto content detection, but since this
is part of the `content` array, it also allows you to add custom paths
if you want.

E.g.:
```js
content: ['auto', './node_modules/my-library/*.{jsx,tsx}']
```

* ensure `content.files` is always an array

This allows us to simplify some checks because 'auto' will now be part
of the array instead of checking if `content.files` is auto or if it is
an array containing 'auto'.

* fix tests, ensure default config is not injected

* simplify `auto` normalization

* always use the defaultFullConfig

No need to override the defaultFullConfig with `{content: 'auto'}`
because this will be included in the default config already.

* drop condition which ensured `files` was an empty array

This is not needed anymore because when `content` is omitted it behaves
as if it was set to `auto`.

* drop removal of `content`

We used to remove the `content` from the actual configuration files when
using `tailwindcss init` for the oxide engine. This was to ensure that
`auto content detection` was used instead (by default).

But now it is always enabled by default therefore this is not needed
anymore.

* ensure setting `content: 'auto'` works

* improve file cache handling

We have to make sure that we get into the previous state whenver a test
is done.
We were using some `!fileCache[filePath]` checks, however we also used
`null` as a sentinel value when something wasn't found. But, `null` is
falsey as well so some checks where incorrect.

Using a dedicated value and a `Map` makes this safer and more correct
because we can now use `.has()`.

* drop `cleanupFile`, `removeFile` will already take care of it

* drop oxide check in `resolvedChangedContent`

At this point the `context.tailwindConfig.content.files` is already
fully resolved regardless of the engine we are in.

* refactor `parseCandidateStringsFromFiles` strategy, to make use of a proper feature flag

* use feature flag for sorting classes

Once the Oxide engine is the default default, then we can drop this
entirely because the result from the Rust parser will already be sorted.

* drop `crosscheck` from tests where it is safe

First pass of deleting `crosscheck`. These are all the places where we
didn't have any differences between the Oxide and Stable engine.

* replace `__OXIDE__` in corePlugins with feature flag

* use `globalThis.__OXIDE__ ?? false`

This will ensure that in places where we don't have `__OXIDE__` that we
can still properly fallback to `false`.

* use `flex` as the go-to utility when testing features

* prefer `space-utilities` tests instead of `oxide` test

* drop crosscheck check

Since the `oxideParser` is disabled by default, all the tests can use
the `stable` output.

* use simple `false` default values for feature flag defaults

* drop `__OXIDE__` injections in build scripts

* use stable `flex` and `z-{...}` utilities instead of color utilities

flex and z-index related utilities are more stable than the color
utilities right now because the color utilities will use a raw color
value or a combination with a css variable depending on a feature flag.

* drop unused `ENGINE` environment variable

* drop stable engine manifest files

* drop `swap-engines` tooling

---------

Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
This commit is contained in:
Robin Malfait 2023-05-25 04:10:00 +02:00 • committed by GitHub
parent 0af64ee3e8
commit 581cf6abcc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
154 changed files with 26652 additions and 48773 deletions

View file

@ -1,59 +0,0 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: CI — Stable
on:
push:
branches: [master]
pull_request:
branches: [master, 3.3]
permissions:
contents: read
env:
CI: true
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
CACHE_PREFIX: stable
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14, 18]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Use the `stable` engine
run: |
node ./scripts/swap-engines.js
- name: Cache node_modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-${{ matrix.node-version }}-${{ env.CACHE_PREFIX }}-node_modules-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
run: npm install
- name: Build Tailwind CSS
run: npm run build
- name: Test
run: |
npm run test || \
npm run test || \
npm run test || exit 1
- name: Lint
run: npm run style

View file

@ -1,10 +1,10 @@
name: CI — Oxide
name: CI
on:
push:
branches: [master, 3.3]
branches: [master]
pull_request:
branches: [master, 3.3]
branches: [master]
permissions:
contents: read
@ -13,7 +13,6 @@ env:
CI: true
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
CACHE_PREFIX: oxide
jobs:
build:
@ -34,7 +33,7 @@ jobs:
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-${{ matrix.node-version }}-${{ env.CACHE_PREFIX }}-node_modules-${{ hashFiles('**/package-lock.json') }}
key: ${{ runner.os }}-${{ matrix.node-version }}-node_modules-${{ hashFiles('**/package-lock.json') }}
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo

View file

@ -1,83 +0,0 @@
name: Integration Tests — Stable
on:
push:
branches: [master, 3.3]
pull_request:
branches: [master, 3.3]
permissions:
contents: read
env:
CI: true
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
CACHE_PREFIX: stable
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
integration:
- content-resolution
- parcel
- postcss-cli
- rollup
- rollup-sass
- tailwindcss-cli
- vite
- webpack-4
- webpack-5
node-version: [18]
fail-fast: false
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Use the `stable` engine
run: |
node ./scripts/swap-engines.js
- name: Cache node_modules (tailwindcss)
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-${{ matrix.node-version }}-${{ env.CACHE_PREFIX }}-node_modules-${{ hashFiles('package-lock.json') }}
- name: Cache node_modules (integrations)
uses: actions/cache@v3
with:
path: ./integrations/node_modules
key: ${{ runner.os }}-${{ matrix.node-version }}-${{ env.CACHE_PREFIX }}-node_modules-integrations-${{ hashFiles('./integrations/package-lock.json') }}
- name: Cache node_modules (integration)
uses: actions/cache@v3
with:
path: ./integrations/${{ matrix.integration }}/node_modules
key: ${{ runner.os }}-${{ matrix.node-version }}-${{ env.CACHE_PREFIX }}-node_modules-integrations-${{ matrix.integration }}-${{ hashFiles('./integrations/${{ matrix.integration }}/package-lock.json') }}
- name: Install dependencies
run: npm install
- name: Install dependencies (shared integrations)
run: npm install --prefix ./integrations
- name: Install dependencies (integration)
run: npm install --prefix ./integrations/${{ matrix.integration }}
- name: Build Tailwind CSS
run: npm run build
- name: Test ${{ matrix.integration }}
run: |
npm run test --prefix ./integrations/${{ matrix.integration }} || \
npm run test --prefix ./integrations/${{ matrix.integration }} || \
npm run test --prefix ./integrations/${{ matrix.integration }} || exit 1

View file

@ -1,10 +1,10 @@
name: Integration Tests — Oxide
name: Integration Tests
on:
push:
branches: [master, 3.3]
branches: [master]
pull_request:
branches: [master, 3.3]
branches: [master]
permissions:
contents: read
@ -13,7 +13,6 @@ env:
CI: true
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
CACHE_PREFIX: oxide
jobs:
test:
@ -46,7 +45,7 @@ jobs:
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-${{ matrix.node-version }}-${{ env.CACHE_PREFIX }}-node_modules-${{ hashFiles('**/package-lock.json') }}
key: ${{ runner.os }}-${{ matrix.node-version }}-node_modules-${{ hashFiles('**/package-lock.json') }}
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo

View file

@ -1,78 +0,0 @@
name: Release Insiders — Stable
on:
push:
branches: [master]
permissions:
contents: read
env:
CI: true
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
CACHE_PREFIX: stable
NODE_VERSION: 16
RELEASE_CHANNEL: insiders
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org'
- name: Use the `stable` engine
run: |
node ./scripts/swap-engines.js
- name: Cache node_modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-${{ env.NODE_VERSION }}-${{ env.CACHE_PREFIX }}-node_modules-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
run: npm install
- name: Build Tailwind CSS
run: npm run build
- name: Test
run: |
npm run test || \
npm run test || \
npm run test || exit 1
- name: Resolve version
id: vars
run: |
echo "SHA_SHORT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: 'Version based on commit: 0.0.0-${{ env.RELEASE_CHANNEL }}.${{ env.SHA_SHORT }}'
run: npm version 0.0.0-${{ env.RELEASE_CHANNEL }}.${{ env.SHA_SHORT }} --force --no-git-tag-version
- name: Publish
run: npm publish --tag ${{ env.RELEASE_CHANNEL }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Trigger Tailwind Play update
uses: actions/github-script@v6
with:
github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }}
script: |
await github.rest.actions.createWorkflowDispatch({
owner: 'tailwindlabs',
repo: 'play.tailwindcss.com',
ref: 'master',
workflow_id: 'upgrade-tailwindcss.yml',
inputs: {
insidersVersion: '0.0.0-${{ env.RELEASE_CHANNEL }}.${{ env.SHA_SHORT }}'
}
})

View file

@ -1,4 +1,4 @@
name: Release Insiders — Oxide
name: Release Insiders
on:
push:
@ -8,7 +8,7 @@ env:
APP_NAME: tailwindcss-oxide
NODE_VERSION: 16
OXIDE_LOCATION: ./oxide/crates/node
RELEASE_CHANNEL: oxide-insiders
RELEASE_CHANNEL: insiders
jobs:
build:
@ -353,7 +353,7 @@ jobs:
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
tailwind_oxide_release:
tailwind-release:
runs-on: ubuntu-latest
name: Build and release Tailwind CSS

View file

@ -1,68 +0,0 @@
name: Release — Stable
on:
release:
types: [published]
permissions:
contents: read
env:
CI: true
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
- name: Use the `stable` engine
run: |
node ./scripts/swap-engines.js
- name: Install dependencies
run: npm install
- name: Build Tailwind CSS
run: npm run build
- name: Test
run: npm run test
- name: Calculate environment variables
run: |
echo "RELEASE_CHANNEL=$(node ./scripts/release-channel.js)" >> $GITHUB_ENV
echo "TAILWINDCSS_VERSION=$(node -e 'console.log(require(`./package.json`).version);')" >> $GITHUB_ENV
- name: Publish
run: npm publish --tag ${{ env.RELEASE_CHANNEL }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Trigger Tailwind Play update
if: env.RELEASE_CHANNEL == 'latest'
uses: actions/github-script@v6
with:
github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }}
script: |
await github.rest.actions.createWorkflowDispatch({
owner: 'tailwindlabs',
repo: 'play.tailwindcss.com',
ref: 'master',
workflow_id: 'upgrade-tailwindcss.yml',
inputs: {
version: '${{ env.TAILWINDCSS_VERSION }}'
}
})

View file

@ -1,15 +1,19 @@
name: Release — Oxide
name: Release
on:
workflow_dispatch:
release:
types: [published]
permissions:
contents: read
env:
APP_NAME: tailwindcss-oxide
NODE_VERSION: 14
NODE_VERSION: 16
OXIDE_LOCATION: ./oxide/crates/node
jobs:
oxide-build:
build:
strategy:
fail-fast: false
matrix:
@ -27,6 +31,35 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Cache node_modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-${{ matrix.target }}-${{ env.NODE_VERSION }}-node_modules-${{ hashFiles('**/package-lock.json') }}
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo
uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
# Cache the `oxide` Rust build
- name: Cache oxide build
uses: actions/cache@v3
with:
path: |
./oxide/target/
./oxide/crates/node/*.node
./oxide/crates/node/index.js
./oxide/crates/node/index.d.ts
key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./oxide/crates/**/*') }}
- name: Install Node.JS
uses: actions/setup-node@v3
with:
@ -42,18 +75,8 @@ jobs:
- name: Setup rust target
run: rustup target add ${{ matrix.target }}
# - name: Use cached oxide node_modules
# id: cache-oxide
# uses: actions/cache@v3
# with:
# path: ${{ env.OXIDE_LOCATION }}/node_modules
# key: nodeModules-${{ hashFiles('${{ env.OXIDE_LOCATION }}/package-lock.json') }}-${{ matrix.os }}-${{ matrix.target }}-${{ env.NODE_VERSION }}
# restore-keys: |
# nodeModules-
- name: Oxide — npm install
# if: steps.cache-oxide.outputs.cache-hit != 'true'
run: cd ${{ env.OXIDE_LOCATION }} && npm install
- name: Install dependencies
run: npm install
- name: Build release
run: cd ${{ env.OXIDE_LOCATION }} && npm run build
@ -72,7 +95,7 @@ jobs:
${{ env.OXIDE_LOCATION }}/*.node
${{ matrix.binary }}
oxide-build-apple-silicon:
build-apple-silicon:
name: Build Apple Silicon (OXIDE)
runs-on: macos-latest
steps:
@ -82,7 +105,7 @@ jobs:
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-${{ matrix.node-version }}-node_modules-${{ hashFiles('**/package-lock.json') }}
key: ${{ runner.os }}-${{ env.NODE_VERSION }}-node_modules-${{ hashFiles('**/package-lock.json') }}
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo
@ -94,7 +117,18 @@ jobs:
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('./oxide/**/Cargo.lock') }}
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
# Cache the `oxide` Rust build
- name: Cache oxide build
uses: actions/cache@v3
with:
path: |
./oxide/target/
./oxide/crates/node/*.node
./oxide/crates/node/index.js
./oxide/crates/node/index.d.ts
key: ${{ runner.os }}-oxide-${{ hashFiles('./oxide/crates/**/*') }}
- name: Install Node.JS
uses: actions/setup-node@v3
@ -131,7 +165,7 @@ jobs:
${{ env.OXIDE_LOCATION }}/*.node
${{ env.APP_NAME }}
oxide-build-linux:
build-linux:
strategy:
fail-fast: false
matrix:
@ -162,6 +196,35 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Cache node_modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-${{ matrix.target }}-${{ env.NODE_VERSION }}-node_modules-${{ hashFiles('**/package-lock.json') }}
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo
uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
# Cache the `oxide` Rust build
- name: Cache oxide build
uses: actions/cache@v3
with:
path: |
./oxide/target/
./oxide/crates/node/*.node
./oxide/crates/node/index.js
./oxide/crates/node/index.d.ts
key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./oxide/crates/**/*') }}
- name: Install Node.JS
uses: actions/setup-node@v3
with:
@ -181,18 +244,8 @@ jobs:
- name: Setup rust target
run: rustup target add ${{ matrix.target }}
# - name: Use cached oxide node_modules
# id: cache-oxide
# uses: actions/cache@v3
# with:
# path: ${{ env.OXIDE_LOCATION }}/node_modules
# key: nodeModules-${{ hashFiles('${{ env.OXIDE_LOCATION }}/package-lock.json') }}-linux-${{ matrix.target }}-${{ env.NODE_VERSION }}
# restore-keys: |
# nodeModules-
- name: Oxide — npm install
# if: steps.cache-oxide.outputs.cache-hit != 'true'
run: cd ${{ env.OXIDE_LOCATION }} && npm install
- name: Install dependencies
run: npm install
- name: Build release
run: cd ${{ env.OXIDE_LOCATION }} && npm run build
@ -211,28 +264,52 @@ jobs:
${{ env.OXIDE_LOCATION }}/*.node
${{ env.APP_NAME }}
oxide-release:
release:
runs-on: ubuntu-latest
name: Build and release
needs:
- oxide-build
- oxide-build-linux
- oxide-build-apple-silicon
- build
- build-linux
- build-apple-silicon
steps:
- uses: actions/checkout@v3
# - name: Use cached oxide node_modules
# id: cache-oxide
# uses: actions/cache@v3
# with:
# path: ${{ env.OXIDE_LOCATION }}/node_modules
# key: nodeModules-${{ hashFiles('${{ env.OXIDE_LOCATION }}/package-lock.json') }}-ubuntu-latest-${{ env.NODE_VERSION }}
# restore-keys: |
# nodeModules-
- name: Cache node_modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-${{ env.NODE_VERSION }}-node_modules-${{ hashFiles('**/package-lock.json') }}
- name: Oxide — npm install
# if: steps.cache-oxide.outputs.cache-hit != 'true'
run: cd ${{ env.OXIDE_LOCATION }} && npm install
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo
uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
# Cache the `oxide` Rust build
- name: Cache oxide build
uses: actions/cache@v3
with:
path: |
./oxide/target/
./oxide/crates/node/*.node
./oxide/crates/node/index.js
./oxide/crates/node/index.d.ts
key: ${{ runner.os }}-oxide-${{ hashFiles('./oxide/crates/**/*') }}
- name: Install dependencies
run: npm install
- name: Calculate environment variables
run: |
echo "RELEASE_CHANNEL=$(node ./scripts/release-channel.js)" >> $GITHUB_ENV
echo "VERSION=$(node -e 'console.log(require(`./package.json`).version);')" >> $GITHUB_ENV
- name: Download artifacts
uses: actions/download-artifact@v3
@ -255,11 +332,6 @@ jobs:
cp bindings-x86_64-unknown-linux-gnu/oxide/crates/node/* ./npm/linux-x64-gnu/
cp bindings-x86_64-unknown-linux-musl/oxide/crates/node/* ./npm/linux-x64-musl/
- name: Resolve version
id: vars
run: |
echo "NPM_VERSION=0.0.0-insiders.$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Generate entry point
run: npm run build --prefix ${{ env.OXIDE_LOCATION }}
@ -268,50 +340,74 @@ jobs:
for pkg in ${{ env.OXIDE_LOCATION }}/npm/*; do
echo "Publishing $pkg..."
cd $pkg
npm version ${{ env.NPM_VERSION }} --force --no-git-tag-version
npm publish --tag insiders --access public
npm version ${{ env.VERSION }} --force --no-git-tag-version
npm publish --tag ${{ env.RELEASE_CHANNEL }} --access public
cd -
done
echo "Publishing ${{ env.APP_NAME }}...";
cd ${{ env.OXIDE_LOCATION }}
npm version ${{ env.NPM_VERSION }} --force --no-git-tag-version
sed "s#\"0.0.0\"#\"${{ env.NPM_VERSION }}\"#g" package.json > package_updated.json
npm version ${{ env.VERSION }} --force --no-git-tag-version
sed "s#\"0.0.0\"#\"${{ env.VERSION }}\"#g" package.json > package_updated.json
mv package_updated.json package.json
npm publish --tag insiders --access public
npm publish --tag ${{ env.RELEASE_CHANNEL }} --access public
cd -
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
tailwind_oxide_release:
tailwind-release:
runs-on: ubuntu-latest
name: Build and release Tailwind CSS
needs:
- oxide-release
strategy:
matrix:
node-version: [14]
- release
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
- name: Cache node_modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-${{ matrix.target }}-${{ env.NODE_VERSION }}-node_modules-${{ hashFiles('**/package-lock.json') }}
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo
uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
# Cache the `oxide` Rust build
- name: Cache oxide build
uses: actions/cache@v3
with:
path: |
./oxide/target/
./oxide/crates/node/*.node
./oxide/crates/node/index.js
./oxide/crates/node/index.d.ts
key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./oxide/crates/**/*') }}
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org'
- name: Resolve version
id: vars
- name: Calculate environment variables
run: |
echo "SHA_SHORT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "OXIDE_NPM_VERSION=0.0.0-insiders.$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "RELEASE_CHANNEL=$(node ./scripts/release-channel.js)" >> $GITHUB_ENV
echo "VERSION=$(node -e 'console.log(require(`./package.json`).version);')" >> $GITHUB_ENV
- name: Setup `@tailwindcss/oxide` version
run: |
sed "s#\"@tailwindcss/oxide\": \".*\"#\"@tailwindcss/oxide\": \"${{ env.OXIDE_NPM_VERSION }}\"#" package.json > package_updated.json
sed "s#\"@tailwindcss/oxide\": \".*\"#\"@tailwindcss/oxide\": \"${{ env.VERSION }}\"#" package.json > package_updated.json
mv package_updated.json package.json
- name: Install dependencies
@ -321,12 +417,31 @@ jobs:
run: npm run build
- name: Test
run: npm test
run: |
npm test || \
npm test || \
npm test || exit 1
- name: 'Version based on commit: 0.0.0-oxide.${{ env.SHA_SHORT }}'
run: npm version 0.0.0-oxide.${{ env.SHA_SHORT }} --force --no-git-tag-version
- name: 'Version: ${{ env.VERSION }}'
run: npm version ${{ env.VERSION }} --force --no-git-tag-version
- name: Publish
run: npm publish --tag oxide
run: npm publish --tag ${{ env.RELEASE_CHANNEL }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# - name: Trigger Tailwind Play update
# if: env.RELEASE_CHANNEL == 'latest'
# uses: actions/github-script@v6
# with:
# github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }}
# script: |
# await github.rest.actions.createWorkflowDispatch({
# owner: 'tailwindlabs',
# repo: 'play.tailwindcss.com',
# ref: 'master',
# workflow_id: 'upgrade-tailwindcss.yml',
# inputs: {
# version: '${{ env.TAILWINDCSS_VERSION }}'
# }
# })

View file

@ -5,6 +5,9 @@ let fs = require('fs/promises')
let chokidar = require('chokidar')
let resolveToolRoot = require('./resolve-tool-root')
let FILE_STATE = {
NotFound: Symbol(),
}
function getWatcherOptions() {
return {
@ -28,7 +31,7 @@ module.exports = function ({
cleanup = true,
} = {}) {
let toolRoot = resolveToolRoot()
let fileCache = {}
let fileCache = new Map()
let absoluteOutputFolder = path.resolve(toolRoot, output)
let absoluteInputFolder = path.resolve(toolRoot, input)
@ -41,9 +44,9 @@ module.exports = function ({
// Restore all written files
afterEach(async () => {
await Promise.all(
Object.entries(fileCache).map(async ([file, content]) => {
Array.from(fileCache.entries()).map(async ([file, content]) => {
try {
if (content === null) {
if (content === FILE_STATE.NotFound) {
return await fs.unlink(file)
} else {
return await fs.writeFile(file, content, 'utf8')
@ -89,7 +92,7 @@ module.exports = function ({
return {
cleanupFile(file) {
let filePath = path.resolve(toolRoot, file)
fileCache[filePath] = null
fileCache.set(filePath, FILE_STATE.NotFound)
},
async fileExists(file) {
let filePath = path.resolve(toolRoot, file)
@ -98,8 +101,11 @@ module.exports = function ({
async removeFile(file) {
let filePath = path.resolve(toolRoot, file)
if (!fileCache[filePath]) {
fileCache[filePath] = await fs.readFile(filePath, 'utf8').catch(() => null)
if (!fileCache.has(filePath)) {
fileCache.set(
filePath,
await fs.readFile(filePath, 'utf8').catch(() => FILE_STATE.NotFound)
)
}
await fs.unlink(filePath).catch(() => null)
@ -114,8 +120,8 @@ module.exports = function ({
},
async appendToInputFile(file, contents) {
let filePath = path.resolve(absoluteInputFolder, file)
if (!fileCache[filePath]) {
fileCache[filePath] = await fs.readFile(filePath, 'utf8')
if (!fileCache.has(filePath)) {
fileCache.set(filePath, await fs.readFile(filePath, 'utf8'))
}
return fs.appendFile(filePath, contents, 'utf8')
@ -133,12 +139,12 @@ module.exports = function ({
await fs.mkdir(path.dirname(filePath), { recursive: true })
}
if (!fileCache[filePath]) {
if (!fileCache.has(filePath)) {
try {
fileCache[filePath] = await fs.readFile(filePath, 'utf8')
fileCache.set(filePath, await fs.readFile(filePath, 'utf8'))
} catch (err) {
if (err.code === 'ENOENT') {
fileCache[filePath] = null // Sentinel value to `delete` the file afterwards. This also means that we are writing to a `new` file inside the test.
fileCache.set(filePath, FILE_STATE.NotFound)
} else {
throw err
}

View file

@ -1,6 +1,5 @@
let $ = require('../../execute')
let { css, html, javascript } = require('../../syntax')
let { env } = require('../../../lib/lib/sharedState')
let {
appendToInputFile,
@ -40,7 +39,7 @@ describe('static build', () => {
'index.html',
html`
<link rel="stylesheet" href="./index.css" />
<div class="bg-primary"></div>
<div class="z-primary"></div>
`
)
await writeInputFile(
@ -58,8 +57,8 @@ describe('static build', () => {
content: ['./src/index.html'],
theme: {
extend: {
colors: {
primary: 'black',
zIndex: {
primary: 0,
},
},
},
@ -74,26 +73,13 @@ describe('static build', () => {
env: { NODE_ENV: 'production' },
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.bg-primary {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.bg-primary {
background-color: black;
}
`
)
}
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.z-primary {
z-index: 0;
}
`
)
})
it('can use a tailwind.config.ts configuration file', async () => {
@ -102,7 +88,7 @@ describe('static build', () => {
'index.html',
html`
<link rel="stylesheet" href="./index.css" />
<div class="bg-primary"></div>
<div class="z-primary"></div>
`
)
await writeInputFile(
@ -122,8 +108,8 @@ describe('static build', () => {
content: ['./src/index.html'],
theme: {
extend: {
colors: {
primary: 'black',
zIndex: {
primary: 0,
},
},
},
@ -138,26 +124,13 @@ describe('static build', () => {
env: { NODE_ENV: 'production' },
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.bg-primary {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.bg-primary {
background-color: black;
}
`
)
}
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.z-primary {
z-index: 0;
}
`
)
})
})
@ -199,41 +172,22 @@ describe('watcher', () => {
)
await waitForOutputFileChange(/index\.\w+\.css$/, async () => {
await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('index.html', html`<div class="z-0"></div>`)
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -271,41 +225,22 @@ describe('watcher', () => {
)
await waitForOutputFileChange(/index\.\w+\.css$/, async () => {
await appendToInputFile('glob/index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('glob/index.html', html`<div class="z-0"></div>`)
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -438,43 +373,25 @@ describe('watcher', () => {
@layer components {
.btn {
@apply rounded bg-red-500 px-2 py-1;
@apply rounded z-0 px-2 py-1;
}
}
`
)
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.btn {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.btn {
background-color: #ef4444;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.btn {
z-index: 0;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
return runningProcess.stop()
})

View file

@ -1,6 +1,5 @@
let $ = require('../../execute')
let { css, html, javascript } = require('../../syntax')
let { env } = require('../../../lib/lib/sharedState')
let { readOutputFile, appendToInputFile, writeInputFile } = require('../../io')({
output: 'dist',
@ -58,41 +57,22 @@ describe('watcher', () => {
`
)
await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('index.html', html`<div class="z-0"></div>`)
await runningProcess.onStderr(ready)
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -125,41 +105,22 @@ describe('watcher', () => {
`
)
await appendToInputFile('glob/index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('glob/index.html', html`<div class="z-0"></div>`)
await runningProcess.onStderr(ready)
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -277,47 +238,26 @@ describe('watcher', () => {
@layer components {
.btn {
@apply rounded bg-red-500 px-2 py-1;
@apply rounded z-0 px-2 py-1;
}
}
`
)
await runningProcess.onStderr(ready)
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.btn {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
/* prettier-ignore */
.btn {
border-radius: 0.25rem;
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
/* prettier-ignore */
.btn {
border-radius: 0.25rem;
background-color: #ef4444;
z-index: 0;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
.font-bold {
font-weight: 700;
}
`
)
return runningProcess.stop()
})

View file

@ -1,6 +1,5 @@
let $ = require('../../execute')
let { css, html, javascript } = require('../../syntax')
let { env } = require('../../../lib/lib/sharedState')
let { readOutputFile, appendToInputFile, writeInputFile } = require('../../io')({
output: 'dist',
@ -58,41 +57,22 @@ describe('watcher', () => {
`
)
await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('index.html', html`<div class="z-0"></div>`)
await runningProcess.onStderr(ready)
if (env.ENGINE === 'stable') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -125,41 +105,22 @@ describe('watcher', () => {
`
)
await appendToInputFile('glob/index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('glob/index.html', html`<div class="z-0"></div>`)
await runningProcess.onStderr(ready)
if (env.ENGINE === 'stable') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -275,43 +236,25 @@ describe('watcher', () => {
@layer components {
.btn {
@apply rounded bg-red-500 px-2 py-1;
@apply rounded z-0 px-2 py-1;
}
}
`
)
await runningProcess.onStderr(ready)
if (env.ENGINE === 'stable') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.btn {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.btn {
background-color: #ef4444;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.btn {
z-index: 0;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
return runningProcess.stop()
})
@ -370,43 +313,25 @@ describe('watcher', () => {
css`
@layer components {
.btn {
@apply rounded bg-red-500 px-2 py-1;
@apply rounded z-0 px-2 py-1;
}
}
`
)
await runningProcess.onStderr(ready)
if (env.ENGINE === 'stable') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.btn {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.btn {
background-color: #ef4444;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.btn {
z-index: 0;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
return runningProcess.stop()
})

View file

@ -1,6 +1,5 @@
let $ = require('../../execute')
let { css, html, javascript } = require('../../syntax')
let { env } = require('../../../lib/lib/sharedState')
let { readOutputFile, appendToInputFile, writeInputFile, removeFile } = require('../../io')({
output: 'dist',
@ -30,7 +29,7 @@ describe('static build', () => {
it('can use a tailwind.config.js configuration file with ESM syntax', async () => {
await removeFile('tailwind.config.js')
await writeInputFile('index.html', html`<div class="bg-primary"></div>`)
await writeInputFile('index.html', html`<div class="z-primary"></div>`)
await writeInputFile(
'index.css',
css`
@ -46,8 +45,8 @@ describe('static build', () => {
content: ['./src/index.html'],
theme: {
extend: {
colors: {
primary: 'black',
zIndex: {
primary: 0,
},
},
},
@ -62,31 +61,18 @@ describe('static build', () => {
env: { NODE_ENV: 'production' },
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.bg-primary {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.bg-primary {
background-color: black;
}
`
)
}
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.z-primary {
z-index: 0;
}
`
)
})
it('can use a tailwind.config.ts configuration file', async () => {
await removeFile('tailwind.config.js')
await writeInputFile('index.html', html`<div class="bg-primary"></div>`)
await writeInputFile('index.html', html`<div class="z-primary"></div>`)
await writeInputFile(
'index.css',
css`
@ -104,8 +90,8 @@ describe('static build', () => {
content: ['./src/index.html'],
theme: {
extend: {
colors: {
primary: 'black',
zIndex: {
primary: 0,
},
},
},
@ -120,26 +106,13 @@ describe('static build', () => {
env: { NODE_ENV: 'production' },
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.bg-primary {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.bg-primary {
background-color: black;
}
`
)
}
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.z-primary {
z-index: 0;
}
`
)
})
})
@ -172,41 +145,22 @@ describe('watcher', () => {
`
)
await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('index.html', html`<div class="z-0"></div>`)
await runningProcess.onStderr(ready)
if (env.ENGINE === 'stable') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -239,41 +193,22 @@ describe('watcher', () => {
`
)
await appendToInputFile('glob/index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('glob/index.html', html`<div class="z-0"></div>`)
await runningProcess.onStderr(ready)
if (env.ENGINE === 'stable') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -389,46 +324,25 @@ describe('watcher', () => {
@layer components {
.btn {
@apply rounded bg-red-500 px-2 py-1;
@apply rounded z-0 px-2 py-1;
}
}
`
)
await runningProcess.onStderr(ready)
if (env.ENGINE === 'stable') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.btn {
border-radius: 0.25rem;
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.btn {
border-radius: 0.25rem;
background-color: #ef4444;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.btn {
border-radius: 0.25rem;
z-index: 0;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
return runningProcess.stop()
})

View file

@ -2,7 +2,6 @@ let path = require('path')
let $ = require('../../execute')
let { css, html, javascript } = require('../../syntax')
let resolveToolRoot = require('../../resolve-tool-root')
let { env } = require('../../../lib/lib/sharedState')
let version = require('../../../package.json').version
@ -19,7 +18,6 @@ let {
})
let EXECUTABLE = 'node ../../lib/cli.js'
let testStable = env.ENGINE === 'stable' ? test : test.skip
function dedent(input) {
let lines = input.split('\n')
@ -98,7 +96,8 @@ describe('Build command', () => {
expect(withoutMinify.length).toBeGreaterThan(withMinify.length)
})
testStable('--no-autoprefixer', async () => {
// TODO: Handle --no-autoprefixer
test.skip('--no-autoprefixer', async () => {
await writeInputFile('index.html', html`<div class="select-none"></div>`)
await $(`${EXECUTABLE} --output ./dist/main.css`)
@ -184,7 +183,7 @@ describe('Build command', () => {
)
})
testStable('--postcss (postcss.config.js)', async () => {
test('--postcss (postcss.config.js)', async () => {
await writeInputFile('index.html', html`<div class="font-bold"></div>`)
let customConfig = javascript`
@ -196,7 +195,7 @@ describe('Build command', () => {
function before(root, result) {
// Inject a custom component with @apply rules to prove that we run
// this _before_ the actual tailwind plugin.
let btn = postcss.parse('.btn { @apply bg-red-500 px-2 py-1 }')
let btn = postcss.parse('.btn { @apply flex px-2 py-1 }')
root.append(btn.nodes)
},
function tailwindcss() {
@ -217,45 +216,24 @@ describe('Build command', () => {
await $(`${EXECUTABLE} --output ./dist/main.css --postcss`)
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.font-bold-after {
font-weight: 700;
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.font-bold-after {
font-weight: 700;
}
.btn-after {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.font-bold-after {
font-weight: 700;
}
.btn-after {
background-color: #ef4444;
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
`
)
}
.btn-after {
display: flex;
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
`
)
})
testStable('--postcss (custom.postcss.config.js)', async () => {
test('--postcss (custom.postcss.config.js)', async () => {
await writeInputFile('index.html', html`<div class="font-bold"></div>`)
let customConfig = javascript`
@ -267,7 +245,7 @@ describe('Build command', () => {
function before(root, result) {
// Inject a custom component with @apply rules to prove that we run
// this _before_ the actual tailwind plugin.
let btn = postcss.parse('.btn { @apply bg-red-500 px-2 py-1 }')
let btn = postcss.parse('.btn { @apply flex px-2 py-1 }')
root.append(btn.nodes)
},
function tailwindcss() {
@ -288,45 +266,24 @@ describe('Build command', () => {
await $(`${EXECUTABLE} --output ./dist/main.css --postcss ./custom.postcss.config.js`)
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.font-bold-after {
font-weight: 700;
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.font-bold-after {
font-weight: 700;
}
.btn-after {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.font-bold-after {
font-weight: 700;
}
.btn-after {
background-color: #ef4444;
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
`
)
}
.btn-after {
display: flex;
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
`
)
})
testStable('--postcss supports process options', async () => {
test('--postcss supports process options', async () => {
await writeInputFile('index.html', html`<div class="font-bold"></div>`)
let customConfig = javascript`
@ -360,7 +317,7 @@ describe('Build command', () => {
expect(contents).toContain(`/*# sourceMappingURL`)
})
testStable('--postcss supports process options with custom config', async () => {
test('--postcss supports process options with custom config', async () => {
await writeInputFile('index.html', html`<div class="font-bold"></div>`)
let customConfig = javascript`
@ -481,10 +438,18 @@ describe('Build command', () => {
return runningProcess.stop()
})
testStable('postcss-import is included when using a custom postcss configuration', async () => {
test('postcss-import is included when using a custom postcss configuration', async () => {
cleanupFile('src/test.css')
await writeInputFile('index.html', html`<div class="md:something-cool"></div>`)
await writeInputFile(
'imported.css',
css`
.foo {
color: white;
}
`
)
await writeInputFile(
'test.css',
css`
@ -501,7 +466,9 @@ describe('Build command', () => {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
@import './imported.css';
.foo {
color: #fff;
}
`
)
})
@ -509,28 +476,8 @@ describe('Build command', () => {
test('--help', async () => {
let { combined } = await $(`${EXECUTABLE} --help`)
if (env.ENGINE === 'oxide') {
expect(dedent(combined)).toEqual(
dedent(`
tailwindcss v${version}
Usage:
tailwindcss build [options]
Options:
-i, --input Input file
-o, --output Output file
-w, --watch Watch for changes and rebuild as needed
-p, --poll Use polling instead of filesystem events when watching
--content Content paths to use for removing unused classes
-m, --minify Minify the output
-c, --config Path to a custom config file
-h, --help Display usage information
`)
)
} else if (env.ENGINE === 'stable') {
expect(dedent(combined)).toEqual(
dedent(`
expect(dedent(combined)).toEqual(
dedent(`
tailwindcss v${version}
Usage:
@ -548,8 +495,7 @@ describe('Build command', () => {
--no-autoprefixer Disable autoprefixer
-h, --help Display usage information
`)
)
}
)
})
})
@ -562,7 +508,6 @@ describe('Init command', () => {
{ flags: ['--ts', '--full'], name: 'tailwind.config.ts' },
{ flags: ['--esm', '--full'], name: 'tailwind.config.js' },
])('works with all these flags: %j', async ({ flags, name }) => {
cleanupFile(name)
await removeFile(name)
let { combined } = await $(`${EXECUTABLE} init ${flags.join(' ')}`)
@ -611,7 +556,7 @@ describe('Init command', () => {
expect((await readOutputFile('../full.config.js')).split('\n').length).toBeGreaterThan(50)
})
testStable('--postcss', async () => {
test('--postcss', async () => {
expect(await fileExists('postcss.config.js')).toBe(true)
await removeFile('postcss.config.js')
expect(await fileExists('postcss.config.js')).toBe(false)
@ -631,24 +576,8 @@ describe('Init command', () => {
test('--help', async () => {
let { combined } = await $(`${EXECUTABLE} init --help`)
if (env.ENGINE === 'oxide') {
expect(dedent(combined)).toEqual(
dedent(`
tailwindcss v${version}
Usage:
tailwindcss init [options]
Options:
--esm Initialize configuration file as ESM
--ts Initialize configuration file as TypeScript
-f, --full Include the default values for all options in the generated configuration file
-h, --help Display usage information
`)
)
} else if (env.ENGINE === 'stable') {
expect(dedent(combined)).toEqual(
dedent(`
expect(dedent(combined)).toEqual(
dedent(`
tailwindcss v${version}
Usage:
@ -661,12 +590,10 @@ describe('Init command', () => {
-f, --full Include the default values for all options in the generated configuration file
-h, --help Display usage information
`)
)
}
)
})
test('ESM config is created by default in an ESM project', async () => {
cleanupFile('tailwind.config.js')
await removeFile('tailwind.config.js')
let pkg = await readOutputFile('../package.json')
@ -696,7 +623,6 @@ describe('Init command', () => {
})
test('CJS config is created by default in a non-ESM project', async () => {
cleanupFile('tailwind.config.js')
await removeFile('tailwind.config.js')
let pkg = await readOutputFile('../package.json')

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,6 @@ require('isomorphic-fetch')
let $ = require('../../execute')
let { css, html, javascript } = require('../../syntax')
let { env } = require('../../../lib/lib/sharedState')
let { readOutputFile, appendToInputFile, writeInputFile, removeFile } = require('../../io')({
output: 'dist',
@ -48,7 +47,7 @@ describe('static build', () => {
'index.html',
html`
<link rel="stylesheet" href="./index.css" />
<div class="bg-primary"></div>
<div class="z-primary"></div>
`
)
await removeFile('tailwind.config.js')
@ -59,8 +58,8 @@ describe('static build', () => {
content: ['index.html'],
theme: {
extend: {
colors: {
primary: 'black',
zIndex: {
primary: 0,
},
},
},
@ -75,26 +74,13 @@ describe('static build', () => {
env: { NODE_ENV: 'production', NO_COLOR: '1' },
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile(/index.\w+\.css$/)).toIncludeCss(
css`
.bg-primary {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile(/index.\w+\.css$/)).toIncludeCss(
css`
.bg-primary {
background-color: black;
}
`
)
}
expect(await readOutputFile(/index.\w+\.css$/)).toIncludeCss(
css`
.z-primary {
z-index: 0;
}
`
)
})
it('can use a tailwind.config.ts configuration file', async () => {
@ -102,7 +88,7 @@ describe('static build', () => {
'index.html',
html`
<link rel="stylesheet" href="./index.css" />
<div class="bg-primary"></div>
<div class="z-primary"></div>
`
)
await removeFile('tailwind.config.js')
@ -115,8 +101,8 @@ describe('static build', () => {
content: ['index.html'],
theme: {
extend: {
colors: {
primary: 'black',
zIndex: {
primary: 0,
},
},
},
@ -131,26 +117,13 @@ describe('static build', () => {
env: { NODE_ENV: 'production', NO_COLOR: '1' },
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile(/index.\w+\.css$/)).toIncludeCss(
css`
.bg-primary {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile(/index.\w+\.css$/)).toIncludeCss(
css`
.bg-primary {
background-color: black;
}
`
)
}
expect(await readOutputFile(/index.\w+\.css$/)).toIncludeCss(
css`
.z-primary {
z-index: 0;
}
`
)
})
})
@ -191,41 +164,22 @@ describe('watcher', () => {
`
)
await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('index.html', html`<div class="z-0"></div>`)
await runningProcess.onStdout((message) => message.includes('page reload'))
if (env.ENGINE === 'stable') {
expect(await fetchCSS()).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await fetchCSS()).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await fetchCSS()).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -262,41 +216,22 @@ describe('watcher', () => {
`
)
await appendToInputFile('glob/index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('glob/index.html', html`<div class="z-0"></div>`)
await runningProcess.onStdout((message) => message.includes('page reload'))
if (env.ENGINE === 'stable') {
expect(await fetchCSS()).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await fetchCSS()).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await fetchCSS()).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -428,49 +363,28 @@ describe('watcher', () => {
@layer components {
.btn {
@apply rounded bg-red-500 px-2 py-1;
@apply rounded z-0 px-2 py-1;
}
}
`
)
await runningProcess.onStdout((message) => message.includes('hmr update /index.css'))
if (env.ENGINE === 'stable') {
expect(await fetchCSS()).toIncludeCss(
css`
.btn {
border-radius: 0.25rem;
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await fetchCSS()).toIncludeCss(
css`
.btn {
border-radius: 0.25rem;
background-color: #ef4444;
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
expect(await fetchCSS()).toIncludeCss(
css`
.btn {
border-radius: 0.25rem;
z-index: 0;
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.font-bold {
font-weight: 700;
}
`
)
return runningProcess.stop()
})

View file

@ -1,6 +1,5 @@
let $ = require('../../execute')
let { css, html, javascript } = require('../../syntax')
let { env } = require('../../../lib/lib/sharedState')
let {
appendToInputFile,
@ -28,7 +27,7 @@ describe('static build', () => {
it('can use a tailwind.config.js configuration file with ESM syntax', async () => {
await removeFile('tailwind.config.js')
await writeInputFile('index.html', html`<div class="bg-primary"></div>`)
await writeInputFile('index.html', html`<div class="z-primary"></div>`)
await writeInputFile(
'index.css',
css`
@ -44,8 +43,8 @@ describe('static build', () => {
content: ['./src/index.html'],
theme: {
extend: {
colors: {
primary: 'black',
zIndex: {
primary: 0,
},
},
},
@ -58,31 +57,18 @@ describe('static build', () => {
await $('webpack --mode=production')
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-primary {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-primary {
background-color: black;
}
`
)
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.z-primary {
z-index: 0;
}
`
)
})
it('can use a tailwind.config.ts configuration file', async () => {
await removeFile('tailwind.config.js')
await writeInputFile('index.html', html`<div class="bg-primary"></div>`)
await writeInputFile('index.html', html`<div class="z-primary"></div>`)
await writeInputFile(
'index.css',
css`
@ -100,8 +86,8 @@ describe('static build', () => {
content: ['./src/index.html'],
theme: {
extend: {
colors: {
primary: 'black',
zIndex: {
primary: 0,
},
},
},
@ -114,26 +100,13 @@ describe('static build', () => {
await $('webpack --mode=production')
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-primary {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-primary {
background-color: black;
}
`
)
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.z-primary {
z-index: 0;
}
`
)
})
})
@ -169,41 +142,22 @@ describe('watcher', () => {
)
await waitForOutputFileChange('main.css', async () => {
await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('index.html', html`<div class="z-0"></div>`)
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -239,41 +193,22 @@ describe('watcher', () => {
)
await waitForOutputFileChange('main.css', async () => {
await appendToInputFile('glob/index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('glob/index.html', html`<div class="z-0"></div>`)
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -394,46 +329,25 @@ describe('watcher', () => {
@layer components {
.btn {
@apply rounded bg-red-500 px-2 py-1;
@apply rounded z-0 px-2 py-1;
}
}
`
)
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.btn {
border-radius: 0.25rem;
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.btn {
background-color: #ef4444;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.btn {
z-index: 0;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
return runningProcess.stop()
})

View file

@ -1,6 +1,5 @@
let $ = require('../../execute')
let { css, html, javascript } = require('../../syntax')
let { env } = require('../../../lib/lib/sharedState')
let {
appendToInputFile,
@ -28,7 +27,7 @@ describe('static build', () => {
it('can use a tailwind.config.js configuration file with ESM syntax', async () => {
await removeFile('tailwind.config.js')
await writeInputFile('index.html', html`<div class="bg-primary"></div>`)
await writeInputFile('index.html', html`<div class="z-primary"></div>`)
await writeInputFile(
'index.css',
css`
@ -44,8 +43,8 @@ describe('static build', () => {
content: ['./src/index.html'],
theme: {
extend: {
colors: {
primary: 'black',
zIndex: {
primary: 0,
},
},
},
@ -58,31 +57,18 @@ describe('static build', () => {
await $('webpack --mode=production')
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-primary {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-primary {
background-color: black;
}
`
)
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.z-primary {
z-index: 0;
}
`
)
})
it('can use a tailwind.config.ts configuration file', async () => {
await removeFile('tailwind.config.js')
await writeInputFile('index.html', html`<div class="bg-primary"></div>`)
await writeInputFile('index.html', html`<div class="z-primary"></div>`)
await writeInputFile(
'index.css',
css`
@ -100,8 +86,8 @@ describe('static build', () => {
content: ['./src/index.html'],
theme: {
extend: {
colors: {
primary: 'black',
zIndex: {
primary: 0,
},
},
},
@ -114,26 +100,13 @@ describe('static build', () => {
await $('webpack --mode=production')
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-primary {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-primary {
background-color: black;
}
`
)
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.z-primary {
z-index: 0;
}
`
)
})
})
@ -169,41 +142,22 @@ describe('watcher', () => {
)
await waitForOutputFileChange('main.css', async () => {
await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('index.html', html`<div class="z-0"></div>`)
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -239,41 +193,22 @@ describe('watcher', () => {
)
await waitForOutputFileChange('main.css', async () => {
await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('index.html', html`<div class="z-0"></div>`)
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -394,46 +329,25 @@ describe('watcher', () => {
@layer components {
.btn {
@apply rounded bg-red-500 px-2 py-1;
@apply rounded z-0 px-2 py-1;
}
}
`
)
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.btn {
border-radius: 0.25rem;
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.btn {
background-color: #ef4444;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.btn {
z-index: 0;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
return runningProcess.stop()
})
@ -447,7 +361,7 @@ describe('watcher', () => {
content: {
files: ['./src/index.html'],
},
safelist: ['bg-red-500','bg-red-600'],
safelist: ['z-0', 'z-10'],
theme: {
extend: {
},
@ -464,43 +378,21 @@ describe('watcher', () => {
await waitForOutputFileCreation('main.css')
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.bg-red-600 {
--tw-bg-opacity: 1;
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
}
.z-10 {
z-index: 10;
}
.font-bold {
font-weight: 700;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.bg-red-600 {
background-color: #dc2626;
}
.font-bold {
font-weight: 700;
}
`
)
}
.font-bold {
font-weight: 700;
}
`
)
return runningProcess.stop()
})

272
package-lock.json generated

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -16,12 +16,9 @@
"integrations/*",
"oxide/crates/node"
],
"tailwindcss": {
"engine": "oxide"
},
"scripts": {
"dev": "concurrently -n tailwind,oxide-node -c green,yellow 'npm run dev:js' 'npm run dev:rust'",
"build": "npm run build:rust && npm run build:js && npm run build:peers",
"build": "npm run build:rust && npm run build:js",
"test": "jest",
"test:integrations": "npm run test -w ./integrations",
"style": "eslint .",
@ -29,8 +26,7 @@
"dev:rust": "npm run --prefix ./oxide dev:node",
"dev:js": "npm run build:js -- --watch",
"build:rust": "npm run --prefix ./oxide build:node",
"build:js": "npm run generate:plugin-list && swc src --out-dir lib --copy-files --delete-dir-on-start --config jsc.transform.optimizer.globals.vars.__OXIDE__='\"true\"'",
"build:peers": "esbuild lib/cli-peer-dependencies.js --bundle --platform=node --outfile=peers/index.js --define:process.env.CSS_TRANSFORMER_WASM=false",
"build:js": "npm run generate:plugin-list && swc src --out-dir lib --copy-files --delete-dir-on-start",
"generate:plugin-list": "node -r @swc/register scripts/create-plugin-list.js",
"generate:types": "node -r @swc/register scripts/generate-types.js"
},
@ -38,7 +34,6 @@
"src/*",
"cli/*",
"lib/*",
"peers/*",
"scripts/*.js",
"stubs/*",
"nesting/*",
@ -54,8 +49,6 @@
"@swc/register": "^0.1.10",
"autoprefixer": "^10.4.14",
"concurrently": "^8.0.1",
"cssnano": "^6.0.1",
"esbuild": "^0.17.18",
"eslint": "^8.39.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",

View file

@ -1,126 +0,0 @@
{
"name": "tailwindcss",
"version": "3.3.2",
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
"license": "MIT",
"main": "lib/index.js",
"types": "types/index.d.ts",
"repository": "https://github.com/tailwindlabs/tailwindcss.git",
"bugs": "https://github.com/tailwindlabs/tailwindcss/issues",
"homepage": "https://tailwindcss.com",
"bin": {
"tailwind": "lib/cli.js",
"tailwindcss": "lib/cli.js"
},
"tailwindcss": {
"engine": "stable"
},
"scripts": {
"prebuild": "npm run generate && rimraf lib",
"build": "swc src --out-dir lib --copy-files --config jsc.transform.optimizer.globals.vars.__OXIDE__='\"false\"'",
"postbuild": "esbuild lib/cli-peer-dependencies.js --bundle --platform=node --outfile=peers/index.js --define:process.env.CSS_TRANSFORMER_WASM=false",
"rebuild-fixtures": "npm run build && node -r @swc/register scripts/rebuildFixtures.js",
"style": "eslint .",
"pretest": "npm run generate",
"test": "jest",
"test:integrations": "npm run test --prefix ./integrations",
"install:integrations": "node scripts/install-integrations.js",
"generate:plugin-list": "node -r @swc/register scripts/create-plugin-list.js",
"generate:types": "node -r @swc/register scripts/generate-types.js",
"generate": "npm run generate:plugin-list && npm run generate:types",
"release-channel": "node ./scripts/release-channel.js",
"release-notes": "node ./scripts/release-notes.js",
"prepublishOnly": "npm install --force && npm run build"
},
"files": [
"src/*",
"cli/*",
"lib/*",
"peers/*",
"scripts/*.js",
"stubs/*",
"nesting/*",
"types/**/*",
"*.d.ts",
"*.js",
"base.css",
"components.css",
"screens.css",
"tailwind.css",
"utilities.css",
"variants.css"
],
"devDependencies": {
"@swc/cli": "^0.1.62",
"@swc/core": "^1.3.55",
"@swc/jest": "^0.2.26",
"@swc/register": "^0.1.10",
"autoprefixer": "^10.4.14",
"browserslist": "^4.21.5",
"concurrently": "^8.0.1",
"cssnano": "^6.0.0",
"esbuild": "^0.17.18",
"eslint": "^8.39.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"jest": "^29.5.0",
"jest-diff": "^29.5.0",
"lightningcss": "1.18.0",
"prettier": "^2.8.8",
"rimraf": "^5.0.0",
"source-map-js": "^1.0.2",
"turbo": "^1.9.3"
},
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
"chokidar": "^3.5.3",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
"fast-glob": "^3.2.12",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.18.2",
"lilconfig": "^2.1.0",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
"object-hash": "^3.0.0",
"picocolors": "^1.0.0",
"postcss": "^8.4.23",
"postcss-import": "^15.1.0",
"postcss-js": "^4.0.1",
"postcss-load-config": "^4.0.1",
"postcss-nested": "^6.0.1",
"postcss-selector-parser": "^6.0.11",
"resolve": "^1.22.2",
"sucrase": "^3.32.0"
},
"browserslist": [
"> 1%",
"not edge <= 18",
"not ie 11",
"not op_mini all"
],
"jest": {
"testTimeout": 30000,
"setupFilesAfterEnv": [
"<rootDir>/jest/customMatchers.js"
],
"testPathIgnorePatterns": [
"/node_modules/",
"/integrations/",
"/standalone-cli/",
"\\.test\\.skip\\.js$"
],
"transformIgnorePatterns": [
"node_modules/(?!lightningcss)"
],
"transform": {
"\\.js$": "@swc/jest",
"\\.ts$": "@swc/jest"
}
},
"engines": {
"node": ">=14.0.0"
}
}

View file

@ -1,40 +0,0 @@
let fs = require('fs')
let path = require('path')
let engines = {
stable: {
files: [
path.resolve(__dirname, '..', 'package.stable.json'),
path.resolve(__dirname, '..', 'package-lock.stable.json'),
],
},
oxide: {
files: [
path.resolve(__dirname, '..', 'package.oxide.json'),
path.resolve(__dirname, '..', 'package-lock.oxide.json'),
],
},
}
// Find out what the current engine is that we are using:
let [otherEngine, info] = Object.entries(engines).find(([, info]) =>
info.files.every((file) => fs.existsSync(file))
)
let currentEngine = otherEngine === 'oxide' ? 'stable' : 'oxide'
console.log(`Current engine: \`${currentEngine}\`, swapping to \`${otherEngine}\``)
// Swap the engines
for (let file of info.files) {
fs.renameSync(
file.replace(`.${otherEngine}`, ''),
file.replace(`.${otherEngine}`, `.${currentEngine}`)
)
}
for (let file of engines[otherEngine].files) {
fs.renameSync(file, file.replace(`.${otherEngine}`, ''))
}
console.log(
'Engines have been swapped. Make sure to run `npm install` to update your dependencies.'
)

View file

@ -1,15 +0,0 @@
export function lazyPostcss() {
return require('postcss')
}
export function lazyPostcssImport() {
return require('postcss-import')
}
export function lazyAutoprefixer() {
return require('autoprefixer')
}
export function lazyCssnano() {
return require('cssnano')
}

View file

@ -1,7 +1,3 @@
#!/usr/bin/env node
if (__OXIDE__) {
module.exports = require('./oxide/cli')
} else {
module.exports = require('./cli/index')
}
module.exports = require('./cli/index')

View file

@ -1,56 +0,0 @@
// @ts-check
import {
// @ts-ignore
lazyPostcss,
// @ts-ignore
lazyPostcssImport,
// @ts-ignore
lazyCssnano,
// @ts-ignore
lazyAutoprefixer,
} from '../../../peers/index.js'
/**
* @returns {import('postcss')}
*/
export function loadPostcss() {
// Try to load a local `postcss` version first
try {
return require('postcss')
} catch {}
return lazyPostcss()
}
export function loadPostcssImport() {
// Try to load a local `postcss-import` version first
try {
return require('postcss-import')
} catch {}
return lazyPostcssImport()
}
export function loadCssNano() {
let options = { preset: ['default', { cssDeclarationSorter: false }] }
// Try to load a local `cssnano` version first
try {
return require('cssnano')
} catch {}
return lazyCssnano()(options)
}
export function loadAutoprefixer() {
// Try to load a local `autoprefixer` version first
try {
return require('autoprefixer')
} catch {}
return lazyAutoprefixer()
}

View file

@ -1,14 +1,17 @@
// @ts-check
import pkg from '../../../package.json'
import path from 'path'
import fs from 'fs'
import postcss from 'postcss'
import postcssrc from 'postcss-load-config'
import browserslist from 'browserslist'
import lightning from 'lightningcss'
import { lilconfig } from 'lilconfig'
import loadPlugins from 'postcss-load-config/src/plugins' // Little bit scary, looking at private/internal API
import loadOptions from 'postcss-load-config/src/options' // Little bit scary, looking at private/internal API
import tailwind from '../../processTailwindFeatures'
import { loadAutoprefixer, loadCssNano, loadPostcss, loadPostcssImport } from './deps'
import { formatNodes, drainStdin, outputFile } from './utils'
import { env } from '../../lib/sharedState'
import resolveConfig from '../../../resolveConfig.js'
@ -20,6 +23,41 @@ import log from '../../util/log'
import { loadConfig } from '../../lib/load-config'
import getModuleDependencies from '../../lib/getModuleDependencies'
import { validateConfig } from '../../util/validateConfig'
import { handleImportAtRules } from '../../lib/handleImportAtRules'
import { flagEnabled } from '../../featureFlags'
async function lightningcss(shouldMinify, result, options = {}) {
// TODO: handle --no-autoprefixer option if possible
try {
let transformed = lightning.transform({
filename: result.opts.from || 'input.css',
code: Buffer.from(result.css, 'utf-8'),
minify: shouldMinify,
sourceMap: result.map === undefined ? options.map : !!result.map,
inputSourceMap: result.map ? result.map.toString() : undefined,
targets: lightning.browserslistToTargets(browserslist(pkg.browserslist)),
drafts: {
nesting: true,
},
})
return Object.assign(result, {
css: transformed.code.toString('utf8'),
map: result.map
? Object.assign(result.map, {
toString() {
return transformed.map.toString()
},
})
: result.map,
})
} catch (err) {
console.error('Unable to use Lightning CSS. Using raw version instead.')
console.error(err)
return result
}
}
/**
*
@ -76,39 +114,6 @@ async function loadPostCssPlugins(customPostCssPath) {
return [beforePlugins, afterPlugins, config.options]
}
function loadBuiltinPostcssPlugins() {
let postcss = loadPostcss()
let IMPORT_COMMENT = '__TAILWIND_RESTORE_IMPORT__: '
return [
[
(root) => {
root.walkAtRules('import', (rule) => {
if (rule.params.slice(1).startsWith('tailwindcss/')) {
rule.after(postcss.comment({ text: IMPORT_COMMENT + rule.params }))
rule.remove()
}
})
},
loadPostcssImport(),
(root) => {
root.walkComments((rule) => {
if (rule.text.startsWith(IMPORT_COMMENT)) {
rule.after(
postcss.atRule({
name: 'import',
params: rule.text.replace(IMPORT_COMMENT, ''),
})
)
rule.remove()
}
})
},
],
[],
{},
]
}
let state = {
/** @type {any} */
context: null,
@ -119,7 +124,7 @@ let state = {
/** @type {{content: string, extension: string}[]} */
changedContent: [],
/** @type {ReturnType<typeof load> | null} */
/** @type {{config: import('../../../types').Config, dependencies: Set<string>, dispose: Function } | null} */
configBag: null,
contextDependencies: new Set(),
@ -161,14 +166,7 @@ let state = {
},
}
// @ts-ignore
if (__OXIDE__) {
this.configBag.config = validateConfig(resolveConfig(this.configBag.config))
} else {
this.configBag.config = validateConfig(
resolveConfig(this.configBag.config, { content: { files: [] } })
)
}
this.configBag.config = validateConfig(resolveConfig(this.configBag.config))
// Override content files if `--content` has been passed explicitly
if (content?.length > 0) {
@ -192,7 +190,7 @@ let state = {
let files = fastGlob.sync(this.contentPatterns.all)
for (let file of files) {
if (__OXIDE__) {
if (flagEnabled(this.config, 'oxideParser')) {
content.push({
file,
extension: path.extname(file).slice(1),
@ -258,8 +256,6 @@ let state = {
}
export async function createProcessor(args, cliConfigPath) {
let postcss = loadPostcss()
let input = args['--input']
let output = args['--output']
let includePostCss = args['--postcss']
@ -267,7 +263,9 @@ export async function createProcessor(args, cliConfigPath) {
let [beforePlugins, afterPlugins, postcssOptions] = includePostCss
? await loadPostCssPlugins(customPostCssPath)
: loadBuiltinPostcssPlugins()
: [[], [], {}]
beforePlugins.unshift(...handleImportAtRules())
if (args['--purge']) {
log.warn('purge-flag-deprecated', [
@ -313,8 +311,6 @@ export async function createProcessor(args, cliConfigPath) {
tailwindPlugin,
!args['--minify'] && formatNodes,
...afterPlugins,
!args['--no-autoprefixer'] && loadAutoprefixer(),
args['--minify'] && loadCssNano(),
].filter(Boolean)
/** @type {import('postcss').Processor} */
@ -338,9 +334,15 @@ export async function createProcessor(args, cliConfigPath) {
async function build() {
let start = process.hrtime.bigint()
let options = {
...postcssOptions,
from: input,
to: output,
}
return readInput()
.then((css) => processor.process(css, { ...postcssOptions, from: input, to: output }))
.then((css) => processor.process(css, options))
.then((result) => lightningcss(!!args['--minify'], result, options))
.then((result) => {
if (!state.watcher) {
return result

View file

@ -38,11 +38,6 @@ export function init(args) {
'utf8'
)
// Drop `content` in the oxide engine to promote auto content
if (__OXIDE__) {
stubContentsFile = stubContentsFile.replace(/ *content: \[\],\n/, '')
}
// Change colors import
stubContentsFile = stubContentsFile.replace('../colors', 'tailwindcss/colors')

View file

@ -1255,13 +1255,13 @@ export let corePlugins = {
],
]),
space: ({ matchUtilities, addUtilities, theme }) => {
space: ({ matchUtilities, addUtilities, theme, config }) => {
matchUtilities(
{
'space-x': (value) => {
value = value === '0' ? '0px' : value
if (__OXIDE__) {
if (flagEnabled(config(), 'logicalSiblingUtilities')) {
return {
'& > :not([hidden]) ~ :not([hidden])': {
'--tw-space-x-reverse': '0',
@ -1300,13 +1300,13 @@ export let corePlugins = {
})
},
divideWidth: ({ matchUtilities, addUtilities, theme }) => {
divideWidth: ({ matchUtilities, addUtilities, theme, config }) => {
matchUtilities(
{
'divide-x': (value) => {
value = value === '0' ? '0px' : value
if (__OXIDE__) {
if (flagEnabled(config(), 'logicalSiblingUtilities')) {
return {
'& > :not([hidden]) ~ :not([hidden])': {
'@defaults border-width': {},

View file

@ -3,13 +3,10 @@ import log from './util/log'
let defaults = {
optimizeUniversalDefaults: false,
generalizedModifiers: true,
get disableColorOpacityUtilitiesByDefault() {
return __OXIDE__
},
get relativeContentPathsByDefault() {
return __OXIDE__
},
disableColorOpacityUtilitiesByDefault: false,
relativeContentPathsByDefault: false,
oxideParser: false,
logicalSiblingUtilities: false,
}
let featureFlags = {
@ -18,10 +15,11 @@ let featureFlags = {
'respectDefaultRingColorOpacity',
'disableColorOpacityUtilitiesByDefault',
'relativeContentPathsByDefault',
'logicalSiblingUtilities',
],
experimental: [
'optimizeUniversalDefaults',
'generalizedModifiers',
'oxideParser',
// 'variantGrouping',
],
}

View file

@ -7,6 +7,7 @@ import fastGlob from 'fast-glob'
import normalizePath from 'normalize-path'
import { parseGlob } from '../util/parseGlob'
import { env } from './sharedState'
import { resolveContentPaths } from '@tailwindcss/oxide'
/** @typedef {import('../../types/config.js').RawFile} RawFile */
/** @typedef {import('../../types/config.js').FilePath} FilePath */
@ -17,17 +18,24 @@ import { env } from './sharedState'
* @returns {ContentPath[]}
*/
function resolveContentFiles(tailwindConfig, { skip = [] } = {}) {
if (tailwindConfig.content.files === 'auto' && __OXIDE__) {
env.DEBUG && console.time('Calculating resolve content paths')
tailwindConfig.content.files = require('@tailwindcss/oxide').resolveContentPaths({
base: process.cwd(),
})
if (skip.length > 0) {
tailwindConfig.content.files = tailwindConfig.content.files.filter(
(filePath) => !skip.includes(filePath)
)
if (
Array.isArray(tailwindConfig.content.files) &&
tailwindConfig.content.files.includes('auto')
) {
let idx = tailwindConfig.content.files.indexOf('auto')
if (idx !== -1) {
env.DEBUG && console.time('Calculating resolve content paths')
let resolved = resolveContentPaths({ base: process.cwd() })
env.DEBUG && console.timeEnd('Calculating resolve content paths')
tailwindConfig.content.files.splice(idx, 1, ...resolved)
}
env.DEBUG && console.timeEnd('Calculating resolve content paths')
}
if (skip.length > 0) {
tailwindConfig.content.files = tailwindConfig.content.files.filter(
(filePath) => !skip.includes(filePath)
)
}
return tailwindConfig.content.files
@ -191,12 +199,9 @@ function resolvePathSymlinks(contentPath) {
* @returns {[{ content: string, extension: string }[], Map<string, number>]}
*/
export function resolvedChangedContent(context, candidateFiles, fileModifiedMap) {
let changedContent =
context.tailwindConfig.content.files === 'auto' && __OXIDE__
? []
: context.tailwindConfig.content.files
.filter((item) => typeof item.raw === 'string')
.map(({ raw, extension = 'html' }) => ({ content: raw, extension }))
let changedContent = context.tailwindConfig.content.files
.filter((item) => typeof item.raw === 'string')
.map(({ raw, extension = 'html' }) => ({ content: raw, extension }))
let [changedFiles, mTimesToCommit] = resolveChangedFiles(candidateFiles, fileModifiedMap)

View file

@ -1,10 +1,12 @@
import fs from 'fs'
import LRU from '@alloc/quick-lru'
import { parseCandidateStringsFromFiles } from '@tailwindcss/oxide'
import * as sharedState from './sharedState'
import { generateRules } from './generateRules'
import log from '../util/log'
import cloneNodes from '../util/cloneNodes'
import { defaultExtractor } from './defaultExtractor'
import { flagEnabled } from '../featureFlags'
let env = sharedState.env
@ -130,9 +132,9 @@ export default function expandTailwindAtRules(context) {
env.DEBUG && console.time('Reading changed files')
if (__OXIDE__) {
if (flagEnabled(context.tailwindConfig, 'oxideParser')) {
// TODO: Pass through or implement `extractor`
for (let candidate of require('@tailwindcss/oxide').parseCandidateStringsFromFiles(
for (let candidate of parseCandidateStringsFromFiles(
context.changedContent
// Object.assign({}, builtInTransformers, context.tailwindConfig.content.transform)
)) {
@ -162,7 +164,7 @@ export default function expandTailwindAtRules(context) {
env.DEBUG && console.time('Generate rules')
env.DEBUG && console.time('Sorting candidates')
let sortedCandidates = __OXIDE__
let sortedCandidates = flagEnabled(context.tailwindConfig, 'oxideParser')
? candidates
: new Set(
[...candidates].sort((a, z) => {

View file

@ -175,10 +175,6 @@ function applyVariant(variant, matches, context) {
if (modifiers.length && !context.variantMap.has(variant)) {
variant = baseVariant
args.modifier = modifiers[0]
if (!flagEnabled(context.tailwindConfig, 'generalizedModifiers')) {
return []
}
}
}

View file

@ -1,8 +1,9 @@
import postcss from 'postcss'
import postcssImport from 'postcss-import'
const TAILWIND = Symbol()
export function handleImportAtRules(postcssImport = require('postcss-import')) {
export function handleImportAtRules() {
let RESTORE_ATRULE_COMMENT = '__TAILWIND_RESTORE__'
let atRulesToRestore = ['tailwind', 'config']

View file

@ -10,35 +10,6 @@ export default function normalizeTailwindDirectives(root) {
applyDirectives.add(atRule)
}
if (!__OXIDE__) {
if (atRule.name === 'import') {
if (atRule.params === '"tailwindcss/base"' || atRule.params === "'tailwindcss/base'") {
atRule.name = 'tailwind'
atRule.params = 'base'
} else if (
atRule.params === '"tailwindcss/components"' ||
atRule.params === "'tailwindcss/components'"
) {
atRule.name = 'tailwind'
atRule.params = 'components'
} else if (
atRule.params === '"tailwindcss/utilities"' ||
atRule.params === "'tailwindcss/utilities'"
) {
atRule.name = 'tailwind'
atRule.params = 'utilities'
} else if (
atRule.params === '"tailwindcss/screens"' ||
atRule.params === "'tailwindcss/screens'" ||
atRule.params === '"tailwindcss/variants"' ||
atRule.params === "'tailwindcss/variants'"
) {
atRule.name = 'tailwind'
atRule.params = 'variants'
}
}
}
if (atRule.name === 'tailwind') {
if (atRule.params === 'screens') {
atRule.params = 'variants'

View file

@ -21,7 +21,6 @@ import isSyntacticallyValidPropertyValue from '../util/isSyntacticallyValidPrope
import { generateRules, getClassNameFromSelector } from './generateRules'
import { hasContentChanged } from './cacheInvalidation.js'
import { Offsets } from './offsets.js'
import { flagEnabled } from '../featureFlags.js'
import { finalizeSelector, formatVariantSelector } from '../util/formatVariantSelector'
const VARIANT_TYPES = {
@ -434,10 +433,8 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
},
}
let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
let ruleSets = []
.concat(modifiersEnabled ? rule(value, extras) : rule(value))
.concat(rule(value, extras))
.filter(Boolean)
.map((declaration) => ({
[nameClass(identifier, modifier)]: declaration,
@ -514,10 +511,8 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
},
}
let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
let ruleSets = []
.concat(modifiersEnabled ? rule(value, extras) : rule(value))
.concat(rule(value, extras))
.filter(Boolean)
.map((declaration) => ({
[nameClass(identifier, modifier)]: declaration,
@ -585,18 +580,13 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
let id = options?.id ?? ++variantIdentifier
let isSpecial = variant === '@'
let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
for (let [key, value] of Object.entries(options?.values ?? {})) {
if (key === 'DEFAULT') continue
api.addVariant(
isSpecial ? `${variant}${key}` : `${variant}-${key}`,
({ args, container }) => {
return variantFn(
value,
modifiersEnabled ? { modifier: args?.modifier, container } : { container }
)
return variantFn(value, { modifier: args?.modifier, container })
},
{
@ -624,7 +614,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
: // Falling back to args if it is a string, otherwise '' for older intellisense
// (JetBrains) plugins.
args?.value ?? (typeof args === 'string' ? args : ''),
modifiersEnabled ? { modifier: args?.modifier, container } : { container }
{ modifier: args?.modifier, container }
)
},
{

View file

@ -63,9 +63,7 @@ function getTailwindConfig(configOrPath) {
}
// It's a plain object, not a path
let newConfig = resolveConfig(
configOrPath.config === undefined ? configOrPath : configOrPath.config
)
let newConfig = resolveConfig(configOrPath?.config ?? configOrPath ?? {})
newConfig = validateConfig(newConfig)

View file

@ -1,16 +1,12 @@
import pkg from '../../package.json'
export const env =
typeof process !== 'undefined'
? {
NODE_ENV: process.env.NODE_ENV,
DEBUG: resolveDebug(process.env.DEBUG),
ENGINE: pkg.tailwindcss.engine,
}
: {
NODE_ENV: 'production',
DEBUG: false,
ENGINE: pkg.tailwindcss.engine,
}
export const contextMap = new Map()

View file

@ -1 +0,0 @@
import './cli/index'

View file

@ -1,91 +0,0 @@
import packageJson from '../../../../package.json'
import browserslist from 'browserslist'
import { Result } from 'postcss'
import {
// @ts-ignore
lazyPostcss,
// @ts-ignore
lazyPostcssImport,
// @ts-ignore
lazyCssnano,
// @ts-ignore
} from '../../../../peers/index'
export function lazyLightningCss() {
// TODO: Make this lazy/bundled
return require('lightningcss')
}
let lightningCss
function loadLightningCss() {
if (lightningCss) {
return lightningCss
}
// Try to load a local version first
try {
return (lightningCss = require('lightningcss'))
} catch {}
return (lightningCss = lazyLightningCss())
}
export async function lightningcss(shouldMinify: boolean, result: Result) {
let css = loadLightningCss()
try {
let transformed = css.transform({
filename: result.opts.from || 'input.css',
code: Buffer.from(result.css, 'utf-8'),
minify: shouldMinify,
sourceMap: !!result.map,
inputSourceMap: result.map ? result.map.toString() : undefined,
targets: css.browserslistToTargets(browserslist(packageJson.browserslist)),
drafts: {
nesting: true,
},
})
return Object.assign(result, {
css: transformed.code.toString('utf8'),
map: result.map
? Object.assign(result.map, {
toString() {
return transformed.map.toString()
},
})
: result.map,
})
} catch (err) {
console.error('Unable to use Lightning CSS. Using raw version instead.')
console.error(err)
return result
}
}
/**
* @returns {import('postcss')}
*/
export function loadPostcss() {
// Try to load a local `postcss` version first
try {
return require('postcss')
} catch {}
return lazyPostcss()
}
export function loadPostcssImport() {
// Try to load a local `postcss-import` version first
try {
return require('postcss-import')
} catch {}
return lazyPostcssImport()
}

View file

@ -1,47 +0,0 @@
import fs from 'fs'
import path from 'path'
import { resolveDefaultConfigPath } from '../../../util/resolveConfigPath'
import { createProcessor } from './plugin'
export async function build(args) {
let input = args['--input']
let shouldWatch = args['--watch']
// TODO: Deprecate this in future versions
if (!input && args['_'][1]) {
console.error('[deprecation] Running tailwindcss without -i, please provide an input file.')
input = args['--input'] = args['_'][1]
}
if (input && input !== '-' && !fs.existsSync((input = path.resolve(input)))) {
console.error(`Specified input file ${args['--input']} does not exist.`)
process.exit(9)
}
if (args['--config'] && !fs.existsSync((args['--config'] = path.resolve(args['--config'])))) {
console.error(`Specified config file ${args['--config']} does not exist.`)
process.exit(9)
}
// TODO: Reference the @config path here if exists
let configPath = args['--config'] ? args['--config'] : resolveDefaultConfigPath()
let processor = await createProcessor(args, configPath)
if (shouldWatch) {
// Abort the watcher if stdin is closed to avoid zombie processes
// You can disable this behavior with --watch=always
if (args['--watch'] !== 'always') {
process.stdin.on('end', () => process.exit(0))
}
process.stdin.resume()
await processor.watch()
} else {
await processor.build().catch((e) => {
console.error(e)
process.exit(1)
})
}
}

View file

@ -1,419 +0,0 @@
import path from 'path'
import fs from 'fs'
import postcssrc from 'postcss-load-config'
import { lilconfig } from 'lilconfig'
import loadPlugins from 'postcss-load-config/src/plugins' // Little bit scary, looking at private/internal API
import loadOptions from 'postcss-load-config/src/options' // Little bit scary, looking at private/internal API
import tailwind from '../../../processTailwindFeatures'
import { loadPostcss, loadPostcssImport, lightningcss } from './deps'
import { formatNodes, drainStdin, outputFile } from './utils'
import { env } from '../../../lib/sharedState'
import resolveConfig from '../../../../resolveConfig'
import { parseCandidateFiles } from '../../../lib/content'
import { createWatcher } from './watching'
import fastGlob from 'fast-glob'
import { findAtConfigPath } from '../../../lib/findAtConfigPath'
import log from '../../../util/log'
import { loadConfig } from '../../../lib/load-config'
import getModuleDependencies from '../../../lib/getModuleDependencies'
import type { Config } from '../../../../types'
import { validateConfig } from '../../../util/validateConfig'
import { handleImportAtRules } from '../../../lib/handleImportAtRules'
/**
*
* @param {string} [customPostCssPath ]
* @returns
*/
async function loadPostCssPlugins(customPostCssPath) {
let config = customPostCssPath
? await (async () => {
let file = path.resolve(customPostCssPath)
// Implementation, see: https://unpkg.com/browse/postcss-load-config@3.1.0/src/index.js
// @ts-ignore
let { config = {} } = await lilconfig('postcss').load(file)
if (typeof config === 'function') {
config = config()
} else {
config = Object.assign({}, config)
}
if (!config.plugins) {
config.plugins = []
}
return {
file,
plugins: loadPlugins(config, file),
options: loadOptions(config, file),
}
})()
: await postcssrc()
let configPlugins = config.plugins
let configPluginTailwindIdx = configPlugins.findIndex((plugin) => {
if (typeof plugin === 'function' && plugin.name === 'tailwindcss') {
return true
}
if (typeof plugin === 'object' && plugin !== null && plugin.postcssPlugin === 'tailwindcss') {
return true
}
return false
})
let beforePlugins =
configPluginTailwindIdx === -1 ? [] : configPlugins.slice(0, configPluginTailwindIdx)
let afterPlugins =
configPluginTailwindIdx === -1
? configPlugins
: configPlugins.slice(configPluginTailwindIdx + 1)
return [beforePlugins, afterPlugins, config.options]
}
let state = {
/** @type {any} */
context: null,
/** @type {ReturnType<typeof createWatcher> | null} */
watcher: null,
/** @type {{content: string, extension: string}[]} */
changedContent: [],
/** @type {{config: Config, dependencies: Set<string>, dispose: Function } | null} */
configBag: null,
contextDependencies: new Set(),
/** @type {import('../../lib/content.js').ContentPath[]} */
contentPaths: [],
refreshContentPaths() {
this.contentPaths = parseCandidateFiles(this.context, this.context?.tailwindConfig)
},
get config() {
return this.context.tailwindConfig
},
get contentPatterns() {
return {
all: this.contentPaths.map((contentPath) => contentPath.pattern),
dynamic: this.contentPaths
.filter((contentPath) => contentPath.glob !== undefined)
.map((contentPath) => contentPath.pattern),
}
},
loadConfig(configPath, content) {
if (this.watcher && configPath) {
this.refreshConfigDependencies()
}
let config = loadConfig(configPath)
let dependencies = getModuleDependencies(configPath)
this.configBag = {
config,
dependencies,
dispose() {
for (let file of dependencies) {
delete require.cache[require.resolve(file)]
}
},
}
// @ts-ignore
if (__OXIDE__) {
this.configBag.config = validateConfig(resolveConfig(this.configBag.config))
} else {
this.configBag.config = validateConfig(
resolveConfig(this.configBag.config, { content: { files: [] } })
)
}
// Override content files if `--content` has been passed explicitly
if (content?.length > 0) {
this.configBag.config.content.files = content
}
return this.configBag.config
},
refreshConfigDependencies(configPath) {
env.DEBUG && console.time('Module dependencies')
this.configBag?.dispose()
env.DEBUG && console.timeEnd('Module dependencies')
},
readContentPaths() {
let content = []
// Resolve globs from the content config
// TODO: When we make the postcss plugin async-capable this can become async
let files = fastGlob.sync(this.contentPatterns.all)
for (let file of files) {
if (__OXIDE__) {
content.push({
file,
extension: path.extname(file).slice(1),
})
} else {
content.push({
content: fs.readFileSync(path.resolve(file), 'utf8'),
extension: path.extname(file).slice(1),
})
}
}
// Resolve raw content in the tailwind config
let rawContent = this.config.content.files.filter((file) => {
return file !== null && typeof file === 'object'
})
for (let { raw: htmlContent, extension = 'html' } of rawContent) {
content.push({ content: htmlContent, extension })
}
return content
},
getContext({ createContext, cliConfigPath, root, result, content }) {
if (this.context) {
this.context.changedContent = this.changedContent.splice(0)
return this.context
}
env.DEBUG && console.time('Searching for config')
let configPath = findAtConfigPath(root, result) ?? cliConfigPath
env.DEBUG && console.timeEnd('Searching for config')
env.DEBUG && console.time('Loading config')
let config = this.loadConfig(configPath, content)
env.DEBUG && console.timeEnd('Loading config')
env.DEBUG && console.time('Creating context')
this.context = createContext(config, [])
Object.assign(this.context, {
userConfigPath: configPath,
})
env.DEBUG && console.timeEnd('Creating context')
env.DEBUG && console.time('Resolving content paths')
this.refreshContentPaths()
env.DEBUG && console.timeEnd('Resolving content paths')
if (this.watcher) {
env.DEBUG && console.time('Watch new files')
this.watcher.refreshWatchedFiles()
env.DEBUG && console.timeEnd('Watch new files')
}
for (let file of this.readContentPaths()) {
this.context.changedContent.push(file)
}
return this.context
},
}
export async function createProcessor(args, cliConfigPath) {
let postcss = loadPostcss()
let input = args['--input']
let output = args['--output']
let includePostCss = args['--postcss']
let customPostCssPath = typeof args['--postcss'] === 'string' ? args['--postcss'] : undefined
let [beforePlugins, afterPlugins, postcssOptions] = includePostCss
? await loadPostCssPlugins(customPostCssPath)
: [[], [], {}]
beforePlugins.unshift(...handleImportAtRules(loadPostcssImport()))
if (args['--purge']) {
log.warn('purge-flag-deprecated', [
'The `--purge` flag has been deprecated.',
'Please use `--content` instead.',
])
if (!args['--content']) {
args['--content'] = args['--purge']
}
}
let content = args['--content']?.split(/(?<!{[^}]+),/) ?? []
let tailwindPlugin = () => {
return {
postcssPlugin: 'tailwindcss',
Once(root, { result }) {
env.DEBUG && console.time('Compiling CSS')
tailwind(({ createContext }) => {
console.error()
console.error('Rebuilding...')
return () => {
return state.getContext({
createContext,
cliConfigPath,
root,
result,
content,
})
}
})(root, result)
env.DEBUG && console.timeEnd('Compiling CSS')
},
}
}
tailwindPlugin.postcss = true
let plugins = [
...beforePlugins,
tailwindPlugin,
!args['--minify'] && formatNodes,
...afterPlugins,
].filter(Boolean)
/** @type {import('postcss').Processor} */
// @ts-ignore
let processor = postcss(plugins)
async function readInput() {
// Piping in data, let's drain the stdin
if (input === '-') {
return drainStdin()
}
// Input file has been provided
if (input) {
return fs.promises.readFile(path.resolve(input), 'utf8')
}
// No input file provided, fallback to default atrules
return '@tailwind base; @tailwind components; @tailwind utilities'
}
async function build() {
let start = process.hrtime.bigint()
return readInput()
.then((css) => processor.process(css, { ...postcssOptions, from: input, to: output }))
.then((result) => lightningcss(!!args['--minify'], result))
.then((result) => {
if (!state.watcher) {
return result
}
env.DEBUG && console.time('Recording PostCSS dependencies')
for (let message of result.messages) {
if (message.type === 'dependency') {
state.contextDependencies.add(message.file)
}
}
env.DEBUG && console.timeEnd('Recording PostCSS dependencies')
// TODO: This needs to be in a different spot
env.DEBUG && console.time('Watch new files')
state.watcher.refreshWatchedFiles()
env.DEBUG && console.timeEnd('Watch new files')
return result
})
.then((result) => {
if (!output) {
process.stdout.write(result.css)
return
}
return Promise.all([
outputFile(result.opts.to, result.css),
result.map && outputFile(result.opts.to + '.map', result.map.toString()),
])
})
.then(() => {
let end = process.hrtime.bigint()
console.error()
console.error('Done in', (end - start) / BigInt(1e6) + 'ms.')
})
.then(
() => {},
(err) => {
// TODO: If an initial build fails we can't easily pick up any PostCSS dependencies
// that were collected before the error occurred
// The result is not stored on the error so we have to store it externally
// and pull the messages off of it here somehow
// This results in a less than ideal DX because the watcher will not pick up
// changes to imported CSS if one of them caused an error during the initial build
// If you fix it and then save the main CSS file so there's no error
// The watcher will start watching the imported CSS files and will be
// resilient to future errors.
if (state.watcher) {
console.error(err)
} else {
return Promise.reject(err)
}
}
)
}
/**
* @param {{file: string, content(): Promise<string>, extension: string}[]} changes
*/
async function parseChanges(changes) {
return Promise.all(
changes.map(async (change) => ({
content: await change.content(),
extension: change.extension,
}))
)
}
if (input !== undefined && input !== '-') {
state.contextDependencies.add(path.resolve(input))
}
return {
build,
watch: async () => {
state.watcher = createWatcher(args, {
state,
/**
* @param {{file: string, content(): Promise<string>, extension: string}[]} changes
*/
async rebuild(changes) {
let needsNewContext = changes.some((change) => {
return (
state.configBag?.dependencies.has(change.file) ||
state.contextDependencies.has(change.file)
)
})
if (needsNewContext) {
state.context = null
} else {
for (let change of await parseChanges(changes)) {
state.changedContent.push(change)
}
}
return build()
},
})
await build()
},
}
}

View file

@ -1,74 +0,0 @@
import fs from 'fs'
import path from 'path'
export function indentRecursive(node, indent = 0) {
node.each &&
node.each((child, i) => {
if (!child.raws.before || !child.raws.before.trim() || child.raws.before.includes('\n')) {
child.raws.before = `\n${node.type !== 'rule' && i > 0 ? '\n' : ''}${' '.repeat(indent)}`
}
child.raws.after = `\n${' '.repeat(indent)}`
indentRecursive(child, indent + 1)
})
}
export function formatNodes(root) {
indentRecursive(root)
if (root.first) {
root.first.raws.before = ''
}
}
/**
* When rapidly saving files atomically a couple of situations can happen:
* - The file is missing since the external program has deleted it by the time we've gotten around to reading it from the earlier save.
* - The file is being written to by the external program by the time we're going to read it and is thus treated as busy because a lock is held.
*
* To work around this we retry reading the file a handful of times with a delay between each attempt
*
* @param {string} path
* @param {number} tries
* @returns {Promise<string | undefined>}
* @throws {Error} If the file is still missing or busy after the specified number of tries
*/
export async function readFileWithRetries(path, tries = 5) {
for (let n = 0; n <= tries; n++) {
try {
return await fs.promises.readFile(path, 'utf8')
} catch (err) {
if (n !== tries) {
if (err.code === 'ENOENT' || err.code === 'EBUSY') {
await new Promise((resolve) => setTimeout(resolve, 10))
continue
}
}
throw err
}
}
}
export function drainStdin() {
return new Promise((resolve, reject) => {
let result = ''
process.stdin.on('data', (chunk) => {
result += chunk
})
process.stdin.on('end', () => resolve(result))
process.stdin.on('error', (err) => reject(err))
})
}
export async function outputFile(file, newContents) {
try {
let currentContents = await fs.promises.readFile(file, 'utf8')
if (currentContents === newContents) {
return // Skip writing the file
}
} catch {}
// Write the file
await fs.promises.mkdir(path.dirname(file), { recursive: true })
await fs.promises.writeFile(file, newContents, 'utf8')
}

View file

@ -1,225 +0,0 @@
import chokidar from 'chokidar'
import fs from 'fs'
import micromatch from 'micromatch'
import normalizePath from 'normalize-path'
import path from 'path'
import { readFileWithRetries } from './utils'
/**
* The core idea of this watcher is:
* 1. Whenever a file is added, changed, or renamed we queue a rebuild
* 2. Perform as few rebuilds as possible by batching them together
* 3. Coalesce events that happen in quick succession to avoid unnecessary rebuilds
* 4. Ensure another rebuild happens _if_ changed while a rebuild is in progress
*/
/**
*
* @param {*} args
* @param {{ state, rebuild(changedFiles: any[]): Promise<any> }} param1
* @returns {{
* fswatcher: import('chokidar').FSWatcher,
* refreshWatchedFiles(): void,
* }}
*/
export function createWatcher(args, { state, rebuild }) {
let shouldPoll = args['--poll']
let shouldCoalesceWriteEvents = shouldPoll || process.platform === 'win32'
// Polling interval in milliseconds
// Used only when polling or coalescing add/change events on Windows
let pollInterval = 10
let watcher = chokidar.watch([], {
// Force checking for atomic writes in all situations
// This causes chokidar to wait up to 100ms for a file to re-added after it's been unlinked
// This only works when watching directories though
atomic: true,
usePolling: shouldPoll,
interval: shouldPoll ? pollInterval : undefined,
ignoreInitial: true,
awaitWriteFinish: shouldCoalesceWriteEvents
? {
stabilityThreshold: 50,
pollInterval: pollInterval,
}
: false,
})
// A queue of rebuilds, file reads, etc… to run
let chain = Promise.resolve()
/**
* A list of files that have been changed since the last rebuild
*
* @type {{file: string, content: () => Promise<string>, extension: string}[]}
*/
let changedContent = []
/**
* A list of files for which a rebuild has already been queued.
* This is used to prevent duplicate rebuilds when multiple events are fired for the same file.
* The rebuilt file is cleared from this list when it's associated rebuild has _started_
* This is because if the file is changed during a rebuild it won't trigger a new rebuild which it should
**/
let pendingRebuilds = new Set()
let _timer
let _reject
/**
* Rebuilds the changed files and resolves when the rebuild is
* complete regardless of whether it was successful or not
*/
async function rebuildAndContinue() {
let changes = changedContent.splice(0)
// There are no changes to rebuild so we can just do nothing
if (changes.length === 0) {
return Promise.resolve()
}
// Clear all pending rebuilds for the about-to-be-built files
changes.forEach((change) => pendingRebuilds.delete(change.file))
// Resolve the promise even when the rebuild fails
return rebuild(changes).then(
() => {},
() => {}
)
}
/**
*
* @param {*} file
* @param {(() => Promise<string>) | null} content
* @param {boolean} skipPendingCheck
* @returns {Promise<void>}
*/
function recordChangedFile(file, content = null, skipPendingCheck = false) {
file = path.resolve(file)
// Applications like Vim/Neovim fire both rename and change events in succession for atomic writes
// In that case rebuild has already been queued by rename, so can be skipped in change
if (pendingRebuilds.has(file) && !skipPendingCheck) {
return Promise.resolve()
}
// Mark that a rebuild of this file is going to happen
// It MUST happen synchronously before the rebuild is queued for this to be effective
pendingRebuilds.add(file)
changedContent.push({
file,
content: content ?? (() => fs.promises.readFile(file, 'utf8')),
extension: path.extname(file).slice(1),
})
if (_timer) {
clearTimeout(_timer)
_reject()
}
// If a rebuild is already in progress we don't want to start another one until the 10ms timer has expired
chain = chain.then(
() =>
new Promise((resolve, reject) => {
_timer = setTimeout(resolve, 10)
_reject = reject
})
)
// Resolves once this file has been rebuilt (or the rebuild for this file has failed)
// This queues as many rebuilds as there are changed files
// But those rebuilds happen after some delay
// And will immediately resolve if there are no changes
chain = chain.then(rebuildAndContinue, rebuildAndContinue)
return chain
}
watcher.on('change', (file) => recordChangedFile(file))
watcher.on('add', (file) => recordChangedFile(file))
// Restore watching any files that are "removed"
// This can happen when a file is pseudo-atomically replaced (a copy is created, overwritten, the old one is unlinked, and the new one is renamed)
// TODO: An an optimization we should allow removal when the config changes
watcher.on('unlink', (file) => {
file = normalizePath(file)
// Only re-add the file if it's not covered by a dynamic pattern
if (!micromatch.some([file], state.contentPatterns.dynamic)) {
watcher.add(file)
}
})
// Some applications such as Visual Studio (but not VS Code)
// will only fire a rename event for atomic writes and not a change event
// This is very likely a chokidar bug but it's one we need to work around
// We treat this as a change event and rebuild the CSS
watcher.on('raw', (evt, filePath, meta) => {
if (evt !== 'rename') {
return
}
let watchedPath = meta.watchedPath
// Watched path might be the file itself
// Or the directory it is in
filePath = watchedPath.endsWith(filePath) ? watchedPath : path.join(watchedPath, filePath)
// Skip this event since the files it is for does not match any of the registered content globs
if (!micromatch.some([filePath], state.contentPatterns.all)) {
return
}
// Skip since we've already queued a rebuild for this file that hasn't happened yet
if (pendingRebuilds.has(filePath)) {
return
}
// We'll go ahead and add the file to the pending rebuilds list here
// It'll be removed when the rebuild starts unless the read fails
// which will be taken care of as well
pendingRebuilds.add(filePath)
async function enqueue() {
try {
// We need to read the file as early as possible outside of the chain
// because it may be gone by the time we get to it. doing the read
// immediately increases the chance that the file is still there
let content = await readFileWithRetries(path.resolve(filePath))
if (content === undefined) {
return
}
// This will push the rebuild onto the chain
// We MUST skip the rebuild check here otherwise the rebuild will never happen on Linux
// This is because the order of events and timing is different on Linux
// @ts-ignore: TypeScript isn't picking up that content is a string here
await recordChangedFile(filePath, () => content, true)
} catch {
// If reading the file fails, it's was probably a deleted temporary file
// So we can ignore it and no rebuild is needed
}
}
enqueue().then(() => {
// If the file read fails we still need to make sure the file isn't stuck in the pending rebuilds list
pendingRebuilds.delete(filePath)
})
})
return {
fswatcher: watcher,
refreshWatchedFiles() {
watcher.add(Array.from(state.contextDependencies))
watcher.add(Array.from(state.configBag.dependencies))
watcher.add(state.contentPatterns.all)
},
}
}

View file

@ -1,69 +0,0 @@
import packageJson from '../../../../package.json'
export function help({ message, usage, commands, options }) {
let indent = 2
// Render header
console.log()
console.log(`${packageJson.name} v${packageJson.version}`)
// Render message
if (message) {
console.log()
for (let msg of message.split('\n')) {
console.log(msg)
}
}
// Render usage
if (usage && usage.length > 0) {
console.log()
console.log('Usage:')
for (let example of usage) {
console.log(' '.repeat(indent), example)
}
}
// Render commands
if (commands && commands.length > 0) {
console.log()
console.log('Commands:')
for (let command of commands) {
console.log(' '.repeat(indent), command)
}
}
// Render options
if (options) {
let groupedOptions = {}
for (let [key, value] of Object.entries(options)) {
if (typeof value === 'object') {
groupedOptions[key] = { ...value, flags: [key] }
} else {
groupedOptions[value].flags.push(key)
}
}
console.log()
console.log('Options:')
for (let { flags, description, deprecated } of Object.values(groupedOptions)) {
if (deprecated) continue
if (flags.length === 1) {
console.log(
' '.repeat(indent + 4 /* 4 = "-i, ".length */),
flags.slice().reverse().join(', ').padEnd(20, ' '),
description
)
} else {
console.log(
' '.repeat(indent),
flags.slice().reverse().join(', ').padEnd(24, ' '),
description
)
}
}
}
console.log()
}

View file

@ -1,204 +0,0 @@
#!/usr/bin/env node
import path from 'path'
import arg from 'arg'
import fs from 'fs'
import { build } from './build'
import { help } from './help'
import { init } from './init'
// ---
function oneOf(...options) {
return Object.assign(
(value = true) => {
for (let option of options) {
let parsed = option(value)
if (parsed === value) {
return parsed
}
}
throw new Error('...')
},
{ manualParsing: true }
)
}
let commands = {
init: {
run: init,
args: {
'--esm': { type: Boolean, description: `Initialize configuration file as ESM` },
'--ts': { type: Boolean, description: `Initialize configuration file as TypeScript` },
'--full': {
type: Boolean,
description: `Include the default values for all options in the generated configuration file`,
},
'-f': '--full',
},
},
build: {
run: build,
args: {
'--input': { type: String, description: 'Input file' },
'--output': { type: String, description: 'Output file' },
'--watch': {
type: oneOf(String, Boolean),
description: 'Watch for changes and rebuild as needed',
},
'--poll': {
type: Boolean,
description: 'Use polling instead of filesystem events when watching',
},
'--content': {
type: String,
description: 'Content paths to use for removing unused classes',
},
'--minify': { type: Boolean, description: 'Minify the output' },
'--config': {
type: String,
description: 'Path to a custom config file',
},
'-c': '--config',
'-i': '--input',
'-o': '--output',
'-m': '--minify',
'-w': '--watch',
'-p': '--poll',
},
},
}
let sharedFlags = {
'--help': { type: Boolean, description: 'Display usage information' },
'-h': '--help',
}
if (
process.stdout.isTTY /* Detect redirecting output to a file */ &&
(process.argv[2] === undefined ||
process.argv.slice(2).every((flag) => sharedFlags[flag] !== undefined))
) {
help({
usage: [
'tailwindcss [--input input.css] [--output output.css] [--watch] [options...]',
'tailwindcss init [--full] [options...]',
],
commands: Object.keys(commands)
.filter((command) => command !== 'build')
.map((command) => `${command} [options]`),
options: { ...commands.build.args, ...sharedFlags },
})
process.exit(0)
}
let command = ((arg = '') => (arg.startsWith('-') ? undefined : arg))(process.argv[2]) || 'build'
if (commands[command] === undefined) {
if (fs.existsSync(path.resolve(command))) {
// TODO: Deprecate this in future versions
// Check if non-existing command, might be a file.
command = 'build'
} else {
help({
message: `Invalid command: ${command}`,
usage: ['tailwindcss <command> [options]'],
commands: Object.keys(commands)
.filter((command) => command !== 'build')
.map((command) => `${command} [options]`),
options: sharedFlags,
})
process.exit(1)
}
}
// Execute command
let { args: flags, run } = commands[command]
let args = (() => {
try {
let result = arg(
Object.fromEntries(
Object.entries({ ...flags, ...sharedFlags })
.filter(([_key, value]) => !value?.type?.manualParsing)
.map(([key, value]) => [key, typeof value === 'object' ? value.type : value])
),
{ permissive: true }
)
// Manual parsing of flags to allow for special flags like oneOf(Boolean, String)
for (let i = result['_'].length - 1; i >= 0; --i) {
let flag = result['_'][i]
if (!flag.startsWith('-')) continue
let [flagName, flagValue] = flag.split('=')
let handler = flags[flagName]
// Resolve flagName & handler
while (typeof handler === 'string') {
flagName = handler
handler = flags[handler]
}
if (!handler) continue
let args = []
let offset = i + 1
// --flag value syntax was used so we need to pull `value` from `args`
if (flagValue === undefined) {
// Parse args for current flag
while (result['_'][offset] && !result['_'][offset].startsWith('-')) {
args.push(result['_'][offset++])
}
// Cleanup manually parsed flags + args
result['_'].splice(i, 1 + args.length)
// No args were provided, use default value defined in handler
// One arg was provided, use that directly
// Multiple args were provided so pass them all in an array
flagValue = args.length === 0 ? undefined : args.length === 1 ? args[0] : args
} else {
// Remove the whole flag from the args array
result['_'].splice(i, 1)
}
// Set the resolved value in the `result` object
result[flagName] = handler.type(flagValue, flagName)
}
// Ensure that the `command` is always the first argument in the `args`.
// This is important so that we don't have to check if a default command
// (build) was used or not from within each plugin.
//
// E.g.: tailwindcss input.css -> _: ['build', 'input.css']
// E.g.: tailwindcss build input.css -> _: ['build', 'input.css']
if (result['_'][0] !== command) {
result['_'].unshift(command)
}
return result
} catch (err) {
if (err.code === 'ARG_UNKNOWN_OPTION') {
help({
message: err.message,
usage: ['tailwindcss <command> [options]'],
options: sharedFlags,
})
process.exit(1)
}
throw err
}
})()
if (args['--help']) {
help({
options: { ...flags, ...sharedFlags },
usage: [`tailwindcss ${command} [options]`],
})
process.exit(0)
}
run(args)

View file

@ -1,64 +0,0 @@
import fs from 'fs'
import path from 'path'
function isESM() {
const pkgPath = path.resolve('./package.json')
try {
let pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
return pkg.type && pkg.type === 'module'
} catch (err) {
return false
}
}
export function init(args) {
let messages: string[] = []
let isProjectESM = args['--ts'] || args['--esm'] || isESM()
let syntax = args['--ts'] ? 'ts' : isProjectESM ? 'js' : 'cjs'
let extension = args['--ts'] ? 'ts' : 'js'
let tailwindConfigLocation = path.resolve(args['_'][1] ?? `./tailwind.config.${extension}`)
if (fs.existsSync(tailwindConfigLocation)) {
messages.push(`${path.basename(tailwindConfigLocation)} already exists.`)
} else {
let stubContentsFile = fs.readFileSync(
args['--full']
? path.resolve(__dirname, '../../../../stubs/config.full.js')
: path.resolve(__dirname, '../../../../stubs/config.simple.js'),
'utf8'
)
let stubFile = fs.readFileSync(
path.resolve(__dirname, `../../../../stubs/tailwind.config.${syntax}`),
'utf8'
)
// Drop `content` in the oxide engine to promote auto content
if (__OXIDE__) {
stubContentsFile = stubContentsFile.replace(/ *content: \[\],\n/, '')
}
// Change colors import
stubContentsFile = stubContentsFile.replace('../colors', 'tailwindcss/colors')
// Replace contents of {ts,js,cjs} file with the stub {simple,full}.
stubFile =
stubFile
.replace('__CONFIG__', stubContentsFile.replace('module.exports =', '').trim())
.trim() + '\n\n'
fs.writeFileSync(tailwindConfigLocation, stubFile, 'utf8')
messages.push(`Created Tailwind CSS config file: ${path.basename(tailwindConfigLocation)}`)
}
if (messages.length > 0) {
console.log()
for (let message of messages) {
console.log(message)
}
}
}

View file

@ -1 +0,0 @@
module.exports = require('../plugin.js')

View file

@ -1,3 +1,6 @@
import postcss from 'postcss'
import lightningcss from 'lightningcss'
import browserslist from 'browserslist'
import setupTrackingContext from './lib/setupTrackingContext'
import processTailwindFeatures from './processTailwindFeatures'
import { env } from './lib/sharedState'
@ -14,7 +17,7 @@ module.exports = function tailwindcss(configOrPath) {
console.time('JIT TOTAL')
return root
},
...(__OXIDE__ ? handleImportAtRules() : []),
...handleImportAtRules(),
function (root, result) {
// Use the path for the `@config` directive if it exists, otherwise use the
// path for the file being processed
@ -36,66 +39,61 @@ module.exports = function tailwindcss(configOrPath) {
processTailwindFeatures(context)(root, result)
},
__OXIDE__ &&
function lightningCssPlugin(_root, result) {
let postcss = require('postcss')
let lightningcss = require('lightningcss')
let browserslist = require('browserslist')
function lightningCssPlugin(_root, result) {
try {
let transformed = lightningcss.transform({
filename: result.opts.from,
code: Buffer.from(result.root.toString()),
minify: false,
sourceMap: !!result.map,
inputSourceMap: result.map ? result.map.toString() : undefined,
targets:
typeof process !== 'undefined' && process.env.JEST_WORKER_ID
? { chrome: 106 << 16 }
: lightningcss.browserslistToTargets(
browserslist(require('../package.json').browserslist)
),
try {
let transformed = lightningcss.transform({
filename: result.opts.from,
code: Buffer.from(result.root.toString()),
minify: false,
sourceMap: !!result.map,
inputSourceMap: result.map ? result.map.toString() : undefined,
targets:
typeof process !== 'undefined' && process.env.JEST_WORKER_ID
? { chrome: 106 << 16 }
: lightningcss.browserslistToTargets(
browserslist(require('../package.json').browserslist)
),
drafts: {
nesting: true,
customMedia: true,
},
})
drafts: {
nesting: true,
customMedia: true,
},
})
result.map = Object.assign(result.map ?? {}, {
toJSON() {
return transformed.map.toJSON()
},
toString() {
return transformed.map.toString()
},
})
result.map = Object.assign(result.map ?? {}, {
toJSON() {
return transformed.map.toJSON()
},
toString() {
return transformed.map.toString()
},
})
result.root = postcss.parse(transformed.code.toString('utf8'))
} catch (err) {
if (typeof process !== 'undefined' && process.env.JEST_WORKER_ID) {
let lines = err.source.split('\n')
err = new Error(
[
'Error formatting using Lightning CSS:',
'',
...[
'```css',
...lines.slice(Math.max(err.loc.line - 3, 0), err.loc.line),
' '.repeat(err.loc.column - 1) + '^-- ' + err.toString(),
...lines.slice(err.loc.line, err.loc.line + 2),
'```',
],
].join('\n')
)
}
if (Error.captureStackTrace) {
Error.captureStackTrace(err, lightningCssPlugin)
}
throw err
result.root = postcss.parse(transformed.code.toString('utf8'))
} catch (err) {
if (typeof process !== 'undefined' && process.env.JEST_WORKER_ID) {
let lines = err.source.split('\n')
err = new Error(
[
'Error formatting using Lightning CSS:',
'',
...[
'```css',
...lines.slice(Math.max(err.loc.line - 3, 0), err.loc.line),
' '.repeat(err.loc.column - 1) + '^-- ' + err.toString(),
...lines.slice(err.loc.line, err.loc.line + 2),
'```',
],
].join('\n')
)
}
},
if (Error.captureStackTrace) {
Error.captureStackTrace(err, lightningCssPlugin)
}
throw err
}
},
env.DEBUG &&
function (root) {
console.timeEnd('JIT TOTAL')

View file

@ -2,14 +2,7 @@ import defaultFullConfig from '../../stubs/config.full.js'
import { flagEnabled } from '../featureFlags'
export default function getAllConfigs(config) {
const configs = (
config?.presets ?? [
__OXIDE__
? // Drop `content` in the oxide engine to promote auto content
Object.assign({}, defaultFullConfig, { content: 'auto' })
: defaultFullConfig,
]
)
const configs = (config?.presets ?? [defaultFullConfig])
.slice()
.reverse()
.flatMap((preset) => getAllConfigs(preset instanceof Function ? preset() : preset))

View file

@ -17,6 +17,30 @@ export function dim(input) {
}
export default {
group(key, cb) {
if (typeof process !== 'undefined' && process.env.JEST_WORKER_ID) return
if (key && alreadyShown.has(key)) return
if (key) alreadyShown.add(key)
console.warn('')
cb({
info(messages) {
messages.forEach((message) => console.warn(colors.bold(colors.cyan('info')), '-', message))
},
warn(messages) {
messages.forEach((message) =>
console.warn(colors.bold(colors.yellow('warn')), '-', message)
)
},
risk(messages) {
messages.forEach((message) =>
console.warn(colors.bold(colors.magenta('risk')), '-', message)
)
},
})
},
info(key, messages) {
log(colors.bold(colors.cyan('info')), ...(Array.isArray(key) ? [key] : [messages, key]))
},

View file

@ -185,31 +185,6 @@ export function normalizeConfig(config) {
config.prefix = config.prefix ?? ''
}
let auto = (() => {
// Config still has a `purge` option (for backwards compatibility), auto content should not be
// used
if (config.purge) return false
//
if (config.content === 'auto') return true
// We don't have content at all, auto content should be used
if (config.content === undefined) return true
// We do have content as an object, but we don't have any files defined, auto content should
// be used
if (
typeof config.content === 'object' &&
config.content !== null &&
!Array.isArray(config.content)
) {
return config.content.files === undefined
}
// We do have content defined, auto content should not be used
return false
})()
// Normalize the `content`
config.content = {
relative: (() => {
@ -222,20 +197,23 @@ export function normalizeConfig(config) {
return flagEnabled(config, 'relativeContentPathsByDefault')
})(),
files: auto
? 'auto'
: (() => {
let { content, purge } = config
files: (() => {
let { content, purge } = config
if (content === undefined && purge === undefined) return []
if (Array.isArray(purge)) return purge
if (Array.isArray(purge?.content)) return purge.content
if (Array.isArray(content)) return content
if (Array.isArray(content?.content)) return content.content
if (Array.isArray(content?.files)) return content.files
if (content === undefined && purge === undefined) return []
if (purge) {
if (Array.isArray(purge)) return purge
if (Array.isArray(purge?.content)) return purge.content
return []
}
return []
})(),
if (Array.isArray(content)) return content
if (Array.isArray(content?.content)) return content.content
if (Array.isArray(content?.files)) return content.files
if (content === 'auto') return ['auto']
return []
})(),
extract: (() => {
let extract = (() => {

View file

@ -18,7 +18,6 @@ import {
} from './dataTypes'
import negateValue from './negateValue'
import { backgroundSize } from './validateFormalSyntax'
import { flagEnabled } from '../featureFlags.js'
/**
* @param {import('postcss-selector-parser').Container} selectors
@ -243,12 +242,9 @@ export function coerceValue(types, modifier, options, tailwindConfig) {
* @returns {Iterator<[value: string, type: string, modifier: string | null]>}
*/
export function* getMatchingTypes(types, rawModifier, options, tailwindConfig) {
let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
let [modifier, utilityModifier] = splitUtilityModifier(rawModifier)
let canUseUtilityModifier =
modifiersEnabled &&
options.modifiers != null &&
(options.modifiers === 'any' ||
(typeof options.modifiers === 'object' &&

View file

@ -1,7 +1,8 @@
import pc from 'picocolors'
import log from './log'
export function validateConfig(config) {
if (config.content.files !== 'auto' && config.content.files.length === 0) {
if (config.content.files.length === 0) {
log.warn('content-problems', [
'The `content` option in your Tailwind CSS configuration is missing or empty.',
'Configure your content sources or your generated CSS will be missing styles.',
@ -9,11 +10,13 @@ export function validateConfig(config) {
])
}
if (config.content.files === 'auto') {
log.warn('auto-content-experimental', [
'Automatic content detection in Tailwind CSS is currently in experimental preview.',
'Preview features are not covered by semver, and may change at any time.',
])
if (config.content.files.includes('auto')) {
log.group('auto-content-experimental', (log) => {
log.info([pc.bold('Automatically detecting Tailwind CSS content sources...')])
log.warn([
'Automatic content detection is experimental, and the behavior may change at any time.',
])
})
}
// Warn if the line-clamp plugin is installed

View file

@ -1,5 +1,5 @@
module.exports = {
content: [],
content: ['auto'],
presets: [],
darkMode: 'media', // or 'class'
theme: {

View file

@ -1,5 +1,5 @@
module.exports = {
content: [],
content: ['auto'],
theme: {
extend: {},
},

View file

@ -1,6 +1,5 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View file

@ -1,6 +1,5 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View file

@ -1,278 +1,276 @@
import { crosscheck, run, html, css } from './util/run'
import { run, html, css } from './util/run'
crosscheck(() => {
test('basic', () => {
let config = {
content: [
{
raw: html`
<div class="animate-spin"></div>
<div class="hover:animate-ping"></div>
<div class="group-hover:animate-bounce"></div>
`,
},
],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: 1s linear infinite spin;
}
@keyframes ping {
75%,
100% {
opacity: 0;
transform: scale(2);
}
}
.hover\:animate-ping:hover {
animation: 1s cubic-bezier(0, 0, 0.2, 1) infinite ping;
}
@keyframes bounce {
0%,
100% {
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
transform: translateY(-25%);
}
50% {
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
transform: none;
}
}
.group:hover .group-hover\:animate-bounce {
animation: 1s infinite bounce;
}
`)
})
})
test('custom', () => {
let config = {
content: [{ raw: html`<div class="animate-one"></div>` }],
theme: {
extend: {
keyframes: {
one: { to: { transform: 'rotate(360deg)' } },
},
animation: {
one: 'one 2s',
},
},
test('basic', () => {
let config = {
content: [
{
raw: html`
<div class="animate-spin"></div>
<div class="hover:animate-ping"></div>
<div class="group-hover:animate-bounce"></div>
`,
},
}
],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes one {
to {
transform: rotate(360deg);
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes spin {
to {
transform: rotate(360deg);
}
.animate-one {
animation: 2s one;
}
.animate-spin {
animation: 1s linear infinite spin;
}
@keyframes ping {
75%,
100% {
opacity: 0;
transform: scale(2);
}
`)
})
})
test('custom prefixed', () => {
let config = {
prefix: 'tw-',
content: [{ raw: `<div class="tw-animate-one"></div>` }],
theme: {
extend: {
keyframes: {
one: { to: { transform: 'rotate(360deg)' } },
},
animation: {
one: 'one 2s',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes tw-one {
to {
transform: rotate(360deg);
}
}
.hover\:animate-ping:hover {
animation: 1s cubic-bezier(0, 0, 0.2, 1) infinite ping;
}
@keyframes bounce {
0%,
100% {
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
transform: translateY(-25%);
}
.tw-animate-one {
animation: 2s tw-one;
50% {
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
transform: none;
}
`)
})
})
test('multiple', () => {
let config = {
content: [{ raw: html`<div class="animate-multiple"></div>` }],
theme: {
extend: {
animation: {
multiple: 'bounce 2s linear, pulse 3s ease-in',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes bounce {
0%,
100% {
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
transform: translateY(-25%);
}
50% {
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
transform: none;
}
}
@keyframes pulse {
50% {
opacity: 0.5;
}
}
.animate-multiple {
animation: 2s linear bounce, 3s ease-in pulse;
}
`)
})
})
test('multiple custom', () => {
let config = {
content: [{ raw: html`<div class="animate-multiple"></div>` }],
theme: {
extend: {
keyframes: {
one: { to: { transform: 'rotate(360deg)' } },
two: { to: { transform: 'scale(1.23)' } },
},
animation: {
multiple: 'one 2s, two 3s',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes one {
to {
transform: rotate(360deg);
}
}
@keyframes two {
to {
transform: scale(1.23);
}
}
.animate-multiple {
animation: 2s one, 3s two;
}
`)
})
})
test('with dots in the name', () => {
let config = {
content: [
{
raw: html`
<div class="animate-zoom-.5"></div>
<div class="animate-zoom-1.5"></div>
`,
},
],
theme: {
extend: {
keyframes: {
'zoom-.5': { to: { transform: 'scale(0.5)' } },
'zoom-1.5': { to: { transform: 'scale(1.5)' } },
},
animation: {
'zoom-.5': 'zoom-.5 2s',
'zoom-1.5': 'zoom-1.5 2s',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes zoom-\.5 {
to {
transform: scale(0.5);
}
}
.animate-zoom-\.5 {
animation: 2s zoom-\.5;
}
@keyframes zoom-1\.5 {
to {
transform: scale(1.5);
}
}
.animate-zoom-1\.5 {
animation: 2s zoom-1\.5;
}
`)
})
})
test('with dots in the name and prefix', () => {
let config = {
prefix: 'tw-',
content: [
{
raw: html`
<div class="tw-animate-zoom-.5"></div>
<div class="tw-animate-zoom-1.5"></div>
`,
},
],
theme: {
extend: {
keyframes: {
'zoom-.5': { to: { transform: 'scale(0.5)' } },
'zoom-1.5': { to: { transform: 'scale(1.5)' } },
},
animation: {
'zoom-.5': 'zoom-.5 2s',
'zoom-1.5': 'zoom-1.5 2s',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes tw-zoom-\.5 {
to {
transform: scale(0.5);
}
}
.tw-animate-zoom-\.5 {
animation: 2s tw-zoom-\.5;
}
@keyframes tw-zoom-1\.5 {
to {
transform: scale(1.5);
}
}
.tw-animate-zoom-1\.5 {
animation: 2s tw-zoom-1\.5;
}
`)
})
}
.group:hover .group-hover\:animate-bounce {
animation: 1s infinite bounce;
}
`)
})
})
test('custom', () => {
let config = {
content: [{ raw: html`<div class="animate-one"></div>` }],
theme: {
extend: {
keyframes: {
one: { to: { transform: 'rotate(360deg)' } },
},
animation: {
one: 'one 2s',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes one {
to {
transform: rotate(360deg);
}
}
.animate-one {
animation: 2s one;
}
`)
})
})
test('custom prefixed', () => {
let config = {
prefix: 'tw-',
content: [{ raw: `<div class="tw-animate-one"></div>` }],
theme: {
extend: {
keyframes: {
one: { to: { transform: 'rotate(360deg)' } },
},
animation: {
one: 'one 2s',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes tw-one {
to {
transform: rotate(360deg);
}
}
.tw-animate-one {
animation: 2s tw-one;
}
`)
})
})
test('multiple', () => {
let config = {
content: [{ raw: html`<div class="animate-multiple"></div>` }],
theme: {
extend: {
animation: {
multiple: 'bounce 2s linear, pulse 3s ease-in',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes bounce {
0%,
100% {
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
transform: translateY(-25%);
}
50% {
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
transform: none;
}
}
@keyframes pulse {
50% {
opacity: 0.5;
}
}
.animate-multiple {
animation: 2s linear bounce, 3s ease-in pulse;
}
`)
})
})
test('multiple custom', () => {
let config = {
content: [{ raw: html`<div class="animate-multiple"></div>` }],
theme: {
extend: {
keyframes: {
one: { to: { transform: 'rotate(360deg)' } },
two: { to: { transform: 'scale(1.23)' } },
},
animation: {
multiple: 'one 2s, two 3s',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes one {
to {
transform: rotate(360deg);
}
}
@keyframes two {
to {
transform: scale(1.23);
}
}
.animate-multiple {
animation: 2s one, 3s two;
}
`)
})
})
test('with dots in the name', () => {
let config = {
content: [
{
raw: html`
<div class="animate-zoom-.5"></div>
<div class="animate-zoom-1.5"></div>
`,
},
],
theme: {
extend: {
keyframes: {
'zoom-.5': { to: { transform: 'scale(0.5)' } },
'zoom-1.5': { to: { transform: 'scale(1.5)' } },
},
animation: {
'zoom-.5': 'zoom-.5 2s',
'zoom-1.5': 'zoom-1.5 2s',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes zoom-\.5 {
to {
transform: scale(0.5);
}
}
.animate-zoom-\.5 {
animation: 2s zoom-\.5;
}
@keyframes zoom-1\.5 {
to {
transform: scale(1.5);
}
}
.animate-zoom-1\.5 {
animation: 2s zoom-1\.5;
}
`)
})
})
test('with dots in the name and prefix', () => {
let config = {
prefix: 'tw-',
content: [
{
raw: html`
<div class="tw-animate-zoom-.5"></div>
<div class="tw-animate-zoom-1.5"></div>
`,
},
],
theme: {
extend: {
keyframes: {
'zoom-.5': { to: { transform: 'scale(0.5)' } },
'zoom-1.5': { to: { transform: 'scale(1.5)' } },
},
animation: {
'zoom-.5': 'zoom-.5 2s',
'zoom-1.5': 'zoom-1.5 2s',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@keyframes tw-zoom-\.5 {
to {
transform: scale(0.5);
}
}
.tw-animate-zoom-\.5 {
animation: 2s tw-zoom-\.5;
}
@keyframes tw-zoom-1\.5 {
to {
transform: scale(1.5);
}
}
.tw-animate-zoom-1\.5 {
animation: 2s tw-zoom-1\.5;
}
`)
})
})

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,460 +1,456 @@
import { crosscheck, run, html, css, defaults } from './util/run'
import { run, html, css, defaults } from './util/run'
crosscheck(() => {
test('basic arbitrary properties', () => {
let config = {
content: [
{
raw: html`<div class="[paint-order:markers]"></div>`,
},
],
corePlugins: { preflight: false },
}
test('basic arbitrary properties', () => {
let config = {
content: [
{
raw: html`<div class="[paint-order:markers]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[paint-order\:markers\] {
paint-order: markers;
}
`)
})
})
test('different arbitrary properties are picked up separately', () => {
let config = {
content: [
{
raw: html`<div class="[foo:bar] [bar:baz]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[bar\:baz\] {
bar: baz;
}
.\[foo\:bar\] {
foo: bar;
}
`)
})
})
test('arbitrary properties with modifiers', () => {
let config = {
content: [
{
raw: html`<div class="dark:lg:hover:[paint-order:markers]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
@media (prefers-color-scheme: dark) {
@media (min-width: 1024px) {
.dark\:lg\:hover\:\[paint-order\:markers\]:hover {
paint-order: markers;
}
}
}
`)
})
})
test('arbitrary properties are sorted after utilities', () => {
let config = {
content: [
{
raw: html`<div
class="content-none [paint-order:markers] hover:pointer-events-none"
></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.content-none {
--tw-content: none;
content: var(--tw-content);
}
.\[paint-order\:markers\] {
paint-order: markers;
}
.hover\:pointer-events-none:hover {
pointer-events: none;
}
`)
})
})
test('using CSS variables', () => {
let config = {
content: [
{
raw: html`<div class="[--my-var:auto]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[--my-var\:auto\] {
--my-var: auto;
}
`)
})
})
test('using underscores as spaces', () => {
let config = {
content: [
{
raw: html`<div class="[--my-var:2px_4px]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[--my-var\:2px_4px\] {
--my-var: 2px 4px;
}
`)
})
})
test('using the important modifier', () => {
let config = {
content: [
{
raw: html`<div class="![--my-var:2px_4px]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\!\[--my-var\:2px_4px\] {
--my-var: 2px 4px !important;
}
`)
})
})
test('colons are allowed in quotes', () => {
let config = {
content: [
{
raw: html`<div class="[content:'foo:bar']"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[content\:\'foo\:bar\'\] {
content: 'foo:bar';
}
`)
})
})
test('colons are allowed in braces', () => {
let config = {
content: [
{
raw: html`<div class="[background-image:url(http://example.com/picture.jpg)]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[background-image\:url\(http\:\/\/example\.com\/picture\.jpg\)\] {
background-image: url('http://example.com/picture.jpg');
}
`)
})
})
test('invalid class', () => {
let config = {
content: [
{
raw: html`<div class="[a:b:c:d]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
`)
})
})
test('invalid arbitrary property', () => {
let config = {
content: [
{
raw: html`<div class="[autoplay:\${autoplay}]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
`)
})
})
test('invalid arbitrary property 2', () => {
let config = {
content: [
{
raw: html`[0:02]`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
`)
})
})
test('using fractional spacing values inside theme() function', () => {
let config = {
content: [
{
raw: html`<div
class="[border:_calc(5vw_-_theme(spacing[2.5]))_double_theme('colors.fuchsia.700')]"
></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[border\:_calc\(5vw_-_theme\(spacing\[2\.5\]\)\)_double_theme\(\'colors\.fuchsia\.700\'\)\] {
border: calc(5vw - 0.625rem) double #a21caf;
}
`)
})
})
test('using multiple arbitrary props having fractional spacing values', () => {
let config = {
content: [
{
raw: html`<div
class="[height:_calc(100vh_-_theme(spacing[2.5]))] [box-shadow:_0_calc(theme(spacing[0.5])_*_-1)_theme(colors.red.400)_inset]"
></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[box-shadow\:_0_calc\(theme\(spacing\[0\.5\]\)_\*_-1\)_theme\(colors\.red\.400\)_inset\] {
box-shadow: inset 0 -0.125rem #f87171;
}
.\[height\:_calc\(100vh_-_theme\(spacing\[2\.5\]\)\)\] {
height: calc(100vh - 0.625rem);
}
`)
})
})
it('should be possible to read theme values in arbitrary properties (without quotes)', () => {
let config = {
content: [{ raw: html`<div class="[--a:theme(colors.blue.500)] [color:var(--a)]"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[--a\:theme\(colors\.blue\.500\)\] {
--a: #3b82f6;
}
.\[color\:var\(--a\)\] {
color: var(--a);
}
`)
})
})
it('should be possible to read theme values in arbitrary properties (with quotes)', () => {
let config = {
content: [{ raw: html`<div class="[color:var(--a)] [--a:theme('colors.blue.500')]"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[--a\:theme\(\'colors\.blue\.500\'\)\] {
--a: #3b82f6;
}
.\[color\:var\(--a\)\] {
color: var(--a);
}
`)
})
})
it('should not generate invalid CSS', () => {
let config = {
content: [
{
raw: html`
<div class="[https://en.wikipedia.org/wiki]"></div>
<div class="[http://example.org]"></div>
<div class="[http://example]"></div>
<div class="[ftp://example]"></div>
<div class="[stillworks:/example]"></div>
`,
// NOTE: In this case `stillworks:/example` being generated is not ideal
// but it at least doesn't produce invalid CSS when run through prettier
// So we can let it through since it is technically valid
},
],
corePlugins: { preflight: false },
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.\[stillworks\:\/example\] {
stillworks: /example;
}
`)
})
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[paint-order\:markers\] {
paint-order: markers;
}
`)
})
})
test('different arbitrary properties are picked up separately', () => {
let config = {
content: [
{
raw: html`<div class="[foo:bar] [bar:baz]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[bar\:baz\] {
bar: baz;
}
.\[foo\:bar\] {
foo: bar;
}
`)
})
})
test('arbitrary properties with modifiers', () => {
let config = {
content: [
{
raw: html`<div class="dark:lg:hover:[paint-order:markers]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
@media (prefers-color-scheme: dark) {
@media (min-width: 1024px) {
.dark\:lg\:hover\:\[paint-order\:markers\]:hover {
paint-order: markers;
}
}
}
`)
})
})
test('arbitrary properties are sorted after utilities', () => {
let config = {
content: [
{
raw: html`<div class="content-none [paint-order:markers] hover:pointer-events-none"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.content-none {
--tw-content: none;
content: var(--tw-content);
}
.\[paint-order\:markers\] {
paint-order: markers;
}
.hover\:pointer-events-none:hover {
pointer-events: none;
}
`)
})
})
test('using CSS variables', () => {
let config = {
content: [
{
raw: html`<div class="[--my-var:auto]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[--my-var\:auto\] {
--my-var: auto;
}
`)
})
})
test('using underscores as spaces', () => {
let config = {
content: [
{
raw: html`<div class="[--my-var:2px_4px]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[--my-var\:2px_4px\] {
--my-var: 2px 4px;
}
`)
})
})
test('using the important modifier', () => {
let config = {
content: [
{
raw: html`<div class="![--my-var:2px_4px]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\!\[--my-var\:2px_4px\] {
--my-var: 2px 4px !important;
}
`)
})
})
test('colons are allowed in quotes', () => {
let config = {
content: [
{
raw: html`<div class="[content:'foo:bar']"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[content\:\'foo\:bar\'\] {
content: 'foo:bar';
}
`)
})
})
test('colons are allowed in braces', () => {
let config = {
content: [
{
raw: html`<div class="[background-image:url(http://example.com/picture.jpg)]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[background-image\:url\(http\:\/\/example\.com\/picture\.jpg\)\] {
background-image: url('http://example.com/picture.jpg');
}
`)
})
})
test('invalid class', () => {
let config = {
content: [
{
raw: html`<div class="[a:b:c:d]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
`)
})
})
test('invalid arbitrary property', () => {
let config = {
content: [
{
raw: html`<div class="[autoplay:\${autoplay}]"></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
`)
})
})
test('invalid arbitrary property 2', () => {
let config = {
content: [
{
raw: html`[0:02]`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
`)
})
})
test('using fractional spacing values inside theme() function', () => {
let config = {
content: [
{
raw: html`<div
class="[border:_calc(5vw_-_theme(spacing[2.5]))_double_theme('colors.fuchsia.700')]"
></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[border\:_calc\(5vw_-_theme\(spacing\[2\.5\]\)\)_double_theme\(\'colors\.fuchsia\.700\'\)\] {
border: calc(5vw - 0.625rem) double #a21caf;
}
`)
})
})
test('using multiple arbitrary props having fractional spacing values', () => {
let config = {
content: [
{
raw: html`<div
class="[height:_calc(100vh_-_theme(spacing[2.5]))] [box-shadow:_0_calc(theme(spacing[0.5])_*_-1)_theme(colors.red.400)_inset]"
></div>`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[box-shadow\:_0_calc\(theme\(spacing\[0\.5\]\)_\*_-1\)_theme\(colors\.red\.400\)_inset\] {
box-shadow: inset 0 -0.125rem #f87171;
}
.\[height\:_calc\(100vh_-_theme\(spacing\[2\.5\]\)\)\] {
height: calc(100vh - 0.625rem);
}
`)
})
})
it('should be possible to read theme values in arbitrary properties (without quotes)', () => {
let config = {
content: [{ raw: html`<div class="[--a:theme(colors.blue.500)] [color:var(--a)]"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[--a\:theme\(colors\.blue\.500\)\] {
--a: #3b82f6;
}
.\[color\:var\(--a\)\] {
color: var(--a);
}
`)
})
})
it('should be possible to read theme values in arbitrary properties (with quotes)', () => {
let config = {
content: [{ raw: html`<div class="[color:var(--a)] [--a:theme('colors.blue.500')]"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
${defaults}
.\[--a\:theme\(\'colors\.blue\.500\'\)\] {
--a: #3b82f6;
}
.\[color\:var\(--a\)\] {
color: var(--a);
}
`)
})
})
it('should not generate invalid CSS', () => {
let config = {
content: [
{
raw: html`
<div class="[https://en.wikipedia.org/wiki]"></div>
<div class="[http://example.org]"></div>
<div class="[http://example]"></div>
<div class="[ftp://example]"></div>
<div class="[stillworks:/example]"></div>
`,
// NOTE: In this case `stillworks:/example` being generated is not ideal
// but it at least doesn't produce invalid CSS when run through prettier
// So we can let it through since it is technically valid
},
],
corePlugins: { preflight: false },
}
return run('@tailwind utilities', config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.\[stillworks\:\/example\] {
stillworks: /example;
}
`)
})
})

File diff suppressed because it is too large Load diff

View file

@ -185,7 +185,7 @@
min-width: 3.23rem;
}
.min-w-\[calc\(1-\(\(12-3\)\*0\.5\)\)\] {
min-width: calc(1 - ((12 - 3) * 0.5));
min-width: -3.5px;
}
.min-w-\[calc\(1-\(var\(--something\)\*0\.5\)\)\] {
min-width: calc(1 - (var(--something) * 0.5));

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,126 +1,107 @@
import { crosscheck, run, html, css } from './util/run'
import { run, html, css } from './util/run'
crosscheck(({ stable, oxide }) => {
it('can block classes matched literally', () => {
let config = {
content: [
{
raw: html`<div
class="font-bold uppercase sm:hover:text-sm hover:text-sm bg-red-500/50 my-custom-class"
></div>`,
},
],
blocklist: ['font', 'uppercase', 'hover:text-sm', 'bg-red-500/50', 'my-custom-class'],
it('can block classes matched literally', () => {
let config = {
content: [
{
raw: html`<div
class="font-bold uppercase sm:hover:text-sm hover:text-sm bg-red-500/50 my-custom-class"
></div>`,
},
],
blocklist: ['font', 'uppercase', 'hover:text-sm', 'bg-red-500/50', 'my-custom-class'],
}
let input = css`
@tailwind utilities;
.my-custom-class {
color: red;
}
`
let input = css`
@tailwind utilities;
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.font-bold {
font-weight: 700;
}
.my-custom-class {
color: red;
}
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.font-bold {
font-weight: 700;
@media (min-width: 640px) {
.sm\:hover\:text-sm:hover {
font-size: 0.875rem;
line-height: 1.25rem;
}
.my-custom-class {
color: red;
}
@media (min-width: 640px) {
.sm\:hover\:text-sm:hover {
font-size: 0.875rem;
line-height: 1.25rem;
}
}
`)
})
})
it('can block classes inside @layer', () => {
let config = {
content: [
{
raw: html`<div class="font-bold my-custom-class"></div>`,
},
],
blocklist: ['my-custom-class'],
}
let input = css`
@tailwind utilities;
@layer utilities {
.my-custom-class {
color: red;
}
}
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.font-bold {
font-weight: 700;
}
`)
})
})
it('blocklists do NOT support regexes', async () => {
let config = {
content: [{ raw: html`<div class="font-bold bg-[#f00d1e]"></div>` }],
blocklist: [/^bg-\[[^]+\]$/],
}
let result = await run('@tailwind utilities', config)
stable.expect(result.css).toMatchFormattedCss(css`
.bg-\[\#f00d1e\] {
--tw-bg-opacity: 1;
background-color: rgb(240 13 30 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
`)
oxide.expect(result.css).toMatchFormattedCss(css`
.bg-\[\#f00d1e\] {
background-color: #f00d1e;
}
.font-bold {
font-weight: 700;
}
`)
expect().toHaveBeenWarnedWith(['blocklist-invalid'])
})
it('can block classes generated by the safelist', () => {
let config = {
content: [{ raw: html`<div class="font-bold"></div>` }],
safelist: [{ pattern: /^bg-red-(400|500)$/ }],
blocklist: ['bg-red-500'],
}
return run('@tailwind utilities', config).then((result) => {
stable.expect(result.css).toMatchFormattedCss(css`
.bg-red-400 {
--tw-bg-opacity: 1;
background-color: rgb(248 113 113 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
`)
oxide.expect(result.css).toMatchFormattedCss(css`
.bg-red-400 {
background-color: #f87171;
}
.font-bold {
font-weight: 700;
}
`)
})
})
})
it('can block classes inside @layer', () => {
let config = {
content: [
{
raw: html`<div class="font-bold my-custom-class"></div>`,
},
],
blocklist: ['my-custom-class'],
}
let input = css`
@tailwind utilities;
@layer utilities {
.my-custom-class {
color: red;
}
}
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.font-bold {
font-weight: 700;
}
`)
})
})
it('blocklists do NOT support regexes', async () => {
let config = {
content: [{ raw: html`<div class="font-bold bg-[#f00d1e]"></div>` }],
blocklist: [/^bg-\[[^]+\]$/],
}
let result = await run('@tailwind utilities', config)
expect(result.css).toMatchFormattedCss(css`
.bg-\[\#f00d1e\] {
--tw-bg-opacity: 1;
background-color: rgb(240 13 30 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
`)
expect().toHaveBeenWarnedWith(['blocklist-invalid'])
})
it('can block classes generated by the safelist', () => {
let config = {
content: [{ raw: html`<div class="font-bold"></div>` }],
safelist: [{ pattern: /^bg-red-(400|500)$/ }],
blocklist: ['bg-red-500'],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-red-400 {
--tw-bg-opacity: 1;
background-color: rgb(248 113 113 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
`)
})
})

View file

@ -1,114 +1,119 @@
import { crosscheck, run, html, css, defaults } from './util/run'
import { run, html, css, defaults } from './util/run'
crosscheck(({ stable, oxide }) => {
test('collapse adjacent rules', () => {
let config = {
content: [
{
raw: html`
<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>
<div class="some-apply-thing"></div>
`,
},
],
corePlugins: { preflight: false },
theme: {},
plugins: [
function ({ addVariant }) {
addVariant('foo-bar', '@supports (foo: bar)')
},
],
test('collapse adjacent rules', () => {
let config = {
content: [
{
raw: html`
<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>
<div class="some-apply-thing"></div>
`,
},
],
corePlugins: { preflight: false },
theme: {},
plugins: [
function ({ addVariant }) {
addVariant('foo-bar', '@supports (foo: bar)')
},
],
}
let input = css`
@tailwind base;
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter.woff2') format('woff2'), url('/fonts/Inter.woff') format('woff');
}
let input = css`
@tailwind base;
@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: 'Inter';
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');
}
}
.foo,
.bar {
color: black;
}
.foo,
.bar {
font-weight: 700;
}
.some-apply-thing {
@apply foo-bar:md:text-black foo-bar:md:font-bold foo-bar:text-black foo-bar:font-bold md:font-bold md:text-black;
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@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');
}
${defaults}
@font-face {
font-family: Inter;
src: url('/fonts/Inter.woff2') format('woff2'), url('/fonts/Inter.woff') format('woff');
}
@font-face {
font-family: 'Gilroy';
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');
}
}
.foo,
.bar {
color: black;
}
.foo,
.bar {
.font-bold {
font-weight: 700;
}
.some-apply-thing {
@apply foo-bar:md:text-black foo-bar:md:font-bold foo-bar:text-black foo-bar:font-bold md:font-bold md:text-black;
.foo,
.bar {
color: #000;
font-weight: 700;
}
`
return run(input, config).then((result) => {
stable.expect(result.css).toMatchFormattedCss(css`
@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');
}
${defaults}
@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 {
@supports (foo: bar) {
.some-apply-thing {
--tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity));
font-weight: 700;
}
.foo,
.bar {
color: #000;
}
@media (min-width: 768px) {
.some-apply-thing {
--tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity));
font-weight: 700;
}
@supports (foo: bar) {
.some-apply-thing {
--tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity));
font-weight: 700;
}
}
}
@supports (foo: bar) {
@media (min-width: 768px) {
.some-apply-thing {
--tw-text-opacity: 1;
@ -116,134 +121,49 @@ crosscheck(({ stable, oxide }) => {
font-weight: 700;
}
}
@supports (foo: bar) {
@media (min-width: 768px) {
.some-apply-thing {
--tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity));
font-weight: 700;
}
}
}
@media (min-width: 640px) {
.sm\:text-center {
text-align: center;
}
@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;
}
}
`)
oxide.expect(result.css).toMatchFormattedCss(css`
@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');
}
${defaults}
@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 {
.sm\:font-bold {
font-weight: 700;
}
.foo,
.bar {
color: #000;
}
@media (min-width: 768px) {
.md\:text-center {
text-align: center;
}
.md\:font-bold {
font-weight: 700;
}
@supports (foo: bar) {
.some-apply-thing {
color: #000;
font-weight: 700;
}
}
@media (min-width: 1024px) {
.lg\:text-center {
text-align: center;
}
@media (min-width: 768px) {
.some-apply-thing {
color: #000;
font-weight: 700;
}
.lg\:font-bold {
font-weight: 700;
}
@supports (foo: bar) {
@media (min-width: 768px) {
.some-apply-thing {
color: #000;
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;
}
}
`)
})
})
test('duplicate url imports does not break rule collapsing', () => {
let config = {
content: [{ raw: html`` }],
corePlugins: { preflight: false },
}
let input = css`
@import url('https://example.com');
@import url('https://example.com');
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@import 'https://example.com';
`)
})
}
`)
})
})
test('duplicate url imports does not break rule collapsing', () => {
let config = {
content: [{ raw: html`` }],
corePlugins: { preflight: false },
}
let input = css`
@import url('https://example.com');
@import url('https://example.com');
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@import 'https://example.com';
`)
})
})

View file

@ -1,177 +1,175 @@
import { crosscheck, run, html, css } from './util/run'
import { run, html, css } from './util/run'
crosscheck(() => {
it('should collapse duplicate declarations with the same units (px)', () => {
let config = {
content: [{ raw: html`<div class="example"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
it('should collapse duplicate declarations with the same units (px)', () => {
let config = {
content: [{ raw: html`<div class="example"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
let input = css`
@tailwind utilities;
@layer utilities {
.example {
height: 100px;
height: 200px;
}
@layer utilities {
.example {
height: 100px;
height: 200px;
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.example {
height: 200px;
}
`)
})
})
it('should collapse duplicate declarations with the same units (no unit)', () => {
let config = {
content: [{ raw: html`<div class="example"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
`
let input = css`
@tailwind utilities;
@layer utilities {
.example {
line-height: 3;
line-height: 2;
}
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.example {
height: 200px;
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.example {
line-height: 2;
}
`)
})
})
it('should not collapse duplicate declarations with the different units', () => {
let config = {
content: [{ raw: html`<div class="example"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
@layer utilities {
.example {
height: 100px;
height: 50%;
}
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.example {
height: 100px;
height: 50%;
}
`)
})
})
it('should collapse the duplicate declarations with the same unit, but leave the ones with different units', () => {
let config = {
content: [{ raw: html`<div class="example"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
@layer utilities {
.example {
height: 100px;
height: 50%;
height: 20vh;
height: 200px;
height: 100%;
height: 30vh;
}
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.example {
height: 200px;
height: 100%;
height: 30vh;
}
`)
})
})
it('should collapse the duplicate declarations with the exact same value', () => {
let config = {
content: [{ raw: html`<div class="example"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
@layer utilities {
.example {
height: var(--value);
color: blue;
height: var(--value);
}
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.example {
color: #00f;
height: var(--value);
}
`)
})
})
it('should work on a real world example', () => {
let config = {
content: [{ raw: html`<div class="h-available"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
@layer utilities {
.h-available {
height: 100%;
height: 100vh;
height: -webkit-fill-available;
}
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.h-available {
height: 100%;
height: 100vh;
height: -webkit-fill-available;
}
`)
})
`)
})
})
it('should collapse duplicate declarations with the same units (no unit)', () => {
let config = {
content: [{ raw: html`<div class="example"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
@layer utilities {
.example {
line-height: 3;
line-height: 2;
}
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.example {
line-height: 2;
}
`)
})
})
it('should not collapse duplicate declarations with the different units', () => {
let config = {
content: [{ raw: html`<div class="example"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
@layer utilities {
.example {
height: 100px;
height: 50%;
}
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.example {
height: 100px;
height: 50%;
}
`)
})
})
it('should collapse the duplicate declarations with the same unit, but leave the ones with different units', () => {
let config = {
content: [{ raw: html`<div class="example"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
@layer utilities {
.example {
height: 100px;
height: 50%;
height: 20vh;
height: 200px;
height: 100%;
height: 30vh;
}
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.example {
height: 200px;
height: 100%;
height: 30vh;
}
`)
})
})
it('should collapse the duplicate declarations with the exact same value', () => {
let config = {
content: [{ raw: html`<div class="example"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
@layer utilities {
.example {
height: var(--value);
color: blue;
height: var(--value);
}
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.example {
color: #00f;
height: var(--value);
}
`)
})
})
it('should work on a real world example', () => {
let config = {
content: [{ raw: html`<div class="h-available"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
@layer utilities {
.h-available {
height: 100%;
height: 100vh;
height: -webkit-fill-available;
}
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.h-available {
height: 100%;
height: 100vh;
height: -webkit-fill-available;
}
`)
})
})

View file

@ -1,264 +1,234 @@
import { crosscheck, run, html, css } from './util/run'
import { run, html, css } from './util/run'
crosscheck(({ stable, oxide }) => {
test('basic color opacity modifier', async () => {
let config = {
content: [{ raw: html`<div class="bg-red-500/50"></div>` }],
}
test('basic color opacity modifier', async () => {
let config = {
content: [{ raw: html`<div class="bg-red-500/50"></div>` }],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-red-500\/50 {
background-color: #ef444480;
}
`)
})
})
test('colors with slashes are matched first', async () => {
let config = {
content: [{ raw: html`<div class="bg-red-500/50"></div>` }],
theme: {
extend: {
colors: {
'red-500/50': '#ff0000',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
stable.expect(result.css).toMatchFormattedCss(css`
.bg-red-500\/50 {
--tw-bg-opacity: 1;
background-color: rgb(255 0 0 / var(--tw-bg-opacity));
}
`)
oxide.expect(result.css).toMatchFormattedCss(css`
.bg-red-500\/50 {
background-color: red;
}
`)
})
})
test('arbitrary color opacity modifier', async () => {
let config = {
content: [{ raw: html`<div class="bg-red-500/[var(--opacity)]"></div>` }],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-red-500\/\[var\(--opacity\)\] {
background-color: rgb(239 68 68 / var(--opacity));
}
`)
})
})
test('missing alpha generates nothing', async () => {
let config = {
content: [{ raw: html`<div class="bg-red-500/"></div>` }],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(``)
})
})
test('arbitrary color with opacity from scale', async () => {
let config = {
content: [{ raw: 'bg-[wheat]/50' }],
theme: {},
plugins: [],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-\[wheat\]\/50 {
background-color: #f5deb380;
}
`)
})
})
test('arbitrary color with arbitrary opacity', async () => {
let config = {
content: [{ raw: 'bg-[#bada55]/[0.2]' }],
theme: {},
plugins: [],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-\[\#bada55\]\/\[0\.2\] {
background-color: #bada5533;
}
`)
})
})
test('undefined theme color with opacity from scale', async () => {
let config = {
content: [{ raw: 'bg-garbage/50' }],
theme: {},
plugins: [],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(``)
})
})
test('values not in the opacity config are ignored', async () => {
let config = {
content: [{ raw: html`<div class="bg-red-500/29"></div>` }],
theme: {
opacity: {
0: '0',
25: '0.25',
5: '0.5',
75: '0.75',
100: '1',
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(``)
})
})
test('function colors are supported', async () => {
let config = {
content: [{ raw: html`<div class="bg-blue/50"></div>` }],
theme: {
colors: {
blue: ({ opacityValue }) => {
return `rgba(var(--colors-blue), ${opacityValue})`
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-blue\/50 {
background-color: rgba(var(--colors-blue), 0.5);
}
`)
})
})
test('utilities that support any type are supported', async () => {
let config = {
content: [
{
raw: html`
<div class="from-red-500/50"></div>
<div class="fill-red-500/25"></div>
<div class="placeholder-red-500/75"></div>
`,
},
],
theme: {
extend: {
fill: (theme) => theme('colors'),
},
},
plugins: [],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.from-red-500\/50 {
--tw-gradient-from: #ef444480 var(--tw-gradient-from-position);
--tw-gradient-to: #ef444400 var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.fill-red-500\/25 {
fill: #ef444440;
}
.placeholder-red-500\/75::placeholder {
color: #ef4444bf;
}
`)
})
})
test('opacity modifier in combination with partial custom properties', async () => {
let config = {
content: [
{
raw: html`
<div class="bg-[hsl(var(--foo),50%,50%)]"></div>
<div class="bg-[hsl(123,var(--foo),50%)]"></div>
<div class="bg-[hsl(123,50%,var(--foo))]"></div>
<div class="bg-[hsl(var(--foo),50%,50%)]/50"></div>
<div class="bg-[hsl(123,var(--foo),50%)]/50"></div>
<div class="bg-[hsl(123,50%,var(--foo))]/50"></div>
<div class="bg-[hsl(var(--foo),var(--bar),var(--baz))]/50"></div>
`,
},
],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
`
return run(input, config).then((result) => {
stable.expect(result.css).toMatchFormattedCss(css`
.bg-\[hsl\(123\,50\%\,var\(--foo\)\)\] {
--tw-bg-opacity: 1;
background-color: hsl(123 50% var(--foo) / var(--tw-bg-opacity));
}
.bg-\[hsl\(123\,50\%\,var\(--foo\)\)\]\/50 {
background-color: hsl(123 50% var(--foo) / 0.5);
}
.bg-\[hsl\(123\,var\(--foo\)\,50\%\)\] {
--tw-bg-opacity: 1;
background-color: hsl(123 var(--foo) 50% / var(--tw-bg-opacity));
}
.bg-\[hsl\(123\,var\(--foo\)\,50\%\)\]\/50 {
background-color: hsl(123 var(--foo) 50% / 0.5);
}
.bg-\[hsl\(var\(--foo\)\,50\%\,50\%\)\] {
--tw-bg-opacity: 1;
background-color: hsl(var(--foo) 50% 50% / var(--tw-bg-opacity));
}
.bg-\[hsl\(var\(--foo\)\,50\%\,50\%\)\]\/50 {
background-color: hsl(var(--foo) 50% 50% / 0.5);
}
.bg-\[hsl\(var\(--foo\)\,var\(--bar\)\,var\(--baz\)\)\]\/50 {
background-color: hsl(var(--foo) var(--bar) var(--baz) / 0.5);
}
`)
oxide.expect(result.css).toMatchFormattedCss(css`
.bg-\[hsl\(123\,50\%\,var\(--foo\)\)\] {
background-color: hsl(123, 50%, var(--foo));
}
.bg-\[hsl\(123\,50\%\,var\(--foo\)\)\]\/50 {
background-color: hsl(123 50% var(--foo) / 0.5);
}
.bg-\[hsl\(123\,var\(--foo\)\,50\%\)\] {
background-color: hsl(123, var(--foo), 50%);
}
.bg-\[hsl\(123\,var\(--foo\)\,50\%\)\]\/50 {
background-color: hsl(123 var(--foo) 50% / 0.5);
}
.bg-\[hsl\(var\(--foo\)\,50\%\,50\%\)\] {
background-color: hsl(var(--foo), 50%, 50%);
}
.bg-\[hsl\(var\(--foo\)\,50\%\,50\%\)\]\/50 {
background-color: hsl(var(--foo) 50% 50% / 0.5);
}
.bg-\[hsl\(var\(--foo\)\,var\(--bar\)\,var\(--baz\)\)\]\/50 {
background-color: hsl(var(--foo) var(--bar) var(--baz) / 0.5);
}
`)
})
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-red-500\/50 {
background-color: #ef444480;
}
`)
})
})
test('colors with slashes are matched first', async () => {
let config = {
content: [{ raw: html`<div class="bg-red-500/50"></div>` }],
theme: {
extend: {
colors: {
'red-500/50': '#ff0000',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-red-500\/50 {
--tw-bg-opacity: 1;
background-color: rgb(255 0 0 / var(--tw-bg-opacity));
}
`)
})
})
test('arbitrary color opacity modifier', async () => {
let config = {
content: [{ raw: html`<div class="bg-red-500/[var(--opacity)]"></div>` }],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-red-500\/\[var\(--opacity\)\] {
background-color: rgb(239 68 68 / var(--opacity));
}
`)
})
})
test('missing alpha generates nothing', async () => {
let config = {
content: [{ raw: html`<div class="bg-red-500/"></div>` }],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(``)
})
})
test('arbitrary color with opacity from scale', async () => {
let config = {
content: [{ raw: 'bg-[wheat]/50' }],
theme: {},
plugins: [],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-\[wheat\]\/50 {
background-color: #f5deb380;
}
`)
})
})
test('arbitrary color with arbitrary opacity', async () => {
let config = {
content: [{ raw: 'bg-[#bada55]/[0.2]' }],
theme: {},
plugins: [],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-\[\#bada55\]\/\[0\.2\] {
background-color: #bada5533;
}
`)
})
})
test('undefined theme color with opacity from scale', async () => {
let config = {
content: [{ raw: 'bg-garbage/50' }],
theme: {},
plugins: [],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(``)
})
})
test('values not in the opacity config are ignored', async () => {
let config = {
content: [{ raw: html`<div class="bg-red-500/29"></div>` }],
theme: {
opacity: {
0: '0',
25: '0.25',
5: '0.5',
75: '0.75',
100: '1',
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(``)
})
})
test('function colors are supported', async () => {
let config = {
content: [{ raw: html`<div class="bg-blue/50"></div>` }],
theme: {
colors: {
blue: ({ opacityValue }) => {
return `rgba(var(--colors-blue), ${opacityValue})`
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-blue\/50 {
background-color: rgba(var(--colors-blue), 0.5);
}
`)
})
})
test('utilities that support any type are supported', async () => {
let config = {
content: [
{
raw: html`
<div class="from-red-500/50"></div>
<div class="fill-red-500/25"></div>
<div class="placeholder-red-500/75"></div>
`,
},
],
theme: {
extend: {
fill: (theme) => theme('colors'),
},
},
plugins: [],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.from-red-500\/50 {
--tw-gradient-from: #ef444480 var(--tw-gradient-from-position);
--tw-gradient-to: #ef444400 var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.fill-red-500\/25 {
fill: #ef444440;
}
.placeholder-red-500\/75::placeholder {
color: #ef4444bf;
}
`)
})
})
test('opacity modifier in combination with partial custom properties', async () => {
let config = {
content: [
{
raw: html`
<div class="bg-[hsl(var(--foo),50%,50%)]"></div>
<div class="bg-[hsl(123,var(--foo),50%)]"></div>
<div class="bg-[hsl(123,50%,var(--foo))]"></div>
<div class="bg-[hsl(var(--foo),50%,50%)]/50"></div>
<div class="bg-[hsl(123,var(--foo),50%)]/50"></div>
<div class="bg-[hsl(123,50%,var(--foo))]/50"></div>
<div class="bg-[hsl(var(--foo),var(--bar),var(--baz))]/50"></div>
`,
},
],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-\[hsl\(123\,50\%\,var\(--foo\)\)\] {
--tw-bg-opacity: 1;
background-color: hsl(123 50% var(--foo) / var(--tw-bg-opacity));
}
.bg-\[hsl\(123\,50\%\,var\(--foo\)\)\]\/50 {
background-color: hsl(123 50% var(--foo) / 0.5);
}
.bg-\[hsl\(123\,var\(--foo\)\,50\%\)\] {
--tw-bg-opacity: 1;
background-color: hsl(123 var(--foo) 50% / var(--tw-bg-opacity));
}
.bg-\[hsl\(123\,var\(--foo\)\,50\%\)\]\/50 {
background-color: hsl(123 var(--foo) 50% / 0.5);
}
.bg-\[hsl\(var\(--foo\)\,50\%\,50\%\)\] {
--tw-bg-opacity: 1;
background-color: hsl(var(--foo) 50% 50% / var(--tw-bg-opacity));
}
.bg-\[hsl\(var\(--foo\)\,50\%\,50\%\)\]\/50 {
background-color: hsl(var(--foo) 50% 50% / 0.5);
}
.bg-\[hsl\(var\(--foo\)\,var\(--bar\)\,var\(--baz\)\)\]\/50 {
background-color: hsl(var(--foo) var(--bar) var(--baz) / 0.5);
}
`)
})
})

View file

@ -1,96 +1,93 @@
import { parseColor, formatColor } from '../src/util/color'
import { crosscheck } from './util/run'
crosscheck(() => {
describe('parseColor', () => {
it.each`
color | output
${'black'} | ${{ mode: 'rgb', color: ['0', '0', '0'], alpha: undefined }}
${'#0088cc'} | ${{ mode: 'rgb', color: ['0', '136', '204'], alpha: undefined }}
${'#08c'} | ${{ mode: 'rgb', color: ['0', '136', '204'], alpha: undefined }}
${'#0088cc99'} | ${{ mode: 'rgb', color: ['0', '136', '204'], alpha: '0.6' }}
${'#08c9'} | ${{ mode: 'rgb', color: ['0', '136', '204'], alpha: '0.6' }}
${'rgb(0, 30, 60)'} | ${{ mode: 'rgb', color: ['0', '30', '60'], alpha: undefined }}
${'rgba(0, 30, 60, 0.5)'} | ${{ mode: 'rgba', color: ['0', '30', '60'], alpha: '0.5' }}
${'rgb(0 30 60)'} | ${{ mode: 'rgb', color: ['0', '30', '60'], alpha: undefined }}
${'rgb(0 30 60 / 0.5)'} | ${{ mode: 'rgb', color: ['0', '30', '60'], alpha: '0.5' }}
${'rgb(var(--foo), 30, 60)'} | ${{ mode: 'rgb', color: ['var(--foo)', '30', '60'], alpha: undefined }}
${'rgb(0, var(--foo), 60)'} | ${{ mode: 'rgb', color: ['0', 'var(--foo)', '60'], alpha: undefined }}
${'rgb(0, 30, var(--foo))'} | ${{ mode: 'rgb', color: ['0', '30', 'var(--foo)'], alpha: undefined }}
${'rgb(0, 30, var(--foo), 0.5)'} | ${{ mode: 'rgb', color: ['0', '30', 'var(--foo)'], alpha: '0.5' }}
${'rgb(var(--foo), 30, var(--bar))'} | ${{ mode: 'rgb', color: ['var(--foo)', '30', 'var(--bar)'], alpha: undefined }}
${'rgb(var(--foo), var(--bar), var(--baz))'} | ${{ mode: 'rgb', color: ['var(--foo)', 'var(--bar)', 'var(--baz)'], alpha: undefined }}
${'rgb(var(--foo) 30 60)'} | ${{ mode: 'rgb', color: ['var(--foo)', '30', '60'], alpha: undefined }}
${'rgb(0 var(--foo) 60)'} | ${{ mode: 'rgb', color: ['0', 'var(--foo)', '60'], alpha: undefined }}
${'rgb(0 30 var(--foo))'} | ${{ mode: 'rgb', color: ['0', '30', 'var(--foo)'], alpha: undefined }}
${'rgb(0 30 var(--foo) / 0.5)'} | ${{ mode: 'rgb', color: ['0', '30', 'var(--foo)'], alpha: '0.5' }}
${'rgb(var(--foo) 30 var(--bar))'} | ${{ mode: 'rgb', color: ['var(--foo)', '30', 'var(--bar)'], alpha: undefined }}
${'rgb(var(--foo) var(--bar) var(--baz))'} | ${{ mode: 'rgb', color: ['var(--foo)', 'var(--bar)', 'var(--baz)'], alpha: undefined }}
${'hsl(0, 30%, 60%)'} | ${{ mode: 'hsl', color: ['0', '30%', '60%'], alpha: undefined }}
${'hsl(0deg, 30%, 60%)'} | ${{ mode: 'hsl', color: ['0deg', '30%', '60%'], alpha: undefined }}
${'hsl(0rad, 30%, 60%)'} | ${{ mode: 'hsl', color: ['0rad', '30%', '60%'], alpha: undefined }}
${'hsl(0grad, 30%, 60%)'} | ${{ mode: 'hsl', color: ['0grad', '30%', '60%'], alpha: undefined }}
${'hsl(0turn, 30%, 60%)'} | ${{ mode: 'hsl', color: ['0turn', '30%', '60%'], alpha: undefined }}
${'hsla(0, 30%, 60%, 0.5)'} | ${{ mode: 'hsla', color: ['0', '30%', '60%'], alpha: '0.5' }}
${'hsla(0deg, 30%, 60%, 0.5)'} | ${{ mode: 'hsla', color: ['0deg', '30%', '60%'], alpha: '0.5' }}
${'hsla(0rad, 30%, 60%, 0.5)'} | ${{ mode: 'hsla', color: ['0rad', '30%', '60%'], alpha: '0.5' }}
${'hsla(0grad, 30%, 60%, 0.5)'} | ${{ mode: 'hsla', color: ['0grad', '30%', '60%'], alpha: '0.5' }}
${'hsla(0turn, 30%, 60%, 0.5)'} | ${{ mode: 'hsla', color: ['0turn', '30%', '60%'], alpha: '0.5' }}
${'hsl(0 30% 60%)'} | ${{ mode: 'hsl', color: ['0', '30%', '60%'], alpha: undefined }}
${'hsl(0deg 30% 60%)'} | ${{ mode: 'hsl', color: ['0deg', '30%', '60%'], alpha: undefined }}
${'hsl(0rad 30% 60%)'} | ${{ mode: 'hsl', color: ['0rad', '30%', '60%'], alpha: undefined }}
${'hsl(0grad 30% 60%)'} | ${{ mode: 'hsl', color: ['0grad', '30%', '60%'], alpha: undefined }}
${'hsl(0turn 30% 60%)'} | ${{ mode: 'hsl', color: ['0turn', '30%', '60%'], alpha: undefined }}
${'hsl(0 30% 60% / 0.5)'} | ${{ mode: 'hsl', color: ['0', '30%', '60%'], alpha: '0.5' }}
${'hsl(0deg 30% 60% / 0.5)'} | ${{ mode: 'hsl', color: ['0deg', '30%', '60%'], alpha: '0.5' }}
${'hsl(0rad 30% 60% / 0.5)'} | ${{ mode: 'hsl', color: ['0rad', '30%', '60%'], alpha: '0.5' }}
${'hsl(0grad 30% 60% / 0.5)'} | ${{ mode: 'hsl', color: ['0grad', '30%', '60%'], alpha: '0.5' }}
${'hsl(0turn 30% 60% / 0.5)'} | ${{ mode: 'hsl', color: ['0turn', '30%', '60%'], alpha: '0.5' }}
${'hsl(var(--foo), 30%, 60%)'} | ${{ mode: 'hsl', color: ['var(--foo)', '30%', '60%'], alpha: undefined }}
${'hsl(0, var(--foo), 60%)'} | ${{ mode: 'hsl', color: ['0', 'var(--foo)', '60%'], alpha: undefined }}
${'hsl(0, 30%, var(--foo))'} | ${{ mode: 'hsl', color: ['0', '30%', 'var(--foo)'], alpha: undefined }}
${'hsl(0, 30%, var(--foo), 0.5)'} | ${{ mode: 'hsl', color: ['0', '30%', 'var(--foo)'], alpha: '0.5' }}
${'hsl(var(--foo), 30%, var(--bar))'} | ${{ mode: 'hsl', color: ['var(--foo)', '30%', 'var(--bar)'], alpha: undefined }}
${'hsl(var(--foo), var(--bar), var(--baz))'} | ${{ mode: 'hsl', color: ['var(--foo)', 'var(--bar)', 'var(--baz)'], alpha: undefined }}
${'hsl(var(--foo) 30% 60%)'} | ${{ mode: 'hsl', color: ['var(--foo)', '30%', '60%'], alpha: undefined }}
${'hsl(0 var(--foo) 60%)'} | ${{ mode: 'hsl', color: ['0', 'var(--foo)', '60%'], alpha: undefined }}
${'hsl(0 30% var(--foo))'} | ${{ mode: 'hsl', color: ['0', '30%', 'var(--foo)'], alpha: undefined }}
${'hsl(0 30% var(--foo) / 0.5)'} | ${{ mode: 'hsl', color: ['0', '30%', 'var(--foo)'], alpha: '0.5' }}
${'hsl(var(--foo) 30% var(--bar))'} | ${{ mode: 'hsl', color: ['var(--foo)', '30%', 'var(--bar)'], alpha: undefined }}
${'hsl(var(--foo) var(--bar) var(--baz))'} | ${{ mode: 'hsl', color: ['var(--foo)', 'var(--bar)', 'var(--baz)'], alpha: undefined }}
${'rgba(var(--foo), 0.1)'} | ${{ mode: 'rgba', color: ['var(--foo)'], alpha: '0.1' }}
${'rgba(var(--foo), var(--alpha))'} | ${{ mode: 'rgba', color: ['var(--foo)'], alpha: 'var(--alpha)' }}
${'hsla(var(--foo), 0.1)'} | ${{ mode: 'hsla', color: ['var(--foo)'], alpha: '0.1' }}
${'hsla(var(--foo), var(--alpha))'} | ${{ mode: 'hsla', color: ['var(--foo)'], alpha: 'var(--alpha)' }}
${'transparent'} | ${{ mode: 'rgb', color: ['0', '0', '0'], alpha: '0' }}
`('should parse "$color" to the correct value', ({ color, output }) => {
expect(parseColor(color)).toEqual(output)
})
it.each`
color
${'var(--my-color)'}
${'currentColor'}
${'inherit'}
${'initial'}
${'revert'}
${'unset'}
`('should return `null` for unparseable color "$color"', ({ color }) => {
expect(parseColor(color)).toBe(null)
})
describe('parseColor', () => {
it.each`
color | output
${'black'} | ${{ mode: 'rgb', color: ['0', '0', '0'], alpha: undefined }}
${'#0088cc'} | ${{ mode: 'rgb', color: ['0', '136', '204'], alpha: undefined }}
${'#08c'} | ${{ mode: 'rgb', color: ['0', '136', '204'], alpha: undefined }}
${'#0088cc99'} | ${{ mode: 'rgb', color: ['0', '136', '204'], alpha: '0.6' }}
${'#08c9'} | ${{ mode: 'rgb', color: ['0', '136', '204'], alpha: '0.6' }}
${'rgb(0, 30, 60)'} | ${{ mode: 'rgb', color: ['0', '30', '60'], alpha: undefined }}
${'rgba(0, 30, 60, 0.5)'} | ${{ mode: 'rgba', color: ['0', '30', '60'], alpha: '0.5' }}
${'rgb(0 30 60)'} | ${{ mode: 'rgb', color: ['0', '30', '60'], alpha: undefined }}
${'rgb(0 30 60 / 0.5)'} | ${{ mode: 'rgb', color: ['0', '30', '60'], alpha: '0.5' }}
${'rgb(var(--foo), 30, 60)'} | ${{ mode: 'rgb', color: ['var(--foo)', '30', '60'], alpha: undefined }}
${'rgb(0, var(--foo), 60)'} | ${{ mode: 'rgb', color: ['0', 'var(--foo)', '60'], alpha: undefined }}
${'rgb(0, 30, var(--foo))'} | ${{ mode: 'rgb', color: ['0', '30', 'var(--foo)'], alpha: undefined }}
${'rgb(0, 30, var(--foo), 0.5)'} | ${{ mode: 'rgb', color: ['0', '30', 'var(--foo)'], alpha: '0.5' }}
${'rgb(var(--foo), 30, var(--bar))'} | ${{ mode: 'rgb', color: ['var(--foo)', '30', 'var(--bar)'], alpha: undefined }}
${'rgb(var(--foo), var(--bar), var(--baz))'} | ${{ mode: 'rgb', color: ['var(--foo)', 'var(--bar)', 'var(--baz)'], alpha: undefined }}
${'rgb(var(--foo) 30 60)'} | ${{ mode: 'rgb', color: ['var(--foo)', '30', '60'], alpha: undefined }}
${'rgb(0 var(--foo) 60)'} | ${{ mode: 'rgb', color: ['0', 'var(--foo)', '60'], alpha: undefined }}
${'rgb(0 30 var(--foo))'} | ${{ mode: 'rgb', color: ['0', '30', 'var(--foo)'], alpha: undefined }}
${'rgb(0 30 var(--foo) / 0.5)'} | ${{ mode: 'rgb', color: ['0', '30', 'var(--foo)'], alpha: '0.5' }}
${'rgb(var(--foo) 30 var(--bar))'} | ${{ mode: 'rgb', color: ['var(--foo)', '30', 'var(--bar)'], alpha: undefined }}
${'rgb(var(--foo) var(--bar) var(--baz))'} | ${{ mode: 'rgb', color: ['var(--foo)', 'var(--bar)', 'var(--baz)'], alpha: undefined }}
${'hsl(0, 30%, 60%)'} | ${{ mode: 'hsl', color: ['0', '30%', '60%'], alpha: undefined }}
${'hsl(0deg, 30%, 60%)'} | ${{ mode: 'hsl', color: ['0deg', '30%', '60%'], alpha: undefined }}
${'hsl(0rad, 30%, 60%)'} | ${{ mode: 'hsl', color: ['0rad', '30%', '60%'], alpha: undefined }}
${'hsl(0grad, 30%, 60%)'} | ${{ mode: 'hsl', color: ['0grad', '30%', '60%'], alpha: undefined }}
${'hsl(0turn, 30%, 60%)'} | ${{ mode: 'hsl', color: ['0turn', '30%', '60%'], alpha: undefined }}
${'hsla(0, 30%, 60%, 0.5)'} | ${{ mode: 'hsla', color: ['0', '30%', '60%'], alpha: '0.5' }}
${'hsla(0deg, 30%, 60%, 0.5)'} | ${{ mode: 'hsla', color: ['0deg', '30%', '60%'], alpha: '0.5' }}
${'hsla(0rad, 30%, 60%, 0.5)'} | ${{ mode: 'hsla', color: ['0rad', '30%', '60%'], alpha: '0.5' }}
${'hsla(0grad, 30%, 60%, 0.5)'} | ${{ mode: 'hsla', color: ['0grad', '30%', '60%'], alpha: '0.5' }}
${'hsla(0turn, 30%, 60%, 0.5)'} | ${{ mode: 'hsla', color: ['0turn', '30%', '60%'], alpha: '0.5' }}
${'hsl(0 30% 60%)'} | ${{ mode: 'hsl', color: ['0', '30%', '60%'], alpha: undefined }}
${'hsl(0deg 30% 60%)'} | ${{ mode: 'hsl', color: ['0deg', '30%', '60%'], alpha: undefined }}
${'hsl(0rad 30% 60%)'} | ${{ mode: 'hsl', color: ['0rad', '30%', '60%'], alpha: undefined }}
${'hsl(0grad 30% 60%)'} | ${{ mode: 'hsl', color: ['0grad', '30%', '60%'], alpha: undefined }}
${'hsl(0turn 30% 60%)'} | ${{ mode: 'hsl', color: ['0turn', '30%', '60%'], alpha: undefined }}
${'hsl(0 30% 60% / 0.5)'} | ${{ mode: 'hsl', color: ['0', '30%', '60%'], alpha: '0.5' }}
${'hsl(0deg 30% 60% / 0.5)'} | ${{ mode: 'hsl', color: ['0deg', '30%', '60%'], alpha: '0.5' }}
${'hsl(0rad 30% 60% / 0.5)'} | ${{ mode: 'hsl', color: ['0rad', '30%', '60%'], alpha: '0.5' }}
${'hsl(0grad 30% 60% / 0.5)'} | ${{ mode: 'hsl', color: ['0grad', '30%', '60%'], alpha: '0.5' }}
${'hsl(0turn 30% 60% / 0.5)'} | ${{ mode: 'hsl', color: ['0turn', '30%', '60%'], alpha: '0.5' }}
${'hsl(var(--foo), 30%, 60%)'} | ${{ mode: 'hsl', color: ['var(--foo)', '30%', '60%'], alpha: undefined }}
${'hsl(0, var(--foo), 60%)'} | ${{ mode: 'hsl', color: ['0', 'var(--foo)', '60%'], alpha: undefined }}
${'hsl(0, 30%, var(--foo))'} | ${{ mode: 'hsl', color: ['0', '30%', 'var(--foo)'], alpha: undefined }}
${'hsl(0, 30%, var(--foo), 0.5)'} | ${{ mode: 'hsl', color: ['0', '30%', 'var(--foo)'], alpha: '0.5' }}
${'hsl(var(--foo), 30%, var(--bar))'} | ${{ mode: 'hsl', color: ['var(--foo)', '30%', 'var(--bar)'], alpha: undefined }}
${'hsl(var(--foo), var(--bar), var(--baz))'} | ${{ mode: 'hsl', color: ['var(--foo)', 'var(--bar)', 'var(--baz)'], alpha: undefined }}
${'hsl(var(--foo) 30% 60%)'} | ${{ mode: 'hsl', color: ['var(--foo)', '30%', '60%'], alpha: undefined }}
${'hsl(0 var(--foo) 60%)'} | ${{ mode: 'hsl', color: ['0', 'var(--foo)', '60%'], alpha: undefined }}
${'hsl(0 30% var(--foo))'} | ${{ mode: 'hsl', color: ['0', '30%', 'var(--foo)'], alpha: undefined }}
${'hsl(0 30% var(--foo) / 0.5)'} | ${{ mode: 'hsl', color: ['0', '30%', 'var(--foo)'], alpha: '0.5' }}
${'hsl(var(--foo) 30% var(--bar))'} | ${{ mode: 'hsl', color: ['var(--foo)', '30%', 'var(--bar)'], alpha: undefined }}
${'hsl(var(--foo) var(--bar) var(--baz))'} | ${{ mode: 'hsl', color: ['var(--foo)', 'var(--bar)', 'var(--baz)'], alpha: undefined }}
${'rgba(var(--foo), 0.1)'} | ${{ mode: 'rgba', color: ['var(--foo)'], alpha: '0.1' }}
${'rgba(var(--foo), var(--alpha))'} | ${{ mode: 'rgba', color: ['var(--foo)'], alpha: 'var(--alpha)' }}
${'hsla(var(--foo), 0.1)'} | ${{ mode: 'hsla', color: ['var(--foo)'], alpha: '0.1' }}
${'hsla(var(--foo), var(--alpha))'} | ${{ mode: 'hsla', color: ['var(--foo)'], alpha: 'var(--alpha)' }}
${'transparent'} | ${{ mode: 'rgb', color: ['0', '0', '0'], alpha: '0' }}
`('should parse "$color" to the correct value', ({ color, output }) => {
expect(parseColor(color)).toEqual(output)
})
describe('formatColor', () => {
it.each`
color | output
${{ mode: 'rgb', color: ['0', '0', '0'], alpha: undefined }} | ${'rgb(0 0 0)'}
${{ mode: 'rgb', color: ['0', '136', '204'], alpha: undefined }} | ${'rgb(0 136 204)'}
${{ mode: 'rgb', color: ['0', '136', '204'], alpha: '0.6' }} | ${'rgb(0 136 204 / 0.6)'}
${{ mode: 'hsl', color: ['0', '0%', '0%'], alpha: undefined }} | ${'hsl(0 0% 0%)'}
${{ mode: 'hsl', color: ['0', '136%', '204%'], alpha: undefined }} | ${'hsl(0 136% 204%)'}
${{ mode: 'hsl', color: ['0', '136%', '204%'], alpha: '0.6' }} | ${'hsl(0 136% 204% / 0.6)'}
`('should format the color pieces into a proper "$output"', ({ color, output }) => {
expect(formatColor(color)).toEqual(output)
})
it.each`
color
${'var(--my-color)'}
${'currentColor'}
${'inherit'}
${'initial'}
${'revert'}
${'unset'}
`('should return `null` for unparseable color "$color"', ({ color }) => {
expect(parseColor(color)).toBe(null)
})
})
describe('formatColor', () => {
it.each`
color | output
${{ mode: 'rgb', color: ['0', '0', '0'], alpha: undefined }} | ${'rgb(0 0 0)'}
${{ mode: 'rgb', color: ['0', '136', '204'], alpha: undefined }} | ${'rgb(0 136 204)'}
${{ mode: 'rgb', color: ['0', '136', '204'], alpha: '0.6' }} | ${'rgb(0 136 204 / 0.6)'}
${{ mode: 'hsl', color: ['0', '0%', '0%'], alpha: undefined }} | ${'hsl(0 0% 0%)'}
${{ mode: 'hsl', color: ['0', '136%', '204%'], alpha: undefined }} | ${'hsl(0 136% 204%)'}
${{ mode: 'hsl', color: ['0', '136%', '204%'], alpha: '0.6' }} | ${'hsl(0 136% 204% / 0.6)'}
`('should format the color pieces into a proper "$output"', ({ color, output }) => {
expect(formatColor(color)).toEqual(output)
})
})

View file

@ -1,76 +1,74 @@
import { crosscheck, run, html, css, defaults } from './util/run'
import { run, html, css, defaults } from './util/run'
crosscheck(() => {
it('should generate the partial selector, if only a partial is used (base layer)', () => {
let config = {
content: [{ raw: html`<div></div>` }],
corePlugins: { preflight: false },
}
it('should generate the partial selector, if only a partial is used (base layer)', () => {
let config = {
content: [{ raw: html`<div></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
let input = css`
@tailwind base;
@layer base {
:root {
font-weight: bold;
}
/* --- */
:root,
.a {
color: black;
}
@layer base {
:root {
font-weight: bold;
}
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
:root {
font-weight: bold;
}
:root,
.a {
color: #000;
}
${defaults}
`)
})
})
/* --- */
it('should generate the partial selector, if only a partial is used (utilities layer)', () => {
let config = {
content: [{ raw: html`<div class="a"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind utilities;
@layer utilities {
:root {
font-weight: bold;
}
/* --- */
:root,
.a {
color: black;
}
:root,
.a {
color: black;
}
`
}
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
:root {
font-weight: bold;
}
:root,
.a {
color: #000;
}
`)
})
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
:root {
font-weight: bold;
}
:root,
.a {
color: #000;
}
${defaults}
`)
})
})
it('should generate the partial selector, if only a partial is used (utilities layer)', () => {
let config = {
content: [{ raw: html`<div class="a"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind utilities;
@layer utilities {
:root {
font-weight: bold;
}
/* --- */
:root,
.a {
color: black;
}
}
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
:root {
font-weight: bold;
}
:root,
.a {
color: #000;
}
`)
})
})

View file

@ -1,28 +1,25 @@
import configurePlugins from '../src/util/configurePlugins'
import { crosscheck } from './util/run'
crosscheck(() => {
test('setting a plugin to false removes it', () => {
const plugins = ['fontSize', 'display', 'backgroundPosition']
test('setting a plugin to false removes it', () => {
const plugins = ['fontSize', 'display', 'backgroundPosition']
const configuredPlugins = configurePlugins({ display: false }, plugins)
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 safelists plugins', () => {
const plugins = ['fontSize', 'display', 'backgroundPosition']
const configuredPlugins = configurePlugins(['display'], plugins)
expect(configuredPlugins).toEqual(['display'])
})
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 safelists plugins', () => {
const plugins = ['fontSize', 'display', 'backgroundPosition']
const configuredPlugins = configurePlugins(['display'], plugins)
expect(configuredPlugins).toEqual(['display'])
})

View file

@ -1,347 +1,345 @@
import { crosscheck, run, html, css } from './util/run'
import { run, html, css } from './util/run'
crosscheck(({}) => {
test('options are not required', () => {
let config = { content: [{ raw: html`<div class="container"></div>` }] }
test('options are not required', () => {
let config = { content: [{ raw: html`<div class="container"></div>` }] }
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
}
@media (min-width: 640px) {
.container {
width: 100%;
max-width: 640px;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
`)
})
})
test('screens can be passed explicitly', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
screens: ['400px', '500px'],
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
}
@media (min-width: 768px) {
.container {
width: 100%;
max-width: 768px;
}
@media (min-width: 400px) {
.container {
max-width: 400px;
}
}
@media (min-width: 500px) {
.container {
max-width: 500px;
}
}
`)
})
})
test('screens are ordered ascending by min-width', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
screens: ['500px', '400px'],
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
}
@media (min-width: 1024px) {
.container {
width: 100%;
max-width: 1024px;
}
@media (min-width: 400px) {
.container {
max-width: 400px;
}
}
@media (min-width: 500px) {
.container {
max-width: 500px;
}
}
`)
})
})
test('screens are deduplicated by min-width', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
screens: {
sm: '576px',
md: '768px',
'sm-only': { min: '576px', max: '767px' },
},
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
}
@media (min-width: 1280px) {
.container {
width: 100%;
max-width: 1280px;
}
@media (min-width: 576px) {
.container {
max-width: 576px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
`)
})
})
test('the container can be centered by default', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
center: true,
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
}
@media (min-width: 1536px) {
.container {
width: 100%;
margin-left: auto;
margin-right: auto;
max-width: 1536px;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
`)
})
})
test('horizontal padding can be included by default', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
padding: '2rem',
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
padding-left: 2rem;
padding-right: 2rem;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
`)
})
})
test('responsive horizontal padding can be included by default', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
screens: {
sm: '576px',
md: { min: '768px' },
lg: { 'min-width': '992px' },
xl: { min: '1200px', max: '1600px' },
},
container: {
padding: {
DEFAULT: '1rem',
sm: '2rem',
lg: '4rem',
xl: '5rem',
},
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
padding-left: 1rem;
padding-right: 1rem;
}
@media (min-width: 576px) {
.container {
max-width: 576px;
padding-left: 2rem;
padding-right: 2rem;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 992px) {
.container {
max-width: 992px;
padding-left: 4rem;
padding-right: 4rem;
}
}
@media (min-width: 1200px) {
.container {
max-width: 1200px;
padding-left: 5rem;
padding-right: 5rem;
}
}
`)
})
})
test('setting all options at once', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
screens: ['400px', '500px'],
center: true,
padding: '2rem',
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
margin-left: auto;
margin-right: auto;
padding-left: 2rem;
padding-right: 2rem;
}
@media (min-width: 400px) {
.container {
max-width: 400px;
}
}
@media (min-width: 500px) {
.container {
max-width: 500px;
}
}
`)
})
})
test('container can use variants', () => {
let config = {
content: [{ raw: html`<div class="lg:hover:container"></div>` }],
theme: {
container: {
screens: ['400px', '500px'],
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 1024px) {
.lg\:hover\:container:hover {
width: 100%;
}
@media (min-width: 400px) {
.lg\:hover\:container:hover {
max-width: 400px;
}
}
@media (min-width: 500px) {
.lg\:hover\:container:hover {
max-width: 500px;
}
}
}
`)
})
}
`)
})
})
test('screens can be passed explicitly', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
screens: ['400px', '500px'],
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
}
@media (min-width: 400px) {
.container {
max-width: 400px;
}
}
@media (min-width: 500px) {
.container {
max-width: 500px;
}
}
`)
})
})
test('screens are ordered ascending by min-width', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
screens: ['500px', '400px'],
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
}
@media (min-width: 400px) {
.container {
max-width: 400px;
}
}
@media (min-width: 500px) {
.container {
max-width: 500px;
}
}
`)
})
})
test('screens are deduplicated by min-width', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
screens: {
sm: '576px',
md: '768px',
'sm-only': { min: '576px', max: '767px' },
},
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
}
@media (min-width: 576px) {
.container {
max-width: 576px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
`)
})
})
test('the container can be centered by default', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
center: true,
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
margin-left: auto;
margin-right: auto;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
`)
})
})
test('horizontal padding can be included by default', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
padding: '2rem',
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
padding-left: 2rem;
padding-right: 2rem;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
`)
})
})
test('responsive horizontal padding can be included by default', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
screens: {
sm: '576px',
md: { min: '768px' },
lg: { 'min-width': '992px' },
xl: { min: '1200px', max: '1600px' },
},
container: {
padding: {
DEFAULT: '1rem',
sm: '2rem',
lg: '4rem',
xl: '5rem',
},
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
padding-left: 1rem;
padding-right: 1rem;
}
@media (min-width: 576px) {
.container {
max-width: 576px;
padding-left: 2rem;
padding-right: 2rem;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 992px) {
.container {
max-width: 992px;
padding-left: 4rem;
padding-right: 4rem;
}
}
@media (min-width: 1200px) {
.container {
max-width: 1200px;
padding-left: 5rem;
padding-right: 5rem;
}
}
`)
})
})
test('setting all options at once', () => {
let config = {
content: [{ raw: html`<div class="container"></div>` }],
theme: {
container: {
screens: ['400px', '500px'],
center: true,
padding: '2rem',
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.container {
width: 100%;
margin-left: auto;
margin-right: auto;
padding-left: 2rem;
padding-right: 2rem;
}
@media (min-width: 400px) {
.container {
max-width: 400px;
}
}
@media (min-width: 500px) {
.container {
max-width: 500px;
}
}
`)
})
})
test('container can use variants', () => {
let config = {
content: [{ raw: html`<div class="lg:hover:container"></div>` }],
theme: {
container: {
screens: ['400px', '500px'],
},
},
}
return run('@tailwind components', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 1024px) {
.lg\:hover\:container:hover {
width: 100%;
}
@media (min-width: 400px) {
.lg\:hover\:container:hover {
max-width: 400px;
}
}
@media (min-width: 500px) {
.lg\:hover\:container:hover {
max-width: 500px;
}
}
}
`)
})
})

View file

@ -1,4 +1,4 @@
import { crosscheck, css } from './util/run'
import { css } from './util/run'
const fs = require('fs')
const path = require('path')
@ -7,126 +7,122 @@ const tailwind = require('../src/index.js')
const sharedState = require('../src/lib/sharedState.js')
const configPath = path.resolve(__dirname, './context-reuse.tailwind.config.js')
crosscheck(({ stable, oxide }) => {
function run(input, config = {}, from = null) {
let { currentTestName } = expect.getState()
function run(input, config = {}, from = null) {
let { currentTestName } = expect.getState()
from = `${path.resolve(__filename)}?test=${currentTestName}&${from}`
from = `${path.resolve(__filename)}?test=${currentTestName}&${from}`
return postcss(tailwind(config)).process(input, { from })
return postcss(tailwind(config)).process(input, { from })
}
beforeEach(async () => {
let config = {
content: [path.resolve(__dirname, './context-reuse.test.html')],
corePlugins: { preflight: false },
}
beforeEach(async () => {
let config = {
content: [path.resolve(__dirname, './context-reuse.test.html')],
corePlugins: { preflight: false },
await fs.promises.writeFile(configPath, `module.exports = ${JSON.stringify(config)};`)
})
afterEach(async () => {
await fs.promises.unlink(configPath)
})
test('re-uses the context across multiple files with the same config', async () => {
let results = [
await run(`@tailwind utilities;`, configPath, `id=1`),
// Using @apply directives should still re-use the context
// They depend on the config but do not the other way around
await run(`body { @apply bg-blue-400; }`, configPath, `id=2`),
await run(`body { @apply text-red-400; }`, configPath, `id=3`),
await run(`body { @apply mb-4; }`, configPath, `id=4`),
]
let dependencies = results.map((result) => {
return result.messages
.filter((message) => message.type === 'dependency')
.map((message) => message.file)
})
// The content files don't have any utilities in them so this should be empty
expect(results[0].css).toMatchFormattedCss(css``)
// However, @apply is being used so we want to verify that they're being inlined into the CSS rules
expect(results[1].css).toMatchFormattedCss(css`
body {
--tw-bg-opacity: 1;
background-color: rgb(96 165 250 / var(--tw-bg-opacity));
}
`)
await fs.promises.writeFile(configPath, `module.exports = ${JSON.stringify(config)};`)
})
expect(results[2].css).toMatchFormattedCss(css`
body {
--tw-text-opacity: 1;
color: rgb(248 113 113 / var(--tw-text-opacity));
}
`)
afterEach(async () => {
await fs.promises.unlink(configPath)
})
expect(results[3].css).toMatchFormattedCss(css`
body {
margin-bottom: 1rem;
}
`)
oxide.test.todo('re-uses the context across multiple files with the same config')
stable.test('re-uses the context across multiple files with the same config', async () => {
let results = [
await run(`@tailwind utilities;`, configPath, `id=1`),
// Files with @tailwind directives depends on the PostCSS tree, config, AND any content files
expect(dependencies[0]).toEqual([
path.resolve(__dirname, 'context-reuse.test.html'),
path.resolve(__dirname, 'context-reuse.tailwind.config.js'),
])
// Using @apply directives should still re-use the context
// They depend on the config but do not the other way around
await run(`body { @apply bg-blue-400; }`, configPath, `id=2`),
await run(`body { @apply text-red-400; }`, configPath, `id=3`),
await run(`body { @apply mb-4; }`, configPath, `id=4`),
]
// @apply depends only on the containing PostCSS tree *and* the config file but no content files
// as they cannot affect the outcome of the @apply directives
expect(dependencies[1]).toEqual([path.resolve(__dirname, 'context-reuse.tailwind.config.js')])
let dependencies = results.map((result) => {
return result.messages
.filter((message) => message.type === 'dependency')
.map((message) => message.file)
})
expect(dependencies[2]).toEqual([path.resolve(__dirname, 'context-reuse.tailwind.config.js')])
// The content files don't have any utilities in them so this should be empty
expect(results[0].css).toMatchFormattedCss(css``)
expect(dependencies[3]).toEqual([path.resolve(__dirname, 'context-reuse.tailwind.config.js')])
// However, @apply is being used so we want to verify that they're being inlined into the CSS rules
expect(results[1].css).toMatchFormattedCss(css`
body {
--tw-bg-opacity: 1;
background-color: rgb(96 165 250 / var(--tw-bg-opacity));
}
`)
// And none of this should have resulted in multiple contexts being created
expect(sharedState.contextSourcesMap.size).toBe(1)
})
expect(results[2].css).toMatchFormattedCss(css`
body {
--tw-text-opacity: 1;
color: rgb(248 113 113 / var(--tw-text-opacity));
}
`)
test('updates layers when any CSS containing @tailwind directives changes', async () => {
let result
expect(results[3].css).toMatchFormattedCss(css`
body {
margin-bottom: 1rem;
}
`)
// Files with @tailwind directives depends on the PostCSS tree, config, AND any content files
expect(dependencies[0]).toEqual([
path.resolve(__dirname, 'context-reuse.test.html'),
path.resolve(__dirname, 'context-reuse.tailwind.config.js'),
])
// @apply depends only on the containing PostCSS tree *and* the config file but no content files
// as they cannot affect the outcome of the @apply directives
expect(dependencies[1]).toEqual([path.resolve(__dirname, 'context-reuse.tailwind.config.js')])
expect(dependencies[2]).toEqual([path.resolve(__dirname, 'context-reuse.tailwind.config.js')])
expect(dependencies[3]).toEqual([path.resolve(__dirname, 'context-reuse.tailwind.config.js')])
// And none of this should have resulted in multiple contexts being created
expect(sharedState.contextSourcesMap.size).toBe(1)
})
oxide.test.todo('updates layers when any CSS containing @tailwind directives changes')
stable.test('updates layers when any CSS containing @tailwind directives changes', async () => {
let result
// Compile the initial version once
let input = css`
@tailwind utilities;
@layer utilities {
.custom-utility {
color: orange;
}
}
`
result = await run(input, configPath, `id=1`)
expect(result.css).toMatchFormattedCss(css`
.only\:custom-utility:only-child {
// Compile the initial version once
let input = css`
@tailwind utilities;
@layer utilities {
.custom-utility {
color: orange;
}
`)
}
`
// Save the file with a change
input = css`
@tailwind utilities;
@layer utilities {
.custom-utility {
color: blue;
}
result = await run(input, configPath, `id=1`)
expect(result.css).toMatchFormattedCss(css`
.only\:custom-utility:only-child {
color: orange;
}
`)
// Save the file with a change
input = css`
@tailwind utilities;
@layer utilities {
.custom-utility {
color: blue;
}
`
}
`
result = await run(input, configPath, `id=1`)
result = await run(input, configPath, `id=1`)
expect(result.css).toMatchFormattedCss(css`
.only\:custom-utility:only-child {
color: #00f;
}
`)
})
expect(result.css).toMatchFormattedCss(css`
.only\:custom-utility:only-child {
color: #00f;
}
`)
})

View file

@ -1,4 +1,4 @@
import { crosscheck, run, html, css } from './util/run'
import { run, html, css } from './util/run'
function customExtractor(content) {
let matches = content.match(/class="([^"]+)"/)
@ -21,117 +21,109 @@ let expected = css`
}
`
crosscheck(({ stable, oxide }) => {
describe('modern', () => {
oxide.test.todo('extract.DEFAULT')
stable.test('extract.DEFAULT', () => {
let config = {
content: {
files: [{ raw: sharedHtml }],
extract: {
DEFAULT: customExtractor,
},
describe('modern', () => {
test('extract.DEFAULT', () => {
let config = {
content: {
files: [{ raw: sharedHtml }],
extract: {
DEFAULT: customExtractor,
},
}
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(expected)
})
})
oxide.test.todo('extract.{extension}')
stable.test('extract.{extension}', () => {
let config = {
content: {
files: [{ raw: sharedHtml }],
extract: {
html: customExtractor,
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(expected)
})
})
oxide.test.todo('extract function')
stable.test('extract function', () => {
let config = {
content: {
files: [{ raw: sharedHtml }],
extract: customExtractor,
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(expected)
})
})
oxide.test.todo('raw content with extension')
stable.test('raw content with extension', () => {
let config = {
content: {
files: [
{
raw: sharedHtml,
extension: 'html',
},
],
extract: {
html: () => ['invisible'],
},
},
corePlugins: { preflight: false },
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.invisible {
visibility: hidden;
}
`)
})
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(expected)
})
})
describe('legacy', () => {
oxide.test.todo('defaultExtractor')
stable.test('defaultExtractor', () => {
let config = {
content: {
files: [{ raw: sharedHtml }],
options: {
defaultExtractor: customExtractor,
},
test('extract.{extension}', () => {
let config = {
content: {
files: [{ raw: sharedHtml }],
extract: {
html: customExtractor,
},
}
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(expected)
})
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(expected)
})
})
oxide.test.todo('extractors array')
stable.test('extractors array', () => {
let config = {
content: {
files: [{ raw: sharedHtml }],
options: {
extractors: [
{
extractor: customExtractor,
extensions: ['html'],
},
],
test('extract function', () => {
let config = {
content: {
files: [{ raw: sharedHtml }],
extract: customExtractor,
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(expected)
})
})
test('raw content with extension', () => {
let config = {
content: {
files: [
{
raw: sharedHtml,
extension: 'html',
},
],
extract: {
html: () => ['invisible'],
},
}
},
corePlugins: { preflight: false },
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(expected)
})
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.invisible {
visibility: hidden;
}
`)
})
})
})
describe('legacy', () => {
test('defaultExtractor', () => {
let config = {
content: {
files: [{ raw: sharedHtml }],
options: {
defaultExtractor: customExtractor,
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(expected)
})
})
test('extractors array', () => {
let config = {
content: {
files: [{ raw: sharedHtml }],
options: {
extractors: [
{
extractor: customExtractor,
extensions: ['html'],
},
],
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(expected)
})
})
})

File diff suppressed because it is too large Load diff

View file

@ -1,57 +1,55 @@
import { crosscheck, run, html, css } from './util/run'
import { run, html, css } from './util/run'
crosscheck(() => {
test('custom separator', () => {
let config = {
darkMode: 'class',
content: [
{
raw: html`
<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>
`,
},
],
separator: '_',
}
test('custom separator', () => {
let config = {
darkMode: 'class',
content: [
{
raw: html`
<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>
`,
},
],
separator: '_',
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.group:hover .group-hover_focus-within_text-left:focus-within {
text-align: left;
}
:is([dir='rtl'] .rtl_active_text-center:active) {
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.group:hover .group-hover_focus-within_text-left:focus-within {
text-align: left;
}
:is([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;
}
@media (prefers-reduced-motion: no-preference) {
.motion-safe_hover_text-center:hover {
text-align: center;
}
}
:is(.dark .dark_focus_text-left:focus) {
text-align: left;
}
@media (min-width: 768px) {
.md_hover_text-right:hover {
text-align: right;
}
:is(.dark .dark_focus_text-left:focus) {
text-align: left;
}
@media (min-width: 768px) {
.md_hover_text-right:hover {
text-align: right;
}
}
`)
})
})
test('dash is not supported', () => {
let config = {
darkMode: 'class',
content: [{ raw: 'lg-hover-font-bold' }],
separator: '-',
}
return expect(run('@tailwind utilities', config)).rejects.toThrowError(
"The '-' character cannot be used as a custom separator in JIT mode due to parsing ambiguity. Please use another character like '_' instead."
)
}
`)
})
})
test('dash is not supported', () => {
let config = {
darkMode: 'class',
content: [{ raw: 'lg-hover-font-bold' }],
separator: '-',
}
return expect(run('@tailwind utilities', config)).rejects.toThrowError(
"The '-' character cannot be used as a custom separator in JIT mode due to parsing ambiguity. Please use another character like '_' instead."
)
})

View file

@ -1,72 +1,67 @@
import { crosscheck, run, html, css } from './util/run'
import { run, html, css } from './util/run'
function customTransformer(content) {
return content.replace(/uppercase/g, 'lowercase')
}
crosscheck(({ stable, oxide }) => {
oxide.test.todo('transform function')
stable.test('transform function', () => {
let config = {
content: {
files: [{ raw: html`<div class="uppercase"></div>` }],
transform: customTransformer,
},
}
test('transform function', () => {
let config = {
content: {
files: [{ raw: html`<div class="uppercase"></div>` }],
transform: customTransformer,
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.lowercase {
text-transform: lowercase;
}
`)
})
})
oxide.test.todo('transform.DEFAULT')
stable.test('transform.DEFAULT', () => {
let config = {
content: {
files: [{ raw: html`<div class="uppercase"></div>` }],
transform: {
DEFAULT: customTransformer,
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.lowercase {
text-transform: lowercase;
}
`)
})
})
oxide.test.todo('transform.{extension}')
stable.test('transform.{extension}', () => {
let config = {
content: {
files: [
{ raw: 'blah blah blah', extension: 'html' },
{ raw: 'blah blah blah', extension: 'php' },
],
transform: {
html: () => 'uppercase',
php: () => 'lowercase',
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.uppercase {
text-transform: uppercase;
}
.lowercase {
text-transform: lowercase;
}
`)
})
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.lowercase {
text-transform: lowercase;
}
`)
})
})
test('transform.DEFAULT', () => {
let config = {
content: {
files: [{ raw: html`<div class="uppercase"></div>` }],
transform: {
DEFAULT: customTransformer,
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.lowercase {
text-transform: lowercase;
}
`)
})
})
test('transform.{extension}', () => {
let config = {
content: {
files: [
{ raw: 'blah blah blah', extension: 'html' },
{ raw: 'blah blah blah', extension: 'php' },
],
transform: {
html: () => 'uppercase',
php: () => 'lowercase',
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.uppercase {
text-transform: uppercase;
}
.lowercase {
text-transform: lowercase;
}
`)
})
})

View file

@ -2,37 +2,100 @@ import fs from 'fs'
import path from 'path'
import inTempDirectory from '../jest/runInTempDirectory'
import { crosscheck, run, html, css, javascript } from './util/run'
import { run, html, css, javascript } from './util/run'
const defaultConfigFile = 'tailwind.config.js'
const cjsConfigFile = 'tailwind.config.cjs'
crosscheck(() => {
test('it uses the values from the custom config file', () => {
let config = require(path.resolve(`${__dirname}/fixtures/custom-config.js`))
test('it uses the values from the custom config file', () => {
let config = require(path.resolve(`${__dirname}/fixtures/custom-config.js`))
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
}
`)
})
}
`)
})
})
test('custom config can be passed as an object', () => {
let config = {
test('custom config can be passed as an object', () => {
let config = {
content: [{ raw: html`<div class="mobile:font-bold"></div>` }],
theme: {
screens: {
mobile: '400px',
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
}
}
`)
})
})
test('custom config path can be passed using `config` property in an object', () => {
let config = {
config: path.resolve(`${__dirname}/fixtures/custom-config.js`),
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
}
}
`)
})
})
test('custom config can be passed under the `config` property', () => {
let config = {
config: {
content: [{ raw: html`<div class="mobile:font-bold"></div>` }],
theme: {
screens: {
mobile: '400px',
},
},
}
},
}
return run('@tailwind utilities', config).then((result) => {
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
}
}
`)
})
})
test('tailwind.config.cjs is picked up by default', () => {
return inTempDirectory(() => {
fs.writeFileSync(
path.resolve(cjsConfigFile),
javascript`module.exports = {
content: [{ raw: '<div class="mobile:font-bold"></div>' }],
theme: {
screens: {
mobile: '400px',
},
},
}`
)
return run('@tailwind utilities').then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
@ -42,349 +105,284 @@ crosscheck(() => {
`)
})
})
test('custom config path can be passed using `config` property in an object', () => {
let config = {
config: path.resolve(`${__dirname}/fixtures/custom-config.js`),
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
}
}
`)
})
})
test('custom config can be passed under the `config` property', () => {
let config = {
config: {
content: [{ raw: html`<div class="mobile:font-bold"></div>` }],
theme: {
screens: {
mobile: '400px',
},
},
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
}
}
`)
})
})
test('tailwind.config.cjs is picked up by default', () => {
return inTempDirectory(() => {
fs.writeFileSync(
path.resolve(cjsConfigFile),
javascript`module.exports = {
content: [{ raw: '<div class="mobile:font-bold"></div>' }],
theme: {
screens: {
mobile: '400px',
},
},
}`
)
return run('@tailwind utilities').then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
}
}
`)
})
})
})
test('tailwind.config.js is picked up by default', () => {
return inTempDirectory(() => {
fs.writeFileSync(
path.resolve(defaultConfigFile),
javascript`module.exports = {
content: [{ raw: '<div class="mobile:font-bold"></div>' }],
theme: {
screens: {
mobile: '400px',
},
},
}`
)
return run('@tailwind utilities').then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
}
}
`)
})
})
})
test('tailwind.config.cjs is picked up by default when passing an empty object', () => {
return inTempDirectory(() => {
fs.writeFileSync(
path.resolve(cjsConfigFile),
javascript`module.exports = {
content: [{ raw: '<div class="mobile:font-bold"></div>' }],
theme: {
screens: {
mobile: '400px',
},
},
}`
)
return run('@tailwind utilities', {}).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
}
}
`)
})
})
})
test('tailwind.config.js is picked up by default when passing an empty object', () => {
return inTempDirectory(() => {
fs.writeFileSync(
path.resolve(defaultConfigFile),
javascript`module.exports = {
content: [{ raw: '<div class="mobile:font-bold"></div>' }],
theme: {
screens: {
mobile: '400px',
},
},
}`
)
return run('@tailwind utilities', {}).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
}
}
`)
})
})
})
test('the default config can be overridden using the presets key', () => {
let config = {
content: [{ raw: html`<div class="min-h-primary min-h-secondary min-h-0"></div>` }],
presets: [
{
theme: {
extend: { minHeight: { secondary: '24px' } },
},
},
],
theme: {
extend: { minHeight: { primary: '48px' } },
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.min-h-0 {
min-height: 0;
}
.min-h-primary {
min-height: 48px;
}
.min-h-secondary {
min-height: 24px;
}
`)
})
})
test('presets can be functions', () => {
let config = {
content: [{ raw: html`<div class="min-h-primary min-h-secondary min-h-0"></div>` }],
presets: [
() => ({
theme: {
extend: { minHeight: { secondary: '24px' } },
},
}),
],
theme: {
extend: { minHeight: { primary: '48px' } },
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.min-h-0 {
min-height: 0;
}
.min-h-primary {
min-height: 48px;
}
.min-h-secondary {
min-height: 24px;
}
`)
})
})
test('the default config can be removed by using an empty presets key in a preset', () => {
let config = {
content: [{ raw: html`<div class="min-h-primary min-h-secondary min-h-0"></div>` }],
presets: [
{
presets: [],
theme: {
extend: { minHeight: { secondary: '24px' } },
},
},
],
theme: {
extend: { minHeight: { primary: '48px' } },
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.min-h-primary {
min-height: 48px;
}
.min-h-secondary {
min-height: 24px;
}
`)
})
})
test('presets can have their own presets', () => {
let config = {
content: [{ raw: html`<div class="bg-red bg-transparent bg-black bg-white"></div>` }],
presets: [
{
presets: [],
theme: {
colors: { red: '#dd0000' },
},
},
{
presets: [
{
presets: [],
theme: {
colors: {
transparent: 'transparent',
red: '#ff0000',
},
},
},
],
theme: {
extend: {
colors: {
black: 'black',
red: '#ee0000',
},
backgroundColor: (theme) => theme('colors'),
},
},
corePlugins: ['backgroundColor'],
},
],
theme: {
extend: { colors: { white: 'white' } },
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-black {
background-color: #000;
}
.bg-red {
background-color: #e00;
}
.bg-transparent {
background-color: #0000;
}
.bg-white {
background-color: #fff;
}
`)
})
})
test('function presets can be mixed with object presets', () => {
let config = {
content: [{ raw: html`<div class="bg-red bg-transparent bg-black bg-white"></div>` }],
presets: [
() => ({
presets: [],
theme: {
colors: { red: '#dd0000' },
},
}),
{
presets: [
() => ({
presets: [],
theme: {
colors: {
transparent: 'transparent',
red: '#ff0000',
},
},
}),
],
theme: {
extend: {
colors: {
black: 'black',
red: '#ee0000',
},
backgroundColor: (theme) => theme('colors'),
},
},
corePlugins: ['backgroundColor'],
},
],
theme: {
extend: { colors: { white: 'white' } },
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-black {
background-color: #000;
}
.bg-red {
background-color: #e00;
}
.bg-transparent {
background-color: #0000;
}
.bg-white {
background-color: #fff;
}
`)
})
})
})
test('tailwind.config.js is picked up by default', () => {
return inTempDirectory(() => {
fs.writeFileSync(
path.resolve(defaultConfigFile),
javascript`module.exports = {
content: [{ raw: '<div class="mobile:font-bold"></div>' }],
theme: {
screens: {
mobile: '400px',
},
},
}`
)
return run('@tailwind utilities').then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
}
}
`)
})
})
})
test('tailwind.config.cjs is picked up by default when passing an empty object', () => {
return inTempDirectory(() => {
fs.writeFileSync(
path.resolve(cjsConfigFile),
javascript`module.exports = {
content: [{ raw: '<div class="mobile:font-bold"></div>' }],
theme: {
screens: {
mobile: '400px',
},
},
}`
)
return run('@tailwind utilities', {}).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
}
}
`)
})
})
})
test('tailwind.config.js is picked up by default when passing an empty object', () => {
return inTempDirectory(() => {
fs.writeFileSync(
path.resolve(defaultConfigFile),
javascript`module.exports = {
content: [{ raw: '<div class="mobile:font-bold"></div>' }],
theme: {
screens: {
mobile: '400px',
},
},
}`
)
return run('@tailwind utilities', {}).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@media (min-width: 400px) {
.mobile\:font-bold {
font-weight: 700;
}
}
`)
})
})
})
test('the default config can be overridden using the presets key', () => {
let config = {
content: [{ raw: html`<div class="min-h-primary min-h-secondary min-h-0"></div>` }],
presets: [
{
theme: {
extend: { minHeight: { secondary: '24px' } },
},
},
],
theme: {
extend: { minHeight: { primary: '48px' } },
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.min-h-0 {
min-height: 0;
}
.min-h-primary {
min-height: 48px;
}
.min-h-secondary {
min-height: 24px;
}
`)
})
})
test('presets can be functions', () => {
let config = {
content: [{ raw: html`<div class="min-h-primary min-h-secondary min-h-0"></div>` }],
presets: [
() => ({
theme: {
extend: { minHeight: { secondary: '24px' } },
},
}),
],
theme: {
extend: { minHeight: { primary: '48px' } },
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.min-h-0 {
min-height: 0;
}
.min-h-primary {
min-height: 48px;
}
.min-h-secondary {
min-height: 24px;
}
`)
})
})
test('the default config can be removed by using an empty presets key in a preset', () => {
let config = {
content: [{ raw: html`<div class="min-h-primary min-h-secondary min-h-0"></div>` }],
presets: [
{
presets: [],
theme: {
extend: { minHeight: { secondary: '24px' } },
},
},
],
theme: {
extend: { minHeight: { primary: '48px' } },
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.min-h-primary {
min-height: 48px;
}
.min-h-secondary {
min-height: 24px;
}
`)
})
})
test('presets can have their own presets', () => {
let config = {
content: [{ raw: html`<div class="bg-red bg-transparent bg-black bg-white"></div>` }],
presets: [
{
presets: [],
theme: {
colors: { red: '#dd0000' },
},
},
{
presets: [
{
presets: [],
theme: {
colors: {
transparent: 'transparent',
red: '#ff0000',
},
},
},
],
theme: {
extend: {
colors: {
black: 'black',
red: '#ee0000',
},
backgroundColor: (theme) => theme('colors'),
},
},
corePlugins: ['backgroundColor'],
},
],
theme: {
extend: { colors: { white: 'white' } },
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-black {
background-color: #000;
}
.bg-red {
background-color: #e00;
}
.bg-transparent {
background-color: #0000;
}
.bg-white {
background-color: #fff;
}
`)
})
})
test('function presets can be mixed with object presets', () => {
let config = {
content: [{ raw: html`<div class="bg-red bg-transparent bg-black bg-white"></div>` }],
presets: [
() => ({
presets: [],
theme: {
colors: { red: '#dd0000' },
},
}),
{
presets: [
() => ({
presets: [],
theme: {
colors: {
transparent: 'transparent',
red: '#ff0000',
},
},
}),
],
theme: {
extend: {
colors: {
black: 'black',
red: '#ee0000',
},
backgroundColor: (theme) => theme('colors'),
},
},
corePlugins: ['backgroundColor'],
},
],
theme: {
extend: { colors: { white: 'white' } },
},
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-black {
background-color: #000;
}
.bg-red {
background-color: #e00;
}
.bg-transparent {
background-color: #0000;
}
.bg-white {
background-color: #fff;
}
`)
})
})

View file

@ -1,123 +1,121 @@
import { crosscheck, run, html, css, defaults } from './util/run'
import { run, html, css, defaults } from './util/run'
crosscheck(() => {
it('should be possible to use the darkMode "class" mode', () => {
let config = {
darkMode: 'class',
content: [{ raw: html`<div class="dark:font-bold"></div>` }],
corePlugins: { preflight: false },
}
it('should be possible to use the darkMode "class" mode', () => {
let config = {
darkMode: 'class',
content: [{ raw: html`<div class="dark:font-bold"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
:is(.dark .dark\:font-bold) {
font-weight: 700;
}
`)
})
})
it('should be possible to change the class name', () => {
let config = {
darkMode: ['class', '.test-dark'],
content: [{ raw: html`<div class="dark:font-bold"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
:is(.test-dark .dark\:font-bold) {
font-weight: 700;
}
`)
})
})
it('should be possible to use the darkMode "media" mode', () => {
let config = {
darkMode: 'media',
content: [{ raw: html`<div class="dark:font-bold"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
@media (prefers-color-scheme: dark) {
.dark\:font-bold {
font-weight: 700;
}
}
`)
})
})
it('should default to the `media` mode when no mode is provided', () => {
let config = {
content: [{ raw: html`<div class="dark:font-bold"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
@media (prefers-color-scheme: dark) {
.dark\:font-bold {
font-weight: 700;
}
}
`)
})
})
it('should default to the `media` mode when mode is set to `false`', () => {
let config = {
darkMode: false,
content: [{ raw: html`<div class="dark:font-bold"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
@media (prefers-color-scheme: dark) {
.dark\:font-bold {
font-weight: 700;
}
}
`)
})
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
:is(.dark .dark\:font-bold) {
font-weight: 700;
}
`)
})
})
it('should be possible to change the class name', () => {
let config = {
darkMode: ['class', '.test-dark'],
content: [{ raw: html`<div class="dark:font-bold"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
:is(.test-dark .dark\:font-bold) {
font-weight: 700;
}
`)
})
})
it('should be possible to use the darkMode "media" mode', () => {
let config = {
darkMode: 'media',
content: [{ raw: html`<div class="dark:font-bold"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
@media (prefers-color-scheme: dark) {
.dark\:font-bold {
font-weight: 700;
}
}
`)
})
})
it('should default to the `media` mode when no mode is provided', () => {
let config = {
content: [{ raw: html`<div class="dark:font-bold"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
@media (prefers-color-scheme: dark) {
.dark\:font-bold {
font-weight: 700;
}
}
`)
})
})
it('should default to the `media` mode when mode is set to `false`', () => {
let config = {
darkMode: false,
content: [{ raw: html`<div class="dark:font-bold"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
@media (prefers-color-scheme: dark) {
.dark\:font-bold {
font-weight: 700;
}
}
`)
})
})

View file

@ -1,130 +1,128 @@
import { crosscheck, run, html, css } from './util/run'
import { run, html, css } from './util/run'
crosscheck(() => {
it('should warn when we detect nested css', () => {
let config = {
content: [{ raw: html`<div class="nested"></div>` }],
}
it('should warn when we detect nested css', () => {
let config = {
content: [{ raw: html`<div class="nested"></div>` }],
}
let input = css`
@tailwind utilities;
let input = css`
@tailwind utilities;
.nested {
.example {
}
.nested {
.example {
}
`
return run(input, config).then((result) => {
expect(result.messages).toHaveLength(1)
expect(result.messages).toMatchObject([
{
type: 'warning',
text: [
'Nested CSS was detected, but CSS nesting has not been configured correctly.',
'Please enable a CSS nesting plugin *before* Tailwind in your configuration.',
'See how here: https://tailwindcss.com/docs/using-with-preprocessors#nesting',
].join('\n'),
},
])
})
})
it('should not warn when we detect nested css inside css @layer rules', () => {
let config = {
content: [{ raw: html`<div class="underline"></div>` }],
}
`
let input = css`
return run(input, config).then((result) => {
expect(result.messages).toHaveLength(1)
expect(result.messages).toMatchObject([
{
type: 'warning',
text: [
'Nested CSS was detected, but CSS nesting has not been configured correctly.',
'Please enable a CSS nesting plugin *before* Tailwind in your configuration.',
'See how here: https://tailwindcss.com/docs/using-with-preprocessors#nesting',
].join('\n'),
},
])
})
})
it('should not warn when we detect nested css inside css @layer rules', () => {
let config = {
content: [{ raw: html`<div class="underline"></div>` }],
}
let input = css`
@layer tw-base, tw-components, tw-utilities;
@layer tw-utilities {
@tailwind utilities;
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@layer tw-base, tw-components, tw-utilities;
@layer tw-utilities {
@tailwind utilities;
}
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
@layer tw-base, tw-components, tw-utilities;
@layer tw-utilities {
.underline {
text-decoration-line: underline;
}
}
`)
expect(result.messages).toHaveLength(0)
})
})
it('should warn when we detect namespaced @tailwind at rules', () => {
let config = {
content: [{ raw: html`<div class="text-center"></div>` }],
}
let input = css`
.namespace {
@tailwind utilities;
}
`
return run(input, config).then((result) => {
expect(result.messages).toHaveLength(1)
expect(result.messages).toMatchObject([
{
type: 'warning',
text: [
'Nested @tailwind rules were detected, but are not supported.',
"Consider using a prefix to scope Tailwind's classes: https://tailwindcss.com/docs/configuration#prefix",
'Alternatively, use the important selector strategy: https://tailwindcss.com/docs/configuration#selector-strategy',
].join('\n'),
},
])
})
})
it('should not warn when nesting a single rule inside a media query', () => {
let config = {
content: [{ raw: html`<div class="nested"></div>` }],
}
let input = css`
@tailwind utilities;
@media (min-width: 768px) {
.nested {
.underline {
text-decoration-line: underline;
}
}
`
return run(input, config).then((result) => {
expect(result.messages).toHaveLength(0)
expect(result.messages).toEqual([])
})
`)
expect(result.messages).toHaveLength(0)
})
})
it('should only warn for the first detected nesting ', () => {
let config = {
content: [{ raw: html`<div class="nested other"></div>` }],
}
it('should warn when we detect namespaced @tailwind at rules', () => {
let config = {
content: [{ raw: html`<div class="text-center"></div>` }],
}
let input = css`
let input = css`
.namespace {
@tailwind utilities;
}
`
return run(input, config).then((result) => {
expect(result.messages).toHaveLength(1)
expect(result.messages).toMatchObject([
{
type: 'warning',
text: [
'Nested @tailwind rules were detected, but are not supported.',
"Consider using a prefix to scope Tailwind's classes: https://tailwindcss.com/docs/configuration#prefix",
'Alternatively, use the important selector strategy: https://tailwindcss.com/docs/configuration#selector-strategy',
].join('\n'),
},
])
})
})
it('should not warn when nesting a single rule inside a media query', () => {
let config = {
content: [{ raw: html`<div class="nested"></div>` }],
}
let input = css`
@tailwind utilities;
@media (min-width: 768px) {
.nested {
.example {
}
}
}
`
.other {
}
return run(input, config).then((result) => {
expect(result.messages).toHaveLength(0)
expect(result.messages).toEqual([])
})
})
it('should only warn for the first detected nesting ', () => {
let config = {
content: [{ raw: html`<div class="nested other"></div>` }],
}
let input = css`
@tailwind utilities;
.nested {
.example {
}
.other {
.example {
}
}
`
}
return run(input, config).then((result) => {
expect(result.messages).toHaveLength(1)
})
.other {
.example {
}
}
`
return run(input, config).then((result) => {
expect(result.messages).toHaveLength(1)
})
})

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,197 +1,195 @@
import { crosscheck, run, html, css } from './util/run'
import { run, html, css } from './util/run'
crosscheck(() => {
test('experimental universal selector improvements (box-shadow)', () => {
let config = {
experimental: 'all',
content: [{ raw: html`<div class="resize shadow"></div>` }],
corePlugins: { preflight: false },
}
test('experimental universal selector improvements (box-shadow)', () => {
let config = {
experimental: 'all',
content: [{ raw: html`<div class="resize shadow"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind utilities;
`
let input = css`
@tailwind base;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.shadow {
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
}
.resize {
resize: both;
}
.shadow {
--tw-shadow: 0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color),
0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
`)
})
})
test('experimental universal selector improvements (pseudo hover)', () => {
let config = {
experimental: 'all',
content: [{ raw: html`<div class="resize hover:shadow"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.hover\:shadow {
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
}
.resize {
resize: both;
}
.hover\:shadow:hover {
--tw-shadow: 0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color),
0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
`)
})
})
test('experimental universal selector improvements (multiple classes: group)', () => {
let config = {
experimental: 'all',
content: [{ raw: html`<div class="resize group-hover:shadow"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.group-hover\:shadow {
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
}
.resize {
resize: both;
}
.group:hover .group-hover\:shadow {
--tw-shadow: 0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color),
0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
`)
})
})
test('experimental universal selector improvements (child selectors: divide-y)', () => {
let config = {
experimental: 'all',
content: [{ raw: html`<div class="resize divide-y"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.resize {
resize: both;
}
.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));
}
`)
})
})
test('experimental universal selector improvements (hover:divide-y)', () => {
let config = {
experimental: 'all',
content: [{ raw: html`<div class="resize hover:divide-y"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.resize {
resize: both;
}
.hover\:divide-y:hover > :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));
}
`)
})
})
test('experimental universal selector improvements (#app important)', () => {
let config = {
experimental: 'all',
important: '#app',
content: [{ raw: html`<div class="resize divide-y shadow"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.shadow {
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
}
#app :is(.resize) {
resize: both;
}
#app :is(.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));
}
#app :is(.shadow) {
--tw-shadow: 0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color),
0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
`)
})
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.shadow {
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
}
.resize {
resize: both;
}
.shadow {
--tw-shadow: 0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color),
0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
`)
})
})
test('experimental universal selector improvements (pseudo hover)', () => {
let config = {
experimental: 'all',
content: [{ raw: html`<div class="resize hover:shadow"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.hover\:shadow {
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
}
.resize {
resize: both;
}
.hover\:shadow:hover {
--tw-shadow: 0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color),
0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
`)
})
})
test('experimental universal selector improvements (multiple classes: group)', () => {
let config = {
experimental: 'all',
content: [{ raw: html`<div class="resize group-hover:shadow"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.group-hover\:shadow {
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
}
.resize {
resize: both;
}
.group:hover .group-hover\:shadow {
--tw-shadow: 0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color),
0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
`)
})
})
test('experimental universal selector improvements (child selectors: divide-y)', () => {
let config = {
experimental: 'all',
content: [{ raw: html`<div class="resize divide-y"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.resize {
resize: both;
}
.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));
}
`)
})
})
test('experimental universal selector improvements (hover:divide-y)', () => {
let config = {
experimental: 'all',
content: [{ raw: html`<div class="resize hover:divide-y"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.resize {
resize: both;
}
.hover\:divide-y:hover > :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));
}
`)
})
})
test('experimental universal selector improvements (#app important)', () => {
let config = {
experimental: 'all',
important: '#app',
content: [{ raw: html`<div class="resize divide-y shadow"></div>` }],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.shadow {
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
}
#app :is(.resize) {
resize: both;
}
#app :is(.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));
}
#app :is(.shadow) {
--tw-shadow: 0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color),
0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
`)
})
})

View file

@ -1,34 +1,32 @@
import { crosscheck, run, html, css } from './util/run'
import { run, html, css } from './util/run'
crosscheck(() => {
test('PHP arrays', async () => {
let config = {
content: [
{
raw: html`<h1 class="<?php echo wrap(['class' => "max-w-[16rem]"]); ?>">Hello world</h1>`,
},
],
}
test('PHP arrays', async () => {
let config = {
content: [
{
raw: html`<h1 class="<?php echo wrap(['class' => "max-w-[16rem]"]); ?>">Hello world</h1>`,
},
],
}
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.max-w-\[16rem\] {
max-width: 16rem;
}
`)
})
})
test('arbitrary values with quotes', async () => {
let config = { content: [{ raw: html`<div class="content-['hello]']"></div>` }] }
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.content-\[\'hello\]\'\] {
--tw-content: 'hello]';
content: var(--tw-content);
}
`)
})
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.max-w-\[16rem\] {
max-width: 16rem;
}
`)
})
})
test('arbitrary values with quotes', async () => {
let config = { content: [{ raw: html`<div class="content-['hello]']"></div>` }] }
return run('@tailwind utilities', config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.content-\[\'hello\]\'\] {
--tw-content: 'hello]';
content: var(--tw-content);
}
`)
})
})

View file

@ -1,84 +1,81 @@
import { crosscheck } from './util/run'
import flattenColorPalette from '../src/util/flattenColorPalette'
crosscheck(() => {
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({
test('it flattens nested color objects', () => {
expect(
flattenColorPalette({
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)',
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)',
},
})
})
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({})
).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({})
})

View file

@ -1,367 +1,364 @@
import { finalizeSelector } from '../src/util/formatVariantSelector'
import { crosscheck } from './util/run'
crosscheck(() => {
it('should be possible to add a simple variant to a simple selector', () => {
let selector = '.text-center'
let candidate = 'hover:text-center'
it('should be possible to add a simple variant to a simple selector', () => {
let selector = '.text-center'
let candidate = 'hover:text-center'
let formats = [{ format: '&:hover', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual('.hover\\:text-center:hover')
})
it('should be possible to add a multiple simple variants to a simple selector', () => {
let selector = '.text-center'
let candidate = 'focus:hover:text-center'
let formats = [
{ format: '&:hover', isArbitraryVariant: false },
{ format: '&:focus', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.focus\\:hover\\:text-center:hover:focus'
)
})
it('should be possible to add a simple variant to a selector containing escaped parts', () => {
let selector = '.bg-\\[rgba\\(0\\,0\\,0\\)\\]'
let candidate = 'hover:bg-[rgba(0,0,0)]'
let formats = [{ format: '&:hover', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.hover\\:bg-\\[rgba\\(0\\2c 0\\2c 0\\)\\]:hover'
)
})
it('should be possible to add a simple variant to a selector containing escaped parts (escape is slightly different)', () => {
let selector = '.bg-\\[rgba\\(0\\2c 0\\2c 0\\)\\]'
let candidate = 'hover:bg-[rgba(0,0,0)]'
let formats = [{ format: '&:hover', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.hover\\:bg-\\[rgba\\(0\\2c 0\\2c 0\\)\\]:hover'
)
})
it('should be possible to add a simple variant to a more complex selector', () => {
let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])'
let candidate = 'hover:space-x-4'
let formats = [{ format: '&:hover', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.hover\\:space-x-4:hover > :not([hidden]) ~ :not([hidden])'
)
})
it('should be possible to add multiple simple variants to a more complex selector', () => {
let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])'
let candidate = 'disabled:focus:hover:space-x-4'
let formats = [
{ format: '&:hover', isArbitraryVariant: false },
{ format: '&:focus', isArbitraryVariant: false },
{ format: '&:disabled', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.disabled\\:focus\\:hover\\:space-x-4:hover:focus:disabled > :not([hidden]) ~ :not([hidden])'
)
})
it('should be possible to add a single merge variant to a simple selector', () => {
let selector = '.text-center'
let candidate = 'group-hover:text-center'
let formats = [{ format: ':merge(.group):hover &', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover .group-hover\\:text-center'
)
})
it('should be possible to add multiple merge variants to a simple selector', () => {
let selector = '.text-center'
let candidate = 'group-focus:group-hover:text-center'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
{ format: ':merge(.group):focus &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:focus:hover .group-focus\\:group-hover\\:text-center'
)
})
it('should be possible to add a single merge variant to a more complex selector', () => {
let selector = '.space-x-4 ~ :not([hidden]) ~ :not([hidden])'
let candidate = 'group-hover:space-x-4'
let formats = [{ format: ':merge(.group):hover &', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover .group-hover\\:space-x-4 ~ :not([hidden]) ~ :not([hidden])'
)
})
it('should be possible to add multiple merge variants to a more complex selector', () => {
let selector = '.space-x-4 ~ :not([hidden]) ~ :not([hidden])'
let candidate = 'group-focus:group-hover:space-x-4'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
{ format: ':merge(.group):focus &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:focus:hover .group-focus\\:group-hover\\:space-x-4 ~ :not([hidden]) ~ :not([hidden])'
)
})
it('should be possible to add multiple unique merge variants to a simple selector', () => {
let selector = '.text-center'
let candidate = 'peer-focus:group-hover:text-center'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
{ format: ':merge(.peer):focus ~ &' },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.peer:focus ~ .group:hover .peer-focus\\:group-hover\\:text-center'
)
})
it('should be possible to add multiple unique merge variants to a simple selector', () => {
let selector = '.text-center'
let candidate = 'group-hover:peer-focus:text-center'
let formats = [
{ format: ':merge(.peer):focus ~ &', isArbitraryVariant: false },
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover .peer:focus ~ .group-hover\\:peer-focus\\:text-center'
)
})
it('should be possible to use multiple :merge() calls with different "arguments"', () => {
let selector = '.foo'
let candidate = 'peer-focus:group-focus:peer-hover:group-hover:foo'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
{ format: ':merge(.peer):hover ~ &', isArbitraryVariant: false },
{ format: ':merge(.group):focus &', isArbitraryVariant: false },
{ format: ':merge(.peer):focus ~ &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.peer:focus:hover ~ .group:focus:hover .peer-focus\\:group-focus\\:peer-hover\\:group-hover\\:foo'
)
})
it('group hover and prose headings combination', () => {
let selector = '.text-center'
let candidate = 'group-hover:prose-headings:text-center'
let formats = [
{ format: ':where(&) :is(h1, h2, h3, h4)', isArbitraryVariant: false }, // Prose Headings
{ format: ':merge(.group):hover &', isArbitraryVariant: false }, // Group Hover
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover :where(.group-hover\\:prose-headings\\:text-center) :is(h1, h2, h3, h4)'
)
})
it('group hover and prose headings combination flipped', () => {
let selector = '.text-center'
let candidate = 'prose-headings:group-hover:text-center'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false }, // Group Hover
{ format: ':where(&) :is(h1, h2, h3, h4)', isArbitraryVariant: false }, // Prose Headings
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
':where(.group:hover .prose-headings\\:group-hover\\:text-center) :is(h1, h2, h3, h4)'
)
})
it('should be possible to handle a complex utility', () => {
let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])'
let candidate = 'peer-disabled:peer-first-child:group-hover:group-focus:focus:hover:space-x-4'
let formats = [
{ format: '&:hover', isArbitraryVariant: false }, // Hover
{ format: '&:focus', isArbitraryVariant: false }, // Focus
{ format: ':merge(.group):focus &', isArbitraryVariant: false }, // Group focus
{ format: ':merge(.group):hover &', isArbitraryVariant: false }, // Group hover
{ format: ':merge(.peer):first-child ~ &', isArbitraryVariant: false }, // Peer first-child
{ format: ':merge(.peer):disabled ~ &', isArbitraryVariant: false }, // Peer disabled
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.peer:disabled:first-child ~ .group:hover:focus .peer-disabled\\:peer-first-child\\:group-hover\\:group-focus\\:focus\\:hover\\:space-x-4:hover:focus > :not([hidden]) ~ :not([hidden])'
)
})
it('should match base utilities that are prefixed', () => {
let context = { tailwindConfig: { prefix: 'tw-' } }
let selector = '.tw-text-center'
let candidate = 'tw-text-center'
let formats = []
expect(finalizeSelector(selector, formats, { candidate, context })).toEqual('.tw-text-center')
})
it('should prefix classes from variants', () => {
let context = { tailwindConfig: { prefix: 'tw-' } }
let selector = '.tw-text-center'
let candidate = 'foo:tw-text-center'
let formats = [{ format: '.foo &', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate, context })).toEqual(
'.tw-foo .foo\\:tw-text-center'
)
})
it('should not prefix classes from arbitrary variants', () => {
let context = { tailwindConfig: { prefix: 'tw-' } }
let selector = '.tw-text-center'
let candidate = '[.foo_&]:tw-text-center'
let formats = [{ format: '.foo &', isArbitraryVariant: true }]
expect(finalizeSelector(selector, formats, { candidate, context })).toEqual(
'.foo .\\[\\.foo_\\&\\]\\:tw-text-center'
)
})
it('Merged selectors with mixed combinators uses the first one', () => {
// This isn't explicitly specced behavior but it is how it works today
let selector = '.text-center'
let candidate = 'text-center'
let formats = [
{ format: ':merge(.group):focus > &', isArbitraryVariant: true },
{ format: ':merge(.group):hover &', isArbitraryVariant: true },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover:focus > .text-center'
)
})
describe('real examples', () => {
it('example a', () => {
let selector = '.placeholder-red-500::placeholder'
let candidate = 'hover:placeholder-red-500'
let formats = [{ format: '&:hover', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual('.hover\\:text-center:hover')
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.hover\\:placeholder-red-500:hover::placeholder'
)
})
it('should be possible to add a multiple simple variants to a simple selector', () => {
let selector = '.text-center'
let candidate = 'focus:hover:text-center'
it('example b', () => {
let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])'
let candidate = 'group-hover:hover:space-x-4'
let formats = [
{ format: '&:hover', isArbitraryVariant: false },
{ format: '&:focus', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.focus\\:hover\\:text-center:hover:focus'
)
})
it('should be possible to add a simple variant to a selector containing escaped parts', () => {
let selector = '.bg-\\[rgba\\(0\\,0\\,0\\)\\]'
let candidate = 'hover:bg-[rgba(0,0,0)]'
let formats = [{ format: '&:hover', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.hover\\:bg-\\[rgba\\(0\\2c 0\\2c 0\\)\\]:hover'
)
})
it('should be possible to add a simple variant to a selector containing escaped parts (escape is slightly different)', () => {
let selector = '.bg-\\[rgba\\(0\\2c 0\\2c 0\\)\\]'
let candidate = 'hover:bg-[rgba(0,0,0)]'
let formats = [{ format: '&:hover', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.hover\\:bg-\\[rgba\\(0\\2c 0\\2c 0\\)\\]:hover'
)
})
it('should be possible to add a simple variant to a more complex selector', () => {
let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])'
let candidate = 'hover:space-x-4'
let formats = [{ format: '&:hover', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.hover\\:space-x-4:hover > :not([hidden]) ~ :not([hidden])'
)
})
it('should be possible to add multiple simple variants to a more complex selector', () => {
let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])'
let candidate = 'disabled:focus:hover:space-x-4'
let formats = [
{ format: '&:hover', isArbitraryVariant: false },
{ format: '&:focus', isArbitraryVariant: false },
{ format: '&:disabled', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.disabled\\:focus\\:hover\\:space-x-4:hover:focus:disabled > :not([hidden]) ~ :not([hidden])'
)
})
it('should be possible to add a single merge variant to a simple selector', () => {
let selector = '.text-center'
let candidate = 'group-hover:text-center'
let formats = [{ format: ':merge(.group):hover &', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover .group-hover\\:text-center'
)
})
it('should be possible to add multiple merge variants to a simple selector', () => {
let selector = '.text-center'
let candidate = 'group-focus:group-hover:text-center'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
{ format: ':merge(.group):focus &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:focus:hover .group-focus\\:group-hover\\:text-center'
)
})
it('should be possible to add a single merge variant to a more complex selector', () => {
let selector = '.space-x-4 ~ :not([hidden]) ~ :not([hidden])'
let candidate = 'group-hover:space-x-4'
let formats = [{ format: ':merge(.group):hover &', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover .group-hover\\:space-x-4 ~ :not([hidden]) ~ :not([hidden])'
)
})
it('should be possible to add multiple merge variants to a more complex selector', () => {
let selector = '.space-x-4 ~ :not([hidden]) ~ :not([hidden])'
let candidate = 'group-focus:group-hover:space-x-4'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
{ format: ':merge(.group):focus &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:focus:hover .group-focus\\:group-hover\\:space-x-4 ~ :not([hidden]) ~ :not([hidden])'
)
})
it('should be possible to add multiple unique merge variants to a simple selector', () => {
let selector = '.text-center'
let candidate = 'peer-focus:group-hover:text-center'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
{ format: ':merge(.peer):focus ~ &' },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.peer:focus ~ .group:hover .peer-focus\\:group-hover\\:text-center'
)
})
it('should be possible to add multiple unique merge variants to a simple selector', () => {
let selector = '.text-center'
let candidate = 'group-hover:peer-focus:text-center'
let formats = [
{ format: ':merge(.peer):focus ~ &', isArbitraryVariant: false },
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover .peer:focus ~ .group-hover\\:peer-focus\\:text-center'
'.group:hover .group-hover\\:hover\\:space-x-4:hover > :not([hidden]) ~ :not([hidden])'
)
})
it('should be possible to use multiple :merge() calls with different "arguments"', () => {
let selector = '.foo'
let candidate = 'peer-focus:group-focus:peer-hover:group-hover:foo'
it('should work for group-hover and class dark mode combinations', () => {
let selector = '.text-center'
let candidate = 'dark:group-hover:text-center'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
{ format: ':merge(.peer):hover ~ &', isArbitraryVariant: false },
{ format: ':merge(.group):focus &', isArbitraryVariant: false },
{ format: ':merge(.peer):focus ~ &', isArbitraryVariant: false },
{ format: '.dark &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.peer:focus:hover ~ .group:focus:hover .peer-focus\\:group-focus\\:peer-hover\\:group-hover\\:foo'
'.dark .group:hover .dark\\:group-hover\\:text-center'
)
})
it('group hover and prose headings combination', () => {
it('should work for group-hover and class dark mode combinations (reversed)', () => {
let selector = '.text-center'
let candidate = 'group-hover:prose-headings:text-center'
let candidate = 'group-hover:dark:text-center'
let formats = [
{ format: ':where(&) :is(h1, h2, h3, h4)', isArbitraryVariant: false }, // Prose Headings
{ format: ':merge(.group):hover &', isArbitraryVariant: false }, // Group Hover
{ format: '.dark &' },
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover :where(.group-hover\\:prose-headings\\:text-center) :is(h1, h2, h3, h4)'
'.group:hover .dark .group-hover\\:dark\\:text-center'
)
})
it('group hover and prose headings combination flipped', () => {
let selector = '.text-center'
let candidate = 'prose-headings:group-hover:text-center'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false }, // Group Hover
{ format: ':where(&) :is(h1, h2, h3, h4)', isArbitraryVariant: false }, // Prose Headings
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
':where(.group:hover .prose-headings\\:group-hover\\:text-center) :is(h1, h2, h3, h4)'
)
})
it('should be possible to handle a complex utility', () => {
let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])'
let candidate = 'peer-disabled:peer-first-child:group-hover:group-focus:focus:hover:space-x-4'
let formats = [
{ format: '&:hover', isArbitraryVariant: false }, // Hover
{ format: '&:focus', isArbitraryVariant: false }, // Focus
{ format: ':merge(.group):focus &', isArbitraryVariant: false }, // Group focus
{ format: ':merge(.group):hover &', isArbitraryVariant: false }, // Group hover
{ format: ':merge(.peer):first-child ~ &', isArbitraryVariant: false }, // Peer first-child
{ format: ':merge(.peer):disabled ~ &', isArbitraryVariant: false }, // Peer disabled
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.peer:disabled:first-child ~ .group:hover:focus .peer-disabled\\:peer-first-child\\:group-hover\\:group-focus\\:focus\\:hover\\:space-x-4:hover:focus > :not([hidden]) ~ :not([hidden])'
)
})
it('should match base utilities that are prefixed', () => {
let context = { tailwindConfig: { prefix: 'tw-' } }
let selector = '.tw-text-center'
let candidate = 'tw-text-center'
let formats = []
expect(finalizeSelector(selector, formats, { candidate, context })).toEqual('.tw-text-center')
})
it('should prefix classes from variants', () => {
let context = { tailwindConfig: { prefix: 'tw-' } }
let selector = '.tw-text-center'
let candidate = 'foo:tw-text-center'
let formats = [{ format: '.foo &', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate, context })).toEqual(
'.tw-foo .foo\\:tw-text-center'
)
})
it('should not prefix classes from arbitrary variants', () => {
let context = { tailwindConfig: { prefix: 'tw-' } }
let selector = '.tw-text-center'
let candidate = '[.foo_&]:tw-text-center'
let formats = [{ format: '.foo &', isArbitraryVariant: true }]
expect(finalizeSelector(selector, formats, { candidate, context })).toEqual(
'.foo .\\[\\.foo_\\&\\]\\:tw-text-center'
)
})
it('Merged selectors with mixed combinators uses the first one', () => {
// This isn't explicitly specced behavior but it is how it works today
let selector = '.text-center'
let candidate = 'text-center'
let formats = [
{ format: ':merge(.group):focus > &', isArbitraryVariant: true },
{ format: ':merge(.group):hover &', isArbitraryVariant: true },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover:focus > .text-center'
)
})
describe('real examples', () => {
it('example a', () => {
let selector = '.placeholder-red-500::placeholder'
let candidate = 'hover:placeholder-red-500'
let formats = [{ format: '&:hover', isArbitraryVariant: false }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.hover\\:placeholder-red-500:hover::placeholder'
)
})
it('example b', () => {
let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])'
let candidate = 'group-hover:hover:space-x-4'
let formats = [
{ format: '&:hover', isArbitraryVariant: false },
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover .group-hover\\:hover\\:space-x-4:hover > :not([hidden]) ~ :not([hidden])'
)
})
it('should work for group-hover and class dark mode combinations', () => {
describe('prose-headings', () => {
it('should be possible to use hover:prose-headings:text-center', () => {
let selector = '.text-center'
let candidate = 'dark:group-hover:text-center'
let candidate = 'hover:prose-headings:text-center'
let formats = [
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
{ format: '.dark &', isArbitraryVariant: false },
]
let formats = [{ format: ':where(&) :is(h1, h2, h3, h4)' }, { format: '&:hover' }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.dark .group:hover .dark\\:group-hover\\:text-center'
':where(.hover\\:prose-headings\\:text-center) :is(h1, h2, h3, h4):hover'
)
})
it('should work for group-hover and class dark mode combinations (reversed)', () => {
it('should be possible to use prose-headings:hover:text-center', () => {
let selector = '.text-center'
let candidate = 'group-hover:dark:text-center'
let candidate = 'prose-headings:hover:text-center'
let formats = [
{ format: '.dark &' },
{ format: ':merge(.group):hover &', isArbitraryVariant: false },
]
let formats = [{ format: '&:hover' }, { format: ':where(&) :is(h1, h2, h3, h4)' }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
'.group:hover .dark .group-hover\\:dark\\:text-center'
':where(.prose-headings\\:hover\\:text-center:hover) :is(h1, h2, h3, h4)'
)
})
describe('prose-headings', () => {
it('should be possible to use hover:prose-headings:text-center', () => {
let selector = '.text-center'
let candidate = 'hover:prose-headings:text-center'
let formats = [{ format: ':where(&) :is(h1, h2, h3, h4)' }, { format: '&:hover' }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
':where(.hover\\:prose-headings\\:text-center) :is(h1, h2, h3, h4):hover'
)
})
it('should be possible to use prose-headings:hover:text-center', () => {
let selector = '.text-center'
let candidate = 'prose-headings:hover:text-center'
let formats = [{ format: '&:hover' }, { format: ':where(&) :is(h1, h2, h3, h4)' }]
expect(finalizeSelector(selector, formats, { candidate })).toEqual(
':where(.prose-headings\\:hover\\:text-center:hover) :is(h1, h2, h3, h4)'
)
})
})
})
describe('pseudo elements', () => {
it.each`
before | after
${'&::before'} | ${'&::before'}
${'&::before:hover'} | ${'&:hover::before'}
${'&:before:hover'} | ${'&:hover:before'}
${'&::file-selector-button:hover'} | ${'&::file-selector-button:hover'}
${'&:hover::file-selector-button'} | ${'&:hover::file-selector-button'}
${'.parent:hover &'} | ${'.parent:hover &'}
${'.parent::before &'} | ${'.parent &::before'}
${'.parent::before &:hover'} | ${'.parent &:hover::before'}
${':where(&::before) :is(h1, h2, h3, h4)'} | ${':where(&) :is(h1, h2, h3, h4)::before'}
${':where(&::file-selector-button) :is(h1, h2, h3, h4)'} | ${':where(&::file-selector-button) :is(h1, h2, h3, h4)'}
${'#app :is(.dark &::before)'} | ${'#app :is(.dark &)::before'}
${'#app :is(:is(.dark &)::before)'} | ${'#app :is(:is(.dark &))::before'}
${'#app :is(.foo::file-selector-button)'} | ${'#app :is(.foo)::file-selector-button'}
${'#app :is(.foo::-webkit-progress-bar)'} | ${'#app :is(.foo)::-webkit-progress-bar'}
${'.parent::marker li'} | ${'.parent li::marker'}
${'.parent::selection li'} | ${'.parent li::selection'}
${'.parent::placeholder input'} | ${'.parent input::placeholder'}
${'.parent::backdrop dialog'} | ${'.parent dialog::backdrop'}
`('should translate "$before" into "$after"', ({ before, after }) => {
let result = finalizeSelector('.a', [{ format: before, isArbitraryVariant: false }], {
candidate: 'a',
})
expect(result).toEqual(after.replace('&', '.a'))
})
})
})
describe('pseudo elements', () => {
it.each`
before | after
${'&::before'} | ${'&::before'}
${'&::before:hover'} | ${'&:hover::before'}
${'&:before:hover'} | ${'&:hover:before'}
${'&::file-selector-button:hover'} | ${'&::file-selector-button:hover'}
${'&:hover::file-selector-button'} | ${'&:hover::file-selector-button'}
${'.parent:hover &'} | ${'.parent:hover &'}
${'.parent::before &'} | ${'.parent &::before'}
${'.parent::before &:hover'} | ${'.parent &:hover::before'}
${':where(&::before) :is(h1, h2, h3, h4)'} | ${':where(&) :is(h1, h2, h3, h4)::before'}
${':where(&::file-selector-button) :is(h1, h2, h3, h4)'} | ${':where(&::file-selector-button) :is(h1, h2, h3, h4)'}
${'#app :is(.dark &::before)'} | ${'#app :is(.dark &)::before'}
${'#app :is(:is(.dark &)::before)'} | ${'#app :is(:is(.dark &))::before'}
${'#app :is(.foo::file-selector-button)'} | ${'#app :is(.foo)::file-selector-button'}
${'#app :is(.foo::-webkit-progress-bar)'} | ${'#app :is(.foo)::-webkit-progress-bar'}
${'.parent::marker li'} | ${'.parent li::marker'}
${'.parent::selection li'} | ${'.parent li::selection'}
${'.parent::placeholder input'} | ${'.parent input::placeholder'}
${'.parent::backdrop dialog'} | ${'.parent dialog::backdrop'}
`('should translate "$before" into "$after"', ({ before, after }) => {
let result = finalizeSelector('.a', [{ format: before, isArbitraryVariant: false }], {
candidate: 'a',
})
expect(result).toEqual(after.replace('&', '.a'))
})
})

View file

@ -1,48 +1,46 @@
import { generateRules } from '../src/lib/generateRules'
import resolveConfig from '../src/public/resolve-config'
import { createContext } from '../src/lib/setupContextUtils'
import { crosscheck, css } from './util/run'
import { css } from './util/run'
crosscheck(() => {
it('should not generate rules that are incorrect', () => {
let config = {
plugins: [
({ matchVariant }) => {
matchVariant('@', (value) => `@container (min-width: ${value})`)
},
],
it('should not generate rules that are incorrect', () => {
let config = {
plugins: [
({ matchVariant }) => {
matchVariant('@', (value) => `@container (min-width: ${value})`)
},
],
}
let context = createContext(resolveConfig(config))
let rules = generateRules(
new Set([
// Invalid, missing `-`
'group[:hover]:underline',
// Invalid, `-` should not be there
'@-[200px]:underline',
// Valid
'group-[:hover]:underline',
'@[200px]:underline',
]),
context
)
// Ensure we only have 2 valid rules
expect(rules).toHaveLength(2)
// Ensure we have the correct values
expect(rules[0][1].toString()).toMatchFormattedCss(css`
.group:hover .group-\[\:hover\]\:underline {
text-decoration-line: underline;
}
let context = createContext(resolveConfig(config))
let rules = generateRules(
new Set([
// Invalid, missing `-`
'group[:hover]:underline',
// Invalid, `-` should not be there
'@-[200px]:underline',
// Valid
'group-[:hover]:underline',
'@[200px]:underline',
]),
context
)
// Ensure we only have 2 valid rules
expect(rules).toHaveLength(2)
// Ensure we have the correct values
expect(rules[0][1].toString()).toMatchFormattedCss(css`
.group:hover .group-\[\:hover\]\:underline {
`)
expect(rules[1][1].toString()).toMatchFormattedCss(css`
@container (min-width: 200px) {
.\@\[200px\]\:underline {
text-decoration-line: underline;
}
`)
expect(rules[1][1].toString()).toMatchFormattedCss(css`
@container (min-width: 200px) {
.\@\[200px\]\:underline {
text-decoration-line: underline;
}
}
`)
})
}
`)
})

View file

@ -1,228 +1,225 @@
import resolveConfig from '../src/public/resolve-config'
import plugin from '../src/public/create-plugin'
import { createContext } from '../src/lib/setupContextUtils'
import { crosscheck } from './util/run'
crosscheck(() => {
it('should generate every possible class, without variants', () => {
let config = {}
it('should generate every possible class, without variants', () => {
let config = {}
let context = createContext(resolveConfig(config))
let classes = context.getClassList()
expect(classes).toBeInstanceOf(Array)
let context = createContext(resolveConfig(config))
let classes = context.getClassList()
expect(classes).toBeInstanceOf(Array)
// Verify we have a `container` for the 'components' section.
expect(classes).toContain('container')
// Verify we have a `container` for the 'components' section.
expect(classes).toContain('container')
// Verify we handle the DEFAULT case correctly
expect(classes).toContain('border')
// Verify we handle the DEFAULT case correctly
expect(classes).toContain('border')
// Verify we handle negative values correctly
expect(classes).toContain('-inset-1/4')
expect(classes).toContain('-m-0')
expect(classes).not.toContain('-uppercase')
expect(classes).not.toContain('-opacity-50')
// Verify we handle negative values correctly
expect(classes).toContain('-inset-1/4')
expect(classes).toContain('-m-0')
expect(classes).not.toContain('-uppercase')
expect(classes).not.toContain('-opacity-50')
config = { theme: { extend: { margin: { DEFAULT: '5px' } } } }
context = createContext(resolveConfig(config))
classes = context.getClassList()
config = { theme: { extend: { margin: { DEFAULT: '5px' } } } }
context = createContext(resolveConfig(config))
classes = context.getClassList()
expect(classes).not.toContain('-m-DEFAULT')
})
expect(classes).not.toContain('-m-DEFAULT')
})
it('should generate every possible class while handling negatives and prefixes', () => {
let config = { prefix: 'tw-' }
let context = createContext(resolveConfig(config))
let classes = context.getClassList()
expect(classes).toBeInstanceOf(Array)
it('should generate every possible class while handling negatives and prefixes', () => {
let config = { prefix: 'tw-' }
let context = createContext(resolveConfig(config))
let classes = context.getClassList()
expect(classes).toBeInstanceOf(Array)
// Verify we have a `container` for the 'components' section.
expect(classes).toContain('tw-container')
// Verify we have a `container` for the 'components' section.
expect(classes).toContain('tw-container')
// Verify we handle the DEFAULT case correctly
expect(classes).toContain('tw-border')
// Verify we handle the DEFAULT case correctly
expect(classes).toContain('tw-border')
// Verify we handle negative values correctly
expect(classes).toContain('-tw-inset-1/4')
expect(classes).toContain('-tw-m-0')
expect(classes).not.toContain('-tw-uppercase')
expect(classes).not.toContain('-tw-opacity-50')
// Verify we handle negative values correctly
expect(classes).toContain('-tw-inset-1/4')
expect(classes).toContain('-tw-m-0')
expect(classes).not.toContain('-tw-uppercase')
expect(classes).not.toContain('-tw-opacity-50')
// These utilities do work but there's no reason to generate
// them alongside the `-{prefix}-{utility}` versions
expect(classes).not.toContain('tw--inset-1/4')
expect(classes).not.toContain('tw--m-0')
// These utilities do work but there's no reason to generate
// them alongside the `-{prefix}-{utility}` versions
expect(classes).not.toContain('tw--inset-1/4')
expect(classes).not.toContain('tw--m-0')
config = {
prefix: 'tw-',
theme: { extend: { margin: { DEFAULT: '5px' } } },
}
context = createContext(resolveConfig(config))
classes = context.getClassList()
config = {
prefix: 'tw-',
theme: { extend: { margin: { DEFAULT: '5px' } } },
}
context = createContext(resolveConfig(config))
classes = context.getClassList()
expect(classes).not.toContain('-tw-m-DEFAULT')
})
expect(classes).not.toContain('-tw-m-DEFAULT')
})
it('should not generate utilities with opacity by default', () => {
let config = {}
let context = createContext(resolveConfig(config))
let classes = context.getClassList()
it('should not generate utilities with opacity by default', () => {
let config = {}
let context = createContext(resolveConfig(config))
let classes = context.getClassList()
expect(classes).not.toContain('bg-red-500/50')
})
expect(classes).not.toContain('bg-red-500/50')
})
it('should not include metadata by default', () => {
let config = {}
let context = createContext(resolveConfig(config))
let classes = context.getClassList()
it('should not include metadata by default', () => {
let config = {}
let context = createContext(resolveConfig(config))
let classes = context.getClassList()
expect(classes.every((cls) => typeof cls === 'string')).toEqual(true)
expect(classes.every((cls) => typeof cls === 'string')).toEqual(true)
expect(classes).toContain('bg-red-500')
expect(classes).toContain('text-2xl')
})
expect(classes).toContain('bg-red-500')
expect(classes).toContain('text-2xl')
})
it('should generate utilities with modifier data when requested', () => {
let config = {}
let context = createContext(resolveConfig(config))
let classes = context.getClassList({ includeMetadata: true })
it('should generate utilities with modifier data when requested', () => {
let config = {}
let context = createContext(resolveConfig(config))
let classes = context.getClassList({ includeMetadata: true })
expect(classes).not.toContain('bg-red-500')
expect(classes).not.toContain('text-2xl')
expect(classes).not.toContain('bg-red-500')
expect(classes).not.toContain('text-2xl')
expect(classes).toContainEqual([
'bg-red-500',
{
modifiers: [
'0',
'5',
'10',
'20',
'25',
'30',
'40',
'50',
'60',
'70',
'75',
'80',
'90',
'95',
'100',
],
},
])
expect(classes).toContainEqual([
'text-2xl',
{
modifiers: [
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'10',
'none',
'tight',
'snug',
'normal',
'relaxed',
'loose',
],
},
])
})
it('should generate plugin-defined utilities with modifier data when requested', () => {
let config = {
plugins: [
plugin(function ({ matchUtilities }) {
matchUtilities(
{
foo: (value) => {
return { margin: value }
},
},
{
values: { xl: '32px' },
modifiers: { bar: 'something' },
}
)
matchUtilities(
{
'foo-negative': (value) => {
return { margin: value }
},
},
{
values: { xl: '32px' },
modifiers: { bar: 'something' },
supportsNegativeValues: true,
}
)
}),
expect(classes).toContainEqual([
'bg-red-500',
{
modifiers: [
'0',
'5',
'10',
'20',
'25',
'30',
'40',
'50',
'60',
'70',
'75',
'80',
'90',
'95',
'100',
],
}
let context = createContext(resolveConfig(config))
let classes = context.getClassList({ includeMetadata: true })
expect(classes).toContainEqual(['foo-xl', { modifiers: ['bar'] }])
expect(classes).toContainEqual(['foo-negative-xl', { modifiers: ['bar'] }])
expect(classes).toContainEqual(['-foo-negative-xl', { modifiers: ['bar'] }])
expect(classes).not.toContain('foo-xl')
expect(classes).not.toContain('-foo-xl')
expect(classes).not.toContainEqual(['-foo-xl', { modifiers: ['bar'] }])
})
it('should not generate utilities with opacity even if safe-listed', () => {
let config = {
safelist: [
{
pattern: /^bg-red-(400|500)(\/(40|50))?$/,
},
},
])
expect(classes).toContainEqual([
'text-2xl',
{
modifiers: [
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'10',
'none',
'tight',
'snug',
'normal',
'relaxed',
'loose',
],
}
},
])
})
let context = createContext(resolveConfig(config))
let classes = context.getClassList()
expect(classes).not.toContain('bg-red-500/50')
})
it('should not generate utilities that are set to undefined or null to so that they are removed', () => {
let config = {
theme: {
extend: {
colors: {
red: null,
green: undefined,
blue: {
100: null,
200: undefined,
it('should generate plugin-defined utilities with modifier data when requested', () => {
let config = {
plugins: [
plugin(function ({ matchUtilities }) {
matchUtilities(
{
foo: (value) => {
return { margin: value }
},
},
{
values: { xl: '32px' },
modifiers: { bar: 'something' },
}
)
matchUtilities(
{
'foo-negative': (value) => {
return { margin: value }
},
},
{
values: { xl: '32px' },
modifiers: { bar: 'something' },
supportsNegativeValues: true,
}
)
}),
],
}
let context = createContext(resolveConfig(config))
let classes = context.getClassList({ includeMetadata: true })
expect(classes).toContainEqual(['foo-xl', { modifiers: ['bar'] }])
expect(classes).toContainEqual(['foo-negative-xl', { modifiers: ['bar'] }])
expect(classes).toContainEqual(['-foo-negative-xl', { modifiers: ['bar'] }])
expect(classes).not.toContain('foo-xl')
expect(classes).not.toContain('-foo-xl')
expect(classes).not.toContainEqual(['-foo-xl', { modifiers: ['bar'] }])
})
it('should not generate utilities with opacity even if safe-listed', () => {
let config = {
safelist: [
{
pattern: /^bg-red-(400|500)(\/(40|50))?$/,
},
],
}
let context = createContext(resolveConfig(config))
let classes = context.getClassList()
expect(classes).not.toContain('bg-red-500/50')
})
it('should not generate utilities that are set to undefined or null to so that they are removed', () => {
let config = {
theme: {
extend: {
colors: {
red: null,
green: undefined,
blue: {
100: null,
200: undefined,
},
},
},
safelist: [
{
pattern: /^bg-(red|green|blue)-.*$/,
},
],
}
},
safelist: [
{
pattern: /^bg-(red|green|blue)-.*$/,
},
],
}
let context = createContext(resolveConfig(config))
let classes = context.getClassList()
let context = createContext(resolveConfig(config))
let classes = context.getClassList()
expect(classes).not.toContain('bg-red-100') // Red is `null`
expect(classes).not.toContain('bg-red-100') // Red is `null`
expect(classes).not.toContain('bg-green-100') // Green is `undefined`
expect(classes).not.toContain('bg-green-100') // Green is `undefined`
expect(classes).not.toContain('bg-blue-100') // Blue.100 is `null`
expect(classes).not.toContain('bg-blue-200') // Blue.200 is `undefined`
expect(classes).not.toContain('bg-blue-100') // Blue.100 is `null`
expect(classes).not.toContain('bg-blue-200') // Blue.200 is `undefined`
expect(classes).toContain('bg-blue-50')
expect(classes).toContain('bg-blue-300')
})
expect(classes).toContain('bg-blue-50')
expect(classes).toContain('bg-blue-300')
})

View file

@ -1,7 +1,6 @@
import resolveConfig from '../src/public/resolve-config'
import { createContext } from '../src/lib/setupContextUtils'
import bigSign from '../src/util/bigSign'
import { crosscheck } from './util/run'
/**
* This is a function that the prettier-plugin-tailwindcss would use. It would
@ -25,121 +24,119 @@ function defaultSort(arrayOfTuples) {
.join(' ')
}
crosscheck(() => {
it('should return a list of tuples with the sort order', () => {
let input = 'font-bold underline hover:font-medium unknown'
let config = {}
let context = createContext(resolveConfig(config))
expect(context.getClassOrder(input.split(' '))).toEqual([
['font-bold', expect.any(BigInt)],
['underline', expect.any(BigInt)],
['hover:font-medium', expect.any(BigInt)],
it('should return a list of tuples with the sort order', () => {
let input = 'font-bold underline hover:font-medium unknown'
let config = {}
let context = createContext(resolveConfig(config))
expect(context.getClassOrder(input.split(' '))).toEqual([
['font-bold', expect.any(BigInt)],
['underline', expect.any(BigInt)],
['hover:font-medium', expect.any(BigInt)],
// Unknown values receive `null`
['unknown', null],
])
})
// Unknown values receive `null`
['unknown', null],
])
})
it.each([
// Utitlies
['px-3 p-1 py-3', 'p-1 px-3 py-3'],
it.each([
// Utitlies
['px-3 p-1 py-3', 'p-1 px-3 py-3'],
// Utitlies and components
['px-4 container', 'container px-4'],
// Utitlies and components
['px-4 container', 'container px-4'],
// Utilities with variants
['px-3 focus:hover:p-3 hover:p-1 py-3', 'px-3 py-3 hover:p-1 focus:hover:p-3'],
// Utilities with variants
['px-3 focus:hover:p-3 hover:p-1 py-3', 'px-3 py-3 hover:p-1 focus:hover:p-3'],
// Utitlies with important
['px-3 !py-4', '!py-4 px-3'],
['!py-4 px-3', '!py-4 px-3'],
// Utitlies with important
['px-3 !py-4', '!py-4 px-3'],
['!py-4 px-3', '!py-4 px-3'],
// Components with variants
['hover:container container', 'container hover:container'],
// Components with variants
['hover:container container', 'container hover:container'],
// Components and utilities with variants
[
'focus:hover:container hover:underline hover:container p-1',
'p-1 hover:container focus:hover:container hover:underline',
],
// Components and utilities with variants
[
'focus:hover:container hover:underline hover:container p-1',
'p-1 hover:container focus:hover:container hover:underline',
],
// Leave user css order alone, and move to the front
['b p-1 a', 'b a p-1'],
['hover:b focus:p-1 a', 'hover:b a focus:p-1'],
// Leave user css order alone, and move to the front
['b p-1 a', 'b a p-1'],
['hover:b focus:p-1 a', 'hover:b a focus:p-1'],
// Add special treatment for `group` and `peer`
['a peer container underline', 'a peer container underline'],
])('should sort "%s" based on the order we generate them in to "%s"', (input, output) => {
let config = {}
// Add special treatment for `group` and `peer`
['a peer container underline', 'a peer container underline'],
])('should sort "%s" based on the order we generate them in to "%s"', (input, output) => {
let config = {}
let context = createContext(resolveConfig(config))
expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
})
it.each([
// Utitlies
['tw-px-3 tw-p-1 tw-py-3', 'tw-p-1 tw-px-3 tw-py-3'],
// Utitlies and components
['tw-px-4 tw-container', 'tw-container tw-px-4'],
// Utilities with variants
[
'tw-px-3 focus:hover:tw-p-3 hover:tw-p-1 tw-py-3',
'tw-px-3 tw-py-3 hover:tw-p-1 focus:hover:tw-p-3',
],
// Utitlies with important
['tw-px-3 !tw-py-4', '!tw-py-4 tw-px-3'],
['!tw-py-4 tw-px-3', '!tw-py-4 tw-px-3'],
// Components with variants
['hover:tw-container tw-container', 'tw-container hover:tw-container'],
// Components and utilities with variants
[
'focus:hover:tw-container hover:tw-underline hover:tw-container tw-p-1',
'tw-p-1 hover:tw-container focus:hover:tw-container hover:tw-underline',
],
// Leave user css order alone, and move to the front
['b tw-p-1 a', 'b a tw-p-1'],
['hover:b focus:tw-p-1 a', 'hover:b a focus:tw-p-1'],
// Add special treatment for `group` and `peer`
['a tw-peer tw-container tw-underline', 'a tw-peer tw-container tw-underline'],
])(
'should sort "%s" with prefixex based on the order we generate them in to "%s"',
(input, output) => {
let config = { prefix: 'tw-' }
let context = createContext(resolveConfig(config))
expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
})
}
)
it.each([
// Utitlies
['tw-px-3 tw-p-1 tw-py-3', 'tw-p-1 tw-px-3 tw-py-3'],
// Utitlies and components
['tw-px-4 tw-container', 'tw-container tw-px-4'],
// Utilities with variants
it('sorts classes deterministically across multiple class lists', () => {
let classes = [
[
'tw-px-3 focus:hover:tw-p-3 hover:tw-p-1 tw-py-3',
'tw-px-3 tw-py-3 hover:tw-p-1 focus:hover:tw-p-3',
'a-class px-3 p-1 b-class py-3 bg-red-500 bg-blue-500',
'a-class b-class bg-blue-500 bg-red-500 p-1 px-3 py-3',
],
// Utitlies with important
['tw-px-3 !tw-py-4', '!tw-py-4 tw-px-3'],
['!tw-py-4 tw-px-3', '!tw-py-4 tw-px-3'],
// Components with variants
['hover:tw-container tw-container', 'tw-container hover:tw-container'],
// Components and utilities with variants
[
'focus:hover:tw-container hover:tw-underline hover:tw-container tw-p-1',
'tw-p-1 hover:tw-container focus:hover:tw-container hover:tw-underline',
'px-3 b-class p-1 py-3 bg-blue-500 a-class bg-red-500',
'b-class a-class bg-blue-500 bg-red-500 p-1 px-3 py-3',
],
]
// Leave user css order alone, and move to the front
['b tw-p-1 a', 'b a tw-p-1'],
['hover:b focus:tw-p-1 a', 'hover:b a focus:tw-p-1'],
let config = {}
// Add special treatment for `group` and `peer`
['a tw-peer tw-container tw-underline', 'a tw-peer tw-container tw-underline'],
])(
'should sort "%s" with prefixex based on the order we generate them in to "%s"',
(input, output) => {
let config = { prefix: 'tw-' }
let context = createContext(resolveConfig(config))
expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
}
)
// Same context, different class lists
let context = createContext(resolveConfig(config))
for (const [input, output] of classes) {
expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
}
it('sorts classes deterministically across multiple class lists', () => {
let classes = [
[
'a-class px-3 p-1 b-class py-3 bg-red-500 bg-blue-500',
'a-class b-class bg-blue-500 bg-red-500 p-1 px-3 py-3',
],
[
'px-3 b-class p-1 py-3 bg-blue-500 a-class bg-red-500',
'b-class a-class bg-blue-500 bg-red-500 p-1 px-3 py-3',
],
]
let config = {}
// Same context, different class lists
let context = createContext(resolveConfig(config))
for (const [input, output] of classes) {
expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
}
// Different context, different class lists
for (const [input, output] of classes) {
context = createContext(resolveConfig(config))
expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
}
})
// Different context, different class lists
for (const [input, output] of classes) {
context = createContext(resolveConfig(config))
expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output)
}
})

View file

@ -2,188 +2,185 @@ import postcss from 'postcss'
import selectorParser from 'postcss-selector-parser'
import resolveConfig from '../src/public/resolve-config'
import { createContext } from '../src/lib/setupContextUtils'
import { crosscheck } from './util/run'
crosscheck(() => {
it('should return a list of variants with meta information about the variant', () => {
let config = {}
let context = createContext(resolveConfig(config))
it('should return a list of variants with meta information about the variant', () => {
let config = {}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
let variants = context.getVariants()
expect(variants).toContainEqual({
name: 'hover',
isArbitrary: false,
hasDash: true,
values: [],
selectors: expect.any(Function),
})
expect(variants).toContainEqual({
name: 'group',
isArbitrary: true,
hasDash: true,
values: expect.any(Array),
selectors: expect.any(Function),
})
// `group-hover` now belongs to the `group` variant. The information exposed for the `group`
// variant is all you need.
expect(variants.find((v) => v.name === 'group-hover')).toBeUndefined()
expect(variants).toContainEqual({
name: 'hover',
isArbitrary: false,
hasDash: true,
values: [],
selectors: expect.any(Function),
})
it('should provide selectors for simple variants', () => {
let config = {}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
let variant = variants.find((v) => v.name === 'hover')
expect(variant.selectors()).toEqual(['&:hover'])
expect(variants).toContainEqual({
name: 'group',
isArbitrary: true,
hasDash: true,
values: expect.any(Array),
selectors: expect.any(Function),
})
it('should provide selectors for parallel variants', () => {
let config = {}
let context = createContext(resolveConfig(config))
// `group-hover` now belongs to the `group` variant. The information exposed for the `group`
// variant is all you need.
expect(variants.find((v) => v.name === 'group-hover')).toBeUndefined()
})
let variants = context.getVariants()
it('should provide selectors for simple variants', () => {
let config = {}
let context = createContext(resolveConfig(config))
let variant = variants.find((v) => v.name === 'marker')
expect(variant.selectors()).toEqual(['& *::marker', '&::marker'])
})
let variants = context.getVariants()
it('should provide selectors for complex matchVariant variants like `group`', () => {
let config = {}
let context = createContext(resolveConfig(config))
let variant = variants.find((v) => v.name === 'hover')
expect(variant.selectors()).toEqual(['&:hover'])
})
let variants = context.getVariants()
it('should provide selectors for parallel variants', () => {
let config = {}
let context = createContext(resolveConfig(config))
let variant = variants.find((v) => v.name === 'group')
expect(variant.selectors()).toEqual(['.group &'])
expect(variant.selectors({})).toEqual(['.group &'])
expect(variant.selectors({ value: 'hover' })).toEqual(['.group:hover &'])
expect(variant.selectors({ value: '.foo_&' })).toEqual(['.foo .group &'])
expect(variant.selectors({ modifier: 'foo', value: 'hover' })).toEqual(['.group\\/foo:hover &'])
expect(variant.selectors({ modifier: 'foo', value: '.foo_&' })).toEqual(['.foo .group\\/foo &'])
})
let variants = context.getVariants()
it('should provide selectors for variants with atrules', () => {
let config = {}
let context = createContext(resolveConfig(config))
let variant = variants.find((v) => v.name === 'marker')
expect(variant.selectors()).toEqual(['& *::marker', '&::marker'])
})
let variants = context.getVariants()
it('should provide selectors for complex matchVariant variants like `group`', () => {
let config = {}
let context = createContext(resolveConfig(config))
let variant = variants.find((v) => v.name === 'supports')
expect(variant.selectors({ value: 'display:grid' })).toEqual(['@supports (display:grid)'])
expect(variant.selectors({ value: 'aspect-ratio' })).toEqual([
'@supports (aspect-ratio: var(--tw))',
])
})
let variants = context.getVariants()
it('should provide selectors for custom plugins that do a combination of parallel variants with modifiers with arbitrary values and with atrules', () => {
let config = {
plugins: [
function ({ matchVariant }) {
matchVariant('foo', (value, { modifier }) => {
return [
`
let variant = variants.find((v) => v.name === 'group')
expect(variant.selectors()).toEqual(['.group &'])
expect(variant.selectors({})).toEqual(['.group &'])
expect(variant.selectors({ value: 'hover' })).toEqual(['.group:hover &'])
expect(variant.selectors({ value: '.foo_&' })).toEqual(['.foo .group &'])
expect(variant.selectors({ modifier: 'foo', value: 'hover' })).toEqual(['.group\\/foo:hover &'])
expect(variant.selectors({ modifier: 'foo', value: '.foo_&' })).toEqual(['.foo .group\\/foo &'])
})
it('should provide selectors for variants with atrules', () => {
let config = {}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
let variant = variants.find((v) => v.name === 'supports')
expect(variant.selectors({ value: 'display:grid' })).toEqual(['@supports (display:grid)'])
expect(variant.selectors({ value: 'aspect-ratio' })).toEqual([
'@supports (aspect-ratio: var(--tw))',
])
})
it('should provide selectors for custom plugins that do a combination of parallel variants with modifiers with arbitrary values and with atrules', () => {
let config = {
plugins: [
function ({ matchVariant }) {
matchVariant('foo', (value, { modifier }) => {
return [
`
@supports (foo: ${modifier}) {
@media (width <= 400px) {
&:hover
}
}
`,
`.${modifier}\\/${value} &:focus`,
]
})
},
],
}
let context = createContext(resolveConfig(config))
`.${modifier}\\/${value} &:focus`,
]
})
},
],
}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
let variants = context.getVariants()
let variant = variants.find((v) => v.name === 'foo')
expect(variant.selectors({ modifier: 'bar', value: 'baz' })).toEqual([
'@supports (foo: bar) { @media (width <= 400px) { &:hover } }',
'.bar\\/baz &:focus',
])
})
it('should work for plugins that still use the modifySelectors API', () => {
let config = {
plugins: [
function ({ addVariant }) {
addVariant('foo', ({ modifySelectors, container }) => {
// Manually mutating the selector
modifySelectors(({ selector }) => {
return selectorParser((selectors) => {
selectors.walkClasses((classNode) => {
classNode.value = `foo:${classNode.value}`
classNode.parent.insertBefore(classNode, selectorParser().astSync(`.foo `))
})
}).processSync(selector)
})
// Manually wrap in supports query
let wrapper = postcss.atRule({ name: 'supports', params: 'display: grid' })
let nodes = container.nodes
container.removeAll()
wrapper.append(nodes)
container.append(wrapper)
})
},
],
}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
let variant = variants.find((v) => v.name === 'foo')
expect(variant.selectors({})).toEqual(['@supports (display: grid) { .foo .foo\\:& }'])
})
it('should special case the `@`', () => {
let config = {
plugins: [
({ matchVariant }) => {
matchVariant(
'@',
(value, { modifier }) => `@container ${modifier ?? ''} (min-width: ${value})`,
{
modifiers: 'any',
values: {
xs: '20rem',
sm: '24rem',
md: '28rem',
lg: '32rem',
xl: '36rem',
'2xl': '42rem',
'3xl': '48rem',
'4xl': '56rem',
'5xl': '64rem',
'6xl': '72rem',
'7xl': '80rem',
},
}
)
},
],
}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
let variant = variants.find((v) => v.name === '@')
expect(variant).toEqual({
name: '@',
isArbitrary: true,
hasDash: false,
values: expect.any(Array),
selectors: expect.any(Function),
})
expect(variant.selectors({ value: 'xs', modifier: 'foo' })).toEqual([
'@container foo (min-width: 20rem)',
])
})
let variant = variants.find((v) => v.name === 'foo')
expect(variant.selectors({ modifier: 'bar', value: 'baz' })).toEqual([
'@supports (foo: bar) { @media (width <= 400px) { &:hover } }',
'.bar\\/baz &:focus',
])
})
it('should work for plugins that still use the modifySelectors API', () => {
let config = {
plugins: [
function ({ addVariant }) {
addVariant('foo', ({ modifySelectors, container }) => {
// Manually mutating the selector
modifySelectors(({ selector }) => {
return selectorParser((selectors) => {
selectors.walkClasses((classNode) => {
classNode.value = `foo:${classNode.value}`
classNode.parent.insertBefore(classNode, selectorParser().astSync(`.foo `))
})
}).processSync(selector)
})
// Manually wrap in supports query
let wrapper = postcss.atRule({ name: 'supports', params: 'display: grid' })
let nodes = container.nodes
container.removeAll()
wrapper.append(nodes)
container.append(wrapper)
})
},
],
}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
let variant = variants.find((v) => v.name === 'foo')
expect(variant.selectors({})).toEqual(['@supports (display: grid) { .foo .foo\\:& }'])
})
it('should special case the `@`', () => {
let config = {
plugins: [
({ matchVariant }) => {
matchVariant(
'@',
(value, { modifier }) => `@container ${modifier ?? ''} (min-width: ${value})`,
{
modifiers: 'any',
values: {
xs: '20rem',
sm: '24rem',
md: '28rem',
lg: '32rem',
xl: '36rem',
'2xl': '42rem',
'3xl': '48rem',
'4xl': '56rem',
'5xl': '64rem',
'6xl': '72rem',
'7xl': '80rem',
},
}
)
},
],
}
let context = createContext(resolveConfig(config))
let variants = context.getVariants()
let variant = variants.find((v) => v.name === '@')
expect(variant).toEqual({
name: '@',
isArbitrary: true,
hasDash: false,
values: expect.any(Array),
selectors: expect.any(Function),
})
expect(variant.selectors({ value: 'xs', modifier: 'foo' })).toEqual([
'@container foo (min-width: 20rem)',
])
})

View file

@ -1,79 +1,76 @@
import { html, css, run, crosscheck } from './util/run'
import { html, css, run } from './util/run'
crosscheck(({ oxide, stable }) => {
stable.test.todo('Import processing is an oxide engine feature')
oxide.describe('import processing', () => {
it('should be possible to import another css file', async () => {
let config = {
darkMode: 'class',
content: [
{
raw: html`<div class="underline" />`,
},
],
corePlugins: { preflight: false },
describe('import processing', () => {
it('should be possible to import another css file', async () => {
let config = {
darkMode: 'class',
content: [
{
raw: html`<div class="underline" />`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@import './import-processing-a.css';
`
let result = await run(input, config)
expect(result.css).toMatchFormattedCss(css`
.underline {
text-decoration-line: underline;
}
`)
})
it('should be possible to import another css file after @tailwind directive', async () => {
let config = {
darkMode: 'class',
content: [
{
raw: html`<div class="foo underline" />`,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind utilities;
@import './import-processing-b.css';
`
let result = await run(input, config)
expect(result.css).toMatchFormattedCss(css`
.underline {
text-decoration-line: underline;
}
let input = css`
@import './import-processing-a.css';
`
.foo {
color: red;
}
`)
})
let result = await run(input, config)
it('should be possible to add @config before @import statements', async () => {
let input = css`
@config "./import-processing-c.js";
@import './import-processing-c.css';
`
expect(result.css).toMatchFormattedCss(css`
.underline {
text-decoration-line: underline;
}
`)
})
let result = await run(input)
it('should be possible to import another css file after @tailwind directive', async () => {
let config = {
darkMode: 'class',
content: [
{
raw: html`<div class="foo underline" />`,
},
],
corePlugins: { preflight: false },
expect(result.css).toMatchFormattedCss(css`
.underline {
text-decoration-line: underline;
}
let input = css`
@tailwind utilities;
@import './import-processing-b.css';
`
let result = await run(input, config)
expect(result.css).toMatchFormattedCss(css`
.underline {
text-decoration-line: underline;
}
.foo {
color: red;
}
`)
})
it('should be possible to add @config before @import statements', async () => {
let input = css`
@config "./import-processing-c.js";
@import './import-processing-c.css';
`
let result = await run(input)
expect(result.css).toMatchFormattedCss(css`
.underline {
text-decoration-line: underline;
}
.foo {
color: red;
}
`)
})
.foo {
color: red;
}
`)
})
})

View file

@ -1,129 +1,82 @@
import { crosscheck, run, html, css, defaults } from './util/run'
import { run, html, css, defaults } from './util/run'
crosscheck(({ stable, oxide }) => {
test('using @import instead of @tailwind', () => {
let config = {
content: [
{
raw: html`
<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>
`,
},
],
corePlugins: { preflight: false },
plugins: [
function ({ addBase }) {
addBase({
h1: {
fontSize: '32px',
},
})
},
],
}
test('using @import instead of @tailwind', () => {
let config = {
content: [
{
raw: html`
<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>
`,
},
],
corePlugins: { preflight: false },
plugins: [
function ({ addBase }) {
addBase({
h1: {
fontSize: '32px',
},
})
},
],
}
let input = css`
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
`
let input = css`
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
`
return run(input, config).then((result) => {
stable.expect(result.css).toMatchFormattedCss(css`
h1 {
font-size: 32px;
}
${defaults}
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
h1 {
font-size: 32px;
}
${defaults}
.container {
width: 100%;
}
@media (min-width: 640px) {
.container {
width: 100%;
max-width: 640px;
}
@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: rgb(0 0 0 / var(--tw-bg-opacity));
}
@media (min-width: 768px) {
.md\:hover\:text-center:hover {
text-align: center;
}
}
`)
oxide.expect(result.css).toMatchFormattedCss(css`
h1 {
font-size: 32px;
}
${defaults}
}
@media (min-width: 768px) {
.container {
width: 100%;
max-width: 768px;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
.mt-6 {
margin-top: 1.5rem;
}
.bg-black {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
@media (min-width: 768px) {
.md\:hover\:text-center:hover {
text-align: center;
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
.mt-6 {
margin-top: 1.5rem;
}
.bg-black {
background-color: #000;
}
@media (min-width: 768px) {
.md\:hover\:text-center:hover {
text-align: center;
}
}
`)
})
}
`)
})
})

View file

@ -2,230 +2,228 @@ import fs from 'fs'
import path from 'path'
import * as sharedState from '../src/lib/sharedState'
import { crosscheck, run, html, css, defaults } from './util/run'
import { run, html, css, defaults } from './util/run'
crosscheck(() => {
test('important boolean', () => {
let config = {
important: true,
darkMode: 'class',
content: [
{
raw: html`
<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>
`,
},
],
corePlugins: { preflight: false },
plugins: [
function ({ addComponents, addUtilities }) {
addComponents(
{
'.btn': {
button: 'yes',
},
test('important boolean', () => {
let config = {
important: true,
darkMode: 'class',
content: [
{
raw: html`
<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>
`,
},
],
corePlugins: { preflight: false },
plugins: [
function ({ addComponents, addUtilities }) {
addComponents(
{
'.btn': {
button: 'yes',
},
{ respectImportant: true }
)
addComponents(
{
'@font-face': {
'font-family': 'Inter',
},
'@page': {
margin: '1cm',
},
},
{ respectImportant: true }
)
addComponents(
{
'@font-face': {
'font-family': 'Inter',
},
{ respectImportant: true }
)
addUtilities(
{
'.custom-util': {
button: 'no',
},
'@page': {
margin: '1cm',
},
{ respectImportant: false }
)
},
],
},
{ respectImportant: true }
)
addUtilities(
{
'.custom-util': {
button: 'no',
},
},
{ respectImportant: false }
)
},
],
}
let input = css`
@tailwind base;
@tailwind components;
@layer components {
.custom-component {
@apply font-bold;
}
.custom-important-component {
@apply text-center !important;
}
}
@tailwind utilities;
`
let input = css`
@tailwind base;
@tailwind components;
@layer components {
.custom-component {
@apply font-bold;
}
.custom-important-component {
@apply text-center !important;
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.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: 1s linear infinite spin !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;
}
:is([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;
}
}
:is(.dark .dark\:focus\:text-left:focus) {
text-align: left !important;
}
@media (min-width: 768px) {
.md\:hover\:text-right:hover {
text-align: right !important;
}
}
`)
})
})
// This is in a describe block so we can use `afterEach` :)
describe('duplicate elision', () => {
let filePath = path.resolve(__dirname, './important-boolean-duplicates.test.html')
afterEach(async () => await fs.promises.unlink(filePath))
test('important rules are not duplicated when rebuilding', async () => {
let config = {
important: true,
content: [filePath],
}
await fs.promises.writeFile(
config.content[0],
html`
<div class="ml-2"></div>
<div class="ml-4"></div>
`
)
let input = css`
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.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: 1s linear infinite spin !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;
}
:is([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;
}
}
:is(.dark .dark\:focus\:text-left:focus) {
text-align: left !important;
}
@media (min-width: 768px) {
.md\:hover\:text-right:hover {
text-align: right !important;
}
}
`)
})
})
let result = await run(input, config)
let allContexts = Array.from(sharedState.contextMap.values())
// This is in a describe block so we can use `afterEach` :)
describe('duplicate elision', () => {
let filePath = path.resolve(__dirname, './important-boolean-duplicates.test.html')
let context = allContexts[allContexts.length - 1]
afterEach(async () => await fs.promises.unlink(filePath))
let ruleCacheSize1 = context.ruleCache.size
test('important rules are not duplicated when rebuilding', async () => {
let config = {
important: true,
content: [filePath],
expect(result.css).toMatchFormattedCss(css`
.ml-2 {
margin-left: 0.5rem !important;
}
.ml-4 {
margin-left: 1rem !important;
}
`)
await fs.promises.writeFile(
config.content[0],
html`
<div class="ml-2"></div>
<div class="ml-4"></div>
`
)
let input = css`
@tailwind utilities;
await fs.promises.writeFile(
config.content[0],
html`
<div class="ml-2"></div>
<div class="ml-6"></div>
`
)
let result = await run(input, config)
let allContexts = Array.from(sharedState.contextMap.values())
result = await run(input, config)
let context = allContexts[allContexts.length - 1]
let ruleCacheSize2 = context.ruleCache.size
let ruleCacheSize1 = context.ruleCache.size
expect(result.css).toMatchFormattedCss(css`
.ml-2 {
margin-left: 0.5rem !important;
}
.ml-4 {
margin-left: 1rem !important;
}
.ml-6 {
margin-left: 1.5rem !important;
}
`)
expect(result.css).toMatchFormattedCss(css`
.ml-2 {
margin-left: 0.5rem !important;
}
.ml-4 {
margin-left: 1rem !important;
}
`)
await fs.promises.writeFile(
config.content[0],
html`
<div class="ml-2"></div>
<div class="ml-6"></div>
`
)
result = await run(input, config)
let ruleCacheSize2 = context.ruleCache.size
expect(result.css).toMatchFormattedCss(css`
.ml-2 {
margin-left: 0.5rem !important;
}
.ml-4 {
margin-left: 1rem !important;
}
.ml-6 {
margin-left: 1.5rem !important;
}
`)
// The rule cache was effectively doubling in size previously
// because the rule cache was never de-duped
// This ensures this behavior doesn't return
expect(ruleCacheSize2 - ruleCacheSize1).toBeLessThan(10)
})
// The rule cache was effectively doubling in size previously
// because the rule cache was never de-duped
// This ensures this behavior doesn't return
expect(ruleCacheSize2 - ruleCacheSize1).toBeLessThan(10)
})
})

View file

@ -1,79 +1,77 @@
import { crosscheck, run, html, css, defaults } from './util/run'
import { run, html, css, defaults } from './util/run'
crosscheck(() => {
test('important modifier with prefix', () => {
let config = {
important: false,
prefix: 'tw-',
darkMode: 'class',
content: [
{
raw: html`<!-- The string "!*" can cause problems if we don't handle it, let's include it -->
<div class="!*"></div>
<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> `,
},
],
corePlugins: { preflight: false },
}
test('important modifier with prefix', () => {
let config = {
important: false,
prefix: 'tw-',
darkMode: 'class',
content: [
{
raw: html`<!-- The string "!*" can cause problems if we don't handle it, let's include it -->
<div class="!*"></div>
<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> `,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.\!tw-container {
width: 100% !important;
}
@media (min-width: 640px) {
.\!tw-container {
width: 100% !important;
max-width: 640px !important;
}
@media (min-width: 640px) {
.\!tw-container {
max-width: 640px !important;
}
}
@media (min-width: 768px) {
.\!tw-container {
max-width: 768px !important;
}
@media (min-width: 768px) {
.\!tw-container {
max-width: 768px !important;
}
}
@media (min-width: 1024px) {
.\!tw-container {
max-width: 1024px !important;
}
@media (min-width: 1024px) {
.\!tw-container {
max-width: 1024px !important;
}
}
@media (min-width: 1280px) {
.\!tw-container {
max-width: 1280px !important;
}
@media (min-width: 1280px) {
.\!tw-container {
max-width: 1280px !important;
}
}
@media (min-width: 1536px) {
.\!tw-container {
max-width: 1536px !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;
}
.\!tw-font-bold {
font-weight: 700 !important;
}
@media (min-width: 1280px) {
.xl\:focus\:disabled\:\!tw-float-right:disabled:focus {
float: right !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:disabled:focus {
float: right !important;
}
}
`)
})
}
`)
})
})

View file

@ -1,153 +1,151 @@
import { crosscheck, run, html, css } from './util/run'
import { run, html, css } from './util/run'
crosscheck(() => {
test('important modifier', () => {
let config = {
important: false,
darkMode: 'class',
content: [
{
raw: html`
<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>
<div class="!custom-parent-5"></div>
<div class="btn !disabled"></div>
`,
},
],
corePlugins: { preflight: false },
plugins: [
function ({ theme, matchUtilities, addComponents }) {
matchUtilities(
{
'custom-parent': (value) => {
return {
'.custom-child': {
margin: value,
},
}
},
test('important modifier', () => {
let config = {
important: false,
darkMode: 'class',
content: [
{
raw: html`
<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>
<div class="!custom-parent-5"></div>
<div class="btn !disabled"></div>
`,
},
],
corePlugins: { preflight: false },
plugins: [
function ({ theme, matchUtilities, addComponents }) {
matchUtilities(
{
'custom-parent': (value) => {
return {
'.custom-child': {
margin: value,
},
}
},
{ values: theme('spacing') }
)
addComponents({
'.btn': {
'&.disabled, &:disabled': {
color: 'gray',
},
},
{ values: theme('spacing') }
)
addComponents({
'.btn': {
'&.disabled, &:disabled': {
color: 'gray',
},
})
},
],
}
},
})
},
],
}
let input = css`
@tailwind components;
@tailwind utilities;
`
let input = css`
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.\!container {
width: 100% !important;
}
@media (min-width: 640px) {
.\!container {
width: 100% !important;
max-width: 640px !important;
}
@media (min-width: 640px) {
.\!container {
max-width: 640px !important;
}
}
@media (min-width: 768px) {
.\!container {
max-width: 768px !important;
}
@media (min-width: 768px) {
.\!container {
max-width: 768px !important;
}
}
@media (min-width: 1024px) {
.\!container {
max-width: 1024px !important;
}
@media (min-width: 1024px) {
.\!container {
max-width: 1024px !important;
}
}
@media (min-width: 1280px) {
.\!container {
max-width: 1280px !important;
}
@media (min-width: 1280px) {
.\!container {
max-width: 1280px !important;
}
}
@media (min-width: 1536px) {
.\!container {
max-width: 1536px !important;
}
@media (min-width: 1536px) {
.\!container {
max-width: 1536px !important;
}
}
.btn.disabled,
.btn:disabled {
color: gray;
}
.btn.\!disabled {
color: gray !important;
}
.\!font-bold {
font-weight: 700 !important;
}
.\!custom-parent-5 .custom-child {
margin: 1.25rem !important;
}
.hover\:\!text-center:hover {
text-align: center !important;
}
@media (min-width: 1024px) {
.lg\:\!opacity-50 {
opacity: 0.5 !important;
}
.btn.disabled,
.btn:disabled {
color: gray;
}
@media (min-width: 1280px) {
.xl\:focus\:disabled\:\!float-right:disabled:focus {
float: right !important;
}
.btn.\!disabled {
color: gray !important;
}
.\!font-bold {
font-weight: 700 !important;
}
.\!custom-parent-5 .custom-child {
margin: 1.25rem !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:disabled:focus {
float: right !important;
}
}
`)
})
})
test('the important modifier works on utilities using :where()', () => {
let config = {
content: [
{
raw: html` <div class="btn hover:btn !btn hover:focus:disabled:!btn"></div> `,
},
],
corePlugins: { preflight: false },
plugins: [
function ({ addComponents }) {
addComponents({
':where(.btn)': {
backgroundColor: '#00f',
},
})
},
],
}
let input = css`
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
:where(.\!btn) {
background-color: #00f !important;
}
:where(.btn) {
background-color: #00f;
}
:where(.hover\:btn:hover) {
background-color: #00f;
}
:where(.hover\:focus\:disabled\:\!btn:disabled:focus:hover) {
background-color: #00f !important;
}
`)
})
}
`)
})
})
test('the important modifier works on utilities using :where()', () => {
let config = {
content: [
{
raw: html` <div class="btn hover:btn !btn hover:focus:disabled:!btn"></div> `,
},
],
corePlugins: { preflight: false },
plugins: [
function ({ addComponents }) {
addComponents({
':where(.btn)': {
backgroundColor: '#00f',
},
})
},
],
}
let input = css`
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
:where(.\!btn) {
background-color: #00f !important;
}
:where(.btn) {
background-color: #00f;
}
:where(.hover\:btn:hover) {
background-color: #00f;
}
:where(.hover\:focus\:disabled\:\!btn:disabled:focus:hover) {
background-color: #00f !important;
}
`)
})
})

View file

@ -1,205 +1,195 @@
import { crosscheck, run, html, css, defaults } from './util/run'
import { run, html, css, defaults } from './util/run'
crosscheck(({ stable, oxide }) => {
test('important selector', () => {
let config = {
important: '#app',
darkMode: 'class',
content: [
{
raw: html`
<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>
<div class="dark:before:underline"></div>
<div class="hover:[&::file-selector-button]:rtl:dark:bg-black/100"></div>
`,
},
],
corePlugins: { preflight: false },
plugins: [
function ({ addComponents, addUtilities }) {
addComponents(
{
'.btn': {
button: 'yes',
},
test('important selector', () => {
let config = {
important: '#app',
darkMode: 'class',
content: [
{
raw: html`
<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>
<div class="dark:before:underline"></div>
<div class="hover:[&::file-selector-button]:rtl:dark:bg-black/100"></div>
`,
},
],
corePlugins: { preflight: false },
plugins: [
function ({ addComponents, addUtilities }) {
addComponents(
{
'.btn': {
button: 'yes',
},
{ respectImportant: true }
)
addComponents(
{
'@font-face': {
'font-family': 'Inter',
},
'@page': {
margin: '1cm',
},
},
{ respectImportant: true }
)
addComponents(
{
'@font-face': {
'font-family': 'Inter',
},
{ respectImportant: true }
)
addUtilities(
{
'.custom-util': {
button: 'no',
},
'@page': {
margin: '1cm',
},
{ respectImportant: false }
)
},
],
},
{ respectImportant: true }
)
addUtilities(
{
'.custom-util': {
button: 'no',
},
},
{ respectImportant: false }
)
},
],
}
let input = css`
@tailwind base;
@tailwind components;
@layer components {
.custom-component {
@apply font-bold;
}
.custom-important-component {
@apply text-center !important;
}
}
@tailwind utilities;
`
let input = css`
@tailwind base;
@tailwind components;
@layer components {
.custom-component {
@apply font-bold;
}
.custom-important-component {
@apply text-center !important;
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
.container {
width: 100%;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
@media (min-width: 768px) {
.container {
width: 100%;
max-width: 768px;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
#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);
}
@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 :is(.animate-spin) {
animation: 1s linear infinite spin;
}
#app :is(.font-bold) {
font-weight: 700;
}
.custom-util {
button: no;
}
#app :is(.group:hover .group-hover\:focus-within\:text-left:focus-within) {
text-align: left;
}
#app :is(:is([dir='rtl'] .rtl\:active\:text-center:active)) {
}
#app :is(.animate-spin) {
animation: 1s linear infinite spin;
}
#app :is(.font-bold) {
font-weight: 700;
}
.custom-util {
button: no;
}
#app :is(.group:hover .group-hover\:focus-within\:text-left:focus-within) {
text-align: left;
}
#app :is(:is([dir='rtl'] .rtl\:active\:text-center:active)) {
text-align: center;
}
@media (prefers-reduced-motion: no-preference) {
#app :is(.motion-safe\:hover\:text-center:hover) {
text-align: center;
}
@media (prefers-reduced-motion: no-preference) {
#app :is(.motion-safe\:hover\:text-center:hover) {
text-align: center;
}
}
#app :is(.dark .dark\:before\:underline):before {
content: var(--tw-content);
text-decoration-line: underline;
}
#app :is(:is(.dark .dark\:focus\:text-left:focus)) {
text-align: left;
}
@media (min-width: 768px) {
#app :is(.md\:hover\:text-right:hover) {
text-align: right;
}
#app :is(.dark .dark\:before\:underline):before {
content: var(--tw-content);
text-decoration-line: underline;
}
#app :is(:is(.dark .dark\:focus\:text-left:focus)) {
text-align: left;
}
@media (min-width: 768px) {
#app :is(.md\:hover\:text-right:hover) {
text-align: right;
}
}
#app
:is(
[dir='rtl'] :is(.dark .hover\:\[\&\:\:file-selector-button\]\:rtl\:dark\:bg-black\/100)
)::file-selector-button:hover {
background-color: #000;
}
`)
})
})
test('pseudo-elements are appended after the `:is()`', () => {
let config = {
important: '#app',
darkMode: 'class',
content: [
{
raw: html` <div class="dark:before:bg-black"></div> `,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
stable.expect(result.css).toMatchFormattedCss(css`
${defaults}
#app :is(.dark .dark\:before\:bg-black)::before {
content: var(--tw-content);
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
`)
oxide.expect(result.css).toMatchFormattedCss(css`
${defaults}
#app :is(.dark .dark\:before\:bg-black)::before {
content: var(--tw-content);
background-color: #000;
}
`)
})
}
#app
:is(
[dir='rtl'] :is(.dark .hover\:\[\&\:\:file-selector-button\]\:rtl\:dark\:bg-black\/100)
)::file-selector-button:hover {
background-color: #000;
}
`)
})
})
test('pseudo-elements are appended after the `:is()`', () => {
let config = {
important: '#app',
darkMode: 'class',
content: [
{
raw: html` <div class="dark:before:flex"></div> `,
},
],
corePlugins: { preflight: false },
}
let input = css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
${defaults}
#app :is(.dark .dark\:before\:flex)::before {
content: var(--tw-content);
display: flex;
}
`)
})
})

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