mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
introduce v4 codebase
This commit is contained in:
parent
32cf8aa0fb
commit
a68de1df27
81
.github/workflows/ci.yml
vendored
Normal file
81
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,81 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: ^8.15.0
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
||||
# 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: pnpm install
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Test
|
||||
run: pnpm run test
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: npm run test:ui
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: packages/tailwindcss/playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
- name: Bench
|
||||
run: pnpm run bench
|
||||
214
.github/workflows/release.yml
vendored
Normal file
214
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,214 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_channel:
|
||||
description: 'Release channel'
|
||||
required: false
|
||||
default: 'internal'
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
APP_NAME: tailwindcss-oxide
|
||||
NODE_VERSION: 20
|
||||
PNPM_VERSION: ^8.15.0
|
||||
OXIDE_LOCATION: ./oxide/crates/node
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
# Windows
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
# macOS
|
||||
- os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
strip: strip -x # Must use -x on macOS. This produces larger results on linux.
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
page-size: 14
|
||||
strip: strip -x # Must use -x on macOS. This produces larger results on linux.
|
||||
# Linux
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
strip: strip
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-gnu
|
||||
strip: llvm-strip
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
|
||||
- os: ubuntu-latest
|
||||
target: armv7-unknown-linux-gnueabihf
|
||||
strip: llvm-strip
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-zig
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-musl
|
||||
strip: aarch64-linux-musl-strip
|
||||
download: true
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
strip: strip
|
||||
download: true
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
|
||||
|
||||
name: Build ${{ matrix.target }} (OXIDE)
|
||||
runs-on: ${{ matrix.os }}
|
||||
container: ${{ matrix.container }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'pnpm'
|
||||
|
||||
# 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:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Setup cross compile toolchain
|
||||
if: ${{ matrix.setup }}
|
||||
run: ${{ matrix.setup }}
|
||||
|
||||
- name: Install Rust (Stable)
|
||||
if: ${{ matrix.download }}
|
||||
run: |
|
||||
rustup default stable
|
||||
|
||||
- name: Setup rust target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --ignore-scripts --filter=!./playgrounds/*
|
||||
|
||||
- name: Build release
|
||||
run: pnpm run build --filter ${{ env.OXIDE_LOCATION }}
|
||||
env:
|
||||
RUST_TARGET: ${{ matrix.target }}
|
||||
JEMALLOC_SYS_WITH_LG_PAGE: ${{ matrix.page-size }}
|
||||
|
||||
- name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034
|
||||
if: ${{ matrix.strip }}
|
||||
run: ${{ matrix.strip }} ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: bindings-${{ matrix.target }}
|
||||
path: ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
name: Build and release Tailwind CSS
|
||||
|
||||
needs:
|
||||
- build
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'pnpm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
# 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 dependencies
|
||||
run: pnpm install --ignore-scripts --filter=!./playgrounds/*
|
||||
|
||||
- name: Build Tailwind CSS
|
||||
run: pnpm run build
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: ${{ env.OXIDE_LOCATION }}
|
||||
|
||||
- name: Move artifacts
|
||||
run: |
|
||||
cd ${{ env.OXIDE_LOCATION }}
|
||||
cp bindings-x86_64-pc-windows-msvc/* ./npm/win32-x64-msvc/
|
||||
cp bindings-x86_64-apple-darwin/* ./npm/darwin-x64/
|
||||
cp bindings-aarch64-apple-darwin/* ./npm/darwin-arm64/
|
||||
cp bindings-aarch64-unknown-linux-gnu/* ./npm/linux-arm64-gnu/
|
||||
cp bindings-aarch64-unknown-linux-musl/* ./npm/linux-arm64-musl/
|
||||
cp bindings-armv7-unknown-linux-gnueabihf/* ./npm/linux-arm-gnueabihf/
|
||||
cp bindings-x86_64-unknown-linux-gnu/* ./npm/linux-x64-gnu/
|
||||
cp bindings-x86_64-unknown-linux-musl/* ./npm/linux-x64-musl/
|
||||
|
||||
- name: Lock pre-release versions
|
||||
run: node ./scripts/lock-pre-release-versions.mjs
|
||||
|
||||
- name: Publish
|
||||
run: pnpm --recursive publish --tag ${{ inputs.release_channel }} --no-git-checks
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
dist/
|
||||
coverage/
|
||||
.turbo
|
||||
test-results/
|
||||
playwright-report/
|
||||
blob-report/
|
||||
playwright/.cache/
|
||||
8
.prettierignore
Normal file
8
.prettierignore
Normal file
@ -0,0 +1,8 @@
|
||||
coverage/
|
||||
node_modules/
|
||||
pnpm-lock.yaml
|
||||
oxide/target/
|
||||
oxide/crates/node/index.d.ts
|
||||
oxide/crates/node/index.js
|
||||
.next
|
||||
.fingerprint
|
||||
1
oxide/.gitignore
vendored
Normal file
1
oxide/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
target/
|
||||
1200
oxide/Cargo.lock
generated
Normal file
1200
oxide/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
oxide/Cargo.toml
Normal file
6
oxide/Cargo.toml
Normal file
@ -0,0 +1,6 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["crates/*"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
1
oxide/README.md
Normal file
1
oxide/README.md
Normal file
@ -0,0 +1 @@
|
||||
## Tailwind CSS Oxide
|
||||
29
oxide/crates/core/Cargo.toml
Normal file
29
oxide/crates/core/Cargo.toml
Normal file
@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "tailwindcss-core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bstr = "1.0.1"
|
||||
globwalk = "0.8.1"
|
||||
log = "0.4"
|
||||
rayon = "1.5.3"
|
||||
fxhash = "0.2.1"
|
||||
crossbeam = "0.8.2"
|
||||
tracing = { version = "0.1.37", features = [] }
|
||||
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
||||
walkdir = "2.3.3"
|
||||
ignore = "0.4.20"
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.3", features = ['html_reports'] }
|
||||
tempfile = "3.5.0"
|
||||
|
||||
[[bench]]
|
||||
name = "parse_candidates"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "scan_files"
|
||||
harness = false
|
||||
2203
oxide/crates/core/benches/fixtures/template-000.html
Normal file
2203
oxide/crates/core/benches/fixtures/template-000.html
Normal file
File diff suppressed because it is too large
Load Diff
4400
oxide/crates/core/benches/fixtures/template-001.html
Normal file
4400
oxide/crates/core/benches/fixtures/template-001.html
Normal file
File diff suppressed because it is too large
Load Diff
9027
oxide/crates/core/benches/fixtures/template-002.html
Normal file
9027
oxide/crates/core/benches/fixtures/template-002.html
Normal file
File diff suppressed because it is too large
Load Diff
1797
oxide/crates/core/benches/fixtures/template-003.html
Normal file
1797
oxide/crates/core/benches/fixtures/template-003.html
Normal file
File diff suppressed because it is too large
Load Diff
1935
oxide/crates/core/benches/fixtures/template-004.html
Normal file
1935
oxide/crates/core/benches/fixtures/template-004.html
Normal file
File diff suppressed because it is too large
Load Diff
5
oxide/crates/core/benches/fixtures/template-005.html
Normal file
5
oxide/crates/core/benches/fixtures/template-005.html
Normal file
@ -0,0 +1,5 @@
|
||||
<div class="sm:items-baseline sm:space-x-4 flex">
|
||||
<a href="#" target="_blank" class="block sm:hidden px-4 py-2 text-sm">
|
||||
This is link afe0b95d-703b-4471-b828-7fee034e18a0
|
||||
</a>
|
||||
</div>
|
||||
552
oxide/crates/core/benches/fixtures/template-006.html
Normal file
552
oxide/crates/core/benches/fixtures/template-006.html
Normal file
@ -0,0 +1,552 @@
|
||||
<div class="font-semibold px-3 text-left text-gray-900 py-3.5 text-sm">
|
||||
<nav class="font-medium text-gray-900">
|
||||
<ul class="h-7 justify-center rounded-full items-center w-7 flex mx-auto">
|
||||
<li class="h-0.5 inset-x-0 absolute bottom-0">
|
||||
<a href="#" target="_blank" class="space-y-1 px-2 mt-3">
|
||||
This is link 4132f37a-a03f-4776-9a8e-1b70ff626f71
|
||||
</a>
|
||||
<img
|
||||
class="text-gray-900 font-medium text-sm ml-3.5"
|
||||
alt="Profile picture of user 40faf8f0-6221-4ec4-a65e-fe22e1d9abd2"
|
||||
src="https://example.org/pictures/4f7d7f80-e9cd-447a-9b35-f9d93befe025"
|
||||
/>
|
||||
</li>
|
||||
<li class="text-indigo-700 order-1 font-semibold">
|
||||
<ol class="h-24 sm:w-32 w-24 object-center rounded-md sm:h-32 object-cover">
|
||||
<li class="lg:justify-center lg:gap-x-12 hidden lg:flex lg:min-w-0 lg:flex-1">
|
||||
<img
|
||||
class="hover:bg-gray-100 bg-gray-50 py-1.5 focus:z-10 text-gray-400"
|
||||
alt="Profile picture of user d27b5a21-1622-4f3a-ba7d-6230fae487c2"
|
||||
src="https://example.org/pictures/2a026d06-0e67-467d-babf-8a18614667f2"
|
||||
/>
|
||||
<ul class="w-6 h-6 mr-3 flex-shrink-0">
|
||||
<li class="flow-root">
|
||||
<img
|
||||
class="bg-white focus:ring-indigo-500 group font-medium items-center inline-flex focus:ring-offset-2 focus:ring-2 focus:outline-none text-base rounded-md hover:text-gray-900"
|
||||
alt="Profile picture of user 0052230e-90d8-4d84-ab64-904b87fe4622"
|
||||
src="https://example.org/pictures/f7ec91ba-17a7-470f-a472-705a8b5a79ce"
|
||||
/>
|
||||
</li>
|
||||
<li class="items-center right-0 flex pointer-events-none absolute inset-y-0">
|
||||
<img
|
||||
class="ml-3"
|
||||
alt="Profile picture of user a7c51adf-3917-4e41-b5f1-d263768c9adf"
|
||||
src="https://example.org/pictures/a56323a8-8aa3-4d39-8c79-9ee875ecd6f0"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="hover:bg-opacity-75 hover:bg-indigo-500 text-white">
|
||||
<li class="font-medium hover:text-indigo-500 text-indigo-600 text-sm">
|
||||
<ul class="sr-only"></ul>
|
||||
<ol class="flex-col px-8 flex pt-8"></ol>
|
||||
<a href="#" class="items-center sm:items-start flex">
|
||||
This is link 8c59d1ad-9ef0-4a41-ab15-e9fe04d77ae2
|
||||
</a>
|
||||
</li>
|
||||
<li class="rounded-full w-8 h-8">
|
||||
<ol class="lg:grid lg:grid-cols-12"></ol>
|
||||
<a
|
||||
href="#"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="px-4 border-t py-6 space-y-6 border-gray-200"
|
||||
>
|
||||
This is link 4182f198-ff54-45a4-ace9-e6f46a60ec92
|
||||
</a>
|
||||
</li>
|
||||
<li class="border-gray-700 border-t pt-4 pb-3">
|
||||
<ol class="font-medium text-gray-900 p-2 block -m-2"></ol>
|
||||
<ol
|
||||
class="sm:py-24 to-green-400 lg:px-0 bg-gradient-to-r lg:items-center lg:justify-end sm:px-6 lg:bg-none from-cyan-600 px-4 lg:pl-8 py-16 lg:flex"
|
||||
></ol>
|
||||
<img
|
||||
class="lg:gap-24 lg:grid-cols-2 lg:grid lg:mx-auto lg:items-start lg:max-w-7xl lg:px-8"
|
||||
alt="Profile picture of user c44d18a8-a1f1-4bf4-87b0-89f37e34aba1"
|
||||
src="https://example.org/pictures/527dca2c-5afe-4a5c-96cd-706396701c36"
|
||||
/>
|
||||
</li>
|
||||
<li class="hover:bg-gray-100 bg-white focus:z-10 text-gray-900 relative py-1.5">
|
||||
<a
|
||||
href="#"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="text-sm font-medium text-gray-500"
|
||||
>
|
||||
This is link f6db0c8d-6409-4abd-9af1-d3e68ebbd25c
|
||||
</a>
|
||||
<a href="#" rel="noreferrer" class="text-sm font-medium mt-12">
|
||||
This is link 51c9b242-e449-44d8-9289-1a5d474d5fbb
|
||||
</a>
|
||||
<ul class="h-12"></ul>
|
||||
</li>
|
||||
<li class="bg-gray-100">
|
||||
<a href="#" rel="noreferrer" class="bg-gray-100">
|
||||
This is link eb479051-0dff-4d8f-9456-bb8b56ab90af
|
||||
</a>
|
||||
<img
|
||||
class="ml-3 text-gray-900 font-medium text-base"
|
||||
alt="Profile picture of user 17d797ac-aea7-4154-b522-f0ac89762b0b"
|
||||
src="https://example.org/pictures/55e31a5b-2fcb-4b35-832c-fad711717f1a"
|
||||
/>
|
||||
<ul class="font-medium hover:text-gray-700 text-gray-500 ml-4 text-sm"></ul>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="space-y-6 border-t py-6 border-gray-200 px-4">
|
||||
<li class="md:hidden z-40 relative">
|
||||
<ol class="h-7 w-7 mx-auto items-center justify-center rounded-full flex"></ol>
|
||||
<ol class="items-center flex rounded-full w-7 mx-auto h-7 justify-center"></ol>
|
||||
<a
|
||||
href="#"
|
||||
target="_blank"
|
||||
class="sm:py-32 lg:px-8 max-w-7xl mx-auto px-4 py-24 sm:px-6 relative"
|
||||
>
|
||||
This is link 5014471c-4f44-4696-a1f6-7817d57827eb
|
||||
</a>
|
||||
<img
|
||||
class="h-5 group-hover:text-gray-500 w-5 ml-2"
|
||||
alt="Profile picture of user 98731873-0af8-4823-9115-f1333a2cdc2e"
|
||||
src="https://example.org/pictures/12ac8cbf-4540-49ff-a71f-4b0ac684b374"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
class="focus:outline-none font-medium px-4 justify-center text-sm focus:ring-2 border hover:bg-gray-50 focus:ring-offset-2 py-2 border-gray-300 bg-white inline-flex text-gray-700 rounded-md shadow-sm focus:ring-gray-900"
|
||||
>
|
||||
<img
|
||||
class="bg-white focus:ring-indigo-500 text-base focus:outline-none focus:ring-2 items-center font-medium focus:ring-offset-2 hover:text-gray-900 rounded-md group inline-flex"
|
||||
alt="Profile picture of user 3ae3670d-c729-41d2-adee-691340673aef"
|
||||
src="https://example.org/pictures/5f289b87-efc2-4cc9-9061-21883b0778db"
|
||||
/>
|
||||
</li>
|
||||
<li class="text-base text-gray-500 mt-6 font-medium text-center">
|
||||
<ol class="text-sm font-medium text-indigo-600 hover:text-indigo-500"></ol>
|
||||
<ol class="rounded-md h-6 w-6 inline-block"></ol>
|
||||
</li>
|
||||
<li class="hidden lg:flex lg:items-center">
|
||||
<ol class="border-indigo-600"></ol>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="sr-only">
|
||||
<img
|
||||
class="justify-center py-2 bg-white flex"
|
||||
alt="Profile picture of user 89195190-9a42-4826-89ba-e7bc1d677520"
|
||||
src="https://example.org/pictures/742d9c44-1c75-461f-b2d0-e52636041966"
|
||||
/>
|
||||
<img
|
||||
class="-ml-14 sticky left-0 z-20 text-gray-400 -mt-2.5 leading-5 pr-2 w-14 text-right text-xs"
|
||||
alt="Profile picture of user 77a37b14-1c0f-4ef0-85d4-a18755065ea3"
|
||||
src="https://example.org/pictures/8ad2e05f-6ea6-4d94-8f82-f8aa59274b9e"
|
||||
/>
|
||||
</li>
|
||||
</ol>
|
||||
<img
|
||||
class="border-t shadow-sm sm:border bg-white sm:rounded-lg border-gray-200 border-b"
|
||||
alt="Profile picture of user 07935cf2-78e5-49ba-afdb-09d13c8320d6"
|
||||
src="https://example.org/pictures/8f895991-6e8c-4bcb-82c3-11c2a3338eb2"
|
||||
/>
|
||||
<ol class="grid-cols-2 grid gap-x-8 gap-y-10">
|
||||
<li
|
||||
class="py-1 w-48 shadow-lg bg-white rounded-md ring-1 absolute ring-black z-10 focus:outline-none right-0 mt-2 ring-opacity-5 origin-top-right"
|
||||
>
|
||||
<ol class="h-6 w-6">
|
||||
<li class="bg-gray-500 inset-0 transition-opacity fixed bg-opacity-75">
|
||||
<ol class="flex"></ol>
|
||||
<a href="#" class="mt-2 flex items-center justify-between">
|
||||
This is link 1a1a3c60-a2ea-4153-acd7-12aa82f03c8d
|
||||
</a>
|
||||
<a href="#" target="_blank" class="text-gray-300 flex-shrink-0 w-5 h-5">
|
||||
This is link bdaa695c-3fe4-4cab-8d68-dbb338601044
|
||||
</a>
|
||||
</li>
|
||||
<li class="text-gray-500">
|
||||
<ol class="py-20"></ol>
|
||||
<a href="#" target="_blank" class="text-sm ml-3">
|
||||
This is link 2183c71d-39ec-42d4-8b1e-eeb7e5490050
|
||||
</a>
|
||||
</li>
|
||||
<li class="mt-10">
|
||||
<ol class="group"></ol>
|
||||
<a href="#" rel="noreferrer" class="bg-gray-50">
|
||||
This is link c49882fd-ab55-41a4-8dac-b96fe2901bce
|
||||
</a>
|
||||
<ol class="bg-white h-[940px] overflow-y-auto"></ol>
|
||||
</li>
|
||||
<li class="flex-1 space-y-1">
|
||||
<ol class="h-6 w-6"></ol>
|
||||
</li>
|
||||
</ol>
|
||||
<ul class="z-10 flex relative items-center lg:hidden">
|
||||
<li class="aspect-w-1 bg-gray-100 rounded-lg overflow-hidden aspect-h-1">
|
||||
<img
|
||||
class="overflow-hidden sm:rounded-md bg-white shadow"
|
||||
alt="Profile picture of user 263bb89c-5b54-4247-8c82-fec829b1a895"
|
||||
src="https://example.org/pictures/4c12a5c4-d117-4f81-93e1-47a45626a36e"
|
||||
/>
|
||||
<a href="#" class="sr-only"> This is link fc3885d8-d63f-4455-b056-f113aa3a2f23 </a>
|
||||
<ol
|
||||
class="border-t grid-cols-1 border-gray-200 gap-6 border-b mt-6 sm:grid-cols-2 grid py-6"
|
||||
></ol>
|
||||
</li>
|
||||
<li class="space-x-3 items-center flex">
|
||||
<a href="#" class="py-2 bg-white">
|
||||
This is link 1d718cb1-05e3-4d14-b959-92f5fd475ce0
|
||||
</a>
|
||||
<img
|
||||
class="rounded-md w-full focus:border-indigo-500 border-gray-300 focus:ring-indigo-500 block mt-1 shadow-sm sm:text-sm"
|
||||
alt="Profile picture of user 1c4d6dc3-700a-4167-8ec3-3dc2f73d4ad5"
|
||||
src="https://example.org/pictures/359599b3-5610-482e-bc09-025ac5283170"
|
||||
/>
|
||||
<ul class="max-w-3xl mx-auto divide-y-2 divide-gray-200"></ul>
|
||||
<ul class="flex space-x-4"></ul>
|
||||
</li>
|
||||
<li class="text-gray-500 hover:text-gray-600">
|
||||
<ul class="h-96 w-full relative lg:hidden"></ul>
|
||||
<ol class="text-gray-500 mt-6 text-sm"></ol>
|
||||
<a href="#" class="sm:col-span-6">
|
||||
This is link 51cc68af-1184-4f8c-9efd-d2f855902b0b
|
||||
</a>
|
||||
<ol
|
||||
class="left-0 inset-y-0 absolute pointer-events-none pl-3 items-center flex"
|
||||
></ol>
|
||||
</li>
|
||||
<li class="flex-shrink-0">
|
||||
<ol
|
||||
class="shadow-lg rounded-lg ring-1 bg-white ring-black ring-opacity-5 divide-gray-50 divide-y-2"
|
||||
></ol>
|
||||
</li>
|
||||
</ul>
|
||||
<img
|
||||
class="pl-3 sm:pr-6 py-3.5 relative pr-4"
|
||||
alt="Profile picture of user 095f88c2-1892-41d1-8165-981ab47b1942"
|
||||
src="https://example.org/pictures/c70d575b-353a-4360-99df-c2100a36e41b"
|
||||
/>
|
||||
</li>
|
||||
<li class="whitespace-nowrap">
|
||||
<a href="#" target="_blank" rel="noreferrer" class="h-full">
|
||||
This is link c804eb7a-39ea-46e6-a79a-2d48e61d5b4e
|
||||
</a>
|
||||
</li>
|
||||
<li class="text-gray-900 text-2xl font-bold tracking-tight">
|
||||
<img
|
||||
class="text-gray-900 font-medium"
|
||||
alt="Profile picture of user 12190673-25cb-4175-90d7-73282e51bd02"
|
||||
src="https://example.org/pictures/e55a076b-0325-4629-8e02-c9491f2f49cc"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
class="ring-black sm:-mx-6 overflow-hidden ring-1 ring-opacity-5 mt-8 md:rounded-lg md:mx-0 -mx-4 shadow"
|
||||
>
|
||||
<ol class="flex items-center font-medium hover:text-gray-800 text-sm text-gray-700">
|
||||
<li class="flex mt-8 flex-col">
|
||||
<ol class="sm:inline hidden"></ol>
|
||||
<ul class="flex items-center absolute inset-0"></ul>
|
||||
<ol class="h-6 w-6"></ol>
|
||||
</li>
|
||||
<li class="-ml-2 rounded-md text-gray-400 p-2 bg-white">
|
||||
<img
|
||||
class="block ml-3 font-medium text-sm text-gray-700"
|
||||
alt="Profile picture of user 2cee83e8-6405-4046-9454-7f6083db307d"
|
||||
src="https://example.org/pictures/93097140-2e56-4a3f-bc41-e00c658b7767"
|
||||
/>
|
||||
<ul class="sm:grid font-medium hidden grid-cols-4 text-gray-600 mt-6 text-sm"></ul>
|
||||
<ul class="h-64 w-64 rounded-full xl:h-80 xl:w-80"></ul>
|
||||
<ul class="sr-only"></ul>
|
||||
</li>
|
||||
</ol>
|
||||
<img
|
||||
class="aspect-w-2 group sm:aspect-w-1 aspect-h-1 overflow-hidden sm:aspect-h-1 sm:row-span-2 rounded-lg"
|
||||
alt="Profile picture of user bc370a72-a44e-44e1-bbec-962c234060da"
|
||||
src="https://example.org/pictures/d284630d-a088-49ad-a018-b245a8d7acb7"
|
||||
/>
|
||||
</li>
|
||||
<li class="space-y-6 mt-6">
|
||||
<ul class="w-5 text-gray-400 h-5">
|
||||
<li
|
||||
class="hover:bg-gray-100 py-1.5 bg-gray-50 text-gray-400 focus:z-10 rounded-tl-lg"
|
||||
>
|
||||
<img
|
||||
class="bg-gray-50 py-1.5 hover:bg-gray-100 focus:z-10 text-gray-400"
|
||||
alt="Profile picture of user 4b298841-5911-4b70-ae5a-a5aa8646e575"
|
||||
src="https://example.org/pictures/fd05d4f7-2b6c-4de4-b7a7-3035e7b5fe78"
|
||||
/>
|
||||
<img
|
||||
class="px-3 py-2 bg-white relative"
|
||||
alt="Profile picture of user 5406aa7d-5563-4ce1-980f-754a912cd9ff"
|
||||
src="https://example.org/pictures/e979117e-580c-40a7-be74-bf1b916b9ac0"
|
||||
/>
|
||||
<a
|
||||
href="#"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="mt-2 font-medium text-gray-900 text-lg"
|
||||
>
|
||||
This is link 95d285c9-6e4e-43c2-a97f-bb958dcce92f
|
||||
</a>
|
||||
<ul class="sr-only"></ul>
|
||||
</li>
|
||||
<li class="mt-2 text-sm text-gray-500">
|
||||
<a href="#" target="_blank" class="text-sm text-blue-gray-900 font-medium block">
|
||||
This is link e0ca5c13-efa5-44d7-b91e-512df7c88410
|
||||
</a>
|
||||
<img
|
||||
class="w-5 h-5"
|
||||
alt="Profile picture of user 4f5aeddc-6718-40fd-87d2-dcca7e4ef8b1"
|
||||
src="https://example.org/pictures/6a2ed606-6c59-413c-911f-612c7390091f"
|
||||
/>
|
||||
<ol class="font-medium text-gray-900"></ol>
|
||||
<ol class="justify-center rounded-full h-7 items-center mx-auto w-7 flex"></ol>
|
||||
</li>
|
||||
<li class="font-medium px-1 whitespace-nowrap py-4 text-sm border-b-2">
|
||||
<a
|
||||
href="#"
|
||||
target="_blank"
|
||||
class="min-h-80 rounded-md aspect-h-1 aspect-w-1 lg:aspect-none w-full group-hover:opacity-75 lg:h-80 overflow-hidden bg-gray-200"
|
||||
>
|
||||
This is link 4f52e535-2e46-44e2-8a5f-fce48c1a673a
|
||||
</a>
|
||||
<ul class="text-gray-300 h-full w-full"></ul>
|
||||
<img
|
||||
class="block"
|
||||
alt="Profile picture of user 76dd6af3-d2b7-4a7a-8bc3-628ce95ea9b3"
|
||||
src="https://example.org/pictures/c6b476c8-283c-44f7-a5c6-4cb48c5ae10b"
|
||||
/>
|
||||
<ol class="h-32 relative lg:hidden w-full"></ol>
|
||||
</li>
|
||||
<li class="bg-gray-100 z-10 sticky sm:pt-3 pl-1 pt-1 md:hidden sm:pl-3 top-0">
|
||||
<ol class="z-40 relative lg:hidden"></ol>
|
||||
<a href="#" class="sr-only"> This is link ec11a608-55b8-41fd-8085-325252c469af </a>
|
||||
<ol class="divide-gray-200 lg:col-span-9 divide-y"></ol>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="text-xl font-semibold ml-1">
|
||||
<li class="lg:flex-1 lg:w-0">
|
||||
<ol class="sm:hidden"></ol>
|
||||
<img
|
||||
class="block py-2 text-blue-gray-900 text-base hover:bg-blue-gray-50 font-medium px-3 rounded-md"
|
||||
alt="Profile picture of user 885eb4a3-98ca-4614-b255-ec03e124c026"
|
||||
src="https://example.org/pictures/6d3a9136-583e-4742-826f-e6d94f60ce99"
|
||||
/>
|
||||
</li>
|
||||
<li class="truncate w-0 ml-2 flex-1">
|
||||
<img
|
||||
class="hover:text-gray-600 text-gray-500"
|
||||
alt="Profile picture of user de38ffb5-607b-4ed6-87bd-dd05fde1731c"
|
||||
src="https://example.org/pictures/da67f442-40c9-425d-9344-2f9772582519"
|
||||
/>
|
||||
<ol class="text-center mt-8 text-gray-400 text-base"></ol>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li class="lg:block hidden lg:flex-1">
|
||||
<a href="#" target="_blank" class="text-base ml-3 text-gray-500">
|
||||
This is link 9a44892f-7ead-48b6-af94-913ec04821a1
|
||||
</a>
|
||||
<ol
|
||||
class="shadow-sm border-gray-300 focus:ring-indigo-500 sm:text-sm focus:border-indigo-500 w-full rounded-md block"
|
||||
>
|
||||
<li class="bg-white hover:bg-gray-100 py-1.5 focus:z-10">
|
||||
<a
|
||||
href="#"
|
||||
target="_blank"
|
||||
class="bg-gray-200 text-gray-700 gap-px lg:flex-none border-b text-center grid-cols-7 grid text-xs leading-6 border-gray-300 font-semibold"
|
||||
>
|
||||
This is link 42a16c66-508d-4d65-bb1a-b252f7e78df2
|
||||
</a>
|
||||
<a href="#" class="h-8 w-auto"> This is link 021bda37-522f-413d-af3c-4a8c4f4d666a </a>
|
||||
<img
|
||||
class="max-h-12"
|
||||
alt="Profile picture of user a1a859bc-ef5e-4349-99ab-a8f4ae262d94"
|
||||
src="https://example.org/pictures/da93a022-cbbf-4ba0-b34c-21c090d87d74"
|
||||
/>
|
||||
<a
|
||||
href="#"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="flex relative justify-center text-sm"
|
||||
>
|
||||
This is link fa5aded2-2906-4809-b742-26832b226f50
|
||||
</a>
|
||||
</li>
|
||||
<li class="py-12 sm:px-6 lg:py-16 px-4 lg:px-8 mx-auto max-w-7xl">
|
||||
<a href="#" rel="noreferrer" class="min-w-0 ml-3 flex-1">
|
||||
This is link 715e397d-5676-42fc-9d8f-456172543c31
|
||||
</a>
|
||||
<img
|
||||
class="bg-gray-800"
|
||||
alt="Profile picture of user c02575ab-6ff1-45f9-a8e3-242c79b133fc"
|
||||
src="https://example.org/pictures/3d8ecc7a-2504-4797-a644-f93d74acf853"
|
||||
/>
|
||||
<img
|
||||
class="text-base text-gray-900 font-medium"
|
||||
alt="Profile picture of user 9f457701-be79-4ff2-9dfc-9382dafb20ab"
|
||||
src="https://example.org/pictures/438b713b-c9fd-4da2-a265-4b8d8f531e30"
|
||||
/>
|
||||
<a href="#" rel="noreferrer" class="text-sm">
|
||||
This is link 4c84098a-c0ac-4044-bbba-6f10bcc315fb
|
||||
</a>
|
||||
</li>
|
||||
<li class="w-6 flex-shrink-0 h-6 text-green-500">
|
||||
<img
|
||||
class="mx-auto sm:px-6 px-4 max-w-7xl"
|
||||
alt="Profile picture of user 00ae83b8-845d-447e-ae1b-cd3c685e1ca0"
|
||||
src="https://example.org/pictures/b9deba2b-c5b3-4b80-bf76-e0a717d310fd"
|
||||
/>
|
||||
<ul class="w-72">
|
||||
<li class="sm:flex sm:justify-between sm:items-center">
|
||||
<img
|
||||
class="block font-medium text-sm text-gray-700"
|
||||
alt="Profile picture of user c0de8cb0-e9d7-4639-8c3d-851ca17e665b"
|
||||
src="https://example.org/pictures/897193ff-aa53-4e9e-b761-bc4f75aef572"
|
||||
/>
|
||||
</li>
|
||||
<li class="text-sm hidden font-medium ml-3 text-gray-700 lg:block">
|
||||
<img
|
||||
class="h-8 w-auto"
|
||||
alt="Profile picture of user 9ff9c8ac-2374-4960-9996-ec258745c91a"
|
||||
src="https://example.org/pictures/e898638f-08fa-4743-aea7-66adba84bded"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
class="mx-auto sm:px-6 px-4 lg:items-center lg:flex lg:py-16 lg:px-8 max-w-7xl py-12"
|
||||
>
|
||||
<img
|
||||
class="lg:max-w-none px-4 max-w-2xl mx-auto lg:px-0"
|
||||
alt="Profile picture of user 7c4d617d-afa2-4410-81c5-2def540d2d20"
|
||||
src="https://example.org/pictures/05a7dbc1-c1cc-4f99-99e5-7b507a4108b3"
|
||||
/>
|
||||
<ul class="hover:text-gray-600 text-gray-500"></ul>
|
||||
<ul
|
||||
class="relative rounded-md border-transparent focus-within:ring-2 focus-within:ring-white -ml-2 group"
|
||||
></ul>
|
||||
</li>
|
||||
</ul>
|
||||
<img
|
||||
class="h-5 text-gray-300 w-5"
|
||||
alt="Profile picture of user bf2c6905-715a-4e38-9b5d-17fd8e7aec2a"
|
||||
src="https://example.org/pictures/e759a4d7-5e63-4075-ba32-c6574107f401"
|
||||
/>
|
||||
</li>
|
||||
<li class="object-cover object-center h-full w-full">
|
||||
<ol class="w-full">
|
||||
<li class="md:mt-0 absolute sm:-mt-32 -mt-72 inset-0">
|
||||
<ul class="w-12 h-12 rounded-full"></ul>
|
||||
</li>
|
||||
<li class="h-1.5 rounded-full w-1.5 mb-1 mx-0.5 bg-gray-400">
|
||||
<ol class="border-gray-200 border-4 rounded-lg border-dashed h-96"></ol>
|
||||
<img
|
||||
class="order-1 font-semibold text-gray-700"
|
||||
alt="Profile picture of user 1c2cabee-08a3-4b48-ba54-5a578b2c3d30"
|
||||
src="https://example.org/pictures/f5c185be-8b9d-490d-81f4-73a6e1517d98"
|
||||
/>
|
||||
<img
|
||||
class="font-bold sm:text-4xl text-gray-900 tracking-tight text-3xl leading-8 text-center"
|
||||
alt="Profile picture of user 1e8a2508-a37d-4f6b-9a8c-c6fcf0773604"
|
||||
src="https://example.org/pictures/90af1bd2-30ed-45d3-917f-c685190ce56e"
|
||||
/>
|
||||
</li>
|
||||
<li class="space-x-2 mt-4 text-sm text-gray-700 flex">
|
||||
<ul
|
||||
class="rounded-full translate-x-1/2 block transform border-2 absolute bottom-0 right-0 border-white translate-y-1/2"
|
||||
></ul>
|
||||
<ol
|
||||
class="sm:hidden text-base py-2 text-gray-900 w-full placeholder-gray-500 h-full focus:outline-none border-transparent pr-3 pl-8 focus:placeholder-gray-400 focus:ring-0 focus:border-transparent"
|
||||
></ol>
|
||||
</li>
|
||||
</ol>
|
||||
<ul class="lg:mt-0 self-center flow-root mt-8">
|
||||
<li
|
||||
class="justify-center rounded-full bg-transparent bg-white hover:text-gray-500 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 text-gray-400 inline-flex focus:outline-none h-8 items-center w-8"
|
||||
>
|
||||
<ol class="block xl:inline"></ol>
|
||||
<ol class="flex-shrink-0 ml-4"></ol>
|
||||
<ol class="text-gray-200"></ol>
|
||||
<img
|
||||
class="border-gray-300 focus:relative md:w-9 rounded-r-md flex bg-white md:hover:bg-gray-50 pl-4 text-gray-400 border hover:text-gray-500 items-center pr-3 border-l-0 justify-center md:px-2 py-2"
|
||||
alt="Profile picture of user b0a1e2d3-84c4-494b-91d6-194fc294b0db"
|
||||
src="https://example.org/pictures/2f414511-756c-40ef-aded-22e3f4d985d7"
|
||||
/>
|
||||
</li>
|
||||
<li class="inset-0 absolute">
|
||||
<img
|
||||
class="text-gray-500 text-base font-medium text-center"
|
||||
alt="Profile picture of user 431f88eb-5002-43ab-b23a-37ae0ef7d424"
|
||||
src="https://example.org/pictures/bc7c19bb-4ef2-46ff-b00e-da70febab926"
|
||||
/>
|
||||
<img
|
||||
class="inset-0 absolute z-10"
|
||||
alt="Profile picture of user 4a3698d0-7cea-4ba2-854d-28b2bb2d374b"
|
||||
src="https://example.org/pictures/6078c4cd-db51-43af-90b3-8aeb0a8f1030"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<a href="#" rel="noreferrer" class="bg-white">
|
||||
This is link ab01c689-e03a-4992-8322-37c20551cb07
|
||||
</a>
|
||||
</li>
|
||||
<li class="flex px-4 pb-2 pt-5">
|
||||
<ol
|
||||
class="sm:px-6 px-4 bg-white relative pb-8 md:p-6 shadow-2xl items-center flex w-full sm:pt-8 overflow-hidden lg:p-8 pt-14"
|
||||
>
|
||||
<li class="py-1.5 hover:bg-gray-100 focus:z-10 text-gray-400 bg-gray-50">
|
||||
<a
|
||||
href="#"
|
||||
rel="noreferrer"
|
||||
class="sm:text-sm bg-gray-50 items-center border-gray-300 border-r-0 rounded-l-md text-gray-500 inline-flex border px-3"
|
||||
>
|
||||
This is link 3ef75acd-3dfc-4e82-801b-eae9ff7c4351
|
||||
</a>
|
||||
<ol class="absolute border-dashed border-gray-200 border-2 rounded-lg inset-0"></ol>
|
||||
</li>
|
||||
<li class="hover:bg-gray-50 block">
|
||||
<ol class="items-center flex justify-center p-8"></ol>
|
||||
<ul class="mx-auto sm:px-6 lg:px-8 pb-12 max-w-7xl px-4"></ul>
|
||||
<img
|
||||
class="h-6 w-6 text-green-400"
|
||||
alt="Profile picture of user f5900afb-7bee-4492-b6e3-148f0afc4f5f"
|
||||
src="https://example.org/pictures/1b12c3fb-9f84-4cc7-84a7-d38f7ea232ee"
|
||||
/>
|
||||
</li>
|
||||
<li class="flex mt-4 lg:flex-grow-0 flex-grow lg:ml-4 flex-shrink-0 ml-8">
|
||||
<img
|
||||
class="focus:ring-indigo-500 block w-full sm:text-sm border-gray-300 focus:border-indigo-500 rounded-md shadow-sm"
|
||||
alt="Profile picture of user c9dd7fa0-c1f4-477c-b24e-87d200ebb161"
|
||||
src="https://example.org/pictures/1a3b2e5c-a192-47f3-a550-e18f715213a2"
|
||||
/>
|
||||
<a href="#" target="_blank" rel="noreferrer" class="text-gray-300 hover:text-white">
|
||||
This is link 81b819f3-b2e8-41db-ab8d-abe8c6926047
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
<img
|
||||
class="lg:flex-1 lg:block hidden"
|
||||
alt="Profile picture of user 352e9ea2-0216-4a0c-917c-1906f5ef4ed1"
|
||||
src="https://example.org/pictures/ec02985c-b92d-4978-906e-7bdc70bfa54e"
|
||||
/>
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li class="sr-only">
|
||||
<a href="#" target="_blank" class="text-gray-500 mt-4 text-sm">
|
||||
This is link a712361b-f51e-4f0a-9d55-b64a5d53e10e
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
rel="noreferrer"
|
||||
class="rounded-lg ring-opacity-5 shadow-lg overflow-hidden ring-1 ring-black"
|
||||
>
|
||||
This is link 8eb412d6-1c39-4fed-b507-cf2a65904247
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<img
|
||||
class="bg-gray-100"
|
||||
alt="Profile picture of user 8825a6f0-3a41-44b8-92ec-abcd0af79bfb"
|
||||
src="https://example.org/pictures/679e2a54-073e-416a-85c4-3eba0626aab3"
|
||||
/>
|
||||
<span class="bg-white focus:z-10 py-1.5 hover:bg-gray-100">
|
||||
This is text 3d190171-53b3-4393-99ab-9bedfc964141
|
||||
</span>
|
||||
</div>
|
||||
6
oxide/crates/core/benches/fixtures/template-007.html
Normal file
6
oxide/crates/core/benches/fixtures/template-007.html
Normal file
@ -0,0 +1,6 @@
|
||||
<div class="md:absolute items-center flex lg:hidden md:inset-y-0 md:right-0">
|
||||
<input
|
||||
type="password"
|
||||
class="text-pink-800 py-0.5 bg-pink-100 font-medium text-sm items-center px-3 inline-flex rounded-full"
|
||||
/>
|
||||
</div>
|
||||
6
oxide/crates/core/benches/fixtures/template-008.html
Normal file
6
oxide/crates/core/benches/fixtures/template-008.html
Normal file
@ -0,0 +1,6 @@
|
||||
<div class="text-gray-200">
|
||||
<span class="lg:hidden z-40 relative"> This is text cd32f5df-af14-4332-9fd3-0e5589f8d457 </span>
|
||||
<a href="#" target="_blank" rel="noreferrer" class="border-gray-200 border-t">
|
||||
This is link 05289860-74df-49a7-91dd-1a3a0d215bc3
|
||||
</a>
|
||||
</div>
|
||||
5
oxide/crates/core/benches/fixtures/template-009.html
Normal file
5
oxide/crates/core/benches/fixtures/template-009.html
Normal file
@ -0,0 +1,5 @@
|
||||
<div class="italic text-sm text-gray-500">
|
||||
<span class="relative text-left inline-block">
|
||||
This is text bec24672-f081-47d1-8130-cc06506974b5
|
||||
</span>
|
||||
</div>
|
||||
3348
oxide/crates/core/benches/fixtures/template-010.html
Normal file
3348
oxide/crates/core/benches/fixtures/template-010.html
Normal file
File diff suppressed because it is too large
Load Diff
46
oxide/crates/core/benches/parse_candidates.rs
Normal file
46
oxide/crates/core/benches/parse_candidates.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use tailwindcss_core::parser::Extractor;
|
||||
|
||||
pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
fn parse(input: &[u8]) {
|
||||
// _ = Extractor::all(black_box(input), ExtractorOptions { preserve_spaces_in_arbitrary: false });
|
||||
Extractor::unique(black_box(input), Default::default());
|
||||
}
|
||||
|
||||
c.bench_function("scan_files (simple)", |b| b.iter(|| parse(b"underline")));
|
||||
|
||||
c.bench_function("scan_files (with variant)", |b| {
|
||||
b.iter(|| parse(b"hover:underline"))
|
||||
});
|
||||
|
||||
c.bench_function("scan_files (with stacked variants)", |b| {
|
||||
b.iter(|| parse(b"focus:hover:underline"))
|
||||
});
|
||||
|
||||
c.bench_function("scan_files (with arbitrary values)", |b| {
|
||||
b.iter(|| parse(b"p-[20px]"))
|
||||
});
|
||||
|
||||
c.bench_function("scan_files (with variant and arbitrary values)", |b| {
|
||||
b.iter(|| parse(b"hover:p-[20px]"))
|
||||
});
|
||||
|
||||
c.bench_function("scan_files (real world)", |b| {
|
||||
b.iter(|| parse(include_bytes!("./fixtures/template-000.html")))
|
||||
});
|
||||
|
||||
let mut group = c.benchmark_group("sample-size-example");
|
||||
group.sample_size(10);
|
||||
|
||||
group.bench_function("scan_files (fast space skipping)", |b| {
|
||||
let count = 10_000;
|
||||
let crazy1 = format!("{}underline", " ".repeat(count));
|
||||
let crazy2 = crazy1.repeat(count);
|
||||
let crazy3 = crazy2.as_bytes();
|
||||
|
||||
b.iter(|| parse(black_box(crazy3)))
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
63
oxide/crates/core/benches/scan_files.rs
Normal file
63
oxide/crates/core/benches/scan_files.rs
Normal file
@ -0,0 +1,63 @@
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use std::path::PathBuf;
|
||||
use tailwindcss_core::{scan_files, ChangedContent, Parsing, IO};
|
||||
|
||||
pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
// current_dir will be set to ./crates/core
|
||||
let fixtures_path = std::env::current_dir()
|
||||
.unwrap()
|
||||
.join("benches")
|
||||
.join("fixtures");
|
||||
|
||||
let mut all_files: Vec<(u64, PathBuf)> = std::fs::read_dir(fixtures_path)
|
||||
.unwrap()
|
||||
.filter_map(Result::ok)
|
||||
.map(|dir_entry| dir_entry.path())
|
||||
.filter(|path| path.is_file())
|
||||
.filter(|path| match path.extension() {
|
||||
Some(ext) => ext == "html",
|
||||
_ => false,
|
||||
})
|
||||
.map(|path| (path.metadata().unwrap().len(), path))
|
||||
.collect();
|
||||
|
||||
// Let's sort them first so that we are working with the same files in the same order every
|
||||
// time.
|
||||
all_files.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
// Let's work with the first middle X items (so that we can skip outliers and work with more
|
||||
// interesting files)
|
||||
let amount = 300;
|
||||
let mut files: Vec<_> = all_files
|
||||
.iter()
|
||||
.skip((all_files.len() - amount) / 2) // Skip the first X, so that we can use the middle
|
||||
// {amount} files.
|
||||
.take(amount)
|
||||
.map(|(_, path)| path)
|
||||
.collect();
|
||||
|
||||
// Two (or more) files can technically have the exact same size, but the order is random, so
|
||||
// now that we are scoped to the middle X files, let's sort these alphabetically to guarantee
|
||||
// the same order in our benchmarks.
|
||||
files.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
|
||||
|
||||
let changed_content: Vec<ChangedContent> = files
|
||||
.into_iter()
|
||||
.map(|file| ChangedContent {
|
||||
file: Some(file.to_path_buf()),
|
||||
content: None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
c.bench_function("scan_files", |b| {
|
||||
b.iter(|| {
|
||||
scan_files(
|
||||
changed_content.clone(),
|
||||
Parsing::Parallel as u8 | IO::Parallel as u8,
|
||||
)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
4
oxide/crates/core/fuzz/.gitignore
vendored
Normal file
4
oxide/crates/core/fuzz/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
target
|
||||
corpus
|
||||
artifacts
|
||||
coverage
|
||||
531
oxide/crates/core/fuzz/Cargo.lock
generated
Normal file
531
oxide/crates/core/fuzz/Cargo.lock
generated
Normal file
@ -0,0 +1,531 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29d47fbf90d5149a107494b15a7dc8d69b351be2db3bb9691740e88ec17fd880"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata 0.4.5",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.74"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"crossbeam-deque",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-queue",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
|
||||
|
||||
[[package]]
|
||||
name = "fxhash"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
"log",
|
||||
"regex-automata 0.4.5",
|
||||
"regex-syntax 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "globwalk"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"ignore",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata 0.4.5",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.137"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
|
||||
|
||||
[[package]]
|
||||
name = "libfuzzer-sys"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8fff891139ee62800da71b7fd5b508d570b9ad95e614a53c6f453ca08366038"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"cc",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
|
||||
dependencies = [
|
||||
"regex-automata 0.1.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
|
||||
dependencies = [
|
||||
"overload",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.78"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
|
||||
dependencies = [
|
||||
"regex-syntax 0.6.28",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
dependencies = [
|
||||
"regex-syntax 0.6.28",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.196"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.196"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tailwindcss-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"crossbeam",
|
||||
"fxhash",
|
||||
"globwalk",
|
||||
"ignore",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"rayon",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tailwindcss-core-fuzz"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"libfuzzer-sys",
|
||||
"tailwindcss-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
27
oxide/crates/core/fuzz/Cargo.toml
Normal file
27
oxide/crates/core/fuzz/Cargo.toml
Normal file
@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "tailwindcss-core-fuzz"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition = "2021"
|
||||
|
||||
[package.metadata]
|
||||
cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
libfuzzer-sys = "0.4"
|
||||
|
||||
[dependencies.tailwindcss-core]
|
||||
path = ".."
|
||||
|
||||
# Prevent this from interfering with workspaces
|
||||
[workspace]
|
||||
members = ["."]
|
||||
|
||||
[profile.release]
|
||||
debug = 1
|
||||
|
||||
[[bin]]
|
||||
name = "parsing"
|
||||
path = "fuzz_targets/parsing.rs"
|
||||
test = false
|
||||
doc = false
|
||||
31
oxide/crates/core/fuzz/fuzz_targets/parsing.rs
Normal file
31
oxide/crates/core/fuzz/fuzz_targets/parsing.rs
Normal file
@ -0,0 +1,31 @@
|
||||
#![no_main]
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use std::path::PathBuf;
|
||||
use tailwindcss_core::candidate::scan_files;
|
||||
use tailwindcss_core::candidate::Candidate;
|
||||
use tailwindcss_core::location::Location;
|
||||
|
||||
// fuzz_target!(|data: &[u8]| {
|
||||
// if let Ok(s) = std::str::from_utf8(data) {
|
||||
// let _ = parse_candidate_strings(s, false);
|
||||
// }
|
||||
// });
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
if let Ok(s) = std::str::from_utf8(data) {
|
||||
let _ = scan_files(s, false)
|
||||
.into_iter()
|
||||
.map(|(c, _)| {
|
||||
Candidate::new(
|
||||
c,
|
||||
Location {
|
||||
file: PathBuf::new(),
|
||||
start: (0, 1),
|
||||
end: (0, 1),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
});
|
||||
57
oxide/crates/core/src/cache.rs
Normal file
57
oxide/crates/core/src/cache.rs
Normal file
@ -0,0 +1,57 @@
|
||||
use std::{path::PathBuf, time::SystemTime};
|
||||
use std::fs::{self};
|
||||
use fxhash::{FxHashMap, FxHashSet};
|
||||
|
||||
/// A cache to manage the list of candidates and the last modified time of files
|
||||
/// in the project. This is used to avoid recompiling files that haven't changed.
|
||||
#[derive(Default)]
|
||||
pub struct Cache {
|
||||
mtimes: FxHashMap<PathBuf, SystemTime>,
|
||||
candidates: FxHashSet<String>,
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
pub fn clear(&mut self) {
|
||||
self.mtimes.clear();
|
||||
self.candidates.clear();
|
||||
}
|
||||
|
||||
pub fn add_candidates(&mut self, additional_candidates: Vec<String>) {
|
||||
self.candidates.extend(additional_candidates);
|
||||
}
|
||||
|
||||
pub fn get_candidates(&self) -> Vec<String> {
|
||||
let mut result = vec![];
|
||||
result.extend(self.candidates.iter().cloned());
|
||||
result.sort();
|
||||
result
|
||||
}
|
||||
|
||||
pub fn find_modified_files<'a>(&mut self, paths: &'a Vec<PathBuf>) -> Vec<&'a PathBuf> {
|
||||
// Get a list of the files that have been modified since the last time we checked
|
||||
let mut modified: Vec<&PathBuf> = vec![];
|
||||
|
||||
for path in paths {
|
||||
let curr = fs::metadata(path)
|
||||
.and_then(|m| m.modified())
|
||||
.unwrap_or(SystemTime::now());
|
||||
|
||||
let prev = self.mtimes.insert(path.clone(), curr);
|
||||
|
||||
match prev {
|
||||
// Only add the file to the modified list if the mod time has changed
|
||||
Some(prev) if prev != curr => {
|
||||
modified.push(path);
|
||||
},
|
||||
|
||||
// If the file was already in the cache then we don't need to do anything
|
||||
Some(_) => (),
|
||||
|
||||
// If the file didn't exist before then it's been modified
|
||||
None => modified.push(path),
|
||||
}
|
||||
}
|
||||
|
||||
modified
|
||||
}
|
||||
}
|
||||
159
oxide/crates/core/src/cursor.rs
Normal file
159
oxide/crates/core/src/cursor.rs
Normal file
@ -0,0 +1,159 @@
|
||||
use std::{ascii::escape_default, fmt::Display};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Cursor<'a> {
|
||||
// The input we're scanning
|
||||
pub input: &'a [u8],
|
||||
|
||||
// The location of the cursor in the input
|
||||
pub pos: usize,
|
||||
|
||||
/// Is the cursor at the start of the input
|
||||
pub at_start: bool,
|
||||
|
||||
/// Is the cursor at the end of the input
|
||||
pub at_end: bool,
|
||||
|
||||
/// The previously consumed character
|
||||
/// If `at_start` is true, this will be NUL
|
||||
pub prev: u8,
|
||||
|
||||
/// The current character
|
||||
pub curr: u8,
|
||||
|
||||
/// The upcoming character (if any)
|
||||
/// If `at_end` is true, this will be NUL
|
||||
pub next: u8,
|
||||
}
|
||||
|
||||
impl<'a> Cursor<'a> {
|
||||
pub fn new(input: &'a [u8]) -> Self {
|
||||
let mut cursor = Self {
|
||||
input,
|
||||
pos: 0,
|
||||
at_start: true,
|
||||
at_end: false,
|
||||
prev: 0x00,
|
||||
curr: 0x00,
|
||||
next: 0x00,
|
||||
};
|
||||
cursor.move_to(0);
|
||||
cursor
|
||||
}
|
||||
|
||||
pub fn rewind_by(&mut self, amount: usize) {
|
||||
self.move_to(self.pos.saturating_sub(amount));
|
||||
}
|
||||
|
||||
pub fn advance_by(&mut self, amount: usize) {
|
||||
self.move_to(self.pos.saturating_add(amount));
|
||||
}
|
||||
|
||||
pub fn move_to(&mut self, pos: usize) {
|
||||
let len = self.input.len();
|
||||
let pos = pos.clamp(0, len);
|
||||
|
||||
self.pos = pos;
|
||||
self.at_start = pos == 0;
|
||||
self.at_end = pos + 1 >= len;
|
||||
|
||||
self.prev = if pos > 0 { self.input[pos - 1] } else { 0x00 };
|
||||
self.curr = if pos < len { self.input[pos] } else { 0x00 };
|
||||
self.next = if pos + 1 < len {
|
||||
self.input[pos + 1]
|
||||
} else {
|
||||
0x00
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Display for Cursor<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let len = self.input.len().to_string();
|
||||
|
||||
let pos = format!("{: >len_count$}", self.pos, len_count = len.len());
|
||||
write!(f, "{}/{} ", pos, len)?;
|
||||
|
||||
if self.at_start {
|
||||
write!(f, "S ")?;
|
||||
} else if self.at_end {
|
||||
write!(f, "E ")?;
|
||||
} else {
|
||||
write!(f, "M ")?;
|
||||
}
|
||||
|
||||
fn to_str(c: u8) -> String {
|
||||
if c == 0x00 {
|
||||
"NUL".into()
|
||||
} else {
|
||||
format!("{:?}", escape_default(c).to_string())
|
||||
}
|
||||
}
|
||||
|
||||
write!(
|
||||
f,
|
||||
"[{} {} {}]",
|
||||
to_str(self.prev),
|
||||
to_str(self.curr),
|
||||
to_str(self.next)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_cursor() {
|
||||
let mut cursor = Cursor::new(b"hello world");
|
||||
assert_eq!(cursor.pos, 0);
|
||||
assert!(cursor.at_start);
|
||||
assert!(!cursor.at_end);
|
||||
assert_eq!(cursor.prev, 0x00);
|
||||
assert_eq!(cursor.curr, b'h');
|
||||
assert_eq!(cursor.next, b'e');
|
||||
|
||||
cursor.advance_by(1);
|
||||
assert_eq!(cursor.pos, 1);
|
||||
assert!(!cursor.at_start);
|
||||
assert!(!cursor.at_end);
|
||||
assert_eq!(cursor.prev, b'h');
|
||||
assert_eq!(cursor.curr, b'e');
|
||||
assert_eq!(cursor.next, b'l');
|
||||
|
||||
// Advancing too far should stop at the end
|
||||
cursor.advance_by(10);
|
||||
assert_eq!(cursor.pos, 11);
|
||||
assert!(!cursor.at_start);
|
||||
assert!(cursor.at_end);
|
||||
assert_eq!(cursor.prev, b'd');
|
||||
assert_eq!(cursor.curr, 0x00);
|
||||
assert_eq!(cursor.next, 0x00);
|
||||
|
||||
// Can't advance past the end
|
||||
cursor.advance_by(1);
|
||||
assert_eq!(cursor.pos, 11);
|
||||
assert!(!cursor.at_start);
|
||||
assert!(cursor.at_end);
|
||||
assert_eq!(cursor.prev, b'd');
|
||||
assert_eq!(cursor.curr, 0x00);
|
||||
assert_eq!(cursor.next, 0x00);
|
||||
|
||||
cursor.rewind_by(1);
|
||||
assert_eq!(cursor.pos, 10);
|
||||
assert!(!cursor.at_start);
|
||||
assert!(cursor.at_end);
|
||||
assert_eq!(cursor.prev, b'l');
|
||||
assert_eq!(cursor.curr, b'd');
|
||||
assert_eq!(cursor.next, 0x00);
|
||||
|
||||
cursor.rewind_by(10);
|
||||
assert_eq!(cursor.pos, 0);
|
||||
assert!(cursor.at_start);
|
||||
assert!(!cursor.at_end);
|
||||
assert_eq!(cursor.prev, 0x00);
|
||||
assert_eq!(cursor.curr, b'h');
|
||||
assert_eq!(cursor.next, b'e');
|
||||
}
|
||||
}
|
||||
89
oxide/crates/core/src/fast_skip.rs
Normal file
89
oxide/crates/core/src/fast_skip.rs
Normal file
@ -0,0 +1,89 @@
|
||||
use crate::cursor::Cursor;
|
||||
|
||||
const STRIDE: usize = 16;
|
||||
type Mask = [bool; STRIDE];
|
||||
|
||||
#[inline(always)]
|
||||
pub fn fast_skip(cursor: &Cursor) -> Option<usize> {
|
||||
// If we don't have enough bytes left to check then bail early
|
||||
if cursor.pos + STRIDE >= cursor.input.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !cursor.curr.is_ascii_whitespace() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut offset = 1;
|
||||
|
||||
// SAFETY: We've already checked (indirectly) that this index is valid
|
||||
let remaining = unsafe { cursor.input.get_unchecked(cursor.pos..) };
|
||||
|
||||
// NOTE: This loop uses primitives designed to be auto-vectorized
|
||||
// Do not change this loop without benchmarking the results
|
||||
// And checking the generated assembly using godbolt.org
|
||||
for (i, chunk) in remaining.chunks_exact(STRIDE).enumerate() {
|
||||
let value = load(chunk);
|
||||
let is_whitespace = is_ascii_whitespace(value);
|
||||
let is_all_whitespace = all_true(is_whitespace);
|
||||
|
||||
if is_all_whitespace {
|
||||
offset = (i + 1) * STRIDE;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Some(cursor.pos + offset)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn load(input: &[u8]) -> [u8; STRIDE] {
|
||||
let mut value = [0u8; STRIDE];
|
||||
value.copy_from_slice(input);
|
||||
value
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn eq(input: [u8; STRIDE], val: u8) -> Mask {
|
||||
let mut res = [false; STRIDE];
|
||||
for n in 0..STRIDE {
|
||||
res[n] = input[n] == val
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn or(a: [bool; STRIDE], b: [bool; STRIDE]) -> [bool; STRIDE] {
|
||||
let mut res = [false; STRIDE];
|
||||
for n in 0..STRIDE {
|
||||
res[n] = a[n] | b[n];
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn all_true(a: [bool; STRIDE]) -> bool {
|
||||
let mut res = true;
|
||||
for item in a.iter().take(STRIDE) {
|
||||
res &= item;
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn is_ascii_whitespace(value: [u8; STRIDE]) -> [bool; STRIDE] {
|
||||
let whitespace_1 = eq(value, b'\t');
|
||||
let whitespace_2 = eq(value, b'\n');
|
||||
let whitespace_3 = eq(value, b'\x0C');
|
||||
let whitespace_4 = eq(value, b'\r');
|
||||
let whitespace_5 = eq(value, b' ');
|
||||
|
||||
or(
|
||||
or(
|
||||
or(or(whitespace_1, whitespace_2), whitespace_3),
|
||||
whitespace_4,
|
||||
),
|
||||
whitespace_5,
|
||||
)
|
||||
}
|
||||
264
oxide/crates/core/src/fixtures/binary-extensions.txt
Normal file
264
oxide/crates/core/src/fixtures/binary-extensions.txt
Normal file
@ -0,0 +1,264 @@
|
||||
3dm
|
||||
3ds
|
||||
3g2
|
||||
3gp
|
||||
7z
|
||||
DS_Store
|
||||
a
|
||||
aac
|
||||
adp
|
||||
ai
|
||||
aif
|
||||
aiff
|
||||
alz
|
||||
ape
|
||||
apk
|
||||
appimage
|
||||
ar
|
||||
arj
|
||||
asf
|
||||
au
|
||||
avi
|
||||
avif
|
||||
bak
|
||||
baml
|
||||
bh
|
||||
bin
|
||||
bk
|
||||
bmp
|
||||
btif
|
||||
bz2
|
||||
bzip2
|
||||
cab
|
||||
caf
|
||||
cgm
|
||||
class
|
||||
cmx
|
||||
cpio
|
||||
cr2
|
||||
cur
|
||||
dat
|
||||
dcm
|
||||
deb
|
||||
dex
|
||||
djvu
|
||||
dll
|
||||
dmg
|
||||
dng
|
||||
doc
|
||||
docm
|
||||
docx
|
||||
dot
|
||||
dotm
|
||||
dra
|
||||
dsk
|
||||
dts
|
||||
dtshd
|
||||
dvb
|
||||
dwg
|
||||
dxf
|
||||
ecelp4800
|
||||
ecelp7470
|
||||
ecelp9600
|
||||
egg
|
||||
eol
|
||||
eot
|
||||
epub
|
||||
exe
|
||||
f4v
|
||||
fbs
|
||||
fh
|
||||
fla
|
||||
flac
|
||||
flatpak
|
||||
fli
|
||||
flv
|
||||
fpx
|
||||
fst
|
||||
fvt
|
||||
g3
|
||||
gh
|
||||
gif
|
||||
graffle
|
||||
gz
|
||||
gzip
|
||||
h261
|
||||
h263
|
||||
h264
|
||||
icns
|
||||
ico
|
||||
ief
|
||||
img
|
||||
ipa
|
||||
iso
|
||||
jar
|
||||
jpeg
|
||||
jpg
|
||||
jpgv
|
||||
jpm
|
||||
jxr
|
||||
key
|
||||
ktx
|
||||
lha
|
||||
lib
|
||||
lockb
|
||||
lvp
|
||||
lz
|
||||
lzh
|
||||
lzma
|
||||
lzo
|
||||
m3u
|
||||
m4a
|
||||
m4v
|
||||
mar
|
||||
mdi
|
||||
mht
|
||||
mid
|
||||
midi
|
||||
mj2
|
||||
mka
|
||||
mkv
|
||||
mmr
|
||||
mng
|
||||
mobi
|
||||
mov
|
||||
movie
|
||||
mp3
|
||||
mp4
|
||||
mp4a
|
||||
mpeg
|
||||
mpg
|
||||
mpga
|
||||
mxu
|
||||
nef
|
||||
npx
|
||||
numbers
|
||||
nupkg
|
||||
o
|
||||
odp
|
||||
ods
|
||||
odt
|
||||
oga
|
||||
ogg
|
||||
ogv
|
||||
otf
|
||||
ott
|
||||
pages
|
||||
pbm
|
||||
pcx
|
||||
pdb
|
||||
pdf
|
||||
pea
|
||||
pgm
|
||||
pic
|
||||
png
|
||||
pnm
|
||||
pot
|
||||
potm
|
||||
potx
|
||||
ppa
|
||||
ppam
|
||||
ppm
|
||||
pps
|
||||
ppsm
|
||||
ppsx
|
||||
ppt
|
||||
pptm
|
||||
pptx
|
||||
psd
|
||||
pya
|
||||
pyc
|
||||
pyo
|
||||
pyv
|
||||
qt
|
||||
rar
|
||||
ras
|
||||
raw
|
||||
resources
|
||||
rgb
|
||||
rip
|
||||
rlc
|
||||
rmf
|
||||
rmvb
|
||||
rpm
|
||||
rtf
|
||||
rz
|
||||
s3m
|
||||
s7z
|
||||
scpt
|
||||
sgi
|
||||
shar
|
||||
sil
|
||||
sketch
|
||||
slk
|
||||
smv
|
||||
snap
|
||||
snk
|
||||
so
|
||||
sqlite
|
||||
sqlite
|
||||
sqlite3
|
||||
sqlite3
|
||||
stl
|
||||
sub
|
||||
suo
|
||||
swf
|
||||
tar
|
||||
tbz
|
||||
tbz2
|
||||
tga
|
||||
tgz
|
||||
thmx
|
||||
tif
|
||||
tiff
|
||||
tlz
|
||||
ttc
|
||||
ttf
|
||||
txz
|
||||
udf
|
||||
uvh
|
||||
uvi
|
||||
uvm
|
||||
uvp
|
||||
uvs
|
||||
uvu
|
||||
viv
|
||||
vob
|
||||
war
|
||||
wav
|
||||
wax
|
||||
wbmp
|
||||
wdp
|
||||
weba
|
||||
webm
|
||||
webp
|
||||
whl
|
||||
wim
|
||||
wm
|
||||
wma
|
||||
wmv
|
||||
wmx
|
||||
woff
|
||||
woff2
|
||||
wrm
|
||||
wvx
|
||||
xbm
|
||||
xif
|
||||
xla
|
||||
xlam
|
||||
xls
|
||||
xlsb
|
||||
xlsm
|
||||
xlsx
|
||||
xlt
|
||||
xltm
|
||||
xltx
|
||||
xm
|
||||
xmind
|
||||
xpi
|
||||
xpm
|
||||
xwd
|
||||
xz
|
||||
z
|
||||
zip
|
||||
zipx
|
||||
6
oxide/crates/core/src/fixtures/ignored-extensions.txt
Normal file
6
oxide/crates/core/src/fixtures/ignored-extensions.txt
Normal file
@ -0,0 +1,6 @@
|
||||
css
|
||||
less
|
||||
lock
|
||||
sass
|
||||
scss
|
||||
styl
|
||||
3
oxide/crates/core/src/fixtures/ignored-files.txt
Normal file
3
oxide/crates/core/src/fixtures/ignored-files.txt
Normal file
@ -0,0 +1,3 @@
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
bun.lockb
|
||||
57
oxide/crates/core/src/fixtures/template-extensions.txt
Normal file
57
oxide/crates/core/src/fixtures/template-extensions.txt
Normal file
@ -0,0 +1,57 @@
|
||||
# HTML
|
||||
html
|
||||
pug
|
||||
|
||||
# JS
|
||||
astro
|
||||
cjs
|
||||
cts
|
||||
jade
|
||||
js
|
||||
jsx
|
||||
mjs
|
||||
mts
|
||||
svelte
|
||||
ts
|
||||
tsx
|
||||
vue
|
||||
|
||||
# Markdown
|
||||
md
|
||||
mdx
|
||||
|
||||
# ASP
|
||||
aspx
|
||||
razor
|
||||
|
||||
# Handlebars
|
||||
handlebars
|
||||
hbs
|
||||
mustache
|
||||
|
||||
# PHP
|
||||
php
|
||||
twig
|
||||
|
||||
# Ruby
|
||||
erb
|
||||
haml
|
||||
liquid
|
||||
rb
|
||||
rhtml
|
||||
slim
|
||||
|
||||
# Elixir / Phoenix
|
||||
eex
|
||||
heex
|
||||
|
||||
# Nunjucks
|
||||
njk
|
||||
nunjucks
|
||||
|
||||
# Python
|
||||
py
|
||||
tpl
|
||||
|
||||
# Rust
|
||||
rs
|
||||
456
oxide/crates/core/src/glob.rs
Normal file
456
oxide/crates/core/src/glob.rs
Normal file
@ -0,0 +1,456 @@
|
||||
use std::iter;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub fn fast_glob(
|
||||
base_path: &Path,
|
||||
patterns: &Vec<String>,
|
||||
) -> Result<impl iter::Iterator<Item = PathBuf>, std::io::Error> {
|
||||
Ok(get_fast_patterns(base_path, patterns)
|
||||
.into_iter()
|
||||
.flat_map(|(base_path, patterns)| {
|
||||
globwalk::GlobWalkerBuilder::from_patterns(base_path, &patterns)
|
||||
.follow_links(true)
|
||||
.build()
|
||||
.unwrap()
|
||||
.filter_map(Result::ok)
|
||||
.map(|file| file.path().to_path_buf())
|
||||
}))
|
||||
}
|
||||
|
||||
/// This function attempts to optimize the glob patterns to improve performance. The problem is
|
||||
/// that if you run the following command:
|
||||
/// ```sh
|
||||
/// tailwind --pwd ./project --content "{pages,components}/**/*.js"
|
||||
/// ```
|
||||
/// Then the globwalk library will scan every single file and folder in the `./project` folder,
|
||||
/// then it will check if the file matches the glob pattern and keep it if it does. This is very
|
||||
/// slow, because if you have vendor folders (like node_modules), then this will take a while...
|
||||
///
|
||||
/// Instead, we will optimize the pattern, and move as many directories as possible to the base
|
||||
/// path. This will allow us to scope the globwalk library to only scan the directories that we
|
||||
/// care about.
|
||||
///
|
||||
/// This means, that the following command:
|
||||
/// ```sh
|
||||
/// tailwind --pwd ./project --content "{pages,components}/**/*.js"
|
||||
/// ```
|
||||
///
|
||||
/// Will now conceptually do this instead behind the scenes:
|
||||
/// ```sh
|
||||
/// tailwind --pwd ./project/pages --content "**/*.js"
|
||||
/// tailwind --pwd ./project/components --content "**/*.js"
|
||||
/// ```
|
||||
fn get_fast_patterns(base_path: &Path, patterns: &Vec<String>) -> Vec<(PathBuf, Vec<String>)> {
|
||||
let mut optimized_patterns: Vec<(PathBuf, Vec<String>)> = vec![];
|
||||
|
||||
for pattern in patterns {
|
||||
let is_negated = pattern.starts_with('!');
|
||||
let mut pattern = pattern.clone();
|
||||
if is_negated {
|
||||
pattern.remove(0);
|
||||
}
|
||||
|
||||
let mut folders = pattern.split('/').collect::<Vec<_>>();
|
||||
|
||||
if folders.len() <= 1 {
|
||||
// No paths we can simplify, so let's use it as-is.
|
||||
optimized_patterns.push((base_path.to_path_buf(), vec![pattern]));
|
||||
} else {
|
||||
// We do have folders because `/` exists. Let's try to simplify the globs!
|
||||
// Safety: We know that the length is greater than 1, so we can safely unwrap.
|
||||
let file_pattern = folders.pop().unwrap();
|
||||
let all_folders = folders.clone();
|
||||
let mut temp_paths = vec![base_path.to_path_buf()];
|
||||
|
||||
let mut bail = false;
|
||||
|
||||
for (i, folder) in folders.into_iter().enumerate() {
|
||||
// There is a wildcard in the folder, so we have to bail now... 😢 But this also
|
||||
// means that we can skip looking at the rest of the folders, so there is at least
|
||||
// this small optimization we can apply!
|
||||
if folder.contains('*') {
|
||||
// Get all the remaining folders, attach the existing file_pattern so that this
|
||||
// can now be the final pattern we use.
|
||||
let mut remaining_folders = all_folders[i..].to_vec();
|
||||
remaining_folders.push(file_pattern);
|
||||
|
||||
let pattern = remaining_folders.join("/");
|
||||
for path in &temp_paths {
|
||||
optimized_patterns.push((path.to_path_buf(), vec![pattern.to_string()]));
|
||||
}
|
||||
|
||||
bail = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// The folder is very likely using an expandable pattern which we can expand!
|
||||
if folder.contains('{') && folder.contains('}') {
|
||||
let branches = expand_braces(folder);
|
||||
|
||||
let existing_paths = temp_paths;
|
||||
temp_paths = branches
|
||||
.iter()
|
||||
.flat_map(|branch| {
|
||||
existing_paths
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|path| path.join(branch))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
// The folder should just be a simple folder name without any glob magic. We should
|
||||
// be able to safely add it to the existing paths.
|
||||
else {
|
||||
temp_paths = temp_paths
|
||||
.into_iter()
|
||||
.map(|path| path.join(folder))
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
|
||||
// As long as we didn't bail, we can now add the current expanded patterns to the
|
||||
// optimized patterns.
|
||||
if !bail {
|
||||
for path in &temp_paths {
|
||||
optimized_patterns.push((path.to_path_buf(), vec![file_pattern.to_string()]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that we re-add all the `!` signs to the patterns.
|
||||
if is_negated {
|
||||
for (_, patterns) in &mut optimized_patterns {
|
||||
for pattern in patterns {
|
||||
pattern.insert(0, '!');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
optimized_patterns
|
||||
}
|
||||
|
||||
/// Given this input: a-{b,c}-d-{e,f}
|
||||
/// We will get:
|
||||
/// [
|
||||
/// a-b-d-e
|
||||
/// a-b-d-f
|
||||
/// a-c-d-e
|
||||
/// a-c-d-f
|
||||
/// ]
|
||||
/// TODO: There is probably a way nicer way of doing this, but this works for now.
|
||||
fn expand_braces(input: &str) -> Vec<String> {
|
||||
let mut result: Vec<String> = vec![];
|
||||
|
||||
let mut in_braces = false;
|
||||
let mut last_char: char = '\0';
|
||||
|
||||
let mut current = String::new();
|
||||
|
||||
// Given the input: a-{b,c}-d-{e,f}-g
|
||||
// The template will look like this: ["a-", "-d-", "g"].
|
||||
let mut template: Vec<String> = vec![];
|
||||
|
||||
// The branches will look like this: [["b", "c"], ["e", "f"]].
|
||||
let mut branches: Vec<Vec<String>> = vec![];
|
||||
|
||||
for (i, c) in input.char_indices() {
|
||||
let is_escaped = i > 0 && last_char == '\\';
|
||||
last_char = c;
|
||||
|
||||
match c {
|
||||
'{' if !is_escaped => {
|
||||
// Ensure that when a new set of braces is opened, that we at least have 1
|
||||
// template.
|
||||
if template.is_empty() {
|
||||
template.push(String::new());
|
||||
}
|
||||
|
||||
in_braces = true;
|
||||
branches.push(vec![]);
|
||||
template.push(String::new());
|
||||
}
|
||||
'}' if !is_escaped => {
|
||||
in_braces = false;
|
||||
if let Some(last) = branches.last_mut() {
|
||||
last.push(current.clone());
|
||||
}
|
||||
current.clear();
|
||||
}
|
||||
',' if !is_escaped && in_braces => {
|
||||
if let Some(last) = branches.last_mut() {
|
||||
last.push(current.clone());
|
||||
}
|
||||
current.clear();
|
||||
}
|
||||
_ if in_braces => current.push(c),
|
||||
_ => {
|
||||
if template.is_empty() {
|
||||
template.push(String::new());
|
||||
}
|
||||
|
||||
if let Some(last) = template.last_mut() {
|
||||
last.push(c);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Ensure we have a string that we can start adding information too.
|
||||
if !template.is_empty() && !branches.is_empty() {
|
||||
result.push("".to_string());
|
||||
}
|
||||
|
||||
// Let's try to generate everything!
|
||||
for (i, template) in template.into_iter().enumerate() {
|
||||
// Append current template string to all existing results.
|
||||
result = result.into_iter().map(|x| x + &template).collect();
|
||||
|
||||
// Get the results, and copy it for every single branch.
|
||||
if let Some(branches) = branches.get(i) {
|
||||
result = branches
|
||||
.iter()
|
||||
.flat_map(|branch| {
|
||||
result
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|x| x + branch)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::get_fast_patterns;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn it_should_keep_globs_that_start_with_file_wildcards_as_is() {
|
||||
let actual = get_fast_patterns(&PathBuf::from("/projects"), &vec!["*.html".to_string()]);
|
||||
let expected = vec![(PathBuf::from("/projects"), vec!["*.html".to_string()])];
|
||||
|
||||
assert_eq!(actual, expected,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_keep_globs_that_start_with_folder_wildcards_as_is() {
|
||||
let actual = get_fast_patterns(&PathBuf::from("/projects"), &vec!["**/*.html".to_string()]);
|
||||
let expected = vec![(PathBuf::from("/projects"), vec!["**/*.html".to_string()])];
|
||||
|
||||
assert_eq!(actual, expected,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_move_the_starting_folder_to_the_path() {
|
||||
let actual = get_fast_patterns(
|
||||
&PathBuf::from("/projects"),
|
||||
&vec!["example/*.html".to_string()],
|
||||
);
|
||||
let expected = vec![(
|
||||
PathBuf::from("/projects/example"),
|
||||
vec!["*.html".to_string()],
|
||||
)];
|
||||
|
||||
assert_eq!(actual, expected,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_move_the_starting_folders_to_the_path() {
|
||||
let actual = get_fast_patterns(
|
||||
&PathBuf::from("/projects"),
|
||||
&vec!["example/other/*.html".to_string()],
|
||||
);
|
||||
let expected = vec![(
|
||||
PathBuf::from("/projects/example/other"),
|
||||
vec!["*.html".to_string()],
|
||||
)];
|
||||
|
||||
assert_eq!(actual, expected,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_branch_expandable_folders() {
|
||||
let actual = get_fast_patterns(
|
||||
&PathBuf::from("/projects"),
|
||||
&vec!["{foo,bar}/*.html".to_string()],
|
||||
);
|
||||
let expected = vec![
|
||||
(PathBuf::from("/projects/foo"), vec!["*.html".to_string()]),
|
||||
(PathBuf::from("/projects/bar"), vec!["*.html".to_string()]),
|
||||
];
|
||||
|
||||
assert_eq!(actual, expected,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_expand_multiple_expansions_in_the_same_folder() {
|
||||
let actual = get_fast_patterns(
|
||||
&PathBuf::from("/projects"),
|
||||
&vec!["a-{b,c}-d-{e,f}-g/*.html".to_string()],
|
||||
);
|
||||
let expected = vec![
|
||||
(
|
||||
PathBuf::from("/projects/a-b-d-e-g"),
|
||||
vec!["*.html".to_string()],
|
||||
),
|
||||
(
|
||||
PathBuf::from("/projects/a-c-d-e-g"),
|
||||
vec!["*.html".to_string()],
|
||||
),
|
||||
(
|
||||
PathBuf::from("/projects/a-b-d-f-g"),
|
||||
vec!["*.html".to_string()],
|
||||
),
|
||||
(
|
||||
PathBuf::from("/projects/a-c-d-f-g"),
|
||||
vec!["*.html".to_string()],
|
||||
),
|
||||
];
|
||||
|
||||
assert_eq!(actual, expected,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_expansions_per_folder_starting_at_the_root() {
|
||||
let actual = get_fast_patterns(
|
||||
&PathBuf::from("/projects"),
|
||||
&vec!["{a,b}-c-{d,e}-f/{b,c}-d-{e,f}-g/*.html".to_string()],
|
||||
);
|
||||
let expected = vec![
|
||||
(
|
||||
PathBuf::from("/projects/a-c-d-f/b-d-e-g"),
|
||||
vec!["*.html".to_string()],
|
||||
),
|
||||
(
|
||||
PathBuf::from("/projects/b-c-d-f/b-d-e-g"),
|
||||
vec!["*.html".to_string()],
|
||||
),
|
||||
(
|
||||
PathBuf::from("/projects/a-c-e-f/b-d-e-g"),
|
||||
vec!["*.html".to_string()],
|
||||
),
|
||||
(
|
||||
PathBuf::from("/projects/b-c-e-f/b-d-e-g"),
|
||||
vec!["*.html".to_string()],
|
||||
),
|
||||
(
|
||||
PathBuf::from("/projects/a-c-d-f/c-d-e-g"),
|
||||
vec!["*.html".to_string()],
|
||||
),
|
||||
(
|
||||
PathBuf::from("/projects/b-c-d-f/c-d-e-g"),
|
||||
vec!["*.html".to_string()],
|
||||
),
|
||||
(
|
||||
PathBuf::from("/projects/a-c-e-f/c-d-e-g"),
|
||||
vec!["*.html".to_string()],
|
||||
),
|
||||
(
|
||||
PathBuf::from("/projects/b-c-e-f/c-d-e-g"),
|
||||
vec!["*.html".to_string()],
|
||||
),
|
||||
(
|
||||
PathBuf::from("/projects/a-c-d-f/b-d-f-g"),
|
||||
vec!["*.html".to_string()],
|
||||
),
|
||||
(
|
||||
PathBuf::from("/projects/b-c-d-f/b-d-f-g"),
|
||||
vec!["*.html".to_string()],
|
||||
),
|
||||
(
|
||||
PathBuf::from("/projects/a-c-e-f/b-d-f-g"),
|
||||
vec!["*.html".to_string()],
|
||||
),
|
||||
(
|
||||
PathBuf::from("/projects/b-c-e-f/b-d-f-g"),
|
||||
vec!["*.html".to_string()],
|
||||
),
|
||||
(
|
||||
PathBuf::from("/projects/a-c-d-f/c-d-f-g"),
|
||||
vec!["*.html".to_string()],
|
||||
),
|
||||
(
|
||||
PathBuf::from("/projects/b-c-d-f/c-d-f-g"),
|
||||
vec!["*.html".to_string()],
|
||||
),
|
||||
(
|
||||
PathBuf::from("/projects/a-c-e-f/c-d-f-g"),
|
||||
vec!["*.html".to_string()],
|
||||
),
|
||||
(
|
||||
PathBuf::from("/projects/b-c-e-f/c-d-f-g"),
|
||||
vec!["*.html".to_string()],
|
||||
),
|
||||
];
|
||||
|
||||
assert_eq!(actual, expected,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_stop_expanding_once_we_hit_a_wildcard() {
|
||||
let actual = get_fast_patterns(
|
||||
&PathBuf::from("/projects"),
|
||||
&vec!["{foo,bar}/example/**/{baz,qux}/*.html".to_string()],
|
||||
);
|
||||
let expected = vec![
|
||||
(
|
||||
PathBuf::from("/projects/foo/example"),
|
||||
vec!["**/{baz,qux}/*.html".to_string()],
|
||||
),
|
||||
(
|
||||
PathBuf::from("/projects/bar/example"),
|
||||
vec!["**/{baz,qux}/*.html".to_string()],
|
||||
),
|
||||
];
|
||||
|
||||
assert_eq!(actual, expected,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_keep_the_negation_symbol_for_all_new_patterns() {
|
||||
let actual = get_fast_patterns(
|
||||
&PathBuf::from("/projects"),
|
||||
&vec!["!{foo,bar}/*.html".to_string()],
|
||||
);
|
||||
let expected = vec![
|
||||
(PathBuf::from("/projects/foo"), vec!["!*.html".to_string()]),
|
||||
(PathBuf::from("/projects/bar"), vec!["!*.html".to_string()]),
|
||||
];
|
||||
|
||||
assert_eq!(actual, expected,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_expand_a_complex_example() {
|
||||
let actual = get_fast_patterns(
|
||||
&PathBuf::from("/projects"),
|
||||
&vec!["a/{b,c}/d/{e,f}/g/*.html".to_string()],
|
||||
);
|
||||
let expected = vec![
|
||||
(
|
||||
PathBuf::from("/projects/a/b/d/e/g"),
|
||||
vec!["*.html".to_string()],
|
||||
),
|
||||
(
|
||||
PathBuf::from("/projects/a/c/d/e/g"),
|
||||
vec!["*.html".to_string()],
|
||||
),
|
||||
(
|
||||
PathBuf::from("/projects/a/b/d/f/g"),
|
||||
vec!["*.html".to_string()],
|
||||
),
|
||||
(
|
||||
PathBuf::from("/projects/a/c/d/f/g"),
|
||||
vec!["*.html".to_string()],
|
||||
),
|
||||
];
|
||||
|
||||
assert_eq!(actual, expected,);
|
||||
}
|
||||
}
|
||||
489
oxide/crates/core/src/lib.rs
Normal file
489
oxide/crates/core/src/lib.rs
Normal file
@ -0,0 +1,489 @@
|
||||
use crate::parser::Extractor;
|
||||
use cache::Cache;
|
||||
use fxhash::FxHashSet;
|
||||
use ignore::DirEntry;
|
||||
use ignore::WalkBuilder;
|
||||
use lazy_static::lazy_static;
|
||||
use rayon::prelude::*;
|
||||
use std::cmp::Ordering;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use tracing::event;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
pub mod cache;
|
||||
pub mod cursor;
|
||||
pub mod fast_skip;
|
||||
pub mod glob;
|
||||
pub mod parser;
|
||||
|
||||
fn init_tracing() {
|
||||
if !*SHOULD_TRACE {
|
||||
return;
|
||||
}
|
||||
|
||||
_ = tracing_subscriber::fmt()
|
||||
.with_max_level(tracing::Level::INFO)
|
||||
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::ACTIVE)
|
||||
.compact()
|
||||
.try_init();
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChangedContent {
|
||||
pub file: Option<PathBuf>,
|
||||
pub content: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScanOptions {
|
||||
pub base: String,
|
||||
pub globs: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScanResult {
|
||||
pub candidates: Vec<String>,
|
||||
pub files: Vec<String>,
|
||||
pub globs: Vec<GlobEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GlobEntry {
|
||||
pub base: String,
|
||||
pub glob: String,
|
||||
}
|
||||
|
||||
pub fn clear_cache() {
|
||||
let mut cache = GLOBAL_CACHE.lock().unwrap();
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
pub fn scan_dir(opts: ScanOptions) -> ScanResult {
|
||||
init_tracing();
|
||||
|
||||
let root = Path::new(&opts.base);
|
||||
|
||||
let (files, dirs) = resolve_files(root);
|
||||
|
||||
let globs = if opts.globs {
|
||||
resolve_globs(root, dirs)
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
let mut cache = GLOBAL_CACHE.lock().unwrap();
|
||||
|
||||
let modified_files = cache.find_modified_files(&files);
|
||||
|
||||
let files = files.iter().map(|x| x.display().to_string()).collect();
|
||||
|
||||
if !modified_files.is_empty() {
|
||||
let content: Vec<_> = modified_files
|
||||
.into_iter()
|
||||
.map(|file| ChangedContent {
|
||||
file: Some(file.clone()),
|
||||
content: None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let candidates = scan_files(content, IO::Parallel as u8 | Parsing::Parallel as u8);
|
||||
cache.add_candidates(candidates);
|
||||
}
|
||||
|
||||
ScanResult {
|
||||
candidates: cache.get_candidates(),
|
||||
files,
|
||||
globs,
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(root))]
|
||||
fn resolve_globs(root: &Path, dirs: Vec<PathBuf>) -> Vec<GlobEntry> {
|
||||
let allowed_paths = FxHashSet::from_iter(dirs);
|
||||
|
||||
// A list of directory names where we can't use globs, but we should track each file
|
||||
// individually instead. This is because these directories are often used for both source and
|
||||
// destination files.
|
||||
let mut forced_static_directories = vec![root.join("public")];
|
||||
|
||||
// A list of known extensions + a list of extensions we found in the project.
|
||||
let mut found_extensions = FxHashSet::from_iter(
|
||||
include_str!("fixtures/template-extensions.txt")
|
||||
.trim()
|
||||
.lines()
|
||||
.filter(|x| !x.starts_with('#')) // Drop commented lines
|
||||
.filter(|x| !x.is_empty()) // Drop empty lines
|
||||
.map(|x| x.to_string()),
|
||||
);
|
||||
|
||||
// All root directories.
|
||||
let mut root_directories = FxHashSet::from_iter(vec![root.to_path_buf()]);
|
||||
|
||||
// All directories where we can safely use deeply nested globs to watch all files.
|
||||
// In other comments we refer to these as "deep glob directories" or similar.
|
||||
//
|
||||
// E.g.: `./src/**/*.{html,js}`
|
||||
let mut deep_globable_directories: FxHashSet<PathBuf> = FxHashSet::default();
|
||||
|
||||
// All directories where we can only use shallow globs to watch all direct files but not
|
||||
// folders.
|
||||
// In other comments we refer to these as "shallow glob directories" or similar.
|
||||
//
|
||||
// E.g.: `./src/*/*.{html,js}`
|
||||
let mut shallow_globable_directories: FxHashSet<PathBuf> = FxHashSet::default();
|
||||
|
||||
// Collect all valid paths from the root. This will already filter out ignored files, unknown
|
||||
// extensions and binary files.
|
||||
let mut it = WalkDir::new(root)
|
||||
// Sorting to make sure that we always see the directories before the files. Also sorting
|
||||
// alphabetically by default.
|
||||
.sort_by(
|
||||
|a, z| match (a.file_type().is_dir(), z.file_type().is_dir()) {
|
||||
(true, false) => Ordering::Less,
|
||||
(false, true) => Ordering::Greater,
|
||||
_ => a.file_name().cmp(z.file_name()),
|
||||
},
|
||||
)
|
||||
.into_iter();
|
||||
|
||||
loop {
|
||||
// We are only interested in valid entries
|
||||
let entry = match it.next() {
|
||||
Some(Ok(entry)) => entry,
|
||||
_ => break,
|
||||
};
|
||||
|
||||
// Ignore known directories that we don't want to traverse into.
|
||||
if entry.file_type().is_dir() && entry.file_name() == ".git" {
|
||||
it.skip_current_dir();
|
||||
continue;
|
||||
}
|
||||
|
||||
if entry.file_type().is_dir() {
|
||||
// If we are in a directory where we know that we can't use any globs, then we have to
|
||||
// track each file individually.
|
||||
if forced_static_directories.contains(&entry.path().to_path_buf()) {
|
||||
forced_static_directories.push(entry.path().to_path_buf());
|
||||
root_directories.insert(entry.path().to_path_buf());
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we are in a directory where the parent is a forced static directory, then this
|
||||
// will become a forced static directory as well.
|
||||
if forced_static_directories.contains(&entry.path().parent().unwrap().to_path_buf()) {
|
||||
forced_static_directories.push(entry.path().to_path_buf());
|
||||
root_directories.insert(entry.path().to_path_buf());
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we are in a directory, and the directory is git ignored, then we don't have to
|
||||
// descent into the directory. However, we have to make sure that we mark the _parent_
|
||||
// directory as a shallow glob directory because using deep globs from any of the
|
||||
// parent directories will include this ignored directory which should not be the case.
|
||||
//
|
||||
// Another important part is that if one of the ignored directories is a deep glob
|
||||
// directory, then all of its parents (until the root) should be marked as shallow glob
|
||||
// directories as well.
|
||||
if !allowed_paths.contains(&entry.path().to_path_buf()) {
|
||||
let mut parent = entry.path().parent();
|
||||
while let Some(parent_path) = parent {
|
||||
// If the parent is already marked as a valid deep glob directory, then we have
|
||||
// to mark it as a shallow glob directory instead, because we won't be able to
|
||||
// use deep globs for this directory anymore.
|
||||
if deep_globable_directories.contains(parent_path) {
|
||||
deep_globable_directories.remove(parent_path);
|
||||
shallow_globable_directories.insert(parent_path.to_path_buf());
|
||||
}
|
||||
|
||||
// If we reached the root, then we can stop.
|
||||
if parent_path == root {
|
||||
break;
|
||||
}
|
||||
|
||||
// Mark the parent directory as a shallow glob directory and continue with its
|
||||
// parent.
|
||||
shallow_globable_directories.insert(parent_path.to_path_buf());
|
||||
parent = parent_path.parent();
|
||||
}
|
||||
|
||||
it.skip_current_dir();
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we are in a directory that is not git ignored, then we can mark this directory as
|
||||
// a valid deep glob directory. This is only necessary if any of its parents aren't
|
||||
// marked as deep glob directories already.
|
||||
let mut found_deep_glob_parent = false;
|
||||
let mut parent = entry.path().parent();
|
||||
while let Some(parent_path) = parent {
|
||||
// If we reached the root, then we can stop.
|
||||
if parent_path == root {
|
||||
break;
|
||||
}
|
||||
|
||||
// If the parent is already marked as a deep glob directory, then we can stop
|
||||
// because this glob will match the current directory already.
|
||||
if deep_globable_directories.contains(parent_path) {
|
||||
found_deep_glob_parent = true;
|
||||
break;
|
||||
}
|
||||
|
||||
parent = parent_path.parent();
|
||||
}
|
||||
|
||||
// If we didn't find a deep glob directory parent, then we can mark this directory as a
|
||||
// deep glob directory (unless it is the root).
|
||||
if !found_deep_glob_parent && entry.path() != root {
|
||||
deep_globable_directories.insert(entry.path().to_path_buf());
|
||||
}
|
||||
}
|
||||
|
||||
// Handle allowed content paths
|
||||
if is_allowed_content_path(entry.path())
|
||||
&& allowed_paths.contains(&entry.path().to_path_buf())
|
||||
{
|
||||
let path = entry.path();
|
||||
|
||||
// Collect the extension for future use when building globs.
|
||||
if let Some(extension) = path.extension().and_then(|x| x.to_str()) {
|
||||
found_extensions.insert(extension.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let extension_list = found_extensions.into_iter().collect::<Vec<_>>().join(",");
|
||||
|
||||
// Build the globs for all globable directories.
|
||||
let shallow_globs = shallow_globable_directories.iter().map(|path| GlobEntry {
|
||||
base: path.display().to_string(),
|
||||
glob: format!("*/*.{{{}}}", extension_list),
|
||||
});
|
||||
|
||||
let deep_globs = deep_globable_directories.iter().map(|path| GlobEntry {
|
||||
base: path.display().to_string(),
|
||||
glob: format!("**/*.{{{}}}", extension_list),
|
||||
});
|
||||
|
||||
shallow_globs.chain(deep_globs).collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(root))]
|
||||
fn resolve_files(root: &Path) -> (Vec<PathBuf>, Vec<PathBuf>) {
|
||||
let mut files: Vec<PathBuf> = vec![];
|
||||
let mut dirs: Vec<PathBuf> = vec![];
|
||||
|
||||
for entry in resolve_allowed_paths(root) {
|
||||
let Some(file_type) = entry.file_type() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if file_type.is_file() {
|
||||
files.push(entry.into_path());
|
||||
} else if file_type.is_dir() {
|
||||
dirs.push(entry.into_path());
|
||||
}
|
||||
}
|
||||
|
||||
(files, dirs)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(root))]
|
||||
pub fn resolve_allowed_paths(root: &Path) -> impl Iterator<Item = DirEntry> {
|
||||
WalkBuilder::new(root)
|
||||
.hidden(false)
|
||||
.filter_entry(|entry| match entry.file_type() {
|
||||
Some(file_type) if file_type.is_dir() => match entry.file_name().to_str() {
|
||||
Some(dir) => !IGNORED_CONTENT_DIRS.contains(&dir),
|
||||
None => false,
|
||||
},
|
||||
Some(file_type) if file_type.is_file() || file_type.is_symlink() => {
|
||||
is_allowed_content_path(entry.path())
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
.build()
|
||||
.filter_map(Result::ok)
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref BINARY_EXTENSIONS: Vec<&'static str> =
|
||||
include_str!("fixtures/binary-extensions.txt")
|
||||
.trim()
|
||||
.lines()
|
||||
.collect::<Vec<_>>();
|
||||
static ref IGNORED_EXTENSIONS: Vec<&'static str> =
|
||||
include_str!("fixtures/ignored-extensions.txt")
|
||||
.trim()
|
||||
.lines()
|
||||
.collect::<Vec<_>>();
|
||||
static ref IGNORED_FILES: Vec<&'static str> = include_str!("fixtures/ignored-files.txt")
|
||||
.trim()
|
||||
.lines()
|
||||
.collect::<Vec<_>>();
|
||||
static ref IGNORED_CONTENT_DIRS: Vec<&'static str> = vec![".git"];
|
||||
static ref SHOULD_TRACE: bool = {
|
||||
matches!(std::env::var("DEBUG"), Ok(value) if value.eq("*") || value.eq("1") || value.eq("true") || value.contains("tailwind"))
|
||||
};
|
||||
|
||||
/// Track file modification times and cache candidates. This cache lives for the lifetime of
|
||||
/// the process and simply adds candidates when files are modified. Since candidates aren't
|
||||
/// removed, incremental builds may contain extra candidates.
|
||||
static ref GLOBAL_CACHE: Mutex<Cache> = {
|
||||
Mutex::new(Cache::default())
|
||||
};
|
||||
}
|
||||
|
||||
pub fn is_allowed_content_path(path: &Path) -> bool {
|
||||
let path = PathBuf::from(path);
|
||||
|
||||
// Skip known ignored files
|
||||
if path
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.map(|s| IGNORED_FILES.contains(&s))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip known ignored extensions
|
||||
path.extension()
|
||||
.map(|s| s.to_str().unwrap_or_default())
|
||||
.map(|ext| !IGNORED_EXTENSIONS.contains(&ext) && !BINARY_EXTENSIONS.contains(&ext))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum IO {
|
||||
Sequential = 0b0001,
|
||||
Parallel = 0b0010,
|
||||
}
|
||||
|
||||
impl From<u8> for IO {
|
||||
fn from(item: u8) -> Self {
|
||||
match item & 0b0011 {
|
||||
0b0001 => IO::Sequential,
|
||||
0b0010 => IO::Parallel,
|
||||
_ => unimplemented!("Unknown 'IO' strategy"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Parsing {
|
||||
Sequential = 0b0100,
|
||||
Parallel = 0b1000,
|
||||
}
|
||||
|
||||
impl From<u8> for Parsing {
|
||||
fn from(item: u8) -> Self {
|
||||
match item & 0b1100 {
|
||||
0b0100 => Parsing::Sequential,
|
||||
0b1000 => Parsing::Parallel,
|
||||
_ => unimplemented!("Unknown 'Parsing' strategy"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(input, options))]
|
||||
pub fn scan_files(input: Vec<ChangedContent>, options: u8) -> Vec<String> {
|
||||
match (IO::from(options), Parsing::from(options)) {
|
||||
(IO::Sequential, Parsing::Sequential) => parse_all_blobs_sync(read_all_files_sync(input)),
|
||||
(IO::Sequential, Parsing::Parallel) => parse_all_blobs(read_all_files_sync(input)),
|
||||
(IO::Parallel, Parsing::Sequential) => parse_all_blobs_sync(read_all_files(input)),
|
||||
(IO::Parallel, Parsing::Parallel) => parse_all_blobs(read_all_files(input)),
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(changed_content))]
|
||||
fn read_all_files(changed_content: Vec<ChangedContent>) -> Vec<Vec<u8>> {
|
||||
event!(
|
||||
tracing::Level::INFO,
|
||||
"Reading {:?} file(s)",
|
||||
changed_content.len()
|
||||
);
|
||||
|
||||
changed_content
|
||||
.into_par_iter()
|
||||
.map(|c| match (c.file, c.content) {
|
||||
(Some(file), None) => match std::fs::read(file) {
|
||||
Ok(content) => content,
|
||||
Err(e) => {
|
||||
event!(tracing::Level::ERROR, "Failed to read file: {:?}", e);
|
||||
Default::default()
|
||||
}
|
||||
},
|
||||
(None, Some(content)) => content.into_bytes(),
|
||||
_ => Default::default(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(changed_content))]
|
||||
fn read_all_files_sync(changed_content: Vec<ChangedContent>) -> Vec<Vec<u8>> {
|
||||
event!(
|
||||
tracing::Level::INFO,
|
||||
"Reading {:?} file(s)",
|
||||
changed_content.len()
|
||||
);
|
||||
|
||||
changed_content
|
||||
.into_iter()
|
||||
.map(|c| match (c.file, c.content) {
|
||||
(Some(file), None) => std::fs::read(file).unwrap(),
|
||||
(None, Some(content)) => content.into_bytes(),
|
||||
_ => Default::default(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(blobs))]
|
||||
fn parse_all_blobs(blobs: Vec<Vec<u8>>) -> Vec<String> {
|
||||
let input: Vec<_> = blobs.iter().map(|blob| &blob[..]).collect();
|
||||
let input = &input[..];
|
||||
|
||||
let mut result: Vec<String> = input
|
||||
.par_iter()
|
||||
.map(|input| Extractor::unique(input, Default::default()))
|
||||
.reduce(Default::default, |mut a, b| {
|
||||
a.extend(b);
|
||||
a
|
||||
})
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
// SAFETY: When we parsed the candidates, we already guaranteed that the byte slices
|
||||
// are valid, therefore we don't have to re-check here when we want to convert it back
|
||||
// to a string.
|
||||
unsafe { String::from_utf8_unchecked(s.to_vec()) }
|
||||
})
|
||||
.collect();
|
||||
result.sort();
|
||||
result
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(blobs))]
|
||||
fn parse_all_blobs_sync(blobs: Vec<Vec<u8>>) -> Vec<String> {
|
||||
let input: Vec<_> = blobs.iter().map(|blob| &blob[..]).collect();
|
||||
let input = &input[..];
|
||||
|
||||
let mut result: Vec<String> = input
|
||||
.iter()
|
||||
.map(|input| Extractor::unique(input, Default::default()))
|
||||
.fold(FxHashSet::default(), |mut a, b| {
|
||||
a.extend(b);
|
||||
a
|
||||
})
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
// SAFETY: When we parsed the candidates, we already guaranteed that the byte slices
|
||||
// are valid, therefore we don't have to re-check here when we want to convert it back
|
||||
// to a string.
|
||||
unsafe { String::from_utf8_unchecked(s.to_vec()) }
|
||||
})
|
||||
.collect();
|
||||
result.sort();
|
||||
result
|
||||
}
|
||||
1427
oxide/crates/core/src/parser.rs
Normal file
1427
oxide/crates/core/src/parser.rs
Normal file
File diff suppressed because it is too large
Load Diff
305
oxide/crates/core/tests/auto_content.rs
Normal file
305
oxide/crates/core/tests/auto_content.rs
Normal file
@ -0,0 +1,305 @@
|
||||
#[cfg(test)]
|
||||
mod auto_content {
|
||||
use std::fs;
|
||||
use std::process::Command;
|
||||
|
||||
use tailwindcss_core::*;
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn scan(paths_with_content: &[(&str, Option<&str>)]) -> (Vec<String>, Vec<String>) {
|
||||
// Create a temporary working directory
|
||||
let dir = tempdir().unwrap().into_path();
|
||||
|
||||
// Initialize this directory as a git repository
|
||||
let _ = Command::new("git").arg("init").current_dir(&dir).output();
|
||||
|
||||
// Create the necessary files
|
||||
for (path, contents) in paths_with_content {
|
||||
let path = dir.join(path);
|
||||
let parent = path.parent().unwrap();
|
||||
if !parent.exists() {
|
||||
fs::create_dir_all(parent).unwrap();
|
||||
}
|
||||
|
||||
match contents {
|
||||
Some(contents) => fs::write(path, contents).unwrap(),
|
||||
None => fs::write(path, "").unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
let base = format!("{}", dir.display());
|
||||
|
||||
// Resolve all content paths for the (temporary) current working directory
|
||||
let result = scan_dir(ScanOptions {
|
||||
base: base.clone(),
|
||||
globs: true,
|
||||
});
|
||||
|
||||
let mut paths: Vec<_> = result
|
||||
.files
|
||||
.into_iter()
|
||||
.map(|x| x.replace(&format!("{}/", &base), ""))
|
||||
.collect();
|
||||
|
||||
for glob in result.globs {
|
||||
paths.push(format!("{}/{}", glob.base, glob.glob));
|
||||
}
|
||||
|
||||
paths = paths
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
let parent_dir = format!("{}/", &base.to_string());
|
||||
x.replace(&parent_dir, "")
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Sort the output for easier comparison (depending on internal datastructure the order
|
||||
// _could_ be random)
|
||||
paths.sort();
|
||||
|
||||
(paths, result.candidates)
|
||||
}
|
||||
|
||||
fn test(paths_with_content: &[(&str, Option<&str>)]) -> Vec<String> {
|
||||
scan(paths_with_content).0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_work_with_a_set_of_root_files() {
|
||||
let globs = test(&[
|
||||
("index.html", None),
|
||||
("a.html", None),
|
||||
("b.html", None),
|
||||
("c.html", None),
|
||||
]);
|
||||
assert_eq!(globs, vec!["a.html", "b.html", "c.html", "index.html"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_work_with_a_set_of_root_files_and_ignore_ignored_files() {
|
||||
let globs = test(&[
|
||||
(".gitignore", Some("b.html")),
|
||||
("index.html", None),
|
||||
("a.html", None),
|
||||
("b.html", None),
|
||||
("c.html", None),
|
||||
]);
|
||||
assert_eq!(globs, vec!["a.html", "c.html", "index.html"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_list_all_files_in_the_public_folder_explicitly() {
|
||||
let globs = test(&[
|
||||
("index.html", None),
|
||||
("public/a.html", None),
|
||||
("public/b.html", None),
|
||||
("public/c.html", None),
|
||||
]);
|
||||
assert_eq!(
|
||||
globs,
|
||||
vec![
|
||||
"index.html",
|
||||
"public/a.html",
|
||||
"public/b.html",
|
||||
"public/c.html",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_list_nested_folders_explicitly_in_the_public_folder() {
|
||||
let globs = test(&[
|
||||
("index.html", None),
|
||||
("public/a.html", None),
|
||||
("public/b.html", None),
|
||||
("public/c.html", None),
|
||||
("public/nested/a.html", None),
|
||||
("public/nested/b.html", None),
|
||||
("public/nested/c.html", None),
|
||||
("public/nested/again/a.html", None),
|
||||
("public/very/deeply/nested/a.html", None),
|
||||
]);
|
||||
assert_eq!(
|
||||
globs,
|
||||
vec![
|
||||
"index.html",
|
||||
"public/a.html",
|
||||
"public/b.html",
|
||||
"public/c.html",
|
||||
"public/nested/a.html",
|
||||
"public/nested/again/a.html",
|
||||
"public/nested/b.html",
|
||||
"public/nested/c.html",
|
||||
"public/very/deeply/nested/a.html",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_list_all_files_in_the_public_folder_explicitly_except_ignored_files() {
|
||||
let globs = test(&[
|
||||
(".gitignore", Some("public/b.html\na.html")),
|
||||
("index.html", None),
|
||||
("public/a.html", None),
|
||||
("public/b.html", None),
|
||||
("public/c.html", None),
|
||||
]);
|
||||
assert_eq!(globs, vec!["index.html", "public/c.html",]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_use_a_glob_for_top_level_folders() {
|
||||
let globs = test(&[
|
||||
("index.html", None),
|
||||
("src/a.html", None),
|
||||
("src/b.html", None),
|
||||
("src/c.html", None),
|
||||
]);
|
||||
assert_eq!(globs, vec![
|
||||
"index.html",
|
||||
"src/**/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
|
||||
"src/a.html",
|
||||
"src/b.html",
|
||||
"src/c.html"
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_ignore_binary_files() {
|
||||
let globs = test(&[
|
||||
("index.html", None),
|
||||
("a.mp4", None),
|
||||
("b.png", None),
|
||||
("c.lock", None),
|
||||
]);
|
||||
assert_eq!(globs, vec!["index.html"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_ignore_known_extensions() {
|
||||
let globs = test(&[
|
||||
("index.html", None),
|
||||
("a.css", None),
|
||||
("b.sass", None),
|
||||
("c.less", None),
|
||||
]);
|
||||
assert_eq!(globs, vec!["index.html"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_ignore_known_files() {
|
||||
let globs = test(&[
|
||||
("index.html", None),
|
||||
("package-lock.json", None),
|
||||
("yarn.lock", None),
|
||||
]);
|
||||
assert_eq!(globs, vec!["index.html"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_ignore_and_expand_nested_ignored_folders() {
|
||||
let globs = test(&[
|
||||
// Explicitly listed root files
|
||||
("foo.html", None),
|
||||
("bar.html", None),
|
||||
("baz.html", None),
|
||||
// Nested folder A, using glob
|
||||
("nested-a/foo.html", None),
|
||||
("nested-a/bar.html", None),
|
||||
("nested-a/baz.html", None),
|
||||
// Nested folder B, with deeply nested files, using glob
|
||||
("nested-b/deeply-nested/foo.html", None),
|
||||
("nested-b/deeply-nested/bar.html", None),
|
||||
("nested-b/deeply-nested/baz.html", None),
|
||||
// Nested folder C, with ignored sub-folder
|
||||
("nested-c/foo.html", None),
|
||||
("nested-c/bar.html", None),
|
||||
("nested-c/baz.html", None),
|
||||
// Ignored folder
|
||||
("nested-c/.gitignore", Some("ignored-folder/")),
|
||||
("nested-c/ignored-folder/foo.html", None),
|
||||
("nested-c/ignored-folder/bar.html", None),
|
||||
("nested-c/ignored-folder/baz.html", None),
|
||||
// Deeply nested, without issues
|
||||
("nested-c/sibling-folder/foo.html", None),
|
||||
("nested-c/sibling-folder/bar.html", None),
|
||||
("nested-c/sibling-folder/baz.html", None),
|
||||
// Nested folder D, with deeply nested ignored folder
|
||||
("nested-d/foo.html", None),
|
||||
("nested-d/bar.html", None),
|
||||
("nested-d/baz.html", None),
|
||||
("nested-d/.gitignore", Some("deep/")),
|
||||
("nested-d/very/deeply/nested/deep/foo.html", None),
|
||||
("nested-d/very/deeply/nested/deep/bar.html", None),
|
||||
("nested-d/very/deeply/nested/deep/baz.html", None),
|
||||
("nested-d/very/deeply/nested/foo.html", None),
|
||||
("nested-d/very/deeply/nested/bar.html", None),
|
||||
("nested-d/very/deeply/nested/baz.html", None),
|
||||
("nested-d/very/deeply/nested/directory/foo.html", None),
|
||||
("nested-d/very/deeply/nested/directory/bar.html", None),
|
||||
("nested-d/very/deeply/nested/directory/baz.html", None),
|
||||
("nested-d/very/deeply/nested/directory/again/foo.html", None),
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
globs,
|
||||
vec![
|
||||
"bar.html",
|
||||
"baz.html",
|
||||
"foo.html",
|
||||
"nested-a/**/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
|
||||
"nested-a/bar.html",
|
||||
"nested-a/baz.html",
|
||||
"nested-a/foo.html",
|
||||
"nested-b/**/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
|
||||
"nested-b/deeply-nested/bar.html",
|
||||
"nested-b/deeply-nested/baz.html",
|
||||
"nested-b/deeply-nested/foo.html",
|
||||
"nested-c/*/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
|
||||
"nested-c/bar.html",
|
||||
"nested-c/baz.html",
|
||||
"nested-c/foo.html",
|
||||
"nested-c/sibling-folder/**/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
|
||||
"nested-c/sibling-folder/bar.html",
|
||||
"nested-c/sibling-folder/baz.html",
|
||||
"nested-c/sibling-folder/foo.html",
|
||||
"nested-d/*/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
|
||||
"nested-d/bar.html",
|
||||
"nested-d/baz.html",
|
||||
"nested-d/foo.html",
|
||||
"nested-d/very/*/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
|
||||
"nested-d/very/deeply/*/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
|
||||
"nested-d/very/deeply/nested/*/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
|
||||
"nested-d/very/deeply/nested/bar.html",
|
||||
"nested-d/very/deeply/nested/baz.html",
|
||||
"nested-d/very/deeply/nested/directory/**/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
|
||||
"nested-d/very/deeply/nested/directory/again/foo.html",
|
||||
"nested-d/very/deeply/nested/directory/bar.html",
|
||||
"nested-d/very/deeply/nested/directory/baz.html",
|
||||
"nested-d/very/deeply/nested/directory/foo.html",
|
||||
"nested-d/very/deeply/nested/foo.html"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_scan_for_utilities() {
|
||||
let mut ignores = String::new();
|
||||
ignores.push_str("# md:font-bold\n");
|
||||
ignores.push_str("foo.html\n");
|
||||
|
||||
let candidates = scan(&[
|
||||
// The gitignore file is used to filter out files but not scanned for candidates
|
||||
(".gitignore", Some(&ignores)),
|
||||
// A file that should definitely be scanned
|
||||
("index.html", Some("font-bold md:flex")),
|
||||
// A file that should definitely not be scanned
|
||||
("foo.jpg", Some("xl:font-bold")),
|
||||
// A file that is ignored
|
||||
("foo.html", Some("lg:font-bold")),
|
||||
])
|
||||
.1;
|
||||
|
||||
assert_eq!(candidates, vec!["font-bold", "md:flex"]);
|
||||
}
|
||||
}
|
||||
7
oxide/crates/node/.cargo/config.toml
Normal file
7
oxide/crates/node/.cargo/config.toml
Normal file
@ -0,0 +1,7 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
[target.aarch64-unknown-linux-musl]
|
||||
linker = "aarch64-linux-musl-gcc"
|
||||
rustflags = ["-C", "target-feature=-crt-static"]
|
||||
[target.armv7-unknown-linux-gnueabihf]
|
||||
linker = "arm-linux-gnueabihf-gcc"
|
||||
201
oxide/crates/node/.gitignore
vendored
Normal file
201
oxide/crates/node/.gitignore
vendored
Normal file
@ -0,0 +1,201 @@
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/node
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=node
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/macos
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=macos
|
||||
|
||||
### macOS ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### macOS Patch ###
|
||||
# iCloud generated files
|
||||
*.icloud
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/macos
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/windows
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=windows
|
||||
|
||||
### Windows ###
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/windows
|
||||
|
||||
#Added by cargo
|
||||
|
||||
/target
|
||||
Cargo.lock
|
||||
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
*.node
|
||||
|
||||
# Generated
|
||||
index.d.ts
|
||||
index.js
|
||||
13
oxide/crates/node/.npmignore
Normal file
13
oxide/crates/node/.npmignore
Normal file
@ -0,0 +1,13 @@
|
||||
target
|
||||
Cargo.lock
|
||||
.cargo
|
||||
.github
|
||||
npm
|
||||
.eslintrc
|
||||
.prettierignore
|
||||
rustfmt.toml
|
||||
yarn.lock
|
||||
*.node
|
||||
.yarn
|
||||
__test__
|
||||
renovate.json
|
||||
18
oxide/crates/node/Cargo.toml
Normal file
18
oxide/crates/node/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
||||
[package]
|
||||
edition = "2021"
|
||||
name = "tailwind-oxide"
|
||||
version = "0.0.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
|
||||
napi = { version = "2.13.1", default-features = false, features = ["napi4"] }
|
||||
napi-derive = "2.13.0"
|
||||
tailwindcss-core = { path = "../core" }
|
||||
rayon = "1.5.3"
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = "2.0.1"
|
||||
|
||||
5
oxide/crates/node/build.rs
Normal file
5
oxide/crates/node/build.rs
Normal file
@ -0,0 +1,5 @@
|
||||
extern crate napi_build;
|
||||
|
||||
fn main() {
|
||||
napi_build::setup();
|
||||
}
|
||||
3
oxide/crates/node/npm/darwin-arm64/README.md
Normal file
3
oxide/crates/node/npm/darwin-arm64/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# `@tailwindcss/oxide-darwin-arm64`
|
||||
|
||||
This is the **aarch64-apple-darwin** binary for `@tailwindcss/oxide`
|
||||
18
oxide/crates/node/npm/darwin-arm64/package.json
Normal file
18
oxide/crates/node/npm/darwin-arm64/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@tailwindcss/oxide-darwin-arm64",
|
||||
"version": "0.0.0-oxide.4",
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"main": "tailwindcss-oxide.darwin-arm64.node",
|
||||
"files": [
|
||||
"tailwindcss-oxide.darwin-arm64.node"
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
3
oxide/crates/node/npm/darwin-x64/README.md
Normal file
3
oxide/crates/node/npm/darwin-x64/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# `@tailwindcss/oxide-darwin-x64`
|
||||
|
||||
This is the **x86_64-apple-darwin** binary for `@tailwindcss/oxide`
|
||||
18
oxide/crates/node/npm/darwin-x64/package.json
Normal file
18
oxide/crates/node/npm/darwin-x64/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@tailwindcss/oxide-darwin-x64",
|
||||
"version": "0.0.0-oxide.4",
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"main": "tailwindcss-oxide.darwin-x64.node",
|
||||
"files": [
|
||||
"tailwindcss-oxide.darwin-x64.node"
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
3
oxide/crates/node/npm/freebsd-x64/README.md
Normal file
3
oxide/crates/node/npm/freebsd-x64/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# `@tailwindcss/oxide-freebsd-x64`
|
||||
|
||||
This is the **x86_64-unknown-freebsd** binary for `@tailwindcss/oxide`
|
||||
18
oxide/crates/node/npm/freebsd-x64/package.json
Normal file
18
oxide/crates/node/npm/freebsd-x64/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@tailwindcss/oxide-freebsd-x64",
|
||||
"version": "0.0.0-oxide.4",
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"main": "tailwindcss-oxide.freebsd-x64.node",
|
||||
"files": [
|
||||
"tailwindcss-oxide.freebsd-x64.node"
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
3
oxide/crates/node/npm/linux-arm-gnueabihf/README.md
Normal file
3
oxide/crates/node/npm/linux-arm-gnueabihf/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# `@tailwindcss/oxide-linux-arm-gnueabihf`
|
||||
|
||||
This is the **armv7-unknown-linux-gnueabihf** binary for `@tailwindcss/oxide`
|
||||
18
oxide/crates/node/npm/linux-arm-gnueabihf/package.json
Normal file
18
oxide/crates/node/npm/linux-arm-gnueabihf/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@tailwindcss/oxide-linux-arm-gnueabihf",
|
||||
"version": "0.0.0-oxide.4",
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"main": "tailwindcss-oxide.linux-arm-gnueabihf.node",
|
||||
"files": [
|
||||
"tailwindcss-oxide.linux-arm-gnueabihf.node"
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
3
oxide/crates/node/npm/linux-arm64-gnu/README.md
Normal file
3
oxide/crates/node/npm/linux-arm64-gnu/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# `@tailwindcss/oxide-linux-arm64-gnu`
|
||||
|
||||
This is the **aarch64-unknown-linux-gnu** binary for `@tailwindcss/oxide`
|
||||
21
oxide/crates/node/npm/linux-arm64-gnu/package.json
Normal file
21
oxide/crates/node/npm/linux-arm64-gnu/package.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@tailwindcss/oxide-linux-arm64-gnu",
|
||||
"version": "0.0.0-oxide.4",
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"main": "tailwindcss-oxide.linux-arm64-gnu.node",
|
||||
"files": [
|
||||
"tailwindcss-oxide.linux-arm64-gnu.node"
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"libc": [
|
||||
"glibc"
|
||||
]
|
||||
}
|
||||
3
oxide/crates/node/npm/linux-arm64-musl/README.md
Normal file
3
oxide/crates/node/npm/linux-arm64-musl/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# `@tailwindcss/oxide-linux-arm64-musl`
|
||||
|
||||
This is the **aarch64-unknown-linux-musl** binary for `@tailwindcss/oxide`
|
||||
21
oxide/crates/node/npm/linux-arm64-musl/package.json
Normal file
21
oxide/crates/node/npm/linux-arm64-musl/package.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@tailwindcss/oxide-linux-arm64-musl",
|
||||
"version": "0.0.0-oxide.4",
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"main": "tailwindcss-oxide.linux-arm64-musl.node",
|
||||
"files": [
|
||||
"tailwindcss-oxide.linux-arm64-musl.node"
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"libc": [
|
||||
"musl"
|
||||
]
|
||||
}
|
||||
3
oxide/crates/node/npm/linux-x64-gnu/README.md
Normal file
3
oxide/crates/node/npm/linux-x64-gnu/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# `@tailwindcss/oxide-linux-x64-gnu`
|
||||
|
||||
This is the **x86_64-unknown-linux-gnu** binary for `@tailwindcss/oxide`
|
||||
21
oxide/crates/node/npm/linux-x64-gnu/package.json
Normal file
21
oxide/crates/node/npm/linux-x64-gnu/package.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@tailwindcss/oxide-linux-x64-gnu",
|
||||
"version": "0.0.0-oxide.4",
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"main": "tailwindcss-oxide.linux-x64-gnu.node",
|
||||
"files": [
|
||||
"tailwindcss-oxide.linux-x64-gnu.node"
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"libc": [
|
||||
"glibc"
|
||||
]
|
||||
}
|
||||
3
oxide/crates/node/npm/linux-x64-musl/README.md
Normal file
3
oxide/crates/node/npm/linux-x64-musl/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# `@tailwindcss/oxide-linux-x64-musl`
|
||||
|
||||
This is the **x86_64-unknown-linux-musl** binary for `@tailwindcss/oxide`
|
||||
21
oxide/crates/node/npm/linux-x64-musl/package.json
Normal file
21
oxide/crates/node/npm/linux-x64-musl/package.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@tailwindcss/oxide-linux-x64-musl",
|
||||
"version": "0.0.0-oxide.4",
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"main": "tailwindcss-oxide.linux-x64-musl.node",
|
||||
"files": [
|
||||
"tailwindcss-oxide.linux-x64-musl.node"
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"libc": [
|
||||
"musl"
|
||||
]
|
||||
}
|
||||
3
oxide/crates/node/npm/win32-x64-msvc/README.md
Normal file
3
oxide/crates/node/npm/win32-x64-msvc/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# `@tailwindcss/oxide-win32-x64-msvc`
|
||||
|
||||
This is the **x86_64-pc-windows-msvc** binary for `@tailwindcss/oxide`
|
||||
18
oxide/crates/node/npm/win32-x64-msvc/package.json
Normal file
18
oxide/crates/node/npm/win32-x64-msvc/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@tailwindcss/oxide-win32-x64-msvc",
|
||||
"version": "0.0.0-oxide.4",
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"main": "tailwindcss-oxide.win32-x64-msvc.node",
|
||||
"files": [
|
||||
"tailwindcss-oxide.win32-x64-msvc.node"
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
49
oxide/crates/node/package.json
Normal file
49
oxide/crates/node/package.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@tailwindcss/oxide",
|
||||
"version": "0.0.0-oxide.4",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"napi": {
|
||||
"name": "tailwindcss-oxide",
|
||||
"triples": {
|
||||
"additional": [
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"aarch64-unknown-linux-musl",
|
||||
"armv7-unknown-linux-gnueabihf",
|
||||
"x86_64-unknown-linux-musl",
|
||||
"x86_64-unknown-freebsd",
|
||||
"i686-pc-windows-msvc"
|
||||
]
|
||||
}
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.17.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"index.d.ts"
|
||||
],
|
||||
"scripts": {
|
||||
"artifacts": "npx napi artifacts",
|
||||
"build": "npx napi build --platform --release --no-const-enum",
|
||||
"dev": "cargo watch --quiet --shell 'npm run build'",
|
||||
"build:debug": "npx napi build --platform --no-const-enum",
|
||||
"version": "npx napi version"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tailwindcss/oxide-darwin-arm64": "workspace:*",
|
||||
"@tailwindcss/oxide-darwin-x64": "workspace:*",
|
||||
"@tailwindcss/oxide-freebsd-x64": "workspace:*",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "workspace:*",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "workspace:*",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "workspace:*",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "workspace:*",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "workspace:*",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "workspace:*"
|
||||
}
|
||||
}
|
||||
2
oxide/crates/node/rustfmt.toml
Normal file
2
oxide/crates/node/rustfmt.toml
Normal file
@ -0,0 +1,2 @@
|
||||
tab_spaces = 2
|
||||
edition = "2021"
|
||||
89
oxide/crates/node/src/lib.rs
Normal file
89
oxide/crates/node/src/lib.rs
Normal file
@ -0,0 +1,89 @@
|
||||
use napi::bindgen_prelude::{FromNapiValue, ToNapiValue};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[macro_use]
|
||||
extern crate napi_derive;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[napi(object)]
|
||||
pub struct ChangedContent {
|
||||
pub file: Option<String>,
|
||||
pub content: Option<String>,
|
||||
pub extension: String,
|
||||
}
|
||||
|
||||
impl From<ChangedContent> for tailwindcss_core::ChangedContent {
|
||||
fn from(changed_content: ChangedContent) -> Self {
|
||||
tailwindcss_core::ChangedContent {
|
||||
file: changed_content.file.map(PathBuf::from),
|
||||
content: changed_content.content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[napi(object)]
|
||||
pub struct ScanResult {
|
||||
pub globs: Vec<GlobEntry>,
|
||||
pub files: Vec<String>,
|
||||
pub candidates: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[napi(object)]
|
||||
pub struct GlobEntry {
|
||||
pub base: String,
|
||||
pub glob: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[napi(object)]
|
||||
pub struct ScanOptions {
|
||||
pub base: String,
|
||||
pub globs: Option<bool>,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn clear_cache() {
|
||||
tailwindcss_core::clear_cache();
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn scan_dir(args: ScanOptions) -> ScanResult {
|
||||
let result = tailwindcss_core::scan_dir(tailwindcss_core::ScanOptions {
|
||||
base: args.base,
|
||||
globs: args.globs.unwrap_or(false),
|
||||
});
|
||||
|
||||
ScanResult {
|
||||
files: result.files,
|
||||
candidates: result.candidates,
|
||||
globs: result
|
||||
.globs
|
||||
.into_iter()
|
||||
.map(|g| GlobEntry {
|
||||
base: g.base,
|
||||
glob: g.glob,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[napi]
|
||||
pub enum IO {
|
||||
Sequential = 0b0001,
|
||||
Parallel = 0b0010,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[napi]
|
||||
pub enum Parsing {
|
||||
Sequential = 0b0100,
|
||||
Parallel = 0b1000,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn scan_files(input: Vec<ChangedContent>, strategy: u8) -> Vec<String> {
|
||||
tailwindcss_core::scan_files(input.into_iter().map(Into::into).collect(), strategy)
|
||||
}
|
||||
20
oxide/package.json
Normal file
20
oxide/package.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "tailwindcss-oxide",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"workspaces": [
|
||||
"node"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "cargo test",
|
||||
"install:cargo": "cargo install cargo-watch cargo-fuzz",
|
||||
"build": "cargo build --release",
|
||||
"build:node": "npm --prefix ./crates/node run build",
|
||||
"dev": "cargo watch --clear --quiet -x 'run --quiet'",
|
||||
"dev:node": "cargo watch --clear --quiet --shell 'npm --prefix ./crates/node run build:debug'",
|
||||
"fuzz": "cd ./crates/core; cargo fuzz run parsing; cd -",
|
||||
"bench": "cargo bench",
|
||||
"postbench": "open ./target/criterion/report/index.html"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
48
package.json
Normal file
48
package.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "@tailwindcss/root",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"prettier": {
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"plugins": [
|
||||
"prettier-plugin-organize-imports"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"tsconfig.json"
|
||||
],
|
||||
"options": {
|
||||
"parser": "jsonc"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"format": "prettier --write .",
|
||||
"lint": "prettier --check . && turbo lint",
|
||||
"build": "turbo build --filter=!./playgrounds/*",
|
||||
"dev": "turbo dev --filter=!./playgrounds/*",
|
||||
"test": "pnpm test --prefix=oxide && vitest run",
|
||||
"test:ui": "pnpm run --filter=tailwindcss test:ui",
|
||||
"tdd": "vitest",
|
||||
"bench": "vitest bench",
|
||||
"version-packages": "node ./scripts/version-packages.mjs",
|
||||
"vite": "pnpm run --filter=vite-playground dev",
|
||||
"nextjs": "pnpm run --filter=nextjs-playground dev"
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.41.2",
|
||||
"@types/node": "^20.11.19",
|
||||
"@vitest/coverage-v8": "^1.2.1",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"tsup": "^8.0.1",
|
||||
"turbo": "^1.12.4",
|
||||
"typescript": "^5.3.3",
|
||||
"vitest": "^1.1.3"
|
||||
}
|
||||
}
|
||||
33
packages/@tailwindcss-postcss/package.json
Normal file
33
packages/@tailwindcss-postcss/package.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@tailwindcss/postcss",
|
||||
"version": "0.0.0-oxide.4",
|
||||
"description": "PostCSS plugin for Tailwind CSS, a utility-first CSS framework for rapidly building custom user interfaces",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/tailwindlabs/tailwindcss.git",
|
||||
"bugs": "https://github.com/tailwindlabs/tailwindcss/issues",
|
||||
"homepage": "https://tailwindcss.com",
|
||||
"scripts": {
|
||||
"lint": "tsc --noEmit",
|
||||
"build": "tsup-node ./src/index.ts --format cjs --dts --cjsInterop --splitting",
|
||||
"dev": "pnpm run build -- --watch"
|
||||
},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"require": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/oxide": "workspace:^",
|
||||
"postcss-import": "^16.0.0",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.17",
|
||||
"@types/postcss-import": "^14.0.3",
|
||||
"postcss": "8.4.24"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,650 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = `
|
||||
"@layer theme {
|
||||
:root {
|
||||
--default-transition-duration: .15s;
|
||||
--default-transition-timing-function: var(--transition-timing-function-in-out);
|
||||
--default-font-family: var(--font-family-sans);
|
||||
--default-font-feature-settings: var(--font-family-sans--font-feature-settings);
|
||||
--default-font-variation-settings: var(--font-family-sans--font-variation-settings);
|
||||
--default-mono-font-family: var(--font-family-mono);
|
||||
--default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings);
|
||||
--default-mono-font-variation-settings: var(--font-family-mono--font-variation-settings);
|
||||
--breakpoint-sm: 640px;
|
||||
--breakpoint-md: 768px;
|
||||
--breakpoint-lg: 1024px;
|
||||
--breakpoint-xl: 1280px;
|
||||
--breakpoint-2xl: 1536px;
|
||||
--color-black: #000;
|
||||
--color-white: #fff;
|
||||
--color-slate-50: #f8fafc;
|
||||
--color-slate-100: #f1f5f9;
|
||||
--color-slate-200: #e2e8f0;
|
||||
--color-slate-300: #cbd5e1;
|
||||
--color-slate-400: #94a3b8;
|
||||
--color-slate-500: #64748b;
|
||||
--color-slate-600: #475569;
|
||||
--color-slate-700: #334155;
|
||||
--color-slate-800: #1e293b;
|
||||
--color-slate-900: #0f172a;
|
||||
--color-slate-950: #020617;
|
||||
--color-gray-50: #f9fafb;
|
||||
--color-gray-100: #f3f4f6;
|
||||
--color-gray-200: #e5e7eb;
|
||||
--color-gray-300: #d1d5db;
|
||||
--color-gray-400: #9ca3af;
|
||||
--color-gray-500: #6b7280;
|
||||
--color-gray-600: #4b5563;
|
||||
--color-gray-700: #374151;
|
||||
--color-gray-800: #1f2937;
|
||||
--color-gray-900: #111827;
|
||||
--color-gray-950: #030712;
|
||||
--color-zinc-50: #fafafa;
|
||||
--color-zinc-100: #f4f4f5;
|
||||
--color-zinc-200: #e4e4e7;
|
||||
--color-zinc-300: #d4d4d8;
|
||||
--color-zinc-400: #a1a1aa;
|
||||
--color-zinc-500: #71717a;
|
||||
--color-zinc-600: #52525b;
|
||||
--color-zinc-700: #3f3f46;
|
||||
--color-zinc-800: #27272a;
|
||||
--color-zinc-900: #18181b;
|
||||
--color-zinc-950: #09090b;
|
||||
--color-neutral-50: #fafafa;
|
||||
--color-neutral-100: #f5f5f5;
|
||||
--color-neutral-200: #e5e5e5;
|
||||
--color-neutral-300: #d4d4d4;
|
||||
--color-neutral-400: #a3a3a3;
|
||||
--color-neutral-500: #737373;
|
||||
--color-neutral-600: #525252;
|
||||
--color-neutral-700: #404040;
|
||||
--color-neutral-800: #262626;
|
||||
--color-neutral-900: #171717;
|
||||
--color-neutral-950: #0a0a0a;
|
||||
--color-stone-50: #fafaf9;
|
||||
--color-stone-100: #f5f5f4;
|
||||
--color-stone-200: #e7e5e4;
|
||||
--color-stone-300: #d6d3d1;
|
||||
--color-stone-400: #a8a29e;
|
||||
--color-stone-500: #78716c;
|
||||
--color-stone-600: #57534e;
|
||||
--color-stone-700: #44403c;
|
||||
--color-stone-800: #292524;
|
||||
--color-stone-900: #1c1917;
|
||||
--color-stone-950: #0c0a09;
|
||||
--color-red-50: #fef2f2;
|
||||
--color-red-100: #fee2e2;
|
||||
--color-red-200: #fecaca;
|
||||
--color-red-300: #fca5a5;
|
||||
--color-red-400: #f87171;
|
||||
--color-red-500: #ef4444;
|
||||
--color-red-600: #dc2626;
|
||||
--color-red-700: #b91c1c;
|
||||
--color-red-800: #991b1b;
|
||||
--color-red-900: #7f1d1d;
|
||||
--color-red-950: #450a0a;
|
||||
--color-orange-50: #fff7ed;
|
||||
--color-orange-100: #ffedd5;
|
||||
--color-orange-200: #fed7aa;
|
||||
--color-orange-300: #fdba74;
|
||||
--color-orange-400: #fb923c;
|
||||
--color-orange-500: #f97316;
|
||||
--color-orange-600: #ea580c;
|
||||
--color-orange-700: #c2410c;
|
||||
--color-orange-800: #9a3412;
|
||||
--color-orange-900: #7c2d12;
|
||||
--color-orange-950: #431407;
|
||||
--color-amber-50: #fffbeb;
|
||||
--color-amber-100: #fef3c7;
|
||||
--color-amber-200: #fde68a;
|
||||
--color-amber-300: #fcd34d;
|
||||
--color-amber-400: #fbbf24;
|
||||
--color-amber-500: #f59e0b;
|
||||
--color-amber-600: #d97706;
|
||||
--color-amber-700: #b45309;
|
||||
--color-amber-800: #92400e;
|
||||
--color-amber-900: #78350f;
|
||||
--color-amber-950: #451a03;
|
||||
--color-yellow-50: #fefce8;
|
||||
--color-yellow-100: #fef9c3;
|
||||
--color-yellow-200: #fef08a;
|
||||
--color-yellow-300: #fde047;
|
||||
--color-yellow-400: #facc15;
|
||||
--color-yellow-500: #eab308;
|
||||
--color-yellow-600: #ca8a04;
|
||||
--color-yellow-700: #a16207;
|
||||
--color-yellow-800: #854d0e;
|
||||
--color-yellow-900: #713f12;
|
||||
--color-yellow-950: #422006;
|
||||
--color-lime-50: #f7fee7;
|
||||
--color-lime-100: #ecfccb;
|
||||
--color-lime-200: #d9f99d;
|
||||
--color-lime-300: #bef264;
|
||||
--color-lime-400: #a3e635;
|
||||
--color-lime-500: #84cc16;
|
||||
--color-lime-600: #65a30d;
|
||||
--color-lime-700: #4d7c0f;
|
||||
--color-lime-800: #3f6212;
|
||||
--color-lime-900: #365314;
|
||||
--color-lime-950: #1a2e05;
|
||||
--color-green-50: #f0fdf4;
|
||||
--color-green-100: #dcfce7;
|
||||
--color-green-200: #bbf7d0;
|
||||
--color-green-300: #86efac;
|
||||
--color-green-400: #4ade80;
|
||||
--color-green-500: #22c55e;
|
||||
--color-green-600: #16a34a;
|
||||
--color-green-700: #15803d;
|
||||
--color-green-800: #166534;
|
||||
--color-green-900: #14532d;
|
||||
--color-green-950: #052e16;
|
||||
--color-emerald-50: #ecfdf5;
|
||||
--color-emerald-100: #d1fae5;
|
||||
--color-emerald-200: #a7f3d0;
|
||||
--color-emerald-300: #6ee7b7;
|
||||
--color-emerald-400: #34d399;
|
||||
--color-emerald-500: #10b981;
|
||||
--color-emerald-600: #059669;
|
||||
--color-emerald-700: #047857;
|
||||
--color-emerald-800: #065f46;
|
||||
--color-emerald-900: #064e3b;
|
||||
--color-emerald-950: #022c22;
|
||||
--color-teal-50: #f0fdfa;
|
||||
--color-teal-100: #ccfbf1;
|
||||
--color-teal-200: #99f6e4;
|
||||
--color-teal-300: #5eead4;
|
||||
--color-teal-400: #2dd4bf;
|
||||
--color-teal-500: #14b8a6;
|
||||
--color-teal-600: #0d9488;
|
||||
--color-teal-700: #0f766e;
|
||||
--color-teal-800: #115e59;
|
||||
--color-teal-900: #134e4a;
|
||||
--color-teal-950: #042f2e;
|
||||
--color-cyan-50: #ecfeff;
|
||||
--color-cyan-100: #cffafe;
|
||||
--color-cyan-200: #a5f3fc;
|
||||
--color-cyan-300: #67e8f9;
|
||||
--color-cyan-400: #22d3ee;
|
||||
--color-cyan-500: #06b6d4;
|
||||
--color-cyan-600: #0891b2;
|
||||
--color-cyan-700: #0e7490;
|
||||
--color-cyan-800: #155e75;
|
||||
--color-cyan-900: #164e63;
|
||||
--color-cyan-950: #083344;
|
||||
--color-sky-50: #f0f9ff;
|
||||
--color-sky-100: #e0f2fe;
|
||||
--color-sky-200: #bae6fd;
|
||||
--color-sky-300: #7dd3fc;
|
||||
--color-sky-400: #38bdf8;
|
||||
--color-sky-500: #0ea5e9;
|
||||
--color-sky-600: #0284c7;
|
||||
--color-sky-700: #0369a1;
|
||||
--color-sky-800: #075985;
|
||||
--color-sky-900: #0c4a6e;
|
||||
--color-sky-950: #082f49;
|
||||
--color-blue-50: #eff6ff;
|
||||
--color-blue-100: #dbeafe;
|
||||
--color-blue-200: #bfdbfe;
|
||||
--color-blue-300: #93c5fd;
|
||||
--color-blue-400: #60a5fa;
|
||||
--color-blue-500: #3b82f6;
|
||||
--color-blue-600: #2563eb;
|
||||
--color-blue-700: #1d4ed8;
|
||||
--color-blue-800: #1e40af;
|
||||
--color-blue-900: #1e3a8a;
|
||||
--color-blue-950: #172554;
|
||||
--color-indigo-50: #eef2ff;
|
||||
--color-indigo-100: #e0e7ff;
|
||||
--color-indigo-200: #c7d2fe;
|
||||
--color-indigo-300: #a5b4fc;
|
||||
--color-indigo-400: #818cf8;
|
||||
--color-indigo-500: #6366f1;
|
||||
--color-indigo-600: #4f46e5;
|
||||
--color-indigo-700: #4338ca;
|
||||
--color-indigo-800: #3730a3;
|
||||
--color-indigo-900: #312e81;
|
||||
--color-indigo-950: #1e1b4b;
|
||||
--color-violet-50: #f5f3ff;
|
||||
--color-violet-100: #ede9fe;
|
||||
--color-violet-200: #ddd6fe;
|
||||
--color-violet-300: #c4b5fd;
|
||||
--color-violet-400: #a78bfa;
|
||||
--color-violet-500: #8b5cf6;
|
||||
--color-violet-600: #7c3aed;
|
||||
--color-violet-700: #6d28d9;
|
||||
--color-violet-800: #5b21b6;
|
||||
--color-violet-900: #4c1d95;
|
||||
--color-violet-950: #2e1065;
|
||||
--color-purple-50: #faf5ff;
|
||||
--color-purple-100: #f3e8ff;
|
||||
--color-purple-200: #e9d5ff;
|
||||
--color-purple-300: #d8b4fe;
|
||||
--color-purple-400: #c084fc;
|
||||
--color-purple-500: #a855f7;
|
||||
--color-purple-600: #9333ea;
|
||||
--color-purple-700: #7e22ce;
|
||||
--color-purple-800: #6b21a8;
|
||||
--color-purple-900: #581c87;
|
||||
--color-purple-950: #3b0764;
|
||||
--color-fuchsia-50: #fdf4ff;
|
||||
--color-fuchsia-100: #fae8ff;
|
||||
--color-fuchsia-200: #f5d0fe;
|
||||
--color-fuchsia-300: #f0abfc;
|
||||
--color-fuchsia-400: #e879f9;
|
||||
--color-fuchsia-500: #d946ef;
|
||||
--color-fuchsia-600: #c026d3;
|
||||
--color-fuchsia-700: #a21caf;
|
||||
--color-fuchsia-800: #86198f;
|
||||
--color-fuchsia-900: #701a75;
|
||||
--color-fuchsia-950: #4a044e;
|
||||
--color-pink-50: #fdf2f8;
|
||||
--color-pink-100: #fce7f3;
|
||||
--color-pink-200: #fbcfe8;
|
||||
--color-pink-300: #f9a8d4;
|
||||
--color-pink-400: #f472b6;
|
||||
--color-pink-500: #ec4899;
|
||||
--color-pink-600: #db2777;
|
||||
--color-pink-700: #be185d;
|
||||
--color-pink-800: #9d174d;
|
||||
--color-pink-900: #831843;
|
||||
--color-pink-950: #500724;
|
||||
--color-rose-50: #fff1f2;
|
||||
--color-rose-100: #ffe4e6;
|
||||
--color-rose-200: #fecdd3;
|
||||
--color-rose-300: #fda4af;
|
||||
--color-rose-400: #fb7185;
|
||||
--color-rose-500: #f43f5e;
|
||||
--color-rose-600: #e11d48;
|
||||
--color-rose-700: #be123c;
|
||||
--color-rose-800: #9f1239;
|
||||
--color-rose-900: #881337;
|
||||
--color-rose-950: #4c0519;
|
||||
--animate-spin: spin 1s linear infinite;
|
||||
--animate-ping: ping 1s cubic-bezier(0, 0, .2, 1) infinite;
|
||||
--animate-pulse: pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;
|
||||
--animate-bounce: bounce 1s infinite;
|
||||
--blur: 8px;
|
||||
--blur-sm: 4px;
|
||||
--blur-md: 12px;
|
||||
--blur-lg: 16px;
|
||||
--blur-xl: 24px;
|
||||
--blur-2xl: 40px;
|
||||
--blur-3xl: 64px;
|
||||
--radius-none: 0px;
|
||||
--radius-full: 9999px;
|
||||
--radius-sm: .125rem;
|
||||
--radius: .25rem;
|
||||
--radius-md: .375rem;
|
||||
--radius-lg: .5rem;
|
||||
--radius-xl: .75rem;
|
||||
--radius-2xl: 1rem;
|
||||
--radius-3xl: 1.5rem;
|
||||
--shadow: 0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;
|
||||
--shadow-xs: 0 1px #0000000d;
|
||||
--shadow-sm: 0 1px 2px 0 #0000000d;
|
||||
--shadow-md: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
|
||||
--shadow-lg: 0 10px 15px -3px #0000001a, 0 4px 6px -4px #0000001a;
|
||||
--shadow-xl: 0 20px 25px -5px #0000001a, 0 8px 10px -6px #0000001a;
|
||||
--shadow-2xl: 0 25px 50px -12px #00000040;
|
||||
--shadow-inner: inset 0 2px 4px 0 #0000000d;
|
||||
--inset-shadow-xs: inset 0 1px #0000000d;
|
||||
--inset-shadow-sm: inset 0 1px 1px #0000000d;
|
||||
--inset-shadow: inset 0 2px 4px #0000000d;
|
||||
--drop-shadow: 0 1px 2px #0000001a, 0 1px 1px #0000000f;
|
||||
--drop-shadow-sm: 0 1px 1px #0000000d;
|
||||
--drop-shadow-md: 0 4px 3px #00000012, 0 2px 2px #0000000f;
|
||||
--drop-shadow-lg: 0 10px 8px #0000000a, 0 4px 3px #0000001a;
|
||||
--drop-shadow-xl: 0 20px 13px #00000008, 0 8px 5px #00000014;
|
||||
--drop-shadow-2xl: 0 25px 25px #00000026;
|
||||
--spacing-px: 1px;
|
||||
--spacing-0: 0px;
|
||||
--spacing-0_5: .125rem;
|
||||
--spacing-1: .25rem;
|
||||
--spacing-1_5: .375rem;
|
||||
--spacing-2: .5rem;
|
||||
--spacing-2_5: .625rem;
|
||||
--spacing-3: .75rem;
|
||||
--spacing-3_5: .875rem;
|
||||
--spacing-4: 1rem;
|
||||
--spacing-5: 1.25rem;
|
||||
--spacing-6: 1.5rem;
|
||||
--spacing-7: 1.75rem;
|
||||
--spacing-8: 2rem;
|
||||
--spacing-9: 2.25rem;
|
||||
--spacing-10: 2.5rem;
|
||||
--spacing-11: 2.75rem;
|
||||
--spacing-12: 3rem;
|
||||
--spacing-14: 3.5rem;
|
||||
--spacing-16: 4rem;
|
||||
--spacing-20: 5rem;
|
||||
--spacing-24: 6rem;
|
||||
--spacing-28: 7rem;
|
||||
--spacing-32: 8rem;
|
||||
--spacing-36: 9rem;
|
||||
--spacing-40: 10rem;
|
||||
--spacing-44: 11rem;
|
||||
--spacing-48: 12rem;
|
||||
--spacing-52: 13rem;
|
||||
--spacing-56: 14rem;
|
||||
--spacing-60: 15rem;
|
||||
--spacing-64: 16rem;
|
||||
--spacing-72: 18rem;
|
||||
--spacing-80: 20rem;
|
||||
--spacing-96: 24rem;
|
||||
--width-3xs: 16rem;
|
||||
--width-2xs: 18rem;
|
||||
--width-xs: 20rem;
|
||||
--width-sm: 24rem;
|
||||
--width-md: 28rem;
|
||||
--width-lg: 32rem;
|
||||
--width-xl: 36rem;
|
||||
--width-2xl: 42rem;
|
||||
--width-3xl: 48rem;
|
||||
--width-4xl: 56rem;
|
||||
--width-5xl: 64rem;
|
||||
--width-6xl: 72rem;
|
||||
--width-7xl: 80rem;
|
||||
--width-prose: 65ch;
|
||||
--font-family-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--font-family-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-family-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--font-size-xs: .75rem;
|
||||
--font-size-xs--line-height: 1rem;
|
||||
--font-size-sm: .875rem;
|
||||
--font-size-sm--line-height: 1.25rem;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-base--line-height: 1.5rem;
|
||||
--font-size-lg: 1.125rem;
|
||||
--font-size-lg--line-height: 1.75rem;
|
||||
--font-size-xl: 1.25rem;
|
||||
--font-size-xl--line-height: 1.75rem;
|
||||
--font-size-2xl: 1.5rem;
|
||||
--font-size-2xl--line-height: 2rem;
|
||||
--font-size-3xl: 1.875rem;
|
||||
--font-size-3xl--line-height: 2.25rem;
|
||||
--font-size-4xl: 2.25rem;
|
||||
--font-size-4xl--line-height: 2.5rem;
|
||||
--font-size-5xl: 3rem;
|
||||
--font-size-5xl--line-height: 1;
|
||||
--font-size-6xl: 3.75rem;
|
||||
--font-size-6xl--line-height: 1;
|
||||
--font-size-7xl: 4.5rem;
|
||||
--font-size-7xl--line-height: 1;
|
||||
--font-size-8xl: 6rem;
|
||||
--font-size-8xl--line-height: 1;
|
||||
--font-size-9xl: 8rem;
|
||||
--font-size-9xl--line-height: 1;
|
||||
--letter-spacing-tighter: -.05em;
|
||||
--letter-spacing-tight: -.025em;
|
||||
--letter-spacing-normal: 0em;
|
||||
--letter-spacing-wide: .025em;
|
||||
--letter-spacing-wider: .05em;
|
||||
--letter-spacing-widest: .1em;
|
||||
--line-height-none: 1;
|
||||
--line-height-tight: 1.25;
|
||||
--line-height-snug: 1.375;
|
||||
--line-height-normal: 1.5;
|
||||
--line-height-relaxed: 1.625;
|
||||
--line-height-loose: 2;
|
||||
--line-height-3: .75rem;
|
||||
--line-height-4: 1rem;
|
||||
--line-height-5: 1.25rem;
|
||||
--line-height-6: 1.5rem;
|
||||
--line-height-7: 1.75rem;
|
||||
--line-height-8: 2rem;
|
||||
--line-height-9: 2.25rem;
|
||||
--line-height-10: 2.5rem;
|
||||
--transition-timing-function: cubic-bezier(.4, 0, .2, 1);
|
||||
--transition-timing-function-linear: linear;
|
||||
--transition-timing-function-in: cubic-bezier(.4, 0, 1, 1);
|
||||
--transition-timing-function-out: cubic-bezier(0, 0, .2, 1);
|
||||
--transition-timing-function-in-out: cubic-bezier(.4, 0, .2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
*, :after, :before, ::backdrop {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html, :host {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
tab-size: 4;
|
||||
line-height: 1.5;
|
||||
font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
|
||||
font-feature-settings: var(--default-font-feature-settings, normal);
|
||||
font-variation-settings: var(--default-font-variation-settings, normal);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
body {
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
hr {
|
||||
color: inherit;
|
||||
border: 0 solid;
|
||||
border-top-width: 1px;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
abbr:where([title]) {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
-webkit-text-decoration: inherit;
|
||||
-webkit-text-decoration: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
b, strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
code, kbd, samp, pre {
|
||||
font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
|
||||
font-feature-settings: var(--default-mono-font-feature-settings, normal);
|
||||
font-variation-settings: var(--default-mono-font-variation-settings, normal);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
sub, sup {
|
||||
vertical-align: baseline;
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -.5em;
|
||||
}
|
||||
|
||||
table {
|
||||
text-indent: 0;
|
||||
border-color: inherit;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
button, input, optgroup, select, textarea {
|
||||
font: inherit;
|
||||
font-feature-settings: inherit;
|
||||
font-variation-settings: inherit;
|
||||
color: inherit;
|
||||
background: none;
|
||||
border: 1px solid;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
font-feature-settings: inherit;
|
||||
font-variation-settings: inherit;
|
||||
color: inherit;
|
||||
background: none;
|
||||
border: 1px solid;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button, input:where([type="button"], [type="reset"], [type="submit"]) {
|
||||
appearance: button;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
appearance: button;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ol, ul, menu {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
dialog {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
opacity: 1;
|
||||
color: color-mix(in srgb, currentColor 50%, transparent);
|
||||
}
|
||||
|
||||
:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
img, svg, video, canvas, audio, iframe, embed, object {
|
||||
vertical-align: middle;
|
||||
display: block;
|
||||
}
|
||||
|
||||
img, video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components;
|
||||
|
||||
@layer utilities {
|
||||
.text-2xl {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
.text-black\\/50 {
|
||||
color: #00000080;
|
||||
}
|
||||
|
||||
.underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
@media (width >= 1536px) {
|
||||
.\\32 xl\\:font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ping {
|
||||
75%, 100% {
|
||||
opacity: 0;
|
||||
transform: scale(2);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
50% {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% {
|
||||
animation-timing-function: cubic-bezier(.8, 0, 1, 1);
|
||||
transform: translateY(-25%);
|
||||
}
|
||||
|
||||
50% {
|
||||
animation-timing-function: cubic-bezier(0, 0, .2, 1);
|
||||
transform: none;
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@ -0,0 +1 @@
|
||||
<div class="underline 2xl:font-bold"></div>
|
||||
@ -0,0 +1 @@
|
||||
const className = 'text-2xl text-black/50'
|
||||
130
packages/@tailwindcss-postcss/src/index.test.ts
Normal file
130
packages/@tailwindcss-postcss/src/index.test.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { unlink, writeFile } from 'node:fs/promises'
|
||||
import postcss from 'postcss'
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'vitest'
|
||||
import tailwindcss from './index'
|
||||
|
||||
// We give this file path to PostCSS for processing.
|
||||
// This file doesn't exist, but the path is used to resolve imports.
|
||||
// We place it in packages/ because Vitest runs in the monorepo root,
|
||||
// and packages/tailwindcss must be a sub-folder for
|
||||
// @import 'tailwindcss' to work.
|
||||
const INPUT_CSS_PATH = `${__dirname}/fixtures/example-project/input.css`
|
||||
|
||||
const css = String.raw
|
||||
|
||||
beforeEach(async () => {
|
||||
const { clearCache } = await import('@tailwindcss/oxide')
|
||||
clearCache()
|
||||
})
|
||||
|
||||
test("`@import 'tailwindcss'` is replaced with the generated CSS", async () => {
|
||||
let processor = postcss([tailwindcss({ base: `${__dirname}/fixtures/example-project` })])
|
||||
|
||||
let result = await processor.process(`@import 'tailwindcss'`, { from: INPUT_CSS_PATH })
|
||||
|
||||
expect(result.css.trim()).toMatchSnapshot()
|
||||
|
||||
// Check for dependency messages
|
||||
expect(result.messages).toContainEqual({
|
||||
type: 'dependency',
|
||||
file: expect.stringMatching(/index.html$/g),
|
||||
parent: expect.any(String),
|
||||
plugin: expect.any(String),
|
||||
})
|
||||
expect(result.messages).toContainEqual({
|
||||
type: 'dependency',
|
||||
file: expect.stringMatching(/index.js$/g),
|
||||
parent: expect.any(String),
|
||||
plugin: expect.any(String),
|
||||
})
|
||||
expect(result.messages).toContainEqual({
|
||||
type: 'dir-dependency',
|
||||
dir: expect.stringMatching(/example-project\/src$/g),
|
||||
glob: expect.stringMatching(/^\*\*\/\*/g),
|
||||
parent: expect.any(String),
|
||||
plugin: expect.any(String),
|
||||
})
|
||||
})
|
||||
|
||||
test('output is optimized by Lightning CSS', async () => {
|
||||
let processor = postcss([tailwindcss({ base: `${__dirname}/fixtures/example-project` })])
|
||||
|
||||
// `@apply` is used because Lightning is skipped if neither `@tailwind` nor
|
||||
// `@apply` is used.
|
||||
let result = await processor.process(
|
||||
css`
|
||||
@layer utilities {
|
||||
.foo {
|
||||
@apply text-[black];
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.bar {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ from: INPUT_CSS_PATH },
|
||||
)
|
||||
|
||||
expect(result.css.trim()).toMatchInlineSnapshot(`
|
||||
"@layer utilities {
|
||||
.foo {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.bar {
|
||||
color: red;
|
||||
}
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('@apply can be used without emitting the theme in the CSS file', async () => {
|
||||
let processor = postcss([tailwindcss({ base: `${__dirname}/fixtures/example-project` })])
|
||||
|
||||
// `@apply` is used because Lightning is skipped if neither `@tailwind` nor
|
||||
// `@apply` is used.
|
||||
let result = await processor.process(
|
||||
css`
|
||||
@import 'tailwindcss/theme.css' reference;
|
||||
.foo {
|
||||
@apply text-red-500;
|
||||
}
|
||||
`,
|
||||
{ from: INPUT_CSS_PATH },
|
||||
)
|
||||
|
||||
expect(result.css.trim()).toMatchInlineSnapshot(`
|
||||
".foo {
|
||||
color: #ef4444;
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
describe('processing without specifying a base path', () => {
|
||||
let filepath = `${process.cwd()}/my-test-file.html`
|
||||
|
||||
beforeEach(() =>
|
||||
writeFile(filepath, `<div class="md:[&:hover]:content-['testing_default_base_path']">`),
|
||||
)
|
||||
afterEach(() => unlink(filepath))
|
||||
|
||||
test('the current working directory is used by default', async () => {
|
||||
let processor = postcss([tailwindcss()])
|
||||
|
||||
let result = await processor.process(`@import "tailwindcss"`, { from: INPUT_CSS_PATH })
|
||||
|
||||
expect(result.css).toContain(
|
||||
".md\\:\\[\\&\\:hover\\]\\:content-\\[\\'testing_default_base_path\\'\\]",
|
||||
)
|
||||
|
||||
expect(result.messages).toContainEqual({
|
||||
type: 'dependency',
|
||||
file: expect.stringMatching(/my-test-file.html$/g),
|
||||
parent: expect.any(String),
|
||||
plugin: expect.any(String),
|
||||
})
|
||||
})
|
||||
})
|
||||
82
packages/@tailwindcss-postcss/src/index.ts
Normal file
82
packages/@tailwindcss-postcss/src/index.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { scanDir } from '@tailwindcss/oxide'
|
||||
import postcss, { type AcceptedPlugin, type PluginCreator } from 'postcss'
|
||||
import postcssImport from 'postcss-import'
|
||||
import { compile, optimizeCss } from 'tailwindcss'
|
||||
|
||||
type PluginOptions = {
|
||||
// The base directory to scan for class candidates.
|
||||
base?: string
|
||||
}
|
||||
|
||||
function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
|
||||
let base = opts.base ?? process.cwd()
|
||||
|
||||
return {
|
||||
postcssPlugin: 'tailwindcss-v4',
|
||||
plugins: [
|
||||
// We need to run `postcss-import` first to handle `@import` rules.
|
||||
postcssImport(),
|
||||
|
||||
(root, result) => {
|
||||
let hasApply = false
|
||||
let hasTailwind = false
|
||||
|
||||
root.walkAtRules((rule) => {
|
||||
if (rule.name === 'apply') {
|
||||
hasApply = true
|
||||
} else if (rule.name === 'tailwind') {
|
||||
hasApply = true
|
||||
hasTailwind = true
|
||||
// If we've found `@tailwind` then we already
|
||||
// know we have to run a "full" build
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
// Do nothing if neither `@tailwind` nor `@apply` is used
|
||||
if (!hasTailwind && !hasApply) return
|
||||
|
||||
function replaceCss(css: string) {
|
||||
root.removeAll()
|
||||
root.append(postcss.parse(optimizeCss(css), result.opts))
|
||||
}
|
||||
|
||||
// No `@tailwind` means we don't have to look for candidates
|
||||
if (!hasTailwind) {
|
||||
replaceCss(compile(root.toString(), []))
|
||||
return
|
||||
}
|
||||
|
||||
// Look for candidates used to generate the CSS
|
||||
let { candidates, files, globs } = scanDir({ base, globs: true })
|
||||
|
||||
// Add all found files as direct dependencies
|
||||
for (let file of files) {
|
||||
result.messages.push({
|
||||
type: 'dependency',
|
||||
plugin: 'tailwindcss-v4',
|
||||
file,
|
||||
parent: result.opts.from,
|
||||
})
|
||||
}
|
||||
|
||||
// Register dependencies so changes in `base` cause a rebuild while
|
||||
// giving tools like Vite or Parcel a glob that can be used to limit
|
||||
// the files that cause a rebuild to only those that match it.
|
||||
for (let { base, glob } of globs) {
|
||||
result.messages.push({
|
||||
type: 'dir-dependency',
|
||||
plugin: 'tailwindcss-v4',
|
||||
dir: base,
|
||||
glob,
|
||||
parent: result.opts.from,
|
||||
})
|
||||
}
|
||||
|
||||
replaceCss(compile(root.toString(), candidates))
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export default Object.assign(tailwindcss, { postcss: true }) as PluginCreator<PluginOptions>
|
||||
3
packages/@tailwindcss-postcss/tsconfig.json
Normal file
3
packages/@tailwindcss-postcss/tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
}
|
||||
30
packages/@tailwindcss-vite/package.json
Normal file
30
packages/@tailwindcss-vite/package.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@tailwindcss/vite",
|
||||
"version": "0.0.0-oxide.4",
|
||||
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/tailwindlabs/tailwindcss.git",
|
||||
"bugs": "https://github.com/tailwindlabs/tailwindcss/issues",
|
||||
"homepage": "https://tailwindcss.com",
|
||||
"scripts": {
|
||||
"build": "tsup-node ./src/index.ts --format esm --dts",
|
||||
"dev": "pnpm run build -- --watch"
|
||||
},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"import": "./dist/index.mjs"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/oxide": "workspace:^",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.17",
|
||||
"vite": "^5.0.11"
|
||||
}
|
||||
}
|
||||
177
packages/@tailwindcss-vite/src/index.ts
Normal file
177
packages/@tailwindcss-vite/src/index.ts
Normal file
@ -0,0 +1,177 @@
|
||||
import { IO, Parsing, scanFiles } from '@tailwindcss/oxide'
|
||||
import path from 'path'
|
||||
import { compile, optimizeCss } from 'tailwindcss'
|
||||
import type { Plugin, Update, ViteDevServer } from 'vite'
|
||||
|
||||
export default function tailwindcss(): Plugin[] {
|
||||
let server: ViteDevServer | null = null
|
||||
let candidates = new Set<string>()
|
||||
let cssModules = new Set<string>()
|
||||
let minify = false
|
||||
|
||||
function isCssFile(id: string) {
|
||||
let [filename] = id.split('?', 2)
|
||||
let extension = path.extname(filename).slice(1)
|
||||
return extension === 'css'
|
||||
}
|
||||
|
||||
// Trigger update to all css modules
|
||||
function updateCssModules() {
|
||||
// If we're building then we don't need to update anything
|
||||
if (!server) return
|
||||
|
||||
let updates: Update[] = []
|
||||
for (let id of cssModules) {
|
||||
let cssModule = server.moduleGraph.getModuleById(id)
|
||||
if (!cssModule) {
|
||||
console.log('Could not find css module', id)
|
||||
continue
|
||||
}
|
||||
|
||||
server.moduleGraph.invalidateModule(cssModule)
|
||||
updates.push({
|
||||
type: `${cssModule.type}-update`,
|
||||
path: cssModule.url,
|
||||
acceptedPath: cssModule.url,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
if (updates.length > 0) {
|
||||
server.hot.send({ type: 'update', updates })
|
||||
}
|
||||
}
|
||||
|
||||
function scan(src: string, extension: string) {
|
||||
let updated = false
|
||||
// Parse all candidates given the resolved files
|
||||
for (let candidate of scanFiles(
|
||||
[{ content: src, extension }],
|
||||
IO.Sequential | Parsing.Sequential,
|
||||
)) {
|
||||
// On an initial or full build, updated becomes true immediately so we
|
||||
// won't be making extra checks.
|
||||
if (!updated) {
|
||||
if (candidates.has(candidate)) continue
|
||||
updated = true
|
||||
}
|
||||
candidates.add(candidate)
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
function generateCss(css: string) {
|
||||
return optimizeCss(compile(css, Array.from(candidates)), { minify })
|
||||
}
|
||||
|
||||
// In dev mode, there isn't a hook to signal that we've seen all files. We use
|
||||
// a timer, resetting it on each file seen, and trigger CSS generation when we
|
||||
// haven't seen any new files after a timeout. If this triggers too early,
|
||||
// there will be a FOOC and but CSS will regenerate after we've seen more files.
|
||||
let initialScan = (() => {
|
||||
// If too short, we're more likely to trigger a FOOC and generate CSS
|
||||
// multiple times. If too long, we delay dev builds.
|
||||
let delayInMs = 50
|
||||
|
||||
let timer: ReturnType<typeof setTimeout>
|
||||
let resolve: () => void
|
||||
let resolved = false
|
||||
|
||||
return {
|
||||
tick() {
|
||||
if (resolved) return
|
||||
timer && clearTimeout(timer)
|
||||
timer = setTimeout(resolve, delayInMs)
|
||||
},
|
||||
|
||||
complete: new Promise<void>((_resolve) => {
|
||||
resolve = () => {
|
||||
resolved = true
|
||||
_resolve()
|
||||
}
|
||||
}),
|
||||
}
|
||||
})()
|
||||
|
||||
return [
|
||||
{
|
||||
// Step 1: Scan source files for candidates
|
||||
name: '@tailwindcss/vite:scan',
|
||||
enforce: 'pre',
|
||||
|
||||
configureServer(_server) {
|
||||
server = _server
|
||||
},
|
||||
|
||||
async configResolved(config) {
|
||||
minify = config.build.cssMinify !== false
|
||||
},
|
||||
|
||||
// Scan index.html for candidates
|
||||
transformIndexHtml(html) {
|
||||
initialScan.tick()
|
||||
let updated = scan(html, 'html')
|
||||
|
||||
// In dev mode, if the generated CSS contains a URL that causes the
|
||||
// browser to load a page (e.g. an URL to a missing image), triggering a
|
||||
// CSS update will cause an infinite loop. We only trigger if the
|
||||
// candidates have been updated.
|
||||
if (server && updated) {
|
||||
updateCssModules()
|
||||
}
|
||||
},
|
||||
|
||||
// Scan all other files for candidates
|
||||
transform(src, id) {
|
||||
initialScan.tick()
|
||||
if (id.includes('/.vite/')) return
|
||||
let [filename] = id.split('?', 2)
|
||||
let extension = path.extname(filename).slice(1)
|
||||
if (extension === '' || extension === 'css') return
|
||||
|
||||
scan(src, extension)
|
||||
|
||||
if (server) {
|
||||
updateCssModules()
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
// Step 2 (dev mode): Generate CSS
|
||||
name: '@tailwindcss/vite:generate:serve',
|
||||
apply: 'serve',
|
||||
async transform(src, id) {
|
||||
if (!isCssFile(id) || !src.includes('@tailwind')) return
|
||||
|
||||
cssModules.add(id)
|
||||
|
||||
// For the initial load we must wait for all source files to be scanned
|
||||
await initialScan.complete
|
||||
|
||||
return { code: generateCss(src) }
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
// Step 2 (full build): Generate CSS
|
||||
name: '@tailwindcss/vite:generate:build',
|
||||
enforce: 'post',
|
||||
apply: 'build',
|
||||
generateBundle(_options, bundle) {
|
||||
for (let id in bundle) {
|
||||
let item = bundle[id]
|
||||
if (item.type !== 'asset') continue
|
||||
if (!isCssFile(id)) continue
|
||||
let rawSource = item.source
|
||||
let source =
|
||||
rawSource instanceof Uint8Array ? new TextDecoder().decode(rawSource) : rawSource
|
||||
|
||||
if (source.includes('@tailwind')) {
|
||||
item.source = generateCss(source)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
] satisfies Plugin[]
|
||||
}
|
||||
3
packages/@tailwindcss-vite/tsconfig.json
Normal file
3
packages/@tailwindcss-vite/tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
}
|
||||
5
packages/tailwindcss/index.css
Normal file
5
packages/tailwindcss/index.css
Normal file
@ -0,0 +1,5 @@
|
||||
@layer theme, base, components, utilities;
|
||||
|
||||
@import './theme.css' layer(theme);
|
||||
@import './preflight.css' layer(base);
|
||||
@import './utilities.css' layer(utilities);
|
||||
67
packages/tailwindcss/package.json
Normal file
67
packages/tailwindcss/package.json
Normal file
@ -0,0 +1,67 @@
|
||||
{
|
||||
"name": "tailwindcss",
|
||||
"version": "0.0.0-oxide.4",
|
||||
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/tailwindlabs/tailwindcss.git",
|
||||
"bugs": "https://github.com/tailwindlabs/tailwindcss/issues",
|
||||
"homepage": "https://tailwindcss.com",
|
||||
"scripts": {
|
||||
"lint": "tsc --noEmit",
|
||||
"build": "tsup-node --env.NODE_ENV production",
|
||||
"dev": "tsup-node --env.NODE_ENV development --watch",
|
||||
"test:ui": "playwright test"
|
||||
},
|
||||
"bin": {
|
||||
"tailwindcss": "./dist/cli.js"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"style": "./index.css",
|
||||
"types": "./src/index.ts",
|
||||
"require": "./dist/lib.js",
|
||||
"import": "./src/index.ts"
|
||||
},
|
||||
"./package.json": "./package.json",
|
||||
"./index.css": "./index.css",
|
||||
"./preflight.css": "./preflight.css",
|
||||
"./theme.css": "./theme.css",
|
||||
"./utilities.css": "./utilities.css"
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/lib.d.mts",
|
||||
"style": "./index.css",
|
||||
"require": "./dist/lib.js",
|
||||
"import": "./dist/lib.mjs"
|
||||
},
|
||||
"./package.json": "./package.json",
|
||||
"./index.css": "./index.css",
|
||||
"./preflight.css": "./preflight.css",
|
||||
"./theme.css": "./theme.css",
|
||||
"./utilities.css": "./utilities.css"
|
||||
}
|
||||
},
|
||||
"style": "index.css",
|
||||
"files": [
|
||||
"dist",
|
||||
"index.css",
|
||||
"preflight.css",
|
||||
"theme.css",
|
||||
"utilities.css"
|
||||
],
|
||||
"dependencies": {
|
||||
"@parcel/watcher": "^2.4.1",
|
||||
"lightningcss": "^1.24.0",
|
||||
"mri": "^1.2.0",
|
||||
"picocolors": "^1.0.0",
|
||||
"postcss": "8.4.24",
|
||||
"postcss-import": "^16.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/oxide": "workspace:^",
|
||||
"@types/node": "^20.10.8",
|
||||
"@types/postcss-import": "^14.0.3"
|
||||
}
|
||||
}
|
||||
72
packages/tailwindcss/playwright.config.ts
Normal file
72
packages/tailwindcss/playwright.config.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://127.0.0.1:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// url: 'http://127.0.0.1:3000',
|
||||
// reuseExistingServer: !process.env.CI,
|
||||
// },
|
||||
})
|
||||
355
packages/tailwindcss/preflight.css
Normal file
355
packages/tailwindcss/preflight.css
Normal file
@ -0,0 +1,355 @@
|
||||
/*
|
||||
Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||
*/
|
||||
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove any default margins.
|
||||
*/
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use a consistent sensible line-height in all browsers.
|
||||
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
3. Use a more readable tab size.
|
||||
4. Use the user's configured `sans` font-family by default.
|
||||
5. Use the user's configured `sans` font-feature-settings by default.
|
||||
6. Use the user's configured `sans` font-variation-settings by default.
|
||||
7. Disable tap highlights on iOS.
|
||||
*/
|
||||
|
||||
html,
|
||||
:host {
|
||||
line-height: 1.5; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
tab-size: 4; /* 3 */
|
||||
font-family: var(
|
||||
--default-font-family,
|
||||
ui-sans-serif,
|
||||
system-ui,
|
||||
sans-serif,
|
||||
'Apple Color Emoji',
|
||||
'Segoe UI Emoji',
|
||||
'Segoe UI Symbol',
|
||||
'Noto Color Emoji'
|
||||
); /* 4 */
|
||||
font-feature-settings: var(--default-font-feature-settings, normal); /* 5 */
|
||||
font-variation-settings: var(--default-font-variation-settings, normal); /* 6 */
|
||||
-webkit-tap-highlight-color: transparent; /* 7 */
|
||||
}
|
||||
|
||||
/*
|
||||
Inherit line-height from `html` so users can set them as a class directly on the `html` element.
|
||||
*/
|
||||
|
||||
body {
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Add the correct height in Firefox.
|
||||
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
|
||||
3. Reset the default border style to a 1px solid border.
|
||||
*/
|
||||
|
||||
hr {
|
||||
height: 0; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
border: 0 solid; /* 3 */
|
||||
border-top-width: 1px; /* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct text decoration in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
abbr:where([title]) {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the default font size and weight for headings.
|
||||
*/
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Reset links to optimize for opt-in styling instead of opt-out.
|
||||
*/
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
-webkit-text-decoration: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font weight in Edge and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use the user's configured `mono` font-family by default.
|
||||
2. Use the user's configured `mono` font-feature-settings by default.
|
||||
3. Use the user's configured `mono` font-variation-settings by default.
|
||||
4. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-family: var(
|
||||
--default-mono-font-family,
|
||||
ui-monospace,
|
||||
SFMono-Regular,
|
||||
Menlo,
|
||||
Monaco,
|
||||
Consolas,
|
||||
'Liberation Mono',
|
||||
'Courier New',
|
||||
monospace
|
||||
); /* 4 */
|
||||
font-feature-settings: var(--default-mono-font-feature-settings, normal); /* 5 */
|
||||
font-variation-settings: var(--default-mono-font-variation-settings, normal); /* 6 */
|
||||
font-size: 1em; /* 4 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
|
||||
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||
3. Remove gaps between table borders by default.
|
||||
*/
|
||||
|
||||
table {
|
||||
text-indent: 0; /* 1 */
|
||||
border-color: inherit; /* 2 */
|
||||
border-collapse: collapse; /* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Inherit the font styles in all browsers.
|
||||
2. Reset the default inset border style to solid.
|
||||
3. Remove the default background color.
|
||||
4. Remove default padding.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea,
|
||||
::file-selector-button {
|
||||
font: inherit; /* 1 */
|
||||
font-feature-settings: inherit; /* 1 */
|
||||
font-variation-settings: inherit; /* 1 */
|
||||
color: inherit; /* 1 */
|
||||
border: 1px solid; /* 2 */
|
||||
background: transparent; /* 3 */
|
||||
padding: 0; /* 4 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style the border radius in iOS Safari.
|
||||
2. Make borders opt-in.
|
||||
*/
|
||||
button,
|
||||
input:where([type='button'], [type='reset'], [type='submit']),
|
||||
::file-selector-button {
|
||||
appearance: button; /* 1 */
|
||||
border: 0; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Use the modern Firefox focus style for all focusable elements.
|
||||
*/
|
||||
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
|
||||
*/
|
||||
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct vertical alignment in Chrome and Firefox.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/*
|
||||
Correct the cursor style of increment and decrement buttons in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-inner-spin-button,
|
||||
::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct display in Chrome and Safari.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the default border and spacing for fieldset and legend elements.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Make lists unstyled by default.
|
||||
*/
|
||||
|
||||
ol,
|
||||
ul,
|
||||
menu {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the default padding from dialog elements.
|
||||
*/
|
||||
|
||||
dialog {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent resizing textareas horizontally by default.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
|
||||
2. Set the default placeholder color to a semi-transparent version of the current text color.
|
||||
*/
|
||||
|
||||
::placeholder {
|
||||
opacity: 1; /* 1 */
|
||||
color: color-mix(in srgb, currentColor 50%, transparent); /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Make sure disabled buttons don't get the pointer cursor.
|
||||
*/
|
||||
|
||||
:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
|
||||
This can trigger a poorly considered lint error in some tools but is included by design.
|
||||
*/
|
||||
|
||||
img,
|
||||
svg,
|
||||
video,
|
||||
canvas,
|
||||
audio,
|
||||
iframe,
|
||||
embed,
|
||||
object {
|
||||
display: block; /* 1 */
|
||||
vertical-align: middle; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
*/
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
Make elements with the HTML hidden attribute stay hidden by default.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
523
packages/tailwindcss/src/__snapshots__/index.test.ts.snap
Normal file
523
packages/tailwindcss/src/__snapshots__/index.test.ts.snap
Normal file
@ -0,0 +1,523 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`compiling CSS > \`@tailwind utilities\` is replaced by utilities using the default theme 1`] = `
|
||||
":root {
|
||||
--default-transition-duration: .15s;
|
||||
--default-transition-timing-function: var(--transition-timing-function-in-out);
|
||||
--default-font-family: var(--font-family-sans);
|
||||
--default-font-feature-settings: var(--font-family-sans--font-feature-settings);
|
||||
--default-font-variation-settings: var(--font-family-sans--font-variation-settings);
|
||||
--default-mono-font-family: var(--font-family-mono);
|
||||
--default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings);
|
||||
--default-mono-font-variation-settings: var(--font-family-mono--font-variation-settings);
|
||||
--breakpoint-sm: 640px;
|
||||
--breakpoint-md: 768px;
|
||||
--breakpoint-lg: 1024px;
|
||||
--breakpoint-xl: 1280px;
|
||||
--breakpoint-2xl: 1536px;
|
||||
--color-black: #000;
|
||||
--color-white: #fff;
|
||||
--color-slate-50: #f8fafc;
|
||||
--color-slate-100: #f1f5f9;
|
||||
--color-slate-200: #e2e8f0;
|
||||
--color-slate-300: #cbd5e1;
|
||||
--color-slate-400: #94a3b8;
|
||||
--color-slate-500: #64748b;
|
||||
--color-slate-600: #475569;
|
||||
--color-slate-700: #334155;
|
||||
--color-slate-800: #1e293b;
|
||||
--color-slate-900: #0f172a;
|
||||
--color-slate-950: #020617;
|
||||
--color-gray-50: #f9fafb;
|
||||
--color-gray-100: #f3f4f6;
|
||||
--color-gray-200: #e5e7eb;
|
||||
--color-gray-300: #d1d5db;
|
||||
--color-gray-400: #9ca3af;
|
||||
--color-gray-500: #6b7280;
|
||||
--color-gray-600: #4b5563;
|
||||
--color-gray-700: #374151;
|
||||
--color-gray-800: #1f2937;
|
||||
--color-gray-900: #111827;
|
||||
--color-gray-950: #030712;
|
||||
--color-zinc-50: #fafafa;
|
||||
--color-zinc-100: #f4f4f5;
|
||||
--color-zinc-200: #e4e4e7;
|
||||
--color-zinc-300: #d4d4d8;
|
||||
--color-zinc-400: #a1a1aa;
|
||||
--color-zinc-500: #71717a;
|
||||
--color-zinc-600: #52525b;
|
||||
--color-zinc-700: #3f3f46;
|
||||
--color-zinc-800: #27272a;
|
||||
--color-zinc-900: #18181b;
|
||||
--color-zinc-950: #09090b;
|
||||
--color-neutral-50: #fafafa;
|
||||
--color-neutral-100: #f5f5f5;
|
||||
--color-neutral-200: #e5e5e5;
|
||||
--color-neutral-300: #d4d4d4;
|
||||
--color-neutral-400: #a3a3a3;
|
||||
--color-neutral-500: #737373;
|
||||
--color-neutral-600: #525252;
|
||||
--color-neutral-700: #404040;
|
||||
--color-neutral-800: #262626;
|
||||
--color-neutral-900: #171717;
|
||||
--color-neutral-950: #0a0a0a;
|
||||
--color-stone-50: #fafaf9;
|
||||
--color-stone-100: #f5f5f4;
|
||||
--color-stone-200: #e7e5e4;
|
||||
--color-stone-300: #d6d3d1;
|
||||
--color-stone-400: #a8a29e;
|
||||
--color-stone-500: #78716c;
|
||||
--color-stone-600: #57534e;
|
||||
--color-stone-700: #44403c;
|
||||
--color-stone-800: #292524;
|
||||
--color-stone-900: #1c1917;
|
||||
--color-stone-950: #0c0a09;
|
||||
--color-red-50: #fef2f2;
|
||||
--color-red-100: #fee2e2;
|
||||
--color-red-200: #fecaca;
|
||||
--color-red-300: #fca5a5;
|
||||
--color-red-400: #f87171;
|
||||
--color-red-500: #ef4444;
|
||||
--color-red-600: #dc2626;
|
||||
--color-red-700: #b91c1c;
|
||||
--color-red-800: #991b1b;
|
||||
--color-red-900: #7f1d1d;
|
||||
--color-red-950: #450a0a;
|
||||
--color-orange-50: #fff7ed;
|
||||
--color-orange-100: #ffedd5;
|
||||
--color-orange-200: #fed7aa;
|
||||
--color-orange-300: #fdba74;
|
||||
--color-orange-400: #fb923c;
|
||||
--color-orange-500: #f97316;
|
||||
--color-orange-600: #ea580c;
|
||||
--color-orange-700: #c2410c;
|
||||
--color-orange-800: #9a3412;
|
||||
--color-orange-900: #7c2d12;
|
||||
--color-orange-950: #431407;
|
||||
--color-amber-50: #fffbeb;
|
||||
--color-amber-100: #fef3c7;
|
||||
--color-amber-200: #fde68a;
|
||||
--color-amber-300: #fcd34d;
|
||||
--color-amber-400: #fbbf24;
|
||||
--color-amber-500: #f59e0b;
|
||||
--color-amber-600: #d97706;
|
||||
--color-amber-700: #b45309;
|
||||
--color-amber-800: #92400e;
|
||||
--color-amber-900: #78350f;
|
||||
--color-amber-950: #451a03;
|
||||
--color-yellow-50: #fefce8;
|
||||
--color-yellow-100: #fef9c3;
|
||||
--color-yellow-200: #fef08a;
|
||||
--color-yellow-300: #fde047;
|
||||
--color-yellow-400: #facc15;
|
||||
--color-yellow-500: #eab308;
|
||||
--color-yellow-600: #ca8a04;
|
||||
--color-yellow-700: #a16207;
|
||||
--color-yellow-800: #854d0e;
|
||||
--color-yellow-900: #713f12;
|
||||
--color-yellow-950: #422006;
|
||||
--color-lime-50: #f7fee7;
|
||||
--color-lime-100: #ecfccb;
|
||||
--color-lime-200: #d9f99d;
|
||||
--color-lime-300: #bef264;
|
||||
--color-lime-400: #a3e635;
|
||||
--color-lime-500: #84cc16;
|
||||
--color-lime-600: #65a30d;
|
||||
--color-lime-700: #4d7c0f;
|
||||
--color-lime-800: #3f6212;
|
||||
--color-lime-900: #365314;
|
||||
--color-lime-950: #1a2e05;
|
||||
--color-green-50: #f0fdf4;
|
||||
--color-green-100: #dcfce7;
|
||||
--color-green-200: #bbf7d0;
|
||||
--color-green-300: #86efac;
|
||||
--color-green-400: #4ade80;
|
||||
--color-green-500: #22c55e;
|
||||
--color-green-600: #16a34a;
|
||||
--color-green-700: #15803d;
|
||||
--color-green-800: #166534;
|
||||
--color-green-900: #14532d;
|
||||
--color-green-950: #052e16;
|
||||
--color-emerald-50: #ecfdf5;
|
||||
--color-emerald-100: #d1fae5;
|
||||
--color-emerald-200: #a7f3d0;
|
||||
--color-emerald-300: #6ee7b7;
|
||||
--color-emerald-400: #34d399;
|
||||
--color-emerald-500: #10b981;
|
||||
--color-emerald-600: #059669;
|
||||
--color-emerald-700: #047857;
|
||||
--color-emerald-800: #065f46;
|
||||
--color-emerald-900: #064e3b;
|
||||
--color-emerald-950: #022c22;
|
||||
--color-teal-50: #f0fdfa;
|
||||
--color-teal-100: #ccfbf1;
|
||||
--color-teal-200: #99f6e4;
|
||||
--color-teal-300: #5eead4;
|
||||
--color-teal-400: #2dd4bf;
|
||||
--color-teal-500: #14b8a6;
|
||||
--color-teal-600: #0d9488;
|
||||
--color-teal-700: #0f766e;
|
||||
--color-teal-800: #115e59;
|
||||
--color-teal-900: #134e4a;
|
||||
--color-teal-950: #042f2e;
|
||||
--color-cyan-50: #ecfeff;
|
||||
--color-cyan-100: #cffafe;
|
||||
--color-cyan-200: #a5f3fc;
|
||||
--color-cyan-300: #67e8f9;
|
||||
--color-cyan-400: #22d3ee;
|
||||
--color-cyan-500: #06b6d4;
|
||||
--color-cyan-600: #0891b2;
|
||||
--color-cyan-700: #0e7490;
|
||||
--color-cyan-800: #155e75;
|
||||
--color-cyan-900: #164e63;
|
||||
--color-cyan-950: #083344;
|
||||
--color-sky-50: #f0f9ff;
|
||||
--color-sky-100: #e0f2fe;
|
||||
--color-sky-200: #bae6fd;
|
||||
--color-sky-300: #7dd3fc;
|
||||
--color-sky-400: #38bdf8;
|
||||
--color-sky-500: #0ea5e9;
|
||||
--color-sky-600: #0284c7;
|
||||
--color-sky-700: #0369a1;
|
||||
--color-sky-800: #075985;
|
||||
--color-sky-900: #0c4a6e;
|
||||
--color-sky-950: #082f49;
|
||||
--color-blue-50: #eff6ff;
|
||||
--color-blue-100: #dbeafe;
|
||||
--color-blue-200: #bfdbfe;
|
||||
--color-blue-300: #93c5fd;
|
||||
--color-blue-400: #60a5fa;
|
||||
--color-blue-500: #3b82f6;
|
||||
--color-blue-600: #2563eb;
|
||||
--color-blue-700: #1d4ed8;
|
||||
--color-blue-800: #1e40af;
|
||||
--color-blue-900: #1e3a8a;
|
||||
--color-blue-950: #172554;
|
||||
--color-indigo-50: #eef2ff;
|
||||
--color-indigo-100: #e0e7ff;
|
||||
--color-indigo-200: #c7d2fe;
|
||||
--color-indigo-300: #a5b4fc;
|
||||
--color-indigo-400: #818cf8;
|
||||
--color-indigo-500: #6366f1;
|
||||
--color-indigo-600: #4f46e5;
|
||||
--color-indigo-700: #4338ca;
|
||||
--color-indigo-800: #3730a3;
|
||||
--color-indigo-900: #312e81;
|
||||
--color-indigo-950: #1e1b4b;
|
||||
--color-violet-50: #f5f3ff;
|
||||
--color-violet-100: #ede9fe;
|
||||
--color-violet-200: #ddd6fe;
|
||||
--color-violet-300: #c4b5fd;
|
||||
--color-violet-400: #a78bfa;
|
||||
--color-violet-500: #8b5cf6;
|
||||
--color-violet-600: #7c3aed;
|
||||
--color-violet-700: #6d28d9;
|
||||
--color-violet-800: #5b21b6;
|
||||
--color-violet-900: #4c1d95;
|
||||
--color-violet-950: #2e1065;
|
||||
--color-purple-50: #faf5ff;
|
||||
--color-purple-100: #f3e8ff;
|
||||
--color-purple-200: #e9d5ff;
|
||||
--color-purple-300: #d8b4fe;
|
||||
--color-purple-400: #c084fc;
|
||||
--color-purple-500: #a855f7;
|
||||
--color-purple-600: #9333ea;
|
||||
--color-purple-700: #7e22ce;
|
||||
--color-purple-800: #6b21a8;
|
||||
--color-purple-900: #581c87;
|
||||
--color-purple-950: #3b0764;
|
||||
--color-fuchsia-50: #fdf4ff;
|
||||
--color-fuchsia-100: #fae8ff;
|
||||
--color-fuchsia-200: #f5d0fe;
|
||||
--color-fuchsia-300: #f0abfc;
|
||||
--color-fuchsia-400: #e879f9;
|
||||
--color-fuchsia-500: #d946ef;
|
||||
--color-fuchsia-600: #c026d3;
|
||||
--color-fuchsia-700: #a21caf;
|
||||
--color-fuchsia-800: #86198f;
|
||||
--color-fuchsia-900: #701a75;
|
||||
--color-fuchsia-950: #4a044e;
|
||||
--color-pink-50: #fdf2f8;
|
||||
--color-pink-100: #fce7f3;
|
||||
--color-pink-200: #fbcfe8;
|
||||
--color-pink-300: #f9a8d4;
|
||||
--color-pink-400: #f472b6;
|
||||
--color-pink-500: #ec4899;
|
||||
--color-pink-600: #db2777;
|
||||
--color-pink-700: #be185d;
|
||||
--color-pink-800: #9d174d;
|
||||
--color-pink-900: #831843;
|
||||
--color-pink-950: #500724;
|
||||
--color-rose-50: #fff1f2;
|
||||
--color-rose-100: #ffe4e6;
|
||||
--color-rose-200: #fecdd3;
|
||||
--color-rose-300: #fda4af;
|
||||
--color-rose-400: #fb7185;
|
||||
--color-rose-500: #f43f5e;
|
||||
--color-rose-600: #e11d48;
|
||||
--color-rose-700: #be123c;
|
||||
--color-rose-800: #9f1239;
|
||||
--color-rose-900: #881337;
|
||||
--color-rose-950: #4c0519;
|
||||
--animate-spin: spin 1s linear infinite;
|
||||
--animate-ping: ping 1s cubic-bezier(0, 0, .2, 1) infinite;
|
||||
--animate-pulse: pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;
|
||||
--animate-bounce: bounce 1s infinite;
|
||||
--blur: 8px;
|
||||
--blur-sm: 4px;
|
||||
--blur-md: 12px;
|
||||
--blur-lg: 16px;
|
||||
--blur-xl: 24px;
|
||||
--blur-2xl: 40px;
|
||||
--blur-3xl: 64px;
|
||||
--radius-none: 0px;
|
||||
--radius-full: 9999px;
|
||||
--radius-sm: .125rem;
|
||||
--radius: .25rem;
|
||||
--radius-md: .375rem;
|
||||
--radius-lg: .5rem;
|
||||
--radius-xl: .75rem;
|
||||
--radius-2xl: 1rem;
|
||||
--radius-3xl: 1.5rem;
|
||||
--shadow: 0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;
|
||||
--shadow-xs: 0 1px #0000000d;
|
||||
--shadow-sm: 0 1px 2px 0 #0000000d;
|
||||
--shadow-md: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
|
||||
--shadow-lg: 0 10px 15px -3px #0000001a, 0 4px 6px -4px #0000001a;
|
||||
--shadow-xl: 0 20px 25px -5px #0000001a, 0 8px 10px -6px #0000001a;
|
||||
--shadow-2xl: 0 25px 50px -12px #00000040;
|
||||
--shadow-inner: inset 0 2px 4px 0 #0000000d;
|
||||
--inset-shadow-xs: inset 0 1px #0000000d;
|
||||
--inset-shadow-sm: inset 0 1px 1px #0000000d;
|
||||
--inset-shadow: inset 0 2px 4px #0000000d;
|
||||
--drop-shadow: 0 1px 2px #0000001a, 0 1px 1px #0000000f;
|
||||
--drop-shadow-sm: 0 1px 1px #0000000d;
|
||||
--drop-shadow-md: 0 4px 3px #00000012, 0 2px 2px #0000000f;
|
||||
--drop-shadow-lg: 0 10px 8px #0000000a, 0 4px 3px #0000001a;
|
||||
--drop-shadow-xl: 0 20px 13px #00000008, 0 8px 5px #00000014;
|
||||
--drop-shadow-2xl: 0 25px 25px #00000026;
|
||||
--spacing-px: 1px;
|
||||
--spacing-0: 0px;
|
||||
--spacing-0_5: .125rem;
|
||||
--spacing-1: .25rem;
|
||||
--spacing-1_5: .375rem;
|
||||
--spacing-2: .5rem;
|
||||
--spacing-2_5: .625rem;
|
||||
--spacing-3: .75rem;
|
||||
--spacing-3_5: .875rem;
|
||||
--spacing-4: 1rem;
|
||||
--spacing-5: 1.25rem;
|
||||
--spacing-6: 1.5rem;
|
||||
--spacing-7: 1.75rem;
|
||||
--spacing-8: 2rem;
|
||||
--spacing-9: 2.25rem;
|
||||
--spacing-10: 2.5rem;
|
||||
--spacing-11: 2.75rem;
|
||||
--spacing-12: 3rem;
|
||||
--spacing-14: 3.5rem;
|
||||
--spacing-16: 4rem;
|
||||
--spacing-20: 5rem;
|
||||
--spacing-24: 6rem;
|
||||
--spacing-28: 7rem;
|
||||
--spacing-32: 8rem;
|
||||
--spacing-36: 9rem;
|
||||
--spacing-40: 10rem;
|
||||
--spacing-44: 11rem;
|
||||
--spacing-48: 12rem;
|
||||
--spacing-52: 13rem;
|
||||
--spacing-56: 14rem;
|
||||
--spacing-60: 15rem;
|
||||
--spacing-64: 16rem;
|
||||
--spacing-72: 18rem;
|
||||
--spacing-80: 20rem;
|
||||
--spacing-96: 24rem;
|
||||
--width-3xs: 16rem;
|
||||
--width-2xs: 18rem;
|
||||
--width-xs: 20rem;
|
||||
--width-sm: 24rem;
|
||||
--width-md: 28rem;
|
||||
--width-lg: 32rem;
|
||||
--width-xl: 36rem;
|
||||
--width-2xl: 42rem;
|
||||
--width-3xl: 48rem;
|
||||
--width-4xl: 56rem;
|
||||
--width-5xl: 64rem;
|
||||
--width-6xl: 72rem;
|
||||
--width-7xl: 80rem;
|
||||
--width-prose: 65ch;
|
||||
--font-family-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--font-family-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-family-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--font-size-xs: .75rem;
|
||||
--font-size-xs--line-height: 1rem;
|
||||
--font-size-sm: .875rem;
|
||||
--font-size-sm--line-height: 1.25rem;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-base--line-height: 1.5rem;
|
||||
--font-size-lg: 1.125rem;
|
||||
--font-size-lg--line-height: 1.75rem;
|
||||
--font-size-xl: 1.25rem;
|
||||
--font-size-xl--line-height: 1.75rem;
|
||||
--font-size-2xl: 1.5rem;
|
||||
--font-size-2xl--line-height: 2rem;
|
||||
--font-size-3xl: 1.875rem;
|
||||
--font-size-3xl--line-height: 2.25rem;
|
||||
--font-size-4xl: 2.25rem;
|
||||
--font-size-4xl--line-height: 2.5rem;
|
||||
--font-size-5xl: 3rem;
|
||||
--font-size-5xl--line-height: 1;
|
||||
--font-size-6xl: 3.75rem;
|
||||
--font-size-6xl--line-height: 1;
|
||||
--font-size-7xl: 4.5rem;
|
||||
--font-size-7xl--line-height: 1;
|
||||
--font-size-8xl: 6rem;
|
||||
--font-size-8xl--line-height: 1;
|
||||
--font-size-9xl: 8rem;
|
||||
--font-size-9xl--line-height: 1;
|
||||
--letter-spacing-tighter: -.05em;
|
||||
--letter-spacing-tight: -.025em;
|
||||
--letter-spacing-normal: 0em;
|
||||
--letter-spacing-wide: .025em;
|
||||
--letter-spacing-wider: .05em;
|
||||
--letter-spacing-widest: .1em;
|
||||
--line-height-none: 1;
|
||||
--line-height-tight: 1.25;
|
||||
--line-height-snug: 1.375;
|
||||
--line-height-normal: 1.5;
|
||||
--line-height-relaxed: 1.625;
|
||||
--line-height-loose: 2;
|
||||
--line-height-3: .75rem;
|
||||
--line-height-4: 1rem;
|
||||
--line-height-5: 1.25rem;
|
||||
--line-height-6: 1.5rem;
|
||||
--line-height-7: 1.75rem;
|
||||
--line-height-8: 2rem;
|
||||
--line-height-9: 2.25rem;
|
||||
--line-height-10: 2.5rem;
|
||||
--transition-timing-function: cubic-bezier(.4, 0, .2, 1);
|
||||
--transition-timing-function-linear: linear;
|
||||
--transition-timing-function-in: cubic-bezier(.4, 0, 1, 1);
|
||||
--transition-timing-function-out: cubic-bezier(0, 0, .2, 1);
|
||||
--transition-timing-function-in-out: cubic-bezier(.4, 0, .2, 1);
|
||||
}
|
||||
|
||||
.w-4 {
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.bg-red-500 {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
|
||||
.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-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
|
||||
@media (width >= 640px) {
|
||||
.sm\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ping {
|
||||
75%, 100% {
|
||||
opacity: 0;
|
||||
transform: scale(2);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
50% {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% {
|
||||
animation-timing-function: cubic-bezier(.8, 0, 1, 1);
|
||||
transform: translateY(-25%);
|
||||
}
|
||||
|
||||
50% {
|
||||
animation-timing-function: cubic-bezier(0, 0, .2, 1);
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@property --tw-shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0 0 #0000;
|
||||
}
|
||||
|
||||
@property --tw-shadow-colored {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0 0 #0000;
|
||||
}
|
||||
|
||||
@property --tw-inset-shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0 0 #0000;
|
||||
}
|
||||
|
||||
@property --tw-inset-shadow-colored {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0 0 #0000;
|
||||
}
|
||||
|
||||
@property --tw-ring-color {
|
||||
syntax: "*";
|
||||
inherits: false
|
||||
}
|
||||
|
||||
@property --tw-ring-shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0 0 #0000;
|
||||
}
|
||||
|
||||
@property --tw-inset-ring-color {
|
||||
syntax: "*";
|
||||
inherits: false
|
||||
}
|
||||
|
||||
@property --tw-inset-ring-shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0 0 #0000;
|
||||
}
|
||||
|
||||
@property --tw-ring-inset {
|
||||
syntax: "*";
|
||||
inherits: false
|
||||
}
|
||||
|
||||
@property --tw-ring-offset-width {
|
||||
syntax: "<length>";
|
||||
inherits: false;
|
||||
initial-value: 0;
|
||||
}
|
||||
|
||||
@property --tw-ring-offset-color {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: #fff;
|
||||
}
|
||||
|
||||
@property --tw-ring-offset-shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: 0 0 #0000;
|
||||
}"
|
||||
`;
|
||||
2234
packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap
Normal file
2234
packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap
Normal file
File diff suppressed because it is too large
Load Diff
1024
packages/tailwindcss/src/__snapshots__/utilities.test.ts.snap
Normal file
1024
packages/tailwindcss/src/__snapshots__/utilities.test.ts.snap
Normal file
File diff suppressed because it is too large
Load Diff
122
packages/tailwindcss/src/ast.ts
Normal file
122
packages/tailwindcss/src/ast.ts
Normal file
@ -0,0 +1,122 @@
|
||||
export type Rule = {
|
||||
kind: 'rule'
|
||||
selector: string
|
||||
nodes: AstNode[]
|
||||
}
|
||||
|
||||
export type Declaration = {
|
||||
kind: 'declaration'
|
||||
property: string
|
||||
value: string
|
||||
important: boolean
|
||||
}
|
||||
|
||||
export type Comment = {
|
||||
kind: 'comment'
|
||||
value: string
|
||||
}
|
||||
|
||||
export type AstNode = Rule | Declaration | Comment
|
||||
|
||||
export function rule(selector: string, nodes: AstNode[]): Rule {
|
||||
return {
|
||||
kind: 'rule',
|
||||
selector,
|
||||
nodes,
|
||||
}
|
||||
}
|
||||
|
||||
export function decl(property: string, value: string): Declaration {
|
||||
return {
|
||||
kind: 'declaration',
|
||||
property,
|
||||
value,
|
||||
important: false,
|
||||
}
|
||||
}
|
||||
|
||||
export function comment(value: string): Comment {
|
||||
return {
|
||||
kind: 'comment',
|
||||
value: value,
|
||||
}
|
||||
}
|
||||
|
||||
export function walk(
|
||||
ast: AstNode[],
|
||||
visit: (
|
||||
node: AstNode,
|
||||
utils: {
|
||||
replaceWith(newNode: AstNode | AstNode[]): void
|
||||
},
|
||||
) => void | false,
|
||||
) {
|
||||
for (let i = 0; i < ast.length; i++) {
|
||||
let node = ast[i]
|
||||
let shouldContinue = visit(node, {
|
||||
replaceWith(newNode) {
|
||||
ast.splice(i, 1, ...(Array.isArray(newNode) ? newNode : [newNode]))
|
||||
// We want to visit the newly replaced node(s), which start at the current
|
||||
// index (i). By decrementing the index here, the next loop will process
|
||||
// this position (containing the replaced node) again.
|
||||
i--
|
||||
},
|
||||
})
|
||||
|
||||
if (shouldContinue === false) return
|
||||
|
||||
if (node.kind === 'rule') {
|
||||
walk(node.nodes, visit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function toCss(ast: AstNode[]) {
|
||||
let atRoots: string[] = []
|
||||
return ast
|
||||
.map(function stringify(node: AstNode): string {
|
||||
let css = ''
|
||||
|
||||
// Rule
|
||||
if (node.kind === 'rule') {
|
||||
// Pull out `@at-root` rules to append later
|
||||
if (node.selector === '@at-root') {
|
||||
for (let child of node.nodes) {
|
||||
atRoots.push(stringify(child))
|
||||
}
|
||||
return css
|
||||
}
|
||||
|
||||
// Print at-rules without nodes with a `;` instead of an empty block.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// ```css
|
||||
// @layer base, components, utilities;
|
||||
// ```
|
||||
if (node.selector[0] === '@' && node.nodes.length === 0) {
|
||||
return `${node.selector};`
|
||||
}
|
||||
|
||||
css += `${node.selector}{`
|
||||
for (let child of node.nodes) {
|
||||
css += stringify(child)
|
||||
}
|
||||
css += '}'
|
||||
}
|
||||
|
||||
// Comment
|
||||
else if (node.kind === 'comment') {
|
||||
css += `/*${node.value}*/\n`
|
||||
}
|
||||
|
||||
// Declaration
|
||||
else if (node.property !== '--tw-sort' && node.value !== undefined && node.value !== null) {
|
||||
css += `${node.property}:${node.value}${node.important ? '!important' : ''};`
|
||||
}
|
||||
|
||||
return css
|
||||
})
|
||||
.concat(atRoots)
|
||||
.join('\n')
|
||||
}
|
||||
24
packages/tailwindcss/src/candidate.bench.ts
Normal file
24
packages/tailwindcss/src/candidate.bench.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { scanDir } from '@tailwindcss/oxide'
|
||||
import { bench } from 'vitest'
|
||||
import { parseCandidate, parseVariant } from './candidate'
|
||||
import { buildDesignSystem } from './design-system'
|
||||
import { Theme } from './theme'
|
||||
import { DefaultMap } from './utils/default-map'
|
||||
|
||||
// FOLDER=path/to/folder vitest bench
|
||||
const root = process.env.FOLDER || process.cwd()
|
||||
|
||||
// Auto content detection
|
||||
const result = scanDir({ base: root, globs: true })
|
||||
|
||||
const designSystem = buildDesignSystem(new Theme())
|
||||
|
||||
bench('parseCandidate', () => {
|
||||
for (let candidate of result.candidates) {
|
||||
parseCandidate(
|
||||
candidate,
|
||||
designSystem.utilities,
|
||||
new DefaultMap((variant, map) => parseVariant(variant, designSystem.variants, map)),
|
||||
)
|
||||
}
|
||||
})
|
||||
1052
packages/tailwindcss/src/candidate.test.ts
Normal file
1052
packages/tailwindcss/src/candidate.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
634
packages/tailwindcss/src/candidate.ts
Normal file
634
packages/tailwindcss/src/candidate.ts
Normal file
@ -0,0 +1,634 @@
|
||||
import { decodeArbitraryValue } from './utils/decode-arbitrary-value'
|
||||
import { segment } from './utils/segment'
|
||||
|
||||
type ArbitraryUtilityValue = {
|
||||
kind: 'arbitrary'
|
||||
|
||||
/**
|
||||
* bg-[color:--my-color]
|
||||
* ^^^^^
|
||||
*/
|
||||
dataType: string | null
|
||||
|
||||
/**
|
||||
* bg-[#0088cc]
|
||||
* ^^^^^^^
|
||||
* bg-[--my_variable]
|
||||
* var(^^^^^^^^^^^^^)
|
||||
*/
|
||||
value: string
|
||||
|
||||
/**
|
||||
* bg-[--my_variable]
|
||||
* ^^^^^^^^^^^^^
|
||||
*/
|
||||
dashedIdent: string | null
|
||||
}
|
||||
|
||||
export type NamedUtilityValue = {
|
||||
kind: 'named'
|
||||
|
||||
/**
|
||||
* bg-red-500
|
||||
* ^^^^^^^
|
||||
*
|
||||
* w-1/2
|
||||
* ^
|
||||
*/
|
||||
value: string
|
||||
|
||||
/**
|
||||
* w-1/2
|
||||
* ^^^
|
||||
*/
|
||||
fraction: string | null
|
||||
}
|
||||
|
||||
type ArbitraryModifier = {
|
||||
kind: 'arbitrary'
|
||||
|
||||
/**
|
||||
* bg-red-500/[50%]
|
||||
* ^^^
|
||||
*/
|
||||
value: string
|
||||
|
||||
/**
|
||||
* bg-red-500/[--my_variable]
|
||||
* ^^^^^^^^^^^^^
|
||||
*/
|
||||
dashedIdent: string | null
|
||||
}
|
||||
|
||||
type NamedModifier = {
|
||||
kind: 'named'
|
||||
|
||||
/**
|
||||
* bg-red-500/50
|
||||
* ^^
|
||||
*/
|
||||
value: string
|
||||
}
|
||||
|
||||
export type CandidateModifier = ArbitraryModifier | NamedModifier
|
||||
|
||||
type ArbitraryVariantValue = {
|
||||
kind: 'arbitrary'
|
||||
value: string
|
||||
}
|
||||
|
||||
type NamedVariantValue = {
|
||||
kind: 'named'
|
||||
value: string
|
||||
}
|
||||
|
||||
export type Variant =
|
||||
/**
|
||||
* Arbitrary variants are variants that take a selector and generate a variant
|
||||
* on the fly.
|
||||
*
|
||||
* E.g.: `[&_p]`
|
||||
*/
|
||||
| {
|
||||
kind: 'arbitrary'
|
||||
selector: string
|
||||
|
||||
// If true, it can be applied as a child of a compound variant
|
||||
compounds: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Static variants are variants that don't take any arguments.
|
||||
*
|
||||
* E.g.: `hover`
|
||||
*/
|
||||
| {
|
||||
kind: 'static'
|
||||
root: string
|
||||
|
||||
// If true, it can be applied as a child of a compound variant
|
||||
compounds: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Functional variants are variants that can take an argument. The argument is
|
||||
* either a named variant value or an arbitrary variant value.
|
||||
*
|
||||
* E.g.:
|
||||
*
|
||||
* - `aria-disabled`
|
||||
* - `aria-[disabled]`
|
||||
* - `@container-size` -> @container, with named value `size`
|
||||
* - `@container-[inline-size]` -> @container, with arbitrary variant value `inline-size`
|
||||
* - `@container` -> @container, with no value
|
||||
*/
|
||||
| {
|
||||
kind: 'functional'
|
||||
root: string
|
||||
value: ArbitraryVariantValue | NamedVariantValue | null
|
||||
modifier: ArbitraryModifier | NamedModifier | null
|
||||
|
||||
// If true, it can be applied as a child of a compound variant
|
||||
compounds: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Compound variants are variants that take another variant as an argument.
|
||||
*
|
||||
* E.g.:
|
||||
*
|
||||
* - `has-[&_p]`
|
||||
* - `group-*`
|
||||
* - `peer-*`
|
||||
*/
|
||||
| {
|
||||
kind: 'compound'
|
||||
root: string
|
||||
modifier: ArbitraryModifier | NamedModifier | null
|
||||
variant: Variant
|
||||
|
||||
// If true, it can be applied as a child of a compound variant
|
||||
compounds: boolean
|
||||
}
|
||||
|
||||
export type Candidate =
|
||||
/**
|
||||
* Arbitrary candidates are candidates that register utilities on the fly with
|
||||
* a property and a value.
|
||||
*
|
||||
* E.g.:
|
||||
*
|
||||
* - `[color:red]`
|
||||
* - `[color:red]/50`
|
||||
* - `[color:red]/50!`
|
||||
*/
|
||||
| {
|
||||
kind: 'arbitrary'
|
||||
property: string
|
||||
value: string
|
||||
modifier: ArbitraryModifier | NamedModifier | null
|
||||
variants: Variant[]
|
||||
important: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Static candidates are candidates that don't take any arguments.
|
||||
*
|
||||
* E.g.:
|
||||
*
|
||||
* - `underline`
|
||||
* - `flex`
|
||||
*/
|
||||
| {
|
||||
kind: 'static'
|
||||
root: string
|
||||
variants: Variant[]
|
||||
negative: boolean
|
||||
important: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Functional candidates are candidates that can take an argument.
|
||||
*
|
||||
* E.g.:
|
||||
*
|
||||
* - `bg-red-500`
|
||||
* - `bg-[#0088cc]`
|
||||
* - `w-1/2`
|
||||
*/
|
||||
| {
|
||||
kind: 'functional'
|
||||
root: string
|
||||
value: ArbitraryUtilityValue | NamedUtilityValue | null
|
||||
modifier: ArbitraryModifier | NamedModifier | null
|
||||
variants: Variant[]
|
||||
negative: boolean
|
||||
important: boolean
|
||||
}
|
||||
|
||||
export function parseCandidate(
|
||||
input: string,
|
||||
utilities: {
|
||||
has: (value: string) => boolean
|
||||
kind: (root: string) => Omit<Candidate['kind'], 'arbitrary'>
|
||||
},
|
||||
parsedVariants: { get: (value: string) => Variant | null },
|
||||
): Candidate | null {
|
||||
// hover:focus:underline
|
||||
// ^^^^^ ^^^^^^ -> Variants
|
||||
// ^^^^^^^^^ -> Base
|
||||
let rawVariants = segment(input, ':')
|
||||
|
||||
// Safety: At this point it is safe to use TypeScript's non-null assertion
|
||||
// operator because even if the `input` was an empty string, splitting an
|
||||
// empty string by `:` will always result in an array with at least one
|
||||
// element.
|
||||
let base = rawVariants.pop()!
|
||||
|
||||
let parsedCandidateVariants: Variant[] = []
|
||||
|
||||
for (let variant of rawVariants) {
|
||||
let parsedVariant = parsedVariants.get(variant)
|
||||
if (parsedVariant === null) return null
|
||||
|
||||
// Variants are applied left-to-right meaning that any representing pseudo-
|
||||
// elements must come first. This is because they cannot have anything
|
||||
// after them in a selector. The problem with this is that it's common for
|
||||
// users to write them in the wrong order, for example:
|
||||
//
|
||||
// `dark:before:underline` (wrong)
|
||||
// `before:dark:underline` (right)
|
||||
//
|
||||
// Add pseudo-element variants to the front, making both examples above
|
||||
// function identically which allows users to not care about the order.
|
||||
switch (variant) {
|
||||
case 'after':
|
||||
case 'backdrop':
|
||||
case 'before':
|
||||
case 'first-letter':
|
||||
case 'first-line':
|
||||
case 'marker':
|
||||
case 'placeholder':
|
||||
case 'selection':
|
||||
parsedCandidateVariants.unshift(parsedVariant)
|
||||
break
|
||||
default:
|
||||
parsedCandidateVariants.push(parsedVariant)
|
||||
}
|
||||
}
|
||||
|
||||
let state = {
|
||||
important: false,
|
||||
negative: false,
|
||||
}
|
||||
|
||||
// Candidates that end with an exclamation mark are the important version with
|
||||
// higher specificity of the non-important candidate, e.g. `mx-4!`.
|
||||
if (base[base.length - 1] === '!') {
|
||||
state.important = true
|
||||
base = base.slice(0, -1)
|
||||
}
|
||||
|
||||
// Arbitrary properties
|
||||
if (base[0] === '[') {
|
||||
let [baseWithoutModifier, modifierSegment = null] = segment(base, '/')
|
||||
if (baseWithoutModifier[baseWithoutModifier.length - 1] !== ']') return null
|
||||
|
||||
// The property part of the arbitrary property can only start with a-z
|
||||
// lowercase or a dash `-` in case of vendor prefixes such as `-webkit-`
|
||||
// or `-moz-`.
|
||||
//
|
||||
// Otherwise, it is an invalid candidate, and skip continue parsing.
|
||||
let charCode = baseWithoutModifier.charCodeAt(1)
|
||||
if (charCode !== 45 && !(charCode >= 97 && charCode <= 122)) return null
|
||||
|
||||
baseWithoutModifier = baseWithoutModifier.slice(1, -1)
|
||||
|
||||
// Arbitrary properties consist of a property and a value separated by a
|
||||
// `:`. If the `:` cannot be found, then it is an invalid candidate, and we
|
||||
// can skip continue parsing.
|
||||
//
|
||||
// Since the property and the value should be separated by a `:`, we can
|
||||
// also verify that the colon is not the first or last character in the
|
||||
// candidate, because that would make it invalid as well.
|
||||
let idx = baseWithoutModifier.indexOf(':')
|
||||
if (idx === -1 || idx === 0 || idx === baseWithoutModifier.length - 1) return null
|
||||
|
||||
let property = baseWithoutModifier.slice(0, idx)
|
||||
let value = decodeArbitraryValue(baseWithoutModifier.slice(idx + 1))
|
||||
|
||||
return {
|
||||
kind: 'arbitrary',
|
||||
property,
|
||||
value,
|
||||
modifier: modifierSegment === null ? null : parseModifier(modifierSegment),
|
||||
variants: parsedCandidateVariants,
|
||||
important: state.important,
|
||||
}
|
||||
}
|
||||
|
||||
// Candidates that start with a dash are the negative versions of another
|
||||
// candidate, e.g. `-mx-4`.
|
||||
if (base[0] === '-') {
|
||||
state.negative = true
|
||||
base = base.slice(1)
|
||||
}
|
||||
|
||||
let [root, value] = findRoot(base, utilities)
|
||||
|
||||
let modifierSegment: string | null = null
|
||||
|
||||
// If the root is null, but it contains a `/`, then it could be that we are
|
||||
// dealing with a functional utility that contains a modifier but doesn't
|
||||
// contain a value.
|
||||
//
|
||||
// E.g.: `@container/parent`
|
||||
if (root === null && base.includes('/')) {
|
||||
let [rootWithoutModifier, rootModifierSegment = null] = segment(base, '/')
|
||||
|
||||
modifierSegment = rootModifierSegment
|
||||
|
||||
// Try to find the root and value, without the modifier present
|
||||
;[root, value] = findRoot(rootWithoutModifier, utilities)
|
||||
}
|
||||
|
||||
// If there's no root, the candidate isn't a valid class and can be discarded.
|
||||
if (root === null) return null
|
||||
|
||||
let kind = utilities.kind(root)
|
||||
|
||||
if (kind === 'static') {
|
||||
if (value !== null) return null
|
||||
|
||||
return {
|
||||
kind: 'static',
|
||||
root,
|
||||
variants: parsedCandidateVariants,
|
||||
negative: state.negative,
|
||||
important: state.important,
|
||||
}
|
||||
}
|
||||
|
||||
let candidate: Candidate = {
|
||||
kind: 'functional',
|
||||
root,
|
||||
modifier: modifierSegment === null ? null : parseModifier(modifierSegment),
|
||||
value: null,
|
||||
variants: parsedCandidateVariants,
|
||||
negative: state.negative,
|
||||
important: state.important,
|
||||
}
|
||||
|
||||
if (value === null) return candidate
|
||||
|
||||
{
|
||||
// Extract a modifier if present, e.g. `text-xl/9` or `bg-red-500/[14%]`
|
||||
let [valueWithoutModifier, modifierSegment = null] = segment(value, '/')
|
||||
|
||||
if (modifierSegment !== null) {
|
||||
candidate.modifier = parseModifier(modifierSegment)
|
||||
}
|
||||
|
||||
let startArbitraryIdx = valueWithoutModifier.indexOf('[')
|
||||
let valueIsArbitrary = startArbitraryIdx !== -1
|
||||
|
||||
if (valueIsArbitrary) {
|
||||
let arbitraryValue = valueWithoutModifier.slice(startArbitraryIdx + 1, -1)
|
||||
|
||||
// Extract an explicit typehint if present, e.g. `bg-[color:var(--my-var)])`
|
||||
let typehint = ''
|
||||
for (let i = 0; i < arbitraryValue.length; i++) {
|
||||
let code = arbitraryValue.charCodeAt(i)
|
||||
|
||||
// If we hit a ":", we're at the end of a typehint.
|
||||
if (code === 58 /* ':' */) {
|
||||
typehint = arbitraryValue.slice(0, i)
|
||||
arbitraryValue = arbitraryValue.slice(i + 1)
|
||||
break
|
||||
}
|
||||
|
||||
// Keep iterating as long as we've only seen valid typehint characters.
|
||||
if (code === 45 /* '-' */ || (code >= 97 && code <= 122) /* [a-z] */) {
|
||||
continue
|
||||
}
|
||||
|
||||
// If we see any other character, there's no typehint so break early.
|
||||
break
|
||||
}
|
||||
|
||||
// If an arbitrary value looks like a CSS variable, we automatically wrap
|
||||
// it with `var(...)`.
|
||||
//
|
||||
// But since some CSS properties accept a `<dashed-ident>` as a value
|
||||
// directly (e.g. `scroll-timeline-name`), we also store the original
|
||||
// value in case the utility matcher is interested in it without
|
||||
// `var(...)`.
|
||||
let dashedIdent: string | null = null
|
||||
if (arbitraryValue[0] === '-' && arbitraryValue[1] === '-') {
|
||||
dashedIdent = arbitraryValue
|
||||
arbitraryValue = `var(${arbitraryValue})`
|
||||
} else {
|
||||
arbitraryValue = decodeArbitraryValue(arbitraryValue)
|
||||
}
|
||||
|
||||
candidate.value = {
|
||||
kind: 'arbitrary',
|
||||
dataType: typehint || null,
|
||||
value: arbitraryValue,
|
||||
dashedIdent,
|
||||
}
|
||||
} else {
|
||||
// Some utilities support fractions as values, e.g. `w-1/2`. Since it's
|
||||
// ambiguous whether the slash signals a modifier or not, we store the
|
||||
// fraction separately in case the utility matcher is interested in it.
|
||||
let fraction =
|
||||
modifierSegment === null || candidate.modifier?.kind === 'arbitrary'
|
||||
? null
|
||||
: value.slice(valueWithoutModifier.lastIndexOf('-') + 1)
|
||||
|
||||
candidate.value = {
|
||||
kind: 'named',
|
||||
value: valueWithoutModifier,
|
||||
fraction,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return candidate
|
||||
}
|
||||
|
||||
function parseModifier(modifier: string): CandidateModifier {
|
||||
if (modifier[0] === '[' && modifier[modifier.length - 1] === ']') {
|
||||
let arbitraryValue = modifier.slice(1, -1)
|
||||
|
||||
// If an arbitrary value looks like a CSS variable, we automatically wrap
|
||||
// it with `var(...)`.
|
||||
//
|
||||
// But since some CSS properties accept a `<dashed-ident>` as a value
|
||||
// directly (e.g. `scroll-timeline-name`), we also store the original
|
||||
// value in case the utility matcher is interested in it without
|
||||
// `var(...)`.
|
||||
let dashedIdent: string | null = null
|
||||
if (arbitraryValue[0] === '-' && arbitraryValue[1] === '-') {
|
||||
dashedIdent = arbitraryValue
|
||||
arbitraryValue = `var(${arbitraryValue})`
|
||||
} else {
|
||||
arbitraryValue = decodeArbitraryValue(arbitraryValue)
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'arbitrary',
|
||||
value: arbitraryValue,
|
||||
dashedIdent,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'named',
|
||||
value: modifier,
|
||||
}
|
||||
}
|
||||
|
||||
export function parseVariant(
|
||||
variant: string,
|
||||
variants: {
|
||||
has: (value: string) => boolean
|
||||
kind: (root: string) => Omit<Variant['kind'], 'arbitrary'>
|
||||
compounds: (root: string) => boolean
|
||||
},
|
||||
parsedVariants: { get: (value: string) => Variant | null },
|
||||
): Variant | null {
|
||||
// Arbitrary variants
|
||||
if (variant[0] === '[' && variant[variant.length - 1] === ']') {
|
||||
/**
|
||||
* TODO: Breaking change
|
||||
*
|
||||
* @deprecated Arbitrary variants containing at-rules with other selectors
|
||||
* are deprecated. Use stacked variants instead.
|
||||
*
|
||||
* Before:
|
||||
* - `[@media(width>=123px){&:hover}]:`
|
||||
*
|
||||
* After:
|
||||
* - `[@media(width>=123px)]:[&:hover]:`
|
||||
* - `[@media(width>=123px)]:hover:`
|
||||
*/
|
||||
if (variant[1] === '@' && variant.includes('&')) return null
|
||||
|
||||
let selector = decodeArbitraryValue(variant.slice(1, -1))
|
||||
|
||||
if (selector[0] !== '@') {
|
||||
// Ensure `&` is always present by wrapping the selector in `&:is(…)`
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// - `[p]:flex`
|
||||
if (!selector.includes('&')) {
|
||||
selector = `&:is(${selector})`
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'arbitrary',
|
||||
selector,
|
||||
compounds: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Static, functional and compound variants
|
||||
{
|
||||
// group-hover/group-name
|
||||
// ^^^^^^^^^^^ -> Variant without modifier
|
||||
// ^^^^^^^^^^ -> Modifier
|
||||
let [variantWithoutModifier, modifier = null, additionalModifier] = segment(variant, '/')
|
||||
|
||||
// If there's more than one modifier, the variant is invalid.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// - `group-hover/foo/bar`
|
||||
if (additionalModifier) return null
|
||||
|
||||
let [root, value] = findRoot(variantWithoutModifier, variants)
|
||||
|
||||
// Variant is invalid, therefore the candidate is invalid and we can skip
|
||||
// continue parsing it.
|
||||
if (root === null) return null
|
||||
|
||||
switch (variants.kind(root)) {
|
||||
case 'static': {
|
||||
if (value !== null) return null
|
||||
|
||||
return {
|
||||
kind: 'static',
|
||||
root,
|
||||
compounds: variants.compounds(root),
|
||||
}
|
||||
}
|
||||
|
||||
case 'functional': {
|
||||
if (value === null) return null
|
||||
|
||||
if (value[0] === '[' && value[value.length - 1] === ']') {
|
||||
return {
|
||||
kind: 'functional',
|
||||
root,
|
||||
modifier: modifier === null ? null : parseModifier(modifier),
|
||||
value: {
|
||||
kind: 'arbitrary',
|
||||
value: decodeArbitraryValue(value.slice(1, -1)),
|
||||
},
|
||||
compounds: variants.compounds(root),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'functional',
|
||||
root,
|
||||
modifier: modifier === null ? null : parseModifier(modifier),
|
||||
value: { kind: 'named', value },
|
||||
compounds: variants.compounds(root),
|
||||
}
|
||||
}
|
||||
|
||||
case 'compound': {
|
||||
if (value === null) return null
|
||||
|
||||
let subVariant = parsedVariants.get(value)
|
||||
if (subVariant === null) return null
|
||||
if (subVariant.compounds === false) return null
|
||||
|
||||
return {
|
||||
kind: 'compound',
|
||||
root,
|
||||
modifier: modifier === null ? null : { kind: 'named', value: modifier },
|
||||
variant: subVariant,
|
||||
compounds: variants.compounds(root),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function findRoot(
|
||||
input: string,
|
||||
lookup: { has: (input: string) => boolean },
|
||||
): [string | null, string | null] {
|
||||
// If the lookup has an exact match, then that's the root.
|
||||
if (lookup.has(input)) return [input, null]
|
||||
|
||||
// Otherwise test every permutation of the input by iteratively removing
|
||||
// everything after the last dash.
|
||||
let idx = input.lastIndexOf('-')
|
||||
if (idx === -1) {
|
||||
// Variants starting with `@` are special because they don't need a `-`
|
||||
// after the `@` (E.g.: `@-lg` should be written as `@lg`).
|
||||
if (input[0] === '@' && lookup.has('@')) {
|
||||
return ['@', input.slice(1)]
|
||||
}
|
||||
|
||||
return [null, null]
|
||||
}
|
||||
|
||||
// Determine the root and value by testing permutations of the incoming input
|
||||
// against the lookup table.
|
||||
//
|
||||
// In case of a candidate like `bg-red-500`, this looks like:
|
||||
//
|
||||
// `bg-red-500` -> No match
|
||||
// `bg-red` -> No match
|
||||
// `bg` -> Match
|
||||
do {
|
||||
let maybeRoot = input.slice(0, idx)
|
||||
|
||||
if (lookup.has(maybeRoot)) {
|
||||
return [maybeRoot, input.slice(idx + 1)]
|
||||
}
|
||||
|
||||
idx = input.lastIndexOf('-', idx - 1)
|
||||
} while (idx > 0)
|
||||
|
||||
return [null, null]
|
||||
}
|
||||
244
packages/tailwindcss/src/cli/commands/build/index.ts
Normal file
244
packages/tailwindcss/src/cli/commands/build/index.ts
Normal file
@ -0,0 +1,244 @@
|
||||
import watcher from '@parcel/watcher'
|
||||
import {
|
||||
IO,
|
||||
Parsing,
|
||||
clearCache,
|
||||
scanDir,
|
||||
scanFiles,
|
||||
type ChangedContent,
|
||||
} from '@tailwindcss/oxide'
|
||||
import { existsSync } from 'node:fs'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import postcss from 'postcss'
|
||||
import atImport from 'postcss-import'
|
||||
import { compile, optimizeCss } from '../../..'
|
||||
import type { Arg, Result } from '../../utils/args'
|
||||
import {
|
||||
eprintln,
|
||||
formatDuration,
|
||||
header,
|
||||
highlight,
|
||||
println,
|
||||
relative,
|
||||
} from '../../utils/renderer'
|
||||
import { resolve } from '../../utils/resolve'
|
||||
import { drainStdin, outputFile } from './utils'
|
||||
|
||||
const css = String.raw
|
||||
|
||||
export function options() {
|
||||
return {
|
||||
'--input': {
|
||||
type: 'string',
|
||||
description: 'Input file',
|
||||
alias: '-i',
|
||||
},
|
||||
'--output': {
|
||||
type: 'string',
|
||||
description: 'Output file',
|
||||
alias: '-o',
|
||||
},
|
||||
'--watch': {
|
||||
type: 'boolean | string',
|
||||
description: 'Watch for changes and rebuild as needed',
|
||||
alias: '-w',
|
||||
},
|
||||
'--minify': {
|
||||
type: 'boolean',
|
||||
description: 'Minify the output',
|
||||
alias: '-m',
|
||||
},
|
||||
'--cwd': {
|
||||
type: 'string',
|
||||
description: 'The current working directory',
|
||||
default: '.',
|
||||
},
|
||||
} satisfies Arg
|
||||
}
|
||||
|
||||
export async function handle(args: Result<ReturnType<typeof options>>) {
|
||||
let base = path.resolve(args['--cwd'])
|
||||
|
||||
// Resolve the output as an absolute path.
|
||||
if (args['--output']) {
|
||||
args['--output'] = path.resolve(base, args['--output'])
|
||||
}
|
||||
|
||||
// Resolve the input as an absolute path. If the input is a `-`, then we don't
|
||||
// need to resolve it because this is a flag to indicate that we want to use
|
||||
// `stdin` instead.
|
||||
if (args['--input'] && args['--input'] !== '-') {
|
||||
args['--input'] = path.resolve(base, args['--input'])
|
||||
|
||||
// Ensure the provided `--input` exists.
|
||||
if (!existsSync(args['--input'])) {
|
||||
eprintln(header())
|
||||
eprintln()
|
||||
eprintln(`Specified input file ${highlight(relative(args['--input']))} does not exist.`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
let start = process.hrtime.bigint()
|
||||
let { candidates } = scanDir({ base })
|
||||
|
||||
// Resolve the input
|
||||
let [input, cssImportPaths] = await handleImports(
|
||||
args['--input']
|
||||
? args['--input'] === '-'
|
||||
? await drainStdin()
|
||||
: await fs.readFile(args['--input'], 'utf-8')
|
||||
: css`
|
||||
@import '${resolve('tailwindcss/index.css')}';
|
||||
`,
|
||||
args['--input'] ?? base,
|
||||
)
|
||||
|
||||
// Compile the input
|
||||
let result = optimizeCss(compile(input, candidates), {
|
||||
file: args['--input'] ?? 'input.css',
|
||||
minify: args['--minify'],
|
||||
})
|
||||
|
||||
// Write the output
|
||||
if (args['--output']) {
|
||||
await outputFile(args['--output'], result)
|
||||
} else {
|
||||
println(result)
|
||||
}
|
||||
|
||||
let end = process.hrtime.bigint()
|
||||
eprintln(header())
|
||||
eprintln()
|
||||
eprintln(`Done in ${formatDuration(end - start)}`)
|
||||
|
||||
// Watch for changes
|
||||
if (args['--watch']) {
|
||||
await watcher.subscribe(base, async (err, events) => {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// If the only change happened to the output file, then we don't want to
|
||||
// trigger a rebuild because that will result in an infinite loop.
|
||||
if (events.length === 1 && events[0].path === args['--output']) return
|
||||
|
||||
let changedFiles: ChangedContent[] = []
|
||||
let rebuildStrategy: 'incremental' | 'full' = 'incremental'
|
||||
|
||||
for (let event of events) {
|
||||
// Track new and updated files for incremental rebuilds.
|
||||
if (event.type === 'create' || event.type === 'update') {
|
||||
changedFiles.push({
|
||||
file: event.path,
|
||||
extension: path.extname(event.path).slice(1),
|
||||
} satisfies ChangedContent)
|
||||
}
|
||||
|
||||
// If one of the changed files is related to the input CSS files, then
|
||||
// we need to do a full rebuild because the theme might have changed.
|
||||
if (cssImportPaths.includes(event.path)) {
|
||||
rebuildStrategy = 'full'
|
||||
|
||||
// No need to check the rest of the events, because we already know we
|
||||
// need to do a full rebuild.
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Re-compile the input
|
||||
let start = process.hrtime.bigint()
|
||||
|
||||
// Scan the entire `base` directory for full rebuilds.
|
||||
if (rebuildStrategy === 'full') {
|
||||
// Clear the cache because we need to re-scan the entire directory.
|
||||
clearCache()
|
||||
|
||||
// Re-scan the directory to get the new `candidates`.
|
||||
candidates = scanDir({ base }).candidates
|
||||
}
|
||||
|
||||
// Scan changed files only for incremental rebuilds.
|
||||
else if (rebuildStrategy === 'incremental') {
|
||||
let uniqueCandidates = new Set(candidates)
|
||||
for (let candidate of scanFiles(changedFiles, IO.Sequential | Parsing.Sequential)) {
|
||||
uniqueCandidates.add(candidate)
|
||||
}
|
||||
candidates = Array.from(uniqueCandidates)
|
||||
}
|
||||
|
||||
// Resolve the input
|
||||
if (rebuildStrategy === 'full') {
|
||||
// Collect the new `input` and `cssImportPaths`.
|
||||
;[input, cssImportPaths] = await handleImports(
|
||||
args['--input']
|
||||
? await fs.readFile(args['--input'], 'utf-8')
|
||||
: css`
|
||||
@import '${resolve('tailwindcss/index.css')}';
|
||||
`,
|
||||
args['--input'] ?? base,
|
||||
)
|
||||
}
|
||||
|
||||
// Compile the input
|
||||
let result = optimizeCss(compile(input, candidates), {
|
||||
file: args['--input'] ?? 'input.css',
|
||||
minify: args['--minify'],
|
||||
})
|
||||
|
||||
// Write the output
|
||||
if (args['--output']) {
|
||||
await outputFile(args['--output'], result)
|
||||
} else {
|
||||
println(result)
|
||||
}
|
||||
|
||||
let end = process.hrtime.bigint()
|
||||
eprintln(`Done in ${formatDuration(end - start)}`)
|
||||
} catch (err) {
|
||||
// Catch any errors and print them to stderr, but don't exit the process
|
||||
// and keep watching.
|
||||
if (err instanceof Error) {
|
||||
eprintln(err.toString())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
|
||||
// Keep the process running
|
||||
process.stdin.resume()
|
||||
}
|
||||
}
|
||||
|
||||
function handleImports(
|
||||
input: string,
|
||||
file: string,
|
||||
): [css: string, paths: string[]] | Promise<[css: string, paths: string[]]> {
|
||||
// TODO: Should we implement this ourselves instead of relying on PostCSS?
|
||||
//
|
||||
// Relevant specification:
|
||||
// - CSS Import Resolve: https://csstools.github.io/css-import-resolve/
|
||||
|
||||
if (!input.includes('@import')) return [input, []]
|
||||
|
||||
return postcss()
|
||||
.use(atImport())
|
||||
.process(input, { from: file })
|
||||
.then((result) => [
|
||||
result.css,
|
||||
|
||||
// Use `result.messages` to get the imported files. This also includes the
|
||||
// current file itself.
|
||||
result.messages.filter((msg) => msg.type === 'postcss-import').map((msg) => msg.file),
|
||||
])
|
||||
}
|
||||
26
packages/tailwindcss/src/cli/commands/build/utils.ts
Normal file
26
packages/tailwindcss/src/cli/commands/build/utils.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
export function drainStdin() {
|
||||
return new Promise<string>((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: string, contents: string) {
|
||||
try {
|
||||
let currentContents = await fs.readFile(file, 'utf8')
|
||||
if (currentContents === contents) return // Skip writing the file
|
||||
} catch {}
|
||||
|
||||
// Ensure the parent directories exist
|
||||
await fs.mkdir(path.dirname(file), { recursive: true })
|
||||
|
||||
// Write the file
|
||||
await fs.writeFile(file, contents, 'utf8')
|
||||
}
|
||||
170
packages/tailwindcss/src/cli/commands/help/index.ts
Normal file
170
packages/tailwindcss/src/cli/commands/help/index.ts
Normal file
@ -0,0 +1,170 @@
|
||||
import pc from 'picocolors'
|
||||
import type { Arg } from '../../utils/args'
|
||||
import { UI, header, highlight, indent, println, wordWrap } from '../../utils/renderer'
|
||||
|
||||
export function help({
|
||||
invalid,
|
||||
usage,
|
||||
options,
|
||||
}: {
|
||||
invalid?: string
|
||||
usage?: string[]
|
||||
options?: Arg
|
||||
}) {
|
||||
// Available terminal width
|
||||
let width = process.stdout.columns
|
||||
|
||||
// Render header
|
||||
println(header())
|
||||
|
||||
// Render the invalid command
|
||||
if (invalid) {
|
||||
println()
|
||||
println(`${pc.dim('Invalid command:')} ${invalid}`)
|
||||
}
|
||||
|
||||
// Render usage
|
||||
if (usage && usage.length > 0) {
|
||||
println()
|
||||
println(pc.dim('Usage:'))
|
||||
for (let [idx, example] of usage.entries()) {
|
||||
// Split the usage example into the command and its options. This allows
|
||||
// us to wrap the options based on the available width of the terminal.
|
||||
let command = example.slice(0, example.indexOf('['))
|
||||
let options = example.slice(example.indexOf('['))
|
||||
|
||||
// Make the options dimmed, to make them stand out less than the command
|
||||
// itself.
|
||||
options = options.replace(/\[.*?\]/g, (option) => pc.dim(option))
|
||||
|
||||
// The space between the command and the options.
|
||||
let space = 1
|
||||
|
||||
// Wrap the options based on the available width of the terminal.
|
||||
let lines = wordWrap(options, width - UI.indent - command.length - space)
|
||||
|
||||
// Print an empty line between the usage examples if we need to split due
|
||||
// to width constraints. This ensures that the usage examples are visually
|
||||
// separated.
|
||||
//
|
||||
// E.g.: when enough space is available
|
||||
//
|
||||
// ```
|
||||
// Usage:
|
||||
// tailwindcss build [--input input.css] [--output output.css] [--watch] [options...]
|
||||
// tailwindcss other [--watch] [options...]
|
||||
// ```
|
||||
//
|
||||
// E.g.: when not enough space is available
|
||||
//
|
||||
// ```
|
||||
// Usage:
|
||||
// tailwindcss build [--input input.css] [--output output.css]
|
||||
// [--watch] [options...]
|
||||
//
|
||||
// tailwindcss other [--watch] [options...]
|
||||
// ```
|
||||
if (lines.length > 1 && idx !== 0) {
|
||||
println()
|
||||
}
|
||||
|
||||
// Print the usage examples based on available width of the terminal.
|
||||
//
|
||||
// E.g.: when enough space is available
|
||||
//
|
||||
// ```
|
||||
// Usage:
|
||||
// tailwindcss [--input input.css] [--output output.css] [--watch] [options...]
|
||||
// ```
|
||||
//
|
||||
// E.g.: when not enough space is available
|
||||
//
|
||||
// ```
|
||||
// Usage:
|
||||
// tailwindcss [--input input.css] [--output output.css]
|
||||
// [--watch] [options...]
|
||||
// ```
|
||||
//
|
||||
// > Note how the second line is indented to align with the first line.
|
||||
println(indent(`${command}${lines.shift()}`))
|
||||
for (let line of lines) {
|
||||
println(indent(line, command.length))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render options
|
||||
if (options) {
|
||||
// Track the max alias length, this is used to indent the options that don't
|
||||
// have an alias such that everything is aligned properly.
|
||||
let maxAliasLength = 0
|
||||
for (let { alias } of Object.values(options)) {
|
||||
if (alias) {
|
||||
maxAliasLength = Math.max(maxAliasLength, alias.length)
|
||||
}
|
||||
}
|
||||
|
||||
// The option strings, which are the combination of the `alias` and the
|
||||
// `flag`, with the correct spacing.
|
||||
let optionStrings: string[] = []
|
||||
|
||||
// Track the max option length, which is the longest combination of an
|
||||
// `alias` followed by `, ` and followed by the `flag`.
|
||||
let maxOptionLength = 0
|
||||
|
||||
for (let [flag, { alias }] of Object.entries(options)) {
|
||||
// The option string, which is the combination of the alias and the flag
|
||||
// but already properly indented based on the other aliases to ensure
|
||||
// everything is aligned properly.
|
||||
let option = [
|
||||
alias ? `${alias.padStart(maxAliasLength)}` : alias,
|
||||
alias ? flag : ' '.repeat(maxAliasLength + 2 /* `, `.length */) + flag,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(', ')
|
||||
|
||||
optionStrings.push(option)
|
||||
maxOptionLength = Math.max(maxOptionLength, option.length)
|
||||
}
|
||||
|
||||
println()
|
||||
println(pc.dim('Options:'))
|
||||
|
||||
// The minimum amount of dots between the option and the description.
|
||||
let minimumGap = 8
|
||||
|
||||
for (let { description, default: defaultValue = null } of Object.values(options)) {
|
||||
// The option to render
|
||||
let option = optionStrings.shift() as string
|
||||
|
||||
// The amount of dots to show between the option and the description.
|
||||
let dotCount = minimumGap + (maxOptionLength - option.length)
|
||||
|
||||
// To account for the space before and after the dots.
|
||||
let spaces = 2
|
||||
|
||||
// The available width remaining for the description.
|
||||
let availableWidth = width - option.length - dotCount - spaces - UI.indent
|
||||
|
||||
// Wrap the description and the default value (if present), based on the
|
||||
// available width.
|
||||
let lines = wordWrap(
|
||||
defaultValue !== null
|
||||
? `${description} ${pc.dim(`[default:\u202F${highlight(`${defaultValue}`)}]`)}`
|
||||
: description,
|
||||
availableWidth,
|
||||
)
|
||||
|
||||
// Print the option, the spacer dots and the start of the description.
|
||||
println(
|
||||
indent(`${pc.blue(option)} ${pc.dim(pc.gray('\u00B7')).repeat(dotCount)} ${lines.shift()}`),
|
||||
)
|
||||
|
||||
// Print the remaining lines of the description, indenting them to align
|
||||
// with the start of the description.
|
||||
for (let line of lines) {
|
||||
println(indent(`${' '.repeat(option.length + dotCount + spaces)}${line}`))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
45
packages/tailwindcss/src/cli/index.ts
Normal file
45
packages/tailwindcss/src/cli/index.ts
Normal file
@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { args, type Arg } from './utils/args'
|
||||
|
||||
import * as build from './commands/build'
|
||||
import { help } from './commands/help'
|
||||
|
||||
const sharedOptions = {
|
||||
'--help': { type: 'boolean', description: 'Display usage information', alias: '-h' },
|
||||
} satisfies Arg
|
||||
|
||||
const shared = args(sharedOptions)
|
||||
const command = shared._[0]
|
||||
|
||||
// Right now we don't support any sub-commands. Let's show the help message
|
||||
// instead.
|
||||
if (command) {
|
||||
help({
|
||||
invalid: command,
|
||||
usage: ['tailwindcss [options]'],
|
||||
options: { ...build.options(), ...sharedOptions },
|
||||
})
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Display main help message if no command is being used.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// - `tailwindcss` // should show the help message
|
||||
//
|
||||
// E.g.: implicit `build` command
|
||||
//
|
||||
// - `tailwindcss -o output.css` // should run the build command, not show the help message
|
||||
// - `tailwindcss > output.css` // should run the build command, not show the help message
|
||||
if ((process.stdout.isTTY && !process.argv.slice(2).includes('-o')) || shared['--help']) {
|
||||
help({
|
||||
usage: ['tailwindcss [--input input.css] [--output output.css] [--watch] [options…]'],
|
||||
options: { ...build.options(), ...sharedOptions },
|
||||
})
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Handle the build command
|
||||
build.handle(args(build.options()))
|
||||
123
packages/tailwindcss/src/cli/utils/args.test.ts
Normal file
123
packages/tailwindcss/src/cli/utils/args.test.ts
Normal file
@ -0,0 +1,123 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { args, type Arg } from './args'
|
||||
|
||||
it('should be possible to parse a single argument', () => {
|
||||
expect(
|
||||
args(
|
||||
{
|
||||
'--input': { type: 'string', description: 'Input file' },
|
||||
},
|
||||
['--input', 'input.css'],
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"--input": "input.css",
|
||||
"_": [],
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('should fallback to the default value if no flag is passed', () => {
|
||||
expect(
|
||||
args(
|
||||
{
|
||||
'--input': { type: 'string', description: 'Input file', default: 'input.css' },
|
||||
},
|
||||
['--other'],
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"--input": "input.css",
|
||||
"_": [],
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('should fallback to null if no flag is passed and no default value is provided', () => {
|
||||
expect(
|
||||
args(
|
||||
{
|
||||
'--input': { type: 'string', description: 'Input file' },
|
||||
},
|
||||
['--other'],
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"--input": null,
|
||||
"_": [],
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('should be possible to parse a single argument using the shorthand alias', () => {
|
||||
expect(
|
||||
args(
|
||||
{
|
||||
'--input': { type: 'string', description: 'Input file', alias: '-i' },
|
||||
},
|
||||
['-i', 'input.css'],
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"--input": "input.css",
|
||||
"_": [],
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('should convert the incoming value to the correct type', () => {
|
||||
expect(
|
||||
args(
|
||||
{
|
||||
'--input': { type: 'string', description: 'Input file' },
|
||||
'--watch': { type: 'boolean', description: 'Watch mode' },
|
||||
'--retries': { type: 'number', description: 'Amount of retries' },
|
||||
},
|
||||
['--input', 'input.css', '--watch', '--retries', '3'],
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"--input": "input.css",
|
||||
"--retries": 3,
|
||||
"--watch": true,
|
||||
"_": [],
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('should be possible to provide multiple types, and convert the value to that type', () => {
|
||||
let options = {
|
||||
'--retries': { type: 'boolean | number | string', description: 'Retries' },
|
||||
} satisfies Arg
|
||||
|
||||
expect(args(options, ['--retries'])).toMatchInlineSnapshot(`
|
||||
{
|
||||
"--retries": true,
|
||||
"_": [],
|
||||
}
|
||||
`)
|
||||
expect(args(options, ['--retries', 'true'])).toMatchInlineSnapshot(`
|
||||
{
|
||||
"--retries": true,
|
||||
"_": [],
|
||||
}
|
||||
`)
|
||||
expect(args(options, ['--retries', 'false'])).toMatchInlineSnapshot(`
|
||||
{
|
||||
"--retries": false,
|
||||
"_": [],
|
||||
}
|
||||
`)
|
||||
expect(args(options, ['--retries', '5'])).toMatchInlineSnapshot(`
|
||||
{
|
||||
"--retries": 5,
|
||||
"_": [],
|
||||
}
|
||||
`)
|
||||
expect(args(options, ['--retries', 'indefinitely'])).toMatchInlineSnapshot(`
|
||||
{
|
||||
"--retries": "indefinitely",
|
||||
"_": [],
|
||||
}
|
||||
`)
|
||||
})
|
||||
160
packages/tailwindcss/src/cli/utils/args.ts
Normal file
160
packages/tailwindcss/src/cli/utils/args.ts
Normal file
@ -0,0 +1,160 @@
|
||||
import parse from 'mri'
|
||||
|
||||
// Definition of the arguments for a command in the CLI.
|
||||
export type Arg = {
|
||||
[key: `--${string}`]: {
|
||||
type: keyof Types
|
||||
description: string
|
||||
alias?: `-${string}`
|
||||
default?: Types[keyof Types]
|
||||
}
|
||||
}
|
||||
|
||||
// Each argument will have a type and we want to convert the incoming raw string
|
||||
// based value to the correct type. We can't use pure TypeScript types because
|
||||
// these don't exist at runtime. Instead, we define a string-based type that
|
||||
// maps to a TypeScript type.
|
||||
type Types = {
|
||||
boolean: boolean
|
||||
number: number | null
|
||||
string: string | null
|
||||
'boolean | string': boolean | string | null
|
||||
'number | string': number | string | null
|
||||
'boolean | number': boolean | number | null
|
||||
'boolean | number | string': boolean | number | string | null
|
||||
}
|
||||
|
||||
// Convert the `Arg` type to a type that can be used at runtime.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// Arg:
|
||||
// ```
|
||||
// { '--input': { type: 'string', description: 'Input file', alias: '-i' } }
|
||||
// ```
|
||||
//
|
||||
// Command:
|
||||
// ```
|
||||
// ./tailwindcss -i input.css
|
||||
// ./tailwindcss --input input.css
|
||||
// ```
|
||||
//
|
||||
// Result type:
|
||||
// ```
|
||||
// {
|
||||
// _: string[], // All non-flag arguments
|
||||
// '--input': string | null // The `--input` flag will be filled with `null`, if the flag is not used.
|
||||
// // The `null` type will not be there if `default` is provided.
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// Result runtime object:
|
||||
// ```
|
||||
// {
|
||||
// _: [],
|
||||
// '--input': 'input.css'
|
||||
// }
|
||||
// ```
|
||||
export type Result<T extends Arg> = {
|
||||
[K in keyof T]: T[K] extends { type: keyof Types; default?: any }
|
||||
? undefined extends T[K]['default']
|
||||
? Types[T[K]['type']]
|
||||
: NonNullable<Types[T[K]['type']]>
|
||||
: never
|
||||
} & {
|
||||
// All non-flag arguments
|
||||
_: string[]
|
||||
}
|
||||
|
||||
export function args<const T extends Arg>(options: T, argv = process.argv.slice(2)): Result<T> {
|
||||
let parsed = parse(argv)
|
||||
|
||||
let result: { _: string[]; [key: string]: unknown } = {
|
||||
_: parsed._,
|
||||
}
|
||||
|
||||
for (let [
|
||||
flag,
|
||||
{ type, alias, default: defaultValue = type === 'boolean' ? false : null },
|
||||
] of Object.entries(options)) {
|
||||
// Start with the default value
|
||||
result[flag] = defaultValue
|
||||
|
||||
// Try to find the `alias`, and map it to long form `flag`
|
||||
if (alias) {
|
||||
let key = alias.slice(1)
|
||||
if (parsed[key] !== undefined) {
|
||||
result[flag] = convert(parsed[key], type)
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find the long form `flag`
|
||||
{
|
||||
let key = flag.slice(2)
|
||||
if (parsed[key] !== undefined) {
|
||||
result[flag] = convert(parsed[key], type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result as Result<T>
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
type ArgumentType = string | boolean
|
||||
|
||||
// Try to convert the raw incoming `value` (which will be a string or a boolean,
|
||||
// this is coming from `mri`'s parse function'), to the correct type based on
|
||||
// the `type` of the argument.
|
||||
function convert<T extends keyof Types>(value: string | boolean, type: T) {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
return convertString(value)
|
||||
case 'boolean':
|
||||
return convertBoolean(value)
|
||||
case 'number':
|
||||
return convertNumber(value)
|
||||
case 'boolean | string':
|
||||
return convertBoolean(value) ?? convertString(value)
|
||||
case 'number | string':
|
||||
return convertNumber(value) ?? convertString(value)
|
||||
case 'boolean | number':
|
||||
return convertBoolean(value) ?? convertNumber(value)
|
||||
case 'boolean | number | string':
|
||||
return convertBoolean(value) ?? convertNumber(value) ?? convertString(value)
|
||||
default:
|
||||
throw new Error(`Unhandled type: ${type}`)
|
||||
}
|
||||
}
|
||||
|
||||
function convertBoolean(value: ArgumentType) {
|
||||
if (value === true || value === false) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (value === 'true') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (value === 'false') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function convertNumber(value: ArgumentType) {
|
||||
if (typeof value === 'number') {
|
||||
return value
|
||||
}
|
||||
|
||||
{
|
||||
let valueAsNumber = Number(value)
|
||||
if (!Number.isNaN(valueAsNumber)) {
|
||||
return valueAsNumber
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function convertString(value: ArgumentType) {
|
||||
return `${value}`
|
||||
}
|
||||
28
packages/tailwindcss/src/cli/utils/format-ns.test.ts
Normal file
28
packages/tailwindcss/src/cli/utils/format-ns.test.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { formatNanoseconds } from './format-ns'
|
||||
|
||||
it.each([
|
||||
[0, '0ns'],
|
||||
[1, '1ns'],
|
||||
[999, '999ns'],
|
||||
[1000, '1µs'],
|
||||
[1001, '1µs'],
|
||||
[999999, '999µs'],
|
||||
[1000000, '1ms'],
|
||||
[1000001, '1ms'],
|
||||
[999999999, '999ms'],
|
||||
[1000000000, '1s'],
|
||||
[1000000001, '1s'],
|
||||
[59999999999, '59s'],
|
||||
[60000000000, '1m'],
|
||||
[60000000001, '1m'],
|
||||
[3599999999999n, '59m'],
|
||||
[3600000000000n, '1h'],
|
||||
[3600000000001n, '1h'],
|
||||
[86399999999999n, '23h'],
|
||||
[86400000000000n, '1d'],
|
||||
[86400000000001n, '1d'],
|
||||
[8640000000000000n, '100d'],
|
||||
])('should format %s nanoseconds as %s', (ns, expected) => {
|
||||
expect(formatNanoseconds(ns)).toBe(expected)
|
||||
})
|
||||
23
packages/tailwindcss/src/cli/utils/format-ns.ts
Normal file
23
packages/tailwindcss/src/cli/utils/format-ns.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export function formatNanoseconds(input: bigint | number) {
|
||||
let ns = typeof input === 'number' ? BigInt(input) : input
|
||||
|
||||
if (ns < 1_000n) return `${ns}ns`
|
||||
ns /= 1_000n
|
||||
|
||||
if (ns < 1_000n) return `${ns}µs`
|
||||
ns /= 1_000n
|
||||
|
||||
if (ns < 1_000n) return `${ns}ms`
|
||||
ns /= 1_000n
|
||||
|
||||
if (ns < 60n) return `${ns}s`
|
||||
ns /= 60n
|
||||
|
||||
if (ns < 60n) return `${ns}m`
|
||||
ns /= 60n
|
||||
|
||||
if (ns < 24n) return `${ns}h`
|
||||
ns /= 24n
|
||||
|
||||
return `${ns}d`
|
||||
}
|
||||
63
packages/tailwindcss/src/cli/utils/renderer.test.ts
Normal file
63
packages/tailwindcss/src/cli/utils/renderer.test.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import path from 'path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { relative, wordWrap } from './renderer'
|
||||
|
||||
describe('relative', () => {
|
||||
it('should print an absolute path relative to the current working directory', () => {
|
||||
expect(relative(path.resolve('index.css'))).toMatchInlineSnapshot(`"./index.css"`)
|
||||
})
|
||||
|
||||
it('should prefer the shortest value by default', () => {
|
||||
// Shortest between absolute and relative paths
|
||||
expect(relative('index.css')).toMatchInlineSnapshot(`"index.css"`)
|
||||
})
|
||||
|
||||
it('should be possible to override the current working directory', () => {
|
||||
expect(relative('../utils/index.css', '..')).toMatchInlineSnapshot(`"./utils/index.css"`)
|
||||
})
|
||||
|
||||
it('should be possible to always prefer the relative path', () => {
|
||||
expect(
|
||||
relative('index.css', process.cwd(), { preferAbsoluteIfShorter: false }),
|
||||
).toMatchInlineSnapshot(`"./index.css"`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('word wrap', () => {
|
||||
it('should wrap a sentence', () => {
|
||||
expect(wordWrap('The quick brown fox jumps over the lazy dog', 10)).toMatchInlineSnapshot(`
|
||||
[
|
||||
"The quick",
|
||||
"brown fox",
|
||||
"jumps over",
|
||||
"the lazy",
|
||||
"dog",
|
||||
]
|
||||
`)
|
||||
expect(wordWrap('The quick brown fox jumps over the lazy dog', 30)).toMatchInlineSnapshot(`
|
||||
[
|
||||
"The quick brown fox jumps over",
|
||||
"the lazy dog",
|
||||
]
|
||||
`)
|
||||
})
|
||||
|
||||
it('should wrap a sentence with ANSI escape codes', () => {
|
||||
// The ANSI escape codes are not counted in the length, but they should
|
||||
// still be rendered correctly.
|
||||
expect(
|
||||
wordWrap(
|
||||
'\x1B[31mThe\x1B[39m \x1B[32mquick\x1B[39m \x1B[34mbrown\x1B[39m \x1B[35mfox\x1B[39m jumps over the lazy dog',
|
||||
10,
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
"[31mThe[39m [32mquick[39m",
|
||||
"[34mbrown[39m [35mfox[39m",
|
||||
"jumps over",
|
||||
"the lazy",
|
||||
"dog",
|
||||
]
|
||||
`)
|
||||
})
|
||||
})
|
||||
98
packages/tailwindcss/src/cli/utils/renderer.ts
Normal file
98
packages/tailwindcss/src/cli/utils/renderer.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import path from 'node:path'
|
||||
import pc from 'picocolors'
|
||||
import { version } from '../../../package.json'
|
||||
import { formatNanoseconds } from './format-ns'
|
||||
|
||||
export const UI = {
|
||||
indent: 2,
|
||||
}
|
||||
|
||||
export function header() {
|
||||
return `${pc.italic(pc.bold(pc.blue('\u2248')))} tailwindcss ${pc.blue(`v${version}`)}`
|
||||
}
|
||||
|
||||
export function highlight(file: string) {
|
||||
return `${pc.dim(pc.blue('`'))}${pc.blue(file)}${pc.dim(pc.blue('`'))}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an `absolute` path to a `relative` path from the current working
|
||||
* directory.
|
||||
*/
|
||||
export function relative(
|
||||
to: string,
|
||||
from = process.cwd(),
|
||||
{ preferAbsoluteIfShorter = true } = {},
|
||||
) {
|
||||
let result = path.relative(from, to)
|
||||
if (!result.startsWith('..')) {
|
||||
result = `.${path.sep}${result}`
|
||||
}
|
||||
|
||||
if (preferAbsoluteIfShorter && result.length > to.length) {
|
||||
return to
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap `text` into multiple lines based on the `width`.
|
||||
*/
|
||||
export function wordWrap(text: string, width: number) {
|
||||
let words = text.split(' ')
|
||||
let lines = []
|
||||
|
||||
let line = ''
|
||||
let lineLength = 0
|
||||
for (let word of words) {
|
||||
let wordLength = clearAnsiEscapes(word).length
|
||||
|
||||
if (lineLength + wordLength + 1 > width) {
|
||||
lines.push(line)
|
||||
line = ''
|
||||
lineLength = 0
|
||||
}
|
||||
|
||||
line += (lineLength ? ' ' : '') + word
|
||||
lineLength += wordLength + (lineLength ? 1 : 0)
|
||||
}
|
||||
|
||||
if (lineLength) {
|
||||
lines.push(line)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
const ESCAPE = /((?:\x9B|\x1B\[)[0-?]*[ -\/]*[@-~])/g
|
||||
function clearAnsiEscapes(input: string) {
|
||||
return input.replace(ESCAPE, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a duration in nanoseconds to a more human readable format.
|
||||
*/
|
||||
export function formatDuration(ns: bigint) {
|
||||
let formatted = formatNanoseconds(ns)
|
||||
|
||||
if (ns <= 50 * 1e6) return pc.green(formatted)
|
||||
if (ns <= 300 * 1e6) return pc.blue(formatted)
|
||||
if (ns <= 1000 * 1e6) return pc.yellow(formatted)
|
||||
|
||||
return pc.red(formatted)
|
||||
}
|
||||
|
||||
export function indent(value: string, offset = 0) {
|
||||
return `${' '.repeat(offset + UI.indent)}${value}`
|
||||
}
|
||||
|
||||
// Rust inspired functions to print to the console:
|
||||
|
||||
export function eprintln(value = '') {
|
||||
process.stderr.write(`${value}\n`)
|
||||
}
|
||||
|
||||
export function println(value = '') {
|
||||
process.stdout.write(`${value}\n`)
|
||||
}
|
||||
4
packages/tailwindcss/src/cli/utils/resolve.ts
Normal file
4
packages/tailwindcss/src/cli/utils/resolve.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { createRequire } from 'node:module'
|
||||
|
||||
export const resolve =
|
||||
typeof require?.resolve !== 'undefined' ? require.resolve : createRequire(import.meta.url).resolve
|
||||
251
packages/tailwindcss/src/compile.ts
Normal file
251
packages/tailwindcss/src/compile.ts
Normal file
@ -0,0 +1,251 @@
|
||||
import { rule, type AstNode, type Rule } from './ast'
|
||||
import { parseCandidate, parseVariant, type Candidate, type Variant } from './candidate'
|
||||
import { type DesignSystem } from './design-system'
|
||||
import GLOBAL_PROPERTY_ORDER from './property-order'
|
||||
import { DefaultMap } from './utils/default-map'
|
||||
import { escape } from './utils/escape'
|
||||
import type { Variants } from './variants'
|
||||
|
||||
export function compileCandidates(
|
||||
rawCandidates: string[],
|
||||
designSystem: DesignSystem,
|
||||
{ throwOnInvalidCandidate = false } = {},
|
||||
) {
|
||||
// Ensure the candidates are sorted alphabetically
|
||||
rawCandidates.sort()
|
||||
|
||||
let nodeSorting = new Map<
|
||||
AstNode,
|
||||
{ properties: number[]; variants: bigint; candidate: string }
|
||||
>()
|
||||
let astNodes: AstNode[] = []
|
||||
|
||||
// A lazy map implementation that will return the variant if it exists. If it
|
||||
// doesn't exist yet, the raw string variant will be parsed and added to the
|
||||
// map.
|
||||
let parsedVariants: DefaultMap<string, Variant | null> = new DefaultMap((variant, map) => {
|
||||
return parseVariant(variant, designSystem.variants, map)
|
||||
})
|
||||
|
||||
let candidates = new Map<Candidate, string>()
|
||||
|
||||
// Parse candidates and variants
|
||||
for (let rawCandidate of rawCandidates) {
|
||||
let candidate = parseCandidate(rawCandidate, designSystem.utilities, parsedVariants)
|
||||
if (candidate === null) {
|
||||
if (throwOnInvalidCandidate) {
|
||||
throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`)
|
||||
}
|
||||
continue // Bail, invalid candidate
|
||||
}
|
||||
candidates.set(candidate, rawCandidate)
|
||||
}
|
||||
|
||||
// Sort the variants
|
||||
let variants = Array.from(parsedVariants.values()).sort((a, z) => {
|
||||
return designSystem.variants.compare(a, z)
|
||||
})
|
||||
|
||||
// Create the AST
|
||||
next: for (let [candidate, rawCandidate] of candidates) {
|
||||
let nodes: AstNode[] = []
|
||||
|
||||
// Handle arbitrary properties
|
||||
if (candidate.kind === 'arbitrary') {
|
||||
let compileFn = designSystem.utilities.getArbitrary()
|
||||
|
||||
// Build the node
|
||||
let compiledNodes = compileFn(candidate)
|
||||
if (compiledNodes === undefined) {
|
||||
if (throwOnInvalidCandidate) {
|
||||
throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`)
|
||||
}
|
||||
continue next
|
||||
}
|
||||
|
||||
nodes = compiledNodes
|
||||
}
|
||||
|
||||
// Handle named utilities
|
||||
else if (candidate.kind === 'static' || candidate.kind === 'functional') {
|
||||
// Safety: At this point it is safe to use TypeScript's non-null assertion
|
||||
// operator because if the `candidate.root` didn't exist, `parseCandidate`
|
||||
// would have returned `null` and we would have returned early resulting
|
||||
// in not hitting this code path.
|
||||
let { compileFn } = designSystem.utilities.get(candidate.root)!
|
||||
|
||||
// Build the node
|
||||
let compiledNodes = compileFn(candidate)
|
||||
if (compiledNodes === undefined) {
|
||||
if (throwOnInvalidCandidate) {
|
||||
throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`)
|
||||
}
|
||||
continue next
|
||||
}
|
||||
|
||||
nodes = compiledNodes
|
||||
}
|
||||
|
||||
let propertySort = getPropertySort(nodes)
|
||||
|
||||
if (candidate.important) {
|
||||
applyImportant(nodes)
|
||||
}
|
||||
|
||||
let node: Rule = {
|
||||
kind: 'rule',
|
||||
selector: `.${escape(rawCandidate)}`,
|
||||
nodes,
|
||||
}
|
||||
|
||||
let variantOrder = 0n
|
||||
for (let variant of candidate.variants) {
|
||||
let result = applyVariant(node, variant, designSystem.variants)
|
||||
|
||||
// When the variant results in `null`, it means that the variant cannot be
|
||||
// applied to the rule. Discard the candidate and continue to the next
|
||||
// one.
|
||||
if (result === null) {
|
||||
if (throwOnInvalidCandidate) {
|
||||
throw new Error(`Cannot apply unknown utility class: ${rawCandidate}`)
|
||||
}
|
||||
continue next
|
||||
}
|
||||
|
||||
// Track the variant order which is a number with each bit representing a
|
||||
// variant. This allows us to sort the rules based on the order of
|
||||
// variants used.
|
||||
variantOrder |= 1n << BigInt(variants.indexOf(variant))
|
||||
}
|
||||
|
||||
nodeSorting.set(node, {
|
||||
properties: propertySort,
|
||||
variants: variantOrder,
|
||||
candidate: rawCandidate,
|
||||
})
|
||||
astNodes.push(node)
|
||||
}
|
||||
|
||||
astNodes.sort((a, z) => {
|
||||
// Safety: At this point it is safe to use TypeScript's non-null assertion
|
||||
// operator because if the ast nodes didn't exist, we introduced a bug
|
||||
// above, but there is no need to re-check just to be sure. If this relied
|
||||
// on pure user input, then we would need to check for its existence.
|
||||
let aSorting = nodeSorting.get(a)!
|
||||
let zSorting = nodeSorting.get(z)!
|
||||
|
||||
// Sort by variant order first
|
||||
if (aSorting.variants - zSorting.variants !== 0n) {
|
||||
return Number(aSorting.variants - zSorting.variants)
|
||||
}
|
||||
|
||||
// Find the first property that is different between the two rules
|
||||
let offset = 0
|
||||
while (
|
||||
aSorting.properties.length < offset &&
|
||||
zSorting.properties.length < offset &&
|
||||
aSorting.properties[offset] === zSorting.properties[offset]
|
||||
) {
|
||||
offset += 1
|
||||
}
|
||||
|
||||
return (
|
||||
// Sort by lowest property index first
|
||||
(aSorting.properties[offset] ?? Infinity) - (zSorting.properties[offset] ?? Infinity) ||
|
||||
// Sort by most properties first, then by least properties
|
||||
zSorting.properties.length - aSorting.properties.length
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
astNodes,
|
||||
nodeSorting,
|
||||
}
|
||||
}
|
||||
|
||||
export function applyVariant(node: Rule, variant: Variant, variants: Variants): null | void {
|
||||
if (variant.kind === 'arbitrary') {
|
||||
node.nodes = [rule(variant.selector, node.nodes)]
|
||||
return
|
||||
}
|
||||
|
||||
// Safety: At this point it is safe to use TypeScript's non-null assertion
|
||||
// operator because if the `candidate.root` didn't exist, `parseCandidate`
|
||||
// would have returned `null` and we would have returned early resulting in
|
||||
// not hitting this code path.
|
||||
let { applyFn } = variants.get(variant.root)!
|
||||
|
||||
if (variant.kind === 'compound') {
|
||||
let result = applyVariant(node, variant.variant, variants)
|
||||
if (result === null) return null
|
||||
|
||||
for (let child of node.nodes) {
|
||||
// Only some variants wrap children in rules. For example, the `force`
|
||||
// variant is a noop on the AST. And the `has` variant modifies the
|
||||
// selector rather than the children.
|
||||
//
|
||||
// This means `child` may be a declaration and we don't want to apply the
|
||||
// variant to it. This also means the entire variant as a whole is not
|
||||
// applicable to the rule and should generate nothing.
|
||||
if (child.kind !== 'rule') return null
|
||||
|
||||
let result = applyFn(child as Rule, variant)
|
||||
if (result === null) return null
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// All other variants
|
||||
let result = applyFn(node, variant)
|
||||
if (result === null) return null
|
||||
}
|
||||
|
||||
function applyImportant(ast: AstNode[]): void {
|
||||
for (let node of ast) {
|
||||
// Skip any `@at-root` rules — we don't want to make the contents of things
|
||||
// like `@keyframes` or `@property` important.
|
||||
if (node.kind === 'rule' && node.selector === '@at-root') {
|
||||
continue
|
||||
}
|
||||
|
||||
if (node.kind === 'declaration') {
|
||||
node.important = true
|
||||
} else if (node.kind === 'rule') {
|
||||
applyImportant(node.nodes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getPropertySort(nodes: AstNode[]) {
|
||||
// Determine sort order based on properties used
|
||||
let propertySort = new Set<number>()
|
||||
let q: AstNode[] = nodes.slice()
|
||||
|
||||
while (q.length > 0) {
|
||||
// Safety: At this point it is safe to use TypeScript's non-null assertion
|
||||
// operator because we guarded against `q.length > 0` above.
|
||||
let node = q.shift()!
|
||||
if (node.kind === 'declaration') {
|
||||
if (node.property === '--tw-sort') {
|
||||
let idx = GLOBAL_PROPERTY_ORDER.indexOf(node.value)
|
||||
if (idx !== -1) {
|
||||
propertySort.add(idx)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let idx = GLOBAL_PROPERTY_ORDER.indexOf(node.property)
|
||||
if (idx !== -1) propertySort.add(idx)
|
||||
} else if (node.kind === 'rule') {
|
||||
// Don't consider properties within `@at-root` when determining the sort
|
||||
// order for a rule.
|
||||
if (node.selector === '@at-root') continue
|
||||
|
||||
for (let child of node.nodes) {
|
||||
q.push(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(propertySort).sort((a, z) => a - z)
|
||||
}
|
||||
995
packages/tailwindcss/src/css-parser.test.ts
Normal file
995
packages/tailwindcss/src/css-parser.test.ts
Normal file
@ -0,0 +1,995 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parse } from './css-parser'
|
||||
|
||||
const css = String.raw
|
||||
|
||||
describe('comments', () => {
|
||||
it('should parse a comment and ignore it', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
/*Hello, world!*/
|
||||
`),
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('should parse a comment with an escaped ending and ignore it', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
/*Hello, \*\/ world!*/
|
||||
`),
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('should parse a comment inside of a selector and ignore it', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
.foo {
|
||||
/*Example comment*/
|
||||
}
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.foo',
|
||||
nodes: [],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should remove comments in between selectors while maintaining the correct whitespace', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
.foo/*.bar*/.baz {
|
||||
}
|
||||
.foo/*.bar*//*.baz*/.qux
|
||||
{
|
||||
}
|
||||
.foo/*.bar*/ /*.baz*/.qux {
|
||||
/* ^ whitespace */
|
||||
}
|
||||
.foo /*.bar*/.baz {
|
||||
/*^ whitespace */
|
||||
}
|
||||
.foo/*.bar*/ .baz {
|
||||
/* ^ whitespace */
|
||||
}
|
||||
.foo/*.bar*/
|
||||
.baz {
|
||||
}
|
||||
`),
|
||||
).toEqual([
|
||||
{ kind: 'rule', selector: '.foo.baz', nodes: [] },
|
||||
{ kind: 'rule', selector: '.foo.qux', nodes: [] },
|
||||
{ kind: 'rule', selector: '.foo .qux', nodes: [] },
|
||||
{ kind: 'rule', selector: '.foo .baz', nodes: [] },
|
||||
{ kind: 'rule', selector: '.foo .baz', nodes: [] },
|
||||
{ kind: 'rule', selector: '.foo .baz', nodes: [] },
|
||||
])
|
||||
})
|
||||
|
||||
it('should collect license comments', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
/*! License #1 */
|
||||
/*!
|
||||
* License #2
|
||||
*/
|
||||
`),
|
||||
).toEqual([
|
||||
{ kind: 'comment', value: '! License #1 ' },
|
||||
{
|
||||
kind: 'comment',
|
||||
value: `!
|
||||
* License #2
|
||||
`,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should hoist all license comments', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
/*! License #1 */
|
||||
.foo {
|
||||
color: red; /*! License #1.5 */
|
||||
}
|
||||
/*! License #2 */
|
||||
.bar {
|
||||
/*! License #2.5 */
|
||||
color: blue;
|
||||
}
|
||||
/*! License #3 */
|
||||
`),
|
||||
).toEqual([
|
||||
{ kind: 'comment', value: '! License #1 ' },
|
||||
{ kind: 'comment', value: '! License #1.5 ' },
|
||||
{ kind: 'comment', value: '! License #2 ' },
|
||||
{ kind: 'comment', value: '! License #2.5 ' },
|
||||
{ kind: 'comment', value: '! License #3 ' },
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.foo',
|
||||
nodes: [{ kind: 'declaration', property: 'color', value: 'red', important: false }],
|
||||
},
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.bar',
|
||||
nodes: [{ kind: 'declaration', property: 'color', value: 'blue', important: false }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should handle comments before element selectors', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
.dark /* comment */p {
|
||||
color: black;
|
||||
}
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.dark p',
|
||||
nodes: [
|
||||
{
|
||||
kind: 'declaration',
|
||||
property: 'color',
|
||||
value: 'black',
|
||||
important: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('declarations', () => {
|
||||
it('should parse a simple declaration', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
color: red;
|
||||
`),
|
||||
).toEqual([{ kind: 'declaration', property: 'color', value: 'red', important: false }])
|
||||
})
|
||||
|
||||
it('should parse declarations with strings', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
content: 'Hello, world!';
|
||||
`),
|
||||
).toEqual([
|
||||
{ kind: 'declaration', property: 'content', value: "'Hello, world!'", important: false },
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse declarations with nested strings', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
content: 'Good, "monday", morning!';
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'declaration',
|
||||
property: 'content',
|
||||
value: `'Good, "monday", morning!'`,
|
||||
important: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse declarations with nested strings that are not balanced', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
content: "It's a beautiful day!";
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'declaration',
|
||||
property: 'content',
|
||||
value: `"It's a beautiful day!"`,
|
||||
important: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse declarations with with strings and escaped string endings', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
content: 'These are not the end "\' of the string';
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'declaration',
|
||||
property: 'content',
|
||||
value: `'These are not the end \"\\' of the string'`,
|
||||
important: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
describe('important', () => {
|
||||
it('should parse declarations with `!important`', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
width: 123px !important;
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'declaration',
|
||||
property: 'width',
|
||||
value: '123px',
|
||||
important: true,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse declarations with `!important` when there is a trailing comment', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
width: 123px !important /* Very important */;
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'declaration',
|
||||
property: 'width',
|
||||
value: '123px',
|
||||
important: true,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Custom properties', () => {
|
||||
it('should parse a custom property', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
--foo: bar;
|
||||
`),
|
||||
).toEqual([{ kind: 'declaration', property: '--foo', value: 'bar', important: false }])
|
||||
})
|
||||
|
||||
it('should parse a minified custom property', () => {
|
||||
expect(parse(':root{--foo:bar;}')).toEqual([
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: ':root',
|
||||
nodes: [
|
||||
{
|
||||
kind: 'declaration',
|
||||
property: '--foo',
|
||||
value: 'bar',
|
||||
important: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse a minified custom property with no semicolon ', () => {
|
||||
expect(parse(':root{--foo:bar}')).toEqual([
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: ':root',
|
||||
nodes: [
|
||||
{
|
||||
kind: 'declaration',
|
||||
property: '--foo',
|
||||
value: 'bar',
|
||||
important: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse a custom property with a missing ending `;`', () => {
|
||||
expect(
|
||||
parse(`
|
||||
--foo: bar
|
||||
`),
|
||||
).toEqual([{ kind: 'declaration', property: '--foo', value: 'bar', important: false }])
|
||||
})
|
||||
|
||||
it('should parse a custom property with a missing ending `;` and `!important`', () => {
|
||||
expect(
|
||||
parse(`
|
||||
--foo: bar !important
|
||||
`),
|
||||
).toEqual([{ kind: 'declaration', property: '--foo', value: 'bar', important: true }])
|
||||
})
|
||||
|
||||
it('should parse a custom property with an embedded programming language', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
--foo: if(x > 5) this.width = 10;
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'declaration',
|
||||
property: '--foo',
|
||||
value: 'if(x > 5) this.width = 10',
|
||||
important: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse a custom property with an empty block as the value', () => {
|
||||
expect(parse('--foo: {};')).toEqual([
|
||||
{
|
||||
kind: 'declaration',
|
||||
property: '--foo',
|
||||
value: '{}',
|
||||
important: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse a custom property with a block including nested "css"', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
--foo: {
|
||||
background-color: red;
|
||||
/* A comment */
|
||||
content: 'Hello, world!';
|
||||
};
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'declaration',
|
||||
property: '--foo',
|
||||
value: `{
|
||||
background-color: red;
|
||||
/* A comment */
|
||||
content: 'Hello, world!';
|
||||
}`,
|
||||
important: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse a custom property with a block including nested "css" and comments with end characters inside them', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
--foo: {
|
||||
background-color: red;
|
||||
/* A comment ; */
|
||||
content: 'Hello, world!';
|
||||
};
|
||||
--bar: {
|
||||
background-color: red;
|
||||
/* A comment } */
|
||||
content: 'Hello, world!';
|
||||
};
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'declaration',
|
||||
property: '--foo',
|
||||
value: `{
|
||||
background-color: red;
|
||||
/* A comment ; */
|
||||
content: 'Hello, world!';
|
||||
}`,
|
||||
important: false,
|
||||
},
|
||||
{
|
||||
kind: 'declaration',
|
||||
property: '--bar',
|
||||
value: `{
|
||||
background-color: red;
|
||||
/* A comment } */
|
||||
content: 'Hello, world!';
|
||||
}`,
|
||||
important: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse a custom property with escaped characters in the value', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
--foo: This is not the end \;, but this is;
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'declaration',
|
||||
property: '--foo',
|
||||
value: 'This is not the end \\;, but this is',
|
||||
important: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse a custom property with escaped characters inside a comment in the value', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
--foo: /* This is not the end \; this is also not the end ; */ but this is;
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'declaration',
|
||||
property: '--foo',
|
||||
value: '/* This is not the end \\; this is also not the end ; */ but this is',
|
||||
important: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse empty custom properties', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
--foo: ;
|
||||
`),
|
||||
).toEqual([{ kind: 'declaration', property: '--foo', value: '', important: false }])
|
||||
})
|
||||
|
||||
it('should parse custom properties with `!important`', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
--foo: bar !important;
|
||||
`),
|
||||
).toEqual([{ kind: 'declaration', property: '--foo', value: 'bar', important: true }])
|
||||
})
|
||||
})
|
||||
|
||||
it('should parse multiple declarations', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
color: red;
|
||||
background-color: blue;
|
||||
`),
|
||||
).toEqual([
|
||||
{ kind: 'declaration', property: 'color', value: 'red', important: false },
|
||||
{ kind: 'declaration', property: 'background-color', value: 'blue', important: false },
|
||||
])
|
||||
})
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
it('should correctly parse comments with `:` inside of them', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
color/* color: #f00; */: red;
|
||||
font-weight:/* font-size: 12px */ bold;
|
||||
|
||||
.foo {
|
||||
background-color/* background-color: #f00; */: red;
|
||||
}
|
||||
`),
|
||||
).toEqual([
|
||||
{ kind: 'declaration', property: 'color', value: 'red', important: false },
|
||||
{ kind: 'declaration', property: 'font-weight', value: 'bold', important: false },
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.foo',
|
||||
nodes: [
|
||||
{
|
||||
kind: 'declaration',
|
||||
property: 'background-color',
|
||||
value: 'red',
|
||||
important: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse mutlti-line declarations', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
.foo {
|
||||
grid-template-areas:
|
||||
'header header header'
|
||||
'sidebar main main'
|
||||
'footer footer footer';
|
||||
}
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.foo',
|
||||
nodes: [
|
||||
{
|
||||
kind: 'declaration',
|
||||
property: 'grid-template-areas',
|
||||
value: "'header header header' 'sidebar main main' 'footer footer footer'",
|
||||
important: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('selectors', () => {
|
||||
it('should parse a simple selector', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
.foo {
|
||||
}
|
||||
`),
|
||||
).toEqual([{ kind: 'rule', selector: '.foo', nodes: [] }])
|
||||
})
|
||||
|
||||
it('should parse selectors with escaped characters', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
.hover\:foo:hover {
|
||||
}
|
||||
.\32 xl\:foo {
|
||||
}
|
||||
`),
|
||||
).toEqual([
|
||||
{ kind: 'rule', selector: '.hover\\:foo:hover', nodes: [] },
|
||||
{ kind: 'rule', selector: '.\\32 xl\\:foo', nodes: [] },
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse multiple simple selectors', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
.foo,
|
||||
.bar {
|
||||
}
|
||||
`),
|
||||
).toEqual([{ kind: 'rule', selector: '.foo, .bar', nodes: [] }])
|
||||
})
|
||||
|
||||
it('should parse multiple declarations inside of a selector', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
.foo {
|
||||
color: red;
|
||||
font-size: 16px;
|
||||
}
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.foo',
|
||||
nodes: [
|
||||
{ kind: 'declaration', property: 'color', value: 'red', important: false },
|
||||
{ kind: 'declaration', property: 'font-size', value: '16px', important: false },
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse rules with declarations that end with a missing `;`', () => {
|
||||
expect(
|
||||
parse(`
|
||||
.foo {
|
||||
color: red;
|
||||
font-size: 16px
|
||||
}
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.foo',
|
||||
nodes: [
|
||||
{ kind: 'declaration', property: 'color', value: 'red', important: false },
|
||||
{ kind: 'declaration', property: 'font-size', value: '16px', important: false },
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse rules with declarations that end with a missing `;` and `!important`', () => {
|
||||
expect(
|
||||
parse(`
|
||||
.foo {
|
||||
color: red;
|
||||
font-size: 16px !important
|
||||
}
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.foo',
|
||||
nodes: [
|
||||
{ kind: 'declaration', property: 'color', value: 'red', important: false },
|
||||
{ kind: 'declaration', property: 'font-size', value: '16px', important: true },
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('at-rules', () => {
|
||||
it('should parse an at-rule without a block', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
@charset "UTF-8";
|
||||
`),
|
||||
).toEqual([{ kind: 'rule', selector: '@charset "UTF-8"', nodes: [] }])
|
||||
})
|
||||
|
||||
it("should parse an at-rule without a block or semicolon when it's the last rule in a block", () => {
|
||||
expect(
|
||||
parse(`
|
||||
@layer utilities {
|
||||
@tailwind utilities
|
||||
}
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '@layer utilities',
|
||||
nodes: [{ kind: 'rule', selector: '@tailwind utilities', nodes: [] }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse a nested at-rule without a block', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
@layer utilities {
|
||||
@charset "UTF-8";
|
||||
}
|
||||
|
||||
.foo {
|
||||
@apply font-bold hover:text-red-500;
|
||||
}
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '@layer utilities',
|
||||
nodes: [{ kind: 'rule', selector: '@charset "UTF-8"', nodes: [] }],
|
||||
},
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.foo',
|
||||
nodes: [{ kind: 'rule', selector: '@apply font-bold hover:text-red-500', nodes: [] }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse custom at-rules without a block', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
@tailwind;
|
||||
@tailwind base;
|
||||
`),
|
||||
).toEqual([
|
||||
{ kind: 'rule', selector: '@tailwind', nodes: [] },
|
||||
{ kind: 'rule', selector: '@tailwind base', nodes: [] },
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse (nested) media queries', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
@media (width >= 600px) {
|
||||
.foo {
|
||||
color: red;
|
||||
@media (width >= 800px) {
|
||||
color: blue;
|
||||
}
|
||||
@media (width >= 1000px) {
|
||||
color: green;
|
||||
}
|
||||
}
|
||||
}
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '@media (width >= 600px)',
|
||||
nodes: [
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.foo',
|
||||
nodes: [
|
||||
{ kind: 'declaration', property: 'color', value: 'red', important: false },
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '@media (width >= 800px)',
|
||||
nodes: [
|
||||
{ kind: 'declaration', property: 'color', value: 'blue', important: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '@media (width >= 1000px)',
|
||||
nodes: [
|
||||
{ kind: 'declaration', property: 'color', value: 'green', important: false },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse at-rules that span multiple lines', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
.foo {
|
||||
@apply hover:text-red-100
|
||||
sm:hover:text-red-200
|
||||
md:hover:text-red-300
|
||||
lg:hover:text-red-400
|
||||
xl:hover:text-red-500;
|
||||
}
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'rule',
|
||||
nodes: [
|
||||
{
|
||||
kind: 'rule',
|
||||
nodes: [],
|
||||
selector:
|
||||
'@apply hover:text-red-100 sm:hover:text-red-200 md:hover:text-red-300 lg:hover:text-red-400 xl:hover:text-red-500',
|
||||
},
|
||||
],
|
||||
selector: '.foo',
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('nesting', () => {
|
||||
it('should parse nested rules', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
.foo {
|
||||
.bar {
|
||||
.baz {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.foo',
|
||||
nodes: [
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.bar',
|
||||
nodes: [
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.baz',
|
||||
nodes: [{ kind: 'declaration', property: 'color', value: 'red', important: false }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse nested selector with `&`', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
.foo {
|
||||
color: red;
|
||||
|
||||
&:hover {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.foo',
|
||||
nodes: [
|
||||
{ kind: 'declaration', property: 'color', value: 'red', important: false },
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '&:hover',
|
||||
nodes: [{ kind: 'declaration', property: 'color', value: 'blue', important: false }],
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse nested sibling selectors', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
.foo {
|
||||
.bar {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.baz {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.foo',
|
||||
nodes: [
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.bar',
|
||||
nodes: [{ kind: 'declaration', property: 'color', value: 'red', important: false }],
|
||||
},
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.baz',
|
||||
nodes: [{ kind: 'declaration', property: 'color', value: 'blue', important: false }],
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse nested sibling selectors and sibling declarations', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
.foo {
|
||||
font-weight: bold;
|
||||
text-declaration-line: underline;
|
||||
|
||||
.bar {
|
||||
color: red;
|
||||
}
|
||||
|
||||
--in-between: 1;
|
||||
|
||||
.baz {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
--at-the-end: 2;
|
||||
}
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.foo',
|
||||
nodes: [
|
||||
{ kind: 'declaration', property: 'font-weight', value: 'bold', important: false },
|
||||
{
|
||||
kind: 'declaration',
|
||||
property: 'text-declaration-line',
|
||||
value: 'underline',
|
||||
important: false,
|
||||
},
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.bar',
|
||||
nodes: [{ kind: 'declaration', property: 'color', value: 'red', important: false }],
|
||||
},
|
||||
{ kind: 'declaration', property: '--in-between', value: '1', important: false },
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.baz',
|
||||
nodes: [{ kind: 'declaration', property: 'color', value: 'blue', important: false }],
|
||||
},
|
||||
{ kind: 'declaration', property: '--at-the-end', value: '2', important: false },
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('complex', () => {
|
||||
it('should parse complex examples', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
@custom \{ {
|
||||
foo: bar;
|
||||
}
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '@custom \\{',
|
||||
nodes: [{ kind: 'declaration', property: 'foo', value: 'bar', important: false }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse minified nested CSS', () => {
|
||||
expect(
|
||||
parse('.foo{color:red;@media(width>=600px){.bar{color:blue;font-weight:bold}}}'),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.foo',
|
||||
nodes: [
|
||||
{ kind: 'declaration', property: 'color', value: 'red', important: false },
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '@media(width>=600px)',
|
||||
nodes: [
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.bar',
|
||||
nodes: [
|
||||
{ kind: 'declaration', property: 'color', value: 'blue', important: false },
|
||||
{ kind: 'declaration', property: 'font-weight', value: 'bold', important: false },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should ignore everything inside of comments', () => {
|
||||
expect(
|
||||
parse(css`
|
||||
.foo:has(.bar /* instead \*\/ of .baz { */) {
|
||||
color: red;
|
||||
}
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'rule',
|
||||
selector: '.foo:has(.bar )',
|
||||
nodes: [{ kind: 'declaration', property: 'color', value: 'red', important: false }],
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('errors', () => {
|
||||
it('should error when curly brackets are unbalanced (opening)', () => {
|
||||
expect(() =>
|
||||
parse(`
|
||||
.foo {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.bar
|
||||
/* ^ Missing opening { */
|
||||
color: blue;
|
||||
}
|
||||
`),
|
||||
).toThrowErrorMatchingInlineSnapshot(`[Error: Missing opening {]`)
|
||||
})
|
||||
|
||||
it('should error when curly brackets are unbalanced (closing)', () => {
|
||||
expect(() =>
|
||||
parse(`
|
||||
.foo {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.bar {
|
||||
color: blue;
|
||||
|
||||
/* ^ Missing closing } */
|
||||
`),
|
||||
).toThrowErrorMatchingInlineSnapshot(`[Error: Missing closing } at .bar]`)
|
||||
})
|
||||
|
||||
it('should error when an unterminated string is used', () => {
|
||||
expect(() =>
|
||||
parse(css`
|
||||
.foo {
|
||||
content: "Hello world!
|
||||
/* ^ missing " */
|
||||
font-weight: bold;
|
||||
}
|
||||
`),
|
||||
).toThrowErrorMatchingInlineSnapshot(`[Error: Unterminated string: "Hello world!"]`)
|
||||
})
|
||||
|
||||
it('should error when an unterminated string is used with a `;`', () => {
|
||||
expect(() =>
|
||||
parse(css`
|
||||
.foo {
|
||||
content: "Hello world!;
|
||||
/* ^ missing " */
|
||||
font-weight: bold;
|
||||
}
|
||||
`),
|
||||
).toThrowErrorMatchingInlineSnapshot(`[Error: Unterminated string: "Hello world!;"]`)
|
||||
})
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user