Compare commits
2 commits
main
...
feat/walk-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7382fedbc | ||
|
|
1ceea2ff77 |
295 changed files with 25080 additions and 49772 deletions
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
.github/workflows/*.lock.yml linguist-generated=true merge=ours
|
||||
4
.github/CONTRIBUTING.md
vendored
4
.github/CONTRIBUTING.md
vendored
|
|
@ -50,7 +50,7 @@ If you open a pull request for a new feature, we're likely to close it not becau
|
|||
|
||||
## Coding standards
|
||||
|
||||
Our code formatting rules are defined in the `"prettier"` section of [package.json](https://github.com/tailwindlabs/tailwindcss/blob/main/package.json). You can check your code against these standards by running:
|
||||
Our code formatting rules are defined in the `"prettier"` section of [package.json](https://github.com/tailwindcss/tailwindcss/blob/main/package.json). You can check your code against these standards by running:
|
||||
|
||||
```sh
|
||||
pnpm run lint
|
||||
|
|
@ -76,7 +76,7 @@ To run the integration tests, use:
|
|||
pnpm build && pnpm test:integrations
|
||||
```
|
||||
|
||||
Additionally, some features require testing in browsers (i.e. to ensure CSS variable resolution works as expected). These can be run via:
|
||||
Additionally, some features require testing in browsers (i.e to ensure CSS variable resolution works as expected). These can be run via:
|
||||
|
||||
```sh
|
||||
pnpm build && pnpm test:ui
|
||||
|
|
|
|||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -8,7 +8,7 @@ It's never a fun experience to have your pull request declined after investing a
|
|||
|
||||
For more info, check out the contributing guide:
|
||||
|
||||
https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md
|
||||
https://github.com/tailwindcss/tailwindcss/blob/main/.github/CONTRIBUTING.md
|
||||
|
||||
-->
|
||||
|
||||
|
|
|
|||
42
.github/workflows/ci.yml
vendored
42
.github/workflows/ci.yml
vendored
|
|
@ -8,18 +8,12 @@ on:
|
|||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NODE_VERSION: 24
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [20]
|
||||
runner:
|
||||
- name: Windows
|
||||
os: windows-latest
|
||||
|
|
@ -27,10 +21,8 @@ jobs:
|
|||
- name: Linux
|
||||
os: namespace-profile-default
|
||||
|
||||
# Playwright 1.62+ dropped WebKit support for macOS 14, and hangs
|
||||
# instead of failing when launching WebKit on a macos-14 runner.
|
||||
- name: macOS
|
||||
os: macos-15
|
||||
os: macos-14
|
||||
|
||||
# Exclude windows and macos from being built on feature branches
|
||||
run-all:
|
||||
|
|
@ -49,21 +41,21 @@ jobs:
|
|||
name: ${{ matrix.runner.name }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
|
|
@ -72,7 +64,7 @@ jobs:
|
|||
|
||||
# Cache the `oxide` Rust build
|
||||
- name: Cache oxide build
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
./crates/node/*.node
|
||||
|
|
@ -90,7 +82,7 @@ jobs:
|
|||
run: rustup target add wasm32-wasip1-threads
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
|
@ -112,13 +104,9 @@ jobs:
|
|||
- name: Run Playwright tests
|
||||
run: npm run test:ui
|
||||
|
||||
notify:
|
||||
if: ${{ always() && github.ref == 'refs/heads/main' && needs.tests.result == 'failure' }}
|
||||
needs: tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify Discord
|
||||
uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # v2
|
||||
if: failure() && github.ref == 'refs/heads/main'
|
||||
uses: discord-actions/message@v2
|
||||
with:
|
||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
message: 'The [most recent ${{ github.workflow }} workflow](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.'
|
||||
message: 'The [most recent build](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.'
|
||||
|
|
|
|||
51
.github/workflows/integration-tests.yml
vendored
51
.github/workflows/integration-tests.yml
vendored
|
|
@ -8,18 +8,13 @@ on:
|
|||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NODE_VERSION: 24
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [20]
|
||||
|
||||
runner:
|
||||
- name: Windows
|
||||
os: windows-latest
|
||||
|
|
@ -55,25 +50,25 @@ jobs:
|
|||
name: ${{ matrix.runner.name }} / ${{ matrix.integration }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
|
|
@ -82,7 +77,7 @@ jobs:
|
|||
|
||||
# Cache the `oxide` Rust build
|
||||
- name: Cache oxide build
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
./crates/node/*.node
|
||||
|
|
@ -108,18 +103,24 @@ jobs:
|
|||
CARGO_PROFILE_RELEASE_LTO: 'off'
|
||||
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER: 'lld-link'
|
||||
|
||||
- name: Test ${{ matrix.integration }}
|
||||
run: pnpm run test:integrations ./integrations/${{ matrix.integration }}
|
||||
- name: Test ${{ matrix.integration }} 1/3
|
||||
run: pnpm run test:integrations ./integrations/${{ matrix.integration }} --shard 1/3
|
||||
env:
|
||||
GITHUB_WORKSPACE: ${{ github.workspace }}
|
||||
|
||||
- name: Test ${{ matrix.integration }} 2/3
|
||||
run: pnpm run test:integrations ./integrations/${{ matrix.integration }} --shard 2/3
|
||||
env:
|
||||
GITHUB_WORKSPACE: ${{ github.workspace }}
|
||||
|
||||
- name: Test ${{ matrix.integration }} 3/3
|
||||
run: pnpm run test:integrations ./integrations/${{ matrix.integration }} --shard 3/3
|
||||
env:
|
||||
GITHUB_WORKSPACE: ${{ github.workspace }}
|
||||
|
||||
notify:
|
||||
if: ${{ always() && github.ref == 'refs/heads/main' && needs.tests.result == 'failure' }}
|
||||
needs: tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify Discord
|
||||
uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # v2
|
||||
if: failure() && github.ref == 'refs/heads/main'
|
||||
uses: discord-actions/message@v2
|
||||
with:
|
||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
message: 'The [most recent ${{ github.workflow }} workflow](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.'
|
||||
message: 'The [most recent build](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.'
|
||||
|
|
|
|||
3310
.github/workflows/issue-triage.lock.yml
generated
vendored
Normal file
3310
.github/workflows/issue-triage.lock.yml
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
91
.github/workflows/issue-triage.md
vendored
Normal file
91
.github/workflows/issue-triage.md
vendored
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
---
|
||||
on:
|
||||
issues:
|
||||
types: [opened, reopened]
|
||||
stop-after: +1d
|
||||
reaction: eyes
|
||||
|
||||
permissions:
|
||||
issues: read
|
||||
discussions: read
|
||||
pull-requests: read
|
||||
contents: read
|
||||
|
||||
network: defaults
|
||||
|
||||
safe-outputs:
|
||||
add-labels:
|
||||
max: 5
|
||||
add-comment:
|
||||
allowed-domains:
|
||||
- github.com
|
||||
- tailwindcss.com
|
||||
- play.tailwindcss.com
|
||||
|
||||
tools:
|
||||
web-fetch:
|
||||
web-search:
|
||||
|
||||
timeout_minutes: 10
|
||||
|
||||
engine:
|
||||
id: claude
|
||||
model: claude-sonnet-4-5-20250929
|
||||
---
|
||||
|
||||
# Agentic Triage
|
||||
|
||||
You're a triage assistant for Tailwind CSS GitHub issues. Your task is to analyze issue #${{ github.event.issue.number }} and perform some initial triage tasks related to that issue.
|
||||
|
||||
1. Retrieve the issue content using the `get_issue` tool. If the issue is obviously spam, or generated by bot, or something else that is not an actual issue to be worked on, then do nothing and exit the workflow.
|
||||
|
||||
2. Next, use the GitHub tools to get the issue details
|
||||
- Fetch the list of labels available in this repository. Use 'gh label list' bash command to fetch the labels. This will give you the labels you can use for triaging issues.
|
||||
- Retrieve the issue content using the `get_issue`
|
||||
- Fetch any comments on the issue using the `get_issue_comments` tool
|
||||
- Find similar issues if needed using the `search_issues` tool
|
||||
- List the issues to see other open issues in the repository using the `list_issues` tool
|
||||
|
||||
3. Analyze the issue content, considering:
|
||||
- The issue title and description
|
||||
- The type of issue (bug report, feature request, question, etc.)
|
||||
- Technical areas mentioned
|
||||
- Severity or priority indicators
|
||||
- User impact
|
||||
- Components affected
|
||||
|
||||
4. Verify that the GitHub issue is related to Tailwind CSS and appears to be a bug. Feature requests and ideas should be created in the discussions area. If the GitHub issue does not appear to be a Tailwind CSS bug, read the `CONTRIBUTING.md` file and write a helpful comment explaining how we track bugs here.
|
||||
|
||||
5. Validate that the GitHub issue contains instructions of a reproduction. Inline instructions or URL to a reproduction are valid. If no reproduction is found, read the `CONTRIBUTING.md` file and write a helpful comment asking for one. Then, add the `needs reproduction` tag.
|
||||
|
||||
6. Select appropriate labels for the issue from the provided list.
|
||||
|
||||
7. Write notes, ideas, nudges, resource links, debugging strategies and/or reproduction steps for the team to consider relevant to the issue.
|
||||
|
||||
8. Select appropriate labels from the available labels list provided above:
|
||||
- Choose labels that accurately reflect the issue's nature
|
||||
- Be specific but comprehensive
|
||||
- Consider platform labels (android, ios) if applicable
|
||||
- Search for similar issues, and if you find similar issues consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
|
||||
- Only select labels from the provided list above
|
||||
- It's okay to not add any labels if none are clearly applicable
|
||||
|
||||
9. Apply the selected labels:
|
||||
- Use the `update_issue` tool to apply the labels to the issue
|
||||
- DO NOT communicate directly with users
|
||||
- If no labels are clearly applicable, do not apply any labels
|
||||
|
||||
10. Add an issue comment to the issue with your analysis:
|
||||
- Start with "🤖 Automatic Triage"
|
||||
- Provide a brief summary of the issue
|
||||
- Mention any relevant details that might help the team understand the issue better
|
||||
- Include any debugging strategies or reproduction steps if applicable
|
||||
- Suggest resources or links that might be helpful for resolving the issue or learning skills related to the issue or the particular area of the codebase affected by it
|
||||
- Mention any nudges or ideas that could help the team in addressing the issue
|
||||
- If you have possible reproduction steps, include them in the comment
|
||||
- If you have any debugging strategies, include them in the comment
|
||||
- If appropriate break the issue down to sub-tasks and write a checklist of things to do.
|
||||
- Use collapsed-by-default sections in the GitHub markdown to keep the comment tidy. Collapse all sections except the short main summary at the top.
|
||||
|
||||
@import .github/CONTRIBUTING.md
|
||||
@import .github/ISSUE_TEMPLATE/bug-report.md
|
||||
219
.github/workflows/prepare-release.yml
vendored
219
.github/workflows/prepare-release.yml
vendored
|
|
@ -2,29 +2,18 @@ name: Prepare Release
|
|||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: Skip creating the draft GitHub release
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
env:
|
||||
APP_NAME: tailwindcss-oxide
|
||||
NODE_VERSION: 24
|
||||
PNPM_VERSION: '11.9.0'
|
||||
NODE_VERSION: 20
|
||||
OXIDE_LOCATION: ./crates/node
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
|
|
@ -54,40 +43,44 @@ jobs:
|
|||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
strip: strip
|
||||
build-flags: --use-napi-cross
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-gnu
|
||||
strip: aarch64-linux-gnu-strip
|
||||
build-flags: --use-napi-cross
|
||||
strip: llvm-strip
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
|
||||
- os: ubuntu-latest
|
||||
target: armv7-unknown-linux-gnueabihf
|
||||
strip: arm-linux-gnueabihf-strip
|
||||
build-flags: --use-napi-cross
|
||||
strip: llvm-strip
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-zig
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-musl
|
||||
strip-zig: true
|
||||
build-flags: -x
|
||||
strip: aarch64-linux-musl-strip
|
||||
download: true
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
strip: strip
|
||||
build-flags: -x
|
||||
download: true
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
|
||||
|
||||
name: Build ${{ matrix.target }} (oxide)
|
||||
runs-on: ${{ matrix.os }}
|
||||
container: ${{ matrix.container }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
package-manager-cache: false
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install gcc-arm-linux-gnueabihf
|
||||
if: ${{ matrix.target == 'armv7-unknown-linux-gnueabihf' }}
|
||||
|
|
@ -95,56 +88,62 @@ jobs:
|
|||
sudo apt-get update
|
||||
sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf -y
|
||||
|
||||
- name: Install binutils-aarch64-linux-gnu
|
||||
if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' }}
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v4
|
||||
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@v4
|
||||
with:
|
||||
path: |
|
||||
./crates/node/*.node
|
||||
./crates/node/*.wasm
|
||||
./crates/node/index.d.ts
|
||||
./crates/node/index.js
|
||||
./crates/node/browser.js
|
||||
./crates/node/tailwindcss-oxide.wasi-browser.js
|
||||
./crates/node/tailwindcss-oxide.wasi.cjs
|
||||
./crates/node/wasi-worker-browser.mjs
|
||||
./crates/node/wasi-worker.mjs
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }}
|
||||
|
||||
- name: Install Node.JS
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install Rust (Stable)
|
||||
if: ${{ matrix.download }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install binutils-aarch64-linux-gnu -y
|
||||
|
||||
- uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
|
||||
if: ${{ contains(matrix.target, 'musl') }}
|
||||
with:
|
||||
version: 0.14.1
|
||||
use-cache: false
|
||||
|
||||
- name: Install cargo-zigbuild
|
||||
uses: taiki-e/install-action@65851e10cd6c377f11a60e600abc07cb08643468 # v2
|
||||
if: ${{ contains(matrix.target, 'musl') }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
tool: cargo-zigbuild
|
||||
rustup default stable
|
||||
|
||||
- name: Setup rust target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/*
|
||||
run: pnpm install --ignore-scripts --filter=!./playgrounds/*
|
||||
|
||||
- name: Build release
|
||||
run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform --target=${{ matrix.target }} ${{ matrix.build-flags }}
|
||||
run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform --target=${{ matrix.target }}
|
||||
env:
|
||||
RUST_TARGET: ${{ matrix.target }}
|
||||
JEMALLOC_SYS_WITH_LG_PAGE: ${{ matrix.page-size }}
|
||||
|
||||
- name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034
|
||||
if: ${{ matrix.strip || matrix.strip-zig }}
|
||||
env:
|
||||
STRIP_COMMAND: ${{ matrix.strip }}
|
||||
STRIP_ZIG: ${{ matrix.strip-zig }}
|
||||
run: |
|
||||
if [ "$STRIP_ZIG" = "true" ]; then
|
||||
for file in ${{ env.OXIDE_LOCATION }}/*.node; do
|
||||
zig objcopy --strip-all "$file" "$file.stripped"
|
||||
mv "$file.stripped" "$file"
|
||||
done
|
||||
exit 0
|
||||
fi
|
||||
|
||||
eval "$STRIP_COMMAND ${{ env.OXIDE_LOCATION }}/*.node"
|
||||
if: ${{ matrix.strip }}
|
||||
run: ${{ matrix.strip }} ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bindings-${{ matrix.target }}
|
||||
path: ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
|
@ -154,11 +153,9 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build FreeBSD
|
||||
uses: cross-platform-actions/action@cdc9ee69ef84a5f2e59c9058335d9c57bcb4ac86 # v0.25.0
|
||||
uses: cross-platform-actions/action@v0.25.0
|
||||
env:
|
||||
DEBUG: napi:*
|
||||
RUSTUP_HOME: /usr/local/rustup
|
||||
|
|
@ -174,12 +171,12 @@ jobs:
|
|||
shell: bash
|
||||
run: |
|
||||
sudo pkg install -y -f curl node libnghttp2 npm
|
||||
sudo npm install -g pnpm@${{ env.PNPM_VERSION }} --unsafe-perm=true
|
||||
sudo npm install -g pnpm@9.6.0 --unsafe-perm=true
|
||||
curl -sSf https://static.rust-lang.org/rustup/archive/1.27.1/x86_64-unknown-freebsd/rustup-init --output rustup-init
|
||||
chmod +x rustup-init
|
||||
./rustup-init -y --profile minimal
|
||||
source "$HOME/.cargo/env"
|
||||
pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/* || true
|
||||
pnpm install --ignore-scripts --filter=!./playgrounds/* || true
|
||||
echo "~~~~ rustc --version ~~~~"
|
||||
rustc --version
|
||||
echo "~~~~ node -v ~~~~"
|
||||
|
|
@ -190,7 +187,7 @@ jobs:
|
|||
strip -x ${{ env.OXIDE_LOCATION }}/*.node
|
||||
ls -la ${{ env.OXIDE_LOCATION }}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bindings-x86_64-unknown-freebsd
|
||||
path: ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
|
@ -201,17 +198,18 @@ jobs:
|
|||
name: Build and release Tailwind CSS
|
||||
|
||||
permissions:
|
||||
contents: write # Required for creating releases
|
||||
contents: write # for softprops/action-gh-release to create GitHub release
|
||||
# https://docs.npmjs.com/generating-provenance-statements#publishing-packages-with-provenance-via-github-actions
|
||||
id-token: write
|
||||
|
||||
needs:
|
||||
- build
|
||||
- build-freebsd
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 20
|
||||
persist-credentials: false
|
||||
|
||||
- run: git fetch --tags -f
|
||||
|
||||
|
|
@ -220,25 +218,51 @@ jobs:
|
|||
run: |
|
||||
echo "TAG_NAME=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
|
||||
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'pnpm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
package-manager-cache: false
|
||||
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v4
|
||||
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@v4
|
||||
with:
|
||||
path: |
|
||||
./crates/node/*.node
|
||||
./crates/node/*.wasm
|
||||
./crates/node/index.d.ts
|
||||
./crates/node/index.js
|
||||
./crates/node/browser.js
|
||||
./crates/node/tailwindcss-oxide.wasi-browser.js
|
||||
./crates/node/tailwindcss-oxide.wasi.cjs
|
||||
./crates/node/wasi-worker-browser.mjs
|
||||
./crates/node/wasi-worker.mjs
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }}
|
||||
|
||||
- name: Setup WASM target
|
||||
run: rustup target add wasm32-wasip1-threads
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm --filter=!./playgrounds/* install --frozen-lockfile
|
||||
run: pnpm --filter=!./playgrounds/* install
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ env.OXIDE_LOCATION }}
|
||||
|
||||
|
|
@ -277,31 +301,30 @@ jobs:
|
|||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload standalone artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tailwindcss-standalone
|
||||
path: packages/@tailwindcss-standalone/dist/
|
||||
|
||||
- name: Upload npm package tarballs
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: npm-package-tarballs
|
||||
path: dist/*.tgz
|
||||
|
||||
- name: Prepare GitHub Release
|
||||
if: ${{ !inputs.dry_run }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release create "${TAG_NAME}" \
|
||||
--draft \
|
||||
--title "${TAG_NAME}" \
|
||||
--notes "${RELEASE_NOTES}" \
|
||||
packages/@tailwindcss-standalone/dist/sha256sums.txt \
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-linux-arm64 \
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-linux-arm64-musl \
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-linux-x64 \
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-linux-x64-musl \
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-macos-arm64 \
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-macos-x64 \
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-windows-x64.exe
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: true
|
||||
tag_name: ${{ env.TAG_NAME }}
|
||||
body: |
|
||||
${{ env.RELEASE_NOTES }}
|
||||
files: |
|
||||
packages/@tailwindcss-standalone/dist/sha256sums.txt
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-linux-arm64
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-linux-arm64-musl
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-linux-x64
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-linux-x64-musl
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-macos-arm64
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-macos-x64
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-windows-x64.exe
|
||||
|
|
|
|||
318
.github/workflows/release-insiders.yml
vendored
Normal file
318
.github/workflows/release-insiders.yml
vendored
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
name: Release Insiders
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
APP_NAME: tailwindcss-oxide
|
||||
NODE_VERSION: 20
|
||||
OXIDE_LOCATION: ./crates/node
|
||||
RELEASE_CHANNEL: insiders
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
# Windows
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
- os: windows-latest
|
||||
target: aarch64-pc-windows-msvc
|
||||
# macOS
|
||||
- os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
strip: strip -x # Must use -x on macOS. This produces larger results on linux.
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
page-size: 14
|
||||
strip: strip -x # Must use -x on macOS. This produces larger results on linux.
|
||||
# Android
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-linux-android
|
||||
strip: ${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip
|
||||
- os: ubuntu-latest
|
||||
target: armv7-linux-androideabi
|
||||
strip: ${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip
|
||||
# Linux
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
strip: strip
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-gnu
|
||||
strip: llvm-strip
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
|
||||
- os: ubuntu-latest
|
||||
target: armv7-unknown-linux-gnueabihf
|
||||
strip: llvm-strip
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-zig
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-musl
|
||||
strip: aarch64-linux-musl-strip
|
||||
download: true
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
strip: strip
|
||||
download: true
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
|
||||
|
||||
name: Build ${{ matrix.target }} (oxide)
|
||||
runs-on: ${{ matrix.os }}
|
||||
container: ${{ matrix.container }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install gcc-arm-linux-gnueabihf
|
||||
if: ${{ matrix.target == 'armv7-unknown-linux-gnueabihf' }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf -y
|
||||
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v4
|
||||
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@v4
|
||||
with:
|
||||
path: |
|
||||
./crates/node/*.node
|
||||
./crates/node/*.wasm
|
||||
./crates/node/index.d.ts
|
||||
./crates/node/index.js
|
||||
./crates/node/browser.js
|
||||
./crates/node/tailwindcss-oxide.wasi-browser.js
|
||||
./crates/node/tailwindcss-oxide.wasi.cjs
|
||||
./crates/node/wasi-worker-browser.mjs
|
||||
./crates/node/wasi-worker.mjs
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }}
|
||||
|
||||
- name: Install Node.JS
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install Rust (Stable)
|
||||
if: ${{ matrix.download }}
|
||||
run: |
|
||||
rustup default stable
|
||||
|
||||
- name: Setup rust target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --ignore-scripts --filter=!./playgrounds/*
|
||||
|
||||
- name: Build release
|
||||
run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform --target=${{ matrix.target }}
|
||||
env:
|
||||
RUST_TARGET: ${{ matrix.target }}
|
||||
JEMALLOC_SYS_WITH_LG_PAGE: ${{ matrix.page-size }}
|
||||
|
||||
- name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034
|
||||
if: ${{ matrix.strip }}
|
||||
run: ${{ matrix.strip }} ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bindings-${{ matrix.target }}
|
||||
path: ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
||||
build-freebsd:
|
||||
name: Build x86_64-unknown-freebsd (OXIDE)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build FreeBSD
|
||||
uses: cross-platform-actions/action@v0.25.0
|
||||
env:
|
||||
DEBUG: napi:*
|
||||
RUSTUP_HOME: /usr/local/rustup
|
||||
CARGO_HOME: /usr/local/cargo
|
||||
RUSTUP_IO_THREADS: 1
|
||||
RUST_TARGET: x86_64-unknown-freebsd
|
||||
with:
|
||||
operating_system: freebsd
|
||||
version: '14.0'
|
||||
memory: 13G
|
||||
cpu_count: 3
|
||||
environment_variables: 'DEBUG RUSTUP_IO_THREADS'
|
||||
shell: bash
|
||||
run: |
|
||||
sudo pkg install -y -f curl node libnghttp2 npm
|
||||
sudo npm install -g pnpm@9.6.0 --unsafe-perm=true
|
||||
curl -sSf https://static.rust-lang.org/rustup/archive/1.27.1/x86_64-unknown-freebsd/rustup-init --output rustup-init
|
||||
chmod +x rustup-init
|
||||
./rustup-init -y --profile minimal
|
||||
source "$HOME/.cargo/env"
|
||||
echo "~~~~ rustc --version ~~~~"
|
||||
rustc --version
|
||||
echo "~~~~ node -v ~~~~"
|
||||
node -v
|
||||
echo "~~~~ pnpm --version ~~~~"
|
||||
pnpm --version
|
||||
pnpm install --ignore-scripts --filter=!./playgrounds/* || true
|
||||
pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform
|
||||
strip -x ${{ env.OXIDE_LOCATION }}/*.node
|
||||
ls -la ${{ env.OXIDE_LOCATION }}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bindings-x86_64-unknown-freebsd
|
||||
path: ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
||||
release:
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 15
|
||||
name: Build and release Tailwind CSS insiders
|
||||
|
||||
permissions:
|
||||
contents: write # for softprops/action-gh-release to create GitHub release
|
||||
# https://docs.npmjs.com/generating-provenance-statements#publishing-packages-with-provenance-via-github-actions
|
||||
id-token: write
|
||||
|
||||
needs:
|
||||
- build
|
||||
- build-freebsd
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 20
|
||||
|
||||
- name: Resolve version
|
||||
id: vars
|
||||
run: |
|
||||
echo "SHA_SHORT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'pnpm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v4
|
||||
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@v4
|
||||
with:
|
||||
path: |
|
||||
./crates/node/*.node
|
||||
./crates/node/*.wasm
|
||||
./crates/node/index.d.ts
|
||||
./crates/node/index.js
|
||||
./crates/node/browser.js
|
||||
./crates/node/tailwindcss-oxide.wasi-browser.js
|
||||
./crates/node/tailwindcss-oxide.wasi.cjs
|
||||
./crates/node/wasi-worker-browser.mjs
|
||||
./crates/node/wasi-worker.mjs
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }}
|
||||
|
||||
- name: Setup WASM target
|
||||
run: rustup target add wasm32-wasip1-threads
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm --filter=!./playgrounds/* install
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ env.OXIDE_LOCATION }}
|
||||
|
||||
- name: Move artifacts
|
||||
run: |
|
||||
cd ${{ env.OXIDE_LOCATION }}
|
||||
cp bindings-x86_64-pc-windows-msvc/* ./npm/win32-x64-msvc/
|
||||
cp bindings-aarch64-pc-windows-msvc/* ./npm/win32-arm64-msvc/
|
||||
cp bindings-x86_64-apple-darwin/* ./npm/darwin-x64/
|
||||
cp bindings-aarch64-apple-darwin/* ./npm/darwin-arm64/
|
||||
cp bindings-aarch64-linux-android/* ./npm/android-arm64/
|
||||
cp bindings-armv7-linux-androideabi/* ./npm/android-arm-eabi/
|
||||
cp bindings-aarch64-unknown-linux-gnu/* ./npm/linux-arm64-gnu/
|
||||
cp bindings-aarch64-unknown-linux-musl/* ./npm/linux-arm64-musl/
|
||||
cp bindings-armv7-unknown-linux-gnueabihf/* ./npm/linux-arm-gnueabihf/
|
||||
cp bindings-x86_64-unknown-linux-gnu/* ./npm/linux-x64-gnu/
|
||||
cp bindings-x86_64-unknown-linux-musl/* ./npm/linux-x64-musl/
|
||||
cp bindings-x86_64-unknown-freebsd/* ./npm/freebsd-x64/
|
||||
|
||||
- name: 'Version based on commit: 0.0.0-${{ env.RELEASE_CHANNEL }}.${{ env.SHA_SHORT }}'
|
||||
run: pnpm run version-packages 0.0.0-${{ env.RELEASE_CHANNEL }}.${{ env.SHA_SHORT }}
|
||||
|
||||
- name: Build Tailwind CSS
|
||||
run: pnpm run build
|
||||
|
||||
- name: Run pre-publish optimizations scripts
|
||||
run: node ./scripts/pre-publish-optimizations.mjs
|
||||
|
||||
- name: Lock pre-release versions
|
||||
run: node ./scripts/lock-pre-release-versions.mjs
|
||||
|
||||
- name: Upload npm package tarballs
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: npm-package-tarballs
|
||||
path: dist/*.tgz
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
pnpm --recursive --filter="!@tailwindcss/oxide-wasm32-wasi" publish --tag ${{ env.RELEASE_CHANNEL }} --no-git-checks
|
||||
# The wasm package needs a special npm config that isn't read when pnpm --recursive is used
|
||||
pushd crates/node/npm/wasm32-wasi; pnpm publish --tag ${{ env.RELEASE_CHANNEL }} --no-git-checks; popd;
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Trigger Tailwind Play update
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }}
|
||||
script: |
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: 'tailwindlabs',
|
||||
repo: 'upgrades',
|
||||
ref: 'main',
|
||||
workflow_id: 'upgrade-tailwindcss.yml'
|
||||
})
|
||||
270
.github/workflows/release.yml
vendored
270
.github/workflows/release.yml
vendored
|
|
@ -1,34 +1,18 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
channel:
|
||||
description: Release channel to publish
|
||||
required: true
|
||||
default: insiders
|
||||
type: choice
|
||||
options:
|
||||
- insiders
|
||||
- release
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
APP_NAME: tailwindcss-oxide
|
||||
NODE_VERSION: 24
|
||||
PNPM_VERSION: '11.9.0'
|
||||
NODE_VERSION: 20
|
||||
OXIDE_LOCATION: ./crates/node
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
|
|
@ -58,40 +42,44 @@ jobs:
|
|||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
strip: strip
|
||||
build-flags: --use-napi-cross
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-gnu
|
||||
strip: aarch64-linux-gnu-strip
|
||||
build-flags: --use-napi-cross
|
||||
strip: llvm-strip
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
|
||||
- os: ubuntu-latest
|
||||
target: armv7-unknown-linux-gnueabihf
|
||||
strip: arm-linux-gnueabihf-strip
|
||||
build-flags: --use-napi-cross
|
||||
strip: llvm-strip
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-zig
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-musl
|
||||
strip-zig: true
|
||||
build-flags: -x
|
||||
strip: aarch64-linux-musl-strip
|
||||
download: true
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
strip: strip
|
||||
build-flags: -x
|
||||
download: true
|
||||
container:
|
||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
|
||||
|
||||
name: Build ${{ matrix.target }} (oxide)
|
||||
runs-on: ${{ matrix.os }}
|
||||
container: ${{ matrix.container }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
package-manager-cache: false
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install gcc-arm-linux-gnueabihf
|
||||
if: ${{ matrix.target == 'armv7-unknown-linux-gnueabihf' }}
|
||||
|
|
@ -99,56 +87,62 @@ jobs:
|
|||
sudo apt-get update
|
||||
sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf -y
|
||||
|
||||
- name: Install binutils-aarch64-linux-gnu
|
||||
if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' }}
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v4
|
||||
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@v4
|
||||
with:
|
||||
path: |
|
||||
./crates/node/*.node
|
||||
./crates/node/*.wasm
|
||||
./crates/node/index.d.ts
|
||||
./crates/node/index.js
|
||||
./crates/node/browser.js
|
||||
./crates/node/tailwindcss-oxide.wasi-browser.js
|
||||
./crates/node/tailwindcss-oxide.wasi.cjs
|
||||
./crates/node/wasi-worker-browser.mjs
|
||||
./crates/node/wasi-worker.mjs
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }}
|
||||
|
||||
- name: Install Node.JS
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install Rust (Stable)
|
||||
if: ${{ matrix.download }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install binutils-aarch64-linux-gnu -y
|
||||
|
||||
- uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
|
||||
if: ${{ contains(matrix.target, 'musl') }}
|
||||
with:
|
||||
version: 0.14.1
|
||||
use-cache: false
|
||||
|
||||
- name: Install cargo-zigbuild
|
||||
uses: taiki-e/install-action@65851e10cd6c377f11a60e600abc07cb08643468 # v2
|
||||
if: ${{ contains(matrix.target, 'musl') }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
tool: cargo-zigbuild
|
||||
rustup default stable
|
||||
|
||||
- name: Setup rust target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/*
|
||||
run: pnpm install --ignore-scripts --filter=!./playgrounds/*
|
||||
|
||||
- name: Build release
|
||||
run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform --target=${{ matrix.target }} ${{ matrix.build-flags }}
|
||||
run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform --target=${{ matrix.target }}
|
||||
env:
|
||||
RUST_TARGET: ${{ matrix.target }}
|
||||
JEMALLOC_SYS_WITH_LG_PAGE: ${{ matrix.page-size }}
|
||||
|
||||
- name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034
|
||||
if: ${{ matrix.strip || matrix.strip-zig }}
|
||||
env:
|
||||
STRIP_COMMAND: ${{ matrix.strip }}
|
||||
STRIP_ZIG: ${{ matrix.strip-zig }}
|
||||
run: |
|
||||
if [ "$STRIP_ZIG" = "true" ]; then
|
||||
for file in ${{ env.OXIDE_LOCATION }}/*.node; do
|
||||
zig objcopy --strip-all "$file" "$file.stripped"
|
||||
mv "$file.stripped" "$file"
|
||||
done
|
||||
exit 0
|
||||
fi
|
||||
|
||||
eval "$STRIP_COMMAND ${{ env.OXIDE_LOCATION }}/*.node"
|
||||
if: ${{ matrix.strip }}
|
||||
run: ${{ matrix.strip }} ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bindings-${{ matrix.target }}
|
||||
path: ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
|
@ -158,11 +152,9 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build FreeBSD
|
||||
uses: cross-platform-actions/action@cdc9ee69ef84a5f2e59c9058335d9c57bcb4ac86 # v0.25.0
|
||||
uses: cross-platform-actions/action@v0.25.0
|
||||
env:
|
||||
DEBUG: napi:*
|
||||
RUSTUP_HOME: /usr/local/rustup
|
||||
|
|
@ -178,7 +170,7 @@ jobs:
|
|||
shell: bash
|
||||
run: |
|
||||
sudo pkg install -y -f curl node libnghttp2 npm
|
||||
sudo npm install -g pnpm@${{ env.PNPM_VERSION }} --unsafe-perm=true
|
||||
sudo npm install -g pnpm@9.6.0 --unsafe-perm=true
|
||||
curl -sSf https://static.rust-lang.org/rustup/archive/1.27.1/x86_64-unknown-freebsd/rustup-init --output rustup-init
|
||||
chmod +x rustup-init
|
||||
./rustup-init -y --profile minimal
|
||||
|
|
@ -189,12 +181,12 @@ jobs:
|
|||
node -v
|
||||
echo "~~~~ pnpm --version ~~~~"
|
||||
pnpm --version
|
||||
pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/* || true
|
||||
pnpm install --ignore-scripts --filter=!./playgrounds/* || true
|
||||
pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform
|
||||
strip -x ${{ env.OXIDE_LOCATION }}/*.node
|
||||
ls -la ${{ env.OXIDE_LOCATION }}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bindings-x86_64-unknown-freebsd
|
||||
path: ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
|
@ -202,10 +194,10 @@ jobs:
|
|||
release:
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 15
|
||||
name: Build and publish Tailwind CSS
|
||||
name: Build and release Tailwind CSS
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
contents: write # for softprops/action-gh-release to create GitHub release
|
||||
# https://docs.npmjs.com/generating-provenance-statements#publishing-packages-with-provenance-via-github-actions
|
||||
id-token: write
|
||||
|
||||
|
|
@ -214,51 +206,55 @@ jobs:
|
|||
- build-freebsd
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 20
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'pnpm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
package-manager-cache: false
|
||||
|
||||
# npm trusted publishing validates the caller workflow filename, so all npm publishes live here.
|
||||
# This workflow rebuilds the publish artifacts instead of depending on prepare-release.yml.
|
||||
- name: Resolve release metadata
|
||||
env:
|
||||
INPUT_CHANNEL: ${{ github.event.inputs.channel || '' }}
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "release" || "$INPUT_CHANNEL" == "release" ]]; then
|
||||
release_channel=$(node ./scripts/release-channel.js)
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
echo "RELEASE_KIND=release" >> $GITHUB_ENV
|
||||
echo "RELEASE_CHANNEL=$release_channel" >> $GITHUB_ENV
|
||||
echo "FEATURES_ENV=stable" >> $GITHUB_ENV
|
||||
else
|
||||
sha_short=$(git rev-parse --short HEAD)
|
||||
|
||||
echo "RELEASE_KIND=insiders" >> $GITHUB_ENV
|
||||
echo "RELEASE_CHANNEL=insiders" >> $GITHUB_ENV
|
||||
echo "SHA_SHORT=$sha_short" >> $GITHUB_ENV
|
||||
echo "INSIDERS_VERSION=0.0.0-insiders.$sha_short" >> $GITHUB_ENV
|
||||
fi
|
||||
# Cache the `oxide` Rust build
|
||||
- name: Cache oxide build
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
./crates/node/*.node
|
||||
./crates/node/*.wasm
|
||||
./crates/node/index.d.ts
|
||||
./crates/node/index.js
|
||||
./crates/node/browser.js
|
||||
./crates/node/tailwindcss-oxide.wasi-browser.js
|
||||
./crates/node/tailwindcss-oxide.wasi.cjs
|
||||
./crates/node/wasi-worker-browser.mjs
|
||||
./crates/node/wasi-worker.mjs
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./crates/**/*') }}
|
||||
|
||||
- name: Setup WASM target
|
||||
run: rustup target add wasm32-wasip1-threads
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm --filter=!./playgrounds/* install --frozen-lockfile
|
||||
run: pnpm --filter=!./playgrounds/* install
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ env.OXIDE_LOCATION }}
|
||||
|
||||
|
|
@ -278,19 +274,10 @@ jobs:
|
|||
cp bindings-x86_64-unknown-linux-musl/* ./npm/linux-x64-musl/
|
||||
cp bindings-x86_64-unknown-freebsd/* ./npm/freebsd-x64/
|
||||
|
||||
- name: 'Version based on commit: ${{ env.INSIDERS_VERSION }}'
|
||||
if: env.RELEASE_KIND == 'insiders'
|
||||
run: pnpm run version-packages ${INSIDERS_VERSION}
|
||||
|
||||
- name: Build Tailwind CSS
|
||||
if: env.RELEASE_KIND == 'insiders'
|
||||
run: pnpm run build
|
||||
|
||||
- name: Build Tailwind CSS
|
||||
if: env.RELEASE_KIND == 'release'
|
||||
run: pnpm run build
|
||||
env:
|
||||
FEATURES_ENV: ${{ env.FEATURES_ENV }}
|
||||
FEATURES_ENV: stable
|
||||
|
||||
- name: Run pre-publish optimizations scripts
|
||||
run: node ./scripts/pre-publish-optimizations.mjs
|
||||
|
|
@ -298,20 +285,22 @@ jobs:
|
|||
- name: Lock pre-release versions
|
||||
run: node ./scripts/lock-pre-release-versions.mjs
|
||||
|
||||
- name: Upload npm package tarballs
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: npm-package-tarballs
|
||||
path: dist/*.tgz
|
||||
- name: Calculate environment variables
|
||||
run: |
|
||||
echo "RELEASE_CHANNEL=$(node ./scripts/release-channel.js)" >> $GITHUB_ENV
|
||||
echo "TAILWINDCSS_VERSION=$(node -e 'console.log(require(`./packages/tailwindcss/package.json`).version);')" >> $GITHUB_ENV
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
pnpm --recursive --filter="!@tailwindcss/oxide-wasm32-wasi" publish --tag ${RELEASE_CHANNEL} --no-git-checks
|
||||
pnpm --recursive --filter="!@tailwindcss/oxide-wasm32-wasi" publish --tag ${{ env.RELEASE_CHANNEL }} --no-git-checks
|
||||
# The wasm package needs a special npm config that isn't read when pnpm --recursive is used
|
||||
pushd crates/node/npm/wasm32-wasi; pnpm publish --tag ${RELEASE_CHANNEL} --no-git-checks --config.node-linker=hoisted; popd;
|
||||
pushd crates/node/npm/wasm32-wasi; pnpm publish --tag ${{ env.RELEASE_CHANNEL }} --no-git-checks; popd;
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Trigger Tailwind Play update
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
if: env.RELEASE_CHANNEL == 'latest'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }}
|
||||
script: |
|
||||
|
|
@ -321,38 +310,3 @@ jobs:
|
|||
ref: 'main',
|
||||
workflow_id: 'upgrade-tailwindcss.yml'
|
||||
})
|
||||
|
||||
notify:
|
||||
if: ${{ always() && (needs.build.result == 'failure' || needs.build-freebsd.result == 'failure' || needs.release.result == 'failure') }}
|
||||
needs:
|
||||
- build
|
||||
- build-freebsd
|
||||
- release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Resolve release label
|
||||
id: release
|
||||
env:
|
||||
INPUT_CHANNEL: ${{ github.event.inputs.channel || '' }}
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name || '' }}
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "release" ]]; then
|
||||
tag_name="${RELEASE_TAG:-${GITHUB_REF_NAME}}"
|
||||
echo "label=release ${tag_name}" >> $GITHUB_OUTPUT
|
||||
elif [[ "$INPUT_CHANNEL" == "release" ]]; then
|
||||
version=$(node -p "require('./packages/tailwindcss/package.json').version")
|
||||
echo "label=release v${version}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
sha_short=$(git rev-parse --short HEAD)
|
||||
echo "label=insiders release 0.0.0-insiders.${sha_short}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Notify Discord
|
||||
uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # v2
|
||||
with:
|
||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
message: 'The [most recent ${{ github.workflow }} workflow](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) for `${{ steps.release.outputs.label }}` has failed.'
|
||||
|
|
|
|||
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
auto-install-peers = true
|
||||
|
|
@ -4,6 +4,5 @@ pnpm-lock.yaml
|
|||
target/
|
||||
crates/node/index.d.ts
|
||||
crates/node/index.js
|
||||
crates/ignore/
|
||||
.next
|
||||
.fingerprint
|
||||
|
|
|
|||
309
CHANGELOG.md
309
CHANGELOG.md
|
|
@ -9,271 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Added
|
||||
|
||||
- Add `@tailwindcss/turbopack` package to run Tailwind CSS with Next.js ([20367](https://github.com/tailwindlabs/tailwindcss/pull/20367))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Ensure watch mode detects changes to symlinked `@source` files whose real paths aren't otherwise scanned ([#20356](https://github.com/tailwindlabs/tailwindcss/pull/20356))
|
||||
- Ensure custom variants using `@scope` wrap the generated utilities instead of nesting inside them ([#20369](https://github.com/tailwindlabs/tailwindcss/pull/20369))
|
||||
- Fix flattening of `@scope` at-rules ([#20369](https://github.com/tailwindlabs/tailwindcss/pull/20369))
|
||||
- Fix standalone declarations in `@scope`, wrap them in `:where(:scope)` ([#20369](https://github.com/tailwindlabs/tailwindcss/pull/20369))
|
||||
- Always emit a space for empty fallback values in CSS variables (e.g. `var(--tw-blur,)` → `var(--tw-blur, )`) ([#20373](https://github.com/tailwindlabs/tailwindcss/pull/20373))
|
||||
- Canonicalization: convert arbitrary breakpoint and container query variants to named equivalents (e.g. `max-[64rem]` → `max-lg`) ([#20380](https://github.com/tailwindlabs/tailwindcss/pull/20380))
|
||||
- Prevent `@tailwindcss/vite` from crashing on every edit under Vite's experimental `bundledDev` mode ([#20379](https://github.com/tailwindlabs/tailwindcss/pull/20379))
|
||||
- Ensure `@tailwindcss/oxide` falls back to WASM on platforms without native bindings ([#20383](https://github.com/tailwindlabs/tailwindcss/pull/20383))
|
||||
- Detect classes in Ruby percent literals using angle brackets or custom delimiters (e.g. `%w<flex>`, `%w|flex|`), including in Slim and Haml templates ([#20387](https://github.com/tailwindlabs/tailwindcss/pull/20387))
|
||||
- Preserve whitespace in `--default(…)` values in custom functional utilities (e.g. `--default(box alphabetic)` no longer becomes `boxalphabetic`) ([#20392](https://github.com/tailwindlabs/tailwindcss/pull/20392))
|
||||
- Don't scan gitignored directories (e.g. `node_modules` and `.git`) when the project uses a safelist-style `.gitignore` (e.g. `/*` followed by `!/…` negations) ([#20397](https://github.com/tailwindlabs/tailwindcss/pull/20397))
|
||||
- Ensure root `theme('…')` namespace lookups in JavaScript plugins and config files return the full namespace object instead of the value of its `DEFAULT` key ([#20399](https://github.com/tailwindlabs/tailwindcss/pull/20399))
|
||||
- Skip ignored directories entirely when computing watch globs (`scanner.globs`), instead of walking their full contents on every rebuild ([#20408](https://github.com/tailwindlabs/tailwindcss/pull/20408))
|
||||
- Oxide: drop invalid UTF-8 candidates ([#20389](https://github.com/tailwindlabs/tailwindcss/pull/20389))
|
||||
- `@tailwindcss/vite` no longer forces a full page reload for external files (e.g.: `.php` files) ([#20414](https://github.com/tailwindlabs/tailwindcss/issues/20414))
|
||||
- Canonicalization: don't merge utilities that reference different theme variables set to CSS-wide keywords like `unset` ([#20417](https://github.com/tailwindlabs/tailwindcss/pull/20417))
|
||||
- Don't generate utilities when a modifier is used that would otherwise be silently ignored (e.g. `rounded-sm/[5]`, `shadow-sm/foo`, `stroke-2/50`) ([#20419](https://github.com/tailwindlabs/tailwindcss/pull/20419))
|
||||
- Only normalize top-level `and`, `or`, and `not` keywords in `supports-[…]` variants (e.g. `selector(a: not (.foo))` → `selector(a:not(.foo))`) ([#20420](https://github.com/tailwindlabs/tailwindcss/pull/20420))
|
||||
- Don't warn about Angular's `::ng-deep` and `:host-context()` when optimizing CSS ([#20434](https://github.com/tailwindlabs/tailwindcss/pull/20434))
|
||||
- Don't generate CSS for candidates containing an empty additional modifier (e.g. `bg-red-500/50/` and `group-hover/foo//bar:flex`) ([#20466](https://github.com/tailwindlabs/tailwindcss/pull/20466))
|
||||
- Sort `min-*`, `max-*`, and container query variants with decimal values numerically (e.g. `min-[40.25rem]` before `min-[40.5rem]`) ([#20512](https://github.com/tailwindlabs/tailwindcss/pull/20512))
|
||||
- Ensure CSS comments ending with `\*/` are closed correctly instead of swallowing the CSS that follows (e.g. `/* C:\temp\*/`) ([#20508](https://github.com/tailwindlabs/tailwindcss/pull/20508))
|
||||
- Improve style invalidation performance of `group-*` and `peer-*` variants ([#20513](https://github.com/tailwindlabs/tailwindcss/pull/20513))
|
||||
|
||||
## [4.3.3] - 2026-07-16
|
||||
|
||||
### Fixed
|
||||
|
||||
- Support `--watch --poll[=ms]` in `@tailwindcss/cli` when filesystem events are unreliable or unavailable ([#20297](https://github.com/tailwindlabs/tailwindcss/pull/20297))
|
||||
- Canonicalization: match arbitrary hex colors against theme colors case-insensitively (e.g. `bg-[#fff]` and `bg-[#FFF]` → `bg-white`) ([#20298](https://github.com/tailwindlabs/tailwindcss/pull/20298))
|
||||
- Prevent Preflight from overriding Firefox's native `iframe:focus-visible` outline styles ([#20292](https://github.com/tailwindlabs/tailwindcss/pull/20292))
|
||||
- Ensure `theme('colors.foo')` in JS plugins resolves correctly when both `--color-foo` and `--color-foo-bar` exist ([#20299](https://github.com/tailwindlabs/tailwindcss/pull/20299))
|
||||
- Ensure fractional opacity modifiers work with named shadow sizes like `shadow-sm/12.5`, `text-shadow-sm/12.5`, `drop-shadow-sm/12.5`, and `inset-shadow-sm/12.5` ([#20302](https://github.com/tailwindlabs/tailwindcss/pull/20302))
|
||||
- Parse selectors like `[data-foo]div` as two selectors instead of one ([#20303](https://github.com/tailwindlabs/tailwindcss/pull/20303))
|
||||
- Ensure `@tailwindcss/postcss` rebuilds when a preprocessor like Sass changes the input CSS without changing the input file on disk ([#20310](https://github.com/tailwindlabs/tailwindcss/pull/20310))
|
||||
- Ensure CSS nesting is handled even when Lightning CSS isn't run, such as in `@tailwindcss/browser` and Tailwind Play ([#20124](https://github.com/tailwindlabs/tailwindcss/pull/20124))
|
||||
- Prevent achromatic theme colors from shifting hue when mixed in polar color spaces like `oklch` ([#20314](https://github.com/tailwindlabs/tailwindcss/pull/20314))
|
||||
- Ensure `--spacing(0)` is optimized to `0px` instead of `0` so it remains a `<length>` when used in `calc(…)` ([#20319](https://github.com/tailwindlabs/tailwindcss/pull/20319))
|
||||
- Load `@parcel/watcher` only when needed in `@tailwindcss/cli --watch` mode, so one-off builds and `--watch --poll` work when `@parcel/watcher` can't be loaded ([#20325](https://github.com/tailwindlabs/tailwindcss/pull/20325))
|
||||
- Use explicit platform fonts instead of `system-ui` and `ui-sans-serif` so CJK text respects the page's `lang` attribute on Windows ([#20318](https://github.com/tailwindlabs/tailwindcss/pull/20318))
|
||||
- Prevent `@tailwindcss/upgrade` from rewriting ignored files when run from a subdirectory ([#20329](https://github.com/tailwindlabs/tailwindcss/pull/20329))
|
||||
- Ensure earlier `@source` rules pointing to nested files are scanned when later `@source` rules point to files in parent folders ([#20335](https://github.com/tailwindlabs/tailwindcss/pull/20335))
|
||||
- Prevent `@tailwindcss/vite` from triggering full page reloads when scanned files are processed by Vite but haven't been loaded as modules yet ([#20336](https://github.com/tailwindlabs/tailwindcss/pull/20336))
|
||||
|
||||
## [4.3.2] - 2026-06-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- Support bare spacing values for `auto-rows-*` and `auto-cols-*` utilities (e.g. `auto-rows-12` and `auto-cols-16`) ([#20229](https://github.com/tailwindlabs/tailwindcss/pull/20229))
|
||||
- Prevent `@tailwindcss/cli` in `--watch` mode from crashing on Windows when `@source` points to a directory that doesn't exist ([#20242](https://github.com/tailwindlabs/tailwindcss/pull/20242))
|
||||
- Prevent `@tailwindcss/vite` from crashing in Deno v2.8.x when `context.parentURL` is not a valid URL ([#20245](https://github.com/tailwindlabs/tailwindcss/pull/20245))
|
||||
- Ensure `@tailwindcss/cli` in `--watch` mode rebuilds when the input CSS file changes in an ignored directory ([#20246](https://github.com/tailwindlabs/tailwindcss/pull/20246))
|
||||
- Allow `@variant` rules used in `addBase(…)` to use custom variants defined later ([#20247](https://github.com/tailwindlabs/tailwindcss/pull/20247))
|
||||
- Prevent `@tailwindcss/vite` from crashing during HMR when scanned files or directories are deleted ([#20259](https://github.com/tailwindlabs/tailwindcss/pull/20259))
|
||||
- Generate `font-size` instead of `color` declarations for `text-[--spacing(…)]` ([#20260](https://github.com/tailwindlabs/tailwindcss/pull/20260))
|
||||
- Prevent `@source` patterns from scanning unrelated sibling files and folders ([#20263](https://github.com/tailwindlabs/tailwindcss/pull/20263))
|
||||
- Extract class candidates adjacent to Template Toolkit delimiters like `%]…[%` in `.tt`, `.tt2`, and `.tx` files ([#20269](https://github.com/tailwindlabs/tailwindcss/pull/20269))
|
||||
- Extract class candidates from conditional Maud syntax like `p.text-black[condition]` ([#20269](https://github.com/tailwindlabs/tailwindcss/pull/20269))
|
||||
- Prevent `@position-try` rules from triggering unknown at-rule warnings when optimizing CSS ([#20277](https://github.com/tailwindlabs/tailwindcss/pull/20277))
|
||||
- Support class suggestions for named opacity modifiers from `--opacity` theme values ([#20287](https://github.com/tailwindlabs/tailwindcss/pull/20287))
|
||||
- Prevent type errors in `@tailwindcss/postcss` when used with newer PostCSS patch releases ([#20289](https://github.com/tailwindlabs/tailwindcss/pull/20289))
|
||||
|
||||
## [4.3.1] - 2026-06-12
|
||||
|
||||
### Added
|
||||
|
||||
- Add `--silent` option to suppress output in `@tailwindcss/cli` ([#20100](https://github.com/tailwindlabs/tailwindcss/pull/20100))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Remove deprecation warnings by using `Module#registerHooks` instead of `Module#register` on Node 26+ ([#20028](https://github.com/tailwindlabs/tailwindcss/pull/20028))
|
||||
- Canonicalization: don't crash when plugin utilities throw for unsupported values ([#20052](https://github.com/tailwindlabs/tailwindcss/pull/20052))
|
||||
- Allow `@apply` to be used with CSS mixins ([#19427](https://github.com/tailwindlabs/tailwindcss/pull/19427))
|
||||
- Ensure `not-*` correctly negates `@container` queries, including `style(…)` queries ([#20059](https://github.com/tailwindlabs/tailwindcss/pull/20059))
|
||||
- Ensure `drop-shadow-*` color utilities work with custom shadow values containing `calc(…)` ([#20080](https://github.com/tailwindlabs/tailwindcss/pull/20080))
|
||||
- Fix 'Sourcemap is likely to be incorrect' warnings when using `@tailwindcss/vite` ([#20103](https://github.com/tailwindlabs/tailwindcss/pull/20103))
|
||||
- Ensure `@tailwindcss/webpack` can be installed in Rspack projects without requiring `webpack` as a peer dependency ([#20027](https://github.com/tailwindlabs/tailwindcss/pull/20027))
|
||||
- Canonicalization: don't suggest invalid `calc(…)` expressions (e.g. `px-[calc(1rem+0px)]` → `px-[calc(1rem+0)]`) ([#20127](https://github.com/tailwindlabs/tailwindcss/pull/20127))
|
||||
- Canonicalization: avoid suggesting large spacing-scale values for arbitrary lengths (e.g. `left-[99999px]` → `left-[99999px]`, not `left-24999.75`) ([#20130](https://github.com/tailwindlabs/tailwindcss/pull/20130))
|
||||
- Ensure `@tailwindcss/cli` in `--watch` mode recovers when a tracked dependency is deleted and restored ([#20137](https://github.com/tailwindlabs/tailwindcss/pull/20137))
|
||||
- Ensure standalone `@tailwindcss/cli` binaries are ignored when scanning for class candidates ([#20139](https://github.com/tailwindlabs/tailwindcss/pull/20139))
|
||||
- Ensure class candidates are extracted from Twig `addClass(…)` and `removeClass(…)` calls ([#20198](https://github.com/tailwindlabs/tailwindcss/pull/20198))
|
||||
- Don't crash in the Ruby or Vue preprocessors when scanning files containing invalid UTF-8 bytes ([#19588](https://github.com/tailwindlabs/tailwindcss/pull/19588))
|
||||
- Allow `@variant` to be used inside `addBase` ([#19480](https://github.com/tailwindlabs/tailwindcss/pull/19480))
|
||||
- Ensure `@source` globs with symlinks are preserved ([#20203](https://github.com/tailwindlabs/tailwindcss/pull/20203))
|
||||
- Ensure later `@source` rules can re-include files excluded by earlier `@source not` rules ([#20203](https://github.com/tailwindlabs/tailwindcss/pull/20203))
|
||||
- Upgrade: don't migrate empty class rules to invalid `@utility` rules ([#20205](https://github.com/tailwindlabs/tailwindcss/pull/20205))
|
||||
- Ensure transitions between `inset-shadow-none` and other inset shadows work correctly ([#20208](https://github.com/tailwindlabs/tailwindcss/pull/20208))
|
||||
- Ensure explicitly referenced `@source` directories are scanned even when ignored by git ([#20214](https://github.com/tailwindlabs/tailwindcss/pull/20214))
|
||||
- Ensure `@source` globs ending in `**/*` preserve dynamic path segments to avoid scanning too many files ([#20217](https://github.com/tailwindlabs/tailwindcss/pull/20217))
|
||||
- Canonicalization: don't fold `calc(…)` divisions when the result would require high precision (e.g. `w-[calc(100%/3.5)]` → `w-[calc(100%/3.5)]`, not `w-[28.571428571428573%]`) ([#20221](https://github.com/tailwindlabs/tailwindcss/pull/20221))
|
||||
- Serve ESM type declarations to ESM importers of `@tailwindcss/postcss` ([#20228](https://github.com/tailwindlabs/tailwindcss/pull/20228))
|
||||
|
||||
### Changed
|
||||
|
||||
- Generate `0` instead of `calc(var(--spacing) * 0)` for spacing utilities like `m-0` and `left-0` ([#20196](https://github.com/tailwindlabs/tailwindcss/pull/20196))
|
||||
- Generate `var(--spacing)` instead of `calc(var(--spacing) * 1)` for spacing utilities like `m-1` and `left-1` ([#20196](https://github.com/tailwindlabs/tailwindcss/pull/20196))
|
||||
|
||||
## [4.3.0] - 2026-05-08
|
||||
|
||||
### Added
|
||||
|
||||
- Add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901))
|
||||
- Add `scrollbar-{auto,thin,none}` utilities for `scrollbar-width`, and `scrollbar-thumb-*` / `scrollbar-track-*` color utilities for `scrollbar-color` ([#19981](https://github.com/tailwindlabs/tailwindcss/pull/19981), [#20019](https://github.com/tailwindlabs/tailwindcss/pull/20019))
|
||||
- Add `scrollbar-gutter-*` utilities ([#20018](https://github.com/tailwindlabs/tailwindcss/pull/20018))
|
||||
- Add `zoom-*` utilities ([#20020](https://github.com/tailwindlabs/tailwindcss/pull/20020))
|
||||
- Add `tab-*` utilities ([#20022](https://github.com/tailwindlabs/tailwindcss/pull/20022))
|
||||
- Allow using `@variant` with stacked variants (e.g. `@variant hover:focus { … }`) ([#19996](https://github.com/tailwindlabs/tailwindcss/pull/19996))
|
||||
- Allow using `@variant` with compound variants (e.g. `@variant hover, focus { … }`) ([#19996](https://github.com/tailwindlabs/tailwindcss/pull/19996))
|
||||
- Support `--default(…)` in `--value(…)` and `--modifier(…)` for functional `@utility` definitions ([#19989](https://github.com/tailwindlabs/tailwindcss/pull/19989))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Ensure `@plugin` resolves package JavaScript entries instead of browser CSS entries when using `@tailwindcss/vite` ([#19949](https://github.com/tailwindlabs/tailwindcss/pull/19949))
|
||||
- Fix relative `@import` and `@plugin` paths resolving from the wrong directory when using `@tailwindcss/vite` ([#19965](https://github.com/tailwindlabs/tailwindcss/pull/19965))
|
||||
- Ensure CSS files containing `@variant` are processed by `@tailwindcss/vite` ([#19966](https://github.com/tailwindlabs/tailwindcss/pull/19966))
|
||||
- Resolve imports relative to `base` when `result.opts.from` is not provided when using `@tailwindcss/postcss` ([#19980](https://github.com/tailwindlabs/tailwindcss/pull/19980))
|
||||
- Canonicalization: preserve significant `_` whitespace in arbitrary values ([#19986](https://github.com/tailwindlabs/tailwindcss/pull/19986))
|
||||
- Canonicalization: add parentheses when removing whitespace from arbitrary values would hurt readability (e.g. `w-[calc(100%---spacing(60))]` → `w-[calc(100%-(--spacing(60)))]`) ([#19986](https://github.com/tailwindlabs/tailwindcss/pull/19986))
|
||||
- Canonicalization: preserve the original unit in arbitrary values instead of normalizing to base units (e.g. `-mt-[20in]` → `mt-[-20in]`, not `mt-[-1920px]`) ([#19988](https://github.com/tailwindlabs/tailwindcss/pull/19988))
|
||||
- Canonicalization: migrate arbitrary `:has()` variants from `[&:has(…)]` to `has-[…]` ([#19991](https://github.com/tailwindlabs/tailwindcss/pull/19991))
|
||||
- Upgrade: don’t migrate inline `style` attributes (e.g. `style="flex-grow: 1"` → `style="flex-grow: 1"`, not `style="grow: 1"`) ([#19918](https://github.com/tailwindlabs/tailwindcss/pull/19918))
|
||||
- Allow multiple `@utility` definitions with the same name but different value types ([#19777](https://github.com/tailwindlabs/tailwindcss/pull/19777))
|
||||
- Export missing `PluginWithConfig` type from `tailwindcss/plugin` to fix errors when inferring plugin config types ([#19707](https://github.com/tailwindlabs/tailwindcss/pull/19707))
|
||||
- Ensure `start` and `end` legacy utilities without values do not generate CSS ([#20003](https://github.com/tailwindlabs/tailwindcss/pull/20003))
|
||||
- Ensure `--value(…)` is required in functional `@utility` definitions ([#20005](https://github.com/tailwindlabs/tailwindcss/pull/20005))
|
||||
- Canonicalization: preserve required whitespace around operators in negated arbitrary values (e.g. `-left-[(var(--a)+var(--b))]`) ([#20011](https://github.com/tailwindlabs/tailwindcss/pull/20011))
|
||||
|
||||
## [4.2.4] - 2026-04-21
|
||||
|
||||
### Fixed
|
||||
|
||||
- Ensure imports in `@import` and `@plugin` still resolve correctly when using Vite aliases in `@tailwindcss/vite` ([#19947](https://github.com/tailwindlabs/tailwindcss/pull/19947))
|
||||
|
||||
## [4.2.3] - 2026-04-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- Canonicalization: improve canonicalization for `tracking-*` utilities by preferring non-negative utilities (e.g. `-tracking-tighter` → `tracking-wider`) ([#19827](https://github.com/tailwindlabs/tailwindcss/pull/19827))
|
||||
- Fix crash due to invalid characters in candidate (exceeding valid unicode code point range) ([#19829](https://github.com/tailwindlabs/tailwindcss/pull/19829))
|
||||
- Ensure query params in imports are considered unique resources when using `@tailwindcss/webpack` ([#19723](https://github.com/tailwindlabs/tailwindcss/pull/19723))
|
||||
- Canonicalization: collapse arbitrary values into shorthand utilities (e.g. `px-[1.2rem] py-[1.2rem]` → `p-[1.2rem]`) ([#19837](https://github.com/tailwindlabs/tailwindcss/pull/19837))
|
||||
- Canonicalization: collapse `border-{t,b}-*` into `border-y-*`, `border-{l,r}-*` into `border-x-*`, and `border-{t,r,b,l}-*` into `border-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842))
|
||||
- Canonicalization: collapse `scroll-m{t,b}-*` into `scroll-my-*`, `scroll-m{l,r}-*` into `scroll-mx-*`, and `scroll-m{t,r,b,l}-*` into `scroll-m-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842))
|
||||
- Canonicalization: collapse `scroll-p{t,b}-*` into `scroll-py-*`, `scroll-p{l,r}-*` into `scroll-px-*`, and `scroll-p{t,r,b,l}-*` into `scroll-p-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842))
|
||||
- Canonicalization: collapse `overflow-{x,y}-*` into `overflow-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842))
|
||||
- Canonicalization: collapse `overscroll-{x,y}-*` into `overscroll-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842))
|
||||
- Read from `--placeholder-color` instead of `--background-color` for `placeholder-*` utilities ([#19843](https://github.com/tailwindlabs/tailwindcss/pull/19843))
|
||||
- Upgrade: ensure files are not emptied out when killing the upgrade process while it's running ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))
|
||||
- Upgrade: use `config.content` when migrating from Tailwind CSS v3 to Tailwind CSS v4 ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))
|
||||
- Upgrade: never migrate files that are ignored by git ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))
|
||||
- Add `.env` and `.env.*` to default ignored content files ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))
|
||||
- Canonicalization: migrate `overflow-ellipsis` into `text-ellipsis` ([#19849](https://github.com/tailwindlabs/tailwindcss/pull/19849))
|
||||
- Canonicalization: migrate `start-full` → `inset-s-full`, `start-auto` → `inset-s-auto`, `start-px` → `inset-s-px`, and `start-<number>` → `inset-s-<number>` as well as negative versions ([#19849](https://github.com/tailwindlabs/tailwindcss/pull/19849))
|
||||
- Canonicalization: migrate `end-full` → `inset-e-full`, `end-auto` → `inset-e-auto`, `end-px` → `inset-e-px`, and `end-<number>` → `inset-e-<number>` as well as negative versions ([#19849](https://github.com/tailwindlabs/tailwindcss/pull/19849))
|
||||
- Canonicalization: move the `-` sign inside the arbitrary value `-left-[9rem]` → `left-[-9rem]` ([#19858](https://github.com/tailwindlabs/tailwindcss/pull/19858))
|
||||
- Canonicalization: move the `-` sign outside the arbitrary value `ml-[calc(-1*var(--width))]` → `-ml-(--width)` ([#19858](https://github.com/tailwindlabs/tailwindcss/pull/19858))
|
||||
- Improve performance when scanning JSONL / NDJSON files ([#19862](https://github.com/tailwindlabs/tailwindcss/pull/19862))
|
||||
- Support `NODE_PATH` environment variable in standalone CLI ([#19617](https://github.com/tailwindlabs/tailwindcss/pull/19617))
|
||||
|
||||
## [4.2.2] - 2026-03-18
|
||||
|
||||
### Fixed
|
||||
|
||||
- Don't crash when candidates contain prototype properties like `row-constructor` ([#19725](https://github.com/tailwindlabs/tailwindcss/pull/19725))
|
||||
- Canonicalize `calc(var(--spacing)*…)` expressions into `--spacing(…)` ([#19769](https://github.com/tailwindlabs/tailwindcss/pull/19769))
|
||||
- Fix crash in canonicalization step when handling utilities containing `@property` at-rules (e.g. `shadow-sm border`) ([#19727](https://github.com/tailwindlabs/tailwindcss/pull/19727))
|
||||
- Skip full reload for server only modules scanned by client CSS when using `@tailwindcss/vite` ([#19745](https://github.com/tailwindlabs/tailwindcss/pull/19745))
|
||||
- Add support for Vite 8 in `@tailwindcss/vite` ([#19790](https://github.com/tailwindlabs/tailwindcss/pull/19790))
|
||||
- Improve canonicalization for bare values exceeding default spacing scale suggestions (e.g. `w-1234 h-1234` → `size-1234`) ([#19809](https://github.com/tailwindlabs/tailwindcss/pull/19809))
|
||||
- Fix canonicalization resulting in empty list (e.g. `w-5 h-5 size-5` → `''` instead of `size-5`) ([#19812](https://github.com/tailwindlabs/tailwindcss/pull/19812))
|
||||
- Resolve tsconfig paths to allow for `@import '@/path/to/file';` when using `@tailwindcss/vite` ([#19803](https://github.com/tailwindlabs/tailwindcss/pull/19803))
|
||||
|
||||
## [4.2.1] - 2026-02-23
|
||||
|
||||
### Fixed
|
||||
|
||||
- Allow trailing dash in functional utility names for backwards compatibility ([#19696](https://github.com/tailwindlabs/tailwindcss/pull/19696))
|
||||
- Properly detect classes containing `.` characters within curly braces in MDX files ([#19711](https://github.com/tailwindlabs/tailwindcss/pull/19711))
|
||||
|
||||
## [4.2.0] - 2026-02-18
|
||||
|
||||
### Added
|
||||
|
||||
- Add mauve, olive, mist, and taupe color palettes to the default theme ([#19627](https://github.com/tailwindlabs/tailwindcss/pull/19627))
|
||||
- Add `@tailwindcss/webpack` package to run Tailwind CSS as a webpack plugin ([#19610](https://github.com/tailwindlabs/tailwindcss/pull/19610))
|
||||
- Add `pbs-*` and `pbe-*` utilities for `padding-block-start` and `padding-block-end` ([#19601](https://github.com/tailwindlabs/tailwindcss/pull/19601))
|
||||
- Add `mbs-*` and `mbe-*` utilities for `margin-block-start` and `margin-block-end` ([#19601](https://github.com/tailwindlabs/tailwindcss/pull/19601))
|
||||
- Add `scroll-pbs-*` and `scroll-pbe-*` utilities for `scroll-padding-block-start` and `scroll-padding-block-end` ([#19601](https://github.com/tailwindlabs/tailwindcss/pull/19601))
|
||||
- Add `scroll-mbs-*` and `scroll-mbe-*` utilities for `scroll-margin-block-start` and `scroll-margin-block-end` ([#19601](https://github.com/tailwindlabs/tailwindcss/pull/19601))
|
||||
- Add `border-bs-*` and `border-be-*` utilities for `border-block-start` and `border-block-end` ([#19601](https://github.com/tailwindlabs/tailwindcss/pull/19601))
|
||||
- Add `inline-*`, `min-inline-*`, `max-inline-*` utilities for `inline-size`, `min-inline-size`, and `max-inline-size` ([#19612](https://github.com/tailwindlabs/tailwindcss/pull/19612))
|
||||
- Add `block-*`, `min-block-*`, `max-block-*` utilities for `block-size`, `min-block-size`, and `max-block-size` ([#19612](https://github.com/tailwindlabs/tailwindcss/pull/19612))
|
||||
- Add `inset-s-*`, `inset-e-*`, `inset-bs-*`, `inset-be-*` utilities for `inset-inline-start`, `inset-inline-end`, `inset-block-start`, and `inset-block-end` ([#19613](https://github.com/tailwindlabs/tailwindcss/pull/19613))
|
||||
- Add `font-features-*` utility for `font-feature-settings` ([#19623](https://github.com/tailwindlabs/tailwindcss/pull/19623))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Prevent double `@supports` wrapper for `color-mix` values ([#19450](https://github.com/tailwindlabs/tailwindcss/pull/19450))
|
||||
- Allow whitespace around `@source inline()` argument ([#19461](https://github.com/tailwindlabs/tailwindcss/pull/19461))
|
||||
- Emit comment when source maps are saved to files when using `@tailwindcss/cli` ([#19447](https://github.com/tailwindlabs/tailwindcss/pull/19447))
|
||||
- Detect utilities containing capital letters followed by numbers ([#19465](https://github.com/tailwindlabs/tailwindcss/pull/19465))
|
||||
- Fix class extraction for Rails' strict locals ([#19525](https://github.com/tailwindlabs/tailwindcss/pull/19525))
|
||||
- Align `@utility` name validation with Oxide scanner rules ([#19524](https://github.com/tailwindlabs/tailwindcss/pull/19524))
|
||||
- Fix infinite loop when using `@variant` inside `@custom-variant` ([#19633](https://github.com/tailwindlabs/tailwindcss/pull/19633))
|
||||
- Allow multiples of `.25` in `aspect-*` fractions (e.g. `aspect-8.5/11`) ([#19688](https://github.com/tailwindlabs/tailwindcss/pull/19688))
|
||||
- Ensure changes to external files listed via `@source` trigger a full page reload when using `@tailwindcss/vite` ([#19670](https://github.com/tailwindlabs/tailwindcss/pull/19670))
|
||||
- Improve performance of Oxide scanner in bigger projects by reducing file system walks ([#19632](https://github.com/tailwindlabs/tailwindcss/pull/19632))
|
||||
- Ensure import aliases in Astro v5 work without crashing when using `@tailwindcss/vite` ([#19677](https://github.com/tailwindlabs/tailwindcss/issues/19677))
|
||||
- Allow escape characters in `@utility` names to improve support with formatters such as Biome ([#19626](https://github.com/tailwindlabs/tailwindcss/pull/19626))
|
||||
- Fix incorrect canonicalization results when canonicalizing multiple times ([#19675](https://github.com/tailwindlabs/tailwindcss/pull/19675))
|
||||
- Add `.jj` to default ignored content directories ([#19687](https://github.com/tailwindlabs/tailwindcss/pull/19687))
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Deprecate `start-*` and `end-*` utilities in favor of `inset-s-*` and `inset-e-*` utilities ([#19613](https://github.com/tailwindlabs/tailwindcss/pull/19613))
|
||||
|
||||
## [4.1.18] - 2025-12-11
|
||||
|
||||
### Fixed
|
||||
|
||||
- Ensure validation of `source(…)` happens relative to the file it is in ([#19274](https://github.com/tailwindlabs/tailwindcss/pull/19274))
|
||||
- Include filename and line numbers in CSS parse errors ([#19282](https://github.com/tailwindlabs/tailwindcss/pull/19282))
|
||||
- Skip comments in Ruby files when checking for class names ([#19243](https://github.com/tailwindlabs/tailwindcss/pull/19243))
|
||||
- Skip over arbitrary property utilities with a top-level `!` in the value ([#19243](https://github.com/tailwindlabs/tailwindcss/pull/19243))
|
||||
- Support environment API in `@tailwindcss/vite` ([#18970](https://github.com/tailwindlabs/tailwindcss/pull/18970))
|
||||
- Preserve case of theme keys from JS configs and plugins ([#19337](https://github.com/tailwindlabs/tailwindcss/pull/19337))
|
||||
- Write source maps correctly on the CLI when using `--watch` ([#19373](https://github.com/tailwindlabs/tailwindcss/pull/19373))
|
||||
- Handle special defaults (like `ringColor.DEFAULT`) in JS configs ([#19348](https://github.com/tailwindlabs/tailwindcss/pull/19348))
|
||||
- Improve backwards compatibility for `content` theme key from JS configs ([#19381](https://github.com/tailwindlabs/tailwindcss/pull/19381))
|
||||
- Upgrade: Handle `future` and `experimental` config keys ([#19344](https://github.com/tailwindlabs/tailwindcss/pull/19344))
|
||||
- Try to canonicalize any arbitrary utility to a bare value ([#19379](https://github.com/tailwindlabs/tailwindcss/pull/19379))
|
||||
- Validate candidates similarly to Oxide ([#19397](https://github.com/tailwindlabs/tailwindcss/pull/19397))
|
||||
- Canonicalization: combine `text-*` and `leading-*` classes ([#19396](https://github.com/tailwindlabs/tailwindcss/pull/19396))
|
||||
- Correctly handle duplicate CLI arguments ([#19416](https://github.com/tailwindlabs/tailwindcss/pull/19416))
|
||||
- Don’t emit color-mix fallback rules inside `@keyframes` ([#19419](https://github.com/tailwindlabs/tailwindcss/pull/19419))
|
||||
- CLI: Don't hang when output is `/dev/stdout` ([#19421](https://github.com/tailwindlabs/tailwindcss/pull/19421))
|
||||
|
||||
## [3.4.19] - 2025-12-10
|
||||
|
||||
### Fixed
|
||||
|
||||
- Don’t break `sibling-*()` functions when used inside `calc(…)` ([#19335](https://github.com/tailwindlabs/tailwindcss/pull/19335))
|
||||
|
||||
## [4.1.17] - 2025-11-06
|
||||
|
||||
### Fixed
|
||||
|
||||
- Substitute `@variant` inside legacy JS APIs ([#19263](https://github.com/tailwindlabs/tailwindcss/pull/19263))
|
||||
- Prevent occasional crash on Windows when loaded into a worker thread ([#19242](https://github.com/tailwindlabs/tailwindcss/pull/19242))
|
||||
|
||||
## [4.1.16] - 2025-10-23
|
||||
|
||||
### Fixed
|
||||
|
||||
- Discard candidates with an empty data type ([#19172](https://github.com/tailwindlabs/tailwindcss/pull/19172))
|
||||
- Fix canonicalization of arbitrary variants with attribute selectors ([#19176](https://github.com/tailwindlabs/tailwindcss/pull/19176))
|
||||
- Fix invalid colors due to nested `&` ([#19184](https://github.com/tailwindlabs/tailwindcss/pull/19184))
|
||||
- Improve canonicalization for `& > :pseudo` and `& :pseudo` arbitrary variants ([#19178](https://github.com/tailwindlabs/tailwindcss/pull/19178))
|
||||
|
||||
## [4.1.15] - 2025-10-20
|
||||
- _Experimental_: Add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901))
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
@ -285,11 +21,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Don’t index into strings with the `theme(…)` function ([#19111](https://github.com/tailwindlabs/tailwindcss/pull/19111))
|
||||
- Fix parsing issue when `\t` is used in at-rules ([#19130](https://github.com/tailwindlabs/tailwindcss/pull/19130))
|
||||
- Upgrade: Canonicalize utilities containing `0` values ([#19095](https://github.com/tailwindlabs/tailwindcss/pull/19095))
|
||||
- Upgrade: Migrate deprecated `break-words` to `wrap-break-word` ([#19157](https://github.com/tailwindlabs/tailwindcss/pull/19157))
|
||||
|
||||
### Changed
|
||||
|
||||
- Remove the `postinstall` script from oxide ([#19149])(https://github.com/tailwindlabs/tailwindcss/pull/19149)
|
||||
|
||||
## [4.1.14] - 2025-10-01
|
||||
|
||||
|
|
@ -313,16 +44,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Upgrade: Ensure first class inside `className` is migrated ([#19031](https://github.com/tailwindlabs/tailwindcss/pull/19031))
|
||||
- Upgrade: Migrate classes inside `*ClassName` and `*Class` attributes ([#19031](https://github.com/tailwindlabs/tailwindcss/pull/19031))
|
||||
|
||||
## [3.4.18] - 2025-10-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improve support for raw `supports-[…]` queries in arbitrary values ([#13605](https://github.com/tailwindlabs/tailwindcss/pull/13605))
|
||||
- Fix `require.cache` error when loaded through a TypeScript file in Node 22.18+ ([#18665](https://github.com/tailwindlabs/tailwindcss/pull/18665))
|
||||
- Support `import.meta.resolve(…)` in configs for new enough Node.js versions ([#18938](https://github.com/tailwindlabs/tailwindcss/pull/18938))
|
||||
- Allow using newer versions of `postcss-load-config` for better ESM and TypeScript PostCSS config support with the CLI ([#18938](https://github.com/tailwindlabs/tailwindcss/pull/18938))
|
||||
- Remove irrelevant utility rules when matching important classes ([#19030](https://github.com/tailwindlabs/tailwindcss/pull/19030))
|
||||
|
||||
## [4.1.13] - 2025-09-03
|
||||
|
||||
### Changed
|
||||
|
|
@ -1655,6 +1376,16 @@ For a deep-dive into everything that's new, [check out the announcement post](ht
|
|||
|
||||
- First 4.0.0-alpha.1 release
|
||||
|
||||
## [3.4.18] - 2024-10-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improve support for raw `supports-[…]` queries in arbitrary values ([#13605](https://github.com/tailwindlabs/tailwindcss/pull/13605))
|
||||
- Fix `require.cache` error when loaded through a TypeScript file in Node 22.18+ ([#18665](https://github.com/tailwindlabs/tailwindcss/pull/18665))
|
||||
- Support `import.meta.resolve(…)` in configs for new enough Node.js versions ([#18938](https://github.com/tailwindlabs/tailwindcss/pull/18938))
|
||||
- Allow using newer versions of `postcss-load-config` for better ESM and TypeScript PostCSS config support with the CLI ([#18938](https://github.com/tailwindlabs/tailwindcss/pull/18938))
|
||||
- Remove irrelevant utility rules when matching important classes ([#19030](https://github.com/tailwindlabs/tailwindcss/pull/19030))
|
||||
|
||||
## [3.4.17] - 2024-12-17
|
||||
|
||||
### Fixed
|
||||
|
|
@ -4123,23 +3854,8 @@ No release notes
|
|||
|
||||
- Everything!
|
||||
|
||||
[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v4.3.3...HEAD
|
||||
[4.3.3]: https://github.com/tailwindlabs/tailwindcss/compare/v4.3.2...v4.3.3
|
||||
[4.3.2]: https://github.com/tailwindlabs/tailwindcss/compare/v4.3.1...v4.3.2
|
||||
[4.3.1]: https://github.com/tailwindlabs/tailwindcss/compare/v4.3.0...v4.3.1
|
||||
[4.3.0]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.4...v4.3.0
|
||||
[4.2.4]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.3...v4.2.4
|
||||
[4.2.3]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.2...v4.2.3
|
||||
[4.2.2]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.1...v4.2.2
|
||||
[4.2.1]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.0...v4.2.1
|
||||
[4.2.0]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.18...v4.2.0
|
||||
[4.1.18]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.17...v4.1.18
|
||||
[3.4.19]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.18...v3.4.19
|
||||
[4.1.17]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.16...v4.1.17
|
||||
[4.1.16]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.15...v4.1.16
|
||||
[4.1.15]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.14...v4.1.15
|
||||
[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.14...HEAD
|
||||
[4.1.14]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.13...v4.1.14
|
||||
[3.4.18]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.17...v3.4.18
|
||||
[4.1.13]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.12...v4.1.13
|
||||
[4.1.12]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.11...v4.1.12
|
||||
[4.1.11]: https://github.com/tailwindlabs/tailwindcss/compare/v4.1.10...v4.1.11
|
||||
|
|
@ -4197,6 +3913,7 @@ No release notes
|
|||
[4.0.0-alpha.24]: https://github.com/tailwindlabs/tailwindcss/compare/v4.0.0-alpha.23...v4.0.0-alpha.24
|
||||
[4.0.0-alpha.23]: https://github.com/tailwindlabs/tailwindcss/compare/v4.0.0-alpha.22...v4.0.0-alpha.23
|
||||
[4.0.0-alpha.22]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.17...v4.0.0-alpha.22
|
||||
[3.4.18]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.17...v3.4.18
|
||||
[3.4.17]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.16...v3.4.17
|
||||
[3.4.16]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.15...v3.4.16
|
||||
[3.4.15]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.14...v3.4.15
|
||||
|
|
|
|||
308
Cargo.lock
generated
308
Cargo.lock
generated
|
|
@ -40,7 +40,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata 0.4.18",
|
||||
"regex-automata 0.4.8",
|
||||
"serde",
|
||||
]
|
||||
|
||||
|
|
@ -59,31 +59,33 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.16.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.11.0"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49"
|
||||
checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
name = "crossbeam"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||
checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"crossbeam-deque",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-queue",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
|
@ -107,6 +109,15 @@ dependencies = [
|
|||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.20"
|
||||
|
|
@ -115,9 +126,19 @@ checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
|
|||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "1.0.12"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d83cb7e7a873830708d6b02a78cd36a592c6fa14bf267b68725103b85c0d77f"
|
||||
checksum = "67773048316103656a637612c4a62477603b777d91d9c62ff2290f9cde178fdb"
|
||||
dependencies = [
|
||||
"ctor-proc-macro",
|
||||
"dtor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctor-proc-macro"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2"
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
|
|
@ -125,6 +146,21 @@ version = "0.1.13"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||
|
||||
[[package]]
|
||||
name = "dtor"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e58a0764cddb55ab28955347b45be00ade43d4d6f3ba4bf3dc354e4ec9432934"
|
||||
dependencies = [
|
||||
"dtor-proc-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dtor-proc-macro"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5"
|
||||
|
||||
[[package]]
|
||||
name = "dunce"
|
||||
version = "1.0.5"
|
||||
|
|
@ -137,12 +173,6 @@ version = "1.8.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.9"
|
||||
|
|
@ -168,104 +198,16 @@ version = "2.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.20"
|
||||
version = "0.4.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988"
|
||||
checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
"log",
|
||||
"regex-automata 0.4.18",
|
||||
"regex-automata 0.4.8",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
|
|
@ -276,10 +218,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"ignore 0.4.23",
|
||||
"ignore 0.4.23 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.23"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"crossbeam-channel",
|
||||
"crossbeam-deque",
|
||||
"dunce",
|
||||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata 0.4.8",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.23"
|
||||
|
|
@ -290,41 +249,12 @@ dependencies = [
|
|||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata 0.4.18",
|
||||
"regex-automata 0.4.8",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.33"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"crossbeam-channel",
|
||||
"crossbeam-deque",
|
||||
"dunce",
|
||||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata 0.4.18",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "insta"
|
||||
version = "1.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82"
|
||||
dependencies = [
|
||||
"console",
|
||||
"once_cell",
|
||||
"similar",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.11.0"
|
||||
|
|
@ -348,12 +278,12 @@ checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
|
|||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.9.0"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60"
|
||||
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-link",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -391,13 +321,12 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
|||
|
||||
[[package]]
|
||||
name = "napi"
|
||||
version = "3.11.0"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de33522036981030a75c231829566bc63414e08101a6f5ff4ac6cef19c8e0941"
|
||||
checksum = "f1b74e3dce5230795bb4d2821b941706dee733c7308752507254b0497f39cad7"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"ctor",
|
||||
"futures",
|
||||
"napi-build",
|
||||
"napi-sys",
|
||||
"nohash-hasher",
|
||||
|
|
@ -406,15 +335,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "napi-build"
|
||||
version = "2.3.2"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9c366d2c8c60b86fa632df75f745509b52f9128f91a6bad4c796e44abb505e1"
|
||||
checksum = "dcae8ad5609d14afb3a3b91dee88c757016261b151e9dcecabf1b2a31a6cab14"
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive"
|
||||
version = "3.6.0"
|
||||
version = "3.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a49c513341a61a16a10af6efcce46b30d0822ba2d4fb197d24d33dfc199c78d5"
|
||||
checksum = "7552d5a579b834614bbd496db5109f1b9f1c758f08224b0dee1e408333adf0d0"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"ctor",
|
||||
|
|
@ -426,9 +355,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "napi-derive-backend"
|
||||
version = "6.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d60b5d773ad46c698c8cc2cd9fde0b283d39cbb7f71c04bee633c7bdba4423bd"
|
||||
checksum = "5f6a81ac7486b70f2532a289603340862c06eea5a1e650c1ffeda2ce1238516a"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"proc-macro2",
|
||||
|
|
@ -439,9 +368,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "napi-sys"
|
||||
version = "3.3.0"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85fbf1fa9f1babfe396d74bbbf52b3643770243e8f5b0b46715d4caf7f0dfc9a"
|
||||
checksum = "3e4e7135a8f97aa0f1509cce21a8a1f9dcec1b50d8dee006b48a5adb69a9d64d"
|
||||
dependencies = [
|
||||
"libloading",
|
||||
]
|
||||
|
|
@ -474,9 +403,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
|
|
@ -511,18 +440,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.12.0"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
|
||||
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
|
|
@ -530,9 +459,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.13.0"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
|
|
@ -546,7 +475,7 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
|||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata 0.4.18",
|
||||
"regex-automata 0.4.8",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
|
|
@ -561,9 +490,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.18"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
||||
checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
|
|
@ -631,18 +560,6 @@ dependencies = [
|
|||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "similar"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.10.0"
|
||||
|
|
@ -651,9 +568,9 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.87"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
|
||||
checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -678,11 +595,11 @@ dependencies = [
|
|||
"bexpand",
|
||||
"bstr",
|
||||
"classification-macros",
|
||||
"crossbeam",
|
||||
"dunce",
|
||||
"fast-glob",
|
||||
"globwalk",
|
||||
"ignore 0.4.33",
|
||||
"insta",
|
||||
"ignore 0.4.23",
|
||||
"log",
|
||||
"pretty_assertions",
|
||||
"rayon",
|
||||
|
|
@ -844,12 +761,6 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
|
|
@ -868,15 +779,6 @@ dependencies = [
|
|||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -13,10 +13,10 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/tailwindlabs/tailwindcss/actions"><img src="https://img.shields.io/github/actions/workflow/status/tailwindlabs/tailwindcss/ci.yml?branch=main" 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=next" 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/tailwindlabs/tailwindcss/releases"><img src="https://img.shields.io/npm/v/tailwindcss.svg" alt="Latest Release"></a>
|
||||
<a href="https://github.com/tailwindlabs/tailwindcss/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/tailwindcss.svg" alt="License"></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>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
|
@ -29,8 +29,8 @@ For full documentation, visit [tailwindcss.com](https://tailwindcss.com).
|
|||
|
||||
For help, discussion about best practices, or feature ideas:
|
||||
|
||||
[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions)
|
||||
[Discuss Tailwind CSS on GitHub](https://github.com/tailwindcss/tailwindcss/discussions)
|
||||
|
||||
## Contributing
|
||||
|
||||
If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**.
|
||||
If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/next/.github/CONTRIBUTING.md) **before submitting a pull request**.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "ignore"
|
||||
version = "0.4.33" #:version
|
||||
version = "0.4.23" #:version
|
||||
authors = ["Andrew Gallant <jamslam@gmail.com>"]
|
||||
description = """
|
||||
A fast library for efficiently matching ignore files such as `.gitignore`
|
||||
|
|
@ -12,10 +12,7 @@ repository = "https://github.com/BurntSushi/ripgrep/tree/master/crates/ignore"
|
|||
readme = "README.md"
|
||||
keywords = ["glob", "ignore", "gitignore", "pattern", "file"]
|
||||
license = "Unlicense OR MIT"
|
||||
# CHANGED: Use an explicit edition instead of `edition.workspace = true` since this crate is
|
||||
# vendored into the Tailwind CSS workspace.
|
||||
edition = "2024"
|
||||
rust-version = "1.88"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "ignore"
|
||||
|
|
@ -23,17 +20,15 @@ bench = false
|
|||
|
||||
[dependencies]
|
||||
crossbeam-deque = "0.8.3"
|
||||
# CHANGED: Use the published globset crate instead of a path dependency.
|
||||
globset = "0.4.20"
|
||||
globset = "0.4.16"
|
||||
log = "0.4.20"
|
||||
memchr = "2.6.3"
|
||||
same-file = "1.0.6"
|
||||
walkdir = "2.4.0"
|
||||
# CHANGED: Added `dunce` to canonicalize paths without UNC prefixes on Windows.
|
||||
dunce = "1.0.5"
|
||||
|
||||
[dependencies.regex-automata]
|
||||
version = "0.4.18"
|
||||
version = "0.4.0"
|
||||
default-features = false
|
||||
features = ["std", "perf", "syntax", "meta", "nfa", "hybrid", "dfa-onepass"]
|
||||
|
||||
|
|
@ -42,7 +37,7 @@ version = "0.1.2"
|
|||
|
||||
[dev-dependencies]
|
||||
bstr = { version = "1.6.2", default-features = false, features = ["std"] }
|
||||
crossbeam-channel = "0.5.15"
|
||||
crossbeam-channel = "0.5.8"
|
||||
|
||||
[features]
|
||||
# DEPRECATED. It is a no-op. SIMD is done automatically through runtime
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
ignore
|
||||
======
|
||||
# ignore
|
||||
|
||||
The ignore crate provides a fast recursive directory iterator that respects
|
||||
various filters such as globs, file types and `.gitignore` files. This crate
|
||||
also provides lower level direct access to gitignore and file type matchers.
|
||||
|
|
@ -29,7 +29,6 @@ recursively traverse the current directory while automatically filtering out
|
|||
files and directories according to ignore globs found in files like
|
||||
`.ignore` and `.gitignore`:
|
||||
|
||||
|
||||
```rust,no_run
|
||||
use ignore::Walk;
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ fn main() {
|
|||
let stdout_thread = std::thread::spawn(move || {
|
||||
let mut stdout = std::io::BufWriter::new(std::io::stdout());
|
||||
for dent in rx {
|
||||
stdout.write_all(&Vec::from_path_lossy(dent.path())).unwrap();
|
||||
stdout.write_all(b"\n").unwrap();
|
||||
stdout.write(&*Vec::from_path_lossy(dent.path())).unwrap();
|
||||
stdout.write(b"\n").unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -27,10 +27,9 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
|
|||
(&["bat", "batch"], &["*.bat"]),
|
||||
(&["bazel"], &[
|
||||
"*.bazel", "*.bzl", "*.BUILD", "*.bazelrc", "BUILD", "MODULE.bazel",
|
||||
"WORKSPACE", "WORKSPACE.bazel", "WORKSPACE.bzlmod",
|
||||
"WORKSPACE", "WORKSPACE.bazel",
|
||||
]),
|
||||
(&["bitbake"], &["*.bb", "*.bbappend", "*.bbclass", "*.conf", "*.inc"]),
|
||||
(&["boxlang"], &["*.bx", "*.bxm", "*.bxs"]),
|
||||
(&["brotli"], &["*.br"]),
|
||||
(&["buildstream"], &["*.bst"]),
|
||||
(&["bzip2"], &["*.bz2", "*.tbz2"]),
|
||||
|
|
@ -40,14 +39,12 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
|
|||
(&["carp"], &["*.carp"]),
|
||||
(&["cbor"], &["*.cbor"]),
|
||||
(&["ceylon"], &["*.ceylon"]),
|
||||
(&["cfml"], &["*.cfc", "*.cfm"]),
|
||||
(&["clojure"], &["*.clj", "*.cljc", "*.cljs", "*.cljx"]),
|
||||
(&["cmake"], &["*.cmake", "CMakeLists.txt"]),
|
||||
(&["cmd"], &["*.bat", "*.cmd"]),
|
||||
(&["cml"], &["*.cml"]),
|
||||
(&["coffeescript"], &["*.coffee"]),
|
||||
(&["config"], &["*.cfg", "*.conf", "*.config", "*.ini"]),
|
||||
(&["container"], &["*Containerfile*", "*Dockerfile*"]),
|
||||
(&["coq"], &["*.v"]),
|
||||
(&["cpp"], &[
|
||||
"*.[ChH]", "*.cc", "*.[ch]pp", "*.[ch]xx", "*.hh", "*.inl",
|
||||
|
|
@ -65,7 +62,7 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
|
|||
(&["cython"], &["*.pyx", "*.pxi", "*.pxd"]),
|
||||
(&["d"], &["*.d"]),
|
||||
(&["dart"], &["*.dart"]),
|
||||
(&["devicetree"], &["*.dts", "*.dtsi", "*.dtso"]),
|
||||
(&["devicetree"], &["*.dts", "*.dtsi"]),
|
||||
(&["dhall"], &["*.dhall"]),
|
||||
(&["diff"], &["*.patch", "*.diff"]),
|
||||
(&["dita"], &["*.dita", "*.ditamap", "*.ditaval"]),
|
||||
|
|
@ -91,8 +88,6 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
|
|||
(&["fsharp"], &["*.fs", "*.fsx", "*.fsi"]),
|
||||
(&["fut"], &["*.fut"]),
|
||||
(&["gap"], &["*.g", "*.gap", "*.gi", "*.gd", "*.tst"]),
|
||||
(&["gdscript"], &["*.gd"]),
|
||||
(&["gleam"], &["*.gleam"]),
|
||||
(&["gn"], &["*.gn", "*.gni"]),
|
||||
(&["go"], &["*.go"]),
|
||||
(&["gprbuild"], &["*.gpr"]),
|
||||
|
|
@ -110,7 +105,6 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
|
|||
(&["hbs"], &["*.hbs"]),
|
||||
(&["hs"], &["*.hs", "*.lhs"]),
|
||||
(&["html"], &["*.htm", "*.html", "*.ejs"]),
|
||||
(&["hurl"], &["*.hurl"]),
|
||||
(&["hy"], &["*.hy"]),
|
||||
(&["idris"], &["*.idr", "*.lidr"]),
|
||||
(&["janet"], &["*.janet"]),
|
||||
|
|
@ -123,7 +117,6 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
|
|||
(&["julia"], &["*.jl"]),
|
||||
(&["jupyter"], &["*.ipynb", "*.jpynb"]),
|
||||
(&["k"], &["*.k"]),
|
||||
(&["kconfig"], &["Kconfig", "Kconfig.*"]),
|
||||
(&["kotlin"], &["*.kt", "*.kts"]),
|
||||
(&["lean"], &["*.lean"]),
|
||||
(&["less"], &["*.less"]),
|
||||
|
|
@ -156,7 +149,6 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
|
|||
]),
|
||||
(&["lilypond"], &["*.ly", "*.ily"]),
|
||||
(&["lisp"], &["*.el", "*.jl", "*.lisp", "*.lsp", "*.sc", "*.scm"]),
|
||||
(&["llvm"], &["*.ll"]),
|
||||
(&["lock"], &["*.lock", "package-lock.json"]),
|
||||
(&["log"], &["*.log"]),
|
||||
(&["lua"], &["*.lua"]),
|
||||
|
|
@ -167,7 +159,6 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
|
|||
"[Gg][Nn][Uu]makefile", "[Mm]akefile",
|
||||
"[Gg][Nn][Uu]makefile.am", "[Mm]akefile.am",
|
||||
"[Gg][Nn][Uu]makefile.in", "[Mm]akefile.in",
|
||||
"Makefile.*",
|
||||
"*.mk", "*.mak"
|
||||
]),
|
||||
(&["mako"], &["*.mako", "*.mao"]),
|
||||
|
|
@ -187,11 +178,10 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
|
|||
(&["mint"], &["*.mint"]),
|
||||
(&["mk"], &["mkfile"]),
|
||||
(&["ml"], &["*.ml"]),
|
||||
(&["mojo"], &["*.mojo"]),
|
||||
(&["motoko"], &["*.mo"]),
|
||||
(&["msbuild"], &[
|
||||
"*.csproj", "*.fsproj", "*.vcxproj", "*.proj", "*.props", "*.targets",
|
||||
"*.sln", "*.slnf"
|
||||
"*.sln",
|
||||
]),
|
||||
(&["nim"], &["*.nim", "*.nimf", "*.nimble", "*.nims"]),
|
||||
(&["nix"], &["*.nix"]),
|
||||
|
|
@ -209,21 +199,18 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
|
|||
"*.php", "*.php3", "*.php4", "*.php5", "*.php7", "*.php8",
|
||||
"*.pht", "*.phtml"
|
||||
]),
|
||||
(&["pkgbuild"], &["PKGBUILD"]),
|
||||
(&["po"], &["*.po"]),
|
||||
(&["pod"], &["*.pod"]),
|
||||
(&["postscript"], &["*.eps", "*.ps"]),
|
||||
(&["prolog"], &["*.pl", "*.pro", "*.prolog", "*.P"]),
|
||||
(&["proto", "protobuf"], &["*.proto"]),
|
||||
(&["protobuf"], &["*.proto"]),
|
||||
(&["ps"], &["*.cdxml", "*.ps1", "*.ps1xml", "*.psd1", "*.psm1"]),
|
||||
(&["puppet"], &["*.epp", "*.erb", "*.pp", "*.rb"]),
|
||||
(&["purs"], &["*.purs"]),
|
||||
(&["py", "python"], &["*.py", "*.pyi"]),
|
||||
(&["qmake"], &["*.pro", "*.pri", "*.prf"]),
|
||||
(&["qml"], &["*.qml"]),
|
||||
(&["qrc"], &["*.qrc"]),
|
||||
(&["qui"], &["*.ui"]),
|
||||
(&["r"], &["*.R", "*.r", "*.Rmd", "*.rmd", "*.Rnw", "*.rnw"]),
|
||||
(&["r"], &["*.R", "*.r", "*.Rmd", "*.Rnw"]),
|
||||
(&["racket"], &["*.rkt"]),
|
||||
(&["raku"], &[
|
||||
"*.raku", "*.rakumod", "*.rakudoc", "*.rakutest",
|
||||
|
|
@ -235,22 +222,19 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
|
|||
(&["red"], &["*.r", "*.red", "*.reds"]),
|
||||
(&["rescript"], &["*.res", "*.resi"]),
|
||||
(&["robot"], &["*.robot"]),
|
||||
(&["rocq"], &["*.v"]),
|
||||
(&["rst"], &["*.rst"]),
|
||||
(&["ruby"], &[
|
||||
// Idiomatic files
|
||||
"config.ru", "Gemfile", ".irbrc", "Rakefile",
|
||||
// Extensions
|
||||
"*.gemspec", "*.rb", "*.rbw", "*.rake"
|
||||
"*.gemspec", "*.rb", "*.rbw"
|
||||
]),
|
||||
(&["rust"], &["*.rs"]),
|
||||
(&["sass"], &["*.sass", "*.scss"]),
|
||||
(&["scala"], &["*.scala", "*.sbt"]),
|
||||
(&["scdoc"], &["*.scd", "*.scdoc"]),
|
||||
(&["seed7"], &["*.sd7", "*.s7i"]),
|
||||
(&["sh"], &[
|
||||
// Portable/misc. init files
|
||||
".env", ".login", ".logout", ".profile", "profile",
|
||||
".login", ".logout", ".profile", "profile",
|
||||
// bash-specific init files
|
||||
".bash_login", "bash_login",
|
||||
".bash_logout", "bash_logout",
|
||||
|
|
@ -269,7 +253,7 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
|
|||
".zprofile", "zprofile",
|
||||
".zshrc", "zshrc",
|
||||
// Extensions
|
||||
"*.bash", "*.csh", "*.env", "*.ksh", "*.sh", "*.tcsh", "*.zsh",
|
||||
"*.bash", "*.csh", "*.ksh", "*.sh", "*.tcsh", "*.zsh",
|
||||
]),
|
||||
(&["slim"], &["*.skim", "*.slim", "*.slime"]),
|
||||
(&["smarty"], &["*.tpl"]),
|
||||
|
|
@ -279,10 +263,9 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
|
|||
(&["spark"], &["*.spark"]),
|
||||
(&["spec"], &["*.spec"]),
|
||||
(&["sql"], &["*.sql", "*.psql"]),
|
||||
(&["ssa"], &["*.ssa"]),
|
||||
(&["stylus"], &["*.styl"]),
|
||||
(&["sv"], &["*.v", "*.vg", "*.sv", "*.svh", "*.h"]),
|
||||
(&["svelte"], &["*.svelte", "*.svelte.ts"]),
|
||||
(&["svelte"], &["*.svelte"]),
|
||||
(&["svg"], &["*.svg"]),
|
||||
(&["swift"], &["*.swift"]),
|
||||
(&["swig"], &["*.def", "*.i"]),
|
||||
|
|
@ -297,8 +280,9 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
|
|||
(&["texinfo"], &["*.texi"]),
|
||||
(&["textile"], &["*.textile"]),
|
||||
(&["tf"], &[
|
||||
"*.tf", "*.tf.json", "*.tfvars", "*.tfvars.json",
|
||||
"*.terraformrc", "terraform.rc", "*.tfrc", "*.terraform.lock.hcl",
|
||||
"*.tf", "*.auto.tfvars", "terraform.tfvars", "*.tf.json",
|
||||
"*.auto.tfvars.json", "terraform.tfvars.json", "*.terraformrc",
|
||||
"terraform.rc", "*.tfrc", "*.terraform.lock.hcl",
|
||||
]),
|
||||
(&["thrift"], &["*.thrift"]),
|
||||
(&["toml"], &["*.toml", "Cargo.lock"]),
|
||||
|
|
@ -306,7 +290,6 @@ pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
|
|||
(&["twig"], &["*.twig"]),
|
||||
(&["txt"], &["*.txt"]),
|
||||
(&["typoscript"], &["*.typoscript", "*.ts"]),
|
||||
(&["typst"], &["*.typ"]),
|
||||
(&["usd"], &["*.usd", "*.usda", "*.usdc"]),
|
||||
(&["v"], &["*.v", "*.vsh"]),
|
||||
(&["vala"], &["*.vala"]),
|
||||
|
|
@ -365,14 +348,4 @@ mod tests {
|
|||
previous_name = name;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_types_aliases_are_sorted() {
|
||||
for (aliases, _) in DEFAULT_TYPES.iter() {
|
||||
assert!(
|
||||
aliases.is_sorted(),
|
||||
"this alias list is not sorted: {aliases:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -20,8 +20,8 @@ use {
|
|||
};
|
||||
|
||||
use crate::{
|
||||
Error, Match, PartialErrorBuilder,
|
||||
pathutil::{is_file_name, strip_prefix},
|
||||
Error, Match, PartialErrorBuilder,
|
||||
};
|
||||
|
||||
/// Glob represents a single glob in a gitignore file.
|
||||
|
|
@ -102,9 +102,7 @@ impl Gitignore {
|
|||
///
|
||||
/// Note that I/O errors are ignored. For more granular control over
|
||||
/// errors, use `GitignoreBuilder`.
|
||||
pub fn new<P: AsRef<Path>>(
|
||||
gitignore_path: P,
|
||||
) -> (Gitignore, Option<Error>) {
|
||||
pub fn new<P: AsRef<Path>>(gitignore_path: P) -> (Gitignore, Option<Error>) {
|
||||
let path = gitignore_path.as_ref();
|
||||
let parent = path.parent().unwrap_or(Path::new("/"));
|
||||
let mut builder = GitignoreBuilder::new(parent);
|
||||
|
|
@ -125,26 +123,12 @@ impl Gitignore {
|
|||
/// The global config file path is specified by git's `core.excludesFile`
|
||||
/// config option.
|
||||
///
|
||||
/// # Behavior
|
||||
///
|
||||
/// This routine does its best to discover any global git exclude files.
|
||||
/// This will try to parse out the `excludesFile` config option in your
|
||||
/// global git configuration, if necessary.
|
||||
///
|
||||
/// The specific things this routine tries (which are subject to change
|
||||
/// based on how git behaves) are:
|
||||
///
|
||||
///
|
||||
///
|
||||
/// Git's config file location is `$HOME/.gitconfig`. If `$HOME/.gitconfig`
|
||||
/// does not exist or does not specify `core.excludesFile`, then
|
||||
/// `$XDG_CONFIG_HOME/git/ignore` is read. If `$XDG_CONFIG_HOME` is not
|
||||
/// set or is empty, then `$HOME/.config/git/ignore` is used instead.
|
||||
pub fn global() -> (Gitignore, Option<Error>) {
|
||||
match std::env::current_dir() {
|
||||
Ok(cwd) => GitignoreBuilder::new(cwd).build_global(),
|
||||
Err(err) => (Gitignore::empty(), Some(err.into())),
|
||||
}
|
||||
GitignoreBuilder::new("").build_global()
|
||||
}
|
||||
|
||||
/// Creates a new empty gitignore matcher that never matches anything.
|
||||
|
|
@ -158,8 +142,7 @@ impl Gitignore {
|
|||
num_ignores: 0,
|
||||
num_whitelists: 0,
|
||||
matches: None,
|
||||
// CHANGED: Add a flag to have Gitignore rules that apply only to
|
||||
// files.
|
||||
// CHANGED: Add a flag to have Gitignore rules that apply only to files.
|
||||
only_on_files: false,
|
||||
}
|
||||
}
|
||||
|
|
@ -204,11 +187,7 @@ impl Gitignore {
|
|||
/// determined by a common suffix of the directory containing this
|
||||
/// gitignore) is stripped. If there is no common suffix/prefix overlap,
|
||||
/// then `path` is assumed to be relative to this matcher.
|
||||
pub fn matched<P: AsRef<Path>>(
|
||||
&self,
|
||||
path: P,
|
||||
is_dir: bool,
|
||||
) -> Match<&Glob> {
|
||||
pub fn matched<P: AsRef<Path>>(&self, path: P, is_dir: bool) -> Match<&Glob> {
|
||||
if self.is_empty() {
|
||||
return Match::None;
|
||||
}
|
||||
|
|
@ -261,16 +240,11 @@ impl Gitignore {
|
|||
}
|
||||
|
||||
/// Like matched, but takes a path that has already been stripped.
|
||||
fn matched_stripped<P: AsRef<Path>>(
|
||||
&self,
|
||||
path: P,
|
||||
is_dir: bool,
|
||||
) -> Match<&Glob> {
|
||||
fn matched_stripped<P: AsRef<Path>>(&self, path: P, is_dir: bool) -> Match<&Glob> {
|
||||
if self.is_empty() {
|
||||
return Match::None;
|
||||
}
|
||||
// CHANGED: Rules marked as only_on_files can not match against
|
||||
// directories.
|
||||
// CHANGED: Rules marked as only_on_files can not match against directories.
|
||||
if self.only_on_files && is_dir {
|
||||
return Match::None;
|
||||
}
|
||||
|
|
@ -293,10 +267,7 @@ impl Gitignore {
|
|||
|
||||
/// Strips the given path such that it's suitable for matching with this
|
||||
/// gitignore matcher.
|
||||
fn strip<'a, P: 'a + AsRef<Path> + ?Sized>(
|
||||
&'a self,
|
||||
path: &'a P,
|
||||
) -> &'a Path {
|
||||
fn strip<'a, P: 'a + AsRef<Path> + ?Sized>(&'a self, path: &'a P) -> &'a Path {
|
||||
let mut path = path.as_ref();
|
||||
// A leading ./ is completely superfluous. We also strip it from
|
||||
// our gitignore root path, so we need to strip it from our candidate
|
||||
|
|
@ -332,7 +303,6 @@ pub struct GitignoreBuilder {
|
|||
root: PathBuf,
|
||||
globs: Vec<Glob>,
|
||||
case_insensitive: bool,
|
||||
allow_unclosed_class: bool,
|
||||
// CHANGED: Add a flag to have Gitignore rules that apply only to files.
|
||||
only_on_files: bool,
|
||||
}
|
||||
|
|
@ -351,9 +321,7 @@ impl GitignoreBuilder {
|
|||
root: strip_prefix("./", root).unwrap_or(root).to_path_buf(),
|
||||
globs: vec![],
|
||||
case_insensitive: false,
|
||||
allow_unclosed_class: true,
|
||||
// CHANGED: Add a flag to have Gitignore rules that apply only to
|
||||
// files.
|
||||
// CHANGED: Add a flag to have Gitignore rules that apply only to files.
|
||||
only_on_files: false,
|
||||
}
|
||||
}
|
||||
|
|
@ -364,21 +332,18 @@ impl GitignoreBuilder {
|
|||
pub fn build(&self) -> Result<Gitignore, Error> {
|
||||
let nignore = self.globs.iter().filter(|g| !g.is_whitelist()).count();
|
||||
let nwhite = self.globs.iter().filter(|g| g.is_whitelist()).count();
|
||||
let set = self
|
||||
.builder
|
||||
.build()
|
||||
.map_err(|err| Error::Glob { glob: None, err: err.to_string() })?;
|
||||
let set = self.builder.build().map_err(|err| Error::Glob {
|
||||
glob: None,
|
||||
err: err.to_string(),
|
||||
})?;
|
||||
Ok(Gitignore {
|
||||
set,
|
||||
root: self.root.clone(),
|
||||
globs: self.globs.clone(),
|
||||
num_ignores: nignore as u64,
|
||||
num_whitelists: nwhite as u64,
|
||||
matches: Some(Arc::new(
|
||||
Pool::with_available_parallelism_capacity(|| vec![]),
|
||||
)),
|
||||
// CHANGED: Add a flag to have Gitignore rules that apply only to
|
||||
// files.
|
||||
matches: Some(Arc::new(Pool::new(|| vec![]))),
|
||||
// CHANGED: Add a flag to have Gitignore rules that apply only to files.
|
||||
only_on_files: self.only_on_files,
|
||||
})
|
||||
}
|
||||
|
|
@ -438,12 +403,6 @@ impl GitignoreBuilder {
|
|||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Match Git's handling of .gitignore files that begin with the Unicode BOM
|
||||
const UTF8_BOM: &str = "\u{feff}";
|
||||
let line =
|
||||
if i == 0 { line.trim_start_matches(UTF8_BOM) } else { &line };
|
||||
|
||||
if let Err(err) = self.add_line(Some(path.to_path_buf()), &line) {
|
||||
errs.push(err.tagged(path, lineno));
|
||||
}
|
||||
|
|
@ -547,7 +506,6 @@ impl GitignoreBuilder {
|
|||
.literal_separator(true)
|
||||
.case_insensitive(self.case_insensitive)
|
||||
.backslash_escape(true)
|
||||
.allow_unclosed_class(self.allow_unclosed_class)
|
||||
.build()
|
||||
.map_err(|err| Error::Glob {
|
||||
glob: Some(glob.original.clone()),
|
||||
|
|
@ -564,36 +522,13 @@ impl GitignoreBuilder {
|
|||
/// affected.
|
||||
///
|
||||
/// This is disabled by default.
|
||||
pub fn case_insensitive(
|
||||
&mut self,
|
||||
yes: bool,
|
||||
) -> Result<&mut GitignoreBuilder, Error> {
|
||||
pub fn case_insensitive(&mut self, yes: bool) -> Result<&mut GitignoreBuilder, Error> {
|
||||
// TODO: This should not return a `Result`. Fix this in the next semver
|
||||
// release.
|
||||
self.case_insensitive = yes;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Toggle whether unclosed character classes are allowed. When allowed,
|
||||
/// a `[` without a matching `]` is treated literally instead of resulting
|
||||
/// in a parse error.
|
||||
///
|
||||
/// For example, if this is set then the glob `[abc` will be treated as the
|
||||
/// literal string `[abc` instead of returning an error.
|
||||
///
|
||||
/// By default, this is true in order to match established `gitignore`
|
||||
/// semantics. Generally speaking, enabling this leads to worse failure
|
||||
/// modes since the glob parser becomes more permissive. You might want to
|
||||
/// enable this when compatibility (e.g., with POSIX glob implementations)
|
||||
/// is more important than good error messages.
|
||||
pub fn allow_unclosed_class(
|
||||
&mut self,
|
||||
yes: bool,
|
||||
) -> &mut GitignoreBuilder {
|
||||
self.allow_unclosed_class = yes;
|
||||
self
|
||||
}
|
||||
|
||||
/// CHANGED: Add a flag to have Gitignore rules that apply only to files.
|
||||
///
|
||||
/// If this is set, then the globs will only be matched against file paths.
|
||||
|
|
@ -609,56 +544,32 @@ impl GitignoreBuilder {
|
|||
///
|
||||
/// Note that the file path returned may not exist.
|
||||
pub fn gitconfig_excludes_path() -> Option<PathBuf> {
|
||||
// When GIT_CONFIG_GLOBAL is set, it replaces both $HOME/.gitconfig and
|
||||
// $XDG_CONFIG_HOME/git/config (per git 2.32+). Otherwise, git supports
|
||||
// $HOME/.gitconfig and $XDG_CONFIG_HOME/git/config simultaneously, where
|
||||
// $HOME/.gitconfig takes precedent.
|
||||
gitconfig_global_env_contents()
|
||||
.and_then(|x| parse_excludes_file(&x))
|
||||
.or_else(|| {
|
||||
gitconfig_home_contents().and_then(|x| parse_excludes_file(&x))
|
||||
})
|
||||
.or_else(|| {
|
||||
gitconfig_xdg_contents().and_then(|x| parse_excludes_file(&x))
|
||||
})
|
||||
// System-level config has the lowest priority for core.excludesFile.
|
||||
// GIT_CONFIG_SYSTEM overrides the default /etc/gitconfig path.
|
||||
.or_else(|| {
|
||||
gitconfig_system_contents().and_then(|x| parse_excludes_file(&x))
|
||||
})
|
||||
.or_else(excludes_file_default)
|
||||
}
|
||||
|
||||
/// Returns the file contents of git's global config file from the path
|
||||
/// specified by the `GIT_CONFIG_GLOBAL` environment variable.
|
||||
fn gitconfig_global_env_contents() -> Option<Vec<u8>> {
|
||||
let path = std::env::var_os("GIT_CONFIG_GLOBAL").map(PathBuf::from)?;
|
||||
if path.as_os_str().is_empty() {
|
||||
return None;
|
||||
// git supports $HOME/.gitconfig and $XDG_CONFIG_HOME/git/config. Notably,
|
||||
// both can be active at the same time, where $HOME/.gitconfig takes
|
||||
// precedent. So if $HOME/.gitconfig defines a `core.excludesFile`, then
|
||||
// we're done.
|
||||
match gitconfig_home_contents().and_then(|x| parse_excludes_file(&x)) {
|
||||
Some(path) => return Some(path),
|
||||
None => {}
|
||||
}
|
||||
let mut file = BufReader::new(File::open(path).ok()?);
|
||||
let mut contents = vec![];
|
||||
file.read_to_end(&mut contents).ok().map(|_| contents)
|
||||
}
|
||||
|
||||
/// Returns the file contents of git's system-level config file.
|
||||
///
|
||||
/// Checks `GIT_CONFIG_SYSTEM` first, then falls back to `/etc/gitconfig`.
|
||||
fn gitconfig_system_contents() -> Option<Vec<u8>> {
|
||||
let path = std::env::var_os("GIT_CONFIG_SYSTEM")
|
||||
.map(PathBuf::from)
|
||||
.filter(|x| !x.as_os_str().is_empty())
|
||||
.unwrap_or_else(|| PathBuf::from("/etc/gitconfig"));
|
||||
let mut file = BufReader::new(File::open(path).ok()?);
|
||||
let mut contents = vec![];
|
||||
file.read_to_end(&mut contents).ok().map(|_| contents)
|
||||
match gitconfig_xdg_contents().and_then(|x| parse_excludes_file(&x)) {
|
||||
Some(path) => return Some(path),
|
||||
None => {}
|
||||
}
|
||||
excludes_file_default()
|
||||
}
|
||||
|
||||
/// Returns the file contents of git's global config file, if one exists, in
|
||||
/// the user's home directory.
|
||||
fn gitconfig_home_contents() -> Option<Vec<u8>> {
|
||||
let home = home_dir()?;
|
||||
let mut file = BufReader::new(File::open(home.join(".gitconfig")).ok()?);
|
||||
let home = match home_dir() {
|
||||
None => return None,
|
||||
Some(home) => home,
|
||||
};
|
||||
let mut file = match File::open(home.join(".gitconfig")) {
|
||||
Err(_) => return None,
|
||||
Ok(file) => BufReader::new(file),
|
||||
};
|
||||
let mut contents = vec![];
|
||||
file.read_to_end(&mut contents).ok().map(|_| contents)
|
||||
}
|
||||
|
|
@ -667,11 +578,19 @@ fn gitconfig_home_contents() -> Option<Vec<u8>> {
|
|||
/// the user's XDG_CONFIG_HOME directory.
|
||||
fn gitconfig_xdg_contents() -> Option<Vec<u8>> {
|
||||
let path = std::env::var_os("XDG_CONFIG_HOME")
|
||||
.map(PathBuf::from)
|
||||
.filter(|x| !x.as_os_str().is_empty())
|
||||
.and_then(|x| {
|
||||
if x.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(PathBuf::from(x))
|
||||
}
|
||||
})
|
||||
.or_else(|| home_dir().map(|p| p.join(".config")))
|
||||
.map(|x| x.join("git/config"))?;
|
||||
let mut file = BufReader::new(File::open(path).ok()?);
|
||||
.map(|x| x.join("git/config"));
|
||||
let mut file = match path.and_then(|p| File::open(p).ok()) {
|
||||
None => return None,
|
||||
Some(file) => BufReader::new(file),
|
||||
};
|
||||
let mut contents = vec![];
|
||||
file.read_to_end(&mut contents).ok().map(|_| contents)
|
||||
}
|
||||
|
|
@ -681,8 +600,13 @@ fn gitconfig_xdg_contents() -> Option<Vec<u8>> {
|
|||
/// Specifically, this respects XDG_CONFIG_HOME.
|
||||
fn excludes_file_default() -> Option<PathBuf> {
|
||||
std::env::var_os("XDG_CONFIG_HOME")
|
||||
.map(PathBuf::from)
|
||||
.filter(|x| !x.as_os_str().is_empty())
|
||||
.and_then(|x| {
|
||||
if x.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(PathBuf::from(x))
|
||||
}
|
||||
})
|
||||
.or_else(|| home_dir().map(|p| p.join(".config")))
|
||||
.map(|x| x.join("git/ignore"))
|
||||
}
|
||||
|
|
@ -711,7 +635,9 @@ fn parse_excludes_file(data: &[u8]) -> Option<PathBuf> {
|
|||
re.captures(data, &mut caps);
|
||||
let span = caps.get_group(1)?;
|
||||
let candidate = &data[span];
|
||||
std::str::from_utf8(candidate).ok().map(|s| PathBuf::from(expand_tilde(s)))
|
||||
std::str::from_utf8(candidate)
|
||||
.ok()
|
||||
.map(|s| PathBuf::from(expand_tilde(s)))
|
||||
}
|
||||
|
||||
/// Expands ~ in file paths to the value of $HOME.
|
||||
|
|
@ -873,10 +799,7 @@ mod tests {
|
|||
fn parse_excludes_file4() {
|
||||
let data = bytes("[core]\nexcludesFile = \"~/foo/bar\"");
|
||||
let got = super::parse_excludes_file(&data);
|
||||
assert_eq!(
|
||||
path_string(got.unwrap()),
|
||||
super::expand_tilde("~/foo/bar")
|
||||
);
|
||||
assert_eq!(path_string(got.unwrap()), super::expand_tilde("~/foo/bar"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -48,16 +48,13 @@ See the documentation for `WalkBuilder` for many other options.
|
|||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub use crate::incremental::{IncrementalIgnore, IncrementalMatch};
|
||||
pub use crate::walk::{
|
||||
DirEntry, ParallelVisitor, ParallelVisitorBuilder, Walk, WalkBuilder,
|
||||
WalkParallel, WalkState,
|
||||
DirEntry, ParallelVisitor, ParallelVisitorBuilder, Walk, WalkBuilder, WalkParallel, WalkState,
|
||||
};
|
||||
|
||||
mod default_types;
|
||||
mod dir;
|
||||
pub mod gitignore;
|
||||
mod incremental;
|
||||
pub mod overrides;
|
||||
mod pathutil;
|
||||
pub mod types;
|
||||
|
|
@ -123,31 +120,34 @@ impl Clone for Error {
|
|||
fn clone(&self) -> Error {
|
||||
match *self {
|
||||
Error::Partial(ref errs) => Error::Partial(errs.clone()),
|
||||
Error::WithLineNumber { line, ref err } => {
|
||||
Error::WithLineNumber { line, err: err.clone() }
|
||||
}
|
||||
Error::WithPath { ref path, ref err } => {
|
||||
Error::WithPath { path: path.clone(), err: err.clone() }
|
||||
}
|
||||
Error::WithDepth { depth, ref err } => {
|
||||
Error::WithDepth { depth, err: err.clone() }
|
||||
}
|
||||
Error::Loop { ref ancestor, ref child } => Error::Loop {
|
||||
Error::WithLineNumber { line, ref err } => Error::WithLineNumber {
|
||||
line,
|
||||
err: err.clone(),
|
||||
},
|
||||
Error::WithPath { ref path, ref err } => Error::WithPath {
|
||||
path: path.clone(),
|
||||
err: err.clone(),
|
||||
},
|
||||
Error::WithDepth { depth, ref err } => Error::WithDepth {
|
||||
depth,
|
||||
err: err.clone(),
|
||||
},
|
||||
Error::Loop {
|
||||
ref ancestor,
|
||||
ref child,
|
||||
} => Error::Loop {
|
||||
ancestor: ancestor.clone(),
|
||||
child: child.clone(),
|
||||
},
|
||||
Error::Io(ref err) => match err.raw_os_error() {
|
||||
Some(e) => Error::Io(std::io::Error::from_raw_os_error(e)),
|
||||
None => {
|
||||
Error::Io(std::io::Error::new(err.kind(), err.to_string()))
|
||||
}
|
||||
None => Error::Io(std::io::Error::new(err.kind(), err.to_string())),
|
||||
},
|
||||
Error::Glob { ref glob, ref err } => {
|
||||
Error::Glob { glob: glob.clone(), err: err.clone() }
|
||||
}
|
||||
Error::UnrecognizedFileType(ref err) => {
|
||||
Error::UnrecognizedFileType(err.clone())
|
||||
}
|
||||
Error::Glob { ref glob, ref err } => Error::Glob {
|
||||
glob: glob.clone(),
|
||||
err: err.clone(),
|
||||
},
|
||||
Error::UnrecognizedFileType(ref err) => Error::UnrecognizedFileType(err.clone()),
|
||||
Error::InvalidDefinition => Error::InvalidDefinition,
|
||||
}
|
||||
}
|
||||
|
|
@ -269,14 +269,19 @@ impl Error {
|
|||
|
||||
/// Turn an error into a tagged error with the given depth.
|
||||
fn with_depth(self, depth: usize) -> Error {
|
||||
Error::WithDepth { depth, err: Box::new(self) }
|
||||
Error::WithDepth {
|
||||
depth,
|
||||
err: Box::new(self),
|
||||
}
|
||||
}
|
||||
|
||||
/// Turn an error into a tagged error with the given file path and line
|
||||
/// number. If path is empty, then it is omitted from the error.
|
||||
fn tagged<P: AsRef<Path>>(self, path: P, lineno: u64) -> Error {
|
||||
let errline =
|
||||
Error::WithLineNumber { line: lineno, err: Box::new(self) };
|
||||
let errline = Error::WithLineNumber {
|
||||
line: lineno,
|
||||
err: Box::new(self),
|
||||
};
|
||||
if path.as_ref().as_os_str().is_empty() {
|
||||
return errline;
|
||||
}
|
||||
|
|
@ -296,12 +301,12 @@ impl Error {
|
|||
};
|
||||
}
|
||||
let path = err.path().map(|p| p.to_path_buf());
|
||||
let mut ig_err = Error::WithDepth {
|
||||
depth,
|
||||
err: Box::new(Error::Io(std::io::Error::from(err))),
|
||||
};
|
||||
let mut ig_err = Error::Io(std::io::Error::from(err));
|
||||
if let Some(path) = path {
|
||||
ig_err = Error::WithPath { path, err: Box::new(ig_err) };
|
||||
ig_err = Error::WithPath {
|
||||
path,
|
||||
err: Box::new(ig_err),
|
||||
};
|
||||
}
|
||||
ig_err
|
||||
}
|
||||
|
|
@ -328,8 +333,7 @@ impl std::fmt::Display for Error {
|
|||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match *self {
|
||||
Error::Partial(ref errs) => {
|
||||
let msgs: Vec<String> =
|
||||
errs.iter().map(|err| err.to_string()).collect();
|
||||
let msgs: Vec<String> = errs.iter().map(|err| err.to_string()).collect();
|
||||
write!(f, "{}", msgs.join("\n"))
|
||||
}
|
||||
Error::WithLineNumber { line, ref err } => {
|
||||
|
|
@ -339,7 +343,10 @@ impl std::fmt::Display for Error {
|
|||
write!(f, "{}: {}", path.display(), err)
|
||||
}
|
||||
Error::WithDepth { ref err, .. } => err.fmt(f),
|
||||
Error::Loop { ref ancestor, ref child } => write!(
|
||||
Error::Loop {
|
||||
ref ancestor,
|
||||
ref child,
|
||||
} => write!(
|
||||
f,
|
||||
"File system loop found: \
|
||||
{} points to an ancestor {}",
|
||||
|
|
@ -347,8 +354,14 @@ impl std::fmt::Display for Error {
|
|||
ancestor.display()
|
||||
),
|
||||
Error::Io(ref err) => err.fmt(f),
|
||||
Error::Glob { glob: None, ref err } => write!(f, "{}", err),
|
||||
Error::Glob { glob: Some(ref glob), ref err } => {
|
||||
Error::Glob {
|
||||
glob: None,
|
||||
ref err,
|
||||
} => write!(f, "{}", err),
|
||||
Error::Glob {
|
||||
glob: Some(ref glob),
|
||||
ref err,
|
||||
} => {
|
||||
write!(f, "error parsing glob '{}': {}", glob, err)
|
||||
}
|
||||
Error::UnrecognizedFileType(ref ty) => {
|
||||
|
|
@ -482,7 +495,11 @@ impl<T> Match<T> {
|
|||
|
||||
/// Return the match if it is not none. Otherwise, return other.
|
||||
pub fn or(self, other: Self) -> Self {
|
||||
if self.is_none() { other } else { self }
|
||||
if self.is_none() {
|
||||
other
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -494,8 +511,7 @@ mod tests {
|
|||
};
|
||||
|
||||
/// A convenient result type alias.
|
||||
pub(crate) type Result<T> =
|
||||
std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
||||
pub(crate) type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
||||
|
||||
macro_rules! err {
|
||||
($($tt:tt)*) => {
|
||||
|
|
@ -528,14 +544,13 @@ mod tests {
|
|||
|
||||
let tmpdir = env::temp_dir();
|
||||
for _ in 0..TRIES {
|
||||
let count = COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let count = COUNTER.fetch_add(1, Ordering::SeqCst);
|
||||
let path = tmpdir.join("rust-ignore").join(count.to_string());
|
||||
if path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
fs::create_dir_all(&path).map_err(|e| {
|
||||
err!("failed to create {}: {}", path.display(), e)
|
||||
})?;
|
||||
fs::create_dir_all(&path)
|
||||
.map_err(|e| err!("failed to create {}: {}", path.display(), e))?;
|
||||
return Ok(TempDir(path));
|
||||
}
|
||||
Err(err!("failed to create temp dir after {} tries", TRIES))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
/*!
|
||||
The overrides module provides a way to specify a set of override globs.
|
||||
|
||||
This provides functionality similar to `--include` or `--exclude` in command
|
||||
line tools.
|
||||
*/
|
||||
|
|
@ -8,8 +7,8 @@ line tools.
|
|||
use std::path::Path;
|
||||
|
||||
use crate::{
|
||||
Error, Match,
|
||||
gitignore::{self, Gitignore, GitignoreBuilder},
|
||||
Error, Match,
|
||||
};
|
||||
|
||||
/// Glob represents a single glob in an override matcher.
|
||||
|
|
@ -94,11 +93,7 @@ impl Override {
|
|||
/// given) is stripped. If there is no common suffix/prefix overlap, then
|
||||
/// `path` is assumed to reside in the same directory as the root path for
|
||||
/// this set of overrides.
|
||||
pub fn matched<'a, P: AsRef<Path>>(
|
||||
&'a self,
|
||||
path: P,
|
||||
is_dir: bool,
|
||||
) -> Match<Glob<'a>> {
|
||||
pub fn matched<'a, P: AsRef<Path>>(&'a self, path: P, is_dir: bool) -> Match<Glob<'a>> {
|
||||
if self.is_empty() {
|
||||
return Match::None;
|
||||
}
|
||||
|
|
@ -121,9 +116,9 @@ impl OverrideBuilder {
|
|||
///
|
||||
/// Matching is done relative to the directory path provided.
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> OverrideBuilder {
|
||||
let mut builder = GitignoreBuilder::new(path);
|
||||
builder.allow_unclosed_class(false);
|
||||
OverrideBuilder { builder }
|
||||
OverrideBuilder {
|
||||
builder: GitignoreBuilder::new(path),
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a new override matcher from the globs added so far.
|
||||
|
|
@ -146,41 +141,15 @@ impl OverrideBuilder {
|
|||
|
||||
/// Toggle whether the globs should be matched case insensitively or not.
|
||||
///
|
||||
/// When this option is changed, only globs added after the change will be
|
||||
/// affected.
|
||||
/// When this option is changed, only globs added after the change will be affected.
|
||||
///
|
||||
/// This is disabled by default.
|
||||
pub fn case_insensitive(
|
||||
&mut self,
|
||||
yes: bool,
|
||||
) -> Result<&mut OverrideBuilder, Error> {
|
||||
pub fn case_insensitive(&mut self, yes: bool) -> Result<&mut OverrideBuilder, Error> {
|
||||
// TODO: This should not return a `Result`. Fix this in the next semver
|
||||
// release.
|
||||
self.builder.case_insensitive(yes)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Toggle whether unclosed character classes are allowed. When allowed,
|
||||
/// a `[` without a matching `]` is treated literally instead of resulting
|
||||
/// in a parse error.
|
||||
///
|
||||
/// For example, if this is set then the glob `[abc` will be treated as the
|
||||
/// literal string `[abc` instead of returning an error.
|
||||
///
|
||||
/// By default, this is false. Generally speaking, enabling this leads to
|
||||
/// worse failure modes since the glob parser becomes more permissive. You
|
||||
/// might want to enable this when compatibility (e.g., with POSIX glob
|
||||
/// implementations) is more important than good error messages.
|
||||
///
|
||||
/// This default is different from the default for [`Gitignore`]. Namely,
|
||||
/// [`Gitignore`] is intended to match git's behavior as-is. But this
|
||||
/// abstraction for "override" globs does not necessarily conform to any
|
||||
/// other known specification and instead prioritizes better error
|
||||
/// messages.
|
||||
pub fn allow_unclosed_class(&mut self, yes: bool) -> &mut OverrideBuilder {
|
||||
self.builder.allow_unclosed_class(yes);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -283,8 +252,11 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn default_case_sensitive() {
|
||||
let ov =
|
||||
OverrideBuilder::new(ROOT).add("*.html").unwrap().build().unwrap();
|
||||
let ov = OverrideBuilder::new(ROOT)
|
||||
.add("*.html")
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
assert!(ov.matched("foo.html", false).is_whitelist());
|
||||
assert!(ov.matched("foo.HTML", false).is_ignore());
|
||||
assert!(ov.matched("foo.htm", false).is_ignore());
|
||||
|
|
|
|||
|
|
@ -2,89 +2,55 @@ use std::{ffi::OsStr, path::Path};
|
|||
|
||||
use crate::walk::DirEntry;
|
||||
|
||||
/// Returns true if and only if this path is considered to be hidden.
|
||||
/// Returns true if and only if this entry is considered to be hidden.
|
||||
///
|
||||
/// # Platform behavior
|
||||
/// This only returns true if the base name of the path starts with a `.`.
|
||||
///
|
||||
/// ## Windows
|
||||
/// On Unix, this implements a more optimized check.
|
||||
#[cfg(unix)]
|
||||
pub(crate) fn is_hidden(dent: &DirEntry) -> bool {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
if let Some(name) = file_name(dent.path()) {
|
||||
name.as_bytes().get(0) == Some(&b'.')
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if and only if this entry is considered to be hidden.
|
||||
///
|
||||
/// This returns true if one of the following is true:
|
||||
/// On Windows, this returns true if one of the following is true:
|
||||
///
|
||||
/// * The base name of the path starts with a `.`.
|
||||
/// * The file attributes have the `HIDDEN` property set.
|
||||
///
|
||||
/// ## All other platforms
|
||||
///
|
||||
/// This only returns true if the base name of the path starts with a `.`.
|
||||
pub(crate) fn is_hidden_path(dent: &Path) -> bool {
|
||||
#[cfg(not(windows))]
|
||||
fn imp(path: &Path) -> bool {
|
||||
is_hidden_path_only(path)
|
||||
}
|
||||
#[cfg(windows)]
|
||||
pub(crate) fn is_hidden(dent: &DirEntry) -> bool {
|
||||
use std::os::windows::fs::MetadataExt;
|
||||
use winapi_util::file;
|
||||
|
||||
#[cfg(windows)]
|
||||
fn imp(path: &Path) -> bool {
|
||||
use std::os::windows::fs::MetadataExt;
|
||||
use winapi_util::file;
|
||||
|
||||
if let Ok(md) = path.metadata() {
|
||||
if file::is_hidden(md.file_attributes() as u64) {
|
||||
return true;
|
||||
}
|
||||
// This looks like we're doing an extra stat call, but on Windows, the
|
||||
// directory traverser reuses the metadata retrieved from each directory
|
||||
// entry and stores it on the DirEntry itself. So this is "free."
|
||||
if let Ok(md) = dent.metadata() {
|
||||
if file::is_hidden(md.file_attributes() as u64) {
|
||||
return true;
|
||||
}
|
||||
is_hidden_path_only(path)
|
||||
}
|
||||
|
||||
imp(dent)
|
||||
if let Some(name) = file_name(dent.path()) {
|
||||
name.to_str().map(|s| s.starts_with(".")).unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if and only if this directory entry is considered to be
|
||||
/// hidden.
|
||||
///
|
||||
/// # Platform behavior
|
||||
///
|
||||
/// ## Windows
|
||||
///
|
||||
/// This returns true if one of the following is true:
|
||||
///
|
||||
/// * The base name of the path starts with a `.`.
|
||||
/// * The file attributes have the `HIDDEN` property set.
|
||||
///
|
||||
/// ## All other platforms
|
||||
/// Returns true if and only if this entry is considered to be hidden.
|
||||
///
|
||||
/// This only returns true if the base name of the path starts with a `.`.
|
||||
pub(crate) fn is_hidden_entry(dent: &DirEntry) -> bool {
|
||||
#[cfg(not(windows))]
|
||||
fn imp(dent: &DirEntry) -> bool {
|
||||
is_hidden_path_only(dent.path())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn imp(dent: &DirEntry) -> bool {
|
||||
use std::os::windows::fs::MetadataExt;
|
||||
use winapi_util::file;
|
||||
|
||||
// This looks like we're doing an extra stat call, but on Windows, the
|
||||
// directory traverser reuses the metadata retrieved from each directory
|
||||
// entry and stores it on the DirEntry itself. So this is "free."
|
||||
if let Ok(md) = dent.metadata() {
|
||||
if file::is_hidden(md.file_attributes() as u64) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
is_hidden_path_only(dent.path())
|
||||
}
|
||||
|
||||
imp(dent)
|
||||
}
|
||||
|
||||
/// Returns true if and only if this path is considered to be hidden from only
|
||||
/// the path itself.
|
||||
///
|
||||
/// This has the same behavior on all platforms.
|
||||
fn is_hidden_path_only(path: &Path) -> bool {
|
||||
if let Some(name) = file_name(path) {
|
||||
name.as_encoded_bytes().starts_with(b".")
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
pub(crate) fn is_hidden(dent: &DirEntry) -> bool {
|
||||
if let Some(name) = file_name(dent.path()) {
|
||||
name.to_str().map(|s| s.starts_with(".")).unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
|
@ -93,79 +59,83 @@ fn is_hidden_path_only(path: &Path) -> bool {
|
|||
/// Strip `prefix` from the `path` and return the remainder.
|
||||
///
|
||||
/// If `path` doesn't have a prefix `prefix`, then return `None`.
|
||||
#[cfg(unix)]
|
||||
pub(crate) fn strip_prefix<'a, P: AsRef<Path> + ?Sized>(
|
||||
prefix: &'a P,
|
||||
path: &'a Path,
|
||||
) -> Option<&'a Path> {
|
||||
#[cfg(unix)]
|
||||
fn imp<'a>(prefix: &'a Path, path: &'a Path) -> Option<&'a Path> {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
let prefix = prefix.as_os_str().as_bytes();
|
||||
let path = path.as_os_str().as_bytes();
|
||||
if prefix.len() > path.len() || prefix != &path[0..prefix.len()] {
|
||||
None
|
||||
} else {
|
||||
Some(&Path::new(OsStr::from_bytes(&path[prefix.len()..])))
|
||||
}
|
||||
let prefix = prefix.as_ref().as_os_str().as_bytes();
|
||||
let path = path.as_os_str().as_bytes();
|
||||
if prefix.len() > path.len() || prefix != &path[0..prefix.len()] {
|
||||
None
|
||||
} else {
|
||||
Some(&Path::new(OsStr::from_bytes(&path[prefix.len()..])))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn imp<'a>(prefix: &'a Path, path: &'a Path) -> Option<&'a Path> {
|
||||
path.strip_prefix(prefix).ok()
|
||||
}
|
||||
|
||||
imp(prefix.as_ref(), path)
|
||||
/// Strip `prefix` from the `path` and return the remainder.
|
||||
///
|
||||
/// If `path` doesn't have a prefix `prefix`, then return `None`.
|
||||
#[cfg(not(unix))]
|
||||
pub(crate) fn strip_prefix<'a, P: AsRef<Path> + ?Sized>(
|
||||
prefix: &'a P,
|
||||
path: &'a Path,
|
||||
) -> Option<&'a Path> {
|
||||
path.strip_prefix(prefix).ok()
|
||||
}
|
||||
|
||||
/// Returns true if this file path is just a file name. i.e., Its parent is
|
||||
/// the empty string.
|
||||
#[cfg(unix)]
|
||||
pub(crate) fn is_file_name<P: AsRef<Path>>(path: P) -> bool {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
memchr::memchr(b'/', path.as_ref().as_os_str().as_encoded_bytes())
|
||||
.is_none()
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
path.as_ref()
|
||||
.parent()
|
||||
.map(|p| p.as_os_str().is_empty())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
use memchr::memchr;
|
||||
|
||||
let path = path.as_ref().as_os_str().as_bytes();
|
||||
memchr(b'/', path).is_none()
|
||||
}
|
||||
|
||||
/// Returns true if this file path is just a file name. i.e., Its parent is
|
||||
/// the empty string.
|
||||
#[cfg(not(unix))]
|
||||
pub(crate) fn is_file_name<P: AsRef<Path>>(path: P) -> bool {
|
||||
path.as_ref()
|
||||
.parent()
|
||||
.map(|p| p.as_os_str().is_empty())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// The final component of the path, if it is a normal file.
|
||||
///
|
||||
/// If the path terminates in `.`, `..`, or consists solely of a root of
|
||||
/// prefix, this will return `None`.
|
||||
pub(crate) fn file_name<'a, P: AsRef<Path> + ?Sized>(
|
||||
path: &'a P,
|
||||
) -> Option<&'a OsStr> {
|
||||
#[cfg(unix)]
|
||||
fn imp(path: &Path) -> Option<&OsStr> {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
/// If the path terminates in ., .., or consists solely of a root of prefix,
|
||||
/// file_name will return None.
|
||||
#[cfg(unix)]
|
||||
pub(crate) fn file_name<'a, P: AsRef<Path> + ?Sized>(path: &'a P) -> Option<&'a OsStr> {
|
||||
use memchr::memrchr;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
use memchr::memrchr;
|
||||
|
||||
let path = path.as_os_str().as_bytes();
|
||||
if path.is_empty() {
|
||||
return None;
|
||||
} else if path.len() == 1 && path[0] == b'.' {
|
||||
return None;
|
||||
} else if path.last() == Some(&b'.') {
|
||||
return None;
|
||||
} else if path.len() >= 2 && &path[path.len() - 2..] == &b".."[..] {
|
||||
return None;
|
||||
}
|
||||
let last_slash = memrchr(b'/', path).map(|i| i + 1).unwrap_or(0);
|
||||
Some(OsStr::from_bytes(&path[last_slash..]))
|
||||
let path = path.as_ref().as_os_str().as_bytes();
|
||||
if path.is_empty() {
|
||||
return None;
|
||||
} else if path.len() == 1 && path[0] == b'.' {
|
||||
return None;
|
||||
} else if path.last() == Some(&b'.') {
|
||||
return None;
|
||||
} else if path.len() >= 2 && &path[path.len() - 2..] == &b".."[..] {
|
||||
return None;
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn imp(path: &Path) -> Option<&OsStr> {
|
||||
path.file_name()
|
||||
}
|
||||
|
||||
imp(path.as_ref())
|
||||
let last_slash = memrchr(b'/', path).map(|i| i + 1).unwrap_or(0);
|
||||
Some(OsStr::from_bytes(&path[last_slash..]))
|
||||
}
|
||||
|
||||
/// The final component of the path, if it is a normal file.
|
||||
///
|
||||
/// If the path terminates in ., .., or consists solely of a root of prefix,
|
||||
/// file_name will return None.
|
||||
#[cfg(not(unix))]
|
||||
pub(crate) fn file_name<'a, P: AsRef<Path> + ?Sized>(path: &'a P) -> Option<&'a OsStr> {
|
||||
path.as_ref().file_name()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ use {
|
|||
regex_automata::util::pool::Pool,
|
||||
};
|
||||
|
||||
use crate::{Error, Match, default_types::DEFAULT_TYPES, pathutil::file_name};
|
||||
use crate::{default_types::DEFAULT_TYPES, pathutil::file_name, Error, Match};
|
||||
|
||||
/// Glob represents a single glob in a set of file type definitions.
|
||||
///
|
||||
|
|
@ -204,12 +204,8 @@ impl<T> Selection<T> {
|
|||
|
||||
fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Selection<U> {
|
||||
match self {
|
||||
Selection::Select(name, inner) => {
|
||||
Selection::Select(name, f(inner))
|
||||
}
|
||||
Selection::Negate(name, inner) => {
|
||||
Selection::Negate(name, f(inner))
|
||||
}
|
||||
Selection::Select(name, inner) => Selection::Select(name, f(inner)),
|
||||
Selection::Negate(name, inner) => Selection::Negate(name, f(inner)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -231,9 +227,7 @@ impl Types {
|
|||
has_selected: false,
|
||||
glob_to_selection: vec![],
|
||||
set: GlobSetBuilder::new().build().unwrap(),
|
||||
matches: Arc::new(Pool::with_available_parallelism_capacity(
|
||||
|| vec![],
|
||||
)),
|
||||
matches: Arc::new(Pool::new(|| vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -260,11 +254,7 @@ impl Types {
|
|||
/// The path is considered ignored if it matches a negated file type.
|
||||
/// If at least one file type is selected and `path` doesn't match, then
|
||||
/// the path is also considered ignored.
|
||||
pub fn matched<'a, P: AsRef<Path>>(
|
||||
&'a self,
|
||||
path: P,
|
||||
is_dir: bool,
|
||||
) -> Match<Glob<'a>> {
|
||||
pub fn matched<'a, P: AsRef<Path>>(&'a self, path: P, is_dir: bool) -> Match<Glob<'a>> {
|
||||
// File types don't apply to directories, and we can't do anything
|
||||
// if our glob set is empty.
|
||||
if is_dir || self.set.is_empty() {
|
||||
|
|
@ -316,7 +306,10 @@ impl TypesBuilder {
|
|||
/// of default type definitions can be added with `add_defaults`, and
|
||||
/// additional type definitions can be added with `select` and `negate`.
|
||||
pub fn new() -> TypesBuilder {
|
||||
TypesBuilder { types: HashMap::new(), selections: vec![] }
|
||||
TypesBuilder {
|
||||
types: HashMap::new(),
|
||||
selections: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the current set of file type definitions *and* selections into
|
||||
|
|
@ -350,18 +343,17 @@ impl TypesBuilder {
|
|||
}
|
||||
selections.push(selection.clone().map(move |_| def));
|
||||
}
|
||||
let set = build_set
|
||||
.build()
|
||||
.map_err(|err| Error::Glob { glob: None, err: err.to_string() })?;
|
||||
let set = build_set.build().map_err(|err| Error::Glob {
|
||||
glob: None,
|
||||
err: err.to_string(),
|
||||
})?;
|
||||
Ok(Types {
|
||||
defs,
|
||||
selections,
|
||||
has_selected,
|
||||
glob_to_selection,
|
||||
set,
|
||||
matches: Arc::new(Pool::with_available_parallelism_capacity(
|
||||
|| vec![],
|
||||
)),
|
||||
matches: Arc::new(Pool::new(|| vec![])),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -385,10 +377,12 @@ impl TypesBuilder {
|
|||
pub fn select(&mut self, name: &str) -> &mut TypesBuilder {
|
||||
if name == "all" {
|
||||
for name in self.types.keys() {
|
||||
self.selections.push(Selection::Select(name.to_string(), ()));
|
||||
self.selections
|
||||
.push(Selection::Select(name.to_string(), ()));
|
||||
}
|
||||
} else {
|
||||
self.selections.push(Selection::Select(name.to_string(), ()));
|
||||
self.selections
|
||||
.push(Selection::Select(name.to_string(), ()));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
|
@ -399,10 +393,12 @@ impl TypesBuilder {
|
|||
pub fn negate(&mut self, name: &str) -> &mut TypesBuilder {
|
||||
if name == "all" {
|
||||
for name in self.types.keys() {
|
||||
self.selections.push(Selection::Negate(name.to_string(), ()));
|
||||
self.selections
|
||||
.push(Selection::Negate(name.to_string(), ()));
|
||||
}
|
||||
} else {
|
||||
self.selections.push(Selection::Negate(name.to_string(), ()));
|
||||
self.selections
|
||||
.push(Selection::Negate(name.to_string(), ()));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
|
@ -457,10 +453,7 @@ impl TypesBuilder {
|
|||
3 => {
|
||||
let name = parts[0];
|
||||
let types_string = parts[2];
|
||||
if name.is_empty()
|
||||
|| parts[1] != "include"
|
||||
|| types_string.is_empty()
|
||||
{
|
||||
if name.is_empty() || parts[1] != "include" || types_string.is_empty() {
|
||||
return Err(Error::InvalidDefinition);
|
||||
}
|
||||
let types = types_string.split(',');
|
||||
|
|
@ -470,8 +463,7 @@ impl TypesBuilder {
|
|||
return Err(Error::InvalidDefinition);
|
||||
}
|
||||
for type_name in types {
|
||||
let globs =
|
||||
self.types.get(type_name).unwrap().globs.clone();
|
||||
let globs = self.types.get(type_name).unwrap().globs.clone();
|
||||
for glob in globs {
|
||||
self.add(name, &glob)?;
|
||||
}
|
||||
|
|
@ -557,9 +549,30 @@ mod tests {
|
|||
|
||||
matched!(not, matchnot1, types(), vec!["rust"], vec![], "index.html");
|
||||
matched!(not, matchnot2, types(), vec![], vec!["rust"], "main.rs");
|
||||
matched!(not, matchnot3, types(), vec!["foo"], vec!["rust"], "main.rs");
|
||||
matched!(not, matchnot4, types(), vec!["rust"], vec!["foo"], "main.rs");
|
||||
matched!(not, matchnot5, types(), vec!["rust"], vec!["foo"], "main.foo");
|
||||
matched!(
|
||||
not,
|
||||
matchnot3,
|
||||
types(),
|
||||
vec!["foo"],
|
||||
vec!["rust"],
|
||||
"main.rs"
|
||||
);
|
||||
matched!(
|
||||
not,
|
||||
matchnot4,
|
||||
types(),
|
||||
vec!["rust"],
|
||||
vec!["foo"],
|
||||
"main.rs"
|
||||
);
|
||||
matched!(
|
||||
not,
|
||||
matchnot5,
|
||||
types(),
|
||||
vec!["rust"],
|
||||
vec!["foo"],
|
||||
"main.foo"
|
||||
);
|
||||
matched!(not, matchnot6, types(), vec!["combo"], vec![], "leftpad.js");
|
||||
matched!(not, matchnot7, types(), vec!["py"], vec![], "index.html");
|
||||
matched!(not, matchnot8, types(), vec!["python"], vec![], "doc.md");
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -2,8 +2,7 @@ use std::path::Path;
|
|||
|
||||
use ignore::gitignore::{Gitignore, GitignoreBuilder};
|
||||
|
||||
const IGNORE_FILE: &'static str =
|
||||
"tests/gitignore_matched_path_or_any_parents_tests.gitignore";
|
||||
const IGNORE_FILE: &'static str = "tests/gitignore_matched_path_or_any_parents_tests.gitignore";
|
||||
|
||||
fn get_gitignore() -> Gitignore {
|
||||
let mut builder = GitignoreBuilder::new("ROOT");
|
||||
|
|
@ -24,9 +23,7 @@ fn test_path_should_be_under_root() {
|
|||
#[test]
|
||||
fn test_files_in_root() {
|
||||
let gitignore = get_gitignore();
|
||||
let m = |path: &str| {
|
||||
gitignore.matched_path_or_any_parents(Path::new(path), false)
|
||||
};
|
||||
let m = |path: &str| gitignore.matched_path_or_any_parents(Path::new(path), false);
|
||||
|
||||
// 0x
|
||||
assert!(m("ROOT/file_root_00").is_ignore());
|
||||
|
|
@ -56,9 +53,7 @@ fn test_files_in_root() {
|
|||
#[test]
|
||||
fn test_files_in_deep() {
|
||||
let gitignore = get_gitignore();
|
||||
let m = |path: &str| {
|
||||
gitignore.matched_path_or_any_parents(Path::new(path), false)
|
||||
};
|
||||
let m = |path: &str| gitignore.matched_path_or_any_parents(Path::new(path), false);
|
||||
|
||||
// 0x
|
||||
assert!(m("ROOT/parent_dir/file_deep_00").is_ignore());
|
||||
|
|
@ -88,9 +83,8 @@ fn test_files_in_deep() {
|
|||
#[test]
|
||||
fn test_dirs_in_root() {
|
||||
let gitignore = get_gitignore();
|
||||
let m = |path: &str, is_dir: bool| {
|
||||
gitignore.matched_path_or_any_parents(Path::new(path), is_dir)
|
||||
};
|
||||
let m =
|
||||
|path: &str, is_dir: bool| gitignore.matched_path_or_any_parents(Path::new(path), is_dir);
|
||||
|
||||
// 00
|
||||
assert!(m("ROOT/dir_root_00", true).is_ignore());
|
||||
|
|
@ -192,25 +186,20 @@ fn test_dirs_in_root() {
|
|||
#[test]
|
||||
fn test_dirs_in_deep() {
|
||||
let gitignore = get_gitignore();
|
||||
let m = |path: &str, is_dir: bool| {
|
||||
gitignore.matched_path_or_any_parents(Path::new(path), is_dir)
|
||||
};
|
||||
let m =
|
||||
|path: &str, is_dir: bool| gitignore.matched_path_or_any_parents(Path::new(path), is_dir);
|
||||
|
||||
// 00
|
||||
assert!(m("ROOT/parent_dir/dir_deep_00", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_00/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_00/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_00/child_dir/file", false).is_ignore()
|
||||
);
|
||||
assert!(m("ROOT/parent_dir/dir_deep_00/child_dir/file", false).is_ignore());
|
||||
|
||||
// 01
|
||||
assert!(m("ROOT/parent_dir/dir_deep_01", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_01/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_01/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_01/child_dir/file", false).is_ignore()
|
||||
);
|
||||
assert!(m("ROOT/parent_dir/dir_deep_01/child_dir/file", false).is_ignore());
|
||||
|
||||
// 02
|
||||
assert!(m("ROOT/parent_dir/dir_deep_02", true).is_none());
|
||||
|
|
@ -252,67 +241,51 @@ fn test_dirs_in_deep() {
|
|||
assert!(m("ROOT/parent_dir/dir_deep_20", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_20/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_20/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_20/child_dir/file", false).is_ignore()
|
||||
);
|
||||
assert!(m("ROOT/parent_dir/dir_deep_20/child_dir/file", false).is_ignore());
|
||||
|
||||
// 21
|
||||
assert!(m("ROOT/parent_dir/dir_deep_21", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_21/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_21/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_21/child_dir/file", false).is_ignore()
|
||||
);
|
||||
assert!(m("ROOT/parent_dir/dir_deep_21/child_dir/file", false).is_ignore());
|
||||
|
||||
// 22
|
||||
// dir itself doesn't match
|
||||
assert!(m("ROOT/parent_dir/dir_deep_22", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_22/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_22/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_22/child_dir/file", false).is_ignore()
|
||||
);
|
||||
assert!(m("ROOT/parent_dir/dir_deep_22/child_dir/file", false).is_ignore());
|
||||
|
||||
// 23
|
||||
// dir itself doesn't match
|
||||
assert!(m("ROOT/parent_dir/dir_deep_23", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_23/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_23/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_23/child_dir/file", false).is_ignore()
|
||||
);
|
||||
assert!(m("ROOT/parent_dir/dir_deep_23/child_dir/file", false).is_ignore());
|
||||
|
||||
// 30
|
||||
assert!(m("ROOT/parent_dir/dir_deep_30", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_30/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_30/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_30/child_dir/file", false).is_ignore()
|
||||
);
|
||||
assert!(m("ROOT/parent_dir/dir_deep_30/child_dir/file", false).is_ignore());
|
||||
|
||||
// 31
|
||||
assert!(m("ROOT/parent_dir/dir_deep_31", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_31/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_31/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_31/child_dir/file", false).is_ignore()
|
||||
);
|
||||
assert!(m("ROOT/parent_dir/dir_deep_31/child_dir/file", false).is_ignore());
|
||||
|
||||
// 32
|
||||
// dir itself doesn't match
|
||||
assert!(m("ROOT/parent_dir/dir_deep_32", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_32/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_32/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_32/child_dir/file", false).is_ignore()
|
||||
);
|
||||
assert!(m("ROOT/parent_dir/dir_deep_32/child_dir/file", false).is_ignore());
|
||||
|
||||
// 33
|
||||
// dir itself doesn't match
|
||||
assert!(m("ROOT/parent_dir/dir_deep_33", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_33/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_33/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_33/child_dir/file", false).is_ignore()
|
||||
);
|
||||
assert!(m("ROOT/parent_dir/dir_deep_33/child_dir/file", false).is_ignore());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
ignore/this/path
|
||||
# This file begins with a BOM (U+FEFF)
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
use ignore::gitignore::GitignoreBuilder;
|
||||
|
||||
const IGNORE_FILE: &'static str = "tests/gitignore_skip_bom.gitignore";
|
||||
|
||||
/// Skip a Byte-Order Mark (BOM) at the beginning of the file, matching Git's
|
||||
/// behavior.
|
||||
///
|
||||
/// Ref: <https://github.com/BurntSushi/ripgrep/issues/2177>
|
||||
#[test]
|
||||
fn gitignore_skip_bom() {
|
||||
let mut builder = GitignoreBuilder::new("ROOT");
|
||||
let error = builder.add(IGNORE_FILE);
|
||||
assert!(error.is_none(), "failed to open gitignore file");
|
||||
let g = builder.build().unwrap();
|
||||
|
||||
assert!(g.matched("ignore/this/path", false).is_ignore());
|
||||
}
|
||||
1
crates/node/.gitignore
vendored
1
crates/node/.gitignore
vendored
|
|
@ -202,6 +202,5 @@ index.js
|
|||
browser.js
|
||||
tailwindcss-oxide.wasi-browser.js
|
||||
tailwindcss-oxide.wasi.cjs
|
||||
tailwindcss-oxide.wasi.d.cts
|
||||
wasi-worker-browser.mjs
|
||||
wasi-worker.mjs
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ crate-type = ["cdylib"]
|
|||
|
||||
[dependencies]
|
||||
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
|
||||
napi = { version = "3.11.0", default-features = false, features = ["napi4"] }
|
||||
napi-derive = "3.6.0"
|
||||
napi = { version = "3.3.0", default-features = false, features = ["napi4"] }
|
||||
napi-derive = "3.2.5"
|
||||
tailwindcss-oxide = { path = "../oxide" }
|
||||
rayon = "1.12.0"
|
||||
rayon = "1.10.0"
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = "2.3.2"
|
||||
napi-build = "2.2.3"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-android-arm-eabi",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.14",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,6 +22,6 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-android-arm64",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.14",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,6 +22,6 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-darwin-arm64",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.14",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,6 +22,6 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-darwin-x64",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.14",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,6 +22,6 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-freebsd-x64",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.14",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,6 +22,6 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-linux-arm-gnueabihf",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.14",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,6 +22,6 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-linux-arm64-gnu",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.14",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
},
|
||||
"libc": [
|
||||
"glibc"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-linux-arm64-musl",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.14",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
},
|
||||
"libc": [
|
||||
"musl"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-linux-x64-gnu",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.14",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
},
|
||||
"libc": [
|
||||
"glibc"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-linux-x64-musl",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.14",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
},
|
||||
"libc": [
|
||||
"musl"
|
||||
|
|
|
|||
1
crates/node/npm/wasm32-wasi/.npmrc
Normal file
1
crates/node/npm/wasm32-wasi/.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
node-linker=hoisted
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
# `@tailwindcss/oxide-wasm32-wasi`
|
||||
|
||||
This is the **wasm32-wasip1-threads** build of `@tailwindcss/oxide`
|
||||
This is the **wasm32-wasip1-threads** binary for `@tailwindcss/oxide`
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-wasm32-wasi",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.14",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"main": "tailwindcss-oxide.wasi.cjs",
|
||||
"files": [
|
||||
"tailwindcss-oxide.wasm32-wasi.wasm",
|
||||
|
|
@ -11,7 +14,7 @@
|
|||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=23.5.0"
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"provenance": true,
|
||||
|
|
@ -24,12 +27,12 @@
|
|||
},
|
||||
"browser": "tailwindcss-oxide.wasi-browser.js",
|
||||
"dependencies": {
|
||||
"@napi-rs/wasm-runtime": "^1.2.2",
|
||||
"@emnapi/core": "^1.11.3",
|
||||
"@emnapi/runtime": "^1.11.3",
|
||||
"@tybys/wasm-util": "^0.10.3",
|
||||
"@emnapi/wasi-threads": "^1.2.3",
|
||||
"tslib": "^2.8.1"
|
||||
"@napi-rs/wasm-runtime": "^1.0.5",
|
||||
"@emnapi/core": "^1.5.0",
|
||||
"@emnapi/runtime": "^1.5.0",
|
||||
"@tybys/wasm-util": "^0.10.1",
|
||||
"@emnapi/wasi-threads": "^1.1.0",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"bundledDependencies": [
|
||||
"@napi-rs/wasm-runtime",
|
||||
|
|
|
|||
75
crates/node/npm/wasm32-wasi/pnpm-lock.yaml
generated
Normal file
75
crates/node/npm/wasm32-wasi/pnpm-lock.yaml
generated
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@emnapi/core':
|
||||
specifier: ^1.5.0
|
||||
version: 1.5.0
|
||||
'@emnapi/runtime':
|
||||
specifier: ^1.5.0
|
||||
version: 1.5.0
|
||||
'@emnapi/wasi-threads':
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0
|
||||
'@napi-rs/wasm-runtime':
|
||||
specifier: ^1.0.5
|
||||
version: 1.0.5
|
||||
'@tybys/wasm-util':
|
||||
specifier: ^0.10.1
|
||||
version: 0.10.1
|
||||
tslib:
|
||||
specifier: ^2.4.0
|
||||
version: 2.8.1
|
||||
|
||||
packages:
|
||||
|
||||
'@emnapi/core@1.5.0':
|
||||
resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==}
|
||||
|
||||
'@emnapi/runtime@1.5.0':
|
||||
resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==}
|
||||
|
||||
'@emnapi/wasi-threads@1.1.0':
|
||||
resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
|
||||
|
||||
'@napi-rs/wasm-runtime@1.0.5':
|
||||
resolution: {integrity: sha512-TBr9Cf9onSAS2LQ2+QHx6XcC6h9+RIzJgbqG3++9TUZSH204AwEy5jg3BTQ0VATsyoGj4ee49tN/y6rvaOOtcg==}
|
||||
|
||||
'@tybys/wasm-util@0.10.1':
|
||||
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@emnapi/core@1.5.0':
|
||||
dependencies:
|
||||
'@emnapi/wasi-threads': 1.1.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@emnapi/runtime@1.5.0':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@emnapi/wasi-threads@1.1.0':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@napi-rs/wasm-runtime@1.0.5':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.5.0
|
||||
'@emnapi/runtime': 1.5.0
|
||||
'@tybys/wasm-util': 0.10.1
|
||||
|
||||
'@tybys/wasm-util@0.10.1':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-win32-arm64-msvc",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.14",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,6 +22,6 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-win32-x64-msvc",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.14",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -22,6 +22,6 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide",
|
||||
"version": "4.3.3",
|
||||
"version": "4.1.14",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
@ -32,19 +32,22 @@
|
|||
}
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tar": "^7.5.1",
|
||||
"detect-libc": "^2.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@emnapi/core": "1.11.3",
|
||||
"@emnapi/runtime": "1.11.3",
|
||||
"@napi-rs/cli": "3.7.4",
|
||||
"@napi-rs/wasm-runtime": "^1.2.2",
|
||||
"emnapi": "1.11.3"
|
||||
"@napi-rs/cli": "^3.3.0",
|
||||
"@napi-rs/wasm-runtime": "^1.0.5",
|
||||
"emnapi": "1.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
"node": ">= 10"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"index.d.ts"
|
||||
"index.d.ts",
|
||||
"scripts/install.js"
|
||||
],
|
||||
"publishConfig": {
|
||||
"provenance": true,
|
||||
|
|
@ -58,7 +61,8 @@
|
|||
"postbuild:wasm": "node ./scripts/move-artifacts.mjs",
|
||||
"dev": "cargo watch --quiet --shell 'npm run build'",
|
||||
"build:debug": "napi build --platform",
|
||||
"version": "napi version"
|
||||
"version": "napi version",
|
||||
"postinstall": "node ./scripts/install.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tailwindcss/oxide-android-arm64": "workspace:*",
|
||||
|
|
|
|||
143
crates/node/scripts/install.js
Normal file
143
crates/node/scripts/install.js
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* @tailwindcss/oxide postinstall script
|
||||
*
|
||||
* This script ensures that the correct binary for the current platform and
|
||||
* architecture is downloaded and available.
|
||||
*/
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const https = require('https')
|
||||
const { extract } = require('tar')
|
||||
const packageJson = require('../package.json')
|
||||
const detectLibc = require('detect-libc')
|
||||
|
||||
const version = packageJson.version
|
||||
|
||||
function getPlatformPackageName() {
|
||||
let platform = process.platform
|
||||
let arch = process.arch
|
||||
|
||||
let libc = ''
|
||||
if (platform === 'linux') {
|
||||
libc = detectLibc.isNonGlibcLinuxSync() ? 'musl' : 'gnu'
|
||||
}
|
||||
|
||||
// Map to our package naming conventions
|
||||
switch (platform) {
|
||||
case 'darwin':
|
||||
return arch === 'arm64' ? '@tailwindcss/oxide-darwin-arm64' : '@tailwindcss/oxide-darwin-x64'
|
||||
case 'win32':
|
||||
if (arch === 'arm64') return '@tailwindcss/oxide-win32-arm64-msvc'
|
||||
if (arch === 'ia32') return '@tailwindcss/oxide-win32-ia32-msvc'
|
||||
return '@tailwindcss/oxide-win32-x64-msvc'
|
||||
case 'linux':
|
||||
if (arch === 'x64') {
|
||||
return libc === 'musl'
|
||||
? '@tailwindcss/oxide-linux-x64-musl'
|
||||
: '@tailwindcss/oxide-linux-x64-gnu'
|
||||
} else if (arch === 'arm64') {
|
||||
return libc === 'musl'
|
||||
? '@tailwindcss/oxide-linux-arm64-musl'
|
||||
: '@tailwindcss/oxide-linux-arm64-gnu'
|
||||
} else if (arch === 'arm') {
|
||||
return '@tailwindcss/oxide-linux-arm-gnueabihf'
|
||||
}
|
||||
break
|
||||
case 'freebsd':
|
||||
return '@tailwindcss/oxide-freebsd-x64'
|
||||
case 'android':
|
||||
return '@tailwindcss/oxide-android-arm64'
|
||||
default:
|
||||
return '@tailwindcss/oxide-wasm32-wasi'
|
||||
}
|
||||
}
|
||||
|
||||
function isPackageAvailable(packageName) {
|
||||
try {
|
||||
require.resolve(packageName)
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Extract all files from a tarball to a destination directory
|
||||
async function extractTarball(tarballStream, destDir) {
|
||||
if (!fs.existsSync(destDir)) {
|
||||
fs.mkdirSync(destDir, { recursive: true })
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
tarballStream
|
||||
.pipe(extract({ cwd: destDir, strip: 1 }))
|
||||
.on('error', (err) => reject(err))
|
||||
.on('end', () => resolve())
|
||||
})
|
||||
}
|
||||
|
||||
async function downloadAndExtractBinary(packageName) {
|
||||
let tarballUrl = `https://registry.npmjs.org/${packageName}/-/${packageName.replace('@tailwindcss/', '')}-${version}.tgz`
|
||||
console.log(`Downloading ${tarballUrl}...`)
|
||||
|
||||
return new Promise((resolve) => {
|
||||
https
|
||||
.get(tarballUrl, (response) => {
|
||||
if (response.statusCode === 302 || response.statusCode === 301) {
|
||||
// Handle redirects
|
||||
https.get(response.headers.location, handleResponse).on('error', (err) => {
|
||||
console.error('Download error:', err)
|
||||
resolve()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
handleResponse(response)
|
||||
|
||||
async function handleResponse(response) {
|
||||
try {
|
||||
if (response.statusCode !== 200) {
|
||||
throw new Error(`Download failed with status code: ${response.statusCode}`)
|
||||
}
|
||||
|
||||
await extractTarball(
|
||||
response,
|
||||
path.join(__dirname, '..', 'node_modules', ...packageName.split('/')),
|
||||
)
|
||||
console.log(`Successfully downloaded and installed ${packageName}`)
|
||||
} catch (error) {
|
||||
console.error('Error during extraction:', error)
|
||||
resolve()
|
||||
} finally {
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
})
|
||||
.on('error', (err) => {
|
||||
console.error('Download error:', err)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Don't run this script in the package source
|
||||
try {
|
||||
if (fs.existsSync(path.join(__dirname, '..', 'build.rs'))) {
|
||||
return
|
||||
}
|
||||
|
||||
let packageName = getPlatformPackageName()
|
||||
if (!packageName) return
|
||||
if (isPackageAvailable(packageName)) return
|
||||
|
||||
await downloadAndExtractBinary(packageName)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
|
|
@ -165,11 +165,6 @@ impl Scanner {
|
|||
self.scanner.get_files()
|
||||
}
|
||||
|
||||
#[napi(getter)]
|
||||
pub fn scanned_files(&self) -> Vec<String> {
|
||||
self.scanner.get_scanned_files()
|
||||
}
|
||||
|
||||
#[napi(getter)]
|
||||
pub fn globs(&mut self) -> Vec<GlobEntry> {
|
||||
self
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ globwalk = "0.9.1"
|
|||
log = "0.4.22"
|
||||
rayon = "1.10.0"
|
||||
fxhash = { package = "rustc-hash", version = "2.1.1" }
|
||||
crossbeam = "0.8.4"
|
||||
tracing = { version = "0.1.40", features = [] }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
walkdir = "2.5.0"
|
||||
|
|
@ -20,7 +21,6 @@ ignore = { path = "../ignore" }
|
|||
regex = "1.11.1"
|
||||
|
||||
[dev-dependencies]
|
||||
insta = "1.48.0"
|
||||
tempfile = "3.13.0"
|
||||
pretty_assertions = "1.4.1"
|
||||
unicode-width = "0.2.0"
|
||||
|
|
|
|||
|
|
@ -1,49 +1,44 @@
|
|||
use std::{ascii::escape_default, fmt::Display};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[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> {
|
||||
#[inline(always)]
|
||||
pub fn new(input: &'a [u8]) -> Self {
|
||||
Self { input, pos: 0 }
|
||||
}
|
||||
|
||||
/// The current byte at `pos`, or 0x00 if past the end.
|
||||
#[inline(always)]
|
||||
pub fn curr(&self) -> u8 {
|
||||
if self.pos < self.input.len() {
|
||||
unsafe { *self.input.get_unchecked(self.pos) }
|
||||
} else {
|
||||
0x00
|
||||
}
|
||||
}
|
||||
|
||||
/// The next byte at `pos + 1`, or 0x00 if past the end.
|
||||
#[inline(always)]
|
||||
pub fn next(&self) -> u8 {
|
||||
let next_pos = self.pos + 1;
|
||||
if next_pos < self.input.len() {
|
||||
unsafe { *self.input.get_unchecked(next_pos) }
|
||||
} else {
|
||||
0x00
|
||||
}
|
||||
}
|
||||
|
||||
/// The previous byte at `pos - 1`, or 0x00 if at the start.
|
||||
#[inline(always)]
|
||||
pub fn prev(&self) -> u8 {
|
||||
if self.pos > 0 {
|
||||
unsafe { *self.input.get_unchecked(self.pos - 1) }
|
||||
} else {
|
||||
0x00
|
||||
}
|
||||
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 advance_by(&mut self, amount: usize) {
|
||||
|
|
@ -53,15 +48,38 @@ impl<'a> Cursor<'a> {
|
|||
#[inline(always)]
|
||||
pub fn advance(&mut self) {
|
||||
self.pos += 1;
|
||||
|
||||
self.prev = self.curr;
|
||||
self.curr = self.next;
|
||||
self.next = *self
|
||||
.input
|
||||
.get(self.pos.saturating_add(1))
|
||||
.unwrap_or(&0x00u8);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn advance_twice(&mut self) {
|
||||
self.pos += 2;
|
||||
|
||||
self.prev = self.next;
|
||||
self.curr = *self.input.get(self.pos).unwrap_or(&0x00u8);
|
||||
self.next = *self
|
||||
.input
|
||||
.get(self.pos.saturating_add(1))
|
||||
.unwrap_or(&0x00u8);
|
||||
}
|
||||
|
||||
pub fn move_to(&mut self, pos: usize) {
|
||||
self.pos = pos.min(self.input.len());
|
||||
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 = *self.input.get(pos.wrapping_sub(1)).unwrap_or(&0x00u8);
|
||||
self.curr = *self.input.get(pos).unwrap_or(&0x00u8);
|
||||
self.next = *self.input.get(pos.saturating_add(1)).unwrap_or(&0x00u8);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,9 +90,9 @@ impl Display for Cursor<'_> {
|
|||
let pos = format!("{: >len_count$}", self.pos, len_count = len.len());
|
||||
write!(f, "{}/{} ", pos, len)?;
|
||||
|
||||
if self.pos == 0 {
|
||||
if self.at_start {
|
||||
write!(f, "S ")?;
|
||||
} else if self.pos + 1 >= self.input.len() {
|
||||
} else if self.at_end {
|
||||
write!(f, "E ")?;
|
||||
} else {
|
||||
write!(f, "M ")?;
|
||||
|
|
@ -91,9 +109,9 @@ impl Display for Cursor<'_> {
|
|||
write!(
|
||||
f,
|
||||
"[{} {} {}]",
|
||||
to_str(self.prev()),
|
||||
to_str(self.curr()),
|
||||
to_str(self.next())
|
||||
to_str(self.prev),
|
||||
to_str(self.curr),
|
||||
to_str(self.next)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -107,28 +125,36 @@ mod test {
|
|||
fn test_cursor() {
|
||||
let mut cursor = Cursor::new(b"hello world");
|
||||
assert_eq!(cursor.pos, 0);
|
||||
assert_eq!(cursor.prev(), 0x00);
|
||||
assert_eq!(cursor.curr(), b'h');
|
||||
assert_eq!(cursor.next(), b'e');
|
||||
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_eq!(cursor.prev(), b'h');
|
||||
assert_eq!(cursor.curr(), b'e');
|
||||
assert_eq!(cursor.next(), b'l');
|
||||
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_eq!(cursor.prev(), b'd');
|
||||
assert_eq!(cursor.curr(), 0x00);
|
||||
assert_eq!(cursor.next(), 0x00);
|
||||
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_eq!(cursor.prev(), b'd');
|
||||
assert_eq!(cursor.curr(), 0x00);
|
||||
assert_eq!(cursor.next(), 0x00);
|
||||
assert!(!cursor.at_start);
|
||||
assert!(cursor.at_end);
|
||||
assert_eq!(cursor.prev, b'd');
|
||||
assert_eq!(cursor.curr, 0x00);
|
||||
assert_eq!(cursor.next, 0x00);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ impl Machine for ArbitraryPropertyMachine<IdleState> {
|
|||
|
||||
#[inline]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match cursor.curr().into() {
|
||||
match cursor.curr.into() {
|
||||
// Start of an arbitrary property
|
||||
Class::OpenBracket => {
|
||||
self.start_pos = cursor.pos;
|
||||
|
|
@ -97,8 +97,8 @@ impl Machine for ArbitraryPropertyMachine<ParsingPropertyState> {
|
|||
let len = cursor.input.len();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr().into() {
|
||||
Class::Dash => match cursor.next().into() {
|
||||
match cursor.curr.into() {
|
||||
Class::Dash => match cursor.next.into() {
|
||||
// Start of a CSS variable
|
||||
//
|
||||
// E.g.: `[--my-color:red]`
|
||||
|
|
@ -137,7 +137,7 @@ impl ArbitraryPropertyMachine<ParsingPropertyState> {
|
|||
fn parse_property_variable(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match self.css_variable_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
MachineState::Done(_) => match cursor.next().into() {
|
||||
MachineState::Done(_) => match cursor.next.into() {
|
||||
// End of the CSS variable, must be followed by a `:`
|
||||
//
|
||||
// E.g.: `[--my-color:red]`
|
||||
|
|
@ -165,8 +165,8 @@ impl Machine for ArbitraryPropertyMachine<ParsingValueState> {
|
|||
let len = cursor.input.len();
|
||||
let start_of_value_pos = cursor.pos;
|
||||
while cursor.pos < len {
|
||||
match cursor.curr().into() {
|
||||
Class::Escape => match cursor.next().into() {
|
||||
match cursor.curr.into() {
|
||||
Class::Escape => match cursor.next.into() {
|
||||
// An escaped whitespace character is not allowed
|
||||
//
|
||||
// E.g.: `[color:var(--my-\ color)]`
|
||||
|
|
@ -181,7 +181,7 @@ impl Machine for ArbitraryPropertyMachine<ParsingValueState> {
|
|||
},
|
||||
|
||||
Class::OpenParen | Class::OpenBracket | Class::OpenCurly => {
|
||||
if !self.bracket_stack.push(cursor.curr()) {
|
||||
if !self.bracket_stack.push(cursor.curr) {
|
||||
return self.restart();
|
||||
}
|
||||
cursor.advance();
|
||||
|
|
@ -190,7 +190,7 @@ impl Machine for ArbitraryPropertyMachine<ParsingValueState> {
|
|||
Class::CloseParen | Class::CloseBracket | Class::CloseCurly
|
||||
if !self.bracket_stack.is_empty() =>
|
||||
{
|
||||
if !self.bracket_stack.pop(cursor.curr()) {
|
||||
if !self.bracket_stack.pop(cursor.curr) {
|
||||
return self.restart();
|
||||
}
|
||||
cursor.advance();
|
||||
|
|
@ -227,16 +227,10 @@ impl Machine for ArbitraryPropertyMachine<ParsingValueState> {
|
|||
Class::Slash if start_of_value_pos == cursor.pos => return self.restart(),
|
||||
|
||||
// String interpolation-like syntax is not allowed. E.g.: `[${x}]`
|
||||
Class::Dollar if matches!(cursor.next().into(), Class::OpenCurly) => {
|
||||
Class::Dollar if matches!(cursor.next.into(), Class::OpenCurly) => {
|
||||
return self.restart()
|
||||
}
|
||||
|
||||
// An `!` at the top-level is invalid. We don't allow things to end with
|
||||
// `!important` either as we have dedicated syntax for this.
|
||||
Class::Exclamation if self.bracket_stack.is_empty() => {
|
||||
return self.restart();
|
||||
}
|
||||
|
||||
// Everything else is valid
|
||||
_ => cursor.advance(),
|
||||
};
|
||||
|
|
@ -299,9 +293,6 @@ enum Class {
|
|||
#[bytes(b'/')]
|
||||
Slash,
|
||||
|
||||
#[bytes(b'!')]
|
||||
Exclamation,
|
||||
|
||||
#[bytes(b' ', b'\t', b'\n', b'\r', b'\x0C')]
|
||||
Whitespace,
|
||||
|
||||
|
|
@ -378,9 +369,6 @@ mod tests {
|
|||
"[background:url(https://example.com?q={[{[([{[[2]]}])]}]})]",
|
||||
vec!["[background:url(https://example.com?q={[{[([{[[2]]}])]}]})]"],
|
||||
),
|
||||
// A property containing `!` at the top-level is invalid
|
||||
("[color:red!]", vec![]),
|
||||
("[color:red!important]", vec![]),
|
||||
] {
|
||||
for wrapper in [
|
||||
// No wrapper
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ impl Machine for ArbitraryValueMachine {
|
|||
#[inline]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
// An arbitrary value must start with an open bracket
|
||||
if Class::OpenBracket != cursor.curr().into() {
|
||||
if Class::OpenBracket != cursor.curr.into() {
|
||||
return MachineState::Idle;
|
||||
}
|
||||
|
||||
|
|
@ -42,8 +42,8 @@ impl Machine for ArbitraryValueMachine {
|
|||
let len = cursor.input.len();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr().into() {
|
||||
Class::Escape => match cursor.next().into() {
|
||||
match cursor.curr.into() {
|
||||
Class::Escape => match cursor.next.into() {
|
||||
// An escaped whitespace character is not allowed
|
||||
//
|
||||
// E.g.: `[color:var(--my-\ color)]`
|
||||
|
|
@ -61,7 +61,7 @@ impl Machine for ArbitraryValueMachine {
|
|||
},
|
||||
|
||||
Class::OpenParen | Class::OpenBracket | Class::OpenCurly => {
|
||||
if !self.bracket_stack.push(cursor.curr()) {
|
||||
if !self.bracket_stack.push(cursor.curr) {
|
||||
return self.restart();
|
||||
}
|
||||
cursor.advance();
|
||||
|
|
@ -70,7 +70,7 @@ impl Machine for ArbitraryValueMachine {
|
|||
Class::CloseParen | Class::CloseBracket | Class::CloseCurly
|
||||
if !self.bracket_stack.is_empty() =>
|
||||
{
|
||||
if !self.bracket_stack.pop(cursor.curr()) {
|
||||
if !self.bracket_stack.pop(cursor.curr) {
|
||||
return self.restart();
|
||||
}
|
||||
cursor.advance();
|
||||
|
|
@ -96,7 +96,7 @@ impl Machine for ArbitraryValueMachine {
|
|||
Class::Whitespace => return self.restart(),
|
||||
|
||||
// String interpolation-like syntax is not allowed. E.g.: `[${x}]`
|
||||
Class::Dollar if matches!(cursor.next().into(), Class::OpenCurly) => {
|
||||
Class::Dollar if matches!(cursor.next.into(), Class::OpenCurly) => {
|
||||
return self.restart()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,13 +80,13 @@ impl Machine for ArbitraryVariableMachine<IdleState> {
|
|||
|
||||
#[inline(always)]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match cursor.curr().into() {
|
||||
match cursor.curr.into() {
|
||||
// Arbitrary variables start with `(` followed by a CSS variable
|
||||
//
|
||||
// E.g.: `(--my-variable)`
|
||||
// ^^
|
||||
//
|
||||
Class::OpenParen => match cursor.next().into() {
|
||||
Class::OpenParen => match cursor.next.into() {
|
||||
Class::Dash => {
|
||||
self.start_pos = cursor.pos;
|
||||
cursor.advance();
|
||||
|
|
@ -117,7 +117,7 @@ impl Machine for ArbitraryVariableMachine<ParsingState> {
|
|||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match self.css_variable_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
MachineState::Done(_) => match cursor.next().into() {
|
||||
MachineState::Done(_) => match cursor.next.into() {
|
||||
// A CSS variable followed by a `,` means that there is a fallback
|
||||
//
|
||||
// E.g.: `(--my-color,red)`
|
||||
|
|
@ -134,7 +134,7 @@ impl Machine for ArbitraryVariableMachine<ParsingState> {
|
|||
_ => {
|
||||
cursor.advance();
|
||||
|
||||
match cursor.curr().into() {
|
||||
match cursor.curr.into() {
|
||||
// End of an arbitrary variable, must be followed by `)`
|
||||
Class::CloseParen => self.done(self.start_pos, cursor),
|
||||
|
||||
|
|
@ -156,7 +156,7 @@ impl Machine for ArbitraryVariableMachine<ParsingDataTypeState> {
|
|||
let len = cursor.input.len();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr().into() {
|
||||
match cursor.curr.into() {
|
||||
// Valid data type characters
|
||||
//
|
||||
// E.g.: `(length:--my-length)`
|
||||
|
|
@ -169,7 +169,7 @@ impl Machine for ArbitraryVariableMachine<ParsingDataTypeState> {
|
|||
//
|
||||
// E.g.: `(length:--my-length)`
|
||||
// ^
|
||||
Class::Colon => match cursor.next().into() {
|
||||
Class::Colon => match cursor.next.into() {
|
||||
Class::Dash => {
|
||||
cursor.advance();
|
||||
return self.transition::<ParsingState>().next(cursor);
|
||||
|
|
@ -196,8 +196,8 @@ impl Machine for ArbitraryVariableMachine<ParsingFallbackState> {
|
|||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
let len = cursor.input.len();
|
||||
while cursor.pos < len {
|
||||
match cursor.curr().into() {
|
||||
Class::Escape => match cursor.next().into() {
|
||||
match cursor.curr.into() {
|
||||
Class::Escape => match cursor.next.into() {
|
||||
// An escaped whitespace character is not allowed
|
||||
//
|
||||
// E.g.: `(--my-\ color)`
|
||||
|
|
@ -212,7 +212,7 @@ impl Machine for ArbitraryVariableMachine<ParsingFallbackState> {
|
|||
},
|
||||
|
||||
Class::OpenParen | Class::OpenBracket | Class::OpenCurly => {
|
||||
if !self.bracket_stack.push(cursor.curr()) {
|
||||
if !self.bracket_stack.push(cursor.curr) {
|
||||
return self.restart();
|
||||
}
|
||||
cursor.advance();
|
||||
|
|
@ -221,7 +221,7 @@ impl Machine for ArbitraryVariableMachine<ParsingFallbackState> {
|
|||
Class::CloseParen | Class::CloseBracket | Class::CloseCurly
|
||||
if !self.bracket_stack.is_empty() =>
|
||||
{
|
||||
if !self.bracket_stack.pop(cursor.curr()) {
|
||||
if !self.bracket_stack.pop(cursor.curr) {
|
||||
return self.restart();
|
||||
}
|
||||
cursor.advance();
|
||||
|
|
@ -253,7 +253,7 @@ impl Machine for ArbitraryVariableMachine<ParsingFallbackState> {
|
|||
Class::Whitespace => return self.restart(),
|
||||
|
||||
// String interpolation-like syntax is not allowed. E.g.: `[${x}]`
|
||||
Class::Dollar if matches!(cursor.next().into(), Class::OpenCurly) => {
|
||||
Class::Dollar if matches!(cursor.next.into(), Class::OpenCurly) => {
|
||||
return self.restart()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ impl BracketStack {
|
|||
b'(' => b')',
|
||||
b'[' => b']',
|
||||
b'{' => b'}',
|
||||
b'<' => b'>',
|
||||
_ => std::hint::unreachable_unchecked(),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,14 +32,14 @@ impl Machine for CandidateMachine {
|
|||
while cursor.pos < len {
|
||||
// Skip ahead for known characters that will never be part of a candidate. No need to
|
||||
// run any sub-machines.
|
||||
if cursor.curr().is_ascii_whitespace() {
|
||||
if cursor.curr.is_ascii_whitespace() {
|
||||
self.reset();
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Candidates don't start with these characters, so we can skip ahead.
|
||||
if matches!(cursor.curr(), b':' | b'"' | b'\'' | b'`') {
|
||||
if matches!(cursor.curr, b':' | b'"' | b'\'' | b'`') {
|
||||
self.reset();
|
||||
cursor.advance();
|
||||
continue;
|
||||
|
|
@ -56,7 +56,7 @@ impl Machine for CandidateMachine {
|
|||
// E.g.: `Some Class`
|
||||
// ^ ^ Invalid, we can jump ahead to the next boundary
|
||||
//
|
||||
if matches!(cursor.curr(), b'<' | b'A'..=b'Z') {
|
||||
if matches!(cursor.curr, b'<' | b'A'..=b'Z') {
|
||||
if let Some(offset) = cursor.input[cursor.pos..]
|
||||
.iter()
|
||||
.position(|&c| is_valid_before_boundary(&c))
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ impl Machine for CssVariableMachine {
|
|||
#[inline]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
// CSS Variables must start with `--`
|
||||
if Class::Dash != cursor.curr().into() || Class::Dash != cursor.next().into() {
|
||||
if Class::Dash != cursor.curr.into() || Class::Dash != cursor.next.into() {
|
||||
return MachineState::Idle;
|
||||
}
|
||||
|
||||
|
|
@ -30,11 +30,11 @@ impl Machine for CssVariableMachine {
|
|||
cursor.advance_twice();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr().into() {
|
||||
match cursor.curr.into() {
|
||||
// https://drafts.csswg.org/css-syntax-3/#ident-token-diagram
|
||||
//
|
||||
Class::AllowedCharacter | Class::Dash => {
|
||||
match cursor.next().into() {
|
||||
match cursor.next.into() {
|
||||
// Valid character followed by a valid character or an escape character
|
||||
//
|
||||
// E.g.: `--my-variable`
|
||||
|
|
@ -58,7 +58,7 @@ impl Machine for CssVariableMachine {
|
|||
}
|
||||
}
|
||||
|
||||
Class::Escape => match cursor.next().into() {
|
||||
Class::Escape => match cursor.next.into() {
|
||||
// An escaped whitespace character is not allowed
|
||||
//
|
||||
// In CSS it is allowed, but in the context of a class it's not because then we
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ impl<'a> Extractor<'a> {
|
|||
{
|
||||
let cursor = &mut self.cursor.clone();
|
||||
while cursor.pos < len {
|
||||
if cursor.curr().is_ascii_whitespace() {
|
||||
if cursor.curr.is_ascii_whitespace() {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
|
@ -104,7 +104,7 @@ impl<'a> Extractor<'a> {
|
|||
let cursor = &mut self.cursor.clone();
|
||||
|
||||
while cursor.pos < len {
|
||||
if cursor.curr().is_ascii_whitespace() {
|
||||
if cursor.curr.is_ascii_whitespace() {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
|
@ -147,7 +147,7 @@ impl<'a> Extractor<'a> {
|
|||
|
||||
let cursor = &mut self.cursor.clone();
|
||||
while cursor.pos < len {
|
||||
if cursor.curr().is_ascii_whitespace() {
|
||||
if cursor.curr.is_ascii_whitespace() {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
|
@ -238,7 +238,7 @@ mod tests {
|
|||
use std::hint::black_box;
|
||||
|
||||
fn pre_process_input(input: &str, extension: &str) -> String {
|
||||
let input = crate::scanner::pre_process_input(input.as_bytes().to_vec(), extension);
|
||||
let input = crate::scanner::pre_process_input(input.as_bytes(), extension);
|
||||
String::from_utf8(input).unwrap()
|
||||
}
|
||||
|
||||
|
|
@ -908,18 +908,6 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20233
|
||||
#[test]
|
||||
fn test_template_toolkit_syntax() {
|
||||
assert_extract_candidates_contains(
|
||||
&pre_process_input(
|
||||
r#"<div class="[% IF $is_open %]bg-white/40[% ELSE %]bg-white/10[% END %]"></div>"#,
|
||||
"tx",
|
||||
),
|
||||
vec!["bg-white/40", "bg-white/10"],
|
||||
);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/17050
|
||||
#[test]
|
||||
fn test_haml_syntax() {
|
||||
|
|
|
|||
|
|
@ -31,14 +31,14 @@ impl Machine for ModifierMachine {
|
|||
#[inline]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
// A modifier must start with a `/`, everything else is not a valid start of a modifier
|
||||
if Class::Slash != cursor.curr().into() {
|
||||
if Class::Slash != cursor.curr.into() {
|
||||
return MachineState::Idle;
|
||||
}
|
||||
|
||||
let start_pos = cursor.pos;
|
||||
cursor.advance();
|
||||
|
||||
match cursor.curr().into() {
|
||||
match cursor.curr.into() {
|
||||
// Start of an arbitrary value:
|
||||
//
|
||||
// ```
|
||||
|
|
@ -70,9 +70,9 @@ impl Machine for ModifierMachine {
|
|||
Class::ValidStart => {
|
||||
let len = cursor.input.len();
|
||||
while cursor.pos < len {
|
||||
match cursor.curr().into() {
|
||||
match cursor.curr.into() {
|
||||
Class::ValidStart | Class::ValidInside => {
|
||||
match cursor.next().into() {
|
||||
match cursor.next.into() {
|
||||
// Only valid characters are allowed, if followed by another valid character
|
||||
Class::ValidStart | Class::ValidInside => cursor.advance(),
|
||||
|
||||
|
|
|
|||
|
|
@ -52,8 +52,8 @@ impl Machine for NamedUtilityMachine<IdleState> {
|
|||
|
||||
#[inline]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match cursor.curr().into() {
|
||||
Class::AlphaLower => match cursor.next().into() {
|
||||
match cursor.curr.into() {
|
||||
Class::AlphaLower => match cursor.next.into() {
|
||||
// Valid single character utility in between quotes
|
||||
//
|
||||
// E.g.: `<div class="a"></div>`
|
||||
|
|
@ -90,7 +90,7 @@ impl Machine for NamedUtilityMachine<IdleState> {
|
|||
//
|
||||
// E.g.: `-mx-2.5`
|
||||
// ^^
|
||||
Class::Dash => match cursor.next().into() {
|
||||
Class::Dash => match cursor.next.into() {
|
||||
Class::AlphaLower => {
|
||||
self.start_pos = cursor.pos;
|
||||
cursor.advance();
|
||||
|
|
@ -118,7 +118,7 @@ impl Machine for NamedUtilityMachine<ParsingState> {
|
|||
let len = cursor.input.len();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr().into() {
|
||||
match cursor.curr.into() {
|
||||
// Followed by a boundary character, we are at the end of the utility.
|
||||
//
|
||||
// E.g.: `'flex'`
|
||||
|
|
@ -132,14 +132,14 @@ impl Machine for NamedUtilityMachine<ParsingState> {
|
|||
// E.g.: `:div="{ flex: true }"` (JavaScript object syntax)
|
||||
// ^
|
||||
Class::AlphaLower | Class::AlphaUpper => {
|
||||
if is_valid_after_boundary(&cursor.next()) || {
|
||||
if is_valid_after_boundary(&cursor.next) || {
|
||||
// Or any of these characters
|
||||
//
|
||||
// - `:`, because of JS object keys
|
||||
// - `/`, because of modifiers
|
||||
// - `!`, because of important
|
||||
matches!(
|
||||
cursor.next().into(),
|
||||
cursor.next.into(),
|
||||
Class::Colon | Class::Slash | Class::Exclamation
|
||||
)
|
||||
} {
|
||||
|
|
@ -150,7 +150,7 @@ impl Machine for NamedUtilityMachine<ParsingState> {
|
|||
cursor.advance()
|
||||
}
|
||||
|
||||
Class::Dash => match cursor.next().into() {
|
||||
Class::Dash => match cursor.next.into() {
|
||||
// Start of an arbitrary value
|
||||
//
|
||||
// E.g.: `bg-[#0088cc]`
|
||||
|
|
@ -196,7 +196,7 @@ impl Machine for NamedUtilityMachine<ParsingState> {
|
|||
_ => return self.restart(),
|
||||
},
|
||||
|
||||
Class::Underscore => match cursor.next().into() {
|
||||
Class::Underscore => match cursor.next.into() {
|
||||
// Valid characters _if_ followed by another valid character. These characters are
|
||||
// only valid inside of the utility but not at the end of the utility.
|
||||
//
|
||||
|
|
@ -225,14 +225,14 @@ impl Machine for NamedUtilityMachine<ParsingState> {
|
|||
// ^
|
||||
// E.g.: `:div="{ flex: true }"` (JavaScript object syntax)
|
||||
// ^
|
||||
_ if is_valid_after_boundary(&cursor.next()) || {
|
||||
_ if is_valid_after_boundary(&cursor.next) || {
|
||||
// Or any of these characters
|
||||
//
|
||||
// - `:`, because of JS object keys
|
||||
// - `/`, because of modifiers
|
||||
// - `!`, because of important
|
||||
matches!(
|
||||
cursor.next().into(),
|
||||
cursor.next.into(),
|
||||
Class::Colon | Class::Slash | Class::Exclamation
|
||||
)
|
||||
} =>
|
||||
|
|
@ -249,11 +249,11 @@ impl Machine for NamedUtilityMachine<ParsingState> {
|
|||
// E.g.: `px-2.5`
|
||||
// ^^^
|
||||
Class::Dot => {
|
||||
if !matches!(cursor.prev().into(), Class::Number) {
|
||||
if !matches!(cursor.prev.into(), Class::Number) {
|
||||
return self.restart();
|
||||
}
|
||||
|
||||
if !matches!(cursor.next().into(), Class::Number) {
|
||||
if !matches!(cursor.next.into(), Class::Number) {
|
||||
return self.restart();
|
||||
}
|
||||
|
||||
|
|
@ -278,19 +278,18 @@ impl Machine for NamedUtilityMachine<ParsingState> {
|
|||
//
|
||||
Class::Number => {
|
||||
if !matches!(
|
||||
cursor.prev().into(),
|
||||
cursor.prev.into(),
|
||||
Class::Dash
|
||||
| Class::Underscore
|
||||
| Class::Dot
|
||||
| Class::Number
|
||||
| Class::AlphaLower
|
||||
| Class::AlphaUpper
|
||||
) {
|
||||
return self.restart();
|
||||
}
|
||||
|
||||
if !matches!(
|
||||
cursor.next().into(),
|
||||
cursor.next.into(),
|
||||
Class::Dot
|
||||
| Class::Number
|
||||
| Class::AlphaLower
|
||||
|
|
@ -314,7 +313,7 @@ impl Machine for NamedUtilityMachine<ParsingState> {
|
|||
// ^^
|
||||
// ```
|
||||
Class::Percent => {
|
||||
if !matches!(cursor.prev().into(), Class::Number) {
|
||||
if !matches!(cursor.prev.into(), Class::Number) {
|
||||
return self.restart();
|
||||
}
|
||||
|
||||
|
|
@ -426,8 +425,6 @@ mod tests {
|
|||
// With number followed by dash or underscore
|
||||
("text-title1-strong", vec!["text-title1-strong"]),
|
||||
("text-title1_strong", vec!["text-title1_strong"]),
|
||||
// With capital letter followed by number
|
||||
("text-titleV1-strong", vec!["text-titleV1-strong"]),
|
||||
// With trailing % sign
|
||||
("from-15%", vec!["from-15%"]),
|
||||
// Arbitrary value with bracket notation
|
||||
|
|
|
|||
|
|
@ -81,8 +81,8 @@ impl Machine for NamedVariantMachine<IdleState> {
|
|||
|
||||
#[inline(always)]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match cursor.curr().into() {
|
||||
Class::AlphaLower | Class::Star => match cursor.next().into() {
|
||||
match cursor.curr.into() {
|
||||
Class::AlphaLower | Class::Star => match cursor.next.into() {
|
||||
// Valid single character variant, must be followed by a `:`
|
||||
//
|
||||
// E.g.: `<div class="x:flex"></div>`
|
||||
|
|
@ -134,8 +134,8 @@ impl Machine for NamedVariantMachine<ParsingState> {
|
|||
let len = cursor.input.len();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr().into() {
|
||||
Class::Dash => match cursor.next().into() {
|
||||
match cursor.curr.into() {
|
||||
Class::Dash => match cursor.next.into() {
|
||||
// Start of an arbitrary value
|
||||
//
|
||||
// E.g.: `data-[state=pending]:`.
|
||||
|
|
@ -192,7 +192,7 @@ impl Machine for NamedVariantMachine<ParsingState> {
|
|||
};
|
||||
}
|
||||
|
||||
Class::Underscore => match cursor.next().into() {
|
||||
Class::Underscore => match cursor.next.into() {
|
||||
// Valid characters _if_ followed by another valid character. These characters are
|
||||
// only valid inside of the variant but not at the end of the variant.
|
||||
//
|
||||
|
|
@ -240,11 +240,11 @@ impl Machine for NamedVariantMachine<ParsingState> {
|
|||
// E.g.: `2.5xl:flex`
|
||||
// ^^^
|
||||
Class::Dot => {
|
||||
if !matches!(cursor.prev().into(), Class::Number) {
|
||||
if !matches!(cursor.prev.into(), Class::Number) {
|
||||
return self.restart();
|
||||
}
|
||||
|
||||
if !matches!(cursor.next().into(), Class::Number) {
|
||||
if !matches!(cursor.next.into(), Class::Number) {
|
||||
return self.restart();
|
||||
}
|
||||
|
||||
|
|
@ -263,7 +263,7 @@ impl Machine for NamedVariantMachine<ParsingState> {
|
|||
impl NamedVariantMachine<ParsingState> {
|
||||
#[inline(always)]
|
||||
fn parse_arbitrary_end(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match cursor.next().into() {
|
||||
match cursor.next.into() {
|
||||
Class::Slash => {
|
||||
cursor.advance();
|
||||
self.transition::<ParsingModifierState>().next(cursor)
|
||||
|
|
@ -285,7 +285,7 @@ impl Machine for NamedVariantMachine<ParsingModifierState> {
|
|||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match self.modifier_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
MachineState::Done(_) => match cursor.next().into() {
|
||||
MachineState::Done(_) => match cursor.next.into() {
|
||||
// Modifier must be followed by a `:`
|
||||
//
|
||||
// E.g.: `group-hover/name:`
|
||||
|
|
@ -308,7 +308,7 @@ impl Machine for NamedVariantMachine<ParsingEndState> {
|
|||
|
||||
#[inline(always)]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match cursor.curr().into() {
|
||||
match cursor.curr.into() {
|
||||
// The end of a variant must be the `:`
|
||||
//
|
||||
// E.g.: `hover:`
|
||||
|
|
|
|||
|
|
@ -32,14 +32,14 @@ impl PreProcessor for Clojure {
|
|||
let mut cursor = cursor::Cursor::new(&content);
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// Consume strings as-is
|
||||
b'"' => {
|
||||
result[cursor.pos] = b' ';
|
||||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// Escaped character, skip ahead to the next character
|
||||
b'\\' => cursor.advance_twice(),
|
||||
|
||||
|
|
@ -57,8 +57,8 @@ impl PreProcessor for Clojure {
|
|||
|
||||
// Discard line comments until the end of the line.
|
||||
// Comments start with `;;`
|
||||
b';' if matches!(cursor.next(), b';') => {
|
||||
while cursor.pos < len && cursor.curr() != b'\n' {
|
||||
b';' if matches!(cursor.next, b';') => {
|
||||
while cursor.pos < len && cursor.curr != b'\n' {
|
||||
result[cursor.pos] = b' ';
|
||||
cursor.advance();
|
||||
}
|
||||
|
|
@ -70,7 +70,7 @@ impl PreProcessor for Clojure {
|
|||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// A `.` surrounded by digits is a decimal number, so we don't want to replace it.
|
||||
//
|
||||
// E.g.:
|
||||
|
|
@ -78,8 +78,8 @@ impl PreProcessor for Clojure {
|
|||
// gap-1.5
|
||||
// ^
|
||||
// ```
|
||||
b'.' if cursor.prev().is_ascii_digit()
|
||||
&& cursor.next().is_ascii_digit() =>
|
||||
b'.' if cursor.prev.is_ascii_digit()
|
||||
&& cursor.next.is_ascii_digit() =>
|
||||
{
|
||||
// Keep the `.` as-is
|
||||
}
|
||||
|
|
@ -95,7 +95,7 @@ impl PreProcessor for Clojure {
|
|||
result[cursor.pos] = b' ';
|
||||
}
|
||||
// End of keyword.
|
||||
_ if !is_keyword_character(cursor.curr()) => {
|
||||
_ if !is_keyword_character(cursor.curr) => {
|
||||
result[cursor.pos] = b' ';
|
||||
break;
|
||||
}
|
||||
|
|
@ -110,11 +110,11 @@ impl PreProcessor for Clojure {
|
|||
|
||||
// Handle quote with a list, e.g.: `'(…)`
|
||||
// and with a vector, e.g.: `'[…]`
|
||||
b'\'' if matches!(cursor.next(), b'[' | b'(') => {
|
||||
b'\'' if matches!(cursor.next, b'[' | b'(') => {
|
||||
result[cursor.pos] = b' ';
|
||||
cursor.advance();
|
||||
result[cursor.pos] = b' ';
|
||||
let end = match cursor.curr() {
|
||||
let end = match cursor.curr {
|
||||
b'[' => b']',
|
||||
b'(' => b')',
|
||||
_ => unreachable!(),
|
||||
|
|
@ -122,7 +122,7 @@ impl PreProcessor for Clojure {
|
|||
|
||||
// Consume until the closing `]`
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
x if x == end => {
|
||||
result[cursor.pos] = b' ';
|
||||
break;
|
||||
|
|
@ -134,7 +134,7 @@ impl PreProcessor for Clojure {
|
|||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// Escaped character, skip ahead to the next character
|
||||
b'\\' => cursor.advance_twice(),
|
||||
|
||||
|
|
@ -157,14 +157,14 @@ impl PreProcessor for Clojure {
|
|||
}
|
||||
|
||||
// Handle quote with a keyword, e.g.: `'bg-white`
|
||||
b'\'' if !cursor.next().is_ascii_whitespace() => {
|
||||
b'\'' if !cursor.next.is_ascii_whitespace() => {
|
||||
result[cursor.pos] = b' ';
|
||||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// End of keyword.
|
||||
_ if !is_keyword_character(cursor.curr()) => {
|
||||
_ if !is_keyword_character(cursor.curr) => {
|
||||
result[cursor.pos] = b' ';
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,13 +13,13 @@ impl PreProcessor for Elixir {
|
|||
|
||||
while cursor.pos < content.len() {
|
||||
// Look for a sigil marker
|
||||
if cursor.curr() != b'~' {
|
||||
if cursor.curr != b'~' {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Scan charlists, strings, and wordlists
|
||||
if !matches!(cursor.next(), b'c' | b'C' | b's' | b'S' | b'w' | b'W') {
|
||||
if !matches!(cursor.next, b'c' | b'C' | b's' | b'S' | b'w' | b'W') {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ impl PreProcessor for Elixir {
|
|||
cursor.advance_twice();
|
||||
|
||||
// Match the opening for a sigil
|
||||
if !matches!(cursor.curr(), b'(' | b'[' | b'{') {
|
||||
if !matches!(cursor.curr, b'(' | b'[' | b'{') {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -35,19 +35,19 @@ impl PreProcessor for Elixir {
|
|||
result[cursor.pos] = b' ';
|
||||
|
||||
// Scan until we find a balanced closing one and replace it too
|
||||
bracket_stack.push(cursor.curr());
|
||||
bracket_stack.push(cursor.curr);
|
||||
|
||||
while cursor.pos < content.len() {
|
||||
cursor.advance();
|
||||
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// Escaped character, skip ahead to the next character
|
||||
b'\\' => cursor.advance_twice(),
|
||||
b'(' | b'[' | b'{' => {
|
||||
bracket_stack.push(cursor.curr());
|
||||
bracket_stack.push(cursor.curr);
|
||||
}
|
||||
b')' | b']' | b'}' if !bracket_stack.is_empty() => {
|
||||
bracket_stack.pop(cursor.curr());
|
||||
bracket_stack.pop(cursor.curr);
|
||||
|
||||
if bracket_stack.is_empty() {
|
||||
// Replace the closing bracket with a space
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ impl PreProcessor for Haml {
|
|||
let mut last_newline_position = 0;
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// Escape the next character
|
||||
b'\\' => {
|
||||
cursor.advance_twice();
|
||||
|
|
@ -119,7 +119,7 @@ impl PreProcessor for Haml {
|
|||
b'-' if cursor.input[last_newline_position..cursor.pos]
|
||||
.iter()
|
||||
.all(u8::is_ascii_whitespace)
|
||||
&& matches!(cursor.next(), b'#') =>
|
||||
&& matches!(cursor.next, b'#') =>
|
||||
{
|
||||
// Just consume the comment
|
||||
let updated_last_newline_position =
|
||||
|
|
@ -159,7 +159,7 @@ impl PreProcessor for Haml {
|
|||
// Override the last known newline position
|
||||
last_newline_position = end;
|
||||
|
||||
let replaced = pre_process_input(ruby_code.to_vec(), "rb");
|
||||
let replaced = pre_process_input(ruby_code, "rb");
|
||||
result.replace_range(start..end, replaced);
|
||||
}
|
||||
|
||||
|
|
@ -190,7 +190,7 @@ impl PreProcessor for Haml {
|
|||
// digit.
|
||||
// E.g.: `bg-red-500.2xl:flex`
|
||||
// ^^^
|
||||
if cursor.prev().is_ascii_digit() && cursor.next().is_ascii_digit() {
|
||||
if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() {
|
||||
let mut next_cursor = cursor.clone();
|
||||
next_cursor.advance();
|
||||
|
||||
|
|
@ -203,81 +203,6 @@ impl PreProcessor for Haml {
|
|||
}
|
||||
}
|
||||
|
||||
// Handle Ruby syntax with `%w[]` arrays embedded in Haml attribute hashes. E.g.:
|
||||
//
|
||||
// ```haml
|
||||
// %div{class: %w[bg-blue-500 w-10 h-10]}
|
||||
// ```
|
||||
//
|
||||
// A `%` that follows a value is not a percent literal. E.g.: the `50%w` in
|
||||
// `hit rate 50%w.`
|
||||
b'%' if matches!(cursor.next(), b'w' | b'W')
|
||||
&& !cursor.prev().is_ascii_alphanumeric()
|
||||
&& !matches!(cursor.prev(), b'_' | b')' | b']' | b'}') =>
|
||||
{
|
||||
// Boundary characters
|
||||
let (open, close) = match cursor.input.get(cursor.pos + 2) {
|
||||
Some(b'[') => (b'[', b']'),
|
||||
Some(b'(') => (b'(', b')'),
|
||||
Some(b'{') => (b'{', b'}'),
|
||||
Some(b'<') => (b'<', b'>'),
|
||||
|
||||
// Any other ASCII punctuation can be used as a custom delimiter
|
||||
Some(&c) if c.is_ascii_punctuation() => (c, c),
|
||||
|
||||
// Everything else is not a valid delimiter
|
||||
_ => {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
result[cursor.pos] = b' '; // Replace `%`
|
||||
cursor.advance();
|
||||
result[cursor.pos] = b' '; // Replace `w`
|
||||
cursor.advance();
|
||||
result[cursor.pos] = b' '; // Replace the opening delimiter
|
||||
cursor.advance();
|
||||
|
||||
// Paired delimiters can be nested as long as they are balanced. E.g.:
|
||||
// `%w[foo[bar]baz]` produces a flat array.
|
||||
let mut depth = 1_usize;
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// Skip escaped characters, unless the backslash is the delimiter
|
||||
// itself
|
||||
b'\\' if close != b'\\' => {
|
||||
// Use backslash to embed spaces in the strings.
|
||||
if cursor.next() == b' ' {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
// Start of a nested delimiter pair
|
||||
c if c == open && open != close => depth += 1,
|
||||
|
||||
// Closing delimiter
|
||||
c if c == close => {
|
||||
depth -= 1;
|
||||
|
||||
// End of the literal, replace the closing delimiter with a space
|
||||
if depth == 0 {
|
||||
result[cursor.pos] = b' ';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Everything else is valid content
|
||||
_ => {}
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
}
|
||||
|
||||
// Replace following characters with spaces if they are not inside of brackets
|
||||
b'#' | b'=' if bracket_stack.is_empty() => {
|
||||
result[cursor.pos] = b' ';
|
||||
|
|
@ -288,11 +213,11 @@ impl PreProcessor for Haml {
|
|||
if bracket_stack.is_empty() {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
bracket_stack.push(cursor.curr());
|
||||
bracket_stack.push(cursor.curr);
|
||||
}
|
||||
|
||||
b')' | b']' | b'}' if !bracket_stack.is_empty() => {
|
||||
bracket_stack.pop(cursor.curr());
|
||||
bracket_stack.pop(cursor.curr);
|
||||
|
||||
// Replace closing bracket with a space
|
||||
if bracket_stack.is_empty() {
|
||||
|
|
@ -332,7 +257,7 @@ impl Haml {
|
|||
// :url => { :action => "add", :id => product.id },
|
||||
// :update => { :success => "cart", :failure => "error" }
|
||||
// ```
|
||||
let evaluation_type = cursor.curr();
|
||||
let evaluation_type = cursor.curr;
|
||||
|
||||
let block_indentation_level = cursor
|
||||
.pos
|
||||
|
|
@ -342,17 +267,17 @@ impl Haml {
|
|||
let mut last_newline_position = last_known_newline_position;
|
||||
|
||||
// Consume until the end of the line first
|
||||
while cursor.pos < len && cursor.curr() != b'\n' {
|
||||
while cursor.pos < len && cursor.curr != b'\n' {
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
// Block is already done, aka just a line
|
||||
if evaluation_type == b'=' && cursor.prev() != b',' {
|
||||
if evaluation_type == b'=' && cursor.prev != b',' {
|
||||
return cursor.pos;
|
||||
}
|
||||
|
||||
'outer: while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// Escape the next character
|
||||
b'\\' => {
|
||||
cursor.advance_twice();
|
||||
|
|
@ -364,7 +289,7 @@ impl Haml {
|
|||
last_newline_position = cursor.pos;
|
||||
|
||||
// We are done with this block
|
||||
if evaluation_type == b'=' && cursor.prev() != b',' {
|
||||
if evaluation_type == b'=' && cursor.prev != b',' {
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -375,11 +300,11 @@ impl Haml {
|
|||
// Skip whitespace and compute the indentation level
|
||||
x if x.is_ascii_whitespace() => {
|
||||
// Find first non-whitespace character
|
||||
while cursor.pos < len && cursor.curr().is_ascii_whitespace() {
|
||||
if cursor.curr() == b'\n' {
|
||||
while cursor.pos < len && cursor.curr.is_ascii_whitespace() {
|
||||
if cursor.curr == b'\n' {
|
||||
last_newline_position = cursor.pos;
|
||||
|
||||
if evaluation_type == b'=' && cursor.prev() != b',' {
|
||||
if evaluation_type == b'=' && cursor.prev != b',' {
|
||||
// We are done with this block
|
||||
break 'outer;
|
||||
}
|
||||
|
|
@ -499,81 +424,6 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20386
|
||||
#[test]
|
||||
fn test_embedded_ruby_percent_w_delimiters() {
|
||||
for (input, expected) in [
|
||||
// %w[…] in an attribute hash
|
||||
(
|
||||
"%div{class: %w[flex px-2.5]}",
|
||||
"%div class: flex px-2.5 ",
|
||||
),
|
||||
// %w<…>
|
||||
(
|
||||
"%div{class: %w<flex px-2.5>}",
|
||||
"%div class: flex px-2.5 ",
|
||||
),
|
||||
// Nested `<…>` does not end the literal
|
||||
(
|
||||
"%div{class: %w<flex <nested> px-2.5>}",
|
||||
"%div class: flex <nested> px-2.5 ",
|
||||
),
|
||||
// Custom delimiters
|
||||
(
|
||||
"%div{class: %w|flex px-2.5|}",
|
||||
"%div class: flex px-2.5 ",
|
||||
),
|
||||
(
|
||||
"%div{class: %W!flex px-2.5!}",
|
||||
"%div class: flex px-2.5 ",
|
||||
),
|
||||
(
|
||||
"%div{class: %w#text-sm leading-6#}",
|
||||
"%div class: text-sm leading-6 ",
|
||||
),
|
||||
(
|
||||
"%div{class: %w=italic tracking-wide=}",
|
||||
"%div class: italic tracking-wide ",
|
||||
),
|
||||
// Nested paired delimiters stay balanced inside the literal
|
||||
(
|
||||
"%div{class: %w[content-['[hello]'] p-4]}",
|
||||
"%div class: content-['[hello]'] p-4 ",
|
||||
),
|
||||
// Escaped spaces embed a space in a single array element
|
||||
(
|
||||
r#"%div{class: %w[foo\ bar baz-1]}"#,
|
||||
r#"%div class: foo bar baz-1 "#,
|
||||
),
|
||||
// A `%` that follows a value is not a percent literal
|
||||
("%p hit rate 50%w.", "%p hit rate 50%w "),
|
||||
] {
|
||||
Haml::test(input, expected);
|
||||
}
|
||||
|
||||
let input = r#"
|
||||
%div{class: %w[bg-blue-500 w-10 h-10]}
|
||||
%div{class: %w<flex px-2.5>}
|
||||
%div{class: %w|underline font-bold|}
|
||||
- classes = %w<mt-4 grid>
|
||||
"#;
|
||||
|
||||
Haml::test_extract_contains(
|
||||
input,
|
||||
vec![
|
||||
"bg-blue-500",
|
||||
"w-10",
|
||||
"h-10",
|
||||
"flex",
|
||||
"px-2.5",
|
||||
"underline",
|
||||
"font-bold",
|
||||
"mt-4",
|
||||
"grid",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/pull/17051#issuecomment-2711181352
|
||||
#[test]
|
||||
fn test_haml_full_file_17051() {
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@ impl PreProcessor for Json {
|
|||
let mut cursor = cursor::Cursor::new(content);
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// Consume strings as-is
|
||||
b'"' => {
|
||||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// Escaped character, skip ahead to the next character
|
||||
b'\\' => cursor.advance_twice(),
|
||||
|
||||
|
|
|
|||
|
|
@ -9,27 +9,20 @@ impl PreProcessor for Markdown {
|
|||
let len = content.len();
|
||||
let mut result = content.to_vec();
|
||||
let mut cursor = cursor::Cursor::new(content);
|
||||
let mut bracket_stack = vec![];
|
||||
|
||||
let mut in_directive = false;
|
||||
|
||||
while cursor.pos < len {
|
||||
match (in_directive, cursor.curr()) {
|
||||
match (in_directive, cursor.curr) {
|
||||
(false, b'{') => {
|
||||
result[cursor.pos] = b' ';
|
||||
in_directive = true;
|
||||
}
|
||||
(true, b'(' | b'[' | b'{' | b'<') => {
|
||||
bracket_stack.push(cursor.curr());
|
||||
}
|
||||
(true, b')' | b']' | b'}' | b'>') if !bracket_stack.is_empty() => {
|
||||
bracket_stack.pop();
|
||||
}
|
||||
(true, b'}') => {
|
||||
result[cursor.pos] = b' ';
|
||||
in_directive = false;
|
||||
}
|
||||
(true, b'.') if bracket_stack.is_empty() => {
|
||||
(true, b'.') => {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -67,17 +60,4 @@ mod tests {
|
|||
Markdown::test(input, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_classes_keep_the_dots() {
|
||||
for (input, expected) in [
|
||||
(
|
||||
r#"{<div class="px-2.5"></div>}"#,
|
||||
r#" <div class="px-2.5"></div> "#,
|
||||
),
|
||||
(r#"{content-['example.js']}"#, r#" content-['example.js'] "#),
|
||||
] {
|
||||
Markdown::test(input, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ pub mod ruby;
|
|||
pub mod rust;
|
||||
pub mod slim;
|
||||
pub mod svelte;
|
||||
pub mod template_toolkit;
|
||||
pub mod twig;
|
||||
pub mod vue;
|
||||
|
||||
pub use clojure::*;
|
||||
|
|
@ -26,6 +24,4 @@ pub use ruby::*;
|
|||
pub use rust::*;
|
||||
pub use slim::*;
|
||||
pub use svelte::*;
|
||||
pub use template_toolkit::*;
|
||||
pub use twig::*;
|
||||
pub use vue::*;
|
||||
|
|
|
|||
|
|
@ -25,33 +25,7 @@ pub trait PreProcessor: Sized + Default {
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn test_extract_exact(input: &str, expected: Vec<&str>) {
|
||||
use crate::extractor::{Extracted, Extractor};
|
||||
|
||||
let input = input.as_bytes();
|
||||
|
||||
let processor = Self::default();
|
||||
let transformed = processor.process(input);
|
||||
|
||||
let extracted = Extractor::new(&transformed).extract();
|
||||
|
||||
// Extract all candidates and css variables.
|
||||
let candidates = extracted
|
||||
.iter()
|
||||
.filter_map(|x| match x {
|
||||
Extracted::Candidate(bytes) => std::str::from_utf8(bytes).ok(),
|
||||
Extracted::CssVariable(bytes) => std::str::from_utf8(bytes).ok(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if candidates != expected {
|
||||
dbg!(&candidates, &expected);
|
||||
panic!("Extracted candidates do not match expected candidates");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn test_extract_contains(input: &str, expected: Vec<&str>) {
|
||||
fn test_extract_contains(input: &str, items: Vec<&str>) {
|
||||
use crate::extractor::{Extracted, Extractor};
|
||||
|
||||
let input = input.as_bytes();
|
||||
|
|
@ -72,7 +46,7 @@ pub trait PreProcessor: Sized + Default {
|
|||
|
||||
// Ensure all items are present in the candidates.
|
||||
let mut missing = vec![];
|
||||
for item in &expected {
|
||||
for item in &items {
|
||||
if !candidates.contains(item) {
|
||||
missing.push(item);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ impl PreProcessor for Pug {
|
|||
let mut bracket_stack = BracketStack::default();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// Only replace `.` with a space if it's not surrounded by numbers. E.g.:
|
||||
//
|
||||
// ```diff
|
||||
|
|
@ -43,7 +43,7 @@ impl PreProcessor for Pug {
|
|||
// digit.
|
||||
// E.g.: `bg-red-500.2xl:flex`
|
||||
// ^^^
|
||||
if cursor.prev().is_ascii_digit() && cursor.next().is_ascii_digit() {
|
||||
if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() {
|
||||
let mut next_cursor = cursor.clone();
|
||||
next_cursor.advance();
|
||||
|
||||
|
|
@ -68,17 +68,17 @@ impl PreProcessor for Pug {
|
|||
//
|
||||
// However, we also need to make sure that we keep the parens that are part of the
|
||||
// utility class. E.g.: `bg-(--my-color)`.
|
||||
b'(' if bracket_stack.is_empty() && !matches!(cursor.prev(), b'-' | b'/') => {
|
||||
b'(' if bracket_stack.is_empty() && !matches!(cursor.prev, b'-' | b'/') => {
|
||||
result[cursor.pos] = b' ';
|
||||
bracket_stack.push(cursor.curr());
|
||||
bracket_stack.push(cursor.curr);
|
||||
}
|
||||
|
||||
b'(' | b'[' | b'{' => {
|
||||
bracket_stack.push(cursor.curr());
|
||||
bracket_stack.push(cursor.curr);
|
||||
}
|
||||
|
||||
b')' | b']' | b'}' if !bracket_stack.is_empty() => {
|
||||
bracket_stack.pop(cursor.curr());
|
||||
bracket_stack.pop(cursor.curr);
|
||||
}
|
||||
|
||||
// Consume everything else
|
||||
|
|
|
|||
|
|
@ -34,135 +34,51 @@ impl PreProcessor for Ruby {
|
|||
|
||||
// Extract embedded template languages
|
||||
// https://viewcomponent.org/guide/templates.html#interpolations
|
||||
// Only process if content is valid UTF-8, otherwise skip HEREDOC extraction
|
||||
// but still perform the byte-level Ruby processing below
|
||||
if let Ok(content_as_str) = std::str::from_utf8(content) {
|
||||
let starts = TEMPLATE_START_REGEX
|
||||
.captures_iter(content_as_str)
|
||||
.collect::<Vec<_>>();
|
||||
let ends = TEMPLATE_END_REGEX
|
||||
.captures_iter(content_as_str)
|
||||
.collect::<Vec<_>>();
|
||||
let content_as_str = std::str::from_utf8(content).unwrap();
|
||||
|
||||
for start in starts.iter() {
|
||||
// The language for this block
|
||||
let lang = start.get(1).unwrap().as_str();
|
||||
let starts = TEMPLATE_START_REGEX
|
||||
.captures_iter(content_as_str)
|
||||
.collect::<Vec<_>>();
|
||||
let ends = TEMPLATE_END_REGEX
|
||||
.captures_iter(content_as_str)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// The HEREDOC delimiter
|
||||
let delimiter_start = start.get(2).unwrap().as_str();
|
||||
for start in starts.iter() {
|
||||
// The language for this block
|
||||
let lang = start.get(1).unwrap().as_str();
|
||||
|
||||
// Where the "body" starts for the HEREDOC block
|
||||
let body_start = start.get(0).unwrap().end();
|
||||
// The HEREDOC delimiter
|
||||
let delimiter_start = start.get(2).unwrap().as_str();
|
||||
|
||||
// Look through all of the ends to find a matching language
|
||||
for end in ends.iter() {
|
||||
// 1. This must appear after the start
|
||||
let body_end = end.get(0).unwrap().start();
|
||||
if body_end < body_start {
|
||||
continue;
|
||||
}
|
||||
// Where the "body" starts for the HEREDOC block
|
||||
let body_start = start.get(0).unwrap().end();
|
||||
|
||||
// The languages must match otherwise we haven't found the end
|
||||
let delimiter_end = end.get(1).unwrap().as_str();
|
||||
if delimiter_end != delimiter_start {
|
||||
continue;
|
||||
}
|
||||
|
||||
let body = &content_as_str[body_start..body_end];
|
||||
let replaced =
|
||||
pre_process_input(body.as_bytes().to_vec(), &lang.to_ascii_lowercase());
|
||||
|
||||
result.replace_range(body_start..body_end, replaced);
|
||||
break;
|
||||
// Look through all of the ends to find a matching language
|
||||
for end in ends.iter() {
|
||||
// 1. This must appear after the start
|
||||
let body_end = end.get(0).unwrap().start();
|
||||
if body_end < body_start {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The languages must match otherwise we haven't found the end
|
||||
let delimiter_end = end.get(1).unwrap().as_str();
|
||||
if delimiter_end != delimiter_start {
|
||||
continue;
|
||||
}
|
||||
|
||||
let body = &content_as_str[body_start..body_end];
|
||||
let replaced = pre_process_input(body.as_bytes(), &lang.to_ascii_lowercase());
|
||||
|
||||
result.replace_range(body_start..body_end, replaced);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Ruby extraction
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
b'"' => {
|
||||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// Escaped character, skip ahead to the next character
|
||||
b'\\' => cursor.advance_twice(),
|
||||
|
||||
// End of the string
|
||||
b'"' => break,
|
||||
|
||||
// Everything else is valid
|
||||
_ => cursor.advance(),
|
||||
};
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
b'\'' => {
|
||||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// Escaped character, skip ahead to the next character
|
||||
b'\\' => cursor.advance_twice(),
|
||||
|
||||
// End of the string
|
||||
b'\'' => break,
|
||||
|
||||
// Everything else is valid
|
||||
_ => cursor.advance(),
|
||||
};
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Replace comments in Ruby files
|
||||
//
|
||||
// Except for strict locals, these are defined in a `<%# locals: … %>`. Checking if
|
||||
// the comment is preceded by a `%` should be enough without having to perform more
|
||||
// parsing logic. Worst case we _do_ scan a few comments.
|
||||
//
|
||||
// We also want to skip interpolation syntax, which look like `#{…}`.
|
||||
b'#' if !matches!(cursor.prev(), b'%') && !matches!(cursor.next(), b'{') => {
|
||||
result[cursor.pos] = b' ';
|
||||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// End of the comment
|
||||
b'\n' => break,
|
||||
|
||||
// Everything else is part of the comment and replaced
|
||||
_ => {
|
||||
result[cursor.pos] = b' ';
|
||||
cursor.advance();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Looking for `%w`, `%W`, or `%p`
|
||||
if cursor.curr() != b'%' || !matches!(cursor.next(), b'w' | b'W' | b'p') {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// A `%` that follows a value is a modulo operation, not a percent literal. E.g.: the
|
||||
// `50%w` in `hit rate 50%w.`
|
||||
if cursor.prev().is_ascii_alphanumeric()
|
||||
|| matches!(cursor.prev(), b'_' | b')' | b']' | b'}')
|
||||
{
|
||||
// Looking for `%w` or `%W`
|
||||
if cursor.curr != b'%' && !matches!(cursor.next, b'w' | b'W') {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
|
|
@ -170,17 +86,10 @@ impl PreProcessor for Ruby {
|
|||
cursor.advance_twice();
|
||||
|
||||
// Boundary character
|
||||
let boundary = match cursor.curr() {
|
||||
let boundary = match cursor.curr {
|
||||
b'[' => b']',
|
||||
b'(' => b')',
|
||||
b'{' => b'}',
|
||||
b'<' => b'>',
|
||||
b' ' => b'\n',
|
||||
|
||||
// Any other ASCII punctuation can be used as a custom delimiter
|
||||
c if c.is_ascii_punctuation() => c,
|
||||
|
||||
// Everything else is not a valid delimiter
|
||||
_ => {
|
||||
cursor.advance();
|
||||
continue;
|
||||
|
|
@ -196,11 +105,11 @@ impl PreProcessor for Ruby {
|
|||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// Skip escaped characters, unless the backslash is the delimiter itself
|
||||
b'\\' if boundary != b'\\' => {
|
||||
match cursor.curr {
|
||||
// Skip escaped characters
|
||||
b'\\' => {
|
||||
// Use backslash to embed spaces in the strings.
|
||||
if cursor.next() == b' ' {
|
||||
if cursor.next == b' ' {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
|
||||
|
|
@ -209,36 +118,20 @@ impl PreProcessor for Ruby {
|
|||
|
||||
// Start of a nested bracket
|
||||
b'[' | b'(' | b'{' => {
|
||||
bracket_stack.push(cursor.curr());
|
||||
}
|
||||
|
||||
// Start of a nested `<…>`, which Ruby allows inside a `%w<…>` literal
|
||||
b'<' if boundary == b'>' => {
|
||||
bracket_stack.push(cursor.curr());
|
||||
bracket_stack.push(cursor.curr);
|
||||
}
|
||||
|
||||
// End of a nested bracket
|
||||
b']' | b')' | b'}' if !bracket_stack.is_empty() => {
|
||||
if !bracket_stack.pop(cursor.curr()) {
|
||||
// Unbalanced
|
||||
cursor.advance();
|
||||
}
|
||||
}
|
||||
|
||||
// End of a nested `<…>`
|
||||
b'>' if boundary == b'>' && !bracket_stack.is_empty() => {
|
||||
if !bracket_stack.pop(cursor.curr()) {
|
||||
if !bracket_stack.pop(cursor.curr) {
|
||||
// Unbalanced
|
||||
cursor.advance();
|
||||
}
|
||||
}
|
||||
|
||||
// End of the pattern, replace the boundary character with a space
|
||||
_ if cursor.curr() == boundary => {
|
||||
if boundary != b'\n' {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
|
||||
_ if cursor.curr == boundary => {
|
||||
result[cursor.pos] = b' ';
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -280,69 +173,12 @@ mod tests {
|
|||
"%w(flex data-[state=pending]:bg-(--my-color) flex-col)",
|
||||
"%w flex data-[state=pending]:bg-(--my-color) flex-col ",
|
||||
),
|
||||
// %w<…>
|
||||
("%w<flex px-2.5>", "%w flex px-2.5 "),
|
||||
(
|
||||
"%w<flex data-[state=pending]:bg-(--my-color) flex-col>",
|
||||
"%w flex data-[state=pending]:bg-(--my-color) flex-col ",
|
||||
),
|
||||
// Nested `<…>` does not end the literal
|
||||
("%w<flex <nested> px-2.5>", "%w flex <nested> px-2.5 "),
|
||||
// %w|…|, %w:…:, %w!…!
|
||||
("%w|flex px-2.5|", "%w flex px-2.5 "),
|
||||
("%w:flex px-2.5:", "%w flex px-2.5 "),
|
||||
("%w!flex px-2.5!", "%w flex px-2.5 "),
|
||||
(r#"%w\flex px-2.5\"#, r#"%w flex px-2.5 "#),
|
||||
// A `%` that follows a value is a modulo operation, not a percent literal
|
||||
(
|
||||
"hit rate 50%w.\n%w[flex px-2.5]",
|
||||
"hit rate 50%w.\n%w flex px-2.5 ",
|
||||
),
|
||||
|
||||
// %w …\n
|
||||
("%w flex px-2.5\n", "%w flex px-2.5\n"),
|
||||
|
||||
// Use backslash to embed spaces in the strings.
|
||||
(r#"%w[foo\ bar baz\ bat]"#, r#"%w foo bar baz bat "#),
|
||||
(r#"%W[foo\ bar baz\ bat]"#, r#"%W foo bar baz bat "#),
|
||||
|
||||
// The nested delimiters evaluated to a flat array of strings
|
||||
// (not nested array).
|
||||
(r#"%w[foo[bar baz]qux]"#, r#"%w foo[bar baz]qux "#),
|
||||
|
||||
(
|
||||
"# test\n# test\n# {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!]\n%w[flex px-2.5]",
|
||||
" \n \n \n%w flex px-2.5 "
|
||||
),
|
||||
|
||||
(r#""foo # bar""#, r#""foo # bar""#),
|
||||
(r#"'foo # bar'"#, r#"'foo # bar'"#),
|
||||
(
|
||||
r#"def call = tag.span "Foo", class: %w[rounded-full h-0.75 w-0.75]"#,
|
||||
r#"def call = tag.span "Foo", class: %w rounded-full h-0.75 w-0.75 "#
|
||||
),
|
||||
|
||||
(r#"%w[foo ' bar]"#, r#"%w foo ' bar "#),
|
||||
(r#"%w[foo " bar]"#, r#"%w foo " bar "#),
|
||||
(r#"%W[foo ' bar]"#, r#"%W foo ' bar "#),
|
||||
(r#"%W[foo " bar]"#, r#"%W foo " bar "#),
|
||||
|
||||
(r#"%p foo ' bar "#, r#"%p foo ' bar "#),
|
||||
(r#"%p foo " bar "#, r#"%p foo " bar "#),
|
||||
|
||||
(
|
||||
"%p has a ' quote\n# this should be removed\n%p has a ' quote",
|
||||
"%p has a ' quote\n \n%p has a ' quote"
|
||||
),
|
||||
(
|
||||
"%p has a \" quote\n# this should be removed\n%p has a \" quote",
|
||||
"%p has a \" quote\n \n%p has a \" quote"
|
||||
),
|
||||
|
||||
(
|
||||
"%w#this text is kept# # this text is not",
|
||||
"%w this text is kept ",
|
||||
),
|
||||
] {
|
||||
Ruby::test(input, expected);
|
||||
}
|
||||
|
|
@ -375,30 +211,6 @@ mod tests {
|
|||
"%w(flex data-[state=pending]:bg-(--my-color) flex-col)",
|
||||
vec!["flex", "data-[state=pending]:bg-(--my-color)", "flex-col"],
|
||||
),
|
||||
// %w<…>
|
||||
("%w<flex px-2.5>", vec!["flex", "px-2.5"]),
|
||||
("%w<px-2.5 flex>", vec!["flex", "px-2.5"]),
|
||||
("%w<2xl:flex>", vec!["2xl:flex"]),
|
||||
(
|
||||
"%w<flex data-[state=pending]:bg-(--my-color) flex-col>",
|
||||
vec!["flex", "data-[state=pending]:bg-(--my-color)", "flex-col"],
|
||||
),
|
||||
// Nested `<…>` does not end the literal
|
||||
("%w<flex <nested> px-2.5>", vec!["flex", "px-2.5"]),
|
||||
// %w|…|, %w:…:, %w!…!
|
||||
("%w|flex px-2.5|", vec!["flex", "px-2.5"]),
|
||||
("%w:flex px-2.5:", vec!["flex", "px-2.5"]),
|
||||
("%w!flex px-2.5!", vec!["flex", "px-2.5"]),
|
||||
|
||||
(
|
||||
"# test\n# test\n# {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!]\n%w[flex px-2.5]",
|
||||
vec!["flex", "px-2.5"],
|
||||
),
|
||||
|
||||
(r#""foo # bar""#, vec!["foo", "bar"]),
|
||||
(r#"'foo # bar'"#, vec!["foo", "bar"]),
|
||||
|
||||
(r#"%w[foo ' bar]"#, vec!["foo", "bar"]),
|
||||
] {
|
||||
Ruby::test_extract_contains(input, expected);
|
||||
}
|
||||
|
|
@ -450,79 +262,4 @@ mod tests {
|
|||
"#;
|
||||
Ruby::test_extract_contains(input, vec!["z-1", "z-2", "z-3"]);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/19728
|
||||
#[test]
|
||||
fn test_interpolated_expressions() {
|
||||
let input = r#"
|
||||
def width_class(width = nil)
|
||||
<<~STYLE_CLASS
|
||||
#{width || 'w-100'}
|
||||
STYLE_CLASS
|
||||
end
|
||||
"#;
|
||||
|
||||
Ruby::test_extract_contains(input, vec!["w-100"]);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/19239
|
||||
#[test]
|
||||
fn test_skip_comments() {
|
||||
let input = r#"
|
||||
# From activerecord-8.1.1/lib/active_record/errors.rb:147
|
||||
# Rails uses RDoc cross-reference syntax in inline documentation:
|
||||
# {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!]
|
||||
"#;
|
||||
|
||||
// Nothing should be extracted from comments, so expect an empty array.
|
||||
Ruby::test_extract_exact(input, vec![]);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/19481
|
||||
#[test]
|
||||
fn test_strict_locals() {
|
||||
// Strict locals are defined in a `<%# locals: … %>`, but the `#` looks like a comment
|
||||
// which we should not ignore in this case.
|
||||
let input = r#"
|
||||
<%# locals: (css: "text-amber-600") %>
|
||||
<% more_css = "text-sky-500" %>
|
||||
|
||||
<p class="text-green-500">
|
||||
In a partial
|
||||
</p>
|
||||
|
||||
<p class="<%= css %>">
|
||||
In a partial using explicit local variables
|
||||
</p>
|
||||
|
||||
<p class="<%= more_css %>">
|
||||
In a partial using explicit local variables
|
||||
</p>
|
||||
"#;
|
||||
|
||||
Ruby::test_extract_contains(
|
||||
input,
|
||||
vec!["text-amber-600", "text-sky-500", "text-green-500"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_utf8_does_not_panic() {
|
||||
// Invalid UTF-8 sequence: 0x80 is a continuation byte without a leading byte
|
||||
let invalid_utf8: &[u8] = &[0x80, 0x81, 0x82];
|
||||
|
||||
let processor = Ruby::default();
|
||||
|
||||
// Should not panic, just return the input unchanged
|
||||
let result = processor.process(invalid_utf8);
|
||||
assert_eq!(result, invalid_utf8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_utf8_with_multibyte_chars() {
|
||||
// Test that valid UTF-8 with multi-byte characters (like em-dashes) works
|
||||
let input = "# Comment with em—dash\n%w[flex px-2.5]";
|
||||
|
||||
Ruby::test_extract_contains(input, vec!["flex", "px-2.5"]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ impl Rust {
|
|||
let mut bracket_stack = bracket_stack::BracketStack::default();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// Escaped character, skip ahead to the next character
|
||||
b'\\' => {
|
||||
cursor.advance_twice();
|
||||
|
|
@ -46,7 +46,7 @@ impl Rust {
|
|||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// Escaped character, skip ahead to the next character
|
||||
b'\\' => cursor.advance_twice(),
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ impl Rust {
|
|||
// digit.
|
||||
// E.g.: `bg-red-500.2xl:flex`
|
||||
// ^^^
|
||||
if cursor.prev().is_ascii_digit() && cursor.next().is_ascii_digit() {
|
||||
if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() {
|
||||
let mut next_cursor = cursor.clone();
|
||||
next_cursor.advance();
|
||||
|
||||
|
|
@ -103,17 +103,11 @@ impl Rust {
|
|||
}
|
||||
|
||||
b'[' => {
|
||||
bracket_stack.push(cursor.curr());
|
||||
|
||||
// Handle `p.flex[condition]`. If there is a `-` before it, it will likely be an
|
||||
// arbitrary value e.g. `text-[red]`
|
||||
if !matches!(cursor.prev(), b'-') {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
bracket_stack.push(cursor.curr);
|
||||
}
|
||||
|
||||
b']' if !bracket_stack.is_empty() => {
|
||||
bracket_stack.pop(cursor.curr());
|
||||
bracket_stack.pop(cursor.curr);
|
||||
}
|
||||
|
||||
// Consume everything else
|
||||
|
|
@ -219,20 +213,4 @@ mod tests {
|
|||
let input = r#"html! { \x.px-4.text-black { } }"#;
|
||||
Rust::test(input, r#"html! { \x px-4 text-black { } }"#);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20233
|
||||
#[test]
|
||||
fn test_maud_template_extraction_with_conditional_classes() {
|
||||
let input = r#"
|
||||
use maud::{html, Markup};
|
||||
|
||||
pub fn main() -> Markup {
|
||||
html! {
|
||||
p.text-black[cuteness > 50] { "Squee!" }
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
Rust::test_extract_contains(input, vec!["text-black"]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ impl PreProcessor for Slim {
|
|||
let mut bracket_stack = BracketStack::default();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
match cursor.curr {
|
||||
// Only replace `.` with a space if it's not surrounded by numbers. E.g.:
|
||||
//
|
||||
// ```diff
|
||||
|
|
@ -43,7 +43,7 @@ impl PreProcessor for Slim {
|
|||
// digit.
|
||||
// E.g.: `bg-red-500.2xl:flex`
|
||||
// ^^^
|
||||
if cursor.prev().is_ascii_digit() && cursor.next().is_ascii_digit() {
|
||||
if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() {
|
||||
let mut next_cursor = cursor.clone();
|
||||
next_cursor.advance();
|
||||
|
||||
|
|
@ -65,74 +65,17 @@ impl PreProcessor for Slim {
|
|||
// class=%w[bg-blue-500 w-10 h-10]
|
||||
// ]
|
||||
// ```
|
||||
//
|
||||
// A `%` that follows a value is not a percent literal. E.g.: the `50%w` in
|
||||
// `hit rate 50%w.`
|
||||
b'%' if matches!(cursor.next(), b'w' | b'W')
|
||||
&& !cursor.prev().is_ascii_alphanumeric()
|
||||
&& !matches!(cursor.prev(), b'_' | b')' | b']' | b'}') =>
|
||||
b'%' if matches!(cursor.next, b'w' | b'W')
|
||||
&& matches!(cursor.input.get(cursor.pos + 2), Some(b'[' | b'(' | b'{')) =>
|
||||
{
|
||||
// Boundary characters
|
||||
let (open, close) = match cursor.input.get(cursor.pos + 2) {
|
||||
Some(b'[') => (b'[', b']'),
|
||||
Some(b'(') => (b'(', b')'),
|
||||
Some(b'{') => (b'{', b'}'),
|
||||
Some(b'<') => (b'<', b'>'),
|
||||
|
||||
// Any other ASCII punctuation can be used as a custom delimiter
|
||||
Some(&c) if c.is_ascii_punctuation() => (c, c),
|
||||
|
||||
// Everything else is not a valid delimiter
|
||||
_ => {
|
||||
cursor.advance();
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
result[cursor.pos] = b' '; // Replace `%`
|
||||
cursor.advance();
|
||||
result[cursor.pos] = b' '; // Replace `w`
|
||||
cursor.advance();
|
||||
result[cursor.pos] = b' '; // Replace the opening delimiter
|
||||
cursor.advance();
|
||||
|
||||
// Paired delimiters can be nested as long as they are balanced. E.g.:
|
||||
// `%w[foo[bar]baz]` produces a flat array.
|
||||
let mut depth = 1_usize;
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr() {
|
||||
// Skip escaped characters, unless the backslash is the delimiter
|
||||
// itself
|
||||
b'\\' if close != b'\\' => {
|
||||
// Use backslash to embed spaces in the strings.
|
||||
if cursor.next() == b' ' {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
// Start of a nested delimiter pair
|
||||
c if c == open && open != close => depth += 1,
|
||||
|
||||
// Closing delimiter
|
||||
c if c == close => {
|
||||
depth -= 1;
|
||||
|
||||
// End of the literal, replace the closing delimiter with a space
|
||||
if depth == 0 {
|
||||
result[cursor.pos] = b' ';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Everything else is valid content
|
||||
_ => {}
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
result[cursor.pos] = b' '; // Replace `[` or `(` or `{`
|
||||
bracket_stack.push(cursor.curr);
|
||||
cursor.advance(); // Move past the bracket
|
||||
continue;
|
||||
}
|
||||
|
||||
// Any `[` preceded by an alphanumeric value will not be part of a candidate.
|
||||
|
|
@ -153,10 +96,10 @@ impl PreProcessor for Slim {
|
|||
// Instead of listing all boundary characters, let's list the characters we know
|
||||
// will be invalid instead.
|
||||
b'[' if bracket_stack.is_empty()
|
||||
&& matches!(cursor.prev(), b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9') =>
|
||||
&& matches!(cursor.prev, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9') =>
|
||||
{
|
||||
result[cursor.pos] = b' ';
|
||||
bracket_stack.push(cursor.curr());
|
||||
bracket_stack.push(cursor.curr);
|
||||
}
|
||||
|
||||
// In Slim the class name shorthand can be followed by a parenthesis. E.g.:
|
||||
|
|
@ -171,17 +114,17 @@ impl PreProcessor for Slim {
|
|||
//
|
||||
// However, we also need to make sure that we keep the parens that are part of the
|
||||
// utility class. E.g.: `bg-(--my-color)`.
|
||||
b'(' if bracket_stack.is_empty() && !matches!(cursor.prev(), b'-' | b'/') => {
|
||||
b'(' if bracket_stack.is_empty() && !matches!(cursor.prev, b'-' | b'/') => {
|
||||
result[cursor.pos] = b' ';
|
||||
bracket_stack.push(cursor.curr());
|
||||
bracket_stack.push(cursor.curr);
|
||||
}
|
||||
|
||||
b'(' | b'[' | b'{' => {
|
||||
bracket_stack.push(cursor.curr());
|
||||
bracket_stack.push(cursor.curr);
|
||||
}
|
||||
|
||||
b')' | b']' | b'}' if !bracket_stack.is_empty() => {
|
||||
bracket_stack.pop(cursor.curr());
|
||||
bracket_stack.pop(cursor.curr);
|
||||
}
|
||||
|
||||
// Consume everything else
|
||||
|
|
@ -373,77 +316,16 @@ mod tests {
|
|||
]
|
||||
"#;
|
||||
|
||||
let expected = "
|
||||
div \n class= bg-blue-500 w-10 h-10 \n ]
|
||||
div \n class= w-10 bg-green-500 h-10 \n ]
|
||||
";
|
||||
let expected = r#"
|
||||
div
|
||||
class= bg-blue-500 w-10 h-10]
|
||||
]
|
||||
div
|
||||
class= w-10 bg-green-500 h-10]
|
||||
]
|
||||
"#;
|
||||
|
||||
Slim::test(input, expected);
|
||||
Slim::test_extract_contains(input, vec!["bg-blue-500", "bg-green-500", "w-10", "h-10"]);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20386
|
||||
#[test]
|
||||
fn test_embedded_ruby_percent_w_delimiters() {
|
||||
for (input, expected) in [
|
||||
// %w<…>, Slim only counts `[({` nesting in attribute values, so the code must be
|
||||
// wrapped in parentheses to contain spaces
|
||||
(
|
||||
"div class=(%w<bg-blue-500 w-10 h-10>)",
|
||||
"div class= bg-blue-500 w-10 h-10 )",
|
||||
),
|
||||
// Nested `<…>` does not end the literal
|
||||
(
|
||||
"div class=(%w<flex <nested> px-2.5>)",
|
||||
"div class= flex <nested> px-2.5 )",
|
||||
),
|
||||
// Custom delimiters
|
||||
("div class=(%w|flex px-2.5|)", "div class= flex px-2.5 )"),
|
||||
("div class=(%W!flex px-2.5!)", "div class= flex px-2.5 )"),
|
||||
(
|
||||
"div class=(%w#text-sm leading-6#)",
|
||||
"div class= text-sm leading-6 )",
|
||||
),
|
||||
(
|
||||
"div class=(%w=italic tracking-wide=)",
|
||||
"div class= italic tracking-wide )",
|
||||
),
|
||||
// Nested paired delimiters stay balanced inside the literal
|
||||
(
|
||||
"div class=(%w[content-['[hello]'] p-4])",
|
||||
"div class= content-['[hello]'] p-4 )",
|
||||
),
|
||||
// Escaped spaces embed a space in a single array element
|
||||
(
|
||||
r#"div class=(%w[foo\ bar baz-1])"#,
|
||||
r#"div class= foo bar baz-1 )"#,
|
||||
),
|
||||
// Ruby control line, which is plain Ruby code
|
||||
("- classes = %w<mt-4 flex>", "- classes = mt-4 flex "),
|
||||
// A `%` that follows a value is not a percent literal
|
||||
("| hit rate 50%w.", "| hit rate 50%w "),
|
||||
] {
|
||||
Slim::test(input, expected);
|
||||
}
|
||||
|
||||
let input = r#"
|
||||
div[
|
||||
class=(%w<bg-blue-500 w-10 h-10>)
|
||||
]
|
||||
- classes = %w|w-10 bg-green-500 h-10|
|
||||
= tag.div class: %W!px-2.5 flex!
|
||||
"#;
|
||||
|
||||
Slim::test_extract_contains(
|
||||
input,
|
||||
vec![
|
||||
"bg-blue-500",
|
||||
"bg-green-500",
|
||||
"w-10",
|
||||
"h-10",
|
||||
"px-2.5",
|
||||
"flex",
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
use crate::cursor;
|
||||
use crate::extractor::pre_processors::pre_processor::PreProcessor;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct TemplateToolkit;
|
||||
|
||||
impl PreProcessor for TemplateToolkit {
|
||||
fn process(&self, content: &[u8]) -> Vec<u8> {
|
||||
let len = content.len();
|
||||
let mut result = content.to_vec();
|
||||
let mut cursor = cursor::Cursor::new(content);
|
||||
|
||||
while cursor.pos < len {
|
||||
match (cursor.curr(), cursor.next()) {
|
||||
(b'[', b'%') => result[cursor.pos] = b' ',
|
||||
(b'%', b']') => result[cursor.pos + 1] = b' ',
|
||||
_ => {}
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::TemplateToolkit;
|
||||
use crate::extractor::pre_processors::pre_processor::PreProcessor;
|
||||
|
||||
#[test]
|
||||
fn test_template_toolkit_pre_processor() {
|
||||
for (input, expected) in [
|
||||
(
|
||||
"[% IF $is_open %]bg-white/40[% ELSE %]bg-white/10[% END %]",
|
||||
" % IF $is_open % bg-white/40 % ELSE % bg-white/10 % END % ",
|
||||
),
|
||||
("[% WRAPPER %]flex[% END %]", " % WRAPPER % flex % END % "),
|
||||
] {
|
||||
TemplateToolkit::test(input, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extraction_between_template_tags_works() {
|
||||
TemplateToolkit::test_extract_contains(
|
||||
r#"<div class="[% IF $is_open %]bg-white/40[% ELSE %]bg-white/10[% END %]"></div>"#,
|
||||
vec!["bg-white/40", "bg-white/10"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
.relative
|
||||
^^^^^^^^
|
||||
- # Blurred background star
|
||||
^^^^^^^^^^ ^^^^
|
||||
.absolute.left-0.z-0{ class: "-top-[400px] -right-[400px]" }
|
||||
^^^^^^^^ ^^^^^^ ^^^ ^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^^^
|
||||
.flex.justify-end.blur-3xl
|
||||
|
|
@ -74,8 +75,8 @@
|
|||
^^^^^^^^^^ ^^^^^^
|
||||
libraries, our components are designed to be fast, flexible, and easy to
|
||||
^^^ ^^^^^^^^^^ ^^^ ^^^^^^^^ ^^ ^^ ^^^ ^^^^ ^^
|
||||
use <i>directly</i> in Ruby on Rails!
|
||||
^^^ ^^^^^^^^ ^^ ^^
|
||||
use <i>directy</i> in Ruby on Rails!
|
||||
^^^ ^^^^^^^ ^^ ^^
|
||||
|
||||
= doc_example(css: "mt-8 lg:mt-0 lg:basis-3/5 h-44") do
|
||||
^^^ ^^^^ ^^^^^^^ ^^^^^^^^^^^^ ^^^^ ^^
|
||||
|
|
@ -101,7 +102,7 @@
|
|||
|
||||
%p.mt-2
|
||||
^^^^
|
||||
Utilize
|
||||
Utlize
|
||||
= daisy_link("HAML", "https://haml.info/", target: "_blank")
|
||||
^^^^^^
|
||||
so your views are simple, concise, and easy to understand.
|
||||
|
|
@ -119,8 +120,8 @@
|
|||
%p.mt-2
|
||||
^^^^
|
||||
:markdown
|
||||
**PLUS!** You can utilize filters like _Markdown_, CoffeeScript,
|
||||
^^^ ^^^^^^^ ^^^^^^^ ^^^^
|
||||
**PLUS!** You can utlize filters like _Markdown_, CoffeeScript,
|
||||
^^^ ^^^^^^ ^^^^^^^ ^^^^
|
||||
Textile, and many more!
|
||||
^^^ ^^^^ ^^^^^
|
||||
|
||||
|
|
@ -195,6 +196,7 @@
|
|||
^^^^^^^^ ^^^^ ^^^ ^^^^ ^^
|
||||
:escaped
|
||||
- # app/components/character_component.html.haml
|
||||
^^^^
|
||||
= part(:component) do
|
||||
^^
|
||||
= part(:head)
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@
|
|||
= daisy_link "DaisyUI", "https://daisyui.com/", target: "_blank"
|
||||
= daisy_link "TailwindCSS", "https://tailwindcss.com/", target: "_blank"
|
||||
libraries, our components are designed to be fast, flexible, and easy to
|
||||
use <i>directly</i> in Ruby on Rails!
|
||||
use <i>directy</i> in Ruby on Rails!
|
||||
|
||||
= doc_example(css: "mt-8 lg:mt-0 lg:basis-3/5 h-44") do
|
||||
.flex.flex-col.sm:flex-row.items-center.gap-4
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
Simple, Concise Views
|
||||
|
||||
%p.mt-2
|
||||
Utilize
|
||||
Utlize
|
||||
= daisy_link("HAML", "https://haml.info/", target: "_blank")
|
||||
so your views are simple, concise, and easy to understand.
|
||||
|
||||
|
|
@ -69,7 +69,7 @@
|
|||
|
||||
%p.mt-2
|
||||
:markdown
|
||||
**PLUS!** You can utilize filters like _Markdown_, CoffeeScript,
|
||||
**PLUS!** You can utlize filters like _Markdown_, CoffeeScript,
|
||||
Textile, and many more!
|
||||
|
||||
.mt-8.xl:mt-0
|
||||
|
|
|
|||
|
|
@ -1,179 +0,0 @@
|
|||
use crate::cursor;
|
||||
use crate::extractor::pre_processors::pre_processor::PreProcessor;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Twig;
|
||||
|
||||
impl PreProcessor for Twig {
|
||||
fn process(&self, content: &[u8]) -> Vec<u8> {
|
||||
let len = content.len();
|
||||
let mut result = content.to_vec();
|
||||
let mut cursor = cursor::Cursor::new(content);
|
||||
let mut bracket_stack = vec![];
|
||||
|
||||
const ADD_CLASS: &[u8] = b"addClass";
|
||||
const REMOVE_CLASS: &[u8] = b"removeClass";
|
||||
|
||||
while cursor.pos < len {
|
||||
match (!bracket_stack.is_empty(), cursor.curr()) {
|
||||
// addClass(
|
||||
// ^
|
||||
(false, b'(')
|
||||
if cursor.pos >= ADD_CLASS.len()
|
||||
&& matches!(
|
||||
&content[cursor.pos - ADD_CLASS.len()..cursor.pos],
|
||||
ADD_CLASS
|
||||
) =>
|
||||
{
|
||||
bracket_stack.push(cursor.curr());
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
|
||||
// removeClass(
|
||||
// ^
|
||||
(false, b'(')
|
||||
if cursor.pos >= REMOVE_CLASS.len()
|
||||
&& matches!(
|
||||
&content[cursor.pos - REMOVE_CLASS.len()..cursor.pos],
|
||||
REMOVE_CLASS
|
||||
) =>
|
||||
{
|
||||
bracket_stack.push(cursor.curr());
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
|
||||
(true, b'(') => {
|
||||
bracket_stack.push(cursor.curr());
|
||||
}
|
||||
|
||||
(true, b')') => {
|
||||
bracket_stack.pop();
|
||||
|
||||
if bracket_stack.is_empty() {
|
||||
result[cursor.pos] = b' ';
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
cursor.advance();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Twig;
|
||||
use crate::extractor::pre_processors::pre_processor::PreProcessor;
|
||||
|
||||
#[test]
|
||||
fn test_twig_pre_processor() {
|
||||
for (input, expected) in [
|
||||
(
|
||||
// Ensure we don't crash when we encounter an `(`
|
||||
"(p-(--value))",
|
||||
"(p-(--value))",
|
||||
),
|
||||
(
|
||||
// addClass with single argument
|
||||
"addClass(p-(--value))",
|
||||
"addClass p-(--value) ",
|
||||
),
|
||||
(
|
||||
// addClass with single argument
|
||||
"addClass(m-4 p-8 w-full)",
|
||||
"addClass m-4 p-8 w-full ",
|
||||
),
|
||||
(
|
||||
// removeClass with single argument
|
||||
"removeClass(p-(--value))",
|
||||
"removeClass p-(--value) ",
|
||||
),
|
||||
(
|
||||
// removeClass with single argument
|
||||
"removeClass(m-4 p-8 w-full)",
|
||||
"removeClass m-4 p-8 w-full ",
|
||||
),
|
||||
(
|
||||
// Combined with single arguments
|
||||
"addClass(m-(--value))|removeClass(p-(--value))",
|
||||
"addClass m-(--value) |removeClass p-(--value) ",
|
||||
),
|
||||
] {
|
||||
Twig::test(input, expected);
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/19458
|
||||
#[test]
|
||||
fn test_extraction_in_add_class_and_remove_class_works() {
|
||||
// Inside normal HTML
|
||||
let input = r#"
|
||||
<div data-loading="addClass(opacity-50)">
|
||||
<!-- -->
|
||||
</div>
|
||||
"#;
|
||||
|
||||
let expected = r#"
|
||||
<div data-loading="addClass opacity-50 ">
|
||||
<!-- -->
|
||||
</div>
|
||||
"#;
|
||||
|
||||
Twig::test(input, expected);
|
||||
Twig::test_extract_contains(input, vec!["opacity-50"]);
|
||||
|
||||
// Inside a component
|
||||
let input = r#"
|
||||
<twig:d:Card
|
||||
{{ ...attributes.defaults({
|
||||
as: "a",
|
||||
class: "cursor-pointer bg-base-200 card-sm md:card-md lg:card-lg",
|
||||
role: "button",
|
||||
tabindex: "0",
|
||||
"data-action": "live#$render",
|
||||
"data-loading": "addAttribute(disabled)",
|
||||
"data-poll": "delay(60000)|$render",
|
||||
"data-loading": "addClass(border border-red-500 opacity-75)",
|
||||
})
|
||||
}}
|
||||
>
|
||||
"#;
|
||||
|
||||
let expected = r#"
|
||||
<twig:d:Card
|
||||
{{ ...attributes.defaults({
|
||||
as: "a",
|
||||
class: "cursor-pointer bg-base-200 card-sm md:card-md lg:card-lg",
|
||||
role: "button",
|
||||
tabindex: "0",
|
||||
"data-action": "live#$render",
|
||||
"data-loading": "addAttribute(disabled)",
|
||||
"data-poll": "delay(60000)|$render",
|
||||
"data-loading": "addClass border border-red-500 opacity-75 ",
|
||||
})
|
||||
}}
|
||||
>
|
||||
"#;
|
||||
|
||||
Twig::test(input, expected);
|
||||
Twig::test_extract_contains(
|
||||
input,
|
||||
vec![
|
||||
// class
|
||||
"cursor-pointer",
|
||||
"bg-base-200",
|
||||
"card-sm",
|
||||
"md:card-md",
|
||||
"lg:card-lg",
|
||||
// data-loading
|
||||
"border",
|
||||
"border-red-500",
|
||||
"opacity-75",
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,15 +15,13 @@ impl PreProcessor for Vue {
|
|||
fn process(&self, content: &[u8]) -> Vec<u8> {
|
||||
let mut result = content.to_vec();
|
||||
|
||||
// Only process template tags if content is valid UTF-8
|
||||
if let Ok(content_as_str) = std::str::from_utf8(content) {
|
||||
for (_, [lang, body]) in TEMPLATE_REGEX
|
||||
.captures_iter(content_as_str)
|
||||
.map(|c| c.extract())
|
||||
{
|
||||
let replaced = pre_process_input(body.as_bytes().to_vec(), lang);
|
||||
result = result.replace(body, replaced);
|
||||
}
|
||||
let content_as_str = std::str::from_utf8(content).unwrap();
|
||||
for (_, [lang, body]) in TEMPLATE_REGEX
|
||||
.captures_iter(content_as_str)
|
||||
.map(|c| c.extract())
|
||||
{
|
||||
let replaced = pre_process_input(body.as_bytes(), lang);
|
||||
result = result.replace(body, replaced);
|
||||
}
|
||||
|
||||
result
|
||||
|
|
@ -45,16 +43,4 @@ mod tests {
|
|||
|
||||
Vue::test_extract_contains(input, vec!["bg-neutral-900", "text-red-500"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_utf8_does_not_panic() {
|
||||
// Invalid UTF-8 sequence: 0x80 is a continuation byte without a leading byte
|
||||
let invalid_utf8: &[u8] = &[0x80, 0x81, 0x82];
|
||||
|
||||
let processor = Vue::default();
|
||||
|
||||
// Should not panic, just return the input unchanged
|
||||
let result = processor.process(invalid_utf8);
|
||||
assert_eq!(result, invalid_utf8);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,20 +30,20 @@ impl Machine for StringMachine {
|
|||
|
||||
#[inline]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
if Class::Quote != cursor.curr().into() {
|
||||
if Class::Quote != cursor.curr.into() {
|
||||
return MachineState::Idle;
|
||||
}
|
||||
|
||||
// Start of a string
|
||||
let len = cursor.input.len();
|
||||
let start_pos = cursor.pos;
|
||||
let end_char = cursor.curr();
|
||||
let end_char = cursor.curr;
|
||||
|
||||
cursor.advance();
|
||||
|
||||
while cursor.pos < len {
|
||||
match cursor.curr().into() {
|
||||
Class::Escape => match cursor.next().into() {
|
||||
match cursor.curr.into() {
|
||||
Class::Escape => match cursor.next.into() {
|
||||
// An escaped whitespace character is not allowed
|
||||
Class::Whitespace => return MachineState::Idle,
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ impl Machine for StringMachine {
|
|||
},
|
||||
|
||||
// End of the string
|
||||
Class::Quote if cursor.curr() == end_char => return self.done(start_pos, cursor),
|
||||
Class::Quote if cursor.curr == end_char => return self.done(start_pos, cursor),
|
||||
|
||||
// Any kind of whitespace is not allowed
|
||||
Class::Whitespace => return MachineState::Idle,
|
||||
|
|
|
|||
|
|
@ -27,12 +27,12 @@ impl Machine for UtilityMachine {
|
|||
|
||||
#[inline]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match cursor.curr().into() {
|
||||
match cursor.curr.into() {
|
||||
// LEGACY: Important marker
|
||||
Class::Exclamation => {
|
||||
self.legacy_important = true;
|
||||
|
||||
match cursor.next().into() {
|
||||
match cursor.next.into() {
|
||||
// Start of an arbitrary property
|
||||
//
|
||||
// E.g.: `![color:red]`
|
||||
|
|
@ -78,7 +78,7 @@ impl UtilityMachine {
|
|||
fn parse_arbitrary_property(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match self.arbitrary_property_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
MachineState::Done(_) => match cursor.next().into() {
|
||||
MachineState::Done(_) => match cursor.next.into() {
|
||||
// End of arbitrary property, but there is a potential modifier.
|
||||
//
|
||||
// E.g.: `[color:#0088cc]/`
|
||||
|
|
@ -109,7 +109,7 @@ impl UtilityMachine {
|
|||
fn parse_named_utility(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match self.named_utility_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
MachineState::Done(_) => match cursor.next().into() {
|
||||
MachineState::Done(_) => match cursor.next.into() {
|
||||
// End of a named utility, but there is a potential modifier.
|
||||
//
|
||||
// E.g.: `bg-red-500/`
|
||||
|
|
@ -140,7 +140,7 @@ impl UtilityMachine {
|
|||
fn parse_modifier(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match self.modifier_machine.next(cursor) {
|
||||
MachineState::Idle => self.restart(),
|
||||
MachineState::Done(_) => match cursor.next().into() {
|
||||
MachineState::Done(_) => match cursor.next.into() {
|
||||
// A modifier followed by a modifier is invalid
|
||||
Class::Slash => self.restart(),
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ impl Machine for VariantMachine {
|
|||
|
||||
#[inline]
|
||||
fn next(&mut self, cursor: &mut cursor::Cursor<'_>) -> MachineState {
|
||||
match cursor.curr().into() {
|
||||
match cursor.curr.into() {
|
||||
// Start of an arbitrary variant
|
||||
//
|
||||
// E.g.: `[&:hover]:`
|
||||
|
|
@ -48,7 +48,7 @@ impl VariantMachine {
|
|||
start_pos: usize,
|
||||
cursor: &mut cursor::Cursor<'_>,
|
||||
) -> MachineState {
|
||||
match cursor.next().into() {
|
||||
match cursor.next.into() {
|
||||
// End of an arbitrary value, must be followed by a `:`
|
||||
//
|
||||
// E.g.: `[&:hover]:`
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ pub fn fast_skip(cursor: &Cursor) -> Option<usize> {
|
|||
return None;
|
||||
}
|
||||
|
||||
if !cursor.curr().is_ascii_whitespace() {
|
||||
if !cursor.curr.is_ascii_whitespace() {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,9 +30,11 @@ fn sort_by_dir_and_name(a: &DirEntry, z: &DirEntry) -> Ordering {
|
|||
|
||||
pub fn resolve_globs(
|
||||
base: PathBuf,
|
||||
dirs: &FxHashSet<PathBuf>,
|
||||
dirs: &[PathBuf],
|
||||
extensions: &FxHashSet<String>,
|
||||
) -> Vec<GlobEntry> {
|
||||
let allowed_paths: FxHashSet<PathBuf> = FxHashSet::from_iter(dirs.iter().cloned());
|
||||
|
||||
// A list of known extensions + a list of extensions we found in the project.
|
||||
let mut found_extensions: FxHashSet<String> =
|
||||
FxHashSet::from_iter(KNOWN_EXTENSIONS.iter().map(|x| x.to_string()));
|
||||
|
|
@ -70,7 +72,7 @@ pub fn resolve_globs(
|
|||
continue;
|
||||
}
|
||||
|
||||
if !dirs.contains(path) {
|
||||
if !allowed_paths.contains(path) {
|
||||
let mut path = path;
|
||||
while let Some(parent) = path.parent() {
|
||||
if parent == base {
|
||||
|
|
@ -111,8 +113,7 @@ pub fn resolve_globs(
|
|||
continue;
|
||||
}
|
||||
|
||||
if !dirs.contains(path) {
|
||||
it.skip_current_dir();
|
||||
if !allowed_paths.contains(path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
.git
|
||||
.hg
|
||||
.jj
|
||||
.next
|
||||
.parcel-cache
|
||||
.pnpm-store
|
||||
.svelte-kit
|
||||
.svn
|
||||
.turbo
|
||||
.venv
|
||||
.vercel
|
||||
.yarn
|
||||
__pycache__
|
||||
node_modules
|
||||
.yarn
|
||||
.venv
|
||||
venv
|
||||
.next
|
||||
.turbo
|
||||
.parcel-cache
|
||||
__pycache__
|
||||
.svelte-kit
|
||||
.pnpm-store
|
||||
.vercel
|
||||
|
|
|
|||
|
|
@ -2,5 +2,3 @@ package-lock.json
|
|||
pnpm-lock.yaml
|
||||
bun.lockb
|
||||
.gitignore
|
||||
.env
|
||||
.env.*
|
||||
|
|
|
|||
|
|
@ -1,102 +0,0 @@
|
|||
use std::fs::OpenOptions;
|
||||
use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
use std::sync::{self, Arc, Mutex};
|
||||
use tracing_subscriber::fmt::writer::BoxMakeWriter;
|
||||
|
||||
pub static SHOULD_TRACE: sync::LazyLock<bool> = sync::LazyLock::new(
|
||||
|| matches!(std::env::var("DEBUG"), Ok(value) if value.eq("*") || (value.contains("tailwindcss:oxide") && !value.contains("-tailwindcss:oxide"))),
|
||||
);
|
||||
|
||||
fn dim(input: &str) -> String {
|
||||
format!("\u{001b}[2m{input}\u{001b}[22m")
|
||||
}
|
||||
|
||||
fn blue(input: &str) -> String {
|
||||
format!("\u{001b}[34m{input}\u{001b}[39m")
|
||||
}
|
||||
|
||||
fn highlight(input: &str) -> String {
|
||||
format!("{}{}{}", dim(&blue("`")), blue(input), dim(&blue("`")))
|
||||
}
|
||||
|
||||
struct MutexWriter(Arc<Mutex<std::fs::File>>);
|
||||
|
||||
impl Write for MutexWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.0.lock().unwrap().write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.0.lock().unwrap().flush()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_tracing() {
|
||||
if !*SHOULD_TRACE {
|
||||
return;
|
||||
}
|
||||
|
||||
let root = Path::new(".tailwindcss");
|
||||
let logs_dir = root.join("logs");
|
||||
if let Err(err) = std::fs::create_dir_all(&logs_dir) {
|
||||
eprintln!(
|
||||
"{} Failed to create {}, skipping debug logs ({err})",
|
||||
dim("[DEBUG]"),
|
||||
highlight(&logs_dir.display().to_string())
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure everything inside `.tailwindcss/` is ignored by git. The file is only created if it
|
||||
// doesn't exist yet, an existing `.gitignore` is left untouched.
|
||||
if let Ok(mut file) = std::fs::File::create_new(root.join(".gitignore")) {
|
||||
_ = file.write_all(b"*\n");
|
||||
}
|
||||
|
||||
let file_path = logs_dir.join(format!(
|
||||
"scanner-{}-{}.log",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis())
|
||||
.unwrap_or(0),
|
||||
std::process::id()
|
||||
));
|
||||
let file = match OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&file_path)
|
||||
{
|
||||
Ok(file) => file,
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"{} Failed to create {}, skipping debug logs ({err})",
|
||||
dim("[DEBUG]"),
|
||||
highlight(&file_path.display().to_string())
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let absolute_file_path = dunce::canonicalize(&file_path).unwrap_or_else(|_| file_path.clone());
|
||||
eprintln!(
|
||||
"{} Writing debug info to: {}\n",
|
||||
dim("[DEBUG]"),
|
||||
highlight(&absolute_file_path.display().to_string())
|
||||
);
|
||||
|
||||
let file = Arc::new(Mutex::new(file));
|
||||
|
||||
let writer: BoxMakeWriter = BoxMakeWriter::new({
|
||||
let file = file.clone();
|
||||
move || Box::new(MutexWriter(file.clone())) as Box<dyn Write + Send>
|
||||
});
|
||||
|
||||
_ = tracing_subscriber::fmt()
|
||||
.with_max_level(tracing::Level::INFO)
|
||||
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::ACTIVE)
|
||||
.with_writer(writer)
|
||||
.with_ansi(false)
|
||||
.compact()
|
||||
.try_init();
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
pub mod auto_source_detection;
|
||||
pub mod detect_sources;
|
||||
pub mod init_tracing;
|
||||
pub mod sources;
|
||||
|
||||
use crate::extractor::{Extracted, Extractor};
|
||||
|
|
@ -15,12 +14,15 @@ use bstr::ByteSlice;
|
|||
use fast_glob::glob_match;
|
||||
use fxhash::{FxHashMap, FxHashSet};
|
||||
use ignore::{gitignore::GitignoreBuilder, WalkBuilder};
|
||||
use init_tracing::{init_tracing, SHOULD_TRACE};
|
||||
use rayon::prelude::*;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::{self, Arc, Mutex};
|
||||
use std::time::SystemTime;
|
||||
use tracing::event;
|
||||
use tracing_subscriber::fmt::writer::BoxMakeWriter;
|
||||
|
||||
// @source "some/folder"; // This is auto source detection
|
||||
// @source "some/folder/**/*"; // This is auto source detection
|
||||
|
|
@ -32,6 +34,70 @@ use tracing::event;
|
|||
//
|
||||
// @source "do-include-me.bin"; // `.bin` is typically ignored, but now it's explicit so should be included
|
||||
// @source "git-ignored.html"; // A git ignored file that is listed explicitly, should be scanned
|
||||
static SHOULD_TRACE: sync::LazyLock<bool> = sync::LazyLock::new(
|
||||
|| matches!(std::env::var("DEBUG"), Ok(value) if value.eq("*") || (value.contains("tailwindcss:oxide") && !value.contains("-tailwindcss:oxide"))),
|
||||
);
|
||||
|
||||
fn dim(input: &str) -> String {
|
||||
format!("\u{001b}[2m{input}\u{001b}[22m")
|
||||
}
|
||||
|
||||
fn blue(input: &str) -> String {
|
||||
format!("\u{001b}[34m{input}\u{001b}[39m")
|
||||
}
|
||||
|
||||
fn highlight(input: &str) -> String {
|
||||
format!("{}{}{}", dim(&blue("`")), blue(input), dim(&blue("`")))
|
||||
}
|
||||
|
||||
fn init_tracing() {
|
||||
if !*SHOULD_TRACE {
|
||||
return;
|
||||
}
|
||||
|
||||
let file_path = format!("tailwindcss-{}.log", std::process::id());
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&file_path)
|
||||
.unwrap_or_else(|_| panic!("Failed to open {file_path}"));
|
||||
|
||||
let file_path = Path::new(&file_path);
|
||||
let absolute_file_path = dunce::canonicalize(file_path)
|
||||
.unwrap_or_else(|_| panic!("Failed to canonicalize {file_path:?}"));
|
||||
eprintln!(
|
||||
"{} Writing debug info to: {}\n",
|
||||
dim("[DEBUG]"),
|
||||
highlight(absolute_file_path.as_path().to_str().unwrap())
|
||||
);
|
||||
|
||||
let file = Arc::new(Mutex::new(file));
|
||||
|
||||
let writer: BoxMakeWriter = BoxMakeWriter::new({
|
||||
let file = file.clone();
|
||||
move || Box::new(MutexWriter(file.clone())) as Box<dyn Write + Send>
|
||||
});
|
||||
|
||||
_ = tracing_subscriber::fmt()
|
||||
.with_max_level(tracing::Level::INFO)
|
||||
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::ACTIVE)
|
||||
.with_writer(writer)
|
||||
.with_ansi(false)
|
||||
.compact()
|
||||
.try_init();
|
||||
}
|
||||
|
||||
struct MutexWriter(Arc<Mutex<std::fs::File>>);
|
||||
|
||||
impl Write for MutexWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.0.lock().unwrap().write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.0.lock().unwrap().flush()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ChangedContent {
|
||||
|
|
@ -63,33 +129,26 @@ pub struct Scanner {
|
|||
/// The walker to detect all files that we have to scan
|
||||
walker: Option<WalkBuilder>,
|
||||
|
||||
/// All changed content that we have to parse
|
||||
changed_content: Vec<ChangedContent>,
|
||||
|
||||
/// All found extensions
|
||||
extensions: FxHashSet<String>,
|
||||
|
||||
/// All CSS files we want to scan for CSS variable usage
|
||||
css_files: Vec<PathBuf>,
|
||||
|
||||
/// All files that we have to scan
|
||||
files: FxHashSet<PathBuf>,
|
||||
files: Vec<PathBuf>,
|
||||
|
||||
/// All directories, sub-directories, etc… we saw during source detection
|
||||
dirs: FxHashSet<PathBuf>,
|
||||
dirs: Vec<PathBuf>,
|
||||
|
||||
/// All generated globs, used for setting up watchers
|
||||
globs: Option<Vec<GlobEntry>>,
|
||||
|
||||
/// Track unique set of candidates
|
||||
candidates: FxHashSet<String>,
|
||||
|
||||
/// Track mtimes for files so incremental scans can skip unchanged files.
|
||||
mtimes: FxHashMap<PathBuf, SystemTime>,
|
||||
|
||||
/// Files that were scanned during the last `scan()` call.
|
||||
scanned_files: Vec<String>,
|
||||
|
||||
/// Whether we've completed at least one full scan. When false, we skip
|
||||
/// mtime tracking entirely so the initial build stays fast.
|
||||
has_scanned_once: bool,
|
||||
|
||||
/// Whether sources have been scanned since the last `scan()` call
|
||||
sources_scanned: bool,
|
||||
}
|
||||
|
||||
impl Scanner {
|
||||
|
|
@ -104,6 +163,7 @@ impl Scanner {
|
|||
}
|
||||
|
||||
let sources = Sources::new(public_source_entries_to_private_source_entries(sources));
|
||||
|
||||
if *SHOULD_TRACE {
|
||||
event!(tracing::Level::INFO, "Optimized sources:");
|
||||
for source in sources.iter() {
|
||||
|
|
@ -111,27 +171,29 @@ impl Scanner {
|
|||
}
|
||||
}
|
||||
|
||||
let walker = create_walker(&sources);
|
||||
|
||||
Self {
|
||||
sources,
|
||||
walker,
|
||||
sources: sources.clone(),
|
||||
walker: create_walker(sources),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scan(&mut self) -> Vec<String> {
|
||||
self.sources_scanned = false;
|
||||
self.scan_sources();
|
||||
|
||||
let (scanned_blobs, css_files, files) = self.discover_sources();
|
||||
// TODO: performance improvement, bail early if we don't have any changed content
|
||||
// if self.changed_content.is_empty() {
|
||||
// return vec![];
|
||||
// }
|
||||
|
||||
self.extract_candidates(scanned_blobs, css_files);
|
||||
self.scanned_files = files;
|
||||
let _new_candidates = self.extract_candidates();
|
||||
|
||||
// Return all candidates sorted
|
||||
let mut result = self.candidates.iter().cloned().collect::<Vec<_>>();
|
||||
result.par_sort_unstable();
|
||||
result
|
||||
// Make sure we have a sorted list of candidates
|
||||
let mut candidates = self.candidates.iter().cloned().collect::<Vec<_>>();
|
||||
candidates.par_sort_unstable();
|
||||
|
||||
// Return all candidates instead of only the new ones
|
||||
candidates
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
|
|
@ -146,7 +208,7 @@ impl Scanner {
|
|||
|
||||
// Raw content can be parsed directly, no need to verify if the file exists and is allowed
|
||||
// to be scanned.
|
||||
let mut content_to_scan: Vec<ChangedContent> = changed_contents;
|
||||
self.changed_content.extend(changed_contents);
|
||||
|
||||
// Fully resolve all files
|
||||
let changed_files = changed_files
|
||||
|
|
@ -170,7 +232,7 @@ impl Scanner {
|
|||
});
|
||||
|
||||
// All known files are allowed to be scanned
|
||||
content_to_scan.extend(known_files);
|
||||
self.changed_content.extend(known_files);
|
||||
|
||||
// Figure out if the new unknown files are allowed to be scanned
|
||||
if !new_unknown_files.is_empty() {
|
||||
|
|
@ -181,11 +243,6 @@ impl Scanner {
|
|||
continue;
|
||||
}
|
||||
|
||||
// The walked path can contain symlinks, while the changed files have already
|
||||
// been canonicalized. Lazily canonicalize the walked path so we can compare
|
||||
// the real paths as well.
|
||||
let mut canonical_path: Option<PathBuf> = None;
|
||||
|
||||
let mut drop_file_indexes = vec![];
|
||||
for (idx, changed_file) in new_unknown_files.iter().enumerate().rev() {
|
||||
let ChangedContent::File(file, _) = changed_file else {
|
||||
|
|
@ -194,17 +251,9 @@ impl Scanner {
|
|||
|
||||
// When the file is found on disk it means that all the rules pass. We can
|
||||
// extract the current file and remove it from the list of passed in files.
|
||||
let matches = file == path
|
||||
|| (file.file_name() == path.file_name() && {
|
||||
if canonical_path.is_none() {
|
||||
canonical_path = dunce::canonicalize(path).ok();
|
||||
}
|
||||
canonical_path.as_deref() == Some(file.as_path())
|
||||
});
|
||||
|
||||
if matches {
|
||||
self.files.insert(path.to_path_buf()); // Track for future use
|
||||
content_to_scan.push(changed_file.clone()); // Track for parsing
|
||||
if file == path {
|
||||
self.files.push(path.to_path_buf()); // Track for future use
|
||||
self.changed_content.push(changed_file.clone()); // Track for parsing
|
||||
drop_file_indexes.push(idx);
|
||||
}
|
||||
}
|
||||
|
|
@ -225,17 +274,18 @@ impl Scanner {
|
|||
}
|
||||
}
|
||||
|
||||
// Read all content into blobs for extraction
|
||||
let blobs = read_all_files(content_to_scan);
|
||||
self.extract_candidates(blobs, vec![])
|
||||
self.extract_candidates()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn extract_candidates(&mut self, blobs: Vec<Vec<u8>>, css_files: Vec<PathBuf>) -> Vec<String> {
|
||||
// Extract all candidates from the pre-read blobs
|
||||
let mut new_candidates = parse_all_blobs(blobs);
|
||||
fn extract_candidates(&mut self) -> Vec<String> {
|
||||
let changed_content = self.changed_content.drain(..).collect::<Vec<_>>();
|
||||
|
||||
// Extract all candidates from the changed content
|
||||
let mut new_candidates = parse_all_blobs(read_all_files(changed_content));
|
||||
|
||||
// Extract all CSS variables from the CSS files
|
||||
let css_files = self.css_files.drain(..).collect::<Vec<_>>();
|
||||
if !css_files.is_empty() {
|
||||
let css_variables = extract_css_variables(read_all_files(
|
||||
css_files
|
||||
|
|
@ -247,23 +297,63 @@ impl Scanner {
|
|||
new_candidates.extend(css_variables);
|
||||
}
|
||||
|
||||
// Only keep candidates we haven't seen before
|
||||
for existing in self.candidates.iter() {
|
||||
new_candidates.remove(existing);
|
||||
}
|
||||
// Only compute the new candidates and ignore the ones we already have. This is for
|
||||
// subsequent calls to prevent serializing the entire set of candidates every time.
|
||||
let mut new_candidates = new_candidates
|
||||
.into_par_iter()
|
||||
.filter(|candidate| !self.candidates.contains(candidate))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
new_candidates.par_sort_unstable();
|
||||
|
||||
// Track new candidates for subsequent calls
|
||||
self.candidates.extend(new_candidates.iter().cloned());
|
||||
self.candidates.par_extend(new_candidates.clone());
|
||||
|
||||
let mut result: Vec<String> = new_candidates.into_iter().collect();
|
||||
result.par_sort_unstable();
|
||||
new_candidates
|
||||
}
|
||||
|
||||
result
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn scan_sources(&mut self) {
|
||||
let Some(walker) = &mut self.walker else {
|
||||
return;
|
||||
};
|
||||
|
||||
for entry in walker.build().filter_map(Result::ok) {
|
||||
let path = entry.into_path();
|
||||
let Ok(metadata) = path.metadata() else {
|
||||
continue;
|
||||
};
|
||||
if metadata.is_dir() {
|
||||
self.dirs.push(path);
|
||||
} else if metadata.is_file() {
|
||||
let extension = path
|
||||
.extension()
|
||||
.and_then(|x| x.to_str())
|
||||
.unwrap_or_default(); // In case the file has no extension
|
||||
|
||||
match extension {
|
||||
// Special handing for CSS files, we don't want to extract candidates from
|
||||
// these files, but we do want to extract used CSS variables.
|
||||
"css" => {
|
||||
self.css_files.push(path.clone());
|
||||
}
|
||||
_ => {
|
||||
self.changed_content.push(ChangedContent::File(
|
||||
path.to_path_buf(),
|
||||
extension.to_owned(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
self.extensions.insert(extension.to_owned());
|
||||
self.files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn get_files(&mut self) -> Vec<String> {
|
||||
let _ = self.discover_sources();
|
||||
self.scan_sources();
|
||||
|
||||
self.files
|
||||
.par_iter()
|
||||
|
|
@ -271,18 +361,13 @@ impl Scanner {
|
|||
.collect()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn get_scanned_files(&self) -> Vec<String> {
|
||||
self.scanned_files.clone()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn get_globs(&mut self) -> Vec<GlobEntry> {
|
||||
if let Some(globs) = &self.globs {
|
||||
return globs.clone();
|
||||
}
|
||||
|
||||
let _ = self.discover_sources();
|
||||
self.scan_sources();
|
||||
|
||||
let mut globs = vec![];
|
||||
for source in self.sources.iter() {
|
||||
|
|
@ -356,167 +441,21 @@ impl Scanner {
|
|||
let i = s.as_ptr() as usize - offset;
|
||||
let original = &original_content[i..i + s.len()];
|
||||
if original.contains_str("-[]") {
|
||||
return String::from_utf8(original.to_vec())
|
||||
.ok()
|
||||
.map(|candidate| (candidate, i));
|
||||
return Some(unsafe {
|
||||
(String::from_utf8_unchecked(original.to_vec()), i)
|
||||
});
|
||||
}
|
||||
|
||||
String::from_utf8(s.to_vec())
|
||||
.ok()
|
||||
.map(|candidate| (candidate, i))
|
||||
// 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.
|
||||
Some(unsafe { (String::from_utf8_unchecked(s.to_vec()), i) })
|
||||
}
|
||||
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn discover_sources(&mut self) -> (Vec<Vec<u8>>, Vec<PathBuf>, Vec<String>) {
|
||||
if self.sources_scanned {
|
||||
return (vec![], vec![], vec![]);
|
||||
}
|
||||
self.sources_scanned = true;
|
||||
|
||||
let Some(walker) = &mut self.walker else {
|
||||
return (vec![], vec![], vec![]);
|
||||
};
|
||||
|
||||
// Use synchronous walk for the initial build (lower overhead) and parallel
|
||||
// walk for subsequent calls (watch mode) where the overhead is amortised.
|
||||
let all_entries = if self.has_scanned_once {
|
||||
walk_parallel(walker)
|
||||
} else {
|
||||
walk_synchronous(walker)
|
||||
};
|
||||
|
||||
let mut css_files: Vec<PathBuf> = vec![];
|
||||
let mut content_paths: Vec<(PathBuf, String)> = vec![];
|
||||
let mut changed_files = vec![];
|
||||
|
||||
// Fresh state
|
||||
self.files.clear();
|
||||
self.dirs.clear();
|
||||
self.extensions.clear();
|
||||
self.globs = None;
|
||||
|
||||
// Cache canonicalized folders in case a file itself is not symlinked, but any of the parent
|
||||
// folders are symlinked.
|
||||
let mut cached_canonical_dirs: FxHashMap<PathBuf, PathBuf> = FxHashMap::default();
|
||||
|
||||
for entry in all_entries {
|
||||
match entry {
|
||||
WalkEntry::Dir(path) => {
|
||||
self.dirs.insert(path);
|
||||
}
|
||||
WalkEntry::File {
|
||||
path,
|
||||
mtime,
|
||||
is_symlink,
|
||||
} => {
|
||||
// Deduplicate: parallel walk can visit the same file from multiple threads
|
||||
if !self.files.insert(path.clone()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Track canonicalized paths in addition to potentially symlinked file paths
|
||||
let canonical = if is_symlink {
|
||||
dunce::canonicalize(&path).ok()
|
||||
} else {
|
||||
path.parent().and_then(|parent| {
|
||||
// Perf: cache the canonicalized parent path such that sibling files don't
|
||||
// have to canonicalize over and over again.
|
||||
let canonical_parent = cached_canonical_dirs
|
||||
.entry(parent.to_path_buf())
|
||||
.or_insert_with(|| {
|
||||
dunce::canonicalize(parent)
|
||||
.unwrap_or_else(|_| parent.to_path_buf())
|
||||
});
|
||||
|
||||
if canonical_parent.as_path() != parent {
|
||||
path.file_name()
|
||||
.map(|file_name| canonical_parent.join(file_name))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
if let Some(canonical) = canonical {
|
||||
if canonical != path {
|
||||
self.files.insert(canonical);
|
||||
}
|
||||
}
|
||||
let extension = path
|
||||
.extension()
|
||||
.and_then(|x| x.to_str())
|
||||
.unwrap_or_default()
|
||||
.to_owned();
|
||||
|
||||
self.extensions.insert(extension.to_owned());
|
||||
|
||||
// On incremental scans, check mtime to skip unchanged files.
|
||||
// On the first scan, track mtimes while still scanning every file.
|
||||
let changed = if self.has_scanned_once {
|
||||
match mtime {
|
||||
Some(mtime) => {
|
||||
let prev = self.mtimes.insert(path.clone(), mtime);
|
||||
prev.is_none_or(|prev| prev != mtime)
|
||||
}
|
||||
None => true,
|
||||
}
|
||||
} else {
|
||||
if let Some(mtime) = mtime {
|
||||
self.mtimes.insert(path.clone(), mtime);
|
||||
}
|
||||
|
||||
true
|
||||
};
|
||||
|
||||
if !changed {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Ok(file) = path.clone().into_os_string().into_string() {
|
||||
changed_files.push(file);
|
||||
}
|
||||
|
||||
match extension.as_str() {
|
||||
// Special handing for CSS files, we don't want to extract candidates from
|
||||
// these files, but we do want to extract used CSS variables.
|
||||
"css" => css_files.push(path),
|
||||
_ => content_paths.push((path, extension)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure `mtimes` don't include stale files
|
||||
self.mtimes.retain(|path, _| self.files.contains(path));
|
||||
|
||||
// Read + preprocess all discovered files in parallel
|
||||
let scanned_blobs: Vec<Vec<u8>> = content_paths
|
||||
.into_par_iter()
|
||||
.filter_map(|(path, ext)| {
|
||||
let content = std::fs::read(&path).ok()?;
|
||||
event!(tracing::Level::INFO, "Reading {:?}", path);
|
||||
let processed = pre_process_input(content, &ext);
|
||||
if processed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(processed)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !self.has_scanned_once {
|
||||
self.has_scanned_once = true;
|
||||
}
|
||||
|
||||
changed_files.par_sort_unstable();
|
||||
|
||||
(scanned_blobs, css_files, changed_files)
|
||||
}
|
||||
}
|
||||
|
||||
fn read_changed_content(c: ChangedContent) -> Option<Vec<u8>> {
|
||||
|
|
@ -535,28 +474,26 @@ fn read_changed_content(c: ChangedContent) -> Option<Vec<u8>> {
|
|||
ChangedContent::Content(contents, extension) => (contents.into_bytes(), extension),
|
||||
};
|
||||
|
||||
Some(pre_process_input(content, &extension))
|
||||
Some(pre_process_input(&content, &extension))
|
||||
}
|
||||
|
||||
pub fn pre_process_input(content: Vec<u8>, extension: &str) -> Vec<u8> {
|
||||
pub fn pre_process_input(content: &[u8], extension: &str) -> Vec<u8> {
|
||||
use crate::extractor::pre_processors::*;
|
||||
|
||||
match extension {
|
||||
"clj" | "cljs" | "cljc" => Clojure.process(&content),
|
||||
"heex" | "eex" | "ex" | "exs" => Elixir.process(&content),
|
||||
"cshtml" | "razor" => Razor.process(&content),
|
||||
"haml" => Haml.process(&content),
|
||||
"json" | "jsonl" | "ndjson" => Json.process(&content),
|
||||
"md" | "mdx" => Markdown.process(&content),
|
||||
"pug" => Pug.process(&content),
|
||||
"rb" | "erb" => Ruby.process(&content),
|
||||
"slim" | "slang" => Slim.process(&content),
|
||||
"svelte" => Svelte.process(&content),
|
||||
"rs" => Rust.process(&content),
|
||||
"tt" | "tt2" | "tx" => TemplateToolkit.process(&content),
|
||||
"twig" => Twig.process(&content),
|
||||
"vue" => Vue.process(&content),
|
||||
_ => content,
|
||||
"clj" | "cljs" | "cljc" => Clojure.process(content),
|
||||
"heex" | "eex" | "ex" | "exs" => Elixir.process(content),
|
||||
"cshtml" | "razor" => Razor.process(content),
|
||||
"haml" => Haml.process(content),
|
||||
"json" => Json.process(content),
|
||||
"md" | "mdx" => Markdown.process(content),
|
||||
"pug" => Pug.process(content),
|
||||
"rb" | "erb" => Ruby.process(content),
|
||||
"slim" | "slang" => Slim.process(content),
|
||||
"svelte" => Svelte.process(content),
|
||||
"rs" => Rust.process(content),
|
||||
"vue" => Vue.process(content),
|
||||
_ => content.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -575,23 +512,23 @@ fn read_all_files(changed_content: Vec<ChangedContent>) -> Vec<Vec<u8>> {
|
|||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn extract_css_variables(blobs: Vec<Vec<u8>>) -> FxHashSet<String> {
|
||||
fn extract_css_variables(blobs: Vec<Vec<u8>>) -> Vec<String> {
|
||||
extract(blobs, |mut extractor| {
|
||||
extractor.extract_variables_from_css()
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn parse_all_blobs(blobs: Vec<Vec<u8>>) -> FxHashSet<String> {
|
||||
fn parse_all_blobs(blobs: Vec<Vec<u8>>) -> Vec<String> {
|
||||
extract(blobs, |mut extractor| extractor.extract())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn extract<H>(blobs: Vec<Vec<u8>>, handle: H) -> FxHashSet<String>
|
||||
fn extract<H>(blobs: Vec<Vec<u8>>, handle: H) -> Vec<String>
|
||||
where
|
||||
H: Fn(Extractor) -> Vec<Extracted> + std::marker::Sync,
|
||||
{
|
||||
blobs
|
||||
let mut result: Vec<_> = blobs
|
||||
.par_iter()
|
||||
.flat_map(|blob| blob.par_split(|x| *x == b'\n'))
|
||||
.filter_map(|blob| {
|
||||
|
|
@ -616,113 +553,26 @@ where
|
|||
a
|
||||
})
|
||||
.into_iter()
|
||||
.filter_map(|s| String::from_utf8(s.to_vec()).ok())
|
||||
.collect()
|
||||
.map(|s| unsafe { String::from_utf8_unchecked(s.to_vec()) })
|
||||
.collect();
|
||||
|
||||
// SAFETY: Unstable sort is faster and in this scenario it's also safe because we are
|
||||
// guaranteed to have unique candidates.
|
||||
result.par_sort_unstable();
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum WalkEntry {
|
||||
Dir(PathBuf),
|
||||
File {
|
||||
path: PathBuf,
|
||||
mtime: Option<SystemTime>,
|
||||
|
||||
/// Whether the path itself is a symlink
|
||||
is_symlink: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<ignore::DirEntry> for WalkEntry {
|
||||
fn from(entry: ignore::DirEntry) -> Self {
|
||||
let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
|
||||
let is_symlink = entry.path_is_symlink();
|
||||
let path = entry.into_path();
|
||||
|
||||
if is_dir {
|
||||
WalkEntry::Dir(path)
|
||||
} else {
|
||||
let mtime = path.metadata().ok().and_then(|m| m.modified().ok());
|
||||
WalkEntry::File {
|
||||
path,
|
||||
mtime,
|
||||
is_symlink,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Walk the file system synchronously. Used for the initial build where the overhead of spawning
|
||||
/// parallel walker threads is not worth it.
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn walk_synchronous(walker: &mut WalkBuilder) -> Vec<WalkEntry> {
|
||||
walker
|
||||
.build()
|
||||
.filter_map(Result::ok)
|
||||
.map(WalkEntry::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Walk the file system in parallel. Used in watch mode where the parallel walker overhead is
|
||||
/// amortised across many rebuilds and subsequent calls are much faster.
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn walk_parallel(walker: &mut WalkBuilder) -> Vec<WalkEntry> {
|
||||
struct FlushOnDrop {
|
||||
local: Vec<WalkEntry>,
|
||||
shared: Arc<Mutex<Vec<WalkEntry>>>,
|
||||
}
|
||||
|
||||
impl Drop for FlushOnDrop {
|
||||
fn drop(&mut self) {
|
||||
if !self.local.is_empty() {
|
||||
self.shared.lock().unwrap().append(&mut self.local);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let collected: Arc<Mutex<Vec<WalkEntry>>> = Arc::new(Mutex::new(vec![]));
|
||||
|
||||
walker.build_parallel().run(|| {
|
||||
let mut buf = FlushOnDrop {
|
||||
local: Vec::with_capacity(256),
|
||||
shared: collected.clone(),
|
||||
};
|
||||
|
||||
Box::new(move |entry| {
|
||||
let Ok(entry) = entry else {
|
||||
return ignore::WalkState::Continue;
|
||||
};
|
||||
|
||||
buf.local.push(WalkEntry::from(entry));
|
||||
|
||||
if buf.local.len() >= 256 {
|
||||
buf.shared.lock().unwrap().append(&mut buf.local);
|
||||
}
|
||||
|
||||
ignore::WalkState::Continue
|
||||
})
|
||||
});
|
||||
|
||||
// All threads have finished and flushed their buffers via FlushOnDrop::drop
|
||||
Arc::try_unwrap(collected).unwrap().into_inner().unwrap()
|
||||
}
|
||||
|
||||
/// Sets up a WalkBuilder with all source roots, gitignore rules, and source pattern matching.
|
||||
/// Create a walker for the given sources to detect all the files that we have to scan.
|
||||
///
|
||||
/// This is the common setup shared between the full walker (with mtime tracking for re-scans)
|
||||
/// and the parallel walker (without mtime tracking for the initial scan).
|
||||
fn create_walker(sources: &Sources) -> Option<WalkBuilder> {
|
||||
/// The `mtimes` map is used to keep track of the last modified time of each file. This is used to
|
||||
/// determine if a file or folder has changed since the last scan and we can skip folders that
|
||||
/// haven't changed.
|
||||
fn create_walker(sources: Sources) -> Option<WalkBuilder> {
|
||||
let mtimes: Arc<Mutex<FxHashMap<PathBuf, SystemTime>>> = Default::default();
|
||||
let mut other_roots: FxHashSet<&PathBuf> = FxHashSet::default();
|
||||
let mut first_root: Option<&PathBuf> = None;
|
||||
|
||||
let mut ignores: Vec<(&PathBuf, Vec<String>)> = Default::default();
|
||||
let mut emit = |base, pattern| match ignores.last_mut() {
|
||||
Some((prev_base, patterns)) if *prev_base == base => {
|
||||
patterns.push(pattern);
|
||||
}
|
||||
_ => {
|
||||
ignores.push((base, vec![pattern]));
|
||||
}
|
||||
};
|
||||
let mut ignores: BTreeMap<&PathBuf, BTreeSet<String>> = Default::default();
|
||||
|
||||
for source in sources.iter() {
|
||||
match source {
|
||||
|
|
@ -734,7 +584,7 @@ fn create_walker(sources: &Sources) -> Option<WalkBuilder> {
|
|||
}
|
||||
}
|
||||
SourceEntry::Pattern { base, pattern } => {
|
||||
let pattern = pattern.to_owned();
|
||||
let mut pattern = pattern.to_string();
|
||||
|
||||
if first_root.is_none() {
|
||||
first_root = Some(base);
|
||||
|
|
@ -743,19 +593,35 @@ fn create_walker(sources: &Sources) -> Option<WalkBuilder> {
|
|||
}
|
||||
|
||||
if !pattern.contains("**") {
|
||||
// Ensure that the pattern is pinned to the base path.
|
||||
if !pattern.starts_with("/") {
|
||||
pattern = format!("/{pattern}");
|
||||
}
|
||||
|
||||
// Specific patterns should take precedence even over git-ignored files:
|
||||
emit(base, format!("!{}", pattern));
|
||||
ignores
|
||||
.entry(base)
|
||||
.or_default()
|
||||
.insert(format!("!{}", pattern));
|
||||
} else {
|
||||
// Assumption: the pattern we receive will already be brace expanded. So
|
||||
// `*.{html,jsx}` will result in two separate patterns: `*.html` and `*.jsx`.
|
||||
if let Some(extension) = Path::new(&pattern).extension() {
|
||||
// Extend auto source detection to include the extension
|
||||
emit(base, format!("!*.{}", extension.to_string_lossy()));
|
||||
ignores
|
||||
.entry(base)
|
||||
.or_default()
|
||||
.insert(format!("!*.{}", extension.to_string_lossy()));
|
||||
}
|
||||
}
|
||||
}
|
||||
SourceEntry::Ignored { base, pattern } => {
|
||||
emit(base, pattern.to_owned());
|
||||
let mut pattern = pattern.to_string();
|
||||
// Ensure that the pattern is pinned to the base path.
|
||||
if !pattern.starts_with("/") {
|
||||
pattern = format!("/{pattern}");
|
||||
}
|
||||
ignores.entry(base).or_default().insert(pattern);
|
||||
}
|
||||
SourceEntry::External { base } => {
|
||||
if first_root.is_none() {
|
||||
|
|
@ -765,10 +631,16 @@ fn create_walker(sources: &Sources) -> Option<WalkBuilder> {
|
|||
}
|
||||
|
||||
// External sources should take precedence even over git-ignored files:
|
||||
emit(base, "!/**/*".to_owned());
|
||||
ignores
|
||||
.entry(base)
|
||||
.or_default()
|
||||
.insert(format!("!{}", "/**/*"));
|
||||
|
||||
// External sources should still disallow binary extensions:
|
||||
emit(base, BINARY_EXTENSIONS_GLOB.clone());
|
||||
ignores
|
||||
.entry(base)
|
||||
.or_default()
|
||||
.insert(BINARY_EXTENSIONS_GLOB.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -845,61 +717,75 @@ fn create_walker(sources: &Sources) -> Option<WalkBuilder> {
|
|||
builder.add_gitignore(ignore);
|
||||
}
|
||||
|
||||
// Pre-compute source matching data to avoid allocations in the hot filter_entry path
|
||||
let auto_bases: Vec<PathBuf> = sources
|
||||
.iter()
|
||||
.filter_map(|source| match source {
|
||||
SourceEntry::Auto { base } | SourceEntry::External { base } => Some(base.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
builder.filter_entry({
|
||||
move |entry| {
|
||||
let path = entry.path();
|
||||
|
||||
let pattern_sources: Vec<(PathBuf, String)> = sources
|
||||
.iter()
|
||||
.filter_map(|source| match source {
|
||||
SourceEntry::Pattern { base, pattern } => Some((base.into(), pattern.into())),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Source pattern matching filter (lock-free, safe for parallel walking)
|
||||
builder.filter_entry(move |entry| {
|
||||
let path = entry.path();
|
||||
|
||||
// Ensure the entries are matching any of the provided source patterns (this is
|
||||
// necessary for manual-patterns that can filter the file extension)
|
||||
if path.is_file() {
|
||||
let mut matches = false;
|
||||
|
||||
for base in &auto_bases {
|
||||
if path.starts_with(base) {
|
||||
matches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !matches {
|
||||
for (base, pattern) in &pattern_sources {
|
||||
let remainder = path.strip_prefix(base);
|
||||
if remainder.is_ok_and(|remainder| {
|
||||
let mut path_str = remainder.to_string_lossy().to_string();
|
||||
if !path_str.starts_with("/") {
|
||||
path_str = format!("/{path_str}");
|
||||
// Ensure the entries are matching any of the provided source patterns (this is
|
||||
// necessary for manual-patterns that can filter the file extension)
|
||||
if path.is_file() {
|
||||
let mut matches = false;
|
||||
for source in sources.iter() {
|
||||
match source {
|
||||
SourceEntry::Auto { base } | SourceEntry::External { base } => {
|
||||
if path.starts_with(base) {
|
||||
matches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
glob_match(pattern, path_str.as_bytes())
|
||||
}) {
|
||||
matches = true;
|
||||
break;
|
||||
SourceEntry::Pattern { base, pattern } => {
|
||||
let mut pattern = pattern.to_string();
|
||||
// Ensure that the pattern is pinned to the base path.
|
||||
if !pattern.starts_with("/") {
|
||||
pattern = format!("/{pattern}");
|
||||
}
|
||||
|
||||
// Check if path starts with base, if so, remove the prefix and check the remainder against the pattern
|
||||
let remainder = path.strip_prefix(base);
|
||||
if remainder.is_ok_and(|remainder| {
|
||||
let mut path_str = remainder.to_string_lossy().to_string();
|
||||
if !path_str.starts_with("/") {
|
||||
path_str = format!("/{path_str}");
|
||||
}
|
||||
glob_match(pattern, path_str.as_bytes())
|
||||
}) {
|
||||
matches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if !matches {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if !matches {
|
||||
return false;
|
||||
let mut mtimes = mtimes.lock().unwrap();
|
||||
let current_time = match entry.metadata() {
|
||||
Ok(metadata) if metadata.is_file() => {
|
||||
if let Ok(time) = metadata.modified() {
|
||||
Some(time)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let previous_time =
|
||||
current_time.and_then(|time| mtimes.insert(entry.clone().into_path(), time));
|
||||
|
||||
match (current_time, previous_time) {
|
||||
(Some(current), Some(prev)) if prev == current => false,
|
||||
_ => {
|
||||
event!(tracing::Level::INFO, "Discovering {:?}", path);
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
});
|
||||
|
||||
Some(builder)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
use crate::glob::split_pattern;
|
||||
use crate::GlobEntry;
|
||||
use bexpand::Expression;
|
||||
use fxhash::{FxHashMap, FxHashSet};
|
||||
use ignore::gitignore::Gitignore;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
use tracing::{event, Level};
|
||||
|
||||
use super::auto_source_detection::IGNORED_CONTENT_DIRS;
|
||||
|
|
@ -50,14 +49,13 @@ pub enum SourceEntry {
|
|||
/// ```
|
||||
Ignored { base: PathBuf, pattern: String },
|
||||
|
||||
/// External sources are directories that are ignored (by us or .gitignore rules), but should be
|
||||
/// included bypassing the default ignore rules.
|
||||
/// External sources are sources outside of your git root which should not
|
||||
/// follow gitignore rules.
|
||||
///
|
||||
/// Represented by:
|
||||
///
|
||||
/// ```css
|
||||
/// @source "../node_modules/my-lib";`
|
||||
/// @source "../node_modules/my-lib/**/*";`
|
||||
/// ```
|
||||
External { base: PathBuf },
|
||||
}
|
||||
|
|
@ -77,164 +75,6 @@ impl Sources {
|
|||
}
|
||||
}
|
||||
|
||||
/// When dealing with a pattern, then it could be that we end up with:
|
||||
///
|
||||
/// ```json
|
||||
/// { base: '/some/folder', pattern: 'foo.ts' }
|
||||
/// ```
|
||||
///
|
||||
/// If we just emit `!foo.ts` for the `/some/folder` path, then _everything_ else in that folder
|
||||
/// would still be walked (but the result will be ignored).
|
||||
///
|
||||
/// Instead, we have to ensure that we ignore everything in that folder _except_ for the `foo.ts`
|
||||
/// pattern.
|
||||
///
|
||||
/// This should be equivalent to:
|
||||
/// ```gitignore
|
||||
/// *
|
||||
/// !foo.ts
|
||||
/// ```
|
||||
///
|
||||
/// However, we have to be careful that we don't start ignoring files/folders that already exist.
|
||||
/// ```css
|
||||
/// @source "./some/folder/foo.ts";
|
||||
/// @source "./some/folder/bar.ts";
|
||||
/// ```
|
||||
/// Would result in:
|
||||
/// ```json
|
||||
/// { base: '/some/folder', pattern: 'foo.ts' }
|
||||
/// { base: '/some/folder', pattern: 'bar.ts' }
|
||||
/// ```
|
||||
///
|
||||
/// If we were to blindly emit `*` for each pattern, then the `.gitignore` equivalent would look like
|
||||
/// this:
|
||||
/// ```gitignore
|
||||
/// *
|
||||
/// !foo.ts
|
||||
/// *
|
||||
/// !bar.ts
|
||||
/// ```
|
||||
///
|
||||
/// This would result in ignoring the `foo.ts` file as well. Therefore we only want to insert
|
||||
/// this `*` pattern when nothing else exists yet.
|
||||
///
|
||||
/// There is another problem that we need to solve. Let's say you have a pattern that contains a `*`
|
||||
/// in the pattern:
|
||||
/// ```css
|
||||
/// @source './src/ba*/*.html';
|
||||
/// ```
|
||||
///
|
||||
/// This would result in
|
||||
/// ```json
|
||||
/// { base: '/src', pattern: '/ba*/*.html' }
|
||||
/// ```
|
||||
///
|
||||
/// If we now inject the `*` pattern for the `/src` folder, then we wouldn't scan any `ba*` folders
|
||||
/// (e.g. `bar` or `baz`). For this, we have to make sure that we add inverse patterns for these
|
||||
/// folders. This would essentially result in:
|
||||
/// ```gitignore
|
||||
/// * ← ignore everything
|
||||
/// !/ba*/ ← except for the `ba*/` pattern, so we scan these folders
|
||||
/// !/ba*/*.html ← then ensure we scan the `*.html` files in it as well
|
||||
/// ```
|
||||
///
|
||||
fn expand_restricted_patterns(sources: Vec<SourceEntry>) -> Vec<SourceEntry> {
|
||||
let unrestricted_roots = sources
|
||||
.iter()
|
||||
.filter_map(|source| match source {
|
||||
SourceEntry::Auto { base } | SourceEntry::External { base } => Some(base.clone()),
|
||||
SourceEntry::Pattern { base, pattern } if pattern.contains("**") => Some(base.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Bases of restricted patterns. Each of these becomes its own walk root with its own
|
||||
// `*` + `!<pattern>` rules, so an ancestor base must not ignore them recursively.
|
||||
let pattern_roots = sources
|
||||
.iter()
|
||||
.filter_map(|source| match source {
|
||||
SourceEntry::Pattern { base, .. } => Some(base.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut restricted_roots: FxHashSet<PathBuf> = FxHashSet::default();
|
||||
let mut expanded = vec![];
|
||||
|
||||
for source in sources {
|
||||
let SourceEntry::Pattern { base, pattern } = &source else {
|
||||
expanded.push(source);
|
||||
continue;
|
||||
};
|
||||
|
||||
// `base` is already included by another `@source` that we know should be walked. This
|
||||
// includes the case where `base` is _nested_ inside such a root, because everything under
|
||||
// an unrestricted root is walked already. Restricting it would incorrectly hide siblings
|
||||
// that the broader source is supposed to pick up.
|
||||
if unrestricted_roots.iter().any(|root| base.starts_with(root)) {
|
||||
expanded.push(source);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore everything in the directory. We will later add the specific patterns we are
|
||||
// interested in.
|
||||
if restricted_roots.insert(base.clone()) {
|
||||
// When another source root is nested inside this base — an unrestricted root, or the
|
||||
// base of another restricted pattern (which is walked from its own root with its own
|
||||
// rules) — only ignore direct children so the nested root can still be walked.
|
||||
let has_nested_root = unrestricted_roots.iter().any(|root| root.starts_with(base))
|
||||
|| pattern_roots
|
||||
.iter()
|
||||
.any(|root| root != base && root.starts_with(base));
|
||||
|
||||
let pattern = if has_nested_root { "/*" } else { "*" };
|
||||
|
||||
expanded.push(SourceEntry::Ignored {
|
||||
base: base.clone(),
|
||||
pattern: pattern.to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure to _include_ parent paths, otherwise the `*` from above would block walking the
|
||||
// folders that need to be walked.
|
||||
//
|
||||
// ```css
|
||||
// @source './src/ba*/*.html';
|
||||
// ```
|
||||
//
|
||||
// ```gitignore
|
||||
// * ← added by the above rule
|
||||
// !/ba*/ ← this is what we're focusing on in this block
|
||||
// !/ba*/*.html ← this is added later
|
||||
// ```
|
||||
{
|
||||
let mut dir = PathBuf::new();
|
||||
let mut components = Path::new(pattern).components().peekable();
|
||||
|
||||
while let Some(component) = components.next() {
|
||||
if components.peek().is_none() {
|
||||
break;
|
||||
}
|
||||
|
||||
match component {
|
||||
Component::Prefix(_) | Component::RootDir | Component::CurDir => continue,
|
||||
Component::ParentDir | Component::Normal(_) => dir.push(component),
|
||||
}
|
||||
|
||||
expanded.push(SourceEntry::Ignored {
|
||||
base: base.clone(),
|
||||
pattern: format!("!/{}/", path_to_posix_string(&dir).trim_start_matches('/')),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Track the original source
|
||||
expanded.push(source);
|
||||
}
|
||||
|
||||
expanded
|
||||
}
|
||||
|
||||
impl PublicSourceEntry {
|
||||
/// Optimize the PublicSourceEntry by trying to move all the static parts of the pattern to the
|
||||
/// base of the PublicSourceEntry.
|
||||
|
|
@ -262,456 +102,72 @@ impl PublicSourceEntry {
|
|||
/// resolved path.
|
||||
pub fn optimize(&mut self) {
|
||||
// Resolve base path immediately
|
||||
let Ok(mut base) = dunce::canonicalize(&self.base) else {
|
||||
let Ok(base) = dunce::canonicalize(&self.base) else {
|
||||
event!(Level::ERROR, "Failed to resolve base: {:?}", self.base);
|
||||
return;
|
||||
};
|
||||
self.base = base.to_string_lossy().to_string();
|
||||
|
||||
let mut new_pattern = PathBuf::new();
|
||||
enum ComponentStage {
|
||||
Base,
|
||||
Pattern,
|
||||
// No dynamic part, figure out if we are dealing with a file or a directory.
|
||||
if !self.pattern.contains('*') {
|
||||
let combined_path = if self.pattern.starts_with("/") {
|
||||
PathBuf::from(&self.pattern)
|
||||
} else {
|
||||
PathBuf::from(&self.base).join(&self.pattern)
|
||||
};
|
||||
|
||||
match dunce::canonicalize(combined_path) {
|
||||
Ok(resolved_path) if resolved_path.is_dir() => {
|
||||
self.base = resolved_path.to_string_lossy().to_string();
|
||||
self.pattern = "**/*".to_owned();
|
||||
}
|
||||
Ok(resolved_path) if resolved_path.is_file() => {
|
||||
self.base = resolved_path
|
||||
.parent()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
// Ensure leading slash, otherwise it will match against all files in all folders/
|
||||
self.pattern =
|
||||
format!("/{}", resolved_path.file_name().unwrap().to_string_lossy());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
let mut stage = ComponentStage::Base;
|
||||
|
||||
let mut components = Path::new(&self.pattern).components().peekable();
|
||||
while let Some(component) = components.next() {
|
||||
match stage {
|
||||
ComponentStage::Base => {
|
||||
match component {
|
||||
// Ignore the current dir, e.g. `.`
|
||||
Component::CurDir => {}
|
||||
// Contains dynamic part
|
||||
let (static_part, dynamic_part) = split_pattern(&self.pattern);
|
||||
|
||||
// Go up a directory, e.g. `..`
|
||||
Component::ParentDir => {
|
||||
base.pop();
|
||||
}
|
||||
|
||||
// Once we hit a component that contains a wildcard character, then we
|
||||
// can't change the base anymore and we must move to the pattern part.
|
||||
Component::Normal(part) if part.to_string_lossy().contains("*") => {
|
||||
new_pattern.push(component);
|
||||
stage = ComponentStage::Pattern;
|
||||
}
|
||||
|
||||
// File or folder, but not the last component
|
||||
Component::Normal(part) if components.peek().is_some() => {
|
||||
base.push(part);
|
||||
}
|
||||
|
||||
// Last file or folder. If it's a folder, we move it to the base,
|
||||
// otherwise we move it to the pattern.
|
||||
Component::Normal(part) => {
|
||||
let full_path = base.join(part);
|
||||
if full_path.is_dir() {
|
||||
base.push(part);
|
||||
} else {
|
||||
new_pattern.push(part);
|
||||
}
|
||||
}
|
||||
|
||||
// When we're dealing with an absolute path, then we have to bypass the
|
||||
// `base` entirely.
|
||||
Component::Prefix(_) => {
|
||||
base.clear();
|
||||
base.push(component);
|
||||
}
|
||||
Component::RootDir => {
|
||||
#[cfg(not(windows))]
|
||||
base.clear();
|
||||
base.push(component);
|
||||
}
|
||||
let base: PathBuf = self.base.clone().into();
|
||||
let base = match static_part {
|
||||
Some(static_part) => {
|
||||
// TODO: If the base does not exist on disk, try removing the last slash and try
|
||||
// again.
|
||||
match dunce::canonicalize(base.join(static_part)) {
|
||||
Ok(base) => base,
|
||||
Err(err) => {
|
||||
event!(tracing::Level::ERROR, "Failed to resolve glob: {:?}", err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
ComponentStage::Pattern => {
|
||||
new_pattern.push(component);
|
||||
}
|
||||
None => base,
|
||||
};
|
||||
|
||||
let pattern = match dynamic_part {
|
||||
Some(dynamic_part) => dynamic_part,
|
||||
None => {
|
||||
if base.is_dir() {
|
||||
"**/*".to_owned()
|
||||
} else {
|
||||
"".to_owned()
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.base = base.to_string_lossy().to_string();
|
||||
self.pattern = path_to_posix_string(&new_pattern);
|
||||
|
||||
// Ensure we have `**/*` when the base is a folder and we don't have a pattern at all
|
||||
if self.pattern == "" {
|
||||
self.pattern = "/**/*".to_owned();
|
||||
}
|
||||
// Ensure that the pattern is pinned to the base path.
|
||||
else if !self.pattern.starts_with("/") {
|
||||
self.pattern = format!("/{}", self.pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn path_to_posix_string(path: &Path) -> String {
|
||||
let mut parts = Vec::new();
|
||||
let mut is_rooted = false;
|
||||
|
||||
for component in path.components() {
|
||||
match component {
|
||||
Component::Prefix(prefix) => {
|
||||
parts.push(prefix.as_os_str().to_string_lossy().to_string());
|
||||
}
|
||||
Component::RootDir => {
|
||||
is_rooted = true;
|
||||
if parts.is_empty() {
|
||||
parts.push(String::new());
|
||||
}
|
||||
}
|
||||
Component::CurDir => {
|
||||
parts.push(".".to_string());
|
||||
}
|
||||
Component::ParentDir => {
|
||||
parts.push("..".to_string());
|
||||
}
|
||||
Component::Normal(part) => {
|
||||
parts.push(part.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = parts.join("/");
|
||||
if result.is_empty() && is_rooted {
|
||||
"/".to_string()
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn path_to_posix_string_serializes_relative_paths() {
|
||||
let path = PathBuf::from("src").join("**").join("*.html");
|
||||
|
||||
assert_eq!(path_to_posix_string(&path), "src/**/*.html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_to_posix_string_serializes_rooted_paths() {
|
||||
let path = PathBuf::from(std::path::MAIN_SEPARATOR.to_string())
|
||||
.join("src")
|
||||
.join("**")
|
||||
.join("*.html");
|
||||
|
||||
assert_eq!(path_to_posix_string(&path), "/src/**/*.html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_to_posix_string_serializes_empty_paths() {
|
||||
assert_eq!(path_to_posix_string(&PathBuf::new()), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optimize_hoists_static_directories_and_keeps_files_in_the_pattern() {
|
||||
let dir = tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join("src").join("examples")).unwrap();
|
||||
|
||||
let mut source = PublicSourceEntry {
|
||||
base: dir.path().to_string_lossy().to_string(),
|
||||
pattern: "src/examples/index.html".to_string(),
|
||||
negated: false,
|
||||
};
|
||||
|
||||
source.optimize();
|
||||
|
||||
assert_eq!(
|
||||
source.base,
|
||||
dunce::canonicalize(dir.path().join("src").join("examples"))
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
);
|
||||
assert_eq!(source.pattern, "/index.html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optimize_hoists_folder_patterns() {
|
||||
let dir = tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join("src").join("examples")).unwrap();
|
||||
|
||||
let mut source = PublicSourceEntry {
|
||||
base: dir.path().to_string_lossy().to_string(),
|
||||
pattern: "src/examples".to_string(),
|
||||
negated: false,
|
||||
};
|
||||
|
||||
source.optimize();
|
||||
|
||||
assert_eq!(
|
||||
source.base,
|
||||
dunce::canonicalize(dir.path().join("src").join("examples"))
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
);
|
||||
assert_eq!(source.pattern, "/**/*");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optimize_keeps_wildcards_in_the_pattern() {
|
||||
let dir = tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join("src")).unwrap();
|
||||
|
||||
let mut source = PublicSourceEntry {
|
||||
base: dir.path().to_string_lossy().to_string(),
|
||||
pattern: "src/**/*.html".to_string(),
|
||||
negated: false,
|
||||
};
|
||||
|
||||
source.optimize();
|
||||
|
||||
assert_eq!(
|
||||
source.base,
|
||||
dunce::canonicalize(dir.path().join("src"))
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
);
|
||||
assert_eq!(source.pattern, "/**/*.html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concrete_patterns_are_expanded_to_restrict_their_base() {
|
||||
let dir = tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join("src")).unwrap();
|
||||
let base = dunce::canonicalize(dir.path().join("src")).unwrap();
|
||||
|
||||
let sources = public_source_entries_to_private_source_entries(vec![PublicSourceEntry {
|
||||
base: dir.path().to_string_lossy().to_string(),
|
||||
pattern: "src/foo.html".to_string(),
|
||||
negated: false,
|
||||
}]);
|
||||
|
||||
assert_eq!(
|
||||
sources,
|
||||
vec![
|
||||
SourceEntry::Ignored {
|
||||
base: base.clone(),
|
||||
pattern: "*".to_string(),
|
||||
},
|
||||
SourceEntry::Pattern {
|
||||
base,
|
||||
pattern: "/foo.html".to_string(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restricted_patterns_include_parent_directory_allow_rules() {
|
||||
let dir = tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join("src")).unwrap();
|
||||
let base = dunce::canonicalize(dir.path().join("src")).unwrap();
|
||||
|
||||
let sources = public_source_entries_to_private_source_entries(vec![PublicSourceEntry {
|
||||
base: dir.path().to_string_lossy().to_string(),
|
||||
pattern: "src/ef*/*.html".to_string(),
|
||||
negated: false,
|
||||
}]);
|
||||
|
||||
assert_eq!(
|
||||
sources,
|
||||
vec![
|
||||
SourceEntry::Ignored {
|
||||
base: base.clone(),
|
||||
pattern: "*".to_string(),
|
||||
},
|
||||
SourceEntry::Ignored {
|
||||
base: base.clone(),
|
||||
pattern: "!/ef*/".to_string(),
|
||||
},
|
||||
SourceEntry::Pattern {
|
||||
base,
|
||||
pattern: "/ef*/*.html".to_string(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unrestricted_sources_do_not_expand_patterns_for_the_same_base() {
|
||||
let dir = tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join("src")).unwrap();
|
||||
let base = dunce::canonicalize(dir.path().join("src")).unwrap();
|
||||
|
||||
let sources = public_source_entries_to_private_source_entries(vec![
|
||||
PublicSourceEntry {
|
||||
base: dir.path().to_string_lossy().to_string(),
|
||||
pattern: "src".to_string(),
|
||||
negated: false,
|
||||
},
|
||||
PublicSourceEntry {
|
||||
base: dir.path().to_string_lossy().to_string(),
|
||||
pattern: "src/foo.html".to_string(),
|
||||
negated: false,
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
sources,
|
||||
vec![
|
||||
SourceEntry::Auto { base: base.clone() },
|
||||
SourceEntry::Pattern {
|
||||
base,
|
||||
pattern: "/foo.html".to_string(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restricted_parent_bases_do_not_open_unrelated_siblings() {
|
||||
let dir = tempdir().unwrap();
|
||||
let project = dir.path().join("Users").join("robin").join("docus-test");
|
||||
fs::create_dir_all(&project).unwrap();
|
||||
|
||||
let users = dunce::canonicalize(dir.path().join("Users")).unwrap();
|
||||
let project = dunce::canonicalize(project).unwrap();
|
||||
|
||||
let sources = public_source_entries_to_private_source_entries(vec![
|
||||
PublicSourceEntry {
|
||||
base: project.to_string_lossy().to_string(),
|
||||
pattern: "**/*".to_string(),
|
||||
negated: false,
|
||||
},
|
||||
PublicSourceEntry {
|
||||
base: project.to_string_lossy().to_string(),
|
||||
pattern: "../../app.config.ts".to_string(),
|
||||
negated: false,
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
sources,
|
||||
vec![
|
||||
SourceEntry::Auto {
|
||||
base: project.clone(),
|
||||
},
|
||||
SourceEntry::Ignored {
|
||||
base: users.clone(),
|
||||
pattern: "/*".to_string(),
|
||||
},
|
||||
SourceEntry::Pattern {
|
||||
base: users,
|
||||
pattern: "/app.config.ts".to_string(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restricted_patterns_preserve_source_order() {
|
||||
let dir = tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join("src")).unwrap();
|
||||
let base = dunce::canonicalize(dir.path().join("src")).unwrap();
|
||||
|
||||
let sources = public_source_entries_to_private_source_entries(vec![
|
||||
PublicSourceEntry {
|
||||
base: dir.path().to_string_lossy().to_string(),
|
||||
pattern: "src/foo.html".to_string(),
|
||||
negated: false,
|
||||
},
|
||||
PublicSourceEntry {
|
||||
base: dir.path().to_string_lossy().to_string(),
|
||||
pattern: "src/foo.html".to_string(),
|
||||
negated: true,
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
sources,
|
||||
vec![
|
||||
SourceEntry::Ignored {
|
||||
base: base.clone(),
|
||||
pattern: "*".to_string(),
|
||||
},
|
||||
SourceEntry::Pattern {
|
||||
base: base.clone(),
|
||||
pattern: "/foo.html".to_string(),
|
||||
},
|
||||
SourceEntry::Ignored {
|
||||
base,
|
||||
pattern: "/foo.html".to_string(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Run the public-to-private conversion for an auto-detected source pointing at `base` and
|
||||
/// return the resulting entry.
|
||||
fn auto_source_entry(base: &Path) -> SourceEntry {
|
||||
public_source_entries_to_private_source_entries(vec![PublicSourceEntry {
|
||||
base: base.to_string_lossy().to_string(),
|
||||
pattern: "**/*".to_string(),
|
||||
negated: false,
|
||||
}])
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_detected_folders_become_auto_sources() {
|
||||
let dir = tempdir().unwrap();
|
||||
let base = dir.path().join("src");
|
||||
fs::create_dir_all(&base).unwrap();
|
||||
let base = dunce::canonicalize(&base).unwrap();
|
||||
|
||||
assert_eq!(auto_source_entry(&base), SourceEntry::Auto { base });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn folders_ignored_by_default_become_external_sources() {
|
||||
let dir = tempdir().unwrap();
|
||||
let base = dir.path().join("node_modules").join("my-lib");
|
||||
fs::create_dir_all(&base).unwrap();
|
||||
let base = dunce::canonicalize(&base).unwrap();
|
||||
|
||||
assert_eq!(auto_source_entry(&base), SourceEntry::External { base });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn folders_ignored_by_gitignore_become_external_sources() {
|
||||
let dir = tempdir().unwrap();
|
||||
// Pretend this is a git repository so the `.gitignore` search is bounded to it.
|
||||
fs::create_dir_all(dir.path().join(".git")).unwrap();
|
||||
fs::write(dir.path().join(".gitignore"), "dist/\n").unwrap();
|
||||
|
||||
let base = dir.path().join("dist");
|
||||
fs::create_dir_all(&base).unwrap();
|
||||
let base = dunce::canonicalize(&base).unwrap();
|
||||
|
||||
assert_eq!(auto_source_entry(&base), SourceEntry::External { base });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn folders_ignored_by_a_parent_gitignore_become_external_sources() {
|
||||
let dir = tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join(".git")).unwrap();
|
||||
// A `.gitignore` higher up in the tree should still apply to nested directories.
|
||||
fs::write(dir.path().join(".gitignore"), "generated/\n").unwrap();
|
||||
|
||||
let base = dir.path().join("packages").join("app").join("generated");
|
||||
fs::create_dir_all(&base).unwrap();
|
||||
let base = dunce::canonicalize(&base).unwrap();
|
||||
|
||||
assert_eq!(auto_source_entry(&base), SourceEntry::External { base });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn folders_not_ignored_by_gitignore_stay_auto_sources() {
|
||||
let dir = tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join(".git")).unwrap();
|
||||
fs::write(dir.path().join(".gitignore"), "dist/\n").unwrap();
|
||||
|
||||
let base = dir.path().join("src");
|
||||
fs::create_dir_all(&base).unwrap();
|
||||
let base = dunce::canonicalize(&base).unwrap();
|
||||
|
||||
assert_eq!(auto_source_entry(&base), SourceEntry::Auto { base });
|
||||
self.pattern = pattern;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -762,111 +218,18 @@ pub fn public_source_entries_to_private_source_entries(
|
|||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Compiled `.gitignore` matchers are cached per directory so we read and parse each
|
||||
// `.gitignore` file at most once, even though entries commonly share ancestor directories (e.g.
|
||||
// the repository root). A cached `None` means the directory has no `.gitignore` file.
|
||||
let mut gitignores: FxHashMap<PathBuf, Option<Gitignore>> = FxHashMap::default();
|
||||
|
||||
// Boundary for the `.gitignore` walk when a source is not inside a git repository (see below).
|
||||
let cwd = std::env::current_dir()
|
||||
.map(|cwd| dunce::canonicalize(&cwd).unwrap_or(cwd))
|
||||
.ok();
|
||||
|
||||
// Convert from public SourceEntry to private SourceEntry
|
||||
let sources = expanded_globs
|
||||
expanded_globs
|
||||
.into_iter()
|
||||
.map(|public_source| {
|
||||
let mut source: SourceEntry = public_source.into();
|
||||
|
||||
// Promote auto-sources to external sources if they were gitignored
|
||||
if let SourceEntry::Auto { ref base } = source {
|
||||
let inside_git_repo = base.ancestors().any(|dir| dir.join(".git").exists());
|
||||
|
||||
// Walk up from the folder, applying each `.gitignore` relative to the directory
|
||||
// that contains it (matching git), and stop at the git repository root so
|
||||
// `.gitignore` files outside of the repo are not considered.
|
||||
for dir in base.ancestors() {
|
||||
let gitignore = gitignores.entry(dir.to_path_buf()).or_insert_with(|| {
|
||||
let path = dir.join(".gitignore");
|
||||
|
||||
// `Gitignore::new` roots the matcher at the directory containing the file,
|
||||
// so patterns match relative to it.
|
||||
path.is_file().then(|| Gitignore::new(&path).0)
|
||||
});
|
||||
|
||||
// Only `.gitignore` files in ancestors of `base` can ignore `base` itself.
|
||||
// Patterns in `base`'s own `.gitignore` only match paths _inside_ `base`, never
|
||||
// `base` itself (the file walker still applies them to `base`'s contents).
|
||||
//
|
||||
// Skipping `base`'s own `.gitignore` also prevents a false positive for
|
||||
// whitelist style `.gitignore` files, because relativizing `base` against
|
||||
// itself yields the empty path, which incorrectly matches `/*`.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// ```gitignore
|
||||
// /*
|
||||
// !/.gitignore
|
||||
// !/app
|
||||
// !/public
|
||||
// ```
|
||||
//
|
||||
// Everything inside `base` except `.gitignore`, `app` and `public` is ignored,
|
||||
// but `base` itself is not.
|
||||
if dir != base {
|
||||
if let Some(gitignore) = gitignore {
|
||||
if gitignore
|
||||
.matched_path_or_any_parents(&base, true)
|
||||
.is_ignore()
|
||||
{
|
||||
source = SourceEntry::External { base: base.into() };
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop at the git repository root.
|
||||
if dir.join(".git").exists() {
|
||||
break;
|
||||
}
|
||||
|
||||
// Without a git repository there is no repository root to stop at. Stop once
|
||||
// the directory contains the current working directory instead, so `.gitignore`
|
||||
// files outside of the project (e.g. in the user's home directory) can never
|
||||
// promote a source to an external source. Note that the file walker still
|
||||
// applies those `.gitignore` files when deciding which files to scan.
|
||||
if !inside_git_repo && cwd.as_ref().is_some_and(|cwd| cwd.starts_with(dir)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
source
|
||||
})
|
||||
.collect::<Vec<SourceEntry>>();
|
||||
|
||||
expand_restricted_patterns(sources)
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
/// Convert a public source entry to a source entry
|
||||
impl From<PublicSourceEntry> for SourceEntry {
|
||||
fn from(value: PublicSourceEntry) -> Self {
|
||||
if value.negated {
|
||||
return SourceEntry::Ignored {
|
||||
base: value.base.into(),
|
||||
pattern: value.pattern,
|
||||
};
|
||||
}
|
||||
|
||||
let auto =
|
||||
value.pattern == "/**/*" || PathBuf::from(&value.base).join(&value.pattern).is_dir();
|
||||
|
||||
if !auto {
|
||||
return SourceEntry::Pattern {
|
||||
base: value.base.into(),
|
||||
pattern: value.pattern,
|
||||
};
|
||||
}
|
||||
let auto = value.pattern.ends_with("**/*")
|
||||
|| PathBuf::from(&value.base).join(&value.pattern).is_dir();
|
||||
|
||||
let inside_ignored_content_dir = IGNORED_CONTENT_DIRS.iter().any(|dir| {
|
||||
value.base.contains(&format!(
|
||||
|
|
@ -876,16 +239,24 @@ impl From<PublicSourceEntry> for SourceEntry {
|
|||
std::path::MAIN_SEPARATOR
|
||||
)) || value
|
||||
.base
|
||||
.ends_with(&format!("{}{}", std::path::MAIN_SEPARATOR, dir))
|
||||
.ends_with(&format!("{}{}", std::path::MAIN_SEPARATOR, dir,))
|
||||
});
|
||||
|
||||
match inside_ignored_content_dir {
|
||||
false => SourceEntry::Auto {
|
||||
match (value.negated, auto, inside_ignored_content_dir) {
|
||||
(false, true, false) => SourceEntry::Auto {
|
||||
base: value.base.into(),
|
||||
},
|
||||
true => SourceEntry::External {
|
||||
(false, true, true) => SourceEntry::External {
|
||||
base: value.base.into(),
|
||||
},
|
||||
(false, false, _) => SourceEntry::Pattern {
|
||||
base: value.base.into(),
|
||||
pattern: value.pattern,
|
||||
},
|
||||
(true, _, _) => SourceEntry::Ignored {
|
||||
base: value.base.into(),
|
||||
pattern: value.pattern,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -126,72 +126,6 @@ test(
|
|||
},
|
||||
)
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/19706
|
||||
test(
|
||||
'Config files (TS, valid types)',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'tsconfig.json': json`
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"declaration": true,
|
||||
"composite": true,
|
||||
"outDir": "./.build",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["tailwind.config.ts"]
|
||||
}
|
||||
`,
|
||||
'index.html': html`
|
||||
<div class="text-primary"></div>
|
||||
`,
|
||||
'tailwind.config.ts': ts`
|
||||
import type { Config } from 'tailwindcss'
|
||||
|
||||
function defineConfig(config: Config): Config {
|
||||
return config
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: 'blue',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
`,
|
||||
'src/index.css': css`
|
||||
@import 'tailwindcss';
|
||||
@config '../tailwind.config.ts';
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec }) => {
|
||||
// We expect that these commands don't crash:
|
||||
await exec('pnpm tsc -b')
|
||||
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css')
|
||||
|
||||
await fs.expectFileToContain('dist/out.css', [candidate`text-primary`])
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'Config files (CJS, watch mode)',
|
||||
{
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,4 @@
|
|||
import { candidate, css, html, json, test, ts } from '../utils'
|
||||
import { candidate, css, html, json, test } from '../utils'
|
||||
|
||||
test(
|
||||
'builds the `@tailwindcss/typography` plugin utilities',
|
||||
|
|
@ -42,86 +42,12 @@ test(
|
|||
|
||||
await fs.expectFileToContain('dist/out.css', [
|
||||
candidate`prose`,
|
||||
':where(h1):not(:where([class~="not-prose"], [class~="not-prose"] *))',
|
||||
':where(tbody td, tfoot td):not(:where([class~="not-prose"], [class~="not-prose"] *))',
|
||||
':where(h1):not(:where([class~="not-prose"],[class~="not-prose"] *))',
|
||||
':where(tbody td, tfoot td):not(:where([class~="not-prose"],[class~="not-prose"] *))',
|
||||
])
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'builds the `@tailwindcss/typography` plugin utilities with `@variant` usages',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"@tailwindcss/typography": "^0.5.14",
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'index.html': html`
|
||||
<div className="prose prose-custom">
|
||||
<h1>Headline</h1>
|
||||
<p>
|
||||
Until now, trying to style an article, document, or blog post with Tailwind has been a
|
||||
tedious task that required a keen eye for typography and a lot of complex custom CSS.
|
||||
</p>
|
||||
</div>
|
||||
`,
|
||||
'src/index.css': css`
|
||||
@import 'tailwindcss/utilities';
|
||||
@theme {
|
||||
--breakpoint-sm: 640px;
|
||||
}
|
||||
@plugin '@tailwindcss/typography';
|
||||
@config '../tailwind.config.js';
|
||||
@custom-variant custom (&.custom);
|
||||
`,
|
||||
'tailwind.config.js': ts`
|
||||
module.exports = {
|
||||
theme: {
|
||||
typography: ({ theme }) => ({
|
||||
custom: {
|
||||
css: {
|
||||
hr: {
|
||||
'--x': '1',
|
||||
'@variant sm:custom': {
|
||||
'--x': '2',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec, expect }) => {
|
||||
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css')
|
||||
|
||||
// We don't want to see `@variant` in the output
|
||||
let contents = await fs.read('dist/out.css')
|
||||
expect(contents).not.toContain('@variant')
|
||||
|
||||
expect(await fs.dumpFiles('dist/out.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- dist/out.css ---
|
||||
.prose-custom :where(hr):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
--x: 1;
|
||||
@media (width >= 640px) {
|
||||
&.custom {
|
||||
--x: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'builds the `@tailwindcss/forms` plugin utilities',
|
||||
{
|
||||
|
|
@ -191,7 +117,7 @@ test(
|
|||
await fs.expectFileToContain('dist/out.css', [
|
||||
//
|
||||
`::-webkit-date-and-time-value`,
|
||||
`input:where([type='checkbox']):indeterminate`,
|
||||
`[type='checkbox']:indeterminate`,
|
||||
])
|
||||
|
||||
// No classes are included even though they are used in the HTML
|
||||
|
|
@ -281,88 +207,3 @@ test(
|
|||
])
|
||||
},
|
||||
)
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/15844
|
||||
test(
|
||||
'builds CSS with a custom plugin compiled from TypeScript',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'tsconfig.json': json`
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"declaration": true,
|
||||
"composite": true,
|
||||
"rootDir": "./src",
|
||||
"outDir": "./.build",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
`,
|
||||
'index.html': html`
|
||||
<div class="test-red"></div>
|
||||
`,
|
||||
'src/index.css': css`
|
||||
@import 'tailwindcss';
|
||||
@plugin '../.build/plugin.js';
|
||||
`,
|
||||
'src/plugin.ts': ts`
|
||||
import plugin from 'tailwindcss/plugin'
|
||||
|
||||
export const typedPlugin = plugin(() => {
|
||||
return ({ matchComponents }) => {
|
||||
matchComponents(
|
||||
{
|
||||
test: (content: string) => ({
|
||||
color: content,
|
||||
}),
|
||||
},
|
||||
{
|
||||
values: {
|
||||
red: 'red',
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default plugin(({ matchComponents }) => {
|
||||
matchComponents(
|
||||
{
|
||||
test: (content: string) => ({
|
||||
color: content,
|
||||
}),
|
||||
},
|
||||
{
|
||||
values: {
|
||||
red: 'red',
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec }) => {
|
||||
// We expect that these commands don't crash:
|
||||
await exec('pnpm tsc -b')
|
||||
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css')
|
||||
|
||||
await fs.expectFileToContain('dist/out.css', [candidate`test-red`])
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import nodeFs from 'node:fs/promises'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { candidate, css, html, IS_WINDOWS, js, json, test, ts } from '../utils'
|
||||
import { candidate, css, html, js, json, test } from '../utils'
|
||||
|
||||
const STANDALONE_BINARY = (() => {
|
||||
switch (os.platform()) {
|
||||
|
|
@ -16,41 +15,6 @@ const STANDALONE_BINARY = (() => {
|
|||
}
|
||||
})()
|
||||
|
||||
test(
|
||||
'does not scan itself for candidates',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {}
|
||||
}
|
||||
`,
|
||||
'src/index.css': css` @import 'tailwindcss'; `,
|
||||
},
|
||||
},
|
||||
async ({ root, fs, exec }) => {
|
||||
let sourceBinary = path.resolve(
|
||||
__dirname,
|
||||
`../../packages/@tailwindcss-standalone/dist/${STANDALONE_BINARY}`,
|
||||
)
|
||||
let binary = IS_WINDOWS ? 'tailwindcss.exe' : 'tailwindcss'
|
||||
let localBinary = path.join(root, binary)
|
||||
await nodeFs.copyFile(sourceBinary, localBinary)
|
||||
|
||||
if (!IS_WINDOWS) {
|
||||
await nodeFs.chmod(localBinary, 0o755)
|
||||
}
|
||||
|
||||
await exec(`${IS_WINDOWS ? binary : `./${binary}`} --input src/index.css --output dist/out.css`)
|
||||
|
||||
await fs.expectFileNotToContain('dist/out.css', [
|
||||
candidate`flex`,
|
||||
candidate`grid`,
|
||||
candidate`underline`,
|
||||
])
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'includes first-party plugins',
|
||||
{
|
||||
|
|
@ -158,7 +122,7 @@ test(
|
|||
})
|
||||
})
|
||||
`,
|
||||
'src/plugin.ts': ts`
|
||||
'src/plugin.ts': js`
|
||||
import plugin from 'tailwindcss/plugin'
|
||||
|
||||
// Make sure all available JS APIs can be imported and used
|
||||
|
|
@ -168,7 +132,8 @@ test(
|
|||
import defaultTheme from 'tailwindcss/defaultTheme'
|
||||
import * as pkg from 'tailwindcss/package.json'
|
||||
|
||||
export interface PluginOptions {}
|
||||
export interface PluginOptions {
|
||||
}
|
||||
|
||||
export default plugin(function ({ addUtilities }) {
|
||||
addUtilities({
|
||||
|
|
|
|||
73
integrations/oxide/postinstall.test.ts
Normal file
73
integrations/oxide/postinstall.test.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { js, json, test } from '../utils'
|
||||
|
||||
test(
|
||||
'@tailwindcss/oxide will fail when architecture-specific packages are missing',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"@tailwindcss/oxide": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'test.js': js`
|
||||
try {
|
||||
let Scanner = require('@tailwindcss/oxide')
|
||||
console.log('SUCCESS: @tailwindcss/oxide loaded successfully', Scanner)
|
||||
} catch (error) {
|
||||
console.log('FAILURE: Failed to load @tailwindcss/oxide:', error.message)
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ exec, root, expect, fs }) => {
|
||||
await removePlatformSpecificExtensions(path.join(root, 'node_modules'))
|
||||
|
||||
// Get last published version
|
||||
let version = (await exec('npm show @tailwindcss/oxide version')).trim()
|
||||
// Ensure that we don't depend on a specific version number in the download
|
||||
// script in case we bump the version number in the repository and CI is run
|
||||
// before a release
|
||||
let packageJson = JSON.parse(await fs.read('node_modules/@tailwindcss/oxide/package.json'))
|
||||
packageJson.version = version
|
||||
await fs.write(
|
||||
'node_modules/@tailwindcss/oxide/package.json',
|
||||
JSON.stringify(packageJson, null, 2),
|
||||
)
|
||||
|
||||
let opts = {
|
||||
// Ensure that we don't include any node paths from the test runner
|
||||
env: { NODE_PATH: '' },
|
||||
}
|
||||
|
||||
expect(await exec('node test.js', opts)).toMatch(/FAILURE/)
|
||||
|
||||
// Now run the post-install script
|
||||
await exec('node node_modules/@tailwindcss/oxide/scripts/install.js', opts)
|
||||
|
||||
expect(await exec('node test.js', opts)).toMatch(/SUCCESS/)
|
||||
},
|
||||
)
|
||||
|
||||
async function removePlatformSpecificExtensions(directory: string) {
|
||||
let entries = await fs.readdir(directory, { withFileTypes: true })
|
||||
|
||||
for (let entry of entries) {
|
||||
let fullPath = path.join(directory, entry.name)
|
||||
|
||||
if (entry.name.startsWith('oxide-')) {
|
||||
if (entry.isSymbolicLink()) {
|
||||
await fs.unlink(fullPath)
|
||||
} else if (entry.isFile()) {
|
||||
await fs.unlink(fullPath)
|
||||
} else if (entry.isDirectory()) {
|
||||
await fs.rm(fullPath, { recursive: true, force: true })
|
||||
}
|
||||
} else if (entry.isDirectory()) {
|
||||
await removePlatformSpecificExtensions(fullPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { css, js, json, test, yaml } from '../utils'
|
||||
import { css, js, json, test } from '../utils'
|
||||
|
||||
// This test runs the wasm build using the `node:wasi` runtime.
|
||||
// This test runs the wasm build using the `node:wasi` runtine.
|
||||
//
|
||||
// There are currently a known problems that the Node WASI preview implementation does not properly
|
||||
// handle FS reads on macOS and it does not implement all APIs on Windows. Because of that, this
|
||||
|
|
@ -57,187 +57,3 @@ testFn(
|
|||
`)
|
||||
},
|
||||
)
|
||||
|
||||
testFn(
|
||||
'`@tailwindcss/oxide` falls back to the wasm build when no native binding is available',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"@tailwindcss/oxide": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
# Trick pnpm in only supporting an architecture that @tailwindcss/oxide
|
||||
# doesn't support, and therefore should fallback to the wasm version.
|
||||
supportedArchitectures:
|
||||
os:
|
||||
- openbsd
|
||||
cpu:
|
||||
- x64
|
||||
`,
|
||||
'src/index.js': js`
|
||||
const className = "content-['src/index.js']"
|
||||
module.exports = { className }
|
||||
`,
|
||||
'index.mjs': js`
|
||||
import { createRequire } from 'node:module'
|
||||
import { join } from 'node:path'
|
||||
|
||||
let require = createRequire(import.meta.url)
|
||||
let { Scanner } = require('@tailwindcss/oxide')
|
||||
|
||||
let loaded = Object.keys(require.cache)
|
||||
|
||||
let scanner = new Scanner({
|
||||
sources: [
|
||||
{
|
||||
base: join(process.cwd(), 'src'),
|
||||
pattern: '**/*',
|
||||
negated: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
native: loaded.filter((file) => file.endsWith('.node')),
|
||||
wasi: loaded.some((file) => file.endsWith('tailwindcss-oxide.wasi.cjs')),
|
||||
candidates: scanner.scan(),
|
||||
}),
|
||||
)
|
||||
process.exit()
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ expect, exec }) => {
|
||||
// Since vitest runs under `pnpm run`, pnpm's bin shims export a NODE_PATH
|
||||
// that includes the repository's hidden hoist directory
|
||||
// (`node_modules/.pnpm/node_modules`), which links every workspace package,
|
||||
// including all native `@tailwindcss/oxide-*` bindings.
|
||||
//
|
||||
// Node uses `NODE_PATH` exactly when the local `node_modules` lookup fails,
|
||||
// which would defeat the simulated unsupported platform, so clear it.
|
||||
let output = await exec(`node index.mjs`, { env: { NODE_PATH: '' } })
|
||||
let { native, wasi, candidates } = JSON.parse(output)
|
||||
|
||||
// No native binding was installed or loaded, ...
|
||||
expect(native).toEqual([])
|
||||
|
||||
// ... the wasm32-wasi binding is what actually loaded, ...
|
||||
expect(wasi).toBe(true)
|
||||
|
||||
// ... and scanning real files on disk works through it.
|
||||
expect(candidates).toMatchInlineSnapshot(`
|
||||
[
|
||||
"className",
|
||||
"const",
|
||||
"content-['src/index.js']",
|
||||
"exports",
|
||||
]
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
testFn(
|
||||
'the wasm build loads even when preopening the filesystem root is denied',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"@tailwindcss/oxide": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
# Trick pnpm in only supporting an architecture that @tailwindcss/oxide
|
||||
# doesn't support, and therefore should fallback to the wasm version.
|
||||
supportedArchitectures:
|
||||
os:
|
||||
- openbsd
|
||||
cpu:
|
||||
- x64
|
||||
`,
|
||||
// The wasm bindings generated by `@napi-rs/cli` preopen the filesystem
|
||||
// root, which sandboxed platforms (e.g. OpenHarmony, Android) deny with
|
||||
// `UVWASI_EACCES`, making the wasm fallback fail to load on exactly the
|
||||
// platforms that need it.
|
||||
//
|
||||
// We patch `@napi-rs/cli`'s templates to retry with narrower preopens
|
||||
// (see `patches/@napi-rs__cli@3.7.4.patch`). Simulate such a sandbox by
|
||||
// denying `/` preopens.
|
||||
'preload.cjs': js`
|
||||
const wasi = require('node:wasi')
|
||||
const RealWASI = wasi.WASI
|
||||
|
||||
wasi.WASI = class WASI extends RealWASI {
|
||||
constructor(options) {
|
||||
if (options?.preopens?.['/'] !== undefined) {
|
||||
const error = new Error('UVWASI_EACCES, uvwasi_init')
|
||||
error.code = 'UVWASI_EACCES'
|
||||
error.syscall = 'uvwasi_init'
|
||||
throw error
|
||||
}
|
||||
super(options)
|
||||
}
|
||||
}
|
||||
`,
|
||||
'src/index.js': js`
|
||||
const className = "content-['src/index.js']"
|
||||
module.exports = { className }
|
||||
`,
|
||||
'index.mjs': js`
|
||||
import { createRequire } from 'node:module'
|
||||
import { join } from 'node:path'
|
||||
|
||||
let require = createRequire(import.meta.url)
|
||||
let { Scanner } = require('@tailwindcss/oxide')
|
||||
|
||||
let loaded = Object.keys(require.cache)
|
||||
|
||||
let scanner = new Scanner({
|
||||
sources: [
|
||||
{
|
||||
base: join(process.cwd(), 'src'),
|
||||
pattern: '**/*',
|
||||
negated: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
wasi: loaded.some((file) => file.endsWith('tailwindcss-oxide.wasi.cjs')),
|
||||
candidates: scanner.scan(),
|
||||
}),
|
||||
)
|
||||
process.exit()
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ expect, exec }) => {
|
||||
// See the note about NODE_PATH in the test above.
|
||||
let output = await exec(`node --require ./preload.cjs index.mjs`, { env: { NODE_PATH: '' } })
|
||||
|
||||
// Only parse the first line, because Node prints an `ExperimentalWarning`
|
||||
// about WASI to stderr, which `exec` appends to the output.
|
||||
let { wasi, candidates } = JSON.parse(output.trim().split('\n')[0])
|
||||
|
||||
// The wasm32-wasi binding loaded despite `/` being denied, ...
|
||||
expect(wasi).toBe(true)
|
||||
|
||||
// ... and scanning files under the current working directory still works
|
||||
// through the narrower preopen.
|
||||
expect(candidates).toMatchInlineSnapshot(`
|
||||
[
|
||||
"className",
|
||||
"const",
|
||||
"content-['src/index.js']",
|
||||
"exports",
|
||||
]
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@
|
|||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"dedent": "catalog:",
|
||||
"dedent": "1.7.0",
|
||||
"fast-glob": "^3.3.3",
|
||||
"source-map-js": "^1.2.1",
|
||||
"yaml": "^2.9.0"
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,9 +48,9 @@ describe.each(Object.keys(variantConfig))('%s', (variant) => {
|
|||
},
|
||||
},
|
||||
async ({ exec, expect }) => {
|
||||
await expect(
|
||||
expect(
|
||||
exec('pnpm postcss src/index.css --output dist/out.css', undefined, { ignoreStdErr: true }),
|
||||
).rejects.toThrow(
|
||||
).rejects.toThrowError(
|
||||
`It looks like you're trying to use \`tailwindcss\` directly as a PostCSS plugin. The PostCSS plugin has moved to a separate package, so to continue using Tailwind CSS with PostCSS you'll need to install \`@tailwindcss/postcss\` and update your PostCSS configuration.`,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -698,7 +698,9 @@ test(
|
|||
<div class="flex underline"></div>
|
||||
`)
|
||||
|
||||
await process.onStderr((message) => message.includes('"./does-not-exist" is not exported'))
|
||||
await process.onStderr((message) =>
|
||||
message.includes('does-not-exist is not exported from package'),
|
||||
)
|
||||
|
||||
expect(await fs.dumpFiles('dist/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -741,7 +743,9 @@ test(
|
|||
`,
|
||||
)
|
||||
|
||||
await process.onStderr((message) => message.includes('"./does-not-exist" is not exported'))
|
||||
await process.onStderr((message) =>
|
||||
message.includes('does-not-exist is not exported from package'),
|
||||
)
|
||||
|
||||
expect(await fs.dumpFiles('dist/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -821,73 +825,3 @@ test(
|
|||
})
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'source(…) and `@source` are relative to the file they are in',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"postcss": "^8",
|
||||
"postcss-cli": "^10",
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/postcss": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
'postcss.config.js': js`
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
}
|
||||
`,
|
||||
|
||||
'index.css': css` @import './project-a/src/index.css'; `,
|
||||
|
||||
'project-a/src/index.css': css`
|
||||
/* Run auto-content detection in ../../project-b */
|
||||
@import 'tailwindcss/utilities' source('../../project-b');
|
||||
|
||||
/* Explicitly using node_modules in the @source allows git ignored folders */
|
||||
@source '../../project-c';
|
||||
`,
|
||||
|
||||
// Project A is the current folder, but we explicitly configured
|
||||
// `source(project-b)`, therefore project-a should not be included in
|
||||
// the output.
|
||||
'project-a/src/index.html': html`
|
||||
<div
|
||||
class="content-['SHOULD-NOT-EXIST-IN-OUTPUT'] content-['project-a/src/index.html']"
|
||||
></div>
|
||||
`,
|
||||
|
||||
// Project B is the configured `source(…)`, therefore auto source
|
||||
// detection should include known extensions and folders in the output.
|
||||
'project-b/src/index.html': html`
|
||||
<div
|
||||
class="content-['project-b/src/index.html']"
|
||||
></div>
|
||||
`,
|
||||
|
||||
// Project C should apply auto source detection, therefore known
|
||||
// extensions and folders should be included in the output.
|
||||
'project-c/src/index.html': html`
|
||||
<div
|
||||
class="content-['project-c/src/index.html']"
|
||||
></div>
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, exec, root, expect }) => {
|
||||
await exec('pnpm postcss ./index.css --output dist/out.css', { cwd: root })
|
||||
|
||||
let content = await fs.dumpFiles('./dist/*.css')
|
||||
|
||||
expect(content).not.toContain(candidate`content-['project-a/src/index.html']`)
|
||||
expect(content).toContain(candidate`content-['project-b/src/index.html']`)
|
||||
expect(content).toContain(candidate`content-['project-c/src/index.html']`)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -468,7 +468,7 @@ test(
|
|||
// the CSS
|
||||
await fetchStyles(url)
|
||||
|
||||
// At this point, no changes should trigger a compile step. If we see any
|
||||
// At this point, no changes should triger a compile step. If we see any
|
||||
// changes, there is an infinite loop because we (the user) didn't write any
|
||||
// files to disk.
|
||||
//
|
||||
|
|
|
|||
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