Compare commits
85 commits
main
...
css-theme-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb7ba4f44a | ||
|
|
54ce83bb86 | ||
|
|
ad014a2ec4 | ||
|
|
1797de2042 | ||
|
|
cb4fd4fcec | ||
|
|
d58afc60d2 | ||
|
|
ed7c448441 | ||
|
|
d88fd1e6e7 | ||
|
|
42699086c3 | ||
|
|
a347719dc8 | ||
|
|
526276cb97 | ||
|
|
bf583334a2 | ||
|
|
e83435295d | ||
|
|
67fc3c30e5 | ||
|
|
7573cc7d2a | ||
|
|
971253b1cf | ||
|
|
b57d3d1bc5 | ||
|
|
665569d626 | ||
|
|
dfb0958147 | ||
|
|
4ae6f06466 | ||
|
|
8809c6a7fb | ||
|
|
3c38f0a391 | ||
|
|
ba7d3be00e | ||
|
|
a5305aa84e | ||
|
|
776d5bf614 | ||
|
|
ac6e8733e9 | ||
|
|
a7a1f9347c | ||
|
|
86f9c6f092 | ||
|
|
3be258c218 | ||
|
|
ef2ebb28d5 | ||
|
|
ea3d9cd57a | ||
|
|
e51b09ca65 | ||
|
|
eb8d9294c5 | ||
|
|
55daf8e0f5 | ||
|
|
e572dc645a | ||
|
|
571c27e1f1 | ||
|
|
fd74f04437 | ||
|
|
f661c0371c | ||
|
|
729dafeb82 | ||
|
|
b05b617601 | ||
|
|
9794b83985 | ||
|
|
5ddd9c4db6 | ||
|
|
5c7a8e98ea | ||
|
|
352c563d99 | ||
|
|
ecebde1d52 | ||
|
|
c9138b2e0b | ||
|
|
cb1a0e1a95 | ||
|
|
e1d4955431 | ||
|
|
69d86873c3 | ||
|
|
7248f2121e | ||
|
|
820815be0b | ||
|
|
8719be3944 | ||
|
|
fd500fcd0c | ||
|
|
0917fcaa33 | ||
|
|
04c4de1997 | ||
|
|
88ab01c7f8 | ||
|
|
49400f90f1 | ||
|
|
581cf6abcc | ||
|
|
0af64ee3e8 | ||
|
|
9ebaaaccb3 | ||
|
|
f49b054b7d | ||
|
|
0e171fd2dd | ||
|
|
764f6a19d4 | ||
|
|
2f21b22049 | ||
|
|
dce8280d76 | ||
|
|
80415ece5e | ||
|
|
ae5d746226 | ||
|
|
8af8e0d1e6 | ||
|
|
3e015818d6 | ||
|
|
9b6047d639 | ||
|
|
9799e88244 | ||
|
|
a4314c052b | ||
|
|
3309d3f635 | ||
|
|
225a3fa3c6 | ||
|
|
71205d8fcb | ||
|
|
f8506be208 | ||
|
|
7dbb59c9db | ||
|
|
251cc37d7d | ||
|
|
960cb40c6b | ||
|
|
cdca9cbcfe | ||
|
|
9a57badc9f | ||
|
|
7b3fe8de0f | ||
|
|
1f74b14513 | ||
|
|
1f1a6faf2c | ||
|
|
ef056a57d3 |
217 changed files with 33625 additions and 51610 deletions
|
|
@ -7,3 +7,11 @@ end_of_line = lf
|
|||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.rs]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
|
|
|||
59
.github/workflows/ci-stable.yml
vendored
59
.github/workflows/ci-stable.yml
vendored
|
|
@ -1,59 +0,0 @@
|
|||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: CI — Stable
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master, 3.3]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CI: true
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
CACHE_PREFIX: stable
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14, 18]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Use the `stable` engine
|
||||
run: |
|
||||
node ./scripts/swap-engines.js
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-${{ matrix.node-version }}-${{ env.CACHE_PREFIX }}-node_modules-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Build Tailwind CSS
|
||||
run: npm run build
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
npm run test || \
|
||||
npm run test || \
|
||||
npm run test || exit 1
|
||||
|
||||
- name: Lint
|
||||
run: npm run style
|
||||
33
.github/workflows/ci.yml
vendored
33
.github/workflows/ci.yml
vendored
|
|
@ -1,10 +1,10 @@
|
|||
name: CI — Oxide
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, 3.3]
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master, 3.3]
|
||||
branches: [master]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
@ -13,7 +13,6 @@ env:
|
|||
CI: true
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
CACHE_PREFIX: oxide
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
@ -34,7 +33,7 @@ jobs:
|
|||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-${{ matrix.node-version }}-${{ env.CACHE_PREFIX }}-node_modules-${{ hashFiles('**/package-lock.json') }}
|
||||
key: ${{ runner.os }}-${{ matrix.node-version }}-node_modules-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
|
|
@ -62,14 +61,36 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Check versions
|
||||
run: |
|
||||
echo "Node:" `node --version`
|
||||
echo "NPM:" `npm --version`
|
||||
echo "Rust:" `rustc --version`
|
||||
echo "Cargo:" `cargo --version`
|
||||
|
||||
- name: Build Tailwind CSS
|
||||
run: npx turbo run build --filter=//
|
||||
|
||||
- name: Test
|
||||
- name: Test (Node.js)
|
||||
run: |
|
||||
npx turbo run test --filter=// || \
|
||||
npx turbo run test --filter=// || \
|
||||
npx turbo run test --filter=// || exit 1
|
||||
|
||||
- name: Test (Rust)
|
||||
run: |
|
||||
cd ./oxide
|
||||
cargo test || \
|
||||
cargo test || \
|
||||
cargo test || exit 1
|
||||
cd -
|
||||
|
||||
- name: Lint
|
||||
run: npx turbo run style --filter=//
|
||||
|
|
|
|||
83
.github/workflows/integration-tests-stable.yml
vendored
83
.github/workflows/integration-tests-stable.yml
vendored
|
|
@ -1,83 +0,0 @@
|
|||
name: Integration Tests — Stable
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, 3.3]
|
||||
pull_request:
|
||||
branches: [master, 3.3]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CI: true
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
CACHE_PREFIX: stable
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
integration:
|
||||
- content-resolution
|
||||
- parcel
|
||||
- postcss-cli
|
||||
- rollup
|
||||
- rollup-sass
|
||||
- tailwindcss-cli
|
||||
- vite
|
||||
- webpack-4
|
||||
- webpack-5
|
||||
node-version: [18]
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Use the `stable` engine
|
||||
run: |
|
||||
node ./scripts/swap-engines.js
|
||||
|
||||
- name: Cache node_modules (tailwindcss)
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-${{ matrix.node-version }}-${{ env.CACHE_PREFIX }}-node_modules-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Cache node_modules (integrations)
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ./integrations/node_modules
|
||||
key: ${{ runner.os }}-${{ matrix.node-version }}-${{ env.CACHE_PREFIX }}-node_modules-integrations-${{ hashFiles('./integrations/package-lock.json') }}
|
||||
|
||||
- name: Cache node_modules (integration)
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ./integrations/${{ matrix.integration }}/node_modules
|
||||
key: ${{ runner.os }}-${{ matrix.node-version }}-${{ env.CACHE_PREFIX }}-node_modules-integrations-${{ matrix.integration }}-${{ hashFiles('./integrations/${{ matrix.integration }}/package-lock.json') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Install dependencies (shared integrations)
|
||||
run: npm install --prefix ./integrations
|
||||
|
||||
- name: Install dependencies (integration)
|
||||
run: npm install --prefix ./integrations/${{ matrix.integration }}
|
||||
|
||||
- name: Build Tailwind CSS
|
||||
run: npm run build
|
||||
|
||||
- name: Test ${{ matrix.integration }}
|
||||
run: |
|
||||
npm run test --prefix ./integrations/${{ matrix.integration }} || \
|
||||
npm run test --prefix ./integrations/${{ matrix.integration }} || \
|
||||
npm run test --prefix ./integrations/${{ matrix.integration }} || exit 1
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
name: Integration Tests — Oxide
|
||||
name: Integration Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, 3.3]
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master, 3.3]
|
||||
branches: [master]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
@ -13,7 +13,6 @@ env:
|
|||
CI: true
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
CACHE_PREFIX: oxide
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
|
@ -46,7 +45,7 @@ jobs:
|
|||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-${{ matrix.node-version }}-${{ env.CACHE_PREFIX }}-node_modules-${{ hashFiles('**/package-lock.json') }}
|
||||
key: ${{ runner.os }}-${{ matrix.node-version }}-node_modules-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
|
|
@ -74,6 +73,20 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Check versions
|
||||
run: |
|
||||
echo "Node:" `node --version`
|
||||
echo "NPM:" `npm --version`
|
||||
echo "Rust:" `rustc --version`
|
||||
echo "Cargo:" `cargo --version`
|
||||
|
||||
- name: Build Tailwind CSS
|
||||
run: npx turbo run build --filter=//
|
||||
|
||||
14
.github/workflows/prepare-release.yml
vendored
14
.github/workflows/prepare-release.yml
vendored
|
|
@ -53,6 +53,20 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Check versions
|
||||
run: |
|
||||
echo "Node:" `node --version`
|
||||
echo "NPM:" `npm --version`
|
||||
echo "Rust:" `rustc --version`
|
||||
echo "Cargo:" `cargo --version`
|
||||
|
||||
- name: Build Tailwind CSS
|
||||
run: npm run build
|
||||
|
||||
|
|
|
|||
78
.github/workflows/release-insiders-stable.yml
vendored
78
.github/workflows/release-insiders-stable.yml
vendored
|
|
@ -1,78 +0,0 @@
|
|||
name: Release Insiders — Stable
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CI: true
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
CACHE_PREFIX: stable
|
||||
NODE_VERSION: 16
|
||||
RELEASE_CHANNEL: insiders
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Use the `stable` engine
|
||||
run: |
|
||||
node ./scripts/swap-engines.js
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-${{ env.NODE_VERSION }}-${{ env.CACHE_PREFIX }}-node_modules-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Build Tailwind CSS
|
||||
run: npm run build
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
npm run test || \
|
||||
npm run test || \
|
||||
npm run test || exit 1
|
||||
|
||||
- name: Resolve version
|
||||
id: vars
|
||||
run: |
|
||||
echo "SHA_SHORT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
|
||||
- name: 'Version based on commit: 0.0.0-${{ env.RELEASE_CHANNEL }}.${{ env.SHA_SHORT }}'
|
||||
run: npm version 0.0.0-${{ env.RELEASE_CHANNEL }}.${{ env.SHA_SHORT }} --force --no-git-tag-version
|
||||
|
||||
- name: Publish
|
||||
run: npm publish --tag ${{ env.RELEASE_CHANNEL }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Trigger Tailwind Play update
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }}
|
||||
script: |
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: 'tailwindlabs',
|
||||
repo: 'play.tailwindcss.com',
|
||||
ref: 'master',
|
||||
workflow_id: 'upgrade-tailwindcss.yml',
|
||||
inputs: {
|
||||
insidersVersion: '0.0.0-${{ env.RELEASE_CHANNEL }}.${{ env.SHA_SHORT }}'
|
||||
}
|
||||
})
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
name: Release Insiders — Oxide
|
||||
name: Release Insiders
|
||||
|
||||
on:
|
||||
push:
|
||||
|
|
@ -8,7 +8,7 @@ env:
|
|||
APP_NAME: tailwindcss-oxide
|
||||
NODE_VERSION: 16
|
||||
OXIDE_LOCATION: ./oxide/crates/node
|
||||
RELEASE_CHANNEL: oxide-insiders
|
||||
RELEASE_CHANNEL: insiders
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
@ -170,15 +170,13 @@ jobs:
|
|||
include:
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
strip: strip
|
||||
image: docker.io/centos/nodejs-14-centos7
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
|
||||
- target: aarch64-unknown-linux-gnu
|
||||
strip: aarch64-linux-gnu-strip
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
|
||||
setup: apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu -y
|
||||
strip: llvm-strip
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
|
||||
- target: armv7-unknown-linux-gnueabihf
|
||||
strip: arm-linux-gnueabihf-strip
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
|
||||
setup: apt install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf -y
|
||||
strip: llvm-strip
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-zig
|
||||
- target: aarch64-unknown-linux-musl
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
|
||||
strip: aarch64-linux-musl-strip
|
||||
|
|
@ -353,7 +351,7 @@ jobs:
|
|||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
tailwind_oxide_release:
|
||||
tailwind-release:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build and release Tailwind CSS
|
||||
|
||||
68
.github/workflows/release-stable.yml
vendored
68
.github/workflows/release-stable.yml
vendored
|
|
@ -1,68 +0,0 @@
|
|||
name: Release — Stable
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CI: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Use the `stable` engine
|
||||
run: |
|
||||
node ./scripts/swap-engines.js
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Build Tailwind CSS
|
||||
run: npm run build
|
||||
|
||||
- name: Test
|
||||
run: npm run test
|
||||
|
||||
- name: Calculate environment variables
|
||||
run: |
|
||||
echo "RELEASE_CHANNEL=$(node ./scripts/release-channel.js)" >> $GITHUB_ENV
|
||||
echo "TAILWINDCSS_VERSION=$(node -e 'console.log(require(`./package.json`).version);')" >> $GITHUB_ENV
|
||||
|
||||
- name: Publish
|
||||
run: npm publish --tag ${{ env.RELEASE_CHANNEL }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Trigger Tailwind Play update
|
||||
if: env.RELEASE_CHANNEL == 'latest'
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }}
|
||||
script: |
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: 'tailwindlabs',
|
||||
repo: 'play.tailwindcss.com',
|
||||
ref: 'master',
|
||||
workflow_id: 'upgrade-tailwindcss.yml',
|
||||
inputs: {
|
||||
version: '${{ env.TAILWINDCSS_VERSION }}'
|
||||
}
|
||||
})
|
||||
|
|
@ -1,15 +1,19 @@
|
|||
name: Release — Oxide
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
APP_NAME: tailwindcss-oxide
|
||||
NODE_VERSION: 14
|
||||
NODE_VERSION: 16
|
||||
OXIDE_LOCATION: ./oxide/crates/node
|
||||
|
||||
jobs:
|
||||
oxide-build:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
|
@ -27,6 +31,35 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ env.NODE_VERSION }}-node_modules-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
# Cache the `oxide` Rust build
|
||||
- name: Cache oxide build
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
./oxide/target/
|
||||
./oxide/crates/node/*.node
|
||||
./oxide/crates/node/index.js
|
||||
./oxide/crates/node/index.d.ts
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./oxide/crates/**/*') }}
|
||||
|
||||
- name: Install Node.JS
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
|
|
@ -42,18 +75,8 @@ jobs:
|
|||
- name: Setup rust target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
# - name: Use cached oxide node_modules
|
||||
# id: cache-oxide
|
||||
# uses: actions/cache@v3
|
||||
# with:
|
||||
# path: ${{ env.OXIDE_LOCATION }}/node_modules
|
||||
# key: nodeModules-${{ hashFiles('${{ env.OXIDE_LOCATION }}/package-lock.json') }}-${{ matrix.os }}-${{ matrix.target }}-${{ env.NODE_VERSION }}
|
||||
# restore-keys: |
|
||||
# nodeModules-
|
||||
|
||||
- name: Oxide — npm install
|
||||
# if: steps.cache-oxide.outputs.cache-hit != 'true'
|
||||
run: cd ${{ env.OXIDE_LOCATION }} && npm install
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Build release
|
||||
run: cd ${{ env.OXIDE_LOCATION }} && npm run build
|
||||
|
|
@ -72,7 +95,7 @@ jobs:
|
|||
${{ env.OXIDE_LOCATION }}/*.node
|
||||
${{ matrix.binary }}
|
||||
|
||||
oxide-build-apple-silicon:
|
||||
build-apple-silicon:
|
||||
name: Build Apple Silicon (OXIDE)
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
|
|
@ -82,7 +105,7 @@ jobs:
|
|||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-${{ matrix.node-version }}-node_modules-${{ hashFiles('**/package-lock.json') }}
|
||||
key: ${{ runner.os }}-${{ env.NODE_VERSION }}-node_modules-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
|
|
@ -94,7 +117,18 @@ jobs:
|
|||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('./oxide/**/Cargo.lock') }}
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
# Cache the `oxide` Rust build
|
||||
- name: Cache oxide build
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
./oxide/target/
|
||||
./oxide/crates/node/*.node
|
||||
./oxide/crates/node/index.js
|
||||
./oxide/crates/node/index.d.ts
|
||||
key: ${{ runner.os }}-oxide-${{ hashFiles('./oxide/crates/**/*') }}
|
||||
|
||||
- name: Install Node.JS
|
||||
uses: actions/setup-node@v3
|
||||
|
|
@ -131,22 +165,20 @@ jobs:
|
|||
${{ env.OXIDE_LOCATION }}/*.node
|
||||
${{ env.APP_NAME }}
|
||||
|
||||
oxide-build-linux:
|
||||
build-linux:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
strip: strip
|
||||
image: docker.io/centos/nodejs-14-centos7
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
|
||||
- target: aarch64-unknown-linux-gnu
|
||||
strip: aarch64-linux-gnu-strip
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
|
||||
setup: apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu -y
|
||||
strip: llvm-strip
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
|
||||
- target: armv7-unknown-linux-gnueabihf
|
||||
strip: arm-linux-gnueabihf-strip
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
|
||||
setup: apt install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf -y
|
||||
strip: llvm-strip
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-zig
|
||||
- target: aarch64-unknown-linux-musl
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
|
||||
strip: aarch64-linux-musl-strip
|
||||
|
|
@ -162,6 +194,35 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ env.NODE_VERSION }}-node_modules-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
# Cache the `oxide` Rust build
|
||||
- name: Cache oxide build
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
./oxide/target/
|
||||
./oxide/crates/node/*.node
|
||||
./oxide/crates/node/index.js
|
||||
./oxide/crates/node/index.d.ts
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./oxide/crates/**/*') }}
|
||||
|
||||
- name: Install Node.JS
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
|
|
@ -181,18 +242,8 @@ jobs:
|
|||
- name: Setup rust target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
# - name: Use cached oxide node_modules
|
||||
# id: cache-oxide
|
||||
# uses: actions/cache@v3
|
||||
# with:
|
||||
# path: ${{ env.OXIDE_LOCATION }}/node_modules
|
||||
# key: nodeModules-${{ hashFiles('${{ env.OXIDE_LOCATION }}/package-lock.json') }}-linux-${{ matrix.target }}-${{ env.NODE_VERSION }}
|
||||
# restore-keys: |
|
||||
# nodeModules-
|
||||
|
||||
- name: Oxide — npm install
|
||||
# if: steps.cache-oxide.outputs.cache-hit != 'true'
|
||||
run: cd ${{ env.OXIDE_LOCATION }} && npm install
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Build release
|
||||
run: cd ${{ env.OXIDE_LOCATION }} && npm run build
|
||||
|
|
@ -211,28 +262,52 @@ jobs:
|
|||
${{ env.OXIDE_LOCATION }}/*.node
|
||||
${{ env.APP_NAME }}
|
||||
|
||||
oxide-release:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build and release
|
||||
needs:
|
||||
- oxide-build
|
||||
- oxide-build-linux
|
||||
- oxide-build-apple-silicon
|
||||
- build
|
||||
- build-linux
|
||||
- build-apple-silicon
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
# - name: Use cached oxide node_modules
|
||||
# id: cache-oxide
|
||||
# uses: actions/cache@v3
|
||||
# with:
|
||||
# path: ${{ env.OXIDE_LOCATION }}/node_modules
|
||||
# key: nodeModules-${{ hashFiles('${{ env.OXIDE_LOCATION }}/package-lock.json') }}-ubuntu-latest-${{ env.NODE_VERSION }}
|
||||
# restore-keys: |
|
||||
# nodeModules-
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-${{ env.NODE_VERSION }}-node_modules-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
- name: Oxide — npm install
|
||||
# if: steps.cache-oxide.outputs.cache-hit != 'true'
|
||||
run: cd ${{ env.OXIDE_LOCATION }} && npm install
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
# Cache the `oxide` Rust build
|
||||
- name: Cache oxide build
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
./oxide/target/
|
||||
./oxide/crates/node/*.node
|
||||
./oxide/crates/node/index.js
|
||||
./oxide/crates/node/index.d.ts
|
||||
key: ${{ runner.os }}-oxide-${{ hashFiles('./oxide/crates/**/*') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Calculate environment variables
|
||||
run: |
|
||||
echo "RELEASE_CHANNEL=$(node ./scripts/release-channel.js)" >> $GITHUB_ENV
|
||||
echo "VERSION=$(node -e 'console.log(require(`./package.json`).version);')" >> $GITHUB_ENV
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
|
|
@ -255,11 +330,6 @@ jobs:
|
|||
cp bindings-x86_64-unknown-linux-gnu/oxide/crates/node/* ./npm/linux-x64-gnu/
|
||||
cp bindings-x86_64-unknown-linux-musl/oxide/crates/node/* ./npm/linux-x64-musl/
|
||||
|
||||
- name: Resolve version
|
||||
id: vars
|
||||
run: |
|
||||
echo "NPM_VERSION=0.0.0-insiders.$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate entry point
|
||||
run: npm run build --prefix ${{ env.OXIDE_LOCATION }}
|
||||
|
||||
|
|
@ -268,50 +338,74 @@ jobs:
|
|||
for pkg in ${{ env.OXIDE_LOCATION }}/npm/*; do
|
||||
echo "Publishing $pkg..."
|
||||
cd $pkg
|
||||
npm version ${{ env.NPM_VERSION }} --force --no-git-tag-version
|
||||
npm publish --tag insiders --access public
|
||||
npm version ${{ env.VERSION }} --force --no-git-tag-version
|
||||
npm publish --tag ${{ env.RELEASE_CHANNEL }} --access public
|
||||
cd -
|
||||
done
|
||||
|
||||
echo "Publishing ${{ env.APP_NAME }}...";
|
||||
cd ${{ env.OXIDE_LOCATION }}
|
||||
npm version ${{ env.NPM_VERSION }} --force --no-git-tag-version
|
||||
sed "s#\"0.0.0\"#\"${{ env.NPM_VERSION }}\"#g" package.json > package_updated.json
|
||||
npm version ${{ env.VERSION }} --force --no-git-tag-version
|
||||
sed "s#\"0.0.0\"#\"${{ env.VERSION }}\"#g" package.json > package_updated.json
|
||||
mv package_updated.json package.json
|
||||
npm publish --tag insiders --access public
|
||||
npm publish --tag ${{ env.RELEASE_CHANNEL }} --access public
|
||||
cd -
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
tailwind_oxide_release:
|
||||
tailwind-release:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build and release Tailwind CSS
|
||||
|
||||
needs:
|
||||
- oxide-release
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14]
|
||||
- release
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ env.NODE_VERSION }}-node_modules-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
# Cache the `oxide` Rust build
|
||||
- name: Cache oxide build
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
./oxide/target/
|
||||
./oxide/crates/node/*.node
|
||||
./oxide/crates/node/index.js
|
||||
./oxide/crates/node/index.d.ts
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./oxide/crates/**/*') }}
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Resolve version
|
||||
id: vars
|
||||
- name: Calculate environment variables
|
||||
run: |
|
||||
echo "SHA_SHORT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
echo "OXIDE_NPM_VERSION=0.0.0-insiders.$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
echo "RELEASE_CHANNEL=$(node ./scripts/release-channel.js)" >> $GITHUB_ENV
|
||||
echo "VERSION=$(node -e 'console.log(require(`./package.json`).version);')" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup `@tailwindcss/oxide` version
|
||||
run: |
|
||||
sed "s#\"@tailwindcss/oxide\": \".*\"#\"@tailwindcss/oxide\": \"${{ env.OXIDE_NPM_VERSION }}\"#" package.json > package_updated.json
|
||||
sed "s#\"@tailwindcss/oxide\": \".*\"#\"@tailwindcss/oxide\": \"${{ env.VERSION }}\"#" package.json > package_updated.json
|
||||
mv package_updated.json package.json
|
||||
|
||||
- name: Install dependencies
|
||||
|
|
@ -321,12 +415,31 @@ jobs:
|
|||
run: npm run build
|
||||
|
||||
- name: Test
|
||||
run: npm test
|
||||
run: |
|
||||
npm test || \
|
||||
npm test || \
|
||||
npm test || exit 1
|
||||
|
||||
- name: 'Version based on commit: 0.0.0-oxide.${{ env.SHA_SHORT }}'
|
||||
run: npm version 0.0.0-oxide.${{ env.SHA_SHORT }} --force --no-git-tag-version
|
||||
- name: 'Version: ${{ env.VERSION }}'
|
||||
run: npm version ${{ env.VERSION }} --force --no-git-tag-version
|
||||
|
||||
- name: Publish
|
||||
run: npm publish --tag oxide
|
||||
run: npm publish --tag ${{ env.RELEASE_CHANNEL }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
# - name: Trigger Tailwind Play update
|
||||
# if: env.RELEASE_CHANNEL == 'latest'
|
||||
# uses: actions/github-script@v6
|
||||
# with:
|
||||
# github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }}
|
||||
# script: |
|
||||
# await github.rest.actions.createWorkflowDispatch({
|
||||
# owner: 'tailwindlabs',
|
||||
# repo: 'play.tailwindcss.com',
|
||||
# ref: 'master',
|
||||
# workflow_id: 'upgrade-tailwindcss.yml',
|
||||
# inputs: {
|
||||
# version: '${{ env.TAILWINDCSS_VERSION }}'
|
||||
# }
|
||||
# })
|
||||
29
CHANGELOG.md
29
CHANGELOG.md
|
|
@ -7,7 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
- Nothing yet!
|
||||
### Fixed
|
||||
|
||||
- Fix issue where some pseudo-element variants generated the wrong selector ([#10943](https://github.com/tailwindlabs/tailwindcss/pull/10943), [#10962](https://github.com/tailwindlabs/tailwindcss/pull/10962))
|
||||
- Make font settings propagate into buttons, inputs, etc. ([#10940](https://github.com/tailwindlabs/tailwindcss/pull/10940))
|
||||
- Fix parsing of `theme()` inside `calc()` when there are no spaces around operators ([#11157](https://github.com/tailwindlabs/tailwindcss/pull/11157))
|
||||
- Ensure `repeating-conic-gradient` is detected as an image ([#11180](https://github.com/tailwindlabs/tailwindcss/pull/11180))
|
||||
- Remove `autoprefixer` dependency ([#11315](https://github.com/tailwindlabs/tailwindcss/pull/11315))
|
||||
- Fix source maps issue resulting in a crash ([#11319](https://github.com/tailwindlabs/tailwindcss/pull/11319))
|
||||
- Fallback to RegEx based parser when using custom transformers or extractors ([#11335](https://github.com/tailwindlabs/tailwindcss/pull/11335))
|
||||
- Move unknown pseudo-elements outside of `:is` by default ([#11345](https://github.com/tailwindlabs/tailwindcss/pull/11345))
|
||||
|
||||
### Added
|
||||
|
||||
- Add `aria-busy` utility ([#10966](https://github.com/tailwindlabs/tailwindcss/pull/10966))
|
||||
- Support `@import "tailwindcss"` using top-level `index.css` file ([#11205](https://github.com/tailwindlabs/tailwindcss/pull/11205), ([#11260](https://github.com/tailwindlabs/tailwindcss/pull/11260)))
|
||||
- Use `lightningcss` for nesting and vendor prefixes in PostCSS plugin ([#10399](https://github.com/tailwindlabs/tailwindcss/pull/10399))
|
||||
- Automatically detect content paths when no `content` configuration is provided ([#11173](https://github.com/tailwindlabs/tailwindcss/pull/11173), [#11221](https://github.com/tailwindlabs/tailwindcss/pull/11221))
|
||||
- Process and inline `@import` at-rules natively ([#11239](https://github.com/tailwindlabs/tailwindcss/pull/11239))
|
||||
- Add `svh`, `lvh`, and `dvh` values to default `height`/`min-height`/`max-height` theme ([#11317](https://github.com/tailwindlabs/tailwindcss/pull/11317))
|
||||
- Add `has-*` variants for `:has(...)` pseudo-class ([#11318](https://github.com/tailwindlabs/tailwindcss/pull/11318))
|
||||
- Add `text-wrap` utilities including `text-balance` ([#11320](https://github.com/tailwindlabs/tailwindcss/pull/11320))
|
||||
- Explicitly configure Lightning CSS features, and prefer user browserslist over default browserslist ([#11402](https://github.com/tailwindlabs/tailwindcss/pull/11402), [#11412](https://github.com/tailwindlabs/tailwindcss/pull/11412))
|
||||
|
||||
### Changed
|
||||
|
||||
- Reset padding for `<dialog>` elements in preflight ([#11069](https://github.com/tailwindlabs/tailwindcss/pull/11069))
|
||||
- Deprecate `--no-autoprefixer` flag in the CLI ([#11280](https://github.com/tailwindlabs/tailwindcss/pull/11280))
|
||||
- Make the Rust based parser the default ([#11394](https://github.com/tailwindlabs/tailwindcss/pull/11394))
|
||||
|
||||
## [3.3.2] - 2023-04-25
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/tailwindlabs/tailwindcss/actions"><img src="https://img.shields.io/github/actions/workflow/status/tailwindlabs/tailwindcss/ci-stable.yml?branch=master" alt="Build Status"></a>
|
||||
<a href="https://github.com/tailwindlabs/tailwindcss/actions"><img src="https://img.shields.io/github/actions/workflow/status/tailwindlabs/tailwindcss/ci.yml?branch=master" alt="Build Status"></a>
|
||||
<a href="https://www.npmjs.com/package/tailwindcss"><img src="https://img.shields.io/npm/dt/tailwindcss.svg" alt="Total Downloads"></a>
|
||||
<a href="https://github.com/tailwindcss/tailwindcss/releases"><img src="https://img.shields.io/npm/v/tailwindcss.svg" alt="Latest Release"></a>
|
||||
<a href="https://github.com/tailwindcss/tailwindcss/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/tailwindcss.svg" alt="License"></a>
|
||||
|
|
|
|||
5
index.css
Normal file
5
index.css
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
@tailwind base;
|
||||
|
||||
@tailwind components;
|
||||
|
||||
@tailwind utilities;
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"postcss": "^8.4.23",
|
||||
"postcss": "^8.4.24",
|
||||
"postcss-cli": "^10.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ it('looks in the CWD by default', async () => {
|
|||
|
||||
let result = await build({ cwd: path.resolve(__dirname, '..') })
|
||||
|
||||
expect(result.css).toMatchCss(css`
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.content-\[real-static-positive\] {
|
||||
--tw-content: real-static-positive;
|
||||
content: var(--tw-content);
|
||||
|
|
@ -69,7 +69,7 @@ it('looks in the CWD by default', async () => {
|
|||
|
||||
result = await build({ cwd: path.resolve(__dirname, '../src') })
|
||||
|
||||
expect(result.css).toMatchCss(``)
|
||||
expect(result.css).toMatchFormattedCss(``)
|
||||
})
|
||||
|
||||
it('looks in the CWD for non-config-relative paths', async () => {
|
||||
|
|
@ -88,7 +88,7 @@ it('looks in the CWD for non-config-relative paths', async () => {
|
|||
|
||||
let result = await build({ cwd: path.resolve(__dirname, '..') })
|
||||
|
||||
expect(result.css).toMatchCss(css`
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.content-\[real-static-positive\] {
|
||||
--tw-content: real-static-positive;
|
||||
content: var(--tw-content);
|
||||
|
|
@ -97,7 +97,7 @@ it('looks in the CWD for non-config-relative paths', async () => {
|
|||
|
||||
result = await build({ cwd: path.resolve(__dirname, '../src') })
|
||||
|
||||
expect(result.css).toMatchCss(``)
|
||||
expect(result.css).toMatchFormattedCss(``)
|
||||
})
|
||||
|
||||
it('can look for content files relative to the config', async () => {
|
||||
|
|
@ -113,12 +113,12 @@ it('can look for content files relative to the config', async () => {
|
|||
// Here `./real` doesn't exist next to the config in the root directory
|
||||
let result = await build({ cwd: path.resolve(__dirname, '..') })
|
||||
|
||||
expect(result.css).toMatchCss(css``)
|
||||
expect(result.css).toMatchFormattedCss(css``)
|
||||
|
||||
// But here it `./real` does exist next to the config in the `./src` directory!
|
||||
result = await build({ cwd: path.resolve(__dirname, '../src') })
|
||||
|
||||
expect(result.css).toMatchCss(css`
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.content-\[real-static-positive\] {
|
||||
--tw-content: real-static-positive;
|
||||
content: var(--tw-content);
|
||||
|
|
@ -143,7 +143,7 @@ it('it handles ignored globs correctly when not relative to the config', async (
|
|||
|
||||
let result = await build({ cwd: path.resolve(__dirname, '..') })
|
||||
|
||||
expect(result.css).toMatchCss(css`
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.content-\[real-dynamic-positive\] {
|
||||
--tw-content: real-dynamic-positive;
|
||||
content: var(--tw-content);
|
||||
|
|
@ -157,7 +157,7 @@ it('it handles ignored globs correctly when not relative to the config', async (
|
|||
// But here it `./real` does exist next to the config in the `./src` directory!
|
||||
result = await build({ cwd: path.resolve(__dirname, '../src') })
|
||||
|
||||
expect(result.css).toMatchCss(``)
|
||||
expect(result.css).toMatchFormattedCss(``)
|
||||
})
|
||||
|
||||
it('it handles ignored globs correctly when relative to the config', async () => {
|
||||
|
|
@ -177,12 +177,12 @@ it('it handles ignored globs correctly when relative to the config', async () =>
|
|||
|
||||
let result = await build({ cwd: path.resolve(__dirname, '..') })
|
||||
|
||||
expect(result.css).toMatchCss(``)
|
||||
expect(result.css).toMatchFormattedCss(``)
|
||||
|
||||
// But here it `./real` does exist next to the config in the `./src` directory!
|
||||
result = await build({ cwd: path.resolve(__dirname, '../src') })
|
||||
|
||||
expect(result.css).toMatchCss(css`
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.content-\[real-dynamic-positive\] {
|
||||
--tw-content: real-dynamic-positive;
|
||||
content: var(--tw-content);
|
||||
|
|
@ -215,7 +215,7 @@ it('it can resolve symlinks for files when not relative to the config', async ()
|
|||
|
||||
let result = await build({ cwd: path.resolve(__dirname, '..') })
|
||||
|
||||
expect(result.css).toMatchCss(css`
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.content-\[real-dynamic-positive\] {
|
||||
--tw-content: real-dynamic-positive;
|
||||
content: var(--tw-content);
|
||||
|
|
@ -237,7 +237,7 @@ it('it can resolve symlinks for files when not relative to the config', async ()
|
|||
// But here it `./real` does exist next to the config in the `./src` directory!
|
||||
result = await build({ cwd: path.resolve(__dirname, '../src') })
|
||||
|
||||
expect(result.css).toMatchCss(``)
|
||||
expect(result.css).toMatchFormattedCss(``)
|
||||
})
|
||||
|
||||
it('it can resolve symlinks for files when relative to the config', async () => {
|
||||
|
|
@ -261,12 +261,12 @@ it('it can resolve symlinks for files when relative to the config', async () =>
|
|||
|
||||
let result = await build({ cwd: path.resolve(__dirname, '..') })
|
||||
|
||||
expect(result.css).toMatchCss(``)
|
||||
expect(result.css).toMatchFormattedCss(``)
|
||||
|
||||
// But here it `./real` does exist next to the config in the `./src` directory!
|
||||
result = await build({ cwd: path.resolve(__dirname, '../src') })
|
||||
|
||||
expect(result.css).toMatchCss(css`
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.content-\[real-dynamic-positive\] {
|
||||
--tw-content: real-dynamic-positive;
|
||||
content: var(--tw-content);
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ module.exports = function $(command, options = {}) {
|
|||
messages.splice(0, idx + 1)
|
||||
let actorIdx = actors.indexOf(next)
|
||||
actors.splice(actorIdx, 1)
|
||||
next.resolve()
|
||||
next.resolve(message)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ let fs = require('fs/promises')
|
|||
let chokidar = require('chokidar')
|
||||
|
||||
let resolveToolRoot = require('./resolve-tool-root')
|
||||
let FILE_STATE = {
|
||||
NotFound: Symbol(),
|
||||
}
|
||||
|
||||
function getWatcherOptions() {
|
||||
return {
|
||||
|
|
@ -28,7 +31,7 @@ module.exports = function ({
|
|||
cleanup = true,
|
||||
} = {}) {
|
||||
let toolRoot = resolveToolRoot()
|
||||
let fileCache = {}
|
||||
let fileCache = new Map()
|
||||
|
||||
let absoluteOutputFolder = path.resolve(toolRoot, output)
|
||||
let absoluteInputFolder = path.resolve(toolRoot, input)
|
||||
|
|
@ -41,9 +44,9 @@ module.exports = function ({
|
|||
// Restore all written files
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
Object.entries(fileCache).map(async ([file, content]) => {
|
||||
Array.from(fileCache.entries()).map(async ([file, content]) => {
|
||||
try {
|
||||
if (content === null) {
|
||||
if (content === FILE_STATE.NotFound) {
|
||||
return await fs.unlink(file)
|
||||
} else {
|
||||
return await fs.writeFile(file, content, 'utf8')
|
||||
|
|
@ -89,7 +92,7 @@ module.exports = function ({
|
|||
return {
|
||||
cleanupFile(file) {
|
||||
let filePath = path.resolve(toolRoot, file)
|
||||
fileCache[filePath] = null
|
||||
fileCache.set(filePath, FILE_STATE.NotFound)
|
||||
},
|
||||
async fileExists(file) {
|
||||
let filePath = path.resolve(toolRoot, file)
|
||||
|
|
@ -98,8 +101,11 @@ module.exports = function ({
|
|||
async removeFile(file) {
|
||||
let filePath = path.resolve(toolRoot, file)
|
||||
|
||||
if (!fileCache[filePath]) {
|
||||
fileCache[filePath] = await fs.readFile(filePath, 'utf8').catch(() => null)
|
||||
if (!fileCache.has(filePath)) {
|
||||
fileCache.set(
|
||||
filePath,
|
||||
await fs.readFile(filePath, 'utf8').catch(() => FILE_STATE.NotFound)
|
||||
)
|
||||
}
|
||||
|
||||
await fs.unlink(filePath).catch(() => null)
|
||||
|
|
@ -114,20 +120,31 @@ module.exports = function ({
|
|||
},
|
||||
async appendToInputFile(file, contents) {
|
||||
let filePath = path.resolve(absoluteInputFolder, file)
|
||||
if (!fileCache[filePath]) {
|
||||
fileCache[filePath] = await fs.readFile(filePath, 'utf8')
|
||||
if (!fileCache.has(filePath)) {
|
||||
fileCache.set(filePath, await fs.readFile(filePath, 'utf8'))
|
||||
}
|
||||
|
||||
return fs.appendFile(filePath, contents, 'utf8')
|
||||
},
|
||||
async writeInputFile(file, contents) {
|
||||
let filePath = path.resolve(absoluteInputFolder, file)
|
||||
if (!fileCache[filePath]) {
|
||||
|
||||
// Ensure the parent folder of the file exists
|
||||
if (
|
||||
!(await fs
|
||||
.stat(filePath)
|
||||
.then(() => true)
|
||||
.catch(() => false))
|
||||
) {
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
||||
}
|
||||
|
||||
if (!fileCache.has(filePath)) {
|
||||
try {
|
||||
fileCache[filePath] = await fs.readFile(filePath, 'utf8')
|
||||
fileCache.set(filePath, await fs.readFile(filePath, 'utf8'))
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
fileCache[filePath] = null // Sentinel value to `delete` the file afterwards. This also means that we are writing to a `new` file inside the test.
|
||||
fileCache.set(filePath, FILE_STATE.NotFound)
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,6 @@
|
|||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"parcel": "^2.8.3"
|
||||
"parcel": "^2.9.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
let $ = require('../../execute')
|
||||
let { css, html, javascript } = require('../../syntax')
|
||||
let { env } = require('../../../lib/lib/sharedState')
|
||||
|
||||
let {
|
||||
appendToInputFile,
|
||||
|
|
@ -40,7 +39,7 @@ describe('static build', () => {
|
|||
'index.html',
|
||||
html`
|
||||
<link rel="stylesheet" href="./index.css" />
|
||||
<div class="bg-primary"></div>
|
||||
<div class="z-primary"></div>
|
||||
`
|
||||
)
|
||||
await writeInputFile(
|
||||
|
|
@ -58,8 +57,8 @@ describe('static build', () => {
|
|||
content: ['./src/index.html'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: 'black',
|
||||
zIndex: {
|
||||
primary: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -74,26 +73,13 @@ describe('static build', () => {
|
|||
env: { NODE_ENV: 'production' },
|
||||
})
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
|
||||
css`
|
||||
.bg-primary {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
|
||||
css`
|
||||
.bg-primary {
|
||||
background-color: black;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
|
||||
css`
|
||||
.z-primary {
|
||||
z-index: 0;
|
||||
}
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
it('can use a tailwind.config.ts configuration file', async () => {
|
||||
|
|
@ -102,7 +88,7 @@ describe('static build', () => {
|
|||
'index.html',
|
||||
html`
|
||||
<link rel="stylesheet" href="./index.css" />
|
||||
<div class="bg-primary"></div>
|
||||
<div class="z-primary"></div>
|
||||
`
|
||||
)
|
||||
await writeInputFile(
|
||||
|
|
@ -122,8 +108,8 @@ describe('static build', () => {
|
|||
content: ['./src/index.html'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: 'black',
|
||||
zIndex: {
|
||||
primary: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -138,26 +124,13 @@ describe('static build', () => {
|
|||
env: { NODE_ENV: 'production' },
|
||||
})
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
|
||||
css`
|
||||
.bg-primary {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
|
||||
css`
|
||||
.bg-primary {
|
||||
background-color: black;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
|
||||
css`
|
||||
.z-primary {
|
||||
z-index: 0;
|
||||
}
|
||||
`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -199,41 +172,22 @@ describe('watcher', () => {
|
|||
)
|
||||
|
||||
await waitForOutputFileChange(/index\.\w+\.css$/, async () => {
|
||||
await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
|
||||
await appendToInputFile('index.html', html`<div class="z-0"></div>`)
|
||||
})
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
|
||||
css`
|
||||
.z-0 {
|
||||
z-index: 0;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
return runningProcess.stop()
|
||||
})
|
||||
|
|
@ -271,41 +225,22 @@ describe('watcher', () => {
|
|||
)
|
||||
|
||||
await waitForOutputFileChange(/index\.\w+\.css$/, async () => {
|
||||
await appendToInputFile('glob/index.html', html`<div class="bg-red-500"></div>`)
|
||||
await appendToInputFile('glob/index.html', html`<div class="z-0"></div>`)
|
||||
})
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
|
||||
css`
|
||||
.z-0 {
|
||||
z-index: 0;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
return runningProcess.stop()
|
||||
})
|
||||
|
|
@ -328,7 +263,7 @@ describe('watcher', () => {
|
|||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
@media (width >= 768px) {
|
||||
.md\:font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
|
@ -366,7 +301,7 @@ describe('watcher', () => {
|
|||
.font-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
@media (min-width: 800px) {
|
||||
@media (width >= 800px) {
|
||||
.md\:font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
|
@ -417,11 +352,11 @@ describe('watcher', () => {
|
|||
|
||||
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
|
||||
css`
|
||||
/* prettier-ignore */
|
||||
.btn {
|
||||
border-radius: .25rem;
|
||||
padding: .25rem .5rem;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
|
@ -438,43 +373,25 @@ describe('watcher', () => {
|
|||
|
||||
@layer components {
|
||||
.btn {
|
||||
@apply rounded bg-red-500 px-2 py-1;
|
||||
@apply rounded z-0 px-2 py-1;
|
||||
}
|
||||
}
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
|
||||
css`
|
||||
.btn {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
|
||||
css`
|
||||
.btn {
|
||||
background-color: #ef4444;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
|
||||
css`
|
||||
.btn {
|
||||
z-index: 0;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
return runningProcess.stop()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"postcss": "^8.4.23",
|
||||
"postcss": "^8.4.24",
|
||||
"postcss-cli": "^10.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
let $ = require('../../execute')
|
||||
let { css, html, javascript } = require('../../syntax')
|
||||
let { env } = require('../../../lib/lib/sharedState')
|
||||
|
||||
let { readOutputFile, appendToInputFile, writeInputFile } = require('../../io')({
|
||||
output: 'dist',
|
||||
|
|
@ -58,41 +57,22 @@ describe('watcher', () => {
|
|||
`
|
||||
)
|
||||
|
||||
await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
|
||||
await appendToInputFile('index.html', html`<div class="z-0"></div>`)
|
||||
await runningProcess.onStderr(ready)
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.z-0 {
|
||||
z-index: 0;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
return runningProcess.stop()
|
||||
})
|
||||
|
|
@ -125,41 +105,22 @@ describe('watcher', () => {
|
|||
`
|
||||
)
|
||||
|
||||
await appendToInputFile('glob/index.html', html`<div class="bg-red-500"></div>`)
|
||||
await appendToInputFile('glob/index.html', html`<div class="z-0"></div>`)
|
||||
await runningProcess.onStderr(ready)
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.z-0 {
|
||||
z-index: 0;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
return runningProcess.stop()
|
||||
})
|
||||
|
|
@ -187,23 +148,23 @@ describe('watcher', () => {
|
|||
await writeInputFile(
|
||||
'../tailwind.config.js',
|
||||
javascript`
|
||||
module.exports = {
|
||||
content: ['./src/index.html'],
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
md: '800px'
|
||||
},
|
||||
fontWeight: {
|
||||
bold: 'bold'
|
||||
}
|
||||
module.exports = {
|
||||
content: ['./src/index.html'],
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
md: '800px'
|
||||
},
|
||||
fontWeight: {
|
||||
bold: 'bold'
|
||||
}
|
||||
},
|
||||
corePlugins: {
|
||||
preflight: false,
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
},
|
||||
corePlugins: {
|
||||
preflight: false,
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
`
|
||||
)
|
||||
await runningProcess.onStderr(ready)
|
||||
|
|
@ -277,47 +238,25 @@ describe('watcher', () => {
|
|||
|
||||
@layer components {
|
||||
.btn {
|
||||
@apply rounded bg-red-500 px-2 py-1;
|
||||
@apply rounded z-0 px-2 py-1;
|
||||
}
|
||||
}
|
||||
`
|
||||
)
|
||||
await runningProcess.onStderr(ready)
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.btn {
|
||||
border-radius: 0.25rem;
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
/* prettier-ignore */
|
||||
.btn {
|
||||
border-radius: 0.25rem;
|
||||
background-color: #ef4444;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.btn {
|
||||
z-index: 0;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
return runningProcess.stop()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@
|
|||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"rollup": "^3.21.0",
|
||||
"rollup": "^3.25.1",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"sass": "^1.62.0"
|
||||
"sass": "^1.63.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
let $ = require('../../execute')
|
||||
let { css, html, javascript } = require('../../syntax')
|
||||
let { env } = require('../../../lib/lib/sharedState')
|
||||
|
||||
let { readOutputFile, appendToInputFile, writeInputFile } = require('../../io')({
|
||||
output: 'dist',
|
||||
|
|
@ -58,41 +57,22 @@ describe('watcher', () => {
|
|||
`
|
||||
)
|
||||
|
||||
await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
|
||||
await appendToInputFile('index.html', html`<div class="z-0"></div>`)
|
||||
await runningProcess.onStderr(ready)
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.z-0 {
|
||||
z-index: 0;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
return runningProcess.stop()
|
||||
})
|
||||
|
|
@ -125,41 +105,22 @@ describe('watcher', () => {
|
|||
`
|
||||
)
|
||||
|
||||
await appendToInputFile('glob/index.html', html`<div class="bg-red-500"></div>`)
|
||||
await appendToInputFile('glob/index.html', html`<div class="z-0"></div>`)
|
||||
await runningProcess.onStderr(ready)
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.z-0 {
|
||||
z-index: 0;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
return runningProcess.stop()
|
||||
})
|
||||
|
|
@ -275,43 +236,25 @@ describe('watcher', () => {
|
|||
|
||||
@layer components {
|
||||
.btn {
|
||||
@apply rounded bg-red-500 px-2 py-1;
|
||||
@apply rounded z-0 px-2 py-1;
|
||||
}
|
||||
}
|
||||
`
|
||||
)
|
||||
await runningProcess.onStderr(ready)
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.btn {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.btn {
|
||||
background-color: #ef4444;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.btn {
|
||||
z-index: 0;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
return runningProcess.stop()
|
||||
})
|
||||
|
|
@ -370,43 +313,25 @@ describe('watcher', () => {
|
|||
css`
|
||||
@layer components {
|
||||
.btn {
|
||||
@apply rounded bg-red-500 px-2 py-1;
|
||||
@apply rounded z-0 px-2 py-1;
|
||||
}
|
||||
}
|
||||
`
|
||||
)
|
||||
await runningProcess.onStderr(ready)
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.btn {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.btn {
|
||||
background-color: #ef4444;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.btn {
|
||||
z-index: 0;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
return runningProcess.stop()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"rollup": "^3.21.0",
|
||||
"rollup": "^3.25.1",
|
||||
"rollup-plugin-postcss": "^4.0.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
let $ = require('../../execute')
|
||||
let { css, html, javascript } = require('../../syntax')
|
||||
let { env } = require('../../../lib/lib/sharedState')
|
||||
|
||||
let { readOutputFile, appendToInputFile, writeInputFile, removeFile } = require('../../io')({
|
||||
output: 'dist',
|
||||
|
|
@ -30,7 +29,7 @@ describe('static build', () => {
|
|||
|
||||
it('can use a tailwind.config.js configuration file with ESM syntax', async () => {
|
||||
await removeFile('tailwind.config.js')
|
||||
await writeInputFile('index.html', html`<div class="bg-primary"></div>`)
|
||||
await writeInputFile('index.html', html`<div class="z-primary"></div>`)
|
||||
await writeInputFile(
|
||||
'index.css',
|
||||
css`
|
||||
|
|
@ -46,8 +45,8 @@ describe('static build', () => {
|
|||
content: ['./src/index.html'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: 'black',
|
||||
zIndex: {
|
||||
primary: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -62,31 +61,18 @@ describe('static build', () => {
|
|||
env: { NODE_ENV: 'production' },
|
||||
})
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-primary {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-primary {
|
||||
background-color: black;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.z-primary {
|
||||
z-index: 0;
|
||||
}
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
it('can use a tailwind.config.ts configuration file', async () => {
|
||||
await removeFile('tailwind.config.js')
|
||||
await writeInputFile('index.html', html`<div class="bg-primary"></div>`)
|
||||
await writeInputFile('index.html', html`<div class="z-primary"></div>`)
|
||||
await writeInputFile(
|
||||
'index.css',
|
||||
css`
|
||||
|
|
@ -104,8 +90,8 @@ describe('static build', () => {
|
|||
content: ['./src/index.html'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: 'black',
|
||||
zIndex: {
|
||||
primary: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -120,26 +106,13 @@ describe('static build', () => {
|
|||
env: { NODE_ENV: 'production' },
|
||||
})
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-primary {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-primary {
|
||||
background-color: black;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.z-primary {
|
||||
z-index: 0;
|
||||
}
|
||||
`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -172,41 +145,22 @@ describe('watcher', () => {
|
|||
`
|
||||
)
|
||||
|
||||
await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
|
||||
await appendToInputFile('index.html', html`<div class="z-0"></div>`)
|
||||
await runningProcess.onStderr(ready)
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.z-0 {
|
||||
z-index: 0;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
return runningProcess.stop()
|
||||
})
|
||||
|
|
@ -239,41 +193,22 @@ describe('watcher', () => {
|
|||
`
|
||||
)
|
||||
|
||||
await appendToInputFile('glob/index.html', html`<div class="bg-red-500"></div>`)
|
||||
await appendToInputFile('glob/index.html', html`<div class="z-0"></div>`)
|
||||
await runningProcess.onStderr(ready)
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.z-0 {
|
||||
z-index: 0;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
return runningProcess.stop()
|
||||
})
|
||||
|
|
@ -389,46 +324,25 @@ describe('watcher', () => {
|
|||
|
||||
@layer components {
|
||||
.btn {
|
||||
@apply rounded bg-red-500 px-2 py-1;
|
||||
@apply rounded z-0 px-2 py-1;
|
||||
}
|
||||
}
|
||||
`
|
||||
)
|
||||
await runningProcess.onStderr(ready)
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.btn {
|
||||
border-radius: 0.25rem;
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.btn {
|
||||
border-radius: 0.25rem;
|
||||
background-color: #ef4444;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile('index.css')).toIncludeCss(
|
||||
css`
|
||||
.btn {
|
||||
z-index: 0;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
return runningProcess.stop()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
|
@ -0,0 +1 @@
|
|||
<div class="font-bold"></div>
|
||||
|
|
@ -2,7 +2,6 @@ let path = require('path')
|
|||
let $ = require('../../execute')
|
||||
let { css, html, javascript } = require('../../syntax')
|
||||
let resolveToolRoot = require('../../resolve-tool-root')
|
||||
let { env } = require('../../../lib/lib/sharedState')
|
||||
|
||||
let version = require('../../../package.json').version
|
||||
|
||||
|
|
@ -19,7 +18,6 @@ let {
|
|||
})
|
||||
|
||||
let EXECUTABLE = 'node ../../lib/cli.js'
|
||||
let testStable = env.ENGINE === 'stable' ? test : test.skip
|
||||
|
||||
function dedent(input) {
|
||||
let lines = input.split('\n')
|
||||
|
|
@ -98,26 +96,35 @@ describe('Build command', () => {
|
|||
expect(withoutMinify.length).toBeGreaterThan(withMinify.length)
|
||||
})
|
||||
|
||||
testStable('--no-autoprefixer', async () => {
|
||||
// Deprecated
|
||||
test('--no-autoprefixer (should produce a warning)', async () => {
|
||||
await writeInputFile('index.html', html`<div class="select-none"></div>`)
|
||||
await writeInputFile(
|
||||
'index.css',
|
||||
css`
|
||||
@tailwind utilities;
|
||||
`
|
||||
)
|
||||
|
||||
await $(`${EXECUTABLE} --output ./dist/main.css`)
|
||||
let withAutoprefixer = await readOutputFile('main.css')
|
||||
let runningProcess = $(
|
||||
`${EXECUTABLE} --input ./src/index.css --output ./dist/main.css --no-autoprefixer`
|
||||
)
|
||||
let warning = runningProcess.onStderr((message) => message.includes('--no-autoprefixer'))
|
||||
await runningProcess
|
||||
expect(await warning).toMatchInlineSnapshot(`
|
||||
"[deprecation] The --no-autoprefixer flag is deprecated and has no effect.
|
||||
"
|
||||
`)
|
||||
let withoutAutoprefixer = await readOutputFile('main.css')
|
||||
|
||||
expect(withAutoprefixer).toIncludeCss(css`
|
||||
.select-none {
|
||||
// This contains --webkit-user-select which may be strange, but it is expected because we are
|
||||
// not handling the `--no-autoprefixer` flag anymore at all.
|
||||
expect(withoutAutoprefixer).toMatchInlineSnapshot(`
|
||||
".select-none {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
`)
|
||||
|
||||
await $(`${EXECUTABLE} --output ./dist/main.css --no-autoprefixer`)
|
||||
let withoutAutoprefixer = await readOutputFile('main.css')
|
||||
|
||||
expect(withoutAutoprefixer).toIncludeCss(css`
|
||||
.select-none {
|
||||
user-select: none;
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -143,7 +150,7 @@ describe('Build command', () => {
|
|||
theme: {
|
||||
extend: {
|
||||
fontWeight: {
|
||||
bold: 'BOLD',
|
||||
bold: 'bold',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -164,7 +171,7 @@ describe('Build command', () => {
|
|||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.font-bold {
|
||||
font-weight: BOLD;
|
||||
font-weight: bold;
|
||||
}
|
||||
`
|
||||
)
|
||||
|
|
@ -184,7 +191,7 @@ describe('Build command', () => {
|
|||
)
|
||||
})
|
||||
|
||||
testStable('--postcss (postcss.config.js)', async () => {
|
||||
test('--postcss (postcss.config.js)', async () => {
|
||||
await writeInputFile('index.html', html`<div class="font-bold"></div>`)
|
||||
|
||||
let customConfig = javascript`
|
||||
|
|
@ -196,7 +203,7 @@ describe('Build command', () => {
|
|||
function before(root, result) {
|
||||
// Inject a custom component with @apply rules to prove that we run
|
||||
// this _before_ the actual tailwind plugin.
|
||||
let btn = postcss.parse('.btn { @apply bg-red-500 px-2 py-1 }')
|
||||
let btn = postcss.parse('.btn { @apply flex px-2 py-1 }')
|
||||
root.append(btn.nodes)
|
||||
},
|
||||
function tailwindcss() {
|
||||
|
|
@ -217,45 +224,21 @@ describe('Build command', () => {
|
|||
|
||||
await $(`${EXECUTABLE} --output ./dist/main.css --postcss`)
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.font-bold-after {
|
||||
font-weight: 700;
|
||||
}
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.font-bold-after {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.btn-after {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.font-bold-after {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.btn-after {
|
||||
background-color: #ef4444;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
.btn-after {
|
||||
padding: 0.25rem 0.5rem;
|
||||
display: flex;
|
||||
}
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
testStable('--postcss (custom.postcss.config.js)', async () => {
|
||||
test('--postcss (custom.postcss.config.js)', async () => {
|
||||
await writeInputFile('index.html', html`<div class="font-bold"></div>`)
|
||||
|
||||
let customConfig = javascript`
|
||||
|
|
@ -267,7 +250,7 @@ describe('Build command', () => {
|
|||
function before(root, result) {
|
||||
// Inject a custom component with @apply rules to prove that we run
|
||||
// this _before_ the actual tailwind plugin.
|
||||
let btn = postcss.parse('.btn { @apply bg-red-500 px-2 py-1 }')
|
||||
let btn = postcss.parse('.btn { @apply flex px-2 py-1 }')
|
||||
root.append(btn.nodes)
|
||||
},
|
||||
function tailwindcss() {
|
||||
|
|
@ -288,45 +271,21 @@ describe('Build command', () => {
|
|||
|
||||
await $(`${EXECUTABLE} --output ./dist/main.css --postcss ./custom.postcss.config.js`)
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.font-bold-after {
|
||||
font-weight: 700;
|
||||
}
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.font-bold-after {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.btn-after {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.font-bold-after {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.btn-after {
|
||||
background-color: #ef4444;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
.btn-after {
|
||||
padding: 0.25rem 0.5rem;
|
||||
display: flex;
|
||||
}
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
testStable('--postcss supports process options', async () => {
|
||||
test('--postcss supports process options', async () => {
|
||||
await writeInputFile('index.html', html`<div class="font-bold"></div>`)
|
||||
|
||||
let customConfig = javascript`
|
||||
|
|
@ -360,7 +319,7 @@ describe('Build command', () => {
|
|||
expect(contents).toContain(`/*# sourceMappingURL`)
|
||||
})
|
||||
|
||||
testStable('--postcss supports process options with custom config', async () => {
|
||||
test('--postcss supports process options with custom config', async () => {
|
||||
await writeInputFile('index.html', html`<div class="font-bold"></div>`)
|
||||
|
||||
let customConfig = javascript`
|
||||
|
|
@ -472,7 +431,7 @@ describe('Build command', () => {
|
|||
css`
|
||||
@media (min-width: 768px) {
|
||||
.md\:something-cool {
|
||||
color: blue;
|
||||
color: #00f;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
|
@ -481,10 +440,18 @@ describe('Build command', () => {
|
|||
return runningProcess.stop()
|
||||
})
|
||||
|
||||
testStable('postcss-import is included when using a custom postcss configuration', async () => {
|
||||
test('postcss-import is included when using a custom postcss configuration', async () => {
|
||||
cleanupFile('src/test.css')
|
||||
|
||||
await writeInputFile('index.html', html`<div class="md:something-cool"></div>`)
|
||||
await writeInputFile(
|
||||
'imported.css',
|
||||
css`
|
||||
.foo {
|
||||
color: white;
|
||||
}
|
||||
`
|
||||
)
|
||||
await writeInputFile(
|
||||
'test.css',
|
||||
css`
|
||||
|
|
@ -501,7 +468,9 @@ describe('Build command', () => {
|
|||
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
@import './imported.css';
|
||||
.foo {
|
||||
color: #fff;
|
||||
}
|
||||
`
|
||||
)
|
||||
})
|
||||
|
|
@ -509,28 +478,8 @@ describe('Build command', () => {
|
|||
test('--help', async () => {
|
||||
let { combined } = await $(`${EXECUTABLE} --help`)
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(dedent(combined)).toEqual(
|
||||
dedent(`
|
||||
tailwindcss v${version}
|
||||
|
||||
Usage:
|
||||
tailwindcss build [options]
|
||||
|
||||
Options:
|
||||
-i, --input Input file
|
||||
-o, --output Output file
|
||||
-w, --watch Watch for changes and rebuild as needed
|
||||
-p, --poll Use polling instead of filesystem events when watching
|
||||
--content Content paths to use for removing unused classes
|
||||
-m, --minify Minify the output
|
||||
-c, --config Path to a custom config file
|
||||
-h, --help Display usage information
|
||||
`)
|
||||
)
|
||||
} else if (env.ENGINE === 'stable') {
|
||||
expect(dedent(combined)).toEqual(
|
||||
dedent(`
|
||||
expect(dedent(combined)).toEqual(
|
||||
dedent(`
|
||||
tailwindcss v${version}
|
||||
|
||||
Usage:
|
||||
|
|
@ -545,11 +494,9 @@ describe('Build command', () => {
|
|||
--postcss Load custom PostCSS configuration
|
||||
-m, --minify Minify the output
|
||||
-c, --config Path to a custom config file
|
||||
--no-autoprefixer Disable autoprefixer
|
||||
-h, --help Display usage information
|
||||
`)
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -562,7 +509,6 @@ describe('Init command', () => {
|
|||
{ flags: ['--ts', '--full'], name: 'tailwind.config.ts' },
|
||||
{ flags: ['--esm', '--full'], name: 'tailwind.config.js' },
|
||||
])('works with all these flags: %j', async ({ flags, name }) => {
|
||||
cleanupFile(name)
|
||||
await removeFile(name)
|
||||
|
||||
let { combined } = await $(`${EXECUTABLE} init ${flags.join(' ')}`)
|
||||
|
|
@ -611,7 +557,7 @@ describe('Init command', () => {
|
|||
expect((await readOutputFile('../full.config.js')).split('\n').length).toBeGreaterThan(50)
|
||||
})
|
||||
|
||||
testStable('--postcss', async () => {
|
||||
test('--postcss', async () => {
|
||||
expect(await fileExists('postcss.config.js')).toBe(true)
|
||||
await removeFile('postcss.config.js')
|
||||
expect(await fileExists('postcss.config.js')).toBe(false)
|
||||
|
|
@ -631,24 +577,8 @@ describe('Init command', () => {
|
|||
test('--help', async () => {
|
||||
let { combined } = await $(`${EXECUTABLE} init --help`)
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(dedent(combined)).toEqual(
|
||||
dedent(`
|
||||
tailwindcss v${version}
|
||||
|
||||
Usage:
|
||||
tailwindcss init [options]
|
||||
|
||||
Options:
|
||||
--esm Initialize configuration file as ESM
|
||||
--ts Initialize configuration file as TypeScript
|
||||
-f, --full Include the default values for all options in the generated configuration file
|
||||
-h, --help Display usage information
|
||||
`)
|
||||
)
|
||||
} else if (env.ENGINE === 'stable') {
|
||||
expect(dedent(combined)).toEqual(
|
||||
dedent(`
|
||||
expect(dedent(combined)).toEqual(
|
||||
dedent(`
|
||||
tailwindcss v${version}
|
||||
|
||||
Usage:
|
||||
|
|
@ -661,12 +591,10 @@ describe('Init command', () => {
|
|||
-f, --full Include the default values for all options in the generated configuration file
|
||||
-h, --help Display usage information
|
||||
`)
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test('ESM config is created by default in an ESM project', async () => {
|
||||
cleanupFile('tailwind.config.js')
|
||||
await removeFile('tailwind.config.js')
|
||||
|
||||
let pkg = await readOutputFile('../package.json')
|
||||
|
|
@ -696,7 +624,6 @@ describe('Init command', () => {
|
|||
})
|
||||
|
||||
test('CJS config is created by default in a non-ESM project', async () => {
|
||||
cleanupFile('tailwind.config.js')
|
||||
await removeFile('tailwind.config.js')
|
||||
|
||||
let pkg = await readOutputFile('../package.json')
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -21,6 +21,6 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"vite": "^4.3.2"
|
||||
"vite": "^4.3.9"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ require('isomorphic-fetch')
|
|||
|
||||
let $ = require('../../execute')
|
||||
let { css, html, javascript } = require('../../syntax')
|
||||
let { env } = require('../../../lib/lib/sharedState')
|
||||
|
||||
let { readOutputFile, appendToInputFile, writeInputFile, removeFile } = require('../../io')({
|
||||
output: 'dist',
|
||||
|
|
@ -48,7 +47,7 @@ describe('static build', () => {
|
|||
'index.html',
|
||||
html`
|
||||
<link rel="stylesheet" href="./index.css" />
|
||||
<div class="bg-primary"></div>
|
||||
<div class="z-primary"></div>
|
||||
`
|
||||
)
|
||||
await removeFile('tailwind.config.js')
|
||||
|
|
@ -59,8 +58,8 @@ describe('static build', () => {
|
|||
content: ['index.html'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: 'black',
|
||||
zIndex: {
|
||||
primary: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -75,26 +74,13 @@ describe('static build', () => {
|
|||
env: { NODE_ENV: 'production', NO_COLOR: '1' },
|
||||
})
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile(/index.\w+\.css$/)).toIncludeCss(
|
||||
css`
|
||||
.bg-primary {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile(/index.\w+\.css$/)).toIncludeCss(
|
||||
css`
|
||||
.bg-primary {
|
||||
background-color: black;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile(/index.\w+\.css$/)).toIncludeCss(
|
||||
css`
|
||||
.z-primary {
|
||||
z-index: 0;
|
||||
}
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
it('can use a tailwind.config.ts configuration file', async () => {
|
||||
|
|
@ -102,7 +88,7 @@ describe('static build', () => {
|
|||
'index.html',
|
||||
html`
|
||||
<link rel="stylesheet" href="./index.css" />
|
||||
<div class="bg-primary"></div>
|
||||
<div class="z-primary"></div>
|
||||
`
|
||||
)
|
||||
await removeFile('tailwind.config.js')
|
||||
|
|
@ -115,8 +101,8 @@ describe('static build', () => {
|
|||
content: ['index.html'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: 'black',
|
||||
zIndex: {
|
||||
primary: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -131,26 +117,13 @@ describe('static build', () => {
|
|||
env: { NODE_ENV: 'production', NO_COLOR: '1' },
|
||||
})
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile(/index.\w+\.css$/)).toIncludeCss(
|
||||
css`
|
||||
.bg-primary {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile(/index.\w+\.css$/)).toIncludeCss(
|
||||
css`
|
||||
.bg-primary {
|
||||
background-color: black;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile(/index.\w+\.css$/)).toIncludeCss(
|
||||
css`
|
||||
.z-primary {
|
||||
z-index: 0;
|
||||
}
|
||||
`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -191,41 +164,22 @@ describe('watcher', () => {
|
|||
`
|
||||
)
|
||||
|
||||
await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
|
||||
await appendToInputFile('index.html', html`<div class="z-0"></div>`)
|
||||
await runningProcess.onStdout((message) => message.includes('page reload'))
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await fetchCSS()).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await fetchCSS()).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await fetchCSS()).toIncludeCss(
|
||||
css`
|
||||
.z-0 {
|
||||
z-index: 0;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
return runningProcess.stop()
|
||||
})
|
||||
|
|
@ -262,41 +216,22 @@ describe('watcher', () => {
|
|||
`
|
||||
)
|
||||
|
||||
await appendToInputFile('glob/index.html', html`<div class="bg-red-500"></div>`)
|
||||
await appendToInputFile('glob/index.html', html`<div class="z-0"></div>`)
|
||||
await runningProcess.onStdout((message) => message.includes('page reload'))
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await fetchCSS()).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await fetchCSS()).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await fetchCSS()).toIncludeCss(
|
||||
css`
|
||||
.z-0 {
|
||||
z-index: 0;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
return runningProcess.stop()
|
||||
})
|
||||
|
|
@ -428,49 +363,25 @@ describe('watcher', () => {
|
|||
|
||||
@layer components {
|
||||
.btn {
|
||||
@apply rounded bg-red-500 px-2 py-1;
|
||||
@apply rounded z-0 px-2 py-1;
|
||||
}
|
||||
}
|
||||
`
|
||||
)
|
||||
await runningProcess.onStdout((message) => message.includes('hmr update /index.css'))
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await fetchCSS()).toIncludeCss(
|
||||
css`
|
||||
.btn {
|
||||
border-radius: 0.25rem;
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await fetchCSS()).toIncludeCss(
|
||||
css`
|
||||
.btn {
|
||||
border-radius: 0.25rem;
|
||||
background-color: #ef4444;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await fetchCSS()).toIncludeCss(
|
||||
css`
|
||||
.btn {
|
||||
z-index: 0;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
return runningProcess.stop()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
let $ = require('../../execute')
|
||||
let { css, html, javascript } = require('../../syntax')
|
||||
let { env } = require('../../../lib/lib/sharedState')
|
||||
|
||||
let {
|
||||
appendToInputFile,
|
||||
|
|
@ -28,7 +27,7 @@ describe('static build', () => {
|
|||
|
||||
it('can use a tailwind.config.js configuration file with ESM syntax', async () => {
|
||||
await removeFile('tailwind.config.js')
|
||||
await writeInputFile('index.html', html`<div class="bg-primary"></div>`)
|
||||
await writeInputFile('index.html', html`<div class="z-primary"></div>`)
|
||||
await writeInputFile(
|
||||
'index.css',
|
||||
css`
|
||||
|
|
@ -44,8 +43,8 @@ describe('static build', () => {
|
|||
content: ['./src/index.html'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: 'black',
|
||||
zIndex: {
|
||||
primary: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -58,31 +57,18 @@ describe('static build', () => {
|
|||
|
||||
await $('webpack --mode=production')
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-primary {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-primary {
|
||||
background-color: black;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.z-primary {
|
||||
z-index: 0;
|
||||
}
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
it('can use a tailwind.config.ts configuration file', async () => {
|
||||
await removeFile('tailwind.config.js')
|
||||
await writeInputFile('index.html', html`<div class="bg-primary"></div>`)
|
||||
await writeInputFile('index.html', html`<div class="z-primary"></div>`)
|
||||
await writeInputFile(
|
||||
'index.css',
|
||||
css`
|
||||
|
|
@ -100,8 +86,8 @@ describe('static build', () => {
|
|||
content: ['./src/index.html'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: 'black',
|
||||
zIndex: {
|
||||
primary: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -114,26 +100,13 @@ describe('static build', () => {
|
|||
|
||||
await $('webpack --mode=production')
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-primary {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-primary {
|
||||
background-color: black;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.z-primary {
|
||||
z-index: 0;
|
||||
}
|
||||
`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -169,41 +142,22 @@ describe('watcher', () => {
|
|||
)
|
||||
|
||||
await waitForOutputFileChange('main.css', async () => {
|
||||
await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
|
||||
await appendToInputFile('index.html', html`<div class="z-0"></div>`)
|
||||
})
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.z-0 {
|
||||
z-index: 0;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
return runningProcess.stop()
|
||||
})
|
||||
|
|
@ -239,41 +193,22 @@ describe('watcher', () => {
|
|||
)
|
||||
|
||||
await waitForOutputFileChange('main.css', async () => {
|
||||
await appendToInputFile('glob/index.html', html`<div class="bg-red-500"></div>`)
|
||||
await appendToInputFile('glob/index.html', html`<div class="z-0"></div>`)
|
||||
})
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.z-0 {
|
||||
z-index: 0;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
return runningProcess.stop()
|
||||
})
|
||||
|
|
@ -394,46 +329,25 @@ describe('watcher', () => {
|
|||
|
||||
@layer components {
|
||||
.btn {
|
||||
@apply rounded bg-red-500 px-2 py-1;
|
||||
@apply rounded z-0 px-2 py-1;
|
||||
}
|
||||
}
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.btn {
|
||||
border-radius: 0.25rem;
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.btn {
|
||||
background-color: #ef4444;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.btn {
|
||||
z-index: 0;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
return runningProcess.stop()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
let $ = require('../../execute')
|
||||
let { css, html, javascript } = require('../../syntax')
|
||||
let { env } = require('../../../lib/lib/sharedState')
|
||||
|
||||
let {
|
||||
appendToInputFile,
|
||||
|
|
@ -28,7 +27,7 @@ describe('static build', () => {
|
|||
|
||||
it('can use a tailwind.config.js configuration file with ESM syntax', async () => {
|
||||
await removeFile('tailwind.config.js')
|
||||
await writeInputFile('index.html', html`<div class="bg-primary"></div>`)
|
||||
await writeInputFile('index.html', html`<div class="z-primary"></div>`)
|
||||
await writeInputFile(
|
||||
'index.css',
|
||||
css`
|
||||
|
|
@ -44,8 +43,8 @@ describe('static build', () => {
|
|||
content: ['./src/index.html'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: 'black',
|
||||
zIndex: {
|
||||
primary: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -58,31 +57,18 @@ describe('static build', () => {
|
|||
|
||||
await $('webpack --mode=production')
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-primary {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-primary {
|
||||
background-color: black;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.z-primary {
|
||||
z-index: 0;
|
||||
}
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
it('can use a tailwind.config.ts configuration file', async () => {
|
||||
await removeFile('tailwind.config.js')
|
||||
await writeInputFile('index.html', html`<div class="bg-primary"></div>`)
|
||||
await writeInputFile('index.html', html`<div class="z-primary"></div>`)
|
||||
await writeInputFile(
|
||||
'index.css',
|
||||
css`
|
||||
|
|
@ -100,8 +86,8 @@ describe('static build', () => {
|
|||
content: ['./src/index.html'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: 'black',
|
||||
zIndex: {
|
||||
primary: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -114,26 +100,13 @@ describe('static build', () => {
|
|||
|
||||
await $('webpack --mode=production')
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-primary {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-primary {
|
||||
background-color: black;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.z-primary {
|
||||
z-index: 0;
|
||||
}
|
||||
`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -169,41 +142,22 @@ describe('watcher', () => {
|
|||
)
|
||||
|
||||
await waitForOutputFileChange('main.css', async () => {
|
||||
await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
|
||||
await appendToInputFile('index.html', html`<div class="z-0"></div>`)
|
||||
})
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.z-0 {
|
||||
z-index: 0;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
return runningProcess.stop()
|
||||
})
|
||||
|
|
@ -239,41 +193,22 @@ describe('watcher', () => {
|
|||
)
|
||||
|
||||
await waitForOutputFileChange('main.css', async () => {
|
||||
await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
|
||||
await appendToInputFile('index.html', html`<div class="z-0"></div>`)
|
||||
})
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.z-0 {
|
||||
z-index: 0;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
return runningProcess.stop()
|
||||
})
|
||||
|
|
@ -394,46 +329,25 @@ describe('watcher', () => {
|
|||
|
||||
@layer components {
|
||||
.btn {
|
||||
@apply rounded bg-red-500 px-2 py-1;
|
||||
@apply rounded z-0 px-2 py-1;
|
||||
}
|
||||
}
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.btn {
|
||||
border-radius: 0.25rem;
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.btn {
|
||||
background-color: #ef4444;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.btn {
|
||||
z-index: 0;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
return runningProcess.stop()
|
||||
})
|
||||
|
|
@ -447,7 +361,7 @@ describe('watcher', () => {
|
|||
content: {
|
||||
files: ['./src/index.html'],
|
||||
},
|
||||
safelist: ['bg-red-500','bg-red-600'],
|
||||
safelist: ['z-0', 'z-10'],
|
||||
theme: {
|
||||
extend: {
|
||||
},
|
||||
|
|
@ -464,43 +378,21 @@ describe('watcher', () => {
|
|||
|
||||
await waitForOutputFileCreation('main.css')
|
||||
|
||||
if (env.ENGINE === 'stable') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
}
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.z-0 {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.bg-red-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
|
||||
}
|
||||
.z-10 {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
if (env.ENGINE === 'oxide') {
|
||||
expect(await readOutputFile('main.css')).toIncludeCss(
|
||||
css`
|
||||
.bg-red-500 {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
|
||||
.bg-red-600 {
|
||||
background-color: #dc2626;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
return runningProcess.stop()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
const prettier = require('prettier')
|
||||
const { diff } = require('jest-diff')
|
||||
const lightningcss = require('lightningcss')
|
||||
const log = require('../src/util/log').default
|
||||
|
||||
let warn
|
||||
|
|
@ -22,46 +21,7 @@ function formatPrettier(input) {
|
|||
}
|
||||
|
||||
function format(input) {
|
||||
try {
|
||||
return lightningcss
|
||||
.transform({
|
||||
filename: 'input.css',
|
||||
code: Buffer.from(input),
|
||||
minify: false,
|
||||
targets: { chrome: 106 << 16 },
|
||||
drafts: {
|
||||
nesting: true,
|
||||
customMedia: true,
|
||||
},
|
||||
})
|
||||
.code.toString('utf8')
|
||||
} catch (err) {
|
||||
try {
|
||||
// Lightning CSS is pretty strict, so it will fail for `@media screen(md) {}` for example,
|
||||
// in that case we can fallback to prettier since it doesn't really care. However if an
|
||||
// actual syntax error is made, then we still want to show the proper error.
|
||||
return formatPrettier(input.replace(/\n/g, ''))
|
||||
} catch {
|
||||
let lines = err.source.split('\n')
|
||||
let e = new Error(
|
||||
[
|
||||
'Error formatting using Lightning CSS:',
|
||||
'',
|
||||
...[
|
||||
'```css',
|
||||
...lines.slice(Math.max(err.loc.line - 3, 0), err.loc.line),
|
||||
' '.repeat(err.loc.column - 1) + '^-- ' + err.toString(),
|
||||
...lines.slice(err.loc.line, err.loc.line + 2),
|
||||
'```',
|
||||
],
|
||||
].join('\n')
|
||||
)
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(e, toMatchFormattedCss)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
return formatPrettier(input).replace(/\n{2,}/g, '\n')
|
||||
}
|
||||
|
||||
function toMatchFormattedCss(received = '', argument = '') {
|
||||
|
|
@ -86,10 +46,7 @@ function toMatchFormattedCss(received = '', argument = '') {
|
|||
)
|
||||
}
|
||||
: () => {
|
||||
let actual = formatPrettier(formattedReceived).replace(/\n\n/g, '\n')
|
||||
let expected = formatPrettier(formattedArgument).replace(/\n\n/g, '\n')
|
||||
|
||||
let diffString = diff(expected, actual, {
|
||||
let diffString = diff(formattedArgument, formattedReceived, {
|
||||
expand: this.expand,
|
||||
})
|
||||
|
||||
|
|
@ -98,8 +55,8 @@ function toMatchFormattedCss(received = '', argument = '') {
|
|||
'\n\n' +
|
||||
(diffString && diffString.includes('- Expect')
|
||||
? `Difference:\n\n${diffString}`
|
||||
: `Expected: ${this.utils.printExpected(expected)}\n` +
|
||||
`Received: ${this.utils.printReceived(actual)}`)
|
||||
: `Expected: ${this.utils.printExpected(formattedArgument)}\n` +
|
||||
`Received: ${this.utils.printReceived(formattedReceived)}`)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -107,13 +64,10 @@ function toMatchFormattedCss(received = '', argument = '') {
|
|||
}
|
||||
|
||||
expect.extend({
|
||||
// Compare two CSS strings with all whitespace removed
|
||||
// This is probably naive but it's fast and works well enough.
|
||||
toMatchCss: toMatchFormattedCss,
|
||||
toMatchFormattedCss: toMatchFormattedCss,
|
||||
toIncludeCss(received, argument) {
|
||||
let options = {
|
||||
comment: 'stripped(received).includes(stripped(argument))',
|
||||
comment: 'formatCSS(received).includes(formatCSS(argument))',
|
||||
isNot: this.isNot,
|
||||
promise: this.promise,
|
||||
}
|
||||
|
|
|
|||
687
oxide/Cargo.lock
generated
687
oxide/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -105,7 +105,7 @@ fn main() -> Result<(), std::io::Error> {
|
|||
let candidates = content_paths.par_bridge().flat_map(|path| {
|
||||
read_lines(path)
|
||||
.unwrap()
|
||||
.filter_map(Result::ok)
|
||||
.map_while(Result::ok)
|
||||
.par_bridge()
|
||||
.flat_map_iter(|line| {
|
||||
Extractor::unique(line.as_bytes(), Default::default())
|
||||
|
|
|
|||
|
|
@ -12,14 +12,17 @@ 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"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.3", features = ['html_reports'] }
|
||||
tempfile = "3.5.0"
|
||||
|
||||
[[bench]]
|
||||
name = "parse_candidates"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "parse_candidate_strings_from_files"
|
||||
name = "parse_candidate_strings"
|
||||
harness = false
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use std::path::PathBuf;
|
||||
use tailwindcss_core::{parse_candidate_strings_from_files, ChangedContent};
|
||||
use tailwindcss_core::{parse_candidate_strings, ChangedContent, Parsing, IO};
|
||||
|
||||
pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
// current_dir will be set to ./crates/core
|
||||
|
|
@ -11,7 +11,6 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
|||
|
||||
let mut all_files: Vec<(u64, PathBuf)> = std::fs::read_dir(fixtures_path)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.map(|dir_entry| dir_entry.path())
|
||||
.filter(|path| path.is_file())
|
||||
|
|
@ -51,8 +50,13 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
|||
})
|
||||
.collect();
|
||||
|
||||
c.bench_function("parse_candidate_strings_from_files", |b| {
|
||||
b.iter(|| parse_candidate_strings_from_files(changed_content.clone()))
|
||||
c.bench_function("parse_candidate_strings", |b| {
|
||||
b.iter(|| {
|
||||
parse_candidate_strings(
|
||||
changed_content.clone(),
|
||||
Parsing::Parallel as u8 | IO::Parallel as u8,
|
||||
)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -31,6 +31,18 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
|||
c.bench_function("parse_candidate_strings (real world)", |b| {
|
||||
b.iter(|| parse(include_bytes!("./fixtures/template-499.html")))
|
||||
});
|
||||
|
||||
let mut group = c.benchmark_group("sample-size-example");
|
||||
group.sample_size(10);
|
||||
|
||||
group.bench_function("parse_candidate_strings (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);
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ impl Candidate {
|
|||
x == ':' && !in_arbitrary
|
||||
}
|
||||
})
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let utility = match parts.pop() {
|
||||
|
|
|
|||
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,
|
||||
)
|
||||
}
|
||||
259
oxide/crates/core/src/fixtures/binary-extensions.txt
Normal file
259
oxide/crates/core/src/fixtures/binary-extensions.txt
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
3dm
|
||||
3ds
|
||||
3g2
|
||||
3gp
|
||||
7z
|
||||
DS_Store
|
||||
a
|
||||
aac
|
||||
adp
|
||||
ai
|
||||
aif
|
||||
aiff
|
||||
alz
|
||||
ape
|
||||
apk
|
||||
appimage
|
||||
ar
|
||||
arj
|
||||
asf
|
||||
au
|
||||
avi
|
||||
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
|
||||
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
|
||||
|
|
@ -12,7 +12,6 @@ pub fn fast_glob(
|
|||
.follow_links(true)
|
||||
.build()
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.map(|file| file.path().to_path_buf())
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
use crate::parser::Extractor;
|
||||
use fxhash::FxHashSet;
|
||||
use ignore::WalkBuilder;
|
||||
use rayon::prelude::*;
|
||||
use std::cmp::Ordering;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use tracing::event;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
pub mod candidate;
|
||||
pub mod cursor;
|
||||
pub mod fast_skip;
|
||||
pub mod glob;
|
||||
pub mod location;
|
||||
pub mod modifier;
|
||||
|
|
@ -11,14 +18,7 @@ pub mod parser;
|
|||
pub mod utility;
|
||||
pub mod variant;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChangedContent {
|
||||
pub file: Option<PathBuf>,
|
||||
pub content: Option<String>,
|
||||
pub extension: String,
|
||||
}
|
||||
|
||||
pub fn parse_candidate_strings_from_files(changed_content: Vec<ChangedContent>) -> Vec<String> {
|
||||
fn init_tracing() {
|
||||
if matches!(std::env::var("DEBUG"), Ok(value) if value.eq("*") || value.eq("1") || value.eq("true") || value.contains("tailwind"))
|
||||
{
|
||||
tracing_subscriber::fmt()
|
||||
|
|
@ -27,8 +27,325 @@ pub fn parse_candidate_strings_from_files(changed_content: Vec<ChangedContent>)
|
|||
.compact()
|
||||
.init();
|
||||
}
|
||||
}
|
||||
|
||||
parse_all_blobs(read_all_files(changed_content))
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChangedContent {
|
||||
pub file: Option<PathBuf>,
|
||||
pub content: Option<String>,
|
||||
pub extension: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContentPathInfo {
|
||||
pub base: String,
|
||||
}
|
||||
|
||||
pub fn resolve_content_paths(args: ContentPathInfo) -> Vec<String> {
|
||||
let root = Path::new(&args.base);
|
||||
|
||||
let allowed_paths = FxHashSet::from_iter(
|
||||
WalkBuilder::new(root)
|
||||
.hidden(false)
|
||||
.filter_entry(|entry| match entry.file_type() {
|
||||
Some(file_type) if file_type.is_dir() => entry
|
||||
.file_name()
|
||||
.to_str()
|
||||
.map(|s| s != ".git")
|
||||
.unwrap_or(false),
|
||||
_ => true,
|
||||
})
|
||||
.build()
|
||||
.filter_map(Result::ok)
|
||||
.map(|x| x.into_path()),
|
||||
);
|
||||
|
||||
// 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()),
|
||||
);
|
||||
|
||||
// A list of static files that we should track directly.
|
||||
let mut static_files = vec![];
|
||||
|
||||
// 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.
|
||||
else {
|
||||
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());
|
||||
}
|
||||
|
||||
// If the current path is a file inside any of the root directories, then we have to
|
||||
// track it directly as a static file.
|
||||
if let Some(parent_path) = path.parent() {
|
||||
if root_directories.contains(parent_path) {
|
||||
static_files.push(format!("{}", path.display()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the globs for all globable directories.
|
||||
let shallow_globs = shallow_globable_directories
|
||||
.iter()
|
||||
.map(|path| format!("{}", path.display()));
|
||||
let deep_globs = deep_globable_directories
|
||||
.iter()
|
||||
.map(|path| format!("{}/**", path.display()));
|
||||
let globs = shallow_globs
|
||||
.chain(deep_globs)
|
||||
.flat_map(|path| match found_extensions.len() {
|
||||
0 => None, // This should never happen
|
||||
1 => Some(format!(
|
||||
"{}/*.{}",
|
||||
path,
|
||||
found_extensions.iter().next().unwrap()
|
||||
)),
|
||||
_ => Some(format!(
|
||||
"{}/*.{{{}}}",
|
||||
path,
|
||||
found_extensions
|
||||
.iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
)),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
static_files.extend(globs);
|
||||
static_files
|
||||
}
|
||||
|
||||
pub fn is_git_ignored_content_path(base: &Path, path: &Path) -> bool {
|
||||
!WalkBuilder::new(base)
|
||||
.hidden(false)
|
||||
.filter_entry(|entry| match entry.file_type() {
|
||||
Some(file_type) if file_type.is_dir() => entry
|
||||
.file_name()
|
||||
.to_str()
|
||||
.map(|s| s != ".git")
|
||||
.unwrap_or(false),
|
||||
_ => true,
|
||||
})
|
||||
.build()
|
||||
.filter_map(Result::ok)
|
||||
.any(|e| e.path() == path)
|
||||
}
|
||||
|
||||
pub fn is_allowed_content_path(path: &Path) -> bool {
|
||||
let binary_extensions = include_str!("fixtures/binary-extensions.txt")
|
||||
.trim()
|
||||
.lines()
|
||||
.collect::<Vec<_>>();
|
||||
let ignored_extensions = include_str!("fixtures/ignored-extensions.txt")
|
||||
.trim()
|
||||
.lines()
|
||||
.collect::<Vec<_>>();
|
||||
let ignored_files = include_str!("fixtures/ignored-files.txt")
|
||||
.trim()
|
||||
.lines()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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
|
||||
return 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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_candidate_strings(input: Vec<ChangedContent>, options: u8) -> Vec<String> {
|
||||
init_tracing();
|
||||
|
||||
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_sync(read_all_files(input)),
|
||||
(IO::Parallel, Parsing::Sequential) => parse_all_blobs(read_all_files_sync(input)),
|
||||
(IO::Parallel, Parsing::Parallel) => parse_all_blobs(read_all_files(input)),
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(changed_content))]
|
||||
|
|
@ -41,6 +358,30 @@ fn read_all_files(changed_content: Vec<ChangedContent>) -> Vec<Vec<u8>> {
|
|||
|
||||
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(),
|
||||
|
|
@ -72,3 +413,27 @@ fn parse_all_blobs(blobs: Vec<Vec<u8>>) -> Vec<String> {
|
|||
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
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
248
oxide/crates/core/tests/auto_content.rs
Normal file
248
oxide/crates/core/tests/auto_content.rs
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
#[cfg(test)]
|
||||
mod auto_content {
|
||||
use std::fs;
|
||||
use std::process::Command;
|
||||
|
||||
use tailwindcss_core::*;
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn test(paths_with_content: &[(&str, Option<&str>)]) -> 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 mut paths: Vec<_> = resolve_content_paths(ContentPathInfo { base: base.clone() })
|
||||
.into_iter()
|
||||
.map(|x| x.replace(&format!("{}/", &base), ""))
|
||||
.collect();
|
||||
|
||||
// Sort the output for easier comparison (depending on internal datastructure the order
|
||||
// _could_ be random)
|
||||
paths.sort();
|
||||
|
||||
paths
|
||||
}
|
||||
|
||||
#[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}"]);
|
||||
}
|
||||
|
||||
#[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![
|
||||
// Listed explicitly because they are in the root
|
||||
"bar.html",
|
||||
"baz.html",
|
||||
"foo.html",
|
||||
|
||||
// Listed using a deep glob, because nothing inside is ignored
|
||||
"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}",
|
||||
|
||||
// Listed using a deep glob, because nothing inside is ignored (but contains deeply
|
||||
// nested folders)
|
||||
"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}",
|
||||
|
||||
// Listed using a shallow glob, because `nested-c` contains ignored folders, deeply
|
||||
// nested globs can't be used anymore
|
||||
"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}",
|
||||
|
||||
// Deeply nested folder can use a deep glob again
|
||||
"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}",
|
||||
|
||||
// The deeply nested folder can use a deep glob, however all of its parents can
|
||||
// only contain a shallow glob because of the ignored folder
|
||||
"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/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/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}",
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,8 +8,8 @@ crate-type = ["cdylib"]
|
|||
|
||||
[dependencies]
|
||||
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
|
||||
napi = { version = "2.10.0", default-features = false, features = ["napi4"] }
|
||||
napi-derive = "2.11.1"
|
||||
napi = { version = "2.13.1", default-features = false, features = ["napi4"] }
|
||||
napi-derive = "2.13.0"
|
||||
tailwindcss-core = { path = "../core" }
|
||||
rayon = "1.5.3"
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.15.2"
|
||||
"@napi-rs/cli": "^2.16.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use napi::bindgen_prelude::{FromNapiValue, ToNapiValue};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[macro_use]
|
||||
|
|
@ -11,16 +12,42 @@ pub struct ChangedContent {
|
|||
pub extension: String,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn parse_candidate_strings_from_files(changed_content: Vec<ChangedContent>) -> Vec<String> {
|
||||
tailwindcss_core::parse_candidate_strings_from_files(
|
||||
changed_content
|
||||
.into_iter()
|
||||
.map(|changed_content| tailwindcss_core::ChangedContent {
|
||||
file: changed_content.file.map(PathBuf::from),
|
||||
content: changed_content.content,
|
||||
extension: changed_content.extension,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
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,
|
||||
extension: changed_content.extension,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[napi(object)]
|
||||
pub struct ContentPathInfo {
|
||||
pub base: String,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn resolve_content_paths(args: ContentPathInfo) -> Vec<String> {
|
||||
tailwindcss_core::resolve_content_paths(tailwindcss_core::ContentPathInfo { base: args.base })
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[napi]
|
||||
pub enum IO {
|
||||
Sequential = 0b0001,
|
||||
Parallel = 0b0010,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[napi]
|
||||
pub enum Parsing {
|
||||
Sequential = 0b0100,
|
||||
Parallel = 0b1000,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn parse_candidate_strings(input: Vec<ChangedContent>, strategy: u8) -> Vec<String> {
|
||||
tailwindcss_core::parse_candidate_strings(input.into_iter().map(Into::into).collect(), strategy)
|
||||
}
|
||||
|
|
|
|||
5049
package-lock.json
generated
5049
package-lock.json
generated
File diff suppressed because it is too large
Load diff
13998
package-lock.stable.json
13998
package-lock.stable.json
File diff suppressed because it is too large
Load diff
36
package.json
36
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "tailwindcss",
|
||||
"version": "3.3.2",
|
||||
"version": "0.0.0-experimental.4",
|
||||
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
|
||||
"license": "MIT",
|
||||
"main": "lib/index.js",
|
||||
|
|
@ -16,12 +16,9 @@
|
|||
"integrations/*",
|
||||
"oxide/crates/node"
|
||||
],
|
||||
"tailwindcss": {
|
||||
"engine": "oxide"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "concurrently -n tailwind,oxide-node -c green,yellow 'npm run dev:js' 'npm run dev:rust'",
|
||||
"build": "npm run build:rust && npm run build:js && npm run build:peers",
|
||||
"build": "npm run build:rust && npm run build:js",
|
||||
"test": "jest",
|
||||
"test:integrations": "npm run test -w ./integrations",
|
||||
"style": "eslint .",
|
||||
|
|
@ -29,8 +26,7 @@
|
|||
"dev:rust": "npm run --prefix ./oxide dev:node",
|
||||
"dev:js": "npm run build:js -- --watch",
|
||||
"build:rust": "npm run --prefix ./oxide build:node",
|
||||
"build:js": "npm run generate:plugin-list && swc src --out-dir lib --copy-files --delete-dir-on-start --config jsc.transform.optimizer.globals.vars.__OXIDE__='\"true\"'",
|
||||
"build:peers": "esbuild lib/cli-peer-dependencies.js --bundle --platform=node --outfile=peers/index.js --define:process.env.CSS_TRANSFORMER_WASM=false",
|
||||
"build:js": "npm run generate:plugin-list && swc src --out-dir lib --copy-files --delete-dir-on-start",
|
||||
"generate:plugin-list": "node -r @swc/register scripts/create-plugin-list.js",
|
||||
"generate:types": "node -r @swc/register scripts/generate-types.js"
|
||||
},
|
||||
|
|
@ -38,7 +34,6 @@
|
|||
"src/*",
|
||||
"cli/*",
|
||||
"lib/*",
|
||||
"peers/*",
|
||||
"scripts/*.js",
|
||||
"stubs/*",
|
||||
"nesting/*",
|
||||
|
|
@ -49,14 +44,11 @@
|
|||
],
|
||||
"devDependencies": {
|
||||
"@swc/cli": "^0.1.62",
|
||||
"@swc/core": "^1.3.55",
|
||||
"@swc/core": "^1.3.62",
|
||||
"@swc/jest": "^0.2.26",
|
||||
"@swc/register": "^0.1.10",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"concurrently": "^8.0.1",
|
||||
"cssnano": "^6.0.0",
|
||||
"esbuild": "^0.17.18",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint": "^8.42.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"jest": "^29.5.0",
|
||||
|
|
@ -68,9 +60,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@tailwindcss/oxide": "file:oxide/crates/node",
|
||||
"@tailwindcss/oxide": "0.0.0-insiders.776d5bf",
|
||||
"arg": "^5.0.2",
|
||||
"browserslist": "^4.21.5",
|
||||
"browserslist": "^4.21.7",
|
||||
"chokidar": "^3.5.3",
|
||||
"didyoumean": "^1.2.2",
|
||||
"dlv": "^1.1.3",
|
||||
|
|
@ -78,27 +70,25 @@
|
|||
"glob-parent": "^6.0.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"jiti": "^1.18.2",
|
||||
"lightningcss": "^1.18.0",
|
||||
"lightningcss": "^1.21.0",
|
||||
"lilconfig": "^2.1.0",
|
||||
"micromatch": "^4.0.5",
|
||||
"normalize-path": "^3.0.0",
|
||||
"object-hash": "^3.0.0",
|
||||
"picocolors": "^1.0.0",
|
||||
"postcss": "^8.4.23",
|
||||
"postcss": "^8.4.24",
|
||||
"postcss-import": "^15.1.0",
|
||||
"postcss-js": "^4.0.1",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"postcss-nested": "^6.0.1",
|
||||
"postcss-selector-parser": "^6.0.11",
|
||||
"postcss-selector-parser": "^6.0.12",
|
||||
"postcss-value-parser": "^4.2.0",
|
||||
"resolve": "^1.22.2",
|
||||
"resolve": "^1.22.3",
|
||||
"sucrase": "^3.32.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"not edge <= 18",
|
||||
"not ie 11",
|
||||
"not op_mini all"
|
||||
"defaults and supports css-variables and supports css-matches-pseudo",
|
||||
"safari >= 14"
|
||||
],
|
||||
"jest": {
|
||||
"testTimeout": 30000,
|
||||
|
|
|
|||
|
|
@ -1,122 +0,0 @@
|
|||
{
|
||||
"name": "tailwindcss",
|
||||
"version": "3.3.2",
|
||||
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
|
||||
"license": "MIT",
|
||||
"main": "lib/index.js",
|
||||
"types": "types/index.d.ts",
|
||||
"repository": "https://github.com/tailwindlabs/tailwindcss.git",
|
||||
"bugs": "https://github.com/tailwindlabs/tailwindcss/issues",
|
||||
"homepage": "https://tailwindcss.com",
|
||||
"bin": {
|
||||
"tailwind": "lib/cli.js",
|
||||
"tailwindcss": "lib/cli.js"
|
||||
},
|
||||
"tailwindcss": {
|
||||
"engine": "stable"
|
||||
},
|
||||
"scripts": {
|
||||
"prebuild": "npm run generate && rimraf lib",
|
||||
"build": "swc src --out-dir lib --copy-files --config jsc.transform.optimizer.globals.vars.__OXIDE__='\"false\"'",
|
||||
"postbuild": "esbuild lib/cli-peer-dependencies.js --bundle --platform=node --outfile=peers/index.js --define:process.env.CSS_TRANSFORMER_WASM=false",
|
||||
"rebuild-fixtures": "npm run build && node -r @swc/register scripts/rebuildFixtures.js",
|
||||
"style": "eslint .",
|
||||
"pretest": "npm run generate",
|
||||
"test": "jest",
|
||||
"test:integrations": "npm run test --prefix ./integrations",
|
||||
"install:integrations": "node scripts/install-integrations.js",
|
||||
"generate:plugin-list": "node -r @swc/register scripts/create-plugin-list.js",
|
||||
"generate:types": "node -r @swc/register scripts/generate-types.js",
|
||||
"generate": "npm run generate:plugin-list && npm run generate:types",
|
||||
"release-channel": "node ./scripts/release-channel.js",
|
||||
"release-notes": "node ./scripts/release-notes.js",
|
||||
"prepublishOnly": "npm install --force && npm run build"
|
||||
},
|
||||
"files": [
|
||||
"src/*",
|
||||
"cli/*",
|
||||
"lib/*",
|
||||
"peers/*",
|
||||
"scripts/*.js",
|
||||
"stubs/*",
|
||||
"nesting/*",
|
||||
"types/**/*",
|
||||
"*.d.ts",
|
||||
"*.css",
|
||||
"*.js"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@swc/cli": "^0.1.62",
|
||||
"@swc/core": "^1.3.55",
|
||||
"@swc/jest": "^0.2.26",
|
||||
"@swc/register": "^0.1.10",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"browserslist": "^4.21.5",
|
||||
"concurrently": "^8.0.1",
|
||||
"cssnano": "^6.0.0",
|
||||
"esbuild": "^0.17.18",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"jest": "^29.5.0",
|
||||
"jest-diff": "^29.5.0",
|
||||
"lightningcss": "1.18.0",
|
||||
"prettier": "^2.8.8",
|
||||
"rimraf": "^5.0.0",
|
||||
"source-map-js": "^1.0.2",
|
||||
"turbo": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
"chokidar": "^3.5.3",
|
||||
"didyoumean": "^1.2.2",
|
||||
"dlv": "^1.1.3",
|
||||
"fast-glob": "^3.2.12",
|
||||
"glob-parent": "^6.0.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"jiti": "^1.18.2",
|
||||
"lilconfig": "^2.1.0",
|
||||
"micromatch": "^4.0.5",
|
||||
"normalize-path": "^3.0.0",
|
||||
"object-hash": "^3.0.0",
|
||||
"picocolors": "^1.0.0",
|
||||
"postcss": "^8.4.23",
|
||||
"postcss-import": "^15.1.0",
|
||||
"postcss-js": "^4.0.1",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"postcss-nested": "^6.0.1",
|
||||
"postcss-selector-parser": "^6.0.11",
|
||||
"postcss-value-parser": "^4.2.0",
|
||||
"resolve": "^1.22.2",
|
||||
"sucrase": "^3.32.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"not edge <= 18",
|
||||
"not ie 11",
|
||||
"not op_mini all"
|
||||
],
|
||||
"jest": {
|
||||
"testTimeout": 30000,
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/jest/customMatchers.js"
|
||||
],
|
||||
"testPathIgnorePatterns": [
|
||||
"/node_modules/",
|
||||
"/integrations/",
|
||||
"/standalone-cli/",
|
||||
"\\.test\\.skip\\.js$"
|
||||
],
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!lightningcss)"
|
||||
],
|
||||
"transform": {
|
||||
"\\.js$": "@swc/jest",
|
||||
"\\.ts$": "@swc/jest"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
let fs = require('fs')
|
||||
let path = require('path')
|
||||
|
||||
let engines = {
|
||||
stable: {
|
||||
files: [
|
||||
path.resolve(__dirname, '..', 'package.stable.json'),
|
||||
path.resolve(__dirname, '..', 'package-lock.stable.json'),
|
||||
],
|
||||
},
|
||||
oxide: {
|
||||
files: [
|
||||
path.resolve(__dirname, '..', 'package.oxide.json'),
|
||||
path.resolve(__dirname, '..', 'package-lock.oxide.json'),
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
// Find out what the current engine is that we are using:
|
||||
let [otherEngine, info] = Object.entries(engines).find(([, info]) =>
|
||||
info.files.every((file) => fs.existsSync(file))
|
||||
)
|
||||
let currentEngine = otherEngine === 'oxide' ? 'stable' : 'oxide'
|
||||
|
||||
console.log(`Current engine: \`${currentEngine}\`, swapping to \`${otherEngine}\``)
|
||||
|
||||
// Swap the engines
|
||||
for (let file of info.files) {
|
||||
fs.renameSync(
|
||||
file.replace(`.${otherEngine}`, ''),
|
||||
file.replace(`.${otherEngine}`, `.${currentEngine}`)
|
||||
)
|
||||
}
|
||||
for (let file of engines[otherEngine].files) {
|
||||
fs.renameSync(file, file.replace(`.${otherEngine}`, ''))
|
||||
}
|
||||
|
||||
console.log(
|
||||
'Engines have been swapped. Make sure to run `npm install` to update your dependencies.'
|
||||
)
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
export function lazyPostcss() {
|
||||
return require('postcss')
|
||||
}
|
||||
|
||||
export function lazyPostcssImport() {
|
||||
return require('postcss-import')
|
||||
}
|
||||
|
||||
export function lazyAutoprefixer() {
|
||||
return require('autoprefixer')
|
||||
}
|
||||
|
||||
export function lazyCssnano() {
|
||||
return require('cssnano')
|
||||
}
|
||||
|
|
@ -1,7 +1,3 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
if (__OXIDE__) {
|
||||
module.exports = require('./oxide/cli')
|
||||
} else {
|
||||
module.exports = require('./cli/index')
|
||||
}
|
||||
module.exports = require('./cli/index')
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
// @ts-check
|
||||
|
||||
import {
|
||||
// @ts-ignore
|
||||
lazyPostcss,
|
||||
|
||||
// @ts-ignore
|
||||
lazyPostcssImport,
|
||||
|
||||
// @ts-ignore
|
||||
lazyCssnano,
|
||||
|
||||
// @ts-ignore
|
||||
lazyAutoprefixer,
|
||||
} from '../../../peers/index.js'
|
||||
|
||||
/**
|
||||
* @returns {import('postcss')}
|
||||
*/
|
||||
export function loadPostcss() {
|
||||
// Try to load a local `postcss` version first
|
||||
try {
|
||||
return require('postcss')
|
||||
} catch {}
|
||||
|
||||
return lazyPostcss()
|
||||
}
|
||||
|
||||
export function loadPostcssImport() {
|
||||
// Try to load a local `postcss-import` version first
|
||||
try {
|
||||
return require('postcss-import')
|
||||
} catch {}
|
||||
|
||||
return lazyPostcssImport()
|
||||
}
|
||||
|
||||
export function loadCssNano() {
|
||||
let options = { preset: ['default', { cssDeclarationSorter: false }] }
|
||||
|
||||
// Try to load a local `cssnano` version first
|
||||
try {
|
||||
return require('cssnano')
|
||||
} catch {}
|
||||
|
||||
return lazyCssnano()(options)
|
||||
}
|
||||
|
||||
export function loadAutoprefixer() {
|
||||
// Try to load a local `autoprefixer` version first
|
||||
try {
|
||||
return require('autoprefixer')
|
||||
} catch {}
|
||||
|
||||
return lazyAutoprefixer()
|
||||
}
|
||||
|
|
@ -25,6 +25,10 @@ export async function build(args) {
|
|||
process.exit(9)
|
||||
}
|
||||
|
||||
if (args['--no-autoprefixer']) {
|
||||
console.error('[deprecation] The --no-autoprefixer flag is deprecated and has no effect.')
|
||||
}
|
||||
|
||||
// TODO: Reference the @config path here if exists
|
||||
let configPath = args['--config'] ? args['--config'] : resolveDefaultConfigPath()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
// @ts-check
|
||||
|
||||
import pkg from '../../../package.json'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import postcss from 'postcss'
|
||||
import postcssrc from 'postcss-load-config'
|
||||
import browserslist from 'browserslist'
|
||||
import lightning, { Features } from 'lightningcss'
|
||||
import { lilconfig } from 'lilconfig'
|
||||
import loadPlugins from 'postcss-load-config/src/plugins' // Little bit scary, looking at private/internal API
|
||||
import loadOptions from 'postcss-load-config/src/options' // Little bit scary, looking at private/internal API
|
||||
|
||||
import tailwind from '../../processTailwindFeatures'
|
||||
import { loadAutoprefixer, loadCssNano, loadPostcss, loadPostcssImport } from './deps'
|
||||
import { formatNodes, drainStdin, outputFile } from './utils'
|
||||
import { env } from '../../lib/sharedState'
|
||||
import resolveConfig from '../../../resolveConfig.js'
|
||||
|
|
@ -19,6 +22,50 @@ import { findAtConfigPath } from '../../lib/findAtConfigPath.js'
|
|||
import log from '../../util/log'
|
||||
import { loadConfig } from '../../lib/load-config'
|
||||
import getModuleDependencies from '../../lib/getModuleDependencies'
|
||||
import { validateConfig } from '../../util/validateConfig'
|
||||
import { handleImportAtRules } from '../../lib/handleImportAtRules'
|
||||
import { flagEnabled } from '../../featureFlags'
|
||||
import { CssBasedConfig } from '../../lib/parseCssConfig.js'
|
||||
|
||||
async function lightningcss(result, { map = true, minify = true } = {}) {
|
||||
try {
|
||||
let resolvedBrowsersListConfig = browserslist.findConfig(
|
||||
result.opts.from ?? process.cwd()
|
||||
)?.defaults
|
||||
let defaultBrowsersListConfig = pkg.browserslist
|
||||
let browsersListConfig = resolvedBrowsersListConfig ?? defaultBrowsersListConfig
|
||||
|
||||
let transformed = lightning.transform({
|
||||
filename: result.opts.from || 'input.css',
|
||||
code: Buffer.from(result.css, 'utf-8'),
|
||||
minify,
|
||||
sourceMap: result.map === undefined ? map : !!result.map,
|
||||
inputSourceMap: result.map ? result.map.toString() : undefined,
|
||||
targets: lightning.browserslistToTargets(browserslist(browsersListConfig)),
|
||||
drafts: {
|
||||
nesting: true,
|
||||
},
|
||||
include: Features.Nesting,
|
||||
exclude: Features.LogicalProperties,
|
||||
})
|
||||
|
||||
return Object.assign(result, {
|
||||
css: transformed.code.toString('utf8'),
|
||||
map: result.map
|
||||
? Object.assign(result.map, {
|
||||
toString() {
|
||||
return transformed.map?.toString()
|
||||
},
|
||||
})
|
||||
: result.map,
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Unable to use Lightning CSS. Using raw version instead.')
|
||||
console.error(err)
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
@ -75,39 +122,6 @@ async function loadPostCssPlugins(customPostCssPath) {
|
|||
return [beforePlugins, afterPlugins, config.options]
|
||||
}
|
||||
|
||||
function loadBuiltinPostcssPlugins() {
|
||||
let postcss = loadPostcss()
|
||||
let IMPORT_COMMENT = '__TAILWIND_RESTORE_IMPORT__: '
|
||||
return [
|
||||
[
|
||||
(root) => {
|
||||
root.walkAtRules('import', (rule) => {
|
||||
if (rule.params.slice(1).startsWith('tailwindcss/')) {
|
||||
rule.after(postcss.comment({ text: IMPORT_COMMENT + rule.params }))
|
||||
rule.remove()
|
||||
}
|
||||
})
|
||||
},
|
||||
loadPostcssImport(),
|
||||
(root) => {
|
||||
root.walkComments((rule) => {
|
||||
if (rule.text.startsWith(IMPORT_COMMENT)) {
|
||||
rule.after(
|
||||
postcss.atRule({
|
||||
name: 'import',
|
||||
params: rule.text.replace(IMPORT_COMMENT, ''),
|
||||
})
|
||||
)
|
||||
rule.remove()
|
||||
}
|
||||
})
|
||||
},
|
||||
],
|
||||
[],
|
||||
{},
|
||||
]
|
||||
}
|
||||
|
||||
let state = {
|
||||
/** @type {any} */
|
||||
context: null,
|
||||
|
|
@ -118,7 +132,7 @@ let state = {
|
|||
/** @type {{content: string, extension: string}[]} */
|
||||
changedContent: [],
|
||||
|
||||
/** @type {ReturnType<typeof load> | null} */
|
||||
/** @type {{config: import('../../../types').Config, dependencies: Set<string>, dispose: Function } | null} */
|
||||
configBag: null,
|
||||
|
||||
contextDependencies: new Set(),
|
||||
|
|
@ -143,7 +157,7 @@ let state = {
|
|||
}
|
||||
},
|
||||
|
||||
loadConfig(configPath, content) {
|
||||
loadConfig(configPath, content, cssThemeValues) {
|
||||
if (this.watcher && configPath) {
|
||||
this.refreshConfigDependencies()
|
||||
}
|
||||
|
|
@ -160,8 +174,7 @@ let state = {
|
|||
},
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
this.configBag.config = resolveConfig(this.configBag.config, { content: { files: [] } })
|
||||
this.configBag.config = validateConfig(resolveConfig(this.configBag.config, { theme: cssThemeValues }))
|
||||
|
||||
// Override content files if `--content` has been passed explicitly
|
||||
if (content?.length > 0) {
|
||||
|
|
@ -185,7 +198,7 @@ let state = {
|
|||
let files = fastGlob.sync(this.contentPatterns.all)
|
||||
|
||||
for (let file of files) {
|
||||
if (__OXIDE__) {
|
||||
if (flagEnabled(this.config, 'oxideParser')) {
|
||||
content.push({
|
||||
file,
|
||||
extension: path.extname(file).slice(1),
|
||||
|
|
@ -210,7 +223,7 @@ let state = {
|
|||
return content
|
||||
},
|
||||
|
||||
getContext({ createContext, cliConfigPath, root, result, content }) {
|
||||
getContext({ createContext, cliConfigPath, root, result, content, cssThemeValues }) {
|
||||
if (this.context) {
|
||||
this.context.changedContent = this.changedContent.splice(0)
|
||||
|
||||
|
|
@ -222,7 +235,7 @@ let state = {
|
|||
env.DEBUG && console.timeEnd('Searching for config')
|
||||
|
||||
env.DEBUG && console.time('Loading config')
|
||||
let config = this.loadConfig(configPath, content)
|
||||
let config = this.loadConfig(configPath, content, cssThemeValues)
|
||||
env.DEBUG && console.timeEnd('Loading config')
|
||||
|
||||
env.DEBUG && console.time('Creating context')
|
||||
|
|
@ -251,8 +264,6 @@ let state = {
|
|||
}
|
||||
|
||||
export async function createProcessor(args, cliConfigPath) {
|
||||
let postcss = loadPostcss()
|
||||
|
||||
let input = args['--input']
|
||||
let output = args['--output']
|
||||
let includePostCss = args['--postcss']
|
||||
|
|
@ -260,7 +271,9 @@ export async function createProcessor(args, cliConfigPath) {
|
|||
|
||||
let [beforePlugins, afterPlugins, postcssOptions] = includePostCss
|
||||
? await loadPostCssPlugins(customPostCssPath)
|
||||
: loadBuiltinPostcssPlugins()
|
||||
: [[], [], {}]
|
||||
|
||||
beforePlugins.unshift(...handleImportAtRules())
|
||||
|
||||
if (args['--purge']) {
|
||||
log.warn('purge-flag-deprecated', [
|
||||
|
|
@ -279,6 +292,9 @@ export async function createProcessor(args, cliConfigPath) {
|
|||
return {
|
||||
postcssPlugin: 'tailwindcss',
|
||||
Once(root, { result }) {
|
||||
// Parse any CSS-based config values from `:theme` rules
|
||||
let cssThemeValues = new CssBasedConfig().parse(root)
|
||||
|
||||
env.DEBUG && console.time('Compiling CSS')
|
||||
tailwind(({ createContext }) => {
|
||||
console.error()
|
||||
|
|
@ -291,6 +307,7 @@ export async function createProcessor(args, cliConfigPath) {
|
|||
root,
|
||||
result,
|
||||
content,
|
||||
cssThemeValues,
|
||||
})
|
||||
}
|
||||
})(root, result)
|
||||
|
|
@ -306,8 +323,6 @@ export async function createProcessor(args, cliConfigPath) {
|
|||
tailwindPlugin,
|
||||
!args['--minify'] && formatNodes,
|
||||
...afterPlugins,
|
||||
!args['--no-autoprefixer'] && loadAutoprefixer(),
|
||||
args['--minify'] && loadCssNano(),
|
||||
].filter(Boolean)
|
||||
|
||||
/** @type {import('postcss').Processor} */
|
||||
|
|
@ -331,9 +346,20 @@ export async function createProcessor(args, cliConfigPath) {
|
|||
|
||||
async function build() {
|
||||
let start = process.hrtime.bigint()
|
||||
let options = {
|
||||
...postcssOptions,
|
||||
from: input,
|
||||
to: output,
|
||||
}
|
||||
|
||||
return readInput()
|
||||
.then((css) => processor.process(css, { ...postcssOptions, from: input, to: output }))
|
||||
.then((css) => processor.process(css, options))
|
||||
.then((result) =>
|
||||
lightningcss(result, {
|
||||
...options,
|
||||
minify: !!args['--minify'],
|
||||
})
|
||||
)
|
||||
.then((result) => {
|
||||
if (!state.watcher) {
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ let commands = {
|
|||
description: 'Path to a custom config file',
|
||||
},
|
||||
'--no-autoprefixer': {
|
||||
deprecated: true,
|
||||
type: Boolean,
|
||||
description: 'Disable autoprefixer',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -386,6 +386,26 @@ export let variantPlugins = {
|
|||
)
|
||||
},
|
||||
|
||||
hasVariants: ({ matchVariant }) => {
|
||||
matchVariant('has', (value) => `&:has(${normalize(value)})`, { values: {} })
|
||||
matchVariant(
|
||||
'group-has',
|
||||
(value, { modifier }) =>
|
||||
modifier
|
||||
? `:merge(.group\\/${modifier}):has(${normalize(value)}) &`
|
||||
: `:merge(.group):has(${normalize(value)}) &`,
|
||||
{ values: {} }
|
||||
)
|
||||
matchVariant(
|
||||
'peer-has',
|
||||
(value, { modifier }) =>
|
||||
modifier
|
||||
? `:merge(.peer\\/${modifier}):has(${normalize(value)}) ~ &`
|
||||
: `:merge(.peer):has(${normalize(value)}) ~ &`,
|
||||
{ values: {} }
|
||||
)
|
||||
},
|
||||
|
||||
ariaVariants: ({ matchVariant, theme }) => {
|
||||
matchVariant('aria', (value) => `&[aria-${normalize(value)}]`, { values: theme('aria') ?? {} })
|
||||
matchVariant(
|
||||
|
|
@ -1255,13 +1275,13 @@ export let corePlugins = {
|
|||
],
|
||||
]),
|
||||
|
||||
space: ({ matchUtilities, addUtilities, theme }) => {
|
||||
space: ({ matchUtilities, addUtilities, theme, config }) => {
|
||||
matchUtilities(
|
||||
{
|
||||
'space-x': (value) => {
|
||||
value = value === '0' ? '0px' : value
|
||||
|
||||
if (__OXIDE__) {
|
||||
if (flagEnabled(config(), 'logicalSiblingUtilities')) {
|
||||
return {
|
||||
'& > :not([hidden]) ~ :not([hidden])': {
|
||||
'--tw-space-x-reverse': '0',
|
||||
|
|
@ -1300,13 +1320,13 @@ export let corePlugins = {
|
|||
})
|
||||
},
|
||||
|
||||
divideWidth: ({ matchUtilities, addUtilities, theme }) => {
|
||||
divideWidth: ({ matchUtilities, addUtilities, theme, config }) => {
|
||||
matchUtilities(
|
||||
{
|
||||
'divide-x': (value) => {
|
||||
value = value === '0' ? '0px' : value
|
||||
|
||||
if (__OXIDE__) {
|
||||
if (flagEnabled(config(), 'logicalSiblingUtilities')) {
|
||||
return {
|
||||
'& > :not([hidden]) ~ :not([hidden])': {
|
||||
'@defaults border-width': {},
|
||||
|
|
@ -1503,6 +1523,14 @@ export let corePlugins = {
|
|||
})
|
||||
},
|
||||
|
||||
textWrap: ({ addUtilities }) => {
|
||||
addUtilities({
|
||||
'.text-wrap': { 'text-wrap': 'wrap' },
|
||||
'.text-nowrap': { 'text-wrap': 'nowrap' },
|
||||
'.text-balance': { 'text-wrap': 'balance' },
|
||||
})
|
||||
},
|
||||
|
||||
wordBreak: ({ addUtilities }) => {
|
||||
addUtilities({
|
||||
'.break-normal': { 'overflow-wrap': 'normal', 'word-break': 'normal' },
|
||||
|
|
|
|||
|
|
@ -163,6 +163,8 @@ optgroup,
|
|||
select,
|
||||
textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-feature-settings: inherit; /* 1 */
|
||||
font-variation-settings: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
font-weight: inherit; /* 1 */
|
||||
line-height: inherit; /* 1 */
|
||||
|
|
@ -264,7 +266,7 @@ summary {
|
|||
}
|
||||
|
||||
/*
|
||||
Removes the default spacing and border for appropriate elements.
|
||||
Removes the default spacing for appropriate elements.
|
||||
*/
|
||||
|
||||
blockquote,
|
||||
|
|
@ -300,6 +302,10 @@ menu {
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
dialog {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent resizing textareas horizontally by default.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -3,25 +3,23 @@ import log from './util/log'
|
|||
|
||||
let defaults = {
|
||||
optimizeUniversalDefaults: false,
|
||||
generalizedModifiers: true,
|
||||
get disableColorOpacityUtilitiesByDefault() {
|
||||
return __OXIDE__
|
||||
},
|
||||
get relativeContentPathsByDefault() {
|
||||
return __OXIDE__
|
||||
},
|
||||
disableColorOpacityUtilitiesByDefault: true,
|
||||
relativeContentPathsByDefault: false,
|
||||
oxideParser: true,
|
||||
logicalSiblingUtilities: false,
|
||||
}
|
||||
|
||||
let featureFlags = {
|
||||
export let featureFlags = {
|
||||
future: [
|
||||
'hoverOnlyWhenSupported',
|
||||
'respectDefaultRingColorOpacity',
|
||||
'disableColorOpacityUtilitiesByDefault',
|
||||
'relativeContentPathsByDefault',
|
||||
'logicalSiblingUtilities',
|
||||
],
|
||||
experimental: [
|
||||
'optimizeUniversalDefaults',
|
||||
'generalizedModifiers',
|
||||
'oxideParser',
|
||||
// 'variantGrouping',
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,40 @@ import fastGlob from 'fast-glob'
|
|||
import normalizePath from 'normalize-path'
|
||||
import { parseGlob } from '../util/parseGlob'
|
||||
import { env } from './sharedState'
|
||||
import { resolveContentPaths } from '@tailwindcss/oxide'
|
||||
|
||||
/** @typedef {import('../../types/config.js').RawFile} RawFile */
|
||||
/** @typedef {import('../../types/config.js').FilePath} FilePath */
|
||||
|
||||
/*
|
||||
* @param {import('tailwindcss').Config} tailwindConfig
|
||||
* @param {{skip:string[]}} options
|
||||
* @returns {ContentPath[]}
|
||||
*/
|
||||
function resolveContentFiles(tailwindConfig, { skip = [] } = {}) {
|
||||
if (
|
||||
Array.isArray(tailwindConfig.content.files) &&
|
||||
tailwindConfig.content.files.includes('auto')
|
||||
) {
|
||||
let idx = tailwindConfig.content.files.indexOf('auto')
|
||||
if (idx !== -1) {
|
||||
env.DEBUG && console.time('Calculating resolve content paths')
|
||||
let resolved = resolveContentPaths({ base: process.cwd() })
|
||||
env.DEBUG && console.timeEnd('Calculating resolve content paths')
|
||||
|
||||
tailwindConfig.content.files.splice(idx, 1, ...resolved)
|
||||
}
|
||||
}
|
||||
|
||||
if (skip.length > 0) {
|
||||
tailwindConfig.content.files = tailwindConfig.content.files.filter(
|
||||
(filePath) => !skip.includes(filePath)
|
||||
)
|
||||
}
|
||||
|
||||
return tailwindConfig.content.files
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} ContentPath
|
||||
* @property {string} original
|
||||
|
|
@ -32,7 +62,9 @@ import { env } from './sharedState'
|
|||
* @returns {ContentPath[]}
|
||||
*/
|
||||
export function parseCandidateFiles(context, tailwindConfig) {
|
||||
let files = tailwindConfig.content.files
|
||||
let files = resolveContentFiles(tailwindConfig, {
|
||||
skip: [context.userConfigPath],
|
||||
})
|
||||
|
||||
// Normalize the file globs
|
||||
files = files.filter((filePath) => typeof filePath === 'string')
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import dlv from 'dlv'
|
||||
import didYouMean from 'didyoumean'
|
||||
import transformThemeValue from '../util/transformThemeValue'
|
||||
import parseValue from 'postcss-value-parser'
|
||||
import parseValue from '../value-parser/index'
|
||||
import { normalizeScreens } from '../util/normalizeScreens'
|
||||
import buildMediaQuery from '../util/buildMediaQuery'
|
||||
import { toPath } from '../util/toPath'
|
||||
|
|
@ -146,6 +146,9 @@ function resolveVNode(node, vNode, functions) {
|
|||
}
|
||||
|
||||
function resolveFunctions(node, input, functions) {
|
||||
let hasAnyFn = Object.keys(functions).some((fn) => input.includes(`${fn}(`))
|
||||
if (!hasAnyFn) return input
|
||||
|
||||
return parseValue(input)
|
||||
.walk((vNode) => {
|
||||
resolveVNode(node, vNode, functions)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import fs from 'fs'
|
||||
import LRU from '@alloc/quick-lru'
|
||||
import { parseCandidateStrings, IO, Parsing } from '@tailwindcss/oxide'
|
||||
import * as sharedState from './sharedState'
|
||||
import { generateRules } from './generateRules'
|
||||
import log from '../util/log'
|
||||
import cloneNodes from '../util/cloneNodes'
|
||||
import { defaultExtractor } from './defaultExtractor'
|
||||
import { flagEnabled } from '../featureFlags'
|
||||
|
||||
let env = sharedState.env
|
||||
|
||||
|
|
@ -24,7 +26,10 @@ function getExtractor(context, fileExtension) {
|
|||
extractors[fileExtension] ||
|
||||
extractors.DEFAULT ||
|
||||
builtInExtractors[fileExtension] ||
|
||||
builtInExtractors.DEFAULT(context)
|
||||
// Because we call `DEFAULT(context)`, the returning function is always a new function without a
|
||||
// stable identity. Marking it with `DEFAULT_EXTRACTOR` allows us to check if it is the default
|
||||
// extractor without relying on the function identity.
|
||||
Object.assign(builtInExtractors.DEFAULT(context), { DEFAULT_EXTRACTOR: true })
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -130,20 +135,36 @@ export default function expandTailwindAtRules(context) {
|
|||
|
||||
env.DEBUG && console.time('Reading changed files')
|
||||
|
||||
if (__OXIDE__) {
|
||||
// TODO: Pass through or implement `extractor`
|
||||
for (let candidate of require('@tailwindcss/oxide').parseCandidateStringsFromFiles(
|
||||
context.changedContent
|
||||
// Object.assign({}, builtInTransformers, context.tailwindConfig.content.transform)
|
||||
)) {
|
||||
candidates.add(candidate)
|
||||
if (flagEnabled(context.tailwindConfig, 'oxideParser')) {
|
||||
let rustParserContent = []
|
||||
let regexParserContent = []
|
||||
|
||||
for (let item of context.changedContent) {
|
||||
let transformer = getTransformer(context.tailwindConfig, item.extension)
|
||||
let extractor = getExtractor(context, item.extension)
|
||||
|
||||
if (transformer === builtInTransformers.DEFAULT && extractor?.DEFAULT_EXTRACTOR === true) {
|
||||
rustParserContent.push(item)
|
||||
} else {
|
||||
regexParserContent.push([item, { transformer, extractor }])
|
||||
}
|
||||
}
|
||||
|
||||
// for (let { file, content, extension } of context.changedContent) {
|
||||
// let transformer = getTransformer(context.tailwindConfig, extension)
|
||||
// let extractor = getExtractor(context, extension)
|
||||
// getClassCandidatesOxide(file, transformer(content), extractor, candidates, seen)
|
||||
// }
|
||||
if (rustParserContent.length > 0) {
|
||||
for (let candidate of parseCandidateStrings(
|
||||
rustParserContent,
|
||||
IO.Parallel | Parsing.Parallel
|
||||
)) {
|
||||
candidates.add(candidate)
|
||||
}
|
||||
}
|
||||
|
||||
if (regexParserContent.length > 0) {
|
||||
for (let [{ file, content }, { transformer, extractor }] of regexParserContent) {
|
||||
content = file ? fs.readFileSync(file, 'utf8') : content
|
||||
getClassCandidates(transformer(content), extractor, candidates, seen)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let { file, content, extension } of context.changedContent) {
|
||||
let transformer = getTransformer(context.tailwindConfig, extension)
|
||||
|
|
@ -162,15 +183,16 @@ export default function expandTailwindAtRules(context) {
|
|||
|
||||
env.DEBUG && console.time('Generate rules')
|
||||
env.DEBUG && console.time('Sorting candidates')
|
||||
let sortedCandidates = __OXIDE__
|
||||
? candidates
|
||||
: new Set(
|
||||
[...candidates].sort((a, z) => {
|
||||
if (a === z) return 0
|
||||
if (a < z) return -1
|
||||
return 1
|
||||
})
|
||||
)
|
||||
// TODO: only sort if we are not using the oxide parser (flagEnabled(context.tailwindConfig,
|
||||
// 'oxideParser')) AND if we got all the candidates form the oxideParser alone. This will not
|
||||
// be the case currently if you have custom transformers / extractors.
|
||||
let sortedCandidates = new Set(
|
||||
[...candidates].sort((a, z) => {
|
||||
if (a === z) return 0
|
||||
if (a < z) return -1
|
||||
return 1
|
||||
})
|
||||
)
|
||||
env.DEBUG && console.timeEnd('Sorting candidates')
|
||||
generateRules(sortedCandidates, context)
|
||||
env.DEBUG && console.timeEnd('Generate rules')
|
||||
|
|
@ -244,11 +266,21 @@ export default function expandTailwindAtRules(context) {
|
|||
)
|
||||
layerNodes.variants.remove()
|
||||
} else if (variantNodes.length > 0) {
|
||||
root.append(
|
||||
cloneNodes(variantNodes, root.source, {
|
||||
layer: 'variants',
|
||||
let cloned = cloneNodes(variantNodes, undefined, {
|
||||
layer: 'variants',
|
||||
})
|
||||
|
||||
cloned.forEach((node) => {
|
||||
let parentLayer = node.raws.tailwind?.parentLayer ?? null
|
||||
|
||||
node.walk((n) => {
|
||||
if (!n.source) {
|
||||
n.source = layerNodes[parentLayer].source
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
root.append(cloned)
|
||||
}
|
||||
|
||||
// If we've got a utility layer and no utilities are generated there's likely something wrong
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ function* candidatePermutations(candidate) {
|
|||
let bracketIdx = candidate.indexOf('[')
|
||||
|
||||
// If character before `[` isn't a dash or a slash, this isn't a dynamic class
|
||||
// eg. string[]
|
||||
// e.g. string[]
|
||||
if (candidate[bracketIdx - 1] === '-') {
|
||||
dashIdx = bracketIdx - 1
|
||||
} else if (candidate[bracketIdx - 1] === '/') {
|
||||
|
|
@ -175,10 +175,6 @@ function applyVariant(variant, matches, context) {
|
|||
if (modifiers.length && !context.variantMap.has(variant)) {
|
||||
variant = baseVariant
|
||||
args.modifier = modifiers[0]
|
||||
|
||||
if (!flagEnabled(context.tailwindConfig, 'generalizedModifiers')) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -193,13 +189,13 @@ function applyVariant(variant, matches, context) {
|
|||
// group[:hover] (`-` is missing)
|
||||
let match = /(.)(-?)\[(.*)\]/g.exec(variant)
|
||||
if (match) {
|
||||
let [, char, seperator, value] = match
|
||||
let [, char, separator, value] = match
|
||||
// @-[200px] case
|
||||
if (char === '@' && seperator === '-') return []
|
||||
if (char === '@' && separator === '-') return []
|
||||
// group[:hover] case
|
||||
if (char !== '@' && seperator === '') return []
|
||||
if (char !== '@' && separator === '') return []
|
||||
|
||||
variant = variant.replace(`${seperator}[${value}]`, '')
|
||||
variant = variant.replace(`${separator}[${value}]`, '')
|
||||
args.value = value
|
||||
}
|
||||
}
|
||||
|
|
@ -459,9 +455,9 @@ function isParsableNode(node) {
|
|||
}
|
||||
|
||||
function isParsableCssValue(property, value) {
|
||||
// We don't want to to treat [https://example.com] as a custom property
|
||||
// We don't want to treat [https://example.com] as a custom property
|
||||
// Even though, according to the CSS grammar, it's a totally valid CSS declaration
|
||||
// So we short-circuit here by checking if the custom property looks like a url
|
||||
// So we short-circuit here by checking if the custom property looks like a URL
|
||||
if (looksLikeUri(`${property}:${value}`)) {
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
34
src/lib/handleImportAtRules.js
Normal file
34
src/lib/handleImportAtRules.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import postcss from 'postcss'
|
||||
import postcssImport from 'postcss-import'
|
||||
|
||||
const TAILWIND = Symbol()
|
||||
|
||||
export function handleImportAtRules() {
|
||||
let RESTORE_ATRULE_COMMENT = '__TAILWIND_RESTORE__'
|
||||
let atRulesToRestore = ['tailwind', 'config']
|
||||
|
||||
return [
|
||||
(root) => {
|
||||
root.walkAtRules((rule) => {
|
||||
if (!atRulesToRestore.includes(rule.name)) return rule
|
||||
|
||||
rule.after(
|
||||
postcss.comment({
|
||||
text: RESTORE_ATRULE_COMMENT,
|
||||
raws: { [TAILWIND]: { rule } },
|
||||
})
|
||||
)
|
||||
rule.remove()
|
||||
})
|
||||
},
|
||||
postcssImport(),
|
||||
(root) => {
|
||||
root.walkComments((rule) => {
|
||||
if (rule.text === RESTORE_ATRULE_COMMENT) {
|
||||
rule.after(rule.raws[TAILWIND].rule)
|
||||
rule.remove()
|
||||
}
|
||||
})
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
@ -10,33 +10,6 @@ export default function normalizeTailwindDirectives(root) {
|
|||
applyDirectives.add(atRule)
|
||||
}
|
||||
|
||||
if (atRule.name === 'import') {
|
||||
if (atRule.params === '"tailwindcss/base"' || atRule.params === "'tailwindcss/base'") {
|
||||
atRule.name = 'tailwind'
|
||||
atRule.params = 'base'
|
||||
} else if (
|
||||
atRule.params === '"tailwindcss/components"' ||
|
||||
atRule.params === "'tailwindcss/components'"
|
||||
) {
|
||||
atRule.name = 'tailwind'
|
||||
atRule.params = 'components'
|
||||
} else if (
|
||||
atRule.params === '"tailwindcss/utilities"' ||
|
||||
atRule.params === "'tailwindcss/utilities'"
|
||||
) {
|
||||
atRule.name = 'tailwind'
|
||||
atRule.params = 'utilities'
|
||||
} else if (
|
||||
atRule.params === '"tailwindcss/screens"' ||
|
||||
atRule.params === "'tailwindcss/screens'" ||
|
||||
atRule.params === '"tailwindcss/variants"' ||
|
||||
atRule.params === "'tailwindcss/variants'"
|
||||
) {
|
||||
atRule.name = 'tailwind'
|
||||
atRule.params = 'variants'
|
||||
}
|
||||
}
|
||||
|
||||
if (atRule.name === 'tailwind') {
|
||||
if (atRule.params === 'screens') {
|
||||
atRule.params = 'variants'
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import { remapBitfield } from './remap-bitfield.js'
|
|||
* @property {bigint} arbitrary 0n if false, 1n if true
|
||||
* @property {bigint} variants Dynamic size. 1 bit per registered variant. 0n means no variants
|
||||
* @property {bigint} parallelIndex Rule index for the parallel variant. 0 if not applicable.
|
||||
* @property {bigint} index Index of the rule / utility in it's given *parent* layer. Monotonically increasing.
|
||||
* @property {bigint} index Index of the rule / utility in its given *parent* layer. Monotonically increasing.
|
||||
* @property {VariantOption[]} options Some information on how we can sort arbitrary variants
|
||||
*/
|
||||
|
||||
|
|
@ -302,7 +302,7 @@ export class Offsets {
|
|||
let mapping = this.recalculateVariantOffsets()
|
||||
|
||||
// No arbitrary variants? Nothing to do.
|
||||
// Everyhing already in order? Nothing to do.
|
||||
// Everything already in order? Nothing to do.
|
||||
if (mapping.length === 0) {
|
||||
return list
|
||||
}
|
||||
|
|
@ -354,7 +354,7 @@ function max(nums) {
|
|||
* Using `.sort()` without a custom compare function is faster
|
||||
* But you can only use that if you're sorting an array of
|
||||
* only strings. If you're sorting strings inside objects
|
||||
* or arrays, you need must use a custom compare function.
|
||||
* or arrays, you need to use a custom compare function.
|
||||
*
|
||||
* @param {string} a
|
||||
* @param {string} b
|
||||
|
|
|
|||
205
src/lib/parseCssConfig.js
Normal file
205
src/lib/parseCssConfig.js
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
// @ts-check
|
||||
|
||||
/**
|
||||
* @typedef {[plugin: string|null, key: string[]|null, option: string|null]} ConfigPath
|
||||
*/
|
||||
|
||||
export class CssBasedConfig {
|
||||
constructor() {
|
||||
this.config = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and convert the `:theme` rule to a configuration object
|
||||
* This is merged with the user's JS config
|
||||
* It should take precedence over the JS config
|
||||
*
|
||||
* @param {import('postcss').Root} root
|
||||
* @returns {*}
|
||||
*/
|
||||
parse(root) {
|
||||
this.config = { extend: {} }
|
||||
|
||||
let rules = this.getThemeRules(root)
|
||||
|
||||
rules.forEach(rule => {
|
||||
rule.walkDecls((decl) => {
|
||||
if (!decl.prop.startsWith('--')) {
|
||||
throw decl.error('Only CSS variables are allowed in `:theme`')
|
||||
}
|
||||
|
||||
this.parseVariable(decl)
|
||||
})
|
||||
|
||||
// Allow the variables to be used with var() anywhere in CSS
|
||||
rule.selector = ':root'
|
||||
})
|
||||
|
||||
return this.config
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk the tree and find all `:theme` rules
|
||||
*
|
||||
* @param {import('postcss').Root} root
|
||||
* @returns {import('postcss').Rule[]}
|
||||
*/
|
||||
getThemeRules(root) {
|
||||
/** @type {any[]} */
|
||||
let rules = root.nodes.filter(node => node.type === 'rule' && node.selector === ':theme')
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a variable declaration and modify the config
|
||||
*
|
||||
* @param {import('postcss').Declaration} decl
|
||||
*/
|
||||
parseVariable(decl) {
|
||||
let config = this.config
|
||||
|
||||
// Parse the variable name into a config plugin and keypath
|
||||
let [name, value] = [decl.prop.slice(2), decl.value]
|
||||
let [plugin, keypath, option] = this.parseVariableName(name)
|
||||
|
||||
if (plugin === null) {
|
||||
// We couldn't find a plugin for this variable, so we can't do anything with it
|
||||
// This also means there is no reason to keep the variable in the CSS
|
||||
decl.remove()
|
||||
return
|
||||
}
|
||||
|
||||
if (keypath === null && value === 'unset') {
|
||||
// when using `--some-plugin: unset` we want to reset whatever the default value is
|
||||
config[plugin] = {}
|
||||
|
||||
// This also means there is no reason to keep the variable in the CSS
|
||||
decl.remove()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (keypath !== null && value === 'unset') {
|
||||
// when using `--plugin-key: unset` we want to remove that from the config
|
||||
throw decl.error("Not implemented yet");
|
||||
}
|
||||
|
||||
// TODO: Can this happen
|
||||
if (keypath === null) {
|
||||
return
|
||||
}
|
||||
|
||||
// All other cases are config keys in a plugin
|
||||
config.extend[plugin] ??= {}
|
||||
|
||||
let [path, key] = [
|
||||
keypath.slice(0, -1),
|
||||
keypath[keypath.length-1],
|
||||
]
|
||||
|
||||
let obj = config.extend[plugin]
|
||||
let segment
|
||||
while (segment = path.shift()) {
|
||||
obj[segment] ??= {}
|
||||
obj = obj[segment]
|
||||
}
|
||||
|
||||
if (option) {
|
||||
// TODO: This is weird
|
||||
if (!Array.isArray(obj[key])) {
|
||||
obj[key] = [obj[key], {}]
|
||||
}
|
||||
|
||||
obj[key][1][option] = value
|
||||
} else {
|
||||
obj[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a variable name into:
|
||||
* - The associated plugin
|
||||
* - A keypath to modify inside plugin's config (if available)
|
||||
* - An "option" which is a key into the "options" object when a Tuple-like value is used
|
||||
*
|
||||
* @param {string} name
|
||||
* @returns {ConfigPath}
|
||||
*/
|
||||
parseVariableName(name) {
|
||||
// Unescape the name if needed
|
||||
// This is needed for CSS variables that contain special characters
|
||||
name = unescape(name)
|
||||
|
||||
for (const [prefix, plugin] of Object.entries(this.prefixPluginMap)) {
|
||||
if (!name.startsWith(prefix)) continue
|
||||
|
||||
// The keypath is the part of the variable name after the prefix
|
||||
let keypath = name.slice(prefix.length)
|
||||
let option = null
|
||||
|
||||
let optionStart = keypath.indexOf('--')
|
||||
if (optionStart !== -1) {
|
||||
option = keypath.slice(optionStart+2)
|
||||
keypath = keypath.slice(0, optionStart)
|
||||
}
|
||||
|
||||
if (keypath == '' && option === 'default') {
|
||||
keypath = 'DEFAULT'
|
||||
option = null
|
||||
} else if (keypath.startsWith('-')) {
|
||||
keypath = keypath.slice(1)
|
||||
}
|
||||
|
||||
if (keypath === '') {
|
||||
return [plugin, null, null]
|
||||
}
|
||||
|
||||
option = option === null ? null : camelize(option);
|
||||
|
||||
if (plugin === 'colors') {
|
||||
return [plugin, keypath.split('-'), option]
|
||||
}
|
||||
|
||||
return [plugin, [keypath], option]
|
||||
}
|
||||
|
||||
return [null, null, null]
|
||||
}
|
||||
|
||||
get prefixPluginMap() {
|
||||
// TODO: Maybe build this based on a list of registered plugins
|
||||
// Or maybe a plugin can register namespaces themselves?
|
||||
return {
|
||||
'animation': 'animation',
|
||||
'colors': 'colors',
|
||||
'spacing': 'spacing',
|
||||
'font-family': 'fontFamily',
|
||||
'font-size': 'fontSize',
|
||||
// 'shadow': 'boxShadow',
|
||||
'blur': 'blur',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
*/
|
||||
function camelize(str) {
|
||||
return str.replace(/-([a-zA-Z])/g, (v) => v.slice(1).toUpperCase())
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
*/
|
||||
function unescape(str) {
|
||||
let pattern = /\\([0-9a-fA-F]{1,6}[\t\n\f\r ]?|[\S\s])/g
|
||||
|
||||
return str.replace(pattern, (match) =>
|
||||
match.length > 2
|
||||
? String.fromCodePoint(parseInt(match.slice(1).trimEnd(), 16))
|
||||
: match[1]
|
||||
)
|
||||
}
|
||||
|
|
@ -21,7 +21,6 @@ import isSyntacticallyValidPropertyValue from '../util/isSyntacticallyValidPrope
|
|||
import { generateRules, getClassNameFromSelector } from './generateRules'
|
||||
import { hasContentChanged } from './cacheInvalidation.js'
|
||||
import { Offsets } from './offsets.js'
|
||||
import { flagEnabled } from '../featureFlags.js'
|
||||
import { finalizeSelector, formatVariantSelector } from '../util/formatVariantSelector'
|
||||
|
||||
const VARIANT_TYPES = {
|
||||
|
|
@ -434,10 +433,8 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
|
|||
},
|
||||
}
|
||||
|
||||
let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
|
||||
|
||||
let ruleSets = []
|
||||
.concat(modifiersEnabled ? rule(value, extras) : rule(value))
|
||||
.concat(rule(value, extras))
|
||||
.filter(Boolean)
|
||||
.map((declaration) => ({
|
||||
[nameClass(identifier, modifier)]: declaration,
|
||||
|
|
@ -514,10 +511,8 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
|
|||
},
|
||||
}
|
||||
|
||||
let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
|
||||
|
||||
let ruleSets = []
|
||||
.concat(modifiersEnabled ? rule(value, extras) : rule(value))
|
||||
.concat(rule(value, extras))
|
||||
.filter(Boolean)
|
||||
.map((declaration) => ({
|
||||
[nameClass(identifier, modifier)]: declaration,
|
||||
|
|
@ -585,18 +580,13 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
|
|||
let id = options?.id ?? ++variantIdentifier
|
||||
let isSpecial = variant === '@'
|
||||
|
||||
let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
|
||||
|
||||
for (let [key, value] of Object.entries(options?.values ?? {})) {
|
||||
if (key === 'DEFAULT') continue
|
||||
|
||||
api.addVariant(
|
||||
isSpecial ? `${variant}${key}` : `${variant}-${key}`,
|
||||
({ args, container }) => {
|
||||
return variantFn(
|
||||
value,
|
||||
modifiersEnabled ? { modifier: args?.modifier, container } : { container }
|
||||
)
|
||||
return variantFn(value, { modifier: args?.modifier, container })
|
||||
},
|
||||
|
||||
{
|
||||
|
|
@ -624,7 +614,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
|
|||
: // Falling back to args if it is a string, otherwise '' for older intellisense
|
||||
// (JetBrains) plugins.
|
||||
args?.value ?? (typeof args === 'string' ? args : ''),
|
||||
modifiersEnabled ? { modifier: args?.modifier, container } : { container }
|
||||
{ modifier: args?.modifier, container }
|
||||
)
|
||||
},
|
||||
{
|
||||
|
|
@ -729,7 +719,7 @@ function collectLayerPlugins(root) {
|
|||
}
|
||||
|
||||
function resolvePlugins(context, root) {
|
||||
let corePluginList = Object.entries({ ...variantPlugins, ...corePlugins })
|
||||
let corePluginList = Object.entries(corePlugins)
|
||||
.map(([name, plugin]) => {
|
||||
if (!context.tailwindConfig.corePlugins.includes(name)) {
|
||||
return null
|
||||
|
|
@ -754,6 +744,7 @@ function resolvePlugins(context, root) {
|
|||
let beforeVariants = [
|
||||
variantPlugins['pseudoElementVariants'],
|
||||
variantPlugins['pseudoClassVariants'],
|
||||
variantPlugins['hasVariants'],
|
||||
variantPlugins['ariaVariants'],
|
||||
variantPlugins['dataVariants'],
|
||||
]
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { validateConfig } from '../util/validateConfig.js'
|
|||
import { parseCandidateFiles, resolvedChangedContent } from './content.js'
|
||||
import { loadConfig } from '../lib/load-config'
|
||||
import getModuleDependencies from './getModuleDependencies'
|
||||
import { CssBasedConfig } from './parseCssConfig.js'
|
||||
|
||||
let configPathCache = new LRU({ maxSize: 100 })
|
||||
|
||||
|
|
@ -28,7 +29,7 @@ function getCandidateFiles(context, tailwindConfig) {
|
|||
}
|
||||
|
||||
// Get the config object based on a path
|
||||
function getTailwindConfig(configOrPath) {
|
||||
function getTailwindConfig(configOrPath, cssThemeValues = {}) {
|
||||
let userConfigPath = resolveConfigPath(configOrPath)
|
||||
|
||||
if (userConfigPath !== null) {
|
||||
|
|
@ -56,7 +57,9 @@ function getTailwindConfig(configOrPath) {
|
|||
for (let file of newDeps) {
|
||||
delete require.cache[file]
|
||||
}
|
||||
let newConfig = validateConfig(resolveConfig(loadConfig(userConfigPath)))
|
||||
let newConfig = validateConfig(
|
||||
resolveConfig(loadConfig(userConfigPath), { theme: cssThemeValues })
|
||||
)
|
||||
let newHash = hash(newConfig)
|
||||
configPathCache.set(userConfigPath, [newConfig, newHash, newDeps, newModified])
|
||||
return [newConfig, userConfigPath, newHash, newDeps]
|
||||
|
|
@ -64,7 +67,8 @@ function getTailwindConfig(configOrPath) {
|
|||
|
||||
// It's a plain object, not a path
|
||||
let newConfig = resolveConfig(
|
||||
configOrPath.config === undefined ? configOrPath : configOrPath.config
|
||||
configOrPath?.config ?? configOrPath ?? {},
|
||||
{ theme: cssThemeValues }
|
||||
)
|
||||
|
||||
newConfig = validateConfig(newConfig)
|
||||
|
|
@ -80,8 +84,11 @@ function getTailwindConfig(configOrPath) {
|
|||
export default function setupTrackingContext(configOrPath) {
|
||||
return ({ tailwindDirectives, registerDependency }) => {
|
||||
return (root, result) => {
|
||||
// Parse any CSS-based config values from `:theme` rules
|
||||
let cssThemeValues = new CssBasedConfig().parse(root)
|
||||
|
||||
let [tailwindConfig, userConfigPath, tailwindConfigHash, configDependencies] =
|
||||
getTailwindConfig(configOrPath)
|
||||
getTailwindConfig(configOrPath, cssThemeValues)
|
||||
|
||||
let contextDependencies = new Set(configDependencies)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,12 @@
|
|||
import pkg from '../../package.json'
|
||||
|
||||
export const env =
|
||||
typeof process !== 'undefined'
|
||||
? {
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
DEBUG: resolveDebug(process.env.DEBUG),
|
||||
ENGINE: pkg.tailwindcss.engine,
|
||||
}
|
||||
: {
|
||||
NODE_ENV: 'production',
|
||||
DEBUG: false,
|
||||
ENGINE: pkg.tailwindcss.engine,
|
||||
}
|
||||
|
||||
export const contextMap = new Map()
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
import './cli/index'
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
import packageJson from '../../../../package.json'
|
||||
import browserslist from 'browserslist'
|
||||
import { Result } from 'postcss'
|
||||
|
||||
import {
|
||||
// @ts-ignore
|
||||
lazyPostcss,
|
||||
|
||||
// @ts-ignore
|
||||
lazyPostcssImport,
|
||||
|
||||
// @ts-ignore
|
||||
lazyCssnano,
|
||||
|
||||
// @ts-ignore
|
||||
} from '../../../../peers/index'
|
||||
|
||||
export function lazyLightningCss() {
|
||||
// TODO: Make this lazy/bundled
|
||||
return require('lightningcss')
|
||||
}
|
||||
|
||||
let lightningCss
|
||||
|
||||
function loadLightningCss() {
|
||||
if (lightningCss) {
|
||||
return lightningCss
|
||||
}
|
||||
|
||||
// Try to load a local version first
|
||||
try {
|
||||
return (lightningCss = require('lightningcss'))
|
||||
} catch {}
|
||||
|
||||
return (lightningCss = lazyLightningCss())
|
||||
}
|
||||
|
||||
export async function lightningcss(shouldMinify: boolean, result: Result) {
|
||||
let css = loadLightningCss()
|
||||
|
||||
try {
|
||||
let transformed = css.transform({
|
||||
filename: result.opts.from || 'input.css',
|
||||
code: Buffer.from(result.css, 'utf-8'),
|
||||
minify: shouldMinify,
|
||||
sourceMap: !!result.map,
|
||||
inputSourceMap: result.map ? result.map.toString() : undefined,
|
||||
targets: css.browserslistToTargets(browserslist(packageJson.browserslist)),
|
||||
drafts: {
|
||||
nesting: true,
|
||||
},
|
||||
})
|
||||
|
||||
return Object.assign(result, {
|
||||
css: transformed.code.toString('utf8'),
|
||||
map: result.map
|
||||
? Object.assign(result.map, {
|
||||
toString() {
|
||||
return transformed.map.toString()
|
||||
},
|
||||
})
|
||||
: result.map,
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Unable to use Lightning CSS. Using raw version instead.')
|
||||
console.error(err)
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {import('postcss')}
|
||||
*/
|
||||
export function loadPostcss() {
|
||||
// Try to load a local `postcss` version first
|
||||
try {
|
||||
return require('postcss')
|
||||
} catch {}
|
||||
|
||||
return lazyPostcss()
|
||||
}
|
||||
|
||||
export function loadPostcssImport() {
|
||||
// Try to load a local `postcss-import` version first
|
||||
try {
|
||||
return require('postcss-import')
|
||||
} catch {}
|
||||
|
||||
return lazyPostcssImport()
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { resolveDefaultConfigPath } from '../../../util/resolveConfigPath'
|
||||
import { createProcessor } from './plugin'
|
||||
|
||||
export async function build(args) {
|
||||
let input = args['--input']
|
||||
let shouldWatch = args['--watch']
|
||||
|
||||
// TODO: Deprecate this in future versions
|
||||
if (!input && args['_'][1]) {
|
||||
console.error('[deprecation] Running tailwindcss without -i, please provide an input file.')
|
||||
input = args['--input'] = args['_'][1]
|
||||
}
|
||||
|
||||
if (input && input !== '-' && !fs.existsSync((input = path.resolve(input)))) {
|
||||
console.error(`Specified input file ${args['--input']} does not exist.`)
|
||||
process.exit(9)
|
||||
}
|
||||
|
||||
if (args['--config'] && !fs.existsSync((args['--config'] = path.resolve(args['--config'])))) {
|
||||
console.error(`Specified config file ${args['--config']} does not exist.`)
|
||||
process.exit(9)
|
||||
}
|
||||
|
||||
// TODO: Reference the @config path here if exists
|
||||
let configPath = args['--config'] ? args['--config'] : resolveDefaultConfigPath()
|
||||
|
||||
let processor = await createProcessor(args, configPath)
|
||||
|
||||
if (shouldWatch) {
|
||||
// Abort the watcher if stdin is closed to avoid zombie processes
|
||||
// You can disable this behavior with --watch=always
|
||||
if (args['--watch'] !== 'always') {
|
||||
process.stdin.on('end', () => process.exit(0))
|
||||
}
|
||||
|
||||
process.stdin.resume()
|
||||
|
||||
await processor.watch()
|
||||
} else {
|
||||
await processor.build().catch((e) => {
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,442 +0,0 @@
|
|||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import postcssrc from 'postcss-load-config'
|
||||
import { lilconfig } from 'lilconfig'
|
||||
import loadPlugins from 'postcss-load-config/src/plugins' // Little bit scary, looking at private/internal API
|
||||
import loadOptions from 'postcss-load-config/src/options' // Little bit scary, looking at private/internal API
|
||||
|
||||
import tailwind from '../../../processTailwindFeatures'
|
||||
import { loadPostcss, loadPostcssImport, lightningcss } from './deps'
|
||||
import { formatNodes, drainStdin, outputFile } from './utils'
|
||||
import { env } from '../../../lib/sharedState'
|
||||
import resolveConfig from '../../../../resolveConfig'
|
||||
import { parseCandidateFiles } from '../../../lib/content'
|
||||
import { createWatcher } from './watching'
|
||||
import fastGlob from 'fast-glob'
|
||||
import { findAtConfigPath } from '../../../lib/findAtConfigPath'
|
||||
import log from '../../../util/log'
|
||||
import { loadConfig } from '../../../lib/load-config'
|
||||
import getModuleDependencies from '../../../lib/getModuleDependencies'
|
||||
import type { Config } from '../../../../types'
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} [customPostCssPath ]
|
||||
* @returns
|
||||
*/
|
||||
async function loadPostCssPlugins(customPostCssPath) {
|
||||
let config = customPostCssPath
|
||||
? await (async () => {
|
||||
let file = path.resolve(customPostCssPath)
|
||||
|
||||
// Implementation, see: https://unpkg.com/browse/postcss-load-config@3.1.0/src/index.js
|
||||
// @ts-ignore
|
||||
let { config = {} } = await lilconfig('postcss').load(file)
|
||||
if (typeof config === 'function') {
|
||||
config = config()
|
||||
} else {
|
||||
config = Object.assign({}, config)
|
||||
}
|
||||
|
||||
if (!config.plugins) {
|
||||
config.plugins = []
|
||||
}
|
||||
|
||||
return {
|
||||
file,
|
||||
plugins: loadPlugins(config, file),
|
||||
options: loadOptions(config, file),
|
||||
}
|
||||
})()
|
||||
: await postcssrc()
|
||||
|
||||
let configPlugins = config.plugins
|
||||
|
||||
let configPluginTailwindIdx = configPlugins.findIndex((plugin) => {
|
||||
if (typeof plugin === 'function' && plugin.name === 'tailwindcss') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (typeof plugin === 'object' && plugin !== null && plugin.postcssPlugin === 'tailwindcss') {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
let beforePlugins =
|
||||
configPluginTailwindIdx === -1 ? [] : configPlugins.slice(0, configPluginTailwindIdx)
|
||||
let afterPlugins =
|
||||
configPluginTailwindIdx === -1
|
||||
? configPlugins
|
||||
: configPlugins.slice(configPluginTailwindIdx + 1)
|
||||
|
||||
return [beforePlugins, afterPlugins, config.options]
|
||||
}
|
||||
|
||||
function loadBuiltinPostcssPlugins() {
|
||||
let postcss = loadPostcss()
|
||||
let IMPORT_COMMENT = '__TAILWIND_RESTORE_IMPORT__: '
|
||||
return [
|
||||
[
|
||||
(root) => {
|
||||
root.walkAtRules('import', (rule) => {
|
||||
if (rule.params.slice(1).startsWith('tailwindcss/')) {
|
||||
rule.after(postcss.comment({ text: IMPORT_COMMENT + rule.params }))
|
||||
rule.remove()
|
||||
}
|
||||
})
|
||||
},
|
||||
loadPostcssImport(),
|
||||
(root) => {
|
||||
root.walkComments((rule) => {
|
||||
if (rule.text.startsWith(IMPORT_COMMENT)) {
|
||||
rule.after(
|
||||
postcss.atRule({
|
||||
name: 'import',
|
||||
params: rule.text.replace(IMPORT_COMMENT, ''),
|
||||
})
|
||||
)
|
||||
rule.remove()
|
||||
}
|
||||
})
|
||||
},
|
||||
],
|
||||
[],
|
||||
{},
|
||||
]
|
||||
}
|
||||
|
||||
let state = {
|
||||
/** @type {any} */
|
||||
context: null,
|
||||
|
||||
/** @type {ReturnType<typeof createWatcher> | null} */
|
||||
watcher: null,
|
||||
|
||||
/** @type {{content: string, extension: string}[]} */
|
||||
changedContent: [],
|
||||
|
||||
/** @type {{config: Config, dependencies: Set<string>, dispose: Function } | null} */
|
||||
configBag: null,
|
||||
|
||||
contextDependencies: new Set(),
|
||||
|
||||
/** @type {import('../../lib/content.js').ContentPath[]} */
|
||||
contentPaths: [],
|
||||
|
||||
refreshContentPaths() {
|
||||
this.contentPaths = parseCandidateFiles(this.context, this.context?.tailwindConfig)
|
||||
},
|
||||
|
||||
get config() {
|
||||
return this.context.tailwindConfig
|
||||
},
|
||||
|
||||
get contentPatterns() {
|
||||
return {
|
||||
all: this.contentPaths.map((contentPath) => contentPath.pattern),
|
||||
dynamic: this.contentPaths
|
||||
.filter((contentPath) => contentPath.glob !== undefined)
|
||||
.map((contentPath) => contentPath.pattern),
|
||||
}
|
||||
},
|
||||
|
||||
loadConfig(configPath, content) {
|
||||
if (this.watcher && configPath) {
|
||||
this.refreshConfigDependencies()
|
||||
}
|
||||
|
||||
let config = loadConfig(configPath)
|
||||
let dependencies = getModuleDependencies(configPath)
|
||||
this.configBag = {
|
||||
config,
|
||||
dependencies,
|
||||
dispose() {
|
||||
for (let file of dependencies) {
|
||||
delete require.cache[require.resolve(file)]
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
this.configBag.config = resolveConfig(this.configBag.config, { content: { files: [] } })
|
||||
|
||||
// Override content files if `--content` has been passed explicitly
|
||||
if (content?.length > 0) {
|
||||
this.configBag.config.content.files = content
|
||||
}
|
||||
|
||||
return this.configBag.config
|
||||
},
|
||||
|
||||
refreshConfigDependencies(configPath) {
|
||||
env.DEBUG && console.time('Module dependencies')
|
||||
this.configBag?.dispose()
|
||||
env.DEBUG && console.timeEnd('Module dependencies')
|
||||
},
|
||||
|
||||
readContentPaths() {
|
||||
let content = []
|
||||
|
||||
// Resolve globs from the content config
|
||||
// TODO: When we make the postcss plugin async-capable this can become async
|
||||
let files = fastGlob.sync(this.contentPatterns.all)
|
||||
|
||||
for (let file of files) {
|
||||
if (__OXIDE__) {
|
||||
content.push({
|
||||
file,
|
||||
extension: path.extname(file).slice(1),
|
||||
})
|
||||
} else {
|
||||
content.push({
|
||||
content: fs.readFileSync(path.resolve(file), 'utf8'),
|
||||
extension: path.extname(file).slice(1),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve raw content in the tailwind config
|
||||
let rawContent = this.config.content.files.filter((file) => {
|
||||
return file !== null && typeof file === 'object'
|
||||
})
|
||||
|
||||
for (let { raw: htmlContent, extension = 'html' } of rawContent) {
|
||||
content.push({ content: htmlContent, extension })
|
||||
}
|
||||
|
||||
return content
|
||||
},
|
||||
|
||||
getContext({ createContext, cliConfigPath, root, result, content }) {
|
||||
if (this.context) {
|
||||
this.context.changedContent = this.changedContent.splice(0)
|
||||
|
||||
return this.context
|
||||
}
|
||||
|
||||
env.DEBUG && console.time('Searching for config')
|
||||
let configPath = findAtConfigPath(root, result) ?? cliConfigPath
|
||||
env.DEBUG && console.timeEnd('Searching for config')
|
||||
|
||||
env.DEBUG && console.time('Loading config')
|
||||
let config = this.loadConfig(configPath, content)
|
||||
env.DEBUG && console.timeEnd('Loading config')
|
||||
|
||||
env.DEBUG && console.time('Creating context')
|
||||
this.context = createContext(config, [])
|
||||
Object.assign(this.context, {
|
||||
userConfigPath: configPath,
|
||||
})
|
||||
env.DEBUG && console.timeEnd('Creating context')
|
||||
|
||||
env.DEBUG && console.time('Resolving content paths')
|
||||
this.refreshContentPaths()
|
||||
env.DEBUG && console.timeEnd('Resolving content paths')
|
||||
|
||||
if (this.watcher) {
|
||||
env.DEBUG && console.time('Watch new files')
|
||||
this.watcher.refreshWatchedFiles()
|
||||
env.DEBUG && console.timeEnd('Watch new files')
|
||||
}
|
||||
|
||||
for (let file of this.readContentPaths()) {
|
||||
this.context.changedContent.push(file)
|
||||
}
|
||||
|
||||
return this.context
|
||||
},
|
||||
}
|
||||
|
||||
export async function createProcessor(args, cliConfigPath) {
|
||||
let postcss = loadPostcss()
|
||||
|
||||
let input = args['--input']
|
||||
let output = args['--output']
|
||||
let includePostCss = args['--postcss']
|
||||
let customPostCssPath = typeof args['--postcss'] === 'string' ? args['--postcss'] : undefined
|
||||
|
||||
let [beforePlugins, afterPlugins, postcssOptions] = includePostCss
|
||||
? await loadPostCssPlugins(customPostCssPath)
|
||||
: loadBuiltinPostcssPlugins()
|
||||
|
||||
if (args['--purge']) {
|
||||
log.warn('purge-flag-deprecated', [
|
||||
'The `--purge` flag has been deprecated.',
|
||||
'Please use `--content` instead.',
|
||||
])
|
||||
|
||||
if (!args['--content']) {
|
||||
args['--content'] = args['--purge']
|
||||
}
|
||||
}
|
||||
|
||||
let content = args['--content']?.split(/(?<!{[^}]+),/) ?? []
|
||||
|
||||
let tailwindPlugin = () => {
|
||||
return {
|
||||
postcssPlugin: 'tailwindcss',
|
||||
Once(root, { result }) {
|
||||
env.DEBUG && console.time('Compiling CSS')
|
||||
tailwind(({ createContext }) => {
|
||||
console.error()
|
||||
console.error('Rebuilding...')
|
||||
|
||||
return () => {
|
||||
return state.getContext({
|
||||
createContext,
|
||||
cliConfigPath,
|
||||
root,
|
||||
result,
|
||||
content,
|
||||
})
|
||||
}
|
||||
})(root, result)
|
||||
env.DEBUG && console.timeEnd('Compiling CSS')
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
tailwindPlugin.postcss = true
|
||||
|
||||
let plugins = [
|
||||
...beforePlugins,
|
||||
tailwindPlugin,
|
||||
!args['--minify'] && formatNodes,
|
||||
...afterPlugins,
|
||||
].filter(Boolean)
|
||||
|
||||
/** @type {import('postcss').Processor} */
|
||||
// @ts-ignore
|
||||
let processor = postcss(plugins)
|
||||
|
||||
async function readInput() {
|
||||
// Piping in data, let's drain the stdin
|
||||
if (input === '-') {
|
||||
return drainStdin()
|
||||
}
|
||||
|
||||
// Input file has been provided
|
||||
if (input) {
|
||||
return fs.promises.readFile(path.resolve(input), 'utf8')
|
||||
}
|
||||
|
||||
// No input file provided, fallback to default atrules
|
||||
return '@tailwind base; @tailwind components; @tailwind utilities'
|
||||
}
|
||||
|
||||
async function build() {
|
||||
let start = process.hrtime.bigint()
|
||||
|
||||
return readInput()
|
||||
.then((css) => processor.process(css, { ...postcssOptions, from: input, to: output }))
|
||||
.then((result) => lightningcss(!!args['--minify'], result))
|
||||
.then((result) => {
|
||||
if (!state.watcher) {
|
||||
return result
|
||||
}
|
||||
|
||||
env.DEBUG && console.time('Recording PostCSS dependencies')
|
||||
for (let message of result.messages) {
|
||||
if (message.type === 'dependency') {
|
||||
state.contextDependencies.add(message.file)
|
||||
}
|
||||
}
|
||||
env.DEBUG && console.timeEnd('Recording PostCSS dependencies')
|
||||
|
||||
// TODO: This needs to be in a different spot
|
||||
env.DEBUG && console.time('Watch new files')
|
||||
state.watcher.refreshWatchedFiles()
|
||||
env.DEBUG && console.timeEnd('Watch new files')
|
||||
|
||||
return result
|
||||
})
|
||||
.then((result) => {
|
||||
if (!output) {
|
||||
process.stdout.write(result.css)
|
||||
return
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
outputFile(result.opts.to, result.css),
|
||||
result.map && outputFile(result.opts.to + '.map', result.map.toString()),
|
||||
])
|
||||
})
|
||||
.then(() => {
|
||||
let end = process.hrtime.bigint()
|
||||
console.error()
|
||||
console.error('Done in', (end - start) / BigInt(1e6) + 'ms.')
|
||||
})
|
||||
.then(
|
||||
() => {},
|
||||
(err) => {
|
||||
// TODO: If an initial build fails we can't easily pick up any PostCSS dependencies
|
||||
// that were collected before the error occurred
|
||||
// The result is not stored on the error so we have to store it externally
|
||||
// and pull the messages off of it here somehow
|
||||
|
||||
// This results in a less than ideal DX because the watcher will not pick up
|
||||
// changes to imported CSS if one of them caused an error during the initial build
|
||||
// If you fix it and then save the main CSS file so there's no error
|
||||
// The watcher will start watching the imported CSS files and will be
|
||||
// resilient to future errors.
|
||||
|
||||
if (state.watcher) {
|
||||
console.error(err)
|
||||
} else {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{file: string, content(): Promise<string>, extension: string}[]} changes
|
||||
*/
|
||||
async function parseChanges(changes) {
|
||||
return Promise.all(
|
||||
changes.map(async (change) => ({
|
||||
content: await change.content(),
|
||||
extension: change.extension,
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
if (input !== undefined && input !== '-') {
|
||||
state.contextDependencies.add(path.resolve(input))
|
||||
}
|
||||
|
||||
return {
|
||||
build,
|
||||
watch: async () => {
|
||||
state.watcher = createWatcher(args, {
|
||||
state,
|
||||
|
||||
/**
|
||||
* @param {{file: string, content(): Promise<string>, extension: string}[]} changes
|
||||
*/
|
||||
async rebuild(changes) {
|
||||
let needsNewContext = changes.some((change) => {
|
||||
return (
|
||||
state.configBag?.dependencies.has(change.file) ||
|
||||
state.contextDependencies.has(change.file)
|
||||
)
|
||||
})
|
||||
|
||||
if (needsNewContext) {
|
||||
state.context = null
|
||||
} else {
|
||||
for (let change of await parseChanges(changes)) {
|
||||
state.changedContent.push(change)
|
||||
}
|
||||
}
|
||||
|
||||
return build()
|
||||
},
|
||||
})
|
||||
|
||||
await build()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export function indentRecursive(node, indent = 0) {
|
||||
node.each &&
|
||||
node.each((child, i) => {
|
||||
if (!child.raws.before || !child.raws.before.trim() || child.raws.before.includes('\n')) {
|
||||
child.raws.before = `\n${node.type !== 'rule' && i > 0 ? '\n' : ''}${' '.repeat(indent)}`
|
||||
}
|
||||
child.raws.after = `\n${' '.repeat(indent)}`
|
||||
indentRecursive(child, indent + 1)
|
||||
})
|
||||
}
|
||||
|
||||
export function formatNodes(root) {
|
||||
indentRecursive(root)
|
||||
if (root.first) {
|
||||
root.first.raws.before = ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When rapidly saving files atomically a couple of situations can happen:
|
||||
* - The file is missing since the external program has deleted it by the time we've gotten around to reading it from the earlier save.
|
||||
* - The file is being written to by the external program by the time we're going to read it and is thus treated as busy because a lock is held.
|
||||
*
|
||||
* To work around this we retry reading the file a handful of times with a delay between each attempt
|
||||
*
|
||||
* @param {string} path
|
||||
* @param {number} tries
|
||||
* @returns {Promise<string | undefined>}
|
||||
* @throws {Error} If the file is still missing or busy after the specified number of tries
|
||||
*/
|
||||
export async function readFileWithRetries(path, tries = 5) {
|
||||
for (let n = 0; n <= tries; n++) {
|
||||
try {
|
||||
return await fs.promises.readFile(path, 'utf8')
|
||||
} catch (err) {
|
||||
if (n !== tries) {
|
||||
if (err.code === 'ENOENT' || err.code === 'EBUSY') {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function drainStdin() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let result = ''
|
||||
process.stdin.on('data', (chunk) => {
|
||||
result += chunk
|
||||
})
|
||||
process.stdin.on('end', () => resolve(result))
|
||||
process.stdin.on('error', (err) => reject(err))
|
||||
})
|
||||
}
|
||||
|
||||
export async function outputFile(file, newContents) {
|
||||
try {
|
||||
let currentContents = await fs.promises.readFile(file, 'utf8')
|
||||
if (currentContents === newContents) {
|
||||
return // Skip writing the file
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Write the file
|
||||
await fs.promises.mkdir(path.dirname(file), { recursive: true })
|
||||
await fs.promises.writeFile(file, newContents, 'utf8')
|
||||
}
|
||||
|
|
@ -1,225 +0,0 @@
|
|||
import chokidar from 'chokidar'
|
||||
import fs from 'fs'
|
||||
import micromatch from 'micromatch'
|
||||
import normalizePath from 'normalize-path'
|
||||
import path from 'path'
|
||||
|
||||
import { readFileWithRetries } from './utils'
|
||||
|
||||
/**
|
||||
* The core idea of this watcher is:
|
||||
* 1. Whenever a file is added, changed, or renamed we queue a rebuild
|
||||
* 2. Perform as few rebuilds as possible by batching them together
|
||||
* 3. Coalesce events that happen in quick succession to avoid unnecessary rebuilds
|
||||
* 4. Ensure another rebuild happens _if_ changed while a rebuild is in progress
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} args
|
||||
* @param {{ state, rebuild(changedFiles: any[]): Promise<any> }} param1
|
||||
* @returns {{
|
||||
* fswatcher: import('chokidar').FSWatcher,
|
||||
* refreshWatchedFiles(): void,
|
||||
* }}
|
||||
*/
|
||||
export function createWatcher(args, { state, rebuild }) {
|
||||
let shouldPoll = args['--poll']
|
||||
let shouldCoalesceWriteEvents = shouldPoll || process.platform === 'win32'
|
||||
|
||||
// Polling interval in milliseconds
|
||||
// Used only when polling or coalescing add/change events on Windows
|
||||
let pollInterval = 10
|
||||
|
||||
let watcher = chokidar.watch([], {
|
||||
// Force checking for atomic writes in all situations
|
||||
// This causes chokidar to wait up to 100ms for a file to re-added after it's been unlinked
|
||||
// This only works when watching directories though
|
||||
atomic: true,
|
||||
|
||||
usePolling: shouldPoll,
|
||||
interval: shouldPoll ? pollInterval : undefined,
|
||||
ignoreInitial: true,
|
||||
awaitWriteFinish: shouldCoalesceWriteEvents
|
||||
? {
|
||||
stabilityThreshold: 50,
|
||||
pollInterval: pollInterval,
|
||||
}
|
||||
: false,
|
||||
})
|
||||
|
||||
// A queue of rebuilds, file reads, etc… to run
|
||||
let chain = Promise.resolve()
|
||||
|
||||
/**
|
||||
* A list of files that have been changed since the last rebuild
|
||||
*
|
||||
* @type {{file: string, content: () => Promise<string>, extension: string}[]}
|
||||
*/
|
||||
let changedContent = []
|
||||
|
||||
/**
|
||||
* A list of files for which a rebuild has already been queued.
|
||||
* This is used to prevent duplicate rebuilds when multiple events are fired for the same file.
|
||||
* The rebuilt file is cleared from this list when it's associated rebuild has _started_
|
||||
* This is because if the file is changed during a rebuild it won't trigger a new rebuild which it should
|
||||
**/
|
||||
let pendingRebuilds = new Set()
|
||||
|
||||
let _timer
|
||||
let _reject
|
||||
|
||||
/**
|
||||
* Rebuilds the changed files and resolves when the rebuild is
|
||||
* complete regardless of whether it was successful or not
|
||||
*/
|
||||
async function rebuildAndContinue() {
|
||||
let changes = changedContent.splice(0)
|
||||
|
||||
// There are no changes to rebuild so we can just do nothing
|
||||
if (changes.length === 0) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
// Clear all pending rebuilds for the about-to-be-built files
|
||||
changes.forEach((change) => pendingRebuilds.delete(change.file))
|
||||
|
||||
// Resolve the promise even when the rebuild fails
|
||||
return rebuild(changes).then(
|
||||
() => {},
|
||||
() => {}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} file
|
||||
* @param {(() => Promise<string>) | null} content
|
||||
* @param {boolean} skipPendingCheck
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function recordChangedFile(file, content = null, skipPendingCheck = false) {
|
||||
file = path.resolve(file)
|
||||
|
||||
// Applications like Vim/Neovim fire both rename and change events in succession for atomic writes
|
||||
// In that case rebuild has already been queued by rename, so can be skipped in change
|
||||
if (pendingRebuilds.has(file) && !skipPendingCheck) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
// Mark that a rebuild of this file is going to happen
|
||||
// It MUST happen synchronously before the rebuild is queued for this to be effective
|
||||
pendingRebuilds.add(file)
|
||||
|
||||
changedContent.push({
|
||||
file,
|
||||
content: content ?? (() => fs.promises.readFile(file, 'utf8')),
|
||||
extension: path.extname(file).slice(1),
|
||||
})
|
||||
|
||||
if (_timer) {
|
||||
clearTimeout(_timer)
|
||||
_reject()
|
||||
}
|
||||
|
||||
// If a rebuild is already in progress we don't want to start another one until the 10ms timer has expired
|
||||
chain = chain.then(
|
||||
() =>
|
||||
new Promise((resolve, reject) => {
|
||||
_timer = setTimeout(resolve, 10)
|
||||
_reject = reject
|
||||
})
|
||||
)
|
||||
|
||||
// Resolves once this file has been rebuilt (or the rebuild for this file has failed)
|
||||
// This queues as many rebuilds as there are changed files
|
||||
// But those rebuilds happen after some delay
|
||||
// And will immediately resolve if there are no changes
|
||||
chain = chain.then(rebuildAndContinue, rebuildAndContinue)
|
||||
|
||||
return chain
|
||||
}
|
||||
|
||||
watcher.on('change', (file) => recordChangedFile(file))
|
||||
watcher.on('add', (file) => recordChangedFile(file))
|
||||
|
||||
// Restore watching any files that are "removed"
|
||||
// This can happen when a file is pseudo-atomically replaced (a copy is created, overwritten, the old one is unlinked, and the new one is renamed)
|
||||
// TODO: An an optimization we should allow removal when the config changes
|
||||
watcher.on('unlink', (file) => {
|
||||
file = normalizePath(file)
|
||||
|
||||
// Only re-add the file if it's not covered by a dynamic pattern
|
||||
if (!micromatch.some([file], state.contentPatterns.dynamic)) {
|
||||
watcher.add(file)
|
||||
}
|
||||
})
|
||||
|
||||
// Some applications such as Visual Studio (but not VS Code)
|
||||
// will only fire a rename event for atomic writes and not a change event
|
||||
// This is very likely a chokidar bug but it's one we need to work around
|
||||
// We treat this as a change event and rebuild the CSS
|
||||
watcher.on('raw', (evt, filePath, meta) => {
|
||||
if (evt !== 'rename') {
|
||||
return
|
||||
}
|
||||
|
||||
let watchedPath = meta.watchedPath
|
||||
|
||||
// Watched path might be the file itself
|
||||
// Or the directory it is in
|
||||
filePath = watchedPath.endsWith(filePath) ? watchedPath : path.join(watchedPath, filePath)
|
||||
|
||||
// Skip this event since the files it is for does not match any of the registered content globs
|
||||
if (!micromatch.some([filePath], state.contentPatterns.all)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip since we've already queued a rebuild for this file that hasn't happened yet
|
||||
if (pendingRebuilds.has(filePath)) {
|
||||
return
|
||||
}
|
||||
|
||||
// We'll go ahead and add the file to the pending rebuilds list here
|
||||
// It'll be removed when the rebuild starts unless the read fails
|
||||
// which will be taken care of as well
|
||||
pendingRebuilds.add(filePath)
|
||||
|
||||
async function enqueue() {
|
||||
try {
|
||||
// We need to read the file as early as possible outside of the chain
|
||||
// because it may be gone by the time we get to it. doing the read
|
||||
// immediately increases the chance that the file is still there
|
||||
let content = await readFileWithRetries(path.resolve(filePath))
|
||||
|
||||
if (content === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
// This will push the rebuild onto the chain
|
||||
// We MUST skip the rebuild check here otherwise the rebuild will never happen on Linux
|
||||
// This is because the order of events and timing is different on Linux
|
||||
// @ts-ignore: TypeScript isn't picking up that content is a string here
|
||||
await recordChangedFile(filePath, () => content, true)
|
||||
} catch {
|
||||
// If reading the file fails, it's was probably a deleted temporary file
|
||||
// So we can ignore it and no rebuild is needed
|
||||
}
|
||||
}
|
||||
|
||||
enqueue().then(() => {
|
||||
// If the file read fails we still need to make sure the file isn't stuck in the pending rebuilds list
|
||||
pendingRebuilds.delete(filePath)
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
fswatcher: watcher,
|
||||
|
||||
refreshWatchedFiles() {
|
||||
watcher.add(Array.from(state.contextDependencies))
|
||||
watcher.add(Array.from(state.configBag.dependencies))
|
||||
watcher.add(state.contentPatterns.all)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
import packageJson from '../../../../package.json'
|
||||
|
||||
export function help({ message, usage, commands, options }) {
|
||||
let indent = 2
|
||||
|
||||
// Render header
|
||||
console.log()
|
||||
console.log(`${packageJson.name} v${packageJson.version}`)
|
||||
|
||||
// Render message
|
||||
if (message) {
|
||||
console.log()
|
||||
for (let msg of message.split('\n')) {
|
||||
console.log(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Render usage
|
||||
if (usage && usage.length > 0) {
|
||||
console.log()
|
||||
console.log('Usage:')
|
||||
for (let example of usage) {
|
||||
console.log(' '.repeat(indent), example)
|
||||
}
|
||||
}
|
||||
|
||||
// Render commands
|
||||
if (commands && commands.length > 0) {
|
||||
console.log()
|
||||
console.log('Commands:')
|
||||
for (let command of commands) {
|
||||
console.log(' '.repeat(indent), command)
|
||||
}
|
||||
}
|
||||
|
||||
// Render options
|
||||
if (options) {
|
||||
let groupedOptions = {}
|
||||
for (let [key, value] of Object.entries(options)) {
|
||||
if (typeof value === 'object') {
|
||||
groupedOptions[key] = { ...value, flags: [key] }
|
||||
} else {
|
||||
groupedOptions[value].flags.push(key)
|
||||
}
|
||||
}
|
||||
|
||||
console.log()
|
||||
console.log('Options:')
|
||||
for (let { flags, description, deprecated } of Object.values(groupedOptions)) {
|
||||
if (deprecated) continue
|
||||
|
||||
if (flags.length === 1) {
|
||||
console.log(
|
||||
' '.repeat(indent + 4 /* 4 = "-i, ".length */),
|
||||
flags.slice().reverse().join(', ').padEnd(20, ' '),
|
||||
description
|
||||
)
|
||||
} else {
|
||||
console.log(
|
||||
' '.repeat(indent),
|
||||
flags.slice().reverse().join(', ').padEnd(24, ' '),
|
||||
description
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log()
|
||||
}
|
||||
|
|
@ -1,204 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import path from 'path'
|
||||
import arg from 'arg'
|
||||
import fs from 'fs'
|
||||
|
||||
import { build } from './build'
|
||||
import { help } from './help'
|
||||
import { init } from './init'
|
||||
|
||||
// ---
|
||||
|
||||
function oneOf(...options) {
|
||||
return Object.assign(
|
||||
(value = true) => {
|
||||
for (let option of options) {
|
||||
let parsed = option(value)
|
||||
if (parsed === value) {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('...')
|
||||
},
|
||||
{ manualParsing: true }
|
||||
)
|
||||
}
|
||||
|
||||
let commands = {
|
||||
init: {
|
||||
run: init,
|
||||
args: {
|
||||
'--esm': { type: Boolean, description: `Initialize configuration file as ESM` },
|
||||
'--ts': { type: Boolean, description: `Initialize configuration file as TypeScript` },
|
||||
'--full': {
|
||||
type: Boolean,
|
||||
description: `Include the default values for all options in the generated configuration file`,
|
||||
},
|
||||
'-f': '--full',
|
||||
},
|
||||
},
|
||||
build: {
|
||||
run: build,
|
||||
args: {
|
||||
'--input': { type: String, description: 'Input file' },
|
||||
'--output': { type: String, description: 'Output file' },
|
||||
'--watch': {
|
||||
type: oneOf(String, Boolean),
|
||||
description: 'Watch for changes and rebuild as needed',
|
||||
},
|
||||
'--poll': {
|
||||
type: Boolean,
|
||||
description: 'Use polling instead of filesystem events when watching',
|
||||
},
|
||||
'--content': {
|
||||
type: String,
|
||||
description: 'Content paths to use for removing unused classes',
|
||||
},
|
||||
'--minify': { type: Boolean, description: 'Minify the output' },
|
||||
'--config': {
|
||||
type: String,
|
||||
description: 'Path to a custom config file',
|
||||
},
|
||||
'-c': '--config',
|
||||
'-i': '--input',
|
||||
'-o': '--output',
|
||||
'-m': '--minify',
|
||||
'-w': '--watch',
|
||||
'-p': '--poll',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
let sharedFlags = {
|
||||
'--help': { type: Boolean, description: 'Display usage information' },
|
||||
'-h': '--help',
|
||||
}
|
||||
|
||||
if (
|
||||
process.stdout.isTTY /* Detect redirecting output to a file */ &&
|
||||
(process.argv[2] === undefined ||
|
||||
process.argv.slice(2).every((flag) => sharedFlags[flag] !== undefined))
|
||||
) {
|
||||
help({
|
||||
usage: [
|
||||
'tailwindcss [--input input.css] [--output output.css] [--watch] [options...]',
|
||||
'tailwindcss init [--full] [options...]',
|
||||
],
|
||||
commands: Object.keys(commands)
|
||||
.filter((command) => command !== 'build')
|
||||
.map((command) => `${command} [options]`),
|
||||
options: { ...commands.build.args, ...sharedFlags },
|
||||
})
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
let command = ((arg = '') => (arg.startsWith('-') ? undefined : arg))(process.argv[2]) || 'build'
|
||||
|
||||
if (commands[command] === undefined) {
|
||||
if (fs.existsSync(path.resolve(command))) {
|
||||
// TODO: Deprecate this in future versions
|
||||
// Check if non-existing command, might be a file.
|
||||
command = 'build'
|
||||
} else {
|
||||
help({
|
||||
message: `Invalid command: ${command}`,
|
||||
usage: ['tailwindcss <command> [options]'],
|
||||
commands: Object.keys(commands)
|
||||
.filter((command) => command !== 'build')
|
||||
.map((command) => `${command} [options]`),
|
||||
options: sharedFlags,
|
||||
})
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute command
|
||||
let { args: flags, run } = commands[command]
|
||||
let args = (() => {
|
||||
try {
|
||||
let result = arg(
|
||||
Object.fromEntries(
|
||||
Object.entries({ ...flags, ...sharedFlags })
|
||||
.filter(([_key, value]) => !value?.type?.manualParsing)
|
||||
.map(([key, value]) => [key, typeof value === 'object' ? value.type : value])
|
||||
),
|
||||
{ permissive: true }
|
||||
)
|
||||
|
||||
// Manual parsing of flags to allow for special flags like oneOf(Boolean, String)
|
||||
for (let i = result['_'].length - 1; i >= 0; --i) {
|
||||
let flag = result['_'][i]
|
||||
if (!flag.startsWith('-')) continue
|
||||
|
||||
let [flagName, flagValue] = flag.split('=')
|
||||
let handler = flags[flagName]
|
||||
|
||||
// Resolve flagName & handler
|
||||
while (typeof handler === 'string') {
|
||||
flagName = handler
|
||||
handler = flags[handler]
|
||||
}
|
||||
|
||||
if (!handler) continue
|
||||
|
||||
let args = []
|
||||
let offset = i + 1
|
||||
|
||||
// --flag value syntax was used so we need to pull `value` from `args`
|
||||
if (flagValue === undefined) {
|
||||
// Parse args for current flag
|
||||
while (result['_'][offset] && !result['_'][offset].startsWith('-')) {
|
||||
args.push(result['_'][offset++])
|
||||
}
|
||||
|
||||
// Cleanup manually parsed flags + args
|
||||
result['_'].splice(i, 1 + args.length)
|
||||
|
||||
// No args were provided, use default value defined in handler
|
||||
// One arg was provided, use that directly
|
||||
// Multiple args were provided so pass them all in an array
|
||||
flagValue = args.length === 0 ? undefined : args.length === 1 ? args[0] : args
|
||||
} else {
|
||||
// Remove the whole flag from the args array
|
||||
result['_'].splice(i, 1)
|
||||
}
|
||||
|
||||
// Set the resolved value in the `result` object
|
||||
result[flagName] = handler.type(flagValue, flagName)
|
||||
}
|
||||
|
||||
// Ensure that the `command` is always the first argument in the `args`.
|
||||
// This is important so that we don't have to check if a default command
|
||||
// (build) was used or not from within each plugin.
|
||||
//
|
||||
// E.g.: tailwindcss input.css -> _: ['build', 'input.css']
|
||||
// E.g.: tailwindcss build input.css -> _: ['build', 'input.css']
|
||||
if (result['_'][0] !== command) {
|
||||
result['_'].unshift(command)
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (err) {
|
||||
if (err.code === 'ARG_UNKNOWN_OPTION') {
|
||||
help({
|
||||
message: err.message,
|
||||
usage: ['tailwindcss <command> [options]'],
|
||||
options: sharedFlags,
|
||||
})
|
||||
process.exit(1)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
})()
|
||||
|
||||
if (args['--help']) {
|
||||
help({
|
||||
options: { ...flags, ...sharedFlags },
|
||||
usage: [`tailwindcss ${command} [options]`],
|
||||
})
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
run(args)
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
function isESM() {
|
||||
const pkgPath = path.resolve('./package.json')
|
||||
|
||||
try {
|
||||
let pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
||||
return pkg.type && pkg.type === 'module'
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function init(args) {
|
||||
let messages: string[] = []
|
||||
|
||||
let isProjectESM = args['--ts'] || args['--esm'] || isESM()
|
||||
let syntax = args['--ts'] ? 'ts' : isProjectESM ? 'js' : 'cjs'
|
||||
let extension = args['--ts'] ? 'ts' : 'js'
|
||||
|
||||
let tailwindConfigLocation = path.resolve(args['_'][1] ?? `./tailwind.config.${extension}`)
|
||||
|
||||
if (fs.existsSync(tailwindConfigLocation)) {
|
||||
messages.push(`${path.basename(tailwindConfigLocation)} already exists.`)
|
||||
} else {
|
||||
let stubContentsFile = fs.readFileSync(
|
||||
args['--full']
|
||||
? path.resolve(__dirname, '../../../../stubs/config.full.js')
|
||||
: path.resolve(__dirname, '../../../../stubs/config.simple.js'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
let stubFile = fs.readFileSync(
|
||||
path.resolve(__dirname, `../../../../stubs/tailwind.config.${syntax}`),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
// Change colors import
|
||||
stubContentsFile = stubContentsFile.replace('../colors', 'tailwindcss/colors')
|
||||
|
||||
// Replace contents of {ts,js,cjs} file with the stub {simple,full}.
|
||||
stubFile =
|
||||
stubFile
|
||||
.replace('__CONFIG__', stubContentsFile.replace('module.exports =', '').trim())
|
||||
.trim() + '\n\n'
|
||||
|
||||
fs.writeFileSync(tailwindConfigLocation, stubFile, 'utf8')
|
||||
|
||||
messages.push(`Created Tailwind CSS config file: ${path.basename(tailwindConfigLocation)}`)
|
||||
}
|
||||
|
||||
if (messages.length > 0) {
|
||||
console.log()
|
||||
for (let message of messages) {
|
||||
console.log(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
module.exports = require('../plugin.js')
|
||||
130
src/plugin.js
130
src/plugin.js
|
|
@ -1,7 +1,12 @@
|
|||
import postcss from 'postcss'
|
||||
import lightningcss, { Features } from 'lightningcss'
|
||||
import browserslist from 'browserslist'
|
||||
import setupTrackingContext from './lib/setupTrackingContext'
|
||||
import processTailwindFeatures from './processTailwindFeatures'
|
||||
import { env } from './lib/sharedState'
|
||||
import { findAtConfigPath } from './lib/findAtConfigPath'
|
||||
import { handleImportAtRules } from './lib/handleImportAtRules'
|
||||
import { CssBasedConfig } from './lib/parseCssConfig'
|
||||
|
||||
module.exports = function tailwindcss(configOrPath) {
|
||||
return {
|
||||
|
|
@ -13,6 +18,7 @@ module.exports = function tailwindcss(configOrPath) {
|
|||
console.time('JIT TOTAL')
|
||||
return root
|
||||
},
|
||||
...handleImportAtRules(),
|
||||
function (root, result) {
|
||||
// Use the path for the `@config` directive if it exists, otherwise use the
|
||||
// path for the file being processed
|
||||
|
|
@ -34,66 +40,82 @@ module.exports = function tailwindcss(configOrPath) {
|
|||
|
||||
processTailwindFeatures(context)(root, result)
|
||||
},
|
||||
__OXIDE__ &&
|
||||
function lightningCssPlugin(_root, result) {
|
||||
let postcss = require('postcss')
|
||||
let lightningcss = require('lightningcss')
|
||||
let browserslist = require('browserslist')
|
||||
function lightningCssPlugin(_root, result) {
|
||||
let map = result.map ?? result.opts.map
|
||||
|
||||
try {
|
||||
let transformed = lightningcss.transform({
|
||||
filename: result.opts.from,
|
||||
code: Buffer.from(result.root.toString()),
|
||||
minify: false,
|
||||
sourceMap: !!result.map,
|
||||
inputSourceMap: result.map ? result.map.toString() : undefined,
|
||||
targets:
|
||||
typeof process !== 'undefined' && process.env.JEST_WORKER_ID
|
||||
? { chrome: 106 << 16 }
|
||||
: lightningcss.browserslistToTargets(
|
||||
browserslist(require('../package.json').browserslist)
|
||||
),
|
||||
let intermediateResult = result.root.toResult({
|
||||
map: map ? { inline: true } : false,
|
||||
})
|
||||
|
||||
drafts: {
|
||||
nesting: true,
|
||||
customMedia: true,
|
||||
},
|
||||
})
|
||||
let intermediateMap = intermediateResult.map?.toJSON?.() ?? map
|
||||
|
||||
result.map = Object.assign(result.map ?? {}, {
|
||||
toJSON() {
|
||||
return transformed.map.toJSON()
|
||||
},
|
||||
toString() {
|
||||
return transformed.map.toString()
|
||||
},
|
||||
})
|
||||
try {
|
||||
let resolvedBrowsersListConfig = browserslist.findConfig(
|
||||
result.opts.from ?? process.cwd()
|
||||
)?.defaults
|
||||
let defaultBrowsersListConfig = require('../package.json').browserslist
|
||||
let browsersListConfig = resolvedBrowsersListConfig ?? defaultBrowsersListConfig
|
||||
|
||||
result.root = postcss.parse(transformed.code.toString('utf8'))
|
||||
} catch (err) {
|
||||
if (typeof process !== 'undefined' && process.env.JEST_WORKER_ID) {
|
||||
let lines = err.source.split('\n')
|
||||
err = new Error(
|
||||
[
|
||||
'Error formatting using Lightning CSS:',
|
||||
'',
|
||||
...[
|
||||
'```css',
|
||||
...lines.slice(Math.max(err.loc.line - 3, 0), err.loc.line),
|
||||
' '.repeat(err.loc.column - 1) + '^-- ' + err.toString(),
|
||||
...lines.slice(err.loc.line, err.loc.line + 2),
|
||||
'```',
|
||||
],
|
||||
].join('\n')
|
||||
)
|
||||
let transformed = lightningcss.transform({
|
||||
filename: result.opts.from,
|
||||
code: Buffer.from(intermediateResult.css),
|
||||
minify: false,
|
||||
sourceMap: !!intermediateMap,
|
||||
targets: lightningcss.browserslistToTargets(browserslist(browsersListConfig)),
|
||||
drafts: {
|
||||
nesting: true,
|
||||
customMedia: true,
|
||||
},
|
||||
nonStandard: {
|
||||
deepSelectorCombinator: true,
|
||||
},
|
||||
include: Features.Nesting,
|
||||
exclude: Features.LogicalProperties,
|
||||
})
|
||||
|
||||
let code = transformed.code.toString()
|
||||
|
||||
// https://postcss.org/api/#sourcemapoptions
|
||||
if (intermediateMap && transformed.map != null) {
|
||||
let prev = transformed.map.toString()
|
||||
|
||||
if (typeof intermediateMap === 'object') {
|
||||
intermediateMap.prev = prev
|
||||
} else {
|
||||
code = `${code}\n/*# sourceMappingURL=data:application/json;base64,${Buffer.from(
|
||||
prev
|
||||
).toString('base64')} */`
|
||||
}
|
||||
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(err, lightningCssPlugin)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
result.root = postcss.parse(code, {
|
||||
...result.opts,
|
||||
map: intermediateMap,
|
||||
})
|
||||
} catch (err) {
|
||||
if (err.source && typeof process !== 'undefined' && process.env.JEST_WORKER_ID) {
|
||||
let lines = err.source.split('\n')
|
||||
err = new Error(
|
||||
[
|
||||
'Error formatting using Lightning CSS:',
|
||||
'',
|
||||
...[
|
||||
'```css',
|
||||
...lines.slice(Math.max(err.loc.line - 3, 0), err.loc.line),
|
||||
' '.repeat(err.loc.column - 1) + '^-- ' + err.toString(),
|
||||
...lines.slice(err.loc.line, err.loc.line + 2),
|
||||
'```',
|
||||
],
|
||||
].join('\n')
|
||||
)
|
||||
}
|
||||
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(err, lightningCssPlugin)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
},
|
||||
env.DEBUG &&
|
||||
function (root) {
|
||||
console.timeEnd('JIT TOTAL')
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import resolveDefaultsAtRules from './lib/resolveDefaultsAtRules'
|
|||
import collapseAdjacentRules from './lib/collapseAdjacentRules'
|
||||
import collapseDuplicateDeclarations from './lib/collapseDuplicateDeclarations'
|
||||
import partitionApplyAtRules from './lib/partitionApplyAtRules'
|
||||
import detectNesting from './lib/detectNesting'
|
||||
// import detectNesting from './lib/detectNesting'
|
||||
import { createContext } from './lib/setupContextUtils'
|
||||
import { issueFlagNotices } from './featureFlags'
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ export default function processTailwindFeatures(setupContext) {
|
|||
return function (root, result) {
|
||||
let { tailwindDirectives, applyDirectives } = normalizeTailwindDirectives(root)
|
||||
|
||||
detectNesting()(root, result)
|
||||
// detectNesting()(root, result)
|
||||
|
||||
// Partition apply rules that are found in the css
|
||||
// itself.
|
||||
|
|
|
|||
|
|
@ -49,10 +49,22 @@ export function normalize(value, isRoot = true) {
|
|||
value = value.trim()
|
||||
}
|
||||
|
||||
// Add spaces around operators inside math functions like calc() that do not follow an operator
|
||||
// or '('.
|
||||
value = value.replace(/(calc|min|max|clamp)\(.+\)/g, (match) => {
|
||||
value = normalizeMathOperatorSpacing(value)
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Add spaces around operators inside math functions
|
||||
* like calc() that do not follow an operator or '('.
|
||||
*
|
||||
* @param {string} value
|
||||
* @returns {string}
|
||||
*/
|
||||
function normalizeMathOperatorSpacing(value) {
|
||||
return value.replace(/(calc|min|max|clamp)\(.+\)/g, (match) => {
|
||||
let vars = []
|
||||
|
||||
return match
|
||||
.replace(/var\((--.+?)[,)]/g, (match, g1) => {
|
||||
vars.push(g1)
|
||||
|
|
@ -61,8 +73,6 @@ export function normalize(value, isRoot = true) {
|
|||
.replace(/(-?\d*\.?\d(?!\b-\d.+[,)](?![^+\-/*])\D)(?:%|[a-z]+)?|\))([+\-/*])/g, '$1 $2 ')
|
||||
.replace(placeholderRe, () => vars.shift())
|
||||
})
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
export function url(value) {
|
||||
|
|
@ -178,11 +188,12 @@ export function image(value) {
|
|||
}
|
||||
|
||||
let gradientTypes = new Set([
|
||||
'conic-gradient',
|
||||
'linear-gradient',
|
||||
'radial-gradient',
|
||||
'repeating-conic-gradient',
|
||||
'repeating-linear-gradient',
|
||||
'repeating-radial-gradient',
|
||||
'conic-gradient',
|
||||
])
|
||||
export function gradient(value) {
|
||||
value = normalize(value)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,30 @@ export function dim(input) {
|
|||
}
|
||||
|
||||
export default {
|
||||
group(key, cb) {
|
||||
if (typeof process !== 'undefined' && process.env.JEST_WORKER_ID) return
|
||||
|
||||
if (key && alreadyShown.has(key)) return
|
||||
if (key) alreadyShown.add(key)
|
||||
|
||||
console.warn('')
|
||||
|
||||
cb({
|
||||
info(messages) {
|
||||
messages.forEach((message) => console.warn(colors.bold(colors.cyan('info')), '-', message))
|
||||
},
|
||||
warn(messages) {
|
||||
messages.forEach((message) =>
|
||||
console.warn(colors.bold(colors.yellow('warn')), '-', message)
|
||||
)
|
||||
},
|
||||
risk(messages) {
|
||||
messages.forEach((message) =>
|
||||
console.warn(colors.bold(colors.magenta('risk')), '-', message)
|
||||
)
|
||||
},
|
||||
})
|
||||
},
|
||||
info(key, messages) {
|
||||
log(colors.bold(colors.cyan('info')), ...(Array.isArray(key) ? [key] : [messages, key]))
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { flagEnabled } from '../featureFlags'
|
||||
import { flagEnabled, featureFlags } from '../featureFlags'
|
||||
import log, { dim } from './log'
|
||||
|
||||
export function normalizeConfig(config) {
|
||||
|
|
@ -18,6 +18,10 @@ export function normalizeConfig(config) {
|
|||
* }
|
||||
*/
|
||||
let valid = (() => {
|
||||
if (config.content === 'auto') {
|
||||
return true
|
||||
}
|
||||
|
||||
// `config.purge` should not exist anymore
|
||||
if (config.purge) {
|
||||
return false
|
||||
|
|
@ -196,11 +200,17 @@ export function normalizeConfig(config) {
|
|||
files: (() => {
|
||||
let { content, purge } = config
|
||||
|
||||
if (Array.isArray(purge)) return purge
|
||||
if (Array.isArray(purge?.content)) return purge.content
|
||||
if (content === undefined && purge === undefined) return []
|
||||
if (purge) {
|
||||
if (Array.isArray(purge)) return purge
|
||||
if (Array.isArray(purge?.content)) return purge.content
|
||||
return []
|
||||
}
|
||||
|
||||
if (Array.isArray(content)) return content
|
||||
if (Array.isArray(content?.content)) return content.content
|
||||
if (Array.isArray(content?.files)) return content.files
|
||||
if (content === 'auto') return ['auto']
|
||||
|
||||
return []
|
||||
})(),
|
||||
|
|
@ -284,16 +294,33 @@ export function normalizeConfig(config) {
|
|||
})(),
|
||||
}
|
||||
|
||||
// Force disable the `oxideParser` feature flag when using unsupported features.
|
||||
// TODO: Remove once we have prefix or separator support in the oxide parser.
|
||||
if (config.prefix !== '' || config.separator !== ':') {
|
||||
if (config.experimental === 'all') {
|
||||
config.experimental = {}
|
||||
for (let key in featureFlags.experimental) {
|
||||
config.experimental[key] = true
|
||||
}
|
||||
} else {
|
||||
config.experimental = config.experimental ?? {}
|
||||
}
|
||||
|
||||
config.experimental.oxideParser = false
|
||||
}
|
||||
|
||||
// Validate globs to prevent bogus globs.
|
||||
// E.g.: `./src/*.{html}` is invalid, the `{html}` should just be `html`
|
||||
for (let file of config.content.files) {
|
||||
if (typeof file === 'string' && /{([^,]*?)}/g.test(file)) {
|
||||
log.warn('invalid-glob-braces', [
|
||||
`The glob pattern ${dim(file)} in your Tailwind CSS configuration is invalid.`,
|
||||
`Update it to ${dim(file.replace(/{([^,]*?)}/g, '$1'))} to silence this warning.`,
|
||||
// TODO: Add https://tw.wtf/invalid-glob-braces
|
||||
])
|
||||
break
|
||||
if (config.content.files !== 'auto') {
|
||||
for (let file of config.content.files) {
|
||||
if (typeof file === 'string' && /{([^,]*?)}/g.test(file)) {
|
||||
log.warn('invalid-glob-braces', [
|
||||
`The glob pattern ${dim(file)} in your Tailwind CSS configuration is invalid.`,
|
||||
`Update it to ${dim(file.replace(/{([^,]*?)}/g, '$1'))} to silence this warning.`,
|
||||
// TODO: Add https://tw.wtf/invalid-glob-braces
|
||||
])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import {
|
|||
} from './dataTypes'
|
||||
import negateValue from './negateValue'
|
||||
import { backgroundSize } from './validateFormalSyntax'
|
||||
import { flagEnabled } from '../featureFlags.js'
|
||||
|
||||
/**
|
||||
* @param {import('postcss-selector-parser').Container} selectors
|
||||
|
|
@ -243,12 +242,9 @@ export function coerceValue(types, modifier, options, tailwindConfig) {
|
|||
* @returns {Iterator<[value: string, type: string, modifier: string | null]>}
|
||||
*/
|
||||
export function* getMatchingTypes(types, rawModifier, options, tailwindConfig) {
|
||||
let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
|
||||
|
||||
let [modifier, utilityModifier] = splitUtilityModifier(rawModifier)
|
||||
|
||||
let canUseUtilityModifier =
|
||||
modifiersEnabled &&
|
||||
options.modifiers != null &&
|
||||
(options.modifiers === 'any' ||
|
||||
(typeof options.modifiers === 'object' &&
|
||||
|
|
|
|||
|
|
@ -19,40 +19,37 @@
|
|||
// **Jumpable**
|
||||
// Any terminal element may "jump" over combinators when moving to the end of the selector
|
||||
//
|
||||
// This is a backwards-compat quirk of :before and :after variants.
|
||||
// This is a backwards-compat quirk of pseudo element variants from earlier versions of Tailwind CSS.
|
||||
|
||||
/** @typedef {'terminal' | 'actionable' | 'jumpable'} PseudoProperty */
|
||||
|
||||
/** @type {Record<string, PseudoProperty[]>} */
|
||||
let elementProperties = {
|
||||
// Pseudo elements from the spec
|
||||
'::after': ['terminal', 'jumpable'],
|
||||
'::backdrop': ['terminal'],
|
||||
'::backdrop': ['terminal', 'jumpable'],
|
||||
'::before': ['terminal', 'jumpable'],
|
||||
'::cue': ['terminal'],
|
||||
'::cue-region': ['terminal'],
|
||||
'::first-letter': ['terminal', 'jumpable'],
|
||||
'::first-line': ['terminal', 'jumpable'],
|
||||
'::grammar-error': ['terminal'],
|
||||
'::marker': ['terminal'],
|
||||
'::marker': ['terminal', 'jumpable'],
|
||||
'::part': ['terminal', 'actionable'],
|
||||
'::placeholder': ['terminal'],
|
||||
'::selection': ['terminal'],
|
||||
'::placeholder': ['terminal', 'jumpable'],
|
||||
'::selection': ['terminal', 'jumpable'],
|
||||
'::slotted': ['terminal'],
|
||||
'::spelling-error': ['terminal'],
|
||||
'::target-text': ['terminal'],
|
||||
|
||||
// other
|
||||
// Pseudo elements from the spec with special rules
|
||||
'::file-selector-button': ['terminal', 'actionable'],
|
||||
'::-webkit-progress-bar': ['terminal', 'actionable'],
|
||||
|
||||
// Webkit scroll bar pseudo elements can be combined with user-action pseudo classes
|
||||
'::-webkit-scrollbar': ['terminal', 'actionable'],
|
||||
'::-webkit-scrollbar-button': ['terminal', 'actionable'],
|
||||
'::-webkit-scrollbar-thumb': ['terminal', 'actionable'],
|
||||
'::-webkit-scrollbar-track': ['terminal', 'actionable'],
|
||||
'::-webkit-scrollbar-track-piece': ['terminal', 'actionable'],
|
||||
'::-webkit-scrollbar-corner': ['terminal', 'actionable'],
|
||||
'::-webkit-resizer': ['terminal', 'actionable'],
|
||||
// Library-specific pseudo elements used by component libraries
|
||||
// These are Shadow DOM-like
|
||||
'::deep': ['actionable'],
|
||||
'::v-deep': ['actionable'],
|
||||
'::ng-deep': ['actionable'],
|
||||
|
||||
// Note: As a rule, double colons (::) should be used instead of a single colon
|
||||
// (:). This distinguishes pseudo-classes from pseudo-elements. However, since
|
||||
|
|
@ -65,8 +62,8 @@ let elementProperties = {
|
|||
|
||||
// The default value is used when the pseudo-element is not recognized
|
||||
// Because it's not recognized, we don't know if it's terminal or not
|
||||
// So we assume it can't be moved AND can have user-action pseudo classes attached to it
|
||||
__default__: ['actionable'],
|
||||
// So we assume it can be moved AND can have user-action pseudo classes attached to it
|
||||
__default__: ['terminal', 'actionable'],
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import pc from 'picocolors'
|
||||
import log from './log'
|
||||
|
||||
export function validateConfig(config) {
|
||||
|
|
@ -9,6 +10,15 @@ export function validateConfig(config) {
|
|||
])
|
||||
}
|
||||
|
||||
if (config.content.files.includes('auto')) {
|
||||
log.group('auto-content-experimental', (log) => {
|
||||
log.info([pc.bold('Automatically detecting Tailwind CSS content sources...')])
|
||||
log.warn([
|
||||
'Automatic content detection is experimental, and the behavior may change at any time.',
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
// Warn if the line-clamp plugin is installed
|
||||
try {
|
||||
let plugin = require('@tailwindcss/line-clamp')
|
||||
|
|
|
|||
22
src/value-parser/LICENSE
Normal file
22
src/value-parser/LICENSE
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
Copyright (c) Bogdan Chadkin <trysound@yandex.ru>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
3
src/value-parser/README.md
Normal file
3
src/value-parser/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# postcss-value-parser (forked + inlined)
|
||||
|
||||
This is a customized version of of [PostCSS Value Parser](https://github.com/TrySound/postcss-value-parser) to fix some bugs around parsing CSS functions.
|
||||
177
src/value-parser/index.d.ts
vendored
Normal file
177
src/value-parser/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
declare namespace postcssValueParser {
|
||||
interface BaseNode {
|
||||
/**
|
||||
* The offset, inclusive, inside the CSS value at which the node starts.
|
||||
*/
|
||||
sourceIndex: number
|
||||
|
||||
/**
|
||||
* The offset, exclusive, inside the CSS value at which the node ends.
|
||||
*/
|
||||
sourceEndIndex: number
|
||||
|
||||
/**
|
||||
* The node's characteristic value
|
||||
*/
|
||||
value: string
|
||||
}
|
||||
|
||||
interface ClosableNode {
|
||||
/**
|
||||
* Whether the parsed CSS value ended before the node was properly closed
|
||||
*/
|
||||
unclosed?: true
|
||||
}
|
||||
|
||||
interface AdjacentAwareNode {
|
||||
/**
|
||||
* The token at the start of the node
|
||||
*/
|
||||
before: string
|
||||
|
||||
/**
|
||||
* The token at the end of the node
|
||||
*/
|
||||
after: string
|
||||
}
|
||||
|
||||
interface CommentNode extends BaseNode, ClosableNode {
|
||||
type: 'comment'
|
||||
}
|
||||
|
||||
interface DivNode extends BaseNode, AdjacentAwareNode {
|
||||
type: 'div'
|
||||
}
|
||||
|
||||
interface FunctionNode extends BaseNode, ClosableNode, AdjacentAwareNode {
|
||||
type: 'function'
|
||||
|
||||
/**
|
||||
* Nodes inside the function
|
||||
*/
|
||||
nodes: Node[]
|
||||
}
|
||||
|
||||
interface SpaceNode extends BaseNode {
|
||||
type: 'space'
|
||||
}
|
||||
|
||||
interface StringNode extends BaseNode, ClosableNode {
|
||||
type: 'string'
|
||||
|
||||
/**
|
||||
* The quote type delimiting the string
|
||||
*/
|
||||
quote: '"' | "'"
|
||||
}
|
||||
|
||||
interface UnicodeRangeNode extends BaseNode {
|
||||
type: 'unicode-range'
|
||||
}
|
||||
|
||||
interface WordNode extends BaseNode {
|
||||
type: 'word'
|
||||
}
|
||||
|
||||
/**
|
||||
* Any node parsed from a CSS value
|
||||
*/
|
||||
type Node =
|
||||
| CommentNode
|
||||
| DivNode
|
||||
| FunctionNode
|
||||
| SpaceNode
|
||||
| StringNode
|
||||
| UnicodeRangeNode
|
||||
| WordNode
|
||||
|
||||
interface CustomStringifierCallback {
|
||||
/**
|
||||
* @param node The node to stringify
|
||||
* @returns The serialized CSS representation of the node
|
||||
*/
|
||||
(nodes: Node): string | undefined
|
||||
}
|
||||
|
||||
interface WalkCallback {
|
||||
/**
|
||||
* @param node The currently visited node
|
||||
* @param index The index of the node in the series of parsed nodes
|
||||
* @param nodes The series of parsed nodes
|
||||
* @returns Returning `false` will prevent traversal of descendant nodes (only applies if `bubble` was set to `true` in the `walk()` call)
|
||||
*/
|
||||
(node: Node, index: number, nodes: Node[]): void | boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* A CSS dimension, decomposed into its numeric and unit parts
|
||||
*/
|
||||
interface Dimension {
|
||||
number: string
|
||||
unit: string
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper around a parsed CSS value that allows for inspecting and walking nodes
|
||||
*/
|
||||
interface ParsedValue {
|
||||
/**
|
||||
* The series of parsed nodes
|
||||
*/
|
||||
nodes: Node[]
|
||||
|
||||
/**
|
||||
* Walk all parsed nodes, applying a callback
|
||||
*
|
||||
* @param callback A visitor callback that will be executed for each node
|
||||
* @param bubble When set to `true`, walking will be done inside-out instead of outside-in
|
||||
*/
|
||||
walk(callback: WalkCallback, bubble?: boolean): this
|
||||
}
|
||||
|
||||
interface ValueParser {
|
||||
/**
|
||||
* Decompose a CSS dimension into its numeric and unit part
|
||||
*
|
||||
* @param value The dimension to decompose
|
||||
* @returns An object representing `number` and `unit` part of the dimension or `false` if the decomposing fails
|
||||
*/
|
||||
unit(value: string): Dimension | false
|
||||
|
||||
/**
|
||||
* Serialize a series of nodes into a CSS value
|
||||
*
|
||||
* @param nodes The nodes to stringify
|
||||
* @param custom A custom stringifier callback
|
||||
* @returns The generated CSS value
|
||||
*/
|
||||
stringify(nodes: Node | Node[], custom?: CustomStringifierCallback): string
|
||||
|
||||
/**
|
||||
* Walk a series of nodes, applying a callback
|
||||
*
|
||||
* @param nodes The nodes to walk
|
||||
* @param callback A visitor callback that will be executed for each node
|
||||
* @param bubble When set to `true`, walking will be done inside-out instead of outside-in
|
||||
*/
|
||||
walk(nodes: Node[], callback: WalkCallback, bubble?: boolean): void
|
||||
|
||||
/**
|
||||
* Parse a CSS value into a series of nodes to operate on
|
||||
*
|
||||
* @param value The value to parse
|
||||
*/
|
||||
new (value: string): ParsedValue
|
||||
|
||||
/**
|
||||
* Parse a CSS value into a series of nodes to operate on
|
||||
*
|
||||
* @param value The value to parse
|
||||
*/
|
||||
(value: string): ParsedValue
|
||||
}
|
||||
}
|
||||
|
||||
declare const postcssValueParser: postcssValueParser.ValueParser
|
||||
|
||||
export = postcssValueParser
|
||||
28
src/value-parser/index.js
Normal file
28
src/value-parser/index.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
var parse = require('./parse')
|
||||
var walk = require('./walk')
|
||||
var stringify = require('./stringify')
|
||||
|
||||
function ValueParser(value) {
|
||||
if (this instanceof ValueParser) {
|
||||
this.nodes = parse(value)
|
||||
return this
|
||||
}
|
||||
return new ValueParser(value)
|
||||
}
|
||||
|
||||
ValueParser.prototype.toString = function () {
|
||||
return Array.isArray(this.nodes) ? stringify(this.nodes) : ''
|
||||
}
|
||||
|
||||
ValueParser.prototype.walk = function (cb, bubble) {
|
||||
walk(this.nodes, cb, bubble)
|
||||
return this
|
||||
}
|
||||
|
||||
ValueParser.unit = require('./unit')
|
||||
|
||||
ValueParser.walk = walk
|
||||
|
||||
ValueParser.stringify = stringify
|
||||
|
||||
module.exports = ValueParser
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue