diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index c6c8b3621938..000000000000 --- a/.editorconfig +++ /dev/null @@ -1,9 +0,0 @@ -root = true - -[*] -indent_style = space -indent_size = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index f7288b4ad2e8..000000000000 --- a/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -/lib -/docs -/__tests__/fixtures/cli-utils.js -defaultConfig.stub.js diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 87712fea03fc..000000000000 --- a/.eslintrc +++ /dev/null @@ -1,29 +0,0 @@ -{ - "env": { - "jest": true - }, - "parserOptions": { - "ecmaVersion": 6, - "sourceType": "module", - "ecmaFeatures": { - "experimentalObjectRestSpread": true - } - }, - "extends": ["eslint-config-postcss", "prettier"], - "plugins": ["prettier"], - "rules": { - "prettier/prettier": [ - "error", - { - "semi": false, - "singleQuote": true, - "printWidth": 100, - "tabWidth": 2, - "useTabs": false, - "trailingComma": "es5", - "bracketSpacing": true, - "parser": "flow" - } - ] - } -} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000000..4bf4af1ed22d --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @tailwindlabs/engineering diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md deleted file mode 100644 index 7ce9543c5173..000000000000 --- a/.github/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,10 +0,0 @@ -# Tailwind CSS Community Guidelines - -The following community guidelines are based on [The Ruby Community Conduct Guidelines](https://www.ruby-lang.org/en/conduct/). - -This document provides community guidelines for a respectful, productive, and collaborative place for any person who is willing to contribute to the Tailwind CSS project. It applies to all “collaborative space”, which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.). - -- Participants will be tolerant of opposing views. -- Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks. -- When interpreting the words and actions of others, participants should always assume good intentions. -- Behaviour which can be reasonably considered harassment will not be tolerated. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index c8025e2f1096..63f79fa100e6 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,33 +1,103 @@ # Contributing -Thanks for your interest in contributing to Tailwind CSS! Please take a moment to review this document **before submitting a pull request**. +## Requirements -## Pull requests +Before getting started, ensure your system has access to the following tools: -**Please ask first before starting work on any significant new features.** +- [Node.js](https://nodejs.org/) +- [Rustup](https://rustup.rs/) +- [pnpm](https://pnpm.io/) -It's never a fun experience to have your pull request declined after investing a lot of time and effort into a new feature. To avoid this from happening, we request that contributors create [an issue](https://github.com/tailwindcss/tailwindcss/issues) to first discuss any significant new features. This includes things like adding new utilities, creating new at-rules, etc. +## Getting started + +```sh +# Install dependencies +pnpm install + +# Install Rust toolchain and WASM targets +rustup default stable +rustup target add wasm32-wasip1-threads + +# Build the project +pnpm build +``` + +## Development workflow + +During development, you can run tests in watch mode: + +```sh +pnpm tdd +``` + +The `playgrounds` directory contains example projects you can use to test your changes. To start the Vite playground, use: + +```sh +pnpm build && pnpm vite +``` + +## Bug fixes + +If you've found a bug in Tailwind that you'd like to fix, [submit a pull request](https://github.com/tailwindlabs/tailwindcss/pulls) with your changes. Include a helpful description of the problem and how your changes address it, and provide tests so we can verify the fix works as expected. + +## New features + +If there's a new feature you'd like to see added to Tailwind, [share your idea with us](https://github.com/tailwindlabs/tailwindcss/discussions/new?category=ideas) in our discussion forum to get it on our radar as something to consider for a future release before starting work on it. + +**Please note that we don't often accept pull requests for new features.** Adding a new feature to Tailwind requires us to think through the entire problem ourselves to make sure we agree with the proposed API, which means the feature needs to be high on our own priority list for us to be able to give it the attention it needs. + +If you open a pull request for a new feature, we're likely to close it not because it's a bad idea, but because we aren't ready to prioritize the feature and don't want the PR to sit open for months or even years. ## Coding standards -Our code formatting rules are defined in [.eslintrc](https://github.com/tailwindcss/tailwindcss/blob/master/.eslintrc). 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/tailwindlabs/tailwindcss/blob/main/package.json). You can check your code against these standards by running: ```sh -npm run style +pnpm run lint ``` To automatically fix any style violations in your code, you can run: ```sh -npm run style --fix +pnpm run format ``` ## Running tests -You can run the test suite using the following commands: +You can run the TypeScript and Rust test suites using the following command: ```sh -npm test +pnpm test ``` -Please ensure that the tests are passing when submitting a pull request. If you're adding new features to Tailwind, please include tests. +To run the integration tests, use: + +```sh +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: + +```sh +pnpm build && pnpm test:ui +``` + +Please ensure that all tests are passing when submitting a pull request. If you're adding new features to Tailwind CSS, always include tests. + +After a successful build, you can also use the npm package tarballs created inside the `dist/` folder to install your build in other local projects. + +## Pull request process + +When submitting a pull request: + +- Ensure the pull request title and description explain the changes you made and why you made them. +- Include a test plan section that outlines how you tested your contributions. We do not accept contributions without tests. +- Ensure all tests pass. You can add the tag `[ci-all]` in your pull request description to run the test suites across all platforms. + +When a pull request is created, Tailwind CSS maintainers will be notified automatically. + +## Communication + +- **GitHub discussions**: For feature ideas and general questions +- **GitHub issues**: For bug reports +- **GitHub pull requests**: For code contributions diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000000..848c851b1773 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: ['https://tailwindcss.com/sponsor'] diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index c55930488792..000000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,11 +0,0 @@ - diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 000000000000..1b8a310c6f52 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,39 @@ +--- +name: Bug report +about: If you've already asked for help with a problem and confirmed something is broken with Tailwind CSS itself, create a bug report. +title: '' +labels: '' +assignees: '' +--- + + + +**What version of Tailwind CSS are you using?** + +For example: v4.0.6 + +**What build tool (or framework if it abstracts the build tool) are you using?** + +For example: postcss-cli 11.0.0, Next.js 15.1.7, Vite 6.1.0 + +**What version of Node.js are you using?** + +For example: v20.0.0 + +**What browser are you using?** + +For example: Chrome, Safari, or N/A + +**What operating system are you using?** + +For example: macOS, Windows + +**Reproduction URL** + +A Tailwind Play link or public GitHub repo that includes a minimal reproduction of the bug. **Please do not link to your actual project**, what we need instead is a _minimal_ reproduction in a fresh project without any unnecessary code. This means it doesn't matter if your real project is private/confidential, since we want a link to a separate, isolated reproduction anyways. + +A reproduction is **required** when filing an issue — any issue opened without a reproduction will be closed and you'll be asked to create a new issue that includes a reproduction. We're a small team and we can't keep up with the volume of issues we receive if we need to reproduce each issue from scratch ourselves. + +**Describe your issue** + +Describe the problem you're seeing, any important steps to reproduce and what behavior you expect instead. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000000..10ee9a6b8afb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Get Help + url: https://github.com/tailwindlabs/tailwindcss/discussions/new?category=help + about: If you can't get something to work the way you expect, open a question in our discussion forums. + - name: Feature Request + url: https://github.com/tailwindlabs/tailwindcss/discussions/new?category=ideas + about: 'Suggest any ideas you have using our discussion forums.' + - name: Documentation Issue + url: https://github.com/tailwindlabs/tailwindcss.com + about: 'For documentation issues, suggest changes on our documentation repository.' diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ce7fafb97dc0..aa0793aa39f8 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,8 +4,26 @@ **Please ask first before starting work on any significant new features.** -It's never a fun experience to have your pull request declined after investing a lot of time and effort into a new feature. To avoid this from happening, we request that contributors create an issue to first discuss any significant new features. This includes things like adding new utilities, creating new at-rules, or adding new component examples to the documentation. +It's never a fun experience to have your pull request declined after investing a lot of time and effort into a new feature. To avoid this from happening, we request that contributors create a discussion to first discuss any significant new features. -https://github.com/tailwindcss/tailwindcss/blob/master/.github/CONTRIBUTING.md +For more info, check out the contributing guide: + +https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md + +--> + +## Summary + + + +## Test plan + + diff --git a/.github/logo-dark.svg b/.github/logo-dark.svg new file mode 100644 index 000000000000..fe7f33aead3c --- /dev/null +++ b/.github/logo-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.github/logo-light.svg b/.github/logo-light.svg new file mode 100644 index 000000000000..a1731300d847 --- /dev/null +++ b/.github/logo-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000000..fca477c08bec --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,122 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +env: + NODE_VERSION: 24 + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + tests: + strategy: + fail-fast: false + matrix: + runner: + - name: Windows + os: windows-latest + + - name: Linux + os: namespace-profile-default + + - name: macOS + os: macos-14 + + # Exclude windows and macos from being built on feature branches + run-all: + - ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.body, '[ci-all]') || github.event.pull_request.user.login == 'depfu[bot]' }} + exclude: + - run-all: false + runner: + name: Windows + - run-all: false + runner: + name: macOS + + runs-on: ${{ matrix.runner.os }} + timeout-minutes: 30 + + name: ${{ matrix.runner.name }} + + steps: + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v4 + + - name: Use Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'pnpm' + + # Cargo already skips downloading dependencies if they already exist + - name: Cache cargo + uses: actions/cache@v5 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + # Cache the `oxide` Rust build + - name: Cache oxide build + uses: actions/cache@v5 + 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 }}-oxide-${{ hashFiles('./crates/**/*') }} + + - name: Setup WASM target + run: rustup target add wasm32-wasip1-threads + + - name: Install dependencies + run: pnpm install + + - name: Build + run: pnpm run build + env: + CARGO_PROFILE_RELEASE_LTO: 'off' + CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER: 'lld-link' + + - name: Lint + run: pnpm run lint + # Only lint on linux to avoid \r\n line ending errors + if: matrix.runner.os == 'ubuntu-latest' + + - name: Test + run: pnpm run test + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + + - 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@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.' diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 000000000000..3b2cf8f1b80a --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,125 @@ +name: Integration Tests + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +env: + NODE_VERSION: 24 + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + tests: + strategy: + fail-fast: false + matrix: + runner: + - name: Windows + os: windows-latest + + - name: Linux + os: namespace-profile-default + + - name: macOS + os: macos-14 + + integration: + - upgrade + - vite + - cli + - postcss + - oxide + - webpack + + # Exclude windows and macos from being built on feature branches + run-all: + - ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.body, '[ci-all]') }} + exclude: + - run-all: false + runner: + name: Windows + - run-all: false + runner: + name: macOS + + runs-on: ${{ matrix.runner.os }} + timeout-minutes: 30 + + name: ${{ matrix.runner.name }} / ${{ matrix.integration }} + + steps: + - uses: actions/checkout@v6 + - 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@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'pnpm' + + # Cargo already skips downloading dependencies if they already exist + - name: Cache cargo + uses: actions/cache@v5 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + # Cache the `oxide` Rust build + - name: Cache oxide build + uses: actions/cache@v5 + 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 }}-oxide-${{ hashFiles('./crates/**/*') }} + + - name: Setup WASM target + run: rustup target add wasm32-wasip1-threads + + - name: Install dependencies + run: pnpm install + + - name: Build + run: pnpm run build + env: + 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 }} + 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@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.' diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 000000000000..d5ad798be455 --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,334 @@ +name: Prepare Release + +on: + workflow_dispatch: + push: + tags: + - 'v*' + +env: + APP_NAME: tailwindcss-oxide + NODE_VERSION: 24 + OXIDE_LOCATION: ./crates/node + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +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@v6 + - uses: pnpm/action-setup@v4 + + - name: Use Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@v6 + 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@v5 + 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@v5 + 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@v6 + 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@v6 + 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@v6 + - 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" + pnpm install --ignore-scripts --filter=!./playgrounds/* || true + echo "~~~~ rustc --version ~~~~" + rustc --version + echo "~~~~ node -v ~~~~" + node -v + echo "~~~~ pnpm --version ~~~~" + pnpm --version + 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@v6 + with: + name: bindings-x86_64-unknown-freebsd + path: ${{ env.OXIDE_LOCATION }}/*.node + + prepare: + runs-on: macos-14 + timeout-minutes: 15 + name: Build and release Tailwind CSS + + 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@v6 + with: + fetch-depth: 20 + + - run: git fetch --tags -f + + - name: Resolve version + id: vars + run: | + echo "TAG_NAME=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV + + - uses: pnpm/action-setup@v4 + + - name: Use Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@v6 + 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@v5 + 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@v5 + 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@v7 + 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: Build Tailwind CSS + run: pnpm run build + env: + FEATURES_ENV: stable + + - 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: Get release notes + run: | + RELEASE_NOTES=$(node ./scripts/release-notes.mjs) + echo "RELEASE_NOTES<> $GITHUB_ENV + echo "$RELEASE_NOTES" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + + - name: Upload standalone artifacts + uses: actions/upload-artifact@v6 + with: + name: tailwindcss-standalone + path: packages/@tailwindcss-standalone/dist/ + + - name: Upload npm package tarballs + uses: actions/upload-artifact@v6 + with: + name: npm-package-tarballs + path: dist/*.tgz + + - name: Prepare GitHub Release + 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 diff --git a/.github/workflows/release-insiders.yml b/.github/workflows/release-insiders.yml new file mode 100644 index 000000000000..4852cce7b7a9 --- /dev/null +++ b/.github/workflows/release-insiders.yml @@ -0,0 +1,322 @@ +name: Release Insiders + +on: + push: + branches: [main] + +permissions: + contents: read + +env: + APP_NAME: tailwindcss-oxide + NODE_VERSION: 24 + OXIDE_LOCATION: ./crates/node + RELEASE_CHANNEL: insiders + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +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@v6 + - uses: pnpm/action-setup@v4 + + - name: Use Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@v6 + 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@v5 + 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@v5 + 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@v6 + 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@v6 + 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@v6 + - 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@v6 + 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@v6 + 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@v6 + 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@v5 + 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@v5 + 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@v7 + 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@v6 + 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@v8 + with: + github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }} + script: | + await github.rest.actions.createWorkflowDispatch({ + owner: 'tailwindlabs', + repo: 'upgrades', + ref: 'main', + workflow_id: 'upgrade-tailwindcss.yml' + }) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000000..a872900ff9e2 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,312 @@ +name: Release + +on: + release: + types: [published] + workflow_dispatch: + +permissions: + contents: read + +env: + APP_NAME: tailwindcss-oxide + NODE_VERSION: 24 + OXIDE_LOCATION: ./crates/node + +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@v6 + - uses: pnpm/action-setup@v4 + + - name: Use Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@v6 + 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@v5 + 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@v5 + 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@v6 + 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@v6 + 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@v6 + - 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@v6 + 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 + + 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@v6 + with: + fetch-depth: 20 + + - uses: pnpm/action-setup@v4 + + - name: Use Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@v6 + 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@v5 + 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@v5 + 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@v7 + 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: Build Tailwind CSS + run: pnpm run build + env: + FEATURES_ENV: stable + + - 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: 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 ${{ 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 + if: env.RELEASE_CHANNEL == 'latest' + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }} + script: | + await github.rest.actions.createWorkflowDispatch({ + owner: 'tailwindlabs', + repo: 'upgrades', + ref: 'main', + workflow_id: 'upgrade-tailwindcss.yml' + }) diff --git a/.gitignore b/.gitignore index 60fd5ef5ade3..bbac7d227997 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,10 @@ -/node_modules -/lib -/example -yarn-error.log +node_modules/ +dist/ +coverage/ +.turbo +test-results/ +playwright-report/ +blob-report/ +playwright/.cache/ +target/ +.debug/ diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 1b92e067e5fe..000000000000 --- a/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -/__tests__/ -/jest/ -/src/ -yarn-error.log diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000000..4f50b932fbca --- /dev/null +++ b/.prettierignore @@ -0,0 +1,9 @@ +coverage/ +node_modules/ +pnpm-lock.yaml +target/ +crates/node/index.d.ts +crates/node/index.js +crates/ignore/ +.next +.fingerprint diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index bc1e36754b34..000000000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -language: node_js -node_js: - - "8" - - "10" - -cache: - directories: - - $HOME/.cache/yarn - -script: - - yarn - - yarn run test diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000000..fccae8e6f7a3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4270 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- _Experimental_: Add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901)) + +### Fixed + +- Improve canonicalizations for `tracking-*` utilities ([#19827](https://github.com/tailwindlabs/tailwindcss/pull/19827)) +- Fix crash due to invalid characters in candidate ([#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)) + +## [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 + +### Fixed + +- Fix Safari devtools rendering issue due to `color-mix` fallback ([#19069](https://github.com/tailwindlabs/tailwindcss/pull/19069)) +- Suppress Lightning CSS warnings about `:deep`, `:slotted`, and `:global` ([#19094](https://github.com/tailwindlabs/tailwindcss/pull/19094)) +- Fix resolving theme keys when starting with the name of another theme key in JS configs and plugins ([#19097](https://github.com/tailwindlabs/tailwindcss/pull/19097)) +- Allow named groups in combination with `not-*`, `has-*`, and `in-*` ([#19100](https://github.com/tailwindlabs/tailwindcss/pull/19100)) +- Prevent important utilities from affecting other utilities ([#19110](https://github.com/tailwindlabs/tailwindcss/pull/19110)) +- 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 + +### Fixed + +- Handle `'` syntax in ClojureScript when extracting classes ([#18888](https://github.com/tailwindlabs/tailwindcss/pull/18888)) +- Handle `@variant` inside `@custom-variant` ([#18885](https://github.com/tailwindlabs/tailwindcss/pull/18885)) +- Merge suggestions when using `@utility` ([#18900](https://github.com/tailwindlabs/tailwindcss/pull/18900)) +- Ensure that file system watchers created when using the CLI are always cleaned up ([#18905](https://github.com/tailwindlabs/tailwindcss/pull/18905)) +- Do not generate `grid-column` utilities when configuring `grid-column-start` or `grid-column-end` ([#18907](https://github.com/tailwindlabs/tailwindcss/pull/18907)) +- Do not generate `grid-row` utilities when configuring `grid-row-start` or `grid-row-end` ([#18907](https://github.com/tailwindlabs/tailwindcss/pull/18907)) +- Prevent duplicate CSS when overwriting a static utility with a theme key ([#18056](https://github.com/tailwindlabs/tailwindcss/pull/18056)) +- Show Lightning CSS warnings (if any) when optimizing/minifying ([#18918](https://github.com/tailwindlabs/tailwindcss/pull/18918)) +- Use `default` export condition for `@tailwindcss/vite` ([#18948](https://github.com/tailwindlabs/tailwindcss/pull/18948)) +- Re-throw errors from PostCSS nodes ([#18373](https://github.com/tailwindlabs/tailwindcss/pull/18373)) +- Detect classes in markdown inline directives ([#18967](https://github.com/tailwindlabs/tailwindcss/pull/18967)) +- Ensure files with only `@theme` produce no output when built ([#18979](https://github.com/tailwindlabs/tailwindcss/pull/18979)) +- Support Maud templates when extracting classes ([#18988](https://github.com/tailwindlabs/tailwindcss/pull/18988)) +- Upgrade: Do not migrate `variant = 'outline'` during upgrades ([#18922](https://github.com/tailwindlabs/tailwindcss/pull/18922)) +- Upgrade: Show version mismatch (if any) when running upgrade tool ([#19028](https://github.com/tailwindlabs/tailwindcss/pull/19028)) +- 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 + +- Drop warning from browser build ([#18731](https://github.com/tailwindlabs/tailwindcss/issues/18731)) +- Drop exact duplicate declarations when emitting CSS ([#18809](https://github.com/tailwindlabs/tailwindcss/issues/18809)) + +### Fixed + +- Don't transition `visibility` when using `transition` ([#18795](https://github.com/tailwindlabs/tailwindcss/pull/18795)) +- Discard matched variants with unknown named values ([#18799](https://github.com/tailwindlabs/tailwindcss/pull/18799)) +- Discard matched variants with non-string values ([#18799](https://github.com/tailwindlabs/tailwindcss/pull/18799)) +- Show suggestions for known `matchVariant` values ([#18798](https://github.com/tailwindlabs/tailwindcss/pull/18798)) +- Replace deprecated `clip` with `clip-path` in `sr-only` ([#18769](https://github.com/tailwindlabs/tailwindcss/pull/18769)) +- Hide internal fields from completions in `matchUtilities` ([#18820](https://github.com/tailwindlabs/tailwindcss/pull/18820)) +- Ignore `.vercel` folders by default (can be overridden by `@source …` rules) ([#18855](https://github.com/tailwindlabs/tailwindcss/pull/18855)) +- Consider variants starting with `@-` to be invalid (e.g. `@-2xl:flex`) ([#18869](https://github.com/tailwindlabs/tailwindcss/pull/18869)) +- Do not allow custom variants to start or end with a `-` or `_` ([#18867](https://github.com/tailwindlabs/tailwindcss/pull/18867), [#18872](https://github.com/tailwindlabs/tailwindcss/pull/18872)) +- Upgrade: Migrate `aria` theme keys to `@custom-variant` ([#18815](https://github.com/tailwindlabs/tailwindcss/pull/18815)) +- Upgrade: Migrate `data` theme keys to `@custom-variant` ([#18816](https://github.com/tailwindlabs/tailwindcss/pull/18816)) +- Upgrade: Migrate `supports` theme keys to `@custom-variant` ([#18817](https://github.com/tailwindlabs/tailwindcss/pull/18817)) + +## [4.1.12] - 2025-08-13 + +### Fixed + +- Don't consider the global important state in `@apply` ([#18404](https://github.com/tailwindlabs/tailwindcss/pull/18404)) +- Add missing suggestions for `flex-` utilities ([#18642](https://github.com/tailwindlabs/tailwindcss/pull/18642)) +- Fix trailing `)` from interfering with extraction in Clojure keywords ([#18345](https://github.com/tailwindlabs/tailwindcss/pull/18345)) +- Detect classes inside Elixir charlist, word list, and string sigils ([#18432](https://github.com/tailwindlabs/tailwindcss/pull/18432)) +- Track source locations through `@plugin` and `@config` ([#18345](https://github.com/tailwindlabs/tailwindcss/pull/18345)) +- Allow boolean values of `process.env.DEBUG` in `@tailwindcss/node` ([#18485](https://github.com/tailwindlabs/tailwindcss/pull/18485)) +- Ignore consecutive semicolons in the CSS parser ([#18532](https://github.com/tailwindlabs/tailwindcss/pull/18532)) +- Center the dropdown icon added to an input with a paired datalist by default ([#18511](https://github.com/tailwindlabs/tailwindcss/pull/18511)) +- Extract candidates in Slang templates ([#18565](https://github.com/tailwindlabs/tailwindcss/pull/18565)) +- Improve error messages when encountering invalid functional utility names ([#18568](https://github.com/tailwindlabs/tailwindcss/pull/18568)) +- Discard CSS AST objects with `false` or `undefined` properties ([#18571](https://github.com/tailwindlabs/tailwindcss/pull/18571)) +- Allow users to disable URL rebasing in `@tailwindcss/postcss` via `transformAssetUrls: false` ([#18321](https://github.com/tailwindlabs/tailwindcss/pull/18321)) +- Fix false-positive migrations in `addEventListener` and JavaScript variable names ([#18718](https://github.com/tailwindlabs/tailwindcss/pull/18718)) +- Fix Standalone CLI showing default Bun help when run via symlink on Windows ([#18723](https://github.com/tailwindlabs/tailwindcss/pull/18723)) +- Read from `--border-color-*` theme keys in `divide-*` utilities for backwards compatibility ([#18704](https://github.com/tailwindlabs/tailwindcss/pull/18704/)) +- Don't scan `.hdr` and `.exr` files for classes by default ([#18734](https://github.com/tailwindlabs/tailwindcss/pull/18734)) + +## [4.1.11] - 2025-06-26 + +### Fixed + +- Add heuristic to skip candidate migrations inside `emit(…)` ([#18330](https://github.com/tailwindlabs/tailwindcss/pull/18330)) +- Extract candidates with variants in Clojure/ClojureScript keywords ([#18338](https://github.com/tailwindlabs/tailwindcss/pull/18338)) +- Document `--watch=always` in the CLI's usage ([#18337](https://github.com/tailwindlabs/tailwindcss/pull/18337)) +- Add support for Vite 7 to `@tailwindcss/vite` ([#18384](https://github.com/tailwindlabs/tailwindcss/pull/18384)) + +## [4.1.10] - 2025-06-11 + +### Fixed + +- Fix incorrectly generated CSS when using percentages in arbitrary values with calc (e.g. `w-[calc(100%-var(--offset))]`) ([#18289](https://github.com/tailwindlabs/tailwindcss/pull/18289)) + +## [4.1.9] - 2025-06-11 + +### Fixed + +- Correctly parse custom properties with strings containing semicolons ([#18251](https://github.com/tailwindlabs/tailwindcss/pull/18251)) +- Upgrade: Migrate arbitrary modifiers without percentage signs to bare values (e.g. `/[0.16]` → `/16`) ([#18184](https://github.com/tailwindlabs/tailwindcss/pull/18184)) +- Upgrade: Migrate CSS variable shorthands where fallback value contains function call ([#18184](https://github.com/tailwindlabs/tailwindcss/pull/18184)) +- Upgrade: Migrate negative arbitrary values to negative bare values (e.g. `mb-[-32rem]` → `-mb-128`) ([#18212](https://github.com/tailwindlabs/tailwindcss/pull/18212)) +- Upgrade: Do not migrate `blur` in `wire:model.blur` ([#18216](https://github.com/tailwindlabs/tailwindcss/pull/18216)) +- Don't add spaces around CSS dashed idents when formatting math expressions ([#18220](https://github.com/tailwindlabs/tailwindcss/pull/18220)) + +## [4.1.8] - 2025-05-27 + +### Added + +- Improve error messages when `@apply` fails ([#18059](https://github.com/tailwindlabs/tailwindcss/pull/18059)) + +### Fixed + +- Upgrade: Do not migrate declarations that look like candidates in ` + `, + 'src/input.css': css` + @import 'tailwindcss'; + + .foo { + flex-shrink: 1; + } + + .bar { + @apply !underline; + } + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('./src/**/*.{css,vue}')).toMatchInlineSnapshot(` + " + --- ./src/index.vue --- + + + + + --- ./src/input.css --- + @import 'tailwindcss'; + + .foo { + flex-shrink: 1; + } + + .bar { + @apply underline!; + } + " + `) + }, +) + +function withBOM(text: string): string { + return '\uFEFF' + text +} diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts new file mode 100644 index 000000000000..450d8254c97b --- /dev/null +++ b/integrations/upgrade/js-config.test.ts @@ -0,0 +1,2088 @@ +import path from 'node:path' +import { describe } from 'vitest' +import { css, html, json, test, ts } from '../utils' + +test( + `upgrade JS config files with flat theme values, darkMode, and content fields`, + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + import defaultTheme from 'tailwindcss/defaultTheme' + + module.exports = { + darkMode: 'selector', + content: ['./src/**/*.{html,js}', './node_modules/my-external-lib/**/*.{html}'], + theme: { + boxShadow: { + sm: '0 2px 6px rgb(15 23 42 / 0.08)', + }, + colors: { + red: { + 400: '#f87171', + 500: 'red', + }, + superRed: '#ff0000', + steel: 'rgb(70 130 180 / )', + smoke: 'rgba(245, 245, 245, var(--smoke-alpha, ))', + }, + ringColor: { + DEFAULT: '#c0ffee', + }, + opacity: { + superOpaque: '0.95', + }, + fontSize: { + xs: ['0.75rem', { lineHeight: '1rem' }], + sm: ['0.875rem', { lineHeight: '1.5rem' }], + base: ['1rem', { lineHeight: '2rem' }], + lg: ['1.125rem', '2.5rem'], + xl: ['1.5rem', '3rem', 'invalid'], + '2xl': ['2rem'], + }, + width: { + px: '1px', + auto: 'auto', + 1: '0.25rem', + 1.5: '0.375rem', + 2: '0.5rem', + 2.5: '0.625rem', + 3: '0.75rem', + 3.5: '0.875rem', + 4: '1rem', + 5: '1.25rem', + 6: '1.5rem', + 8: '2rem', + 10: '2.5rem', + 11: '2.75rem', + 12: '3rem', + 16: '4rem', + 24: '6rem', + 32: '8rem', + 40: '10rem', + 48: '12rem', + 64: '16rem', + 80: '20rem', + 96: '24rem', + 128: '32rem', + + full: '100%', + 0: '0%', + '1/2': '50%', + '1/3': 'calc(100% / 3)', + '2/3': 'calc(100% / 3 * 2)', + '1/4': '25%', + '3/4': '75%', + '1/5': '20%', + '2/5': '40%', + '3/5': '60%', + '4/5': '80%', + '1/6': 'calc(100% / 6)', + '5/6': 'calc(100% / 6 * 5)', + '1/7': 'calc(100% / 7)', + '1/10': 'calc(100% / 10)', + '3/10': 'calc(100% / 10 * 3)', + '7/10': 'calc(100% / 10 * 7)', + '9/10': 'calc(100% / 10 * 9)', + screen: '100vw', + + 'full-minus-80': 'calc(100% - 20rem)', + 'full-minus-96': 'calc(100% - 24rem)', + + '225px': '225px', + }, + extend: { + colors: { + red: { + 500: '#ef4444', + 600: '#dc2626', + }, + }, + fontFamily: { + sans: 'Inter, system-ui, sans-serif', + display: ['Cabinet Grotesk', ...defaultTheme.fontFamily.sans], + }, + borderRadius: { + '4xl': '2rem', + }, + keyframes: { + 'spin-clockwise': { + '0%': { transform: 'rotate(0deg)' }, + '100%': { transform: 'rotate(360deg)' }, + }, + 'spin-counterclockwise': { + '0%': { transform: 'rotate(0deg)' }, + '100%': { transform: 'rotate(-360deg)' }, + }, + }, + animation: { + 'spin-clockwise': 'spin-clockwise 1s linear infinite', + 'spin-counterclockwise': 'spin-counterclockwise 1s linear infinite', + }, + letterSpacing: { + superWide: '0.25em', + }, + lineHeight: { + superLoose: '3', + }, + }, + }, + plugins: [], + } satisfies Config + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + // prettier-ignore + 'src/test.js': ts` + export default { + 'shouldNotMigrate': !border.test + '', + 'filter': 'drop-shadow(0 0 0.5rem #000)', + } + `, + 'src/index.html': html` +
+
+ `, + 'node_modules/my-external-lib/src/template.html': html` +
+ Hello world! +
+ `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/**/*.{css,js,html}')).toMatchInlineSnapshot(` + " + --- src/index.html --- +
+
+ + --- src/input.css --- + @import 'tailwindcss'; + + @source '../node_modules/my-external-lib/**/*.{html}'; + + @custom-variant dark (&:where(.dark, .dark *)); + + @theme { + --shadow-*: initial; + --shadow-sm: 0 2px 6px rgb(15 23 42 / 0.08); + + --color-*: initial; + --color-red-400: #f87171; + --color-red-500: #ef4444; + --color-red-600: #dc2626; + + --color-superRed: #ff0000; + --color-steel: rgb(70 130 180); + --color-smoke: rgba(245, 245, 245, var(--smoke-alpha, 1)); + + --default-ring-color: #c0ffee; + + --opacity-*: initial; + --opacity-superOpaque: 95%; + + --text-*: initial; + --text-xs: 0.75rem; + --text-xs--line-height: 1rem; + --text-sm: 0.875rem; + --text-sm--line-height: 1.5rem; + --text-base: 1rem; + --text-base--line-height: 2rem; + --text-lg: 1.125rem; + --text-lg--line-height: 2.5rem; + --text-xl: 1.5rem; + --text-xl--line-height: 3rem; + --text-2xl: 2rem; + + --width-*: initial; + --width-0: 0%; + --width-1: 0.25rem; + --width-2: 0.5rem; + --width-3: 0.75rem; + --width-4: 1rem; + --width-5: 1.25rem; + --width-6: 1.5rem; + --width-8: 2rem; + --width-10: 2.5rem; + --width-11: 2.75rem; + --width-12: 3rem; + --width-16: 4rem; + --width-24: 6rem; + --width-32: 8rem; + --width-40: 10rem; + --width-48: 12rem; + --width-64: 16rem; + --width-80: 20rem; + --width-96: 24rem; + --width-128: 32rem; + --width-px: 1px; + --width-auto: auto; + --width-1_5: 0.375rem; + --width-2_5: 0.625rem; + --width-3_5: 0.875rem; + --width-full: 100%; + --width-1\\/2: 50%; + --width-1\\/3: calc(100% / 3); + --width-2\\/3: calc(100% / 3 * 2); + --width-1\\/4: 25%; + --width-3\\/4: 75%; + --width-1\\/5: 20%; + --width-2\\/5: 40%; + --width-3\\/5: 60%; + --width-4\\/5: 80%; + --width-1\\/6: calc(100% / 6); + --width-5\\/6: calc(100% / 6 * 5); + --width-1\\/7: calc(100% / 7); + --width-1\\/10: calc(100% / 10); + --width-3\\/10: calc(100% / 10 * 3); + --width-7\\/10: calc(100% / 10 * 7); + --width-9\\/10: calc(100% / 10 * 9); + --width-screen: 100vw; + --width-full-minus-80: calc(100% - 20rem); + --width-full-minus-96: calc(100% - 24rem); + --width-225px: 225px; + + --font-sans: Inter, system-ui, sans-serif; + --font-display: + Cabinet Grotesk, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', + 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + + --radius-4xl: 2rem; + + --animate-spin-clockwise: spin-clockwise 1s linear infinite; + --animate-spin-counterclockwise: spin-counterclockwise 1s linear infinite; + + --tracking-superWide: 0.25em; + + --leading-superLoose: 3; + + @keyframes spin-clockwise { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } + @keyframes spin-counterclockwise { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(-360deg); + } + } + } + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + + --- src/test.js --- + export default { + 'shouldNotMigrate': !border.test + '', + 'filter': 'drop-shadow(0 0 0.5rem #000)', + } + " + `) + + expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toBe('') + }, +) + +test( + 'upgrades JS config files with plugins', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/typography": "^0.5.15", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + import typography from '@tailwindcss/typography' + import customPlugin from './custom-plugin' + + export default { + darkMode: [ + 'variant', + [ + '@media not print { .dark & }', + '@media not eink { .dark & }', + '&:where(.dark, .dark *)', + ], + ], + plugins: [ + typography, + customPlugin({ + 'is-null': null, + 'is-true': true, + 'is-false': false, + 'is-int': 1234567, + 'is-float': 1.35, + 'is-sci': 1.35e-5, + 'is-str-null': 'null', + 'is-str-true': 'true', + 'is-str-false': 'false', + 'is-str-int': '1234567', + 'is-str-float': '1.35', + 'is-str-sci': '1.35e-5', + 'is-arr': ['foo', 'bar'], + 'is-arr-mixed': [null, true, false, 1234567, 1.35, 'foo', 'bar', 'true'], + }), + ], + } satisfies Config + `, + 'custom-plugin.js': ts` + import plugin from 'tailwindcss/plugin' + export default plugin.withOptions((_options) => ({ addVariant }) => { + addVariant('inverted', '@media (inverted-colors: inverted)') + addVariant('hocus', ['&:focus', '&:hover']) + }) + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + + @plugin '@tailwindcss/typography'; + @plugin '../custom-plugin' { + is-null: null; + is-true: true; + is-false: false; + is-int: 1234567; + is-float: 1.35; + is-sci: 0.0000135; + is-str-null: 'null'; + is-str-true: 'true'; + is-str-false: 'false'; + is-str-int: '1234567'; + is-str-float: '1.35'; + is-str-sci: '1.35e-5'; + is-arr: 'foo', 'bar'; + is-arr-mixed: null, true, false, 1234567, 1.35, 'foo', 'bar', 'true'; + } + + @custom-variant dark { + @media not print { + .dark & { + @slot; + } + } + @media not eink { + .dark & { + @slot; + } + } + &:where(.dark, .dark *) { + @slot; + } + } + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + " + `) + + expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(` + " + + " + `) + }, +) + +test( + 'upgrades JS config files with functions in the theme config', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + + export default { + theme: { + extend: { + colors: ({ colors }) => ({ + gray: colors.neutral, + }), + }, + }, + } satisfies Config + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/**/*.{css,ts}')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + + @theme { + --color-gray-50: oklch(98.5% 0 0); + --color-gray-100: oklch(97% 0 0); + --color-gray-200: oklch(92.2% 0 0); + --color-gray-300: oklch(87% 0 0); + --color-gray-400: oklch(70.8% 0 0); + --color-gray-500: oklch(55.6% 0 0); + --color-gray-600: oklch(43.9% 0 0); + --color-gray-700: oklch(37.1% 0 0); + --color-gray-800: oklch(26.9% 0 0); + --color-gray-900: oklch(20.5% 0 0); + --color-gray-950: oklch(14.5% 0 0); + } + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + " + `) + + expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(` + " + + " + `) + }, +) + +test( + 'does not upgrade JS config files with theme keys contributed to by plugins in the theme config', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + + export default { + theme: { + typography: { + DEFAULT: { + css: { + '--tw-prose-body': 'red', + color: 'var(--tw-prose-body)', + }, + }, + }, + }, + } satisfies Config + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + @config '../tailwind.config.ts'; + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + + @config '../tailwind.config.ts'; + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + " + `) + + expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(` + " + --- tailwind.config.ts --- + import { type Config } from 'tailwindcss' + + export default { + theme: { + typography: { + DEFAULT: { + css: { + '--tw-prose-body': 'red', + color: 'var(--tw-prose-body)', + }, + }, + }, + }, + } satisfies Config + " + `) + }, +) + +test( + `does not upgrade JS config files with inline plugins`, + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + + export default { + plugins: [function complexConfig() {}], + } satisfies Config + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + + @config '../tailwind.config.ts'; + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + " + `) + + expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(` + " + --- tailwind.config.ts --- + import { type Config } from 'tailwindcss' + + export default { + plugins: [function complexConfig() {}], + } satisfies Config + " + `) + }, +) + +test( + `does not upgrade JS config files with non-simple screens object`, + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + + export default { + theme: { + screens: { + xl: { min: '1024px', max: '1279px' }, + tall: { raw: '(min-height: 800px)' }, + }, + }, + } satisfies Config + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + + @config '../tailwind.config.ts'; + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + " + `) + + expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(` + " + --- tailwind.config.ts --- + import { type Config } from 'tailwindcss' + + export default { + theme: { + screens: { + xl: { min: '1024px', max: '1279px' }, + tall: { raw: '(min-height: 800px)' }, + }, + }, + } satisfies Config + " + `) + }, +) + +test( + 'multi-root project', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + + // Project A + 'project-a/tailwind.config.ts': ts` + export default { + content: { + relative: true, + files: ['./src/**/*.html'], + }, + theme: { + extend: { + colors: { + primary: 'red', + }, + }, + }, + } + `, + 'project-a/src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + @config "../tailwind.config.ts"; + `, + 'project-a/src/index.html': html`
`, + + // Project B + 'project-b/tailwind.config.ts': ts` + export default { + content: { + relative: true, + files: ['./src/**/*.html'], + }, + theme: { + extend: { + colors: { + primary: 'blue', + }, + }, + }, + } + `, + 'project-b/src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + @config "../tailwind.config.ts"; + `, + 'project-b/src/index.html': html`
`, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('project-{a,b}/**/*.{css,ts}')).toMatchInlineSnapshot(` + " + --- project-a/src/input.css --- + @import 'tailwindcss'; + + @theme { + --color-primary: red; + } + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + + --- project-b/src/input.css --- + @import 'tailwindcss'; + + @theme { + --color-primary: blue; + } + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + " + `) + }, +) + +test( + 'migrate sources when pointing to folders outside the project root', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + + 'frontend/tailwind.config.ts': ts` + export default { + content: { + relative: true, + files: ['./src/**/*.html', '../backend/mails/**/*.blade.php'], + }, + theme: { + extend: { + colors: { + primary: 'red', + }, + }, + }, + } + `, + 'frontend/src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + @config "../tailwind.config.ts"; + `, + 'frontend/src/index.html': html`
`, + + 'backend/mails/welcome.blade.php': html`
`, + }, + }, + async ({ root, exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade', { + cwd: path.join(root, 'frontend'), + }) + + expect(await fs.dumpFiles('frontend/**/*.css')).toMatchInlineSnapshot(` + " + --- frontend/src/input.css --- + @import 'tailwindcss'; + + @source '../../backend/mails/**/*.blade.php'; + + @theme { + --color-primary: red; + } + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + " + `) + }, +) + +test( + 'migrate aria theme keys to custom variants', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + export default { + content: { + relative: true, + files: ['./src/**/*.html'], + }, + theme: { + extend: { + aria: { + // Built-in (not really, but visible because of intellisense) + busy: 'busy="true"', + + // Automatically handled by bare values + foo: 'foo="true"', + + // Quotes are optional in CSS for these kinds of attribute + // selectors + bar: 'bar=true', + + // Not automatically handled by bare values because names differ + baz: 'qux="true"', + + // Completely custom + asc: 'sort="ascending"', + desc: 'sort="descending"', + }, + }, + }, + } + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + + @custom-variant aria-baz (&[aria-qux="true"]); + @custom-variant aria-asc (&[aria-sort="ascending"]); + @custom-variant aria-desc (&[aria-sort="descending"]); + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + " + `) + }, +) + +test( + 'migrate data theme keys to custom variants', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + export default { + content: { + relative: true, + files: ['./src/**/*.html'], + }, + theme: { + extend: { + data: { + // Automatically handled by bare values + foo: 'foo', + + // Not automatically handled by bare values because names differ + bar: 'baz', + + // Custom + checked: 'ui~="checked"', + }, + }, + }, + } + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + + @custom-variant data-bar (&[data-baz]); + @custom-variant data-checked (&[data-ui~="checked"]); + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + " + `) + }, +) + +test( + 'migrate supports theme keys to custom variants', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + export default { + content: { + relative: true, + files: ['./src/**/*.html'], + }, + theme: { + extend: { + supports: { + // Automatically handled by bare values (using CSS variable as the value) + foo: 'foo: var(--foo)', // parentheses are optional + bar: '(bar: var(--bar))', + + // Not automatically handled by bare values because names differ + foo: 'bar: var(--foo)', // parentheses are optional + bar: '(qux: var(--bar))', + + // Custom + grid: 'display: grid', + }, + }, + }, + } + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + + @custom-variant supports-foo { + @supports (bar: var(--foo)) { + @slot; + } + } + @custom-variant supports-bar { + @supports ((qux: var(--bar))) { + @slot; + } + } + @custom-variant supports-grid { + @supports (display: grid) { + @slot; + } + } + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + " + `) + }, +) + +describe('border compatibility', () => { + test( + 'migrate border compatibility', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + + // Empty / default config + export default { + theme: { + extend: {}, + }, + } satisfies Config + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + " + `) + }, + ) + + test( + 'migrate border compatibility if a custom border color is used', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + + export default { + theme: { + extend: { + borderColor: ({ colors }) => ({ + DEFAULT: colors.blue[500], + }), + }, + }, + } satisfies Config + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: oklch(62.3% 0.214 259.815); + } + } + " + `) + }, + ) + + test( + 'migrate border compatibility if a custom border color is used, that matches the default v4 border color', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + + export default { + theme: { + extend: { + borderColor: ({ colors }) => ({ + DEFAULT: 'currentcolor', + }), + }, + }, + } satisfies Config + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + " + `) + }, + ) + + test( + 'migrate border compatibility in the file that uses the `@import "tailwindcss"` import', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + + export default { + theme: {}, + } satisfies Config + `, + 'src/input.css': css`@import './tailwind.css';`, + 'src/tailwind.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import './tailwind.css'; + + --- src/tailwind.css --- + @import 'tailwindcss'; + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + " + `) + }, + ) + + test( + 'migrate border compatibility in the file that uses the `@import "tailwindcss/preflight"` import', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + + export default { + theme: {}, + } satisfies Config + `, + 'src/input.css': css` + @import './base.css'; + @import './my-base.css'; + @import './utilities.css'; + `, + 'src/base.css': css`@tailwind base;`, + 'src/utilities.css': css` + @tailwind components; + @tailwind utilities; + `, + 'src/my-base.css': css` + @layer base { + html { + color: black; + } + } + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + " + --- src/base.css --- + @import 'tailwindcss/theme' layer(theme); + @import 'tailwindcss/preflight' layer(base); + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + + --- src/input.css --- + @import './base.css'; + @import './my-base.css'; + @import './utilities.css'; + + --- src/my-base.css --- + @layer base { + html { + color: black; + } + } + + --- src/utilities.css --- + @import 'tailwindcss/utilities' layer(utilities); + " + `) + }, + ) + + test( + 'migrates extended spacing keys', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + + export default { + content: ['./src/**/*.html'], + theme: { + extend: { + spacing: { + 2: '0.5rem', + 4.5: '1.125rem', + 5.5: '1.375em', // Units are different from --spacing scale + 13: '3.25rem', + 100: '100px', + miami: '1337px', + }, + }, + }, + } satisfies Config + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + + .container { + width: theme(spacing.2); + width: theme(spacing[4.5]); + width: theme(spacing[5.5]); + width: theme(spacing[13]); + width: theme(spacing[100]); + width: theme(spacing.miami); + } + `, + 'src/index.html': html` +
+ `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(` + " + --- src/index.html --- +
+ + --- src/input.css --- + @import 'tailwindcss'; + + @theme { + --spacing-100: 100px; + --spacing-5_5: 1.375em; + --spacing-miami: 1337px; + } + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + + .container { + width: --spacing(2); + width: --spacing(4.5); + width: var(--spacing-5_5); + width: --spacing(13); + width: var(--spacing-100); + width: var(--spacing-miami); + } + " + `) + }, + ) + + test( + 'retains overwriting spacing scale', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + + export default { + content: ['./src/**/*.html'], + theme: { + spacing: { + 2: '0.5rem', + 4.5: '1.125rem', + 5.5: '1.375em', + 13: '3.25rem', + 100: '100px', + miami: '1337px', + }, + }, + } satisfies Config + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + + .container { + width: theme(spacing.2); + width: theme(spacing[4.5]); + width: theme(spacing[5.5]); + width: theme(spacing[13]); + width: theme(spacing[100]); + width: theme(spacing.miami); + } + `, + 'src/index.html': html` +
+ `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(` + " + --- src/index.html --- +
+ + --- src/input.css --- + @import 'tailwindcss'; + + @theme { + --spacing-*: initial; + --spacing-2: 0.5rem; + --spacing-13: 3.25rem; + --spacing-100: 100px; + --spacing-4_5: 1.125rem; + --spacing-5_5: 1.375em; + --spacing-miami: 1337px; + } + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + + .container { + width: var(--spacing-2); + width: var(--spacing-4_5); + width: var(--spacing-5_5); + width: var(--spacing-13); + width: var(--spacing-100); + width: var(--spacing-miami); + } + " + `) + }, + ) + + test( + 'migrates `container` component configurations', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + + export default { + content: ['./src/**/*.html'], + theme: { + container: { + center: true, + padding: { + DEFAULT: '2rem', + '2xl': '4rem', + }, + screens: { + md: '48rem', // Matches a default --breakpoint + xl: '1280px', + '2xl': '1536px', + }, + }, + }, + } satisfies Config + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + 'src/index.html': html` +
+ `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(` + " + --- src/index.html --- +
+ + --- src/input.css --- + @import 'tailwindcss'; + + @utility container { + margin-inline: auto; + padding-inline: 2rem; + @media (width >= --theme(--breakpoint-sm)) { + max-width: none; + } + @media (width >= 48rem) { + max-width: 48rem; + } + @media (width >= 1280px) { + max-width: 1280px; + } + @media (width >= 1536px) { + max-width: 1536px; + padding-inline: 4rem; + } + } + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + " + `) + }, + ) +}) + +test( + `future and experimental keys are supported`, + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + import defaultTheme from 'tailwindcss/defaultTheme' + + module.exports = { + darkMode: 'selector', + content: ['./src/**/*.{html,js}'], + future: { + hoverOnlyWhenSupported: true, + respectDefaultRingColorOpacity: true, + disableColorOpacityUtilitiesByDefault: true, + relativeContentPathsByDefault: true, + }, + experimental: { + generalizedModifiers: true, + }, + theme: { + colors: { + red: { + 400: '#f87171', + 500: 'red', + }, + }, + }, + plugins: [], + } satisfies Config + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + + @custom-variant dark (&:where(.dark, .dark *)); + + @theme { + --color-*: initial; + --color-red-400: #f87171; + --color-red-500: red; + } + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + " + `) + + expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toBe('') + }, +) + +test( + `unknown future keys dont migrate the config`, + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + import defaultTheme from 'tailwindcss/defaultTheme' + + module.exports = { + darkMode: 'selector', + content: ['./src/**/*.{html,js}'], + future: { + something: true, + }, + } satisfies Config + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + + @config '../tailwind.config.ts'; + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + " + `) + + expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toMatchInlineSnapshot(` + "--- tailwind.config.ts --- + import { type Config } from 'tailwindcss' + import defaultTheme from 'tailwindcss/defaultTheme' + + module.exports = { + darkMode: 'selector', + content: ['./src/**/*.{html,js}'], + future: { + something: true, + }, + } satisfies Config" + `) + }, +) + +test( + `unknown experimental keys dont migrate the config`, + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'tailwind.config.ts': ts` + import { type Config } from 'tailwindcss' + import defaultTheme from 'tailwindcss/defaultTheme' + + module.exports = { + darkMode: 'selector', + content: ['./src/**/*.{html,js}'], + experimental: { + something: true, + }, + } satisfies Config + `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + " + --- src/input.css --- + @import 'tailwindcss'; + + @config '../tailwind.config.ts'; + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + " + `) + + expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toMatchInlineSnapshot(` + "--- tailwind.config.ts --- + import { type Config } from 'tailwindcss' + import defaultTheme from 'tailwindcss/defaultTheme' + + module.exports = { + darkMode: 'selector', + content: ['./src/**/*.{html,js}'], + experimental: { + something: true, + }, + } satisfies Config" + `) + }, +) diff --git a/integrations/upgrade/upgrade-errors.test.ts b/integrations/upgrade/upgrade-errors.test.ts new file mode 100644 index 000000000000..6733801e087f --- /dev/null +++ b/integrations/upgrade/upgrade-errors.test.ts @@ -0,0 +1,251 @@ +import { stripVTControlCharacters } from 'node:util' +import { css, html, js, json, test } from '../utils' + +test( + 'upgrades half-upgraded v3 project to v4 (pnpm)', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + }, + "devDependencies": { + "@tailwindcss/cli": "workspace:^" + } + } + `, + 'tailwind.config.js': js` + /** @type {import('tailwindcss').Config} */ + module.exports = { + content: ['./src/**/*.{html,js}'], + } + `, + 'src/index.html': html` +
Hello World
+ `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, expect }) => { + // Ensure we are in a git repo + await exec('git init') + await exec('git add --all') + await exec('git commit -m "before migration"') + + // Fully upgrade to v4 + await exec('npx @tailwindcss/upgrade') + + // Undo all changes to the current repo. This will bring the repo back to a + // v3 state, but the `node_modules` will now have v4 installed. + await exec('git reset --hard HEAD') + + // Re-running the upgrade should result in an error + return expect(() => { + return exec('npx @tailwindcss/upgrade', {}, { ignoreStdErr: true }).catch((e) => { + // Replacing the current version with a hardcoded `v4` to make it stable + // when we release new minor/patch versions. + return Promise.reject( + stripVTControlCharacters(e.message.replace(/\d+\.\d+\.\d+/g, '4.0.0')), + ) + }) + }).rejects.toThrowErrorMatchingInlineSnapshot(` + "Command failed: npx @tailwindcss/upgrade + ≈ tailwindcss v4.0.0 + + │ ↳ Upgrading from Tailwind CSS \`v4.0.0\` + + │ ↳ Version mismatch + │ + │ \`\`\`diff + │ - "tailwindcss": "^3" (expected version in \`package.json\`) + │ + "tailwindcss": "4.0.0" (installed version in \`node_modules\`) + │ \`\`\` + │ + │ Make sure to run \`pnpm install\` and try again. + + " + `) + }, +) + +test( + 'upgrades half-upgraded v3 project to v4 (bun)', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + }, + "devDependencies": { + "@tailwindcss/cli": "workspace:^", + "bun": "^1.0.0" + } + } + `, + 'tailwind.config.js': js` + /** @type {import('tailwindcss').Config} */ + module.exports = { + content: ['./src/**/*.{html,js}'], + } + `, + 'src/index.html': html` +
Hello World
+ `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, expect }) => { + // Use `bun` to install dependencies + await exec('rm ./pnpm-lock.yaml') + try { + await exec('npx bun install', {}, { ignoreStdErr: true }) + } catch (e) { + // When preparing for a release, the version in `package.json` will point + // to a non-existent version because it's not published yet. + // TODO: Find a better approach to handle this and actually test it even + // on release branches. Note: the pnpm version _does_ work because of + // overrides in the package.json file. + if (`${e}`.includes('No version matching')) return + throw e + } + + // Ensure we are in a git repo + await exec('git init') + await exec('git add --all') + await exec('git commit -m "before migration"') + + // Fully upgrade to v4 + await exec('npx @tailwindcss/upgrade') + + // Undo all changes to the current repo. This will bring the repo back to a + // v3 state, but the `node_modules` will now have v4 installed. + await exec('git reset --hard HEAD') + + // Re-running the upgrade should result in an error + return expect(() => { + return exec('npx @tailwindcss/upgrade', {}, { ignoreStdErr: true }).catch((e) => { + // Replacing the current version with a hardcoded `v4` to make it stable + // when we release new minor/patch versions. + return Promise.reject( + stripVTControlCharacters(e.message.replace(/\d+\.\d+\.\d+/g, '4.0.0')), + ) + }) + }).rejects.toThrowErrorMatchingInlineSnapshot(` + "Command failed: npx @tailwindcss/upgrade + ≈ tailwindcss v4.0.0 + + │ ↳ Upgrading from Tailwind CSS \`v4.0.0\` + + │ ↳ Version mismatch + │ + │ \`\`\`diff + │ - "tailwindcss": "^3" (expected version in \`package.json\`) + │ + "tailwindcss": "4.0.0" (installed version in \`node_modules\`) + │ \`\`\` + │ + │ Make sure to run \`bun install\` and try again. + + " + `) + }, +) + +test( + 'upgrades half-upgraded v3 project to v4 (npm)', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + }, + "devDependencies": { + "@tailwindcss/cli": "workspace:^" + } + } + `, + 'tailwind.config.js': js` + /** @type {import('tailwindcss').Config} */ + module.exports = { + content: ['./src/**/*.{html,js}'], + } + `, + 'src/index.html': html` +
Hello World
+ `, + 'src/input.css': css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + }, + }, + async ({ exec, expect }) => { + // Use `npm` to install dependencies + await exec('rm ./pnpm-lock.yaml') + await exec('rm -rf ./node_modules') + try { + await exec('npm install', {}, { ignoreStdErr: true }) + } catch (e) { + // When preparing for a release, the version in `package.json` will point + // to a non-existent version because it's not published yet. + // TODO: Find a better approach to handle this and actually test it even + // on release branches. Note: the pnpm version _does_ work because of + // overrides in the package.json file. + if (`${e}`.includes('npm error code ETARGET')) return + throw e + } + + // Ensure we are in a git repo + await exec('git init') + await exec('git add --all') + await exec('git commit -m "before migration"') + + // Fully upgrade to v4 + await exec('npx @tailwindcss/upgrade') + + // Undo all changes to the current repo. This will bring the repo back to a + // v3 state, but the `node_modules` will now have v4 installed. + await exec('git reset --hard HEAD') + + // Re-running the upgrade should result in an error + return expect(() => { + return exec('npx @tailwindcss/upgrade', {}, { ignoreStdErr: true }).catch((e) => { + // Replacing the current version with a hardcoded `v4` to make it stable + // when we release new minor/patch versions. + return Promise.reject( + stripVTControlCharacters(e.message.replace(/\d+\.\d+\.\d+/g, '4.0.0')), + ) + }) + }).rejects.toThrowErrorMatchingInlineSnapshot(` + "Command failed: npx @tailwindcss/upgrade + ≈ tailwindcss v4.0.0 + + │ ↳ Upgrading from Tailwind CSS \`v4.0.0\` + + │ ↳ Version mismatch + │ + │ \`\`\`diff + │ - "tailwindcss": "^3" (expected version in \`package.json\`) + │ + "tailwindcss": "4.0.0" (installed version in \`node_modules\`) + │ \`\`\` + │ + │ Make sure to run \`npm install\` and try again. + + " + `) + }, +) diff --git a/integrations/utils.ts b/integrations/utils.ts new file mode 100644 index 000000000000..f9638b8572d7 --- /dev/null +++ b/integrations/utils.ts @@ -0,0 +1,695 @@ +import dedent from 'dedent' +import fastGlob from 'fast-glob' +import { exec, spawn } from 'node:child_process' +import fs from 'node:fs/promises' +import { platform, tmpdir } from 'node:os' +import path from 'node:path' +import { stripVTControlCharacters } from 'node:util' +import { RawSourceMap, SourceMapConsumer } from 'source-map-js' +import { test as defaultTest, type ExpectStatic } from 'vitest' +import { createLineTable } from '../packages/tailwindcss/src/source-maps/line-table' +import { escape } from '../packages/tailwindcss/src/utils/escape' + +const REPO_ROOT = path.join(__dirname, '..') +const PUBLIC_PACKAGES = (await fs.readdir(path.join(REPO_ROOT, 'dist'))).map((name) => + name.replace('tailwindcss-', '@tailwindcss/').replace('.tgz', ''), +) + +interface SpawnedProcess { + dispose: () => void + flush: () => void + onStdout: (predicate: (message: string) => boolean) => Promise + onStderr: (predicate: (message: string) => boolean) => Promise +} + +interface ChildProcessOptions { + cwd?: string + env?: Record +} + +interface ExecOptions { + ignoreStdErr?: boolean + stdin?: string +} + +interface TestConfig { + fs: { + [filePath: string]: string | Uint8Array + } + + timeout?: number + installDependencies?: boolean +} +interface TestContext { + root: string + expect: ExpectStatic + exec(command: string, options?: ChildProcessOptions, execOptions?: ExecOptions): Promise + spawn(command: string, options?: ChildProcessOptions): Promise + parseSourceMap(opts: string | SourceMapOptions): SourceMap + fs: { + write(filePath: string, content: string, encoding?: BufferEncoding): Promise + create(filePaths: string[]): Promise + read(filePath: string): Promise + glob(pattern: string): Promise<[string, string][]> + dumpFiles(pattern: string): Promise + expectFileToContain( + filePath: string, + contents: string | RegExp | (string | RegExp)[], + ): Promise + expectFileNotToContain(filePath: string, contents: string | string[]): Promise + } +} +type TestCallback = (context: TestContext) => Promise | void +interface TestFlags { + only?: boolean + skip?: boolean + debug?: boolean +} + +type SpawnActor = { predicate: (message: string) => boolean; resolve: () => void } + +export const IS_WINDOWS = platform() === 'win32' + +const TEST_TIMEOUT = IS_WINDOWS ? 120000 : 60000 +const ASSERTION_TIMEOUT = IS_WINDOWS ? 10000 : 5000 + +// On Windows CI, tmpdir returns a path containing a weird RUNNER~1 folder that +// apparently causes the vite builds to not work. +const TMP_ROOT = + process.env.CI && IS_WINDOWS ? path.dirname(process.env.GITHUB_WORKSPACE!) : tmpdir() + +export function test( + name: string, + config: TestConfig, + testCallback: TestCallback, + { only = false, skip = false, debug = false }: TestFlags = {}, +) { + return defaultTest( + name, + { + timeout: config.timeout ?? TEST_TIMEOUT, + retry: process.env.CI ? 2 : 0, + only: only || (!process.env.CI && debug), + skip, + concurrent: true, + }, + async (options) => { + let rootDir = debug ? path.join(REPO_ROOT, '.debug') : TMP_ROOT + await fs.mkdir(rootDir, { recursive: true }) + + let root = await fs.mkdtemp(path.join(rootDir, 'tailwind-integrations')) + + if (debug) { + console.log('Running test in debug mode. File system will be written to:') + console.log(root) + console.log() + } + + let context = { + root, + expect: options.expect, + parseSourceMap, + async exec( + command: string, + childProcessOptions: ChildProcessOptions = {}, + execOptions: ExecOptions = {}, + ) { + let cwd = childProcessOptions.cwd ?? root + if (debug && cwd !== root) { + let relative = path.relative(root, cwd) + if (relative[0] !== '.') relative = `./${relative}` + console.log(`> cd ${relative}`) + } + if (debug) console.log(`> ${command}`) + return new Promise((resolve, reject) => { + let child = exec( + command, + { + cwd, + ...childProcessOptions, + env: { + ...process.env, + ...childProcessOptions.env, + }, + }, + (error, stdout, stderr) => { + if (error) { + if (execOptions.ignoreStdErr !== true) console.error(stderr) + if (only || debug) { + console.error(stdout) + } + reject(error) + } else { + if (only || debug) { + console.log(stdout.toString() + '\n\n' + stderr.toString()) + } + resolve(stdout.toString() + '\n\n' + stderr.toString()) + } + }, + ) + if (execOptions.stdin) { + child.stdin?.write(execOptions.stdin) + child.stdin?.end() + } + }) + }, + async spawn(command: string, childProcessOptions: ChildProcessOptions = {}) { + let resolveDisposal: (() => void) | undefined + let rejectDisposal: ((error: Error) => void) | undefined + let disposePromise = new Promise((resolve, reject) => { + resolveDisposal = resolve + rejectDisposal = reject + }) + + let cwd = childProcessOptions.cwd ?? root + if (debug && cwd !== root) { + let relative = path.relative(root, cwd) + if (relative[0] !== '.') relative = `./${relative}` + console.log(`> cd ${relative}`) + } + if (debug) console.log(`>& ${command}`) + let child = spawn(command, { + cwd, + shell: true, + ...childProcessOptions, + env: { + ...process.env, + ...childProcessOptions.env, + }, + }) + + function dispose() { + if (!child.kill()) { + child.kill('SIGKILL') + } + + let timer = setTimeout( + () => + rejectDisposal?.(new Error(`spawned process (${command}) did not exit in time`)), + ASSERTION_TIMEOUT, + ) + disposePromise.finally(() => { + clearTimeout(timer) + }) + return disposePromise + } + disposables.push(dispose) + function onExit() { + resolveDisposal?.() + } + + let stdoutMessages: string[] = [] + let stderrMessages: string[] = [] + + let stdoutActors: SpawnActor[] = [] + let stderrActors: SpawnActor[] = [] + + function notifyNext(actors: SpawnActor[], messages: string[]) { + if (actors.length <= 0) return + let [next] = actors + + for (let [idx, message] of messages.entries()) { + if (next.predicate(message)) { + messages.splice(0, idx + 1) + let actorIdx = actors.indexOf(next) + actors.splice(actorIdx, 1) + next.resolve() + break + } + } + } + + let combined: ['stdout' | 'stderr', string][] = [] + + child.stdout.on('data', (result) => { + let content = result.toString() + if (debug || only) console.log(content) + combined.push(['stdout', content]) + for (let line of content.split('\n')) { + stdoutMessages.push(stripVTControlCharacters(line)) + } + notifyNext(stdoutActors, stdoutMessages) + }) + child.stderr.on('data', (result) => { + let content = result.toString() + if (debug || only) console.error(content) + combined.push(['stderr', content]) + for (let line of content.split('\n')) { + stderrMessages.push(stripVTControlCharacters(line)) + } + notifyNext(stderrActors, stderrMessages) + }) + child.on('exit', onExit) + child.on('error', (error) => { + if (error.name !== 'AbortError') { + throw error + } + }) + + options.onTestFailed(() => { + // In only or debug mode, messages are logged to the console + // immediately. + if (only || debug) return + + for (let [type, message] of combined) { + if (type === 'stdout') { + console.log(message) + } else { + console.error(message) + } + } + }) + + return { + dispose, + flush() { + stdoutActors.splice(0) + stderrActors.splice(0) + + stdoutMessages.splice(0) + stderrMessages.splice(0) + }, + onStdout(predicate: (message: string) => boolean) { + return new Promise((resolve) => { + stdoutActors.push({ predicate, resolve }) + notifyNext(stdoutActors, stdoutMessages) + }) + }, + onStderr(predicate: (message: string) => boolean) { + return new Promise((resolve) => { + stderrActors.push({ predicate, resolve }) + notifyNext(stderrActors, stderrMessages) + }) + }, + } + }, + fs: { + async write( + filename: string, + content: string | Uint8Array, + encoding: BufferEncoding = 'utf8', + ): Promise { + let full = path.join(root, filename) + let dir = path.dirname(full) + await fs.mkdir(dir, { recursive: true }) + + if (typeof content !== 'string') { + return await fs.writeFile(full, content) + } + + if (filename.endsWith('package.json')) { + content = await overwriteVersionsInPackageJson(content) + } + + // Ensure that files written on Windows use \r\n line ending + if (IS_WINDOWS) { + content = content.replace(/\n/g, '\r\n') + } + + await fs.writeFile(full, content, encoding) + }, + + async create(filenames: string[]): Promise { + for (let filename of filenames) { + let full = path.join(root, filename) + + let dir = path.dirname(full) + await fs.mkdir(dir, { recursive: true }) + await fs.writeFile(full, '') + } + }, + + async read(filePath: string) { + let content = await fs.readFile(path.resolve(root, filePath), 'utf8') + + // Ensure that files read on Windows have \r\n line endings removed + if (IS_WINDOWS) { + content = content.replace(/\r\n/g, '\n') + } + + return content + }, + async glob(pattern: string) { + let files = await fastGlob(pattern, { cwd: root }) + return Promise.all( + files.map(async (file) => { + let content = await fs.readFile(path.join(root, file), 'utf8') + return [ + file, + // Drop license comment + content.replace(/[\s\n]*\/\*![\s\S]*?\*\/[\s\n]*/g, ''), + ] + }), + ) + }, + async dumpFiles(pattern: string) { + let files = await context.fs.glob(pattern) + return `\n${files + .slice() + .sort((a: [string], z: [string]) => { + let aParts = a[0].split('/') + let zParts = z[0].split('/') + + let aFile = aParts.at(-1) + let zFile = zParts.at(-1) + + // Sort by depth, shallow first + if (aParts.length < zParts.length) return -1 + if (aParts.length > zParts.length) return 1 + + // Sort by folder names, alphabetically + for (let i = 0; i < aParts.length - 1; i++) { + let diff = aParts[i].localeCompare(zParts[i]) + if (diff !== 0) return diff + } + + // Sort by filename, sort files named `index` before others + if (aFile?.startsWith('index') && !zFile?.startsWith('index')) return -1 + if (zFile?.startsWith('index') && !aFile?.startsWith('index')) return 1 + + // Sort by filename, alphabetically + return a[0].localeCompare(z[0]) + }) + .map(([file, content]) => `--- ${file} ---\n${content || ''}`) + .join('\n\n') + .trim()}\n` + }, + async expectFileToContain(filePath, contents) { + return retryAssertion(async () => { + let fileContent = await this.read(filePath) + for (let content of Array.isArray(contents) ? contents : [contents]) { + if (content instanceof RegExp) { + options.expect(fileContent).toMatch(content) + } else { + options.expect(fileContent).toContain(content) + } + } + }) + }, + async expectFileNotToContain(filePath, contents) { + return retryAssertion(async () => { + let fileContent = await this.read(filePath) + for (let content of contents) { + options.expect(fileContent).not.toContain(content) + } + }) + }, + }, + } satisfies TestContext + + config.fs['.gitignore'] ??= txt` + node_modules/ + ` + + for (let [filename, content] of Object.entries(config.fs)) { + await context.fs.write(filename, content) + } + + let shouldInstallDependencies = config.installDependencies ?? true + + try { + // In debug mode, the directory is going to be inside the pnpm workspace + // of the tailwindcss package. This means that `pnpm install` will run + // pnpm install on the workspace instead (expect if the root dir defines + // a separate workspace). We work around this by using the + // `--ignore-workspace` flag. + if (shouldInstallDependencies) { + let ignoreWorkspace = debug && !config.fs['pnpm-workspace.yaml'] + await context.exec(`pnpm install${ignoreWorkspace ? ' --ignore-workspace' : ''}`) + } + } catch (error: any) { + console.error(error) + console.error(error.stdout?.toString()) + console.error(error.stderr?.toString()) + throw error + } + + let disposables: (() => Promise)[] = [] + + async function dispose() { + await Promise.all(disposables.map((dispose) => dispose())) + + if (!debug) { + await gracefullyRemove(root) + } + } + + options.onTestFinished(dispose) + + // Make it a git repository, and commit all files + if (only || debug) { + try { + await context.exec('git init', { cwd: root }) + await context.exec('git add --all', { cwd: root }) + await context.exec('git commit -m "before migration"', { cwd: root }) + } catch (error: any) { + console.error(error) + console.error(error.stdout?.toString()) + console.error(error.stderr?.toString()) + throw error + } + } + + return await testCallback(context) + }, + ) +} +test.only = (name: string, config: TestConfig, testCallback: TestCallback) => { + return test(name, config, testCallback, { only: true }) +} +test.skip = (name: string, config: TestConfig, testCallback: TestCallback) => { + return test(name, config, testCallback, { skip: true }) +} +test.debug = (name: string, config: TestConfig, testCallback: TestCallback) => { + return test(name, config, testCallback, { debug: true }) +} + +// Maps package names to their tarball filenames. See scripts/pack-packages.ts +// for more details. +function pkgToFilename(name: string) { + return `${name.replace('@', '').replace('/', '-')}.tgz` +} + +async function overwriteVersionsInPackageJson(content: string): Promise { + let json = JSON.parse(content) + + // Resolve all workspace:^ versions to local tarballs + for (let key of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) { + let dependencies = json[key] || {} + for (let dependency in dependencies) { + if (dependencies[dependency] === 'workspace:^') { + dependencies[dependency] = resolveVersion(dependency) + } + } + } + + // Inject transitive dependency overwrite. This is necessary because + // @tailwindcss/vite internally depends on a specific version of + // @tailwindcss/oxide and we instead want to resolve it to the locally built + // version. + json.pnpm ||= {} + json.pnpm.overrides ||= {} + for (let pkg of PUBLIC_PACKAGES) { + if (pkg === 'tailwindcss') { + // We want to be explicit about the `tailwindcss` package so our tests can + // also import v3 without conflicting v4 tarballs. + json.pnpm.overrides['@tailwindcss/node>tailwindcss'] = resolveVersion(pkg) + json.pnpm.overrides['@tailwindcss/upgrade>tailwindcss'] = resolveVersion(pkg) + json.pnpm.overrides['@tailwindcss/cli>tailwindcss'] = resolveVersion(pkg) + json.pnpm.overrides['@tailwindcss/postcss>tailwindcss'] = resolveVersion(pkg) + json.pnpm.overrides['@tailwindcss/vite>tailwindcss'] = resolveVersion(pkg) + json.pnpm.overrides['@tailwindcss/webpack>tailwindcss'] = resolveVersion(pkg) + } else { + json.pnpm.overrides[pkg] = resolveVersion(pkg) + } + } + + return JSON.stringify(json, null, 2) +} + +function resolveVersion(dependency: string) { + let tarball = path.join(REPO_ROOT, 'dist', pkgToFilename(dependency)) + return `file:${tarball}` +} + +export function stripTailwindComment(content: string) { + return content.replace(/\/\*! tailwindcss .*? \*\//g, '').trim() +} + +export let svg = dedent +export let css = dedent +export let html = dedent +export let ts = dedent +export let js = dedent +export let jsx = dedent +export let json = dedent +export let yaml = dedent +export let txt = dedent + +export function binary(str: string | TemplateStringsArray, ...values: unknown[]): Uint8Array { + let base64 = typeof str === 'string' ? str : String.raw(str, ...values) + + return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)) +} + +export function candidate(strings: TemplateStringsArray, ...values: any[]) { + let output: string[] = [] + for (let i = 0; i < strings.length; i++) { + output.push(strings[i]) + if (i < values.length) { + output.push(values[i]) + } + } + + return `.${escape(output.join('').trim())}` +} + +export async function retryAssertion( + fn: () => Promise, + { timeout = ASSERTION_TIMEOUT, delay = 5 }: { timeout?: number; delay?: number } = {}, +) { + let end = Date.now() + timeout + let error: any + while (Date.now() < end) { + try { + return await fn() + } catch (err) { + error = err + await new Promise((resolve) => setTimeout(resolve, delay)) + } + } + throw error +} + +export async function fetchStyles(base: string, path = '/'): Promise { + while (base.endsWith('/')) { + base = base.slice(0, -1) + } + + let index = await fetch(`${base}${path}`) + let html = await index.text() + + let linkRegex = /]*>([\s\S]*?)<\/style>/gi + + let stylesheets: string[] = [] + + let paths: string[] = [] + for (let match of html.matchAll(linkRegex)) { + let path: string = match[1] + if (path.startsWith('./')) { + path = path.slice(1) + } + paths.push(path) + } + stylesheets.push( + ...(await Promise.all( + paths.map(async (path) => { + let css = await fetch(`${base}${path}`, { + headers: { + Accept: 'text/css', + }, + }) + return await css.text() + }), + )), + ) + + for (let match of html.matchAll(styleRegex)) { + stylesheets.push(match[1]) + } + + return stylesheets.reduce((acc, css) => { + if (acc.length > 0) acc += '\n' + acc += css + return acc + }, '') +} + +async function gracefullyRemove(dir: string) { + // Skip removing the directory in CI because it can stall on Windows + if (!process.env.CI) { + await fs.rm(dir, { recursive: true, force: true }).catch((error) => { + console.log(`Failed to remove ${dir}`, error) + }) + } +} + +const SOURCE_MAP_COMMENT = /^\/\*# sourceMappingURL=data:application\/json;base64,(.*) \*\/$/ + +export interface SourceMap { + at( + line: number, + column: number, + ): { + source: string | null + original: string + generated: string + } +} + +interface SourceMapOptions { + /** + * A raw source map + * + * This may be a string or an object. Strings will be decoded. + */ + map: string | object + + /** + * The content of the generated file the source map is for + */ + content: string + + /** + * The encoding of the source map + * + * Can be used to decode a base64 map (e.g. an inline source map URI) + */ + encoding?: BufferEncoding +} + +function parseSourceMap(opts: string | SourceMapOptions): SourceMap { + if (typeof opts === 'string') { + let lines = opts.trimEnd().split('\n') + let comment = lines.at(-1) ?? '' + let map = String(comment).match(SOURCE_MAP_COMMENT)?.[1] ?? null + if (!map) throw new Error('No source map comment found') + + return parseSourceMap({ + map, + content: lines.slice(0, -1).join('\n'), + encoding: 'base64', + }) + } + + let rawMap: RawSourceMap + let content = opts.content + + if (typeof opts.map === 'object') { + rawMap = opts.map as RawSourceMap + } else { + rawMap = JSON.parse(Buffer.from(opts.map, opts.encoding ?? 'utf-8').toString()) + } + + let map = new SourceMapConsumer(rawMap) + let generatedTable = createLineTable(content) + + return { + at(line: number, column: number) { + let pos = map.originalPositionFor({ line, column }) + let source = pos.source ? map.sourceContentFor(pos.source) : null + let originalTable = createLineTable(source ?? '') + let originalOffset = originalTable.findOffset(pos) + let generatedOffset = generatedTable.findOffset({ line, column }) + + return { + source: pos.source, + original: source + ? source.slice(originalOffset, originalOffset + 10).trim() + '...' + : '(none)', + generated: content.slice(generatedOffset, generatedOffset + 10).trim() + '...', + } + }, + } +} diff --git a/integrations/vite/astro.test.ts b/integrations/vite/astro.test.ts new file mode 100644 index 000000000000..f828b5071b60 --- /dev/null +++ b/integrations/vite/astro.test.ts @@ -0,0 +1,201 @@ +import { candidate, css, fetchStyles, html, js, json, retryAssertion, test, ts } from '../utils' + +test( + 'dev mode', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "astro": "^4.15.2", + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + } + } + `, + 'astro.config.mjs': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'astro/config' + + // https://astro.build/config + export default defineConfig({ + vite: { plugins: [tailwindcss()] }, + build: { inlineStylesheets: 'never' }, + }) + `, + 'src/pages/index.astro': html` +
Hello, world!
+ + + `, + }, + }, + async ({ fs, spawn, expect }) => { + let process = await spawn('pnpm astro dev') + await process.onStdout((m) => m.includes('ready in')) + + let url = '' + await process.onStdout((m) => { + let match = /Local\s*(http.*)\//.exec(m) + if (match) url = match[1] + return Boolean(url) + }) + + await process.onStdout((m) => m.includes('watching for file changes')) + + await retryAssertion(async () => { + let css = await fetchStyles(url) + expect(css).toContain(candidate`underline`) + }) + + await retryAssertion(async () => { + await fs.write( + 'src/pages/index.astro', + html` +
Hello, world!
+ + + `, + ) + + let css = await fetchStyles(url) + expect(css).toContain(candidate`underline`) + expect(css).toContain(candidate`font-bold`) + }) + }, +) + +test( + 'build mode', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "astro": "^4.15.2", + "react": "^19", + "react-dom": "^19", + "@astrojs/react": "^4", + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + } + } + `, + 'astro.config.mjs': ts` + import tailwindcss from '@tailwindcss/vite' + import react from '@astrojs/react' + import { defineConfig } from 'astro/config' + + // https://astro.build/config + export default defineConfig({ + vite: { plugins: [tailwindcss()] }, + integrations: [react()], + build: { inlineStylesheets: 'never' }, + }) + `, + // prettier-ignore + 'src/pages/index.astro': html` + --- + import ClientOnly from './client-only'; + --- + +
Hello, world!
+ + + + + `, + 'src/pages/client-only.jsx': js` + export default function ClientOnly() { + return
Hello, world!
+ } + `, + }, + }, + async ({ fs, exec, expect }) => { + await exec('pnpm astro build') + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + + await fs.expectFileToContain(files[0][0], [candidate`underline`, candidate`overline`]) + }, +) + +// https://github.com/tailwindlabs/tailwindcss/issues/19677 +test( + 'import aliases should work in + + +

Astro

+ + + `, + 'src/styles/global.css': css`@import 'tailwindcss';`, + }, + }, + async ({ fs, exec, expect }) => { + await exec('pnpm astro build') + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + + await fs.expectFileToContain(files[0][0], [candidate`underline`]) + }, +) diff --git a/integrations/vite/config.test.ts b/integrations/vite/config.test.ts new file mode 100644 index 000000000000..0ac68f5e9593 --- /dev/null +++ b/integrations/vite/config.test.ts @@ -0,0 +1,287 @@ +import { candidate, css, fetchStyles, html, js, json, retryAssertion, test, ts } from '../utils' + +test( + 'Config files (CJS)', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'index.html': html` + + + + +
+ + `, + 'tailwind.config.cjs': js` + module.exports = { + theme: { + extend: { + colors: { + primary: 'blue', + }, + }, + }, + } + `, + 'src/index.css': css` + @import 'tailwindcss'; + @config '../tailwind.config.cjs'; + `, + }, + }, + async ({ fs, exec, expect }) => { + await exec('pnpm vite build') + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] + + await fs.expectFileToContain(filename, [ + // + candidate`text-primary`, + ]) + }, +) + +test( + 'Config files (ESM)', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'index.html': html` + + + + +
+ + `, + 'tailwind.config.js': js` + export default { + theme: { + extend: { + colors: { + primary: 'blue', + }, + }, + }, + } + `, + 'src/index.css': css` + @import 'tailwindcss'; + @config '../tailwind.config.js'; + `, + }, + }, + async ({ fs, exec, expect }) => { + await exec('pnpm vite build') + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] + + await fs.expectFileToContain(filename, [ + // + candidate`text-primary`, + ]) + }, +) + +test( + 'Config files (CJS, dev mode)', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'index.html': html` + + + + +
+ + `, + 'tailwind.config.cjs': js` + const myColor = require('./my-color.cjs') + module.exports = { + theme: { + extend: { + colors: { + primary: myColor, + }, + }, + }, + } + `, + 'my-color.cjs': js`module.exports = 'blue'`, + 'src/index.css': css` + @import 'tailwindcss'; + @config '../tailwind.config.cjs'; + `, + }, + }, + async ({ fs, spawn, expect }) => { + let process = await spawn('pnpm vite dev') + await process.onStdout((m) => m.includes('ready in')) + + let url = '' + await process.onStdout((m) => { + let match = /Local:\s*(http.*)\//.exec(m) + if (match) url = match[1] + return Boolean(url) + }) + + await retryAssertion(async () => { + let css = await fetchStyles(url, '/index.html') + expect(css).toContain(candidate`text-primary`) + expect(css).toContain('color: blue') + }) + + await retryAssertion(async () => { + await fs.write('my-color.cjs', js`module.exports = 'red'`) + + let css = await fetchStyles(url, '/index.html') + expect(css).toContain(candidate`text-primary`) + expect(css).toContain('color: red') + }) + }, +) + +test( + 'Config files (ESM, dev mode)', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'index.html': html` + + + + +
+ + `, + 'tailwind.config.mjs': js` + import myColor from './my-color.mjs' + export default { + theme: { + extend: { + colors: { + primary: myColor, + }, + }, + }, + } + `, + 'my-color.mjs': js`export default 'blue'`, + 'src/index.css': css` + @import 'tailwindcss'; + @config '../tailwind.config.mjs'; + `, + }, + }, + async ({ fs, spawn, expect }) => { + let process = await spawn('pnpm vite dev') + await process.onStdout((m) => m.includes('ready in')) + + let url = '' + await process.onStdout((m) => { + let match = /Local:\s*(http.*)\//.exec(m) + if (match) url = match[1] + return Boolean(url) + }) + + await retryAssertion(async () => { + let css = await fetchStyles(url, '/index.html') + expect(css).toContain(candidate`text-primary`) + expect(css).toContain('color: blue') + }) + + await retryAssertion(async () => { + await fs.write('my-color.mjs', js`export default 'red'`) + + let css = await fetchStyles(url, '/index.html') + expect(css).toContain(candidate`text-primary`) + expect(css).toContain('color: red') + }) + }, +) diff --git a/integrations/vite/css-modules.test.ts b/integrations/vite/css-modules.test.ts new file mode 100644 index 000000000000..a680ab9b703c --- /dev/null +++ b/integrations/vite/css-modules.test.ts @@ -0,0 +1,65 @@ +import { describe } from 'vitest' +import { css, html, test, ts, txt } from '../utils' + +describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { + test( + `dev mode`, + { + fs: { + 'package.json': txt` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'index.html': html` + + + + +
+ + `, + 'src/component.ts': ts` + import { foo } from './component.module.css' + let root = document.getElementById('root') + root.className = foo + root.innerText = 'Hello, world!' + `, + 'src/component.module.css': css` + @import 'tailwindcss/utilities'; + + .foo { + @apply underline; + } + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec(`pnpm vite build`) + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] + + await fs.expectFileToContain(filename, [/text-decoration-line: underline;/gi]) + }, + ) +}) diff --git a/integrations/vite/html-style-blocks.test.ts b/integrations/vite/html-style-blocks.test.ts new file mode 100644 index 000000000000..22bb91d78cfc --- /dev/null +++ b/integrations/vite/html-style-blocks.test.ts @@ -0,0 +1,58 @@ +import { html, json, test, ts } from '../utils' + +test( + 'transforms html style blocks', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "@tailwindcss/vite": "workspace:^", + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import { defineConfig } from 'vite' + import tailwindcss from '@tailwindcss/vite' + + export default defineConfig({ + plugins: [tailwindcss()], + }) + `, + 'index.html': html` + + + +
+ + + + `, + }, + }, + async ({ fs, exec, expect }) => { + await exec('pnpm vite build') + + expect(await fs.dumpFiles('dist/*.html')).toMatchInlineSnapshot(` + " + --- dist/index.html --- + + + +
+ + + + " + `) + }, +) diff --git a/integrations/vite/ignored-packages.test.ts b/integrations/vite/ignored-packages.test.ts new file mode 100644 index 000000000000..1efa9dc6136a --- /dev/null +++ b/integrations/vite/ignored-packages.test.ts @@ -0,0 +1,81 @@ +import { candidate, css, fetchStyles, html, js, retryAssertion, test, ts, txt } from '../utils' + +const WORKSPACE = { + fs: { + 'package.json': txt` + { + "type": "module", + "dependencies": { + "tailwind-merge": "^2", + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'index.html': html` + + + + + `, + 'src/index.js': js` + import { twMerge } from 'tailwind-merge' + + twMerge('underline') + + console.log('underline') + `, + 'src/index.css': css`@import 'tailwindcss/utilities' layer(utilities);`, + }, +} + +test( + 'does not scan tailwind-merge in production builds', + WORKSPACE, + async ({ fs, exec, expect }) => { + await exec('pnpm vite build') + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + let [, content] = files[0] + + expect(content).toMatchInlineSnapshot(` + "@layer utilities { + .underline { + text-decoration-line: underline; + } + } + " + `) + }, +) + +test('does not scan tailwind-merge in dev builds', WORKSPACE, async ({ spawn, expect }) => { + let process = await spawn('pnpm vite dev') + await process.onStdout((m) => m.includes('ready in')) + + let url = '' + await process.onStdout((m) => { + let match = /Local:\s*(http.*)\//.exec(m) + if (match) url = match[1] + return Boolean(url) + }) + + await retryAssertion(async () => { + let styles = await fetchStyles(url, '/index.html') + + expect(styles).not.toContain(candidate`flex`) + }) +}) diff --git a/integrations/vite/index.test.ts b/integrations/vite/index.test.ts new file mode 100644 index 000000000000..28c218b81227 --- /dev/null +++ b/integrations/vite/index.test.ts @@ -0,0 +1,1277 @@ +import path from 'node:path' +import { describe } from 'vitest' +import { + candidate, + css, + fetchStyles, + html, + js, + json, + jsx, + retryAssertion, + test, + ts, + txt, + yaml, +} from '../utils' + +describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { + test( + `production build`, + { + fs: { + 'package.json': json`{}`, + 'pnpm-workspace.yaml': yaml` + # + packages: + - project-a + `, + 'project-a/package.json': txt` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} + "vite": "^7" + } + } + `, + 'project-a/vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'project-a/index.html': html` + + + + +
Hello, world!
+ + `, + 'project-a/tailwind.config.js': js` + export default { + content: ['../project-b/src/**/*.js'], + } + `, + 'project-a/src/index.css': css` + @reference 'tailwindcss/theme'; + @import 'tailwindcss/utilities'; + @config '../tailwind.config.js'; + @source '../../project-b/src/**/*.html'; + `, + 'project-b/src/index.html': html` +
+ `, + 'project-b/src/index.js': js` + const className = "content-['project-b/src/index.js']" + module.exports = { className } + `, + }, + }, + async ({ root, fs, exec, expect }) => { + await exec('pnpm vite build', { cwd: path.join(root, 'project-a') }) + + let files = await fs.glob('project-a/dist/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] + + await fs.expectFileToContain(filename, [ + candidate`underline`, + candidate`m-2`, + candidate`flex`, + candidate`content-['project-b/src/index.js']`, + ]) + }, + ) + + test( + 'dev mode', + { + fs: { + 'package.json': json`{}`, + 'pnpm-workspace.yaml': yaml` + # + packages: + - project-a + `, + 'project-a/package.json': txt` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} + "vite": "^7" + } + } + `, + 'project-a/vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'project-a/index.html': html` + + + + +
Hello, world!
+ + `, + 'project-a/about.html': html` + + + + +
Tailwind Labs
+ + `, + 'project-a/tailwind.config.js': js` + export default { + content: ['../project-b/src/**/*.js'], + } + `, + 'project-a/src/index.css': css` + @reference 'tailwindcss/theme'; + @import 'tailwindcss/utilities'; + @import './imported.css'; + @config '../tailwind.config.js'; + @source '../../project-b/src/**/*.html'; + `, + 'project-a/src/imported.css': css` + .imported { + color: red; + } + `, + 'project-b/src/index.html': html` +
+ `, + 'project-b/src/index.js': js` + const className = "content-['project-b/src/index.js']" + module.exports = { className } + `, + }, + }, + async ({ root, spawn, fs, expect }) => { + let process = await spawn('pnpm vite dev', { + cwd: path.join(root, 'project-a'), + }) + await process.onStdout((m) => m.includes('ready in')) + + let url = '' + await process.onStdout((m) => { + let match = /Local:\s*(http.*)\//.exec(m) + if (match) url = match[1] + return Boolean(url) + }) + + await retryAssertion(async () => { + let styles = await fetchStyles(url, '/index.html') + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`flex`) + expect(styles).toContain(candidate`font-bold`) + expect(styles).toContain(candidate`imported`) + }) + + await retryAssertion(async () => { + // Updates are additive and cause new candidates to be added. + await fs.write( + 'project-a/index.html', + html` + + + + +
Hello, world!
+ + `, + ) + + let styles = await fetchStyles(url) + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`flex`) + expect(styles).toContain(candidate`font-bold`) + expect(styles).toContain(candidate`imported`) + expect(styles).toContain(candidate`m-2`) + }) + + await retryAssertion(async () => { + // Manually added `@source`s are watched and trigger a rebuild + await fs.write( + 'project-b/src/index.js', + js` + const className = "[.changed_&]:content-['project-b/src/index.js']" + module.exports = { className } + `, + ) + + let styles = await fetchStyles(url) + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`flex`) + expect(styles).toContain(candidate`font-bold`) + expect(styles).toContain(candidate`imported`) + expect(styles).toContain(candidate`m-2`) + expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`) + }) + + await retryAssertion(async () => { + // After updates to the CSS file, all previous candidates should still be in + // the generated CSS + await fs.write( + 'project-a/src/index.css', + css` + ${await fs.read('project-a/src/index.css')} + + .red { + color: red; + } + `, + ) + + let styles = await fetchStyles(url) + expect(styles).toContain(candidate`red`) + expect(styles).toContain(candidate`flex`) + expect(styles).toContain(candidate`imported`) + expect(styles).toContain(candidate`m-2`) + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`) + expect(styles).toContain(candidate`font-bold`) + }) + + await retryAssertion(async () => { + // Trigger a partial rebuild for the next test + await fs.write( + 'project-a/index.html', + html` + + + + +
Hello, world!
+ + `, + ) + let styles = await fetchStyles(url) + expect(styles).toContain(candidate`m-4`) + }) + + await retryAssertion(async () => { + // Changing an `@imported` CSS file after a partial rebuild also triggers the correct update + await fs.write( + 'project-a/src/imported.css', + css` + .imported-updated { + color: red; + } + `, + ) + + let styles = await fetchStyles(url) + expect(styles).toContain(candidate`red`) + expect(styles).toContain(candidate`flex`) + expect(styles).toContain(candidate`m-2`) + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`) + expect(styles).toContain(candidate`font-bold`) + expect(styles).toContain(candidate`imported-updated`) + }) + }, + ) + + test( + 'watch mode', + { + fs: { + 'package.json': json`{}`, + 'pnpm-workspace.yaml': yaml` + # + packages: + - project-a + `, + 'project-a/package.json': txt` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} + "vite": "^7" + } + } + `, + 'project-a/vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'project-a/index.html': html` + + + + +
Hello, world!
+ + `, + 'project-a/tailwind.config.js': js` + export default { + content: ['../project-b/src/**/*.js'], + } + `, + 'project-a/src/index.css': css` + @reference 'tailwindcss/theme'; + @import 'tailwindcss/utilities'; + @import './custom-theme.css'; + @config '../tailwind.config.js'; + @source '../../project-b/src/**/*.html'; + `, + 'project-a/src/custom-theme.css': css` + /* Will be overwritten later */ + @theme { + --color-primary: black; + } + `, + 'project-b/src/index.html': html` +
+ `, + 'project-b/src/index.js': js` + const className = "content-['project-b/src/index.js']" + module.exports = { className } + `, + }, + }, + async ({ root, spawn, fs, expect }) => { + let process = await spawn('pnpm vite build --watch', { + cwd: path.join(root, 'project-a'), + }) + await process.onStdout((m) => m.includes('built in')) + + let filename = '' + await retryAssertion(async () => { + let files = await fs.glob('project-a/dist/**/*.css') + expect(files).toHaveLength(1) + filename = files[0][0] + }) + + await fs.expectFileToContain(filename, [ + candidate`underline`, + candidate`flex`, + css` + .text-primary { + color: var(--color-primary); + } + `, + ]) + + await retryAssertion(async () => { + await fs.write( + 'project-a/src/custom-theme.css', + css` + /* Overriding the primary color */ + @theme { + --color-primary: red; + } + `, + ) + + let files = await fs.glob('project-a/dist/**/*.css') + expect(files).toHaveLength(1) + let [, styles] = files[0] + + expect(styles).toContain(css` + .text-primary { + color: var(--color-primary); + } + `) + }) + + await retryAssertion(async () => { + // Updates are additive and cause new candidates to be added. + await fs.write( + 'project-a/index.html', + html` + + + + +
Hello, world!
+ + `, + ) + + let files = await fs.glob('project-a/dist/**/*.css') + expect(files).toHaveLength(1) + let [, styles] = files[0] + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`flex`) + expect(styles).toContain(candidate`m-2`) + }) + + await retryAssertion(async () => { + // Manually added `@source`s are watched and trigger a rebuild + await fs.write( + 'project-b/src/index.js', + js` + const className = "[.changed_&]:content-['project-b/src/index.js']" + module.exports = { className } + `, + ) + + let files = await fs.glob('project-a/dist/**/*.css') + expect(files).toHaveLength(1) + let [, styles] = files[0] + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`flex`) + expect(styles).toContain(candidate`m-2`) + expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`) + }) + + await retryAssertion(async () => { + // After updates to the CSS file, all previous candidates should still be in + // the generated CSS + await fs.write( + 'project-a/src/index.css', + css` + ${await fs.read('project-a/src/index.css')} + + .red { + color: red; + } + `, + ) + + let files = await fs.glob('project-a/dist/**/*.css') + expect(files).toHaveLength(1) + let [, styles] = files[0] + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`flex`) + expect(styles).toContain(candidate`m-2`) + expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`) + expect(styles).toContain(candidate`red`) + }) + }, + ) + + describe.sequential.each([['^6'], ['7.0.8'], ['7.1.12'], ['7.3.1'], ['8.0.0']])( + 'Using Vite %s', + (version) => { + test( + 'external source file changes trigger a full reload', + { + fs: { + 'package.json': json`{}`, + 'pnpm-workspace.yaml': yaml` + # + packages: + - project-a + `, + 'project-a/package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} + "vite": "${version}" + } + } + `, + 'project-a/vite.config.ts': ts` + import fs from 'node:fs' + import path from 'node:path' + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, + build: { cssMinify: false }, + plugins: [tailwindcss()], + logLevel: 'info', + }) + `, + 'project-a/index.html': html` + + + + + +
+ + + + `, + 'project-a/src/main.ts': jsx`import { classes } from './app'`, + 'project-a/src/app.ts': jsx`export let classes = "content-['project-a/src/app.ts']"`, + 'project-a/src/index.css': css` + @import 'tailwindcss'; + @source '../../project-b/**/*.php'; + `, + 'project-b/src/index.php': html` +
+ `, + }, + }, + async ({ root, spawn, fs, expect }) => { + let process = await spawn('pnpm vite dev --debug hmr', { + cwd: path.join(root, 'project-a'), + }) + await process.onStdout((m) => m.includes('ready in')) + + let url = '' + await process.onStdout((m) => { + let match = /Local:\s*(http.*)\//.exec(m) + if (match) url = match[1] + return Boolean(url) + }) + + await retryAssertion(async () => { + let styles = await fetchStyles(url, '/index.html') + expect(styles).toContain(candidate`content-['project-b/src/index.php']`) + }) + + // Flush all messages so that we can be sure the next messages are from + // the file changes we're about to make + process.flush() + + // Changing an external .php file should trigger a full reload + { + await fs.write( + 'project-b/src/index.php', + txt`
`, + ) + + // Ensure the page reloaded + if (version === '^6' || version === '7.0.8') { + await process.onStdout((m) => m.includes('page reload') && m.includes('index.php')) + } else { + await process.onStderr( + (m) => m.includes('vite:hmr (client)') && m.includes('index.php'), + ) + } + await process.onStderr((m) => m.includes('vite:hmr (ssr)') && m.includes('index.php')) + + // Ensure the styles were regenerated with the new content + let styles = await fetchStyles(url, '/index.html') + expect(styles).toContain(candidate`content-['updated:project-b/src/index.php']`) + } + }, + ) + }, + ) + + test( + `source(none) disables looking at the module graph`, + { + fs: { + 'package.json': json`{}`, + 'pnpm-workspace.yaml': yaml` + # + packages: + - project-a + `, + 'project-a/package.json': txt` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} + "vite": "^7" + } + } + `, + 'project-a/vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'project-a/index.html': html` + + + + +
Hello, world!
+ + `, + 'project-a/src/index.css': css` + @import 'tailwindcss' source(none); + @source '../../project-b/src/**/*.html'; + `, + 'project-b/src/index.html': html` +
+ `, + 'project-b/src/index.js': js` + const className = "content-['project-b/src/index.js']" + module.exports = { className } + `, + }, + }, + async ({ root, fs, exec, expect }) => { + await exec('pnpm vite build', { cwd: path.join(root, 'project-a') }) + + let files = await fs.glob('project-a/dist/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] + + // `underline` and `m-2` are only present from files in the module graph + // which we've explicitly disabled with source(none) so they should not + // be present + await fs.expectFileNotToContain(filename, [ + // + candidate`underline`, + candidate`m-2`, + ]) + + // The files from `project-b` should be included because there is an + // explicit `@source` directive for it + await fs.expectFileToContain(filename, [ + // + candidate`flex`, + ]) + + // The explicit source directive only covers HTML files, so the JS file + // should not be included + await fs.expectFileNotToContain(filename, [ + // + candidate`content-['project-b/src/index.js']`, + ]) + }, + ) + + test( + `source("…") filters the module graph`, + { + fs: { + 'package.json': json`{}`, + 'pnpm-workspace.yaml': yaml` + # + packages: + - project-a + `, + 'project-a/package.json': txt` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} + "vite": "^7" + } + } + `, + 'project-a/vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'project-a/index.html': html` + + + + +
Hello, world!
+ + + `, + 'project-a/app/index.js': js` + const className = "content-['project-a/app/index.js']" + export default { className } + `, + 'project-a/src/index.css': css` + @import 'tailwindcss' source('../app'); + @source '../../project-b/src/**/*.html'; + `, + 'project-b/src/index.html': html` +
+ `, + 'project-b/src/index.js': js` + const className = "content-['project-b/src/index.js']" + module.exports = { className } + `, + }, + }, + async ({ root, fs, exec, expect }) => { + await exec('pnpm vite build', { cwd: path.join(root, 'project-a') }) + + let files = await fs.glob('project-a/dist/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] + + // `underline` and `m-2` are present in files in the module graph but + // we've filtered the module graph such that we only look in + // `./app/**/*` so they should not be present + await fs.expectFileNotToContain(filename, [ + // + candidate`underline`, + candidate`m-2`, + candidate`content-['project-a/index.html']`, + ]) + + // We've filtered the module graph to only look in ./app/**/* so the + // candidates from that project should be present + await fs.expectFileToContain(filename, [ + // + candidate`content-['project-a/app/index.js']`, + ]) + + // Even through we're filtering the module graph explicit sources are + // additive and as such files from `project-b` should be included + // because there is an explicit `@source` directive for it + await fs.expectFileToContain(filename, [ + // + candidate`content-['project-b/src/index.html']`, + ]) + + // The explicit source directive only covers HTML files, so the JS file + // should not be included + await fs.expectFileNotToContain(filename, [ + // + candidate`content-['project-b/src/index.js']`, + ]) + }, + ) + + test( + `source("…") must be a directory`, + { + fs: { + 'package.json': json`{}`, + 'pnpm-workspace.yaml': yaml` + # + packages: + - project-a + `, + 'project-a/package.json': txt` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} + "vite": "^7" + } + } + `, + 'project-a/vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'project-a/index.html': html` + + + + +
Hello, world!
+ + + `, + 'project-a/app/index.js': js` + const className = "content-['project-a/app/index.js']" + export default { className } + `, + 'project-a/src/index.css': css` + @import 'tailwindcss' source('../i-do-not-exist'); + @source '../../project-b/src/**/*.html'; + `, + 'project-b/src/index.html': html` +
+ `, + 'project-b/src/index.js': js` + const className = "content-['project-b/src/index.js']" + module.exports = { className } + `, + }, + }, + async ({ root, fs, exec, expect }) => { + await expect(() => + exec('pnpm vite build', { cwd: path.join(root, 'project-a') }, { ignoreStdErr: true }), + ).rejects.toThrowError('The `source(../i-do-not-exist)` does not exist') + + let files = await fs.glob('project-a/dist/**/*.css') + expect(files).toHaveLength(0) + }, + ) + + test( + 'source(…) and `@source` are relative to the file they are in', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'index.html': html` + + + + + `, + '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` +
+ `, + + // 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` +
+ `, + + // Project C should apply auto source detection, therefore known + // extensions and folders should be included in the output. + 'project-c/src/index.html': html` +
+ `, + }, + }, + async ({ fs, exec, spawn, root, expect }) => { + await exec('pnpm vite build', { cwd: root }) + + let content = await fs.dumpFiles('./dist/assets/*.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']`) + }, + ) +}) + +test( + `demote Tailwind roots to regular CSS files and back to Tailwind roots`, + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'index.html': html` + + + + +
Hello, world!
+ + `, + 'about.html': html` + + + + +
Tailwind Labs
+ + `, + 'src/index.css': css`@import 'tailwindcss';`, + }, + }, + async ({ spawn, fs, expect }) => { + let process = await spawn('pnpm vite dev') + await process.onStdout((m) => m.includes('ready in')) + + let url = '' + await process.onStdout((m) => { + let match = /Local:\s*(http.*)\//.exec(m) + if (match) url = match[1] + return Boolean(url) + }) + + await retryAssertion(async () => { + let styles = await fetchStyles(url, '/index.html') + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`font-bold`) + }) + + await retryAssertion(async () => { + // We change the CSS file so it is no longer a valid Tailwind root. + await fs.write('src/index.css', css`@import 'tailwindcss';`) + + let styles = await fetchStyles(url) + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`font-bold`) + }) + }, +) + +test( + `does not interfere with ?raw and ?url static asset handling`, + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'index.html': html` + + + + `, + 'src/index.js': js` + import url from './index.css?url' + import raw from './index.css?raw' + `, + 'src/index.css': css`@import 'tailwindcss';`, + }, + }, + async ({ spawn, expect }) => { + let process = await spawn('pnpm vite dev') + await process.onStdout((m) => m.includes('ready in')) + + let baseUrl = '' + await process.onStdout((m) => { + let match = /Local:\s*(http.*)\//.exec(m) + if (match) baseUrl = match[1] + return Boolean(baseUrl) + }) + + await retryAssertion(async () => { + // We have to load the .js file first so that the static assets are + // resolved + await fetch(`${baseUrl}/src/index.js`).then((r) => r.text()) + + let [raw, url] = await Promise.all([ + fetch(`${baseUrl}/src/index.css?raw`).then((r) => r.text()), + fetch(`${baseUrl}/src/index.css?url`).then((r) => r.text()), + ]) + + expect(firstLine(raw)).toBe(`export default "@import 'tailwindcss';"`) + expect(firstLine(url)).toBe(`export default "/src/index.css"`) + }) + }, +) + +test( + `does not interfere with ?commonjs-proxy modules`, + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^", + "plotly.js": "^3", + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'index.html': html` + + + + `, + 'src/index.js': js`import Plotly from 'plotly.js/lib/core'`, + }, + }, + async ({ exec, expect, fs }) => { + await exec('pnpm vite build') + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] + + await fs.expectFileToContain(filename, [candidate`maplibregl-map`]) + }, +) + +function firstLine(str: string) { + return str.split('\n')[0] +} + +test( + 'optimize option: disabled', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + build: { cssMinify: false }, + plugins: [tailwindcss({ optimize: false })], + }) + `, + 'index.html': html` + + + + +
Hello, world!
+ + `, + 'src/index.css': css` + @reference 'tailwindcss/theme'; + @import 'tailwindcss/utilities'; + `, + }, + }, + async ({ exec, expect, fs }) => { + await exec('pnpm vite build') + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] + + // Should not be minified when optimize is disabled + let content = await fs.read(filename) + expect(content).toContain('.hover\\:flex {') + expect(content).toContain('&:hover {') + expect(content).toContain('@media (hover: hover) {') + expect(content).toContain('display: flex;') + }, +) + +test( + 'optimize option: enabled with minify disabled', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + build: { cssMinify: false }, + plugins: [tailwindcss({ optimize: { minify: false } })], + }) + `, + 'index.html': html` + + + + +
Hello, world!
+ + `, + 'src/index.css': css` + @reference 'tailwindcss/theme'; + @import 'tailwindcss/utilities'; + `, + }, + }, + async ({ exec, expect, fs }) => { + await exec('pnpm vite build') + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] + + // Should be optimized but not minified + let content = await fs.read(filename) + expect(content).toContain('@media (hover: hover) {') + expect(content).toContain('.hover\\:flex:hover {') + expect(content).toContain('display: flex;') + }, +) + +test( + `the plugin works when using the environment API`, + { + fs: { + 'package.json': txt` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + plugins: [tailwindcss()], + builder: {}, + environments: { + server: { + build: { + cssMinify: false, + emitAssets: true, + rollupOptions: { input: './src/server.ts' }, + }, + }, + }, + }) + `, + // Has to exist or the build fails + 'index.html': html` +
+ `, + 'src/server.ts': js` + // Import the stylesheet in the server build + import a from './index.css?url' + console.log(a) + `, + 'src/index.css': css` + @reference 'tailwindcss/theme'; + @import 'tailwindcss/utilities'; + `, + }, + }, + async ({ root, fs, exec, expect }) => { + await exec('pnpm vite build', { cwd: root }) + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] + + await fs.expectFileToContain(filename, [candidate`content-['index.html']`]) + }, +) diff --git a/integrations/vite/multi-root.test.ts b/integrations/vite/multi-root.test.ts new file mode 100644 index 000000000000..4020aff0f006 --- /dev/null +++ b/integrations/vite/multi-root.test.ts @@ -0,0 +1,170 @@ +import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts } from '../utils' + +test( + `production build`, + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import path from 'node:path' + import { defineConfig } from 'vite' + + export default defineConfig({ + build: { + cssMinify: false, + rollupOptions: { + input: { + root1: path.resolve(__dirname, 'root1.html'), + root2: path.resolve(__dirname, 'root2.html'), + }, + }, + }, + plugins: [tailwindcss()], + }) + `, + 'root1.html': html` + + + + +
Hello, world!
+ + `, + 'src/shared.css': css` + @reference 'tailwindcss/theme'; + @import 'tailwindcss/utilities'; + `, + 'src/root1.css': css` + @import './shared.css'; + @custom-variant one (&:is([data-root='1'])); + `, + 'root2.html': html` + + + + +
Hello, world!
+ + `, + 'src/root2.css': css` + @import './shared.css'; + @custom-variant two (&:is([data-root='2'])); + `, + }, + }, + async ({ fs, exec, expect }) => { + await exec('pnpm vite build') + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(2) + + let root1 = files.find(([filename]) => filename.includes('root1')) + let root2 = files.find(([filename]) => filename.includes('root2')) + + expect(root1).toBeDefined() + expect(root2).toBeDefined() + + expect(root1![1]).toContain(candidate`one:underline`) + expect(root1![1]).not.toContain(candidate`two:underline`) + + expect(root2![1]).not.toContain(candidate`one:underline`) + expect(root2![1]).toContain(candidate`two:underline`) + }, +) + +test( + 'dev mode', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import path from 'node:path' + import { defineConfig } from 'vite' + + export default defineConfig({ + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'root1.html': html` + + + + +
Hello, world!
+ + `, + 'src/shared.css': css` + @reference 'tailwindcss/theme'; + @import 'tailwindcss/utilities'; + `, + 'src/root1.css': css` + @import './shared.css'; + @custom-variant one (&:is([data-root='1'])); + `, + 'root2.html': html` + + + + +
Hello, world!
+ + `, + 'src/root2.css': css` + @import './shared.css'; + @custom-variant two (&:is([data-root='2'])); + `, + }, + }, + async ({ spawn, expect }) => { + let process = await spawn('pnpm vite dev') + await process.onStdout((m) => m.includes('ready in')) + + let url = '' + await process.onStdout((m) => { + let match = /Local:\s*(http.*)\//.exec(m) + if (match) url = match[1] + return Boolean(url) + }) + + // Candidates are resolved lazily, so the first visit of index.html + // will only have candidates from this file. + await retryAssertion(async () => { + let styles = await fetchStyles(url, '/root1.html') + expect(styles).toContain(candidate`one:underline`) + expect(styles).not.toContain(candidate`two:underline`) + }) + + // Going to about.html will extend the candidate list to include + // candidates from about.html. + await retryAssertion(async () => { + let styles = await fetchStyles(url, '/root2.html') + expect(styles).not.toContain(candidate`one:underline`) + expect(styles).toContain(candidate`two:underline`) + }) + }, +) diff --git a/integrations/vite/nuxt.test.ts b/integrations/vite/nuxt.test.ts new file mode 100644 index 000000000000..a803a89dac8b --- /dev/null +++ b/integrations/vite/nuxt.test.ts @@ -0,0 +1,105 @@ +import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts } from '../utils' + +const SETUP = { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "nuxt": "3.16.0", + "nitropack": "2.11.0", + "tailwindcss": "workspace:^", + "vue": "latest" + }, + "pnpm": { + "overrides": { + "nuxi": "3.28.0" + } + } + } + `, + 'nuxt.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + + // https://nuxt.com/docs/api/configuration/nuxt-config + export default defineNuxtConfig({ + vite: { + plugins: [tailwindcss()], + }, + + css: ['~/assets/css/main.css'], + devtools: { enabled: true }, + compatibilityDate: '2024-08-30', + }) + `, + 'app.vue': html` + + `, + 'assets/css/main.css': css`@import 'tailwindcss';`, + }, +} + +test('dev mode', SETUP, async ({ fs, spawn, expect }) => { + let process = await spawn('pnpm nuxt dev', { + env: { + TEST: 'false', // VERY IMPORTANT OTHERWISE YOU WON'T GET OUTPUT + NODE_ENV: 'development', + }, + }) + + let url = '' + await process.onStdout((m) => { + let match = /Local:\s*(http.*)\//.exec(m) + if (match) url = match[1] + return Boolean(url) + }) + + await process.onStdout((m) => m.includes('server warmed up in')) + + await retryAssertion(async () => { + let css = await fetchStyles(url) + expect(css).toContain(candidate`underline`) + }) + + await retryAssertion(async () => { + await fs.write( + 'app.vue', + html` + + `, + ) + + let css = await fetchStyles(url) + expect(css).toContain(candidate`underline`) + expect(css).toContain(candidate`font-bold`) + }) +}) + +test('build', SETUP, async ({ spawn, exec, expect }) => { + await exec('pnpm nuxt build') + // The Nuxt preview server does not automatically assign a free port if 3000 + // is taken, so we use a random port instead. + let process = await spawn(`pnpm nuxt preview --port 8724`, { + env: { + TEST: 'false', + NODE_ENV: 'development', + }, + }) + + let url = '' + await process.onStdout((m) => { + let match = /Listening on\s*(http.*)\/?/.exec(m) + if (match) url = match[1].replace('http://[::]', 'http://127.0.0.1') + return m.includes('Listening on') + }) + + await retryAssertion(async () => { + let css = await fetchStyles(url) + expect(css).toContain(candidate`underline`) + }) +}) diff --git a/integrations/vite/other-transforms.test.ts b/integrations/vite/other-transforms.test.ts new file mode 100644 index 000000000000..d56396065fa2 --- /dev/null +++ b/integrations/vite/other-transforms.test.ts @@ -0,0 +1,178 @@ +import dedent from 'dedent' +import { describe } from 'vitest' +import { css, fetchStyles, html, retryAssertion, test, ts, txt } from '../utils' + +function createSetup(transformer: 'postcss' | 'lightningcss') { + return { + fs: { + 'package.json': txt` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, + build: { cssMinify: false }, + plugins: [ + tailwindcss(), + { + name: 'recolor', + transform(code, id) { + if (id.includes('.css')) { + return code.replace(/red;/g, 'blue;') + } + }, + }, + ], + }) + `, + 'index.html': html` + + + + +
Hello, world!
+ + `, + 'src/index.css': css` + @reference 'tailwindcss/theme'; + @import 'tailwindcss/utilities'; + + .foo { + color: red; + } + `, + }, + } +} + +describe.each(['postcss', 'lightningcss'] as const)('%s', (transformer) => { + test(`production build`, createSetup(transformer), async ({ fs, exec, expect }) => { + await exec('pnpm vite build') + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] + + await fs.expectFileToContain(filename, [ + css` + .foo { + color: blue; + } + `, + // Running the transforms on utilities generated by Tailwind might change in the future + dedent` + .\[background-color\:red\] { + background-color: blue; + } + `, + ]) + }) + + test('dev mode', createSetup(transformer), async ({ spawn, fs, expect }) => { + let process = await spawn('pnpm vite dev') + await process.onStdout((m) => m.includes('ready in')) + + let url = '' + await process.onStdout((m) => { + let match = /Local:\s*(http.*)\//.exec(m) + if (match) url = match[1] + return Boolean(url) + }) + + await retryAssertion(async () => { + let styles = await fetchStyles(url, '/index.html') + expect(styles).toContain(css` + .foo { + color: blue; + } + `) + // Running the transforms on utilities generated by Tailwind might change in the future + expect(styles).toContain(dedent` + .\[background-color\:red\] { + background-color: blue; + } + `) + }) + + await retryAssertion(async () => { + await fs.write( + 'src/index.css', + css` + @reference 'tailwindcss/theme'; + @import 'tailwindcss/utilities'; + + .foo { + background-color: red; + } + `, + ) + + let styles = await fetchStyles(url) + expect(styles).toContain(css` + .foo { + background-color: blue; + } + `) + }) + }) + + test('watch mode', createSetup(transformer), async ({ spawn, fs, expect }) => { + let process = await spawn('pnpm vite build --watch') + await process.onStdout((m) => m.includes('built in')) + + await retryAssertion(async () => { + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + let [, styles] = files[0] + + expect(styles).toContain(css` + .foo { + color: blue; + } + `) + // Running the transforms on utilities generated by Tailwind might change in the future + expect(styles).toContain(dedent` + .\[background-color\:red\] { + background-color: blue; + } + `) + }) + + await retryAssertion(async () => { + await fs.write( + 'src/index.css', + css` + @reference 'tailwindcss/theme'; + @import 'tailwindcss/utilities'; + + .foo { + background-color: red; + } + `, + ) + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + let [, styles] = files[0] + + expect(styles).toContain(css` + .foo { + background-color: blue; + } + `) + }) + }) +}) diff --git a/integrations/vite/qwik.test.ts b/integrations/vite/qwik.test.ts new file mode 100644 index 000000000000..42db6563aa7a --- /dev/null +++ b/integrations/vite/qwik.test.ts @@ -0,0 +1,100 @@ +import { candidate, css, fetchStyles, json, retryAssertion, test, ts } from '../utils' + +test( + 'dev mode', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@builder.io/qwik": "^1", + "@builder.io/qwik-city": "^1", + "vite": "^5", + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + } + } + `, + 'vite.config.ts': ts` + import { defineConfig } from 'vite' + import { qwikVite } from '@builder.io/qwik/optimizer' + import { qwikCity } from '@builder.io/qwik-city/vite' + import tailwindcss from '@tailwindcss/vite' + + export default defineConfig(() => { + return { + plugins: [tailwindcss(), qwikCity(), qwikVite()], + } + }) + `, + 'src/root.tsx': ts` + import { component$ } from '@builder.io/qwik' + import { QwikCityProvider, RouterOutlet } from '@builder.io/qwik-city' + + import './global.css' + + export default component$(() => { + return ( + + + + + + + ) + }) + `, + 'src/global.css': css`@import 'tailwindcss/utilities.css';`, + 'src/entry.ssr.tsx': ts` + import { renderToStream, type RenderToStreamOptions } from '@builder.io/qwik/server' + import Root from './root' + + export default function (opts: RenderToStreamOptions) { + return renderToStream(, opts) + } + `, + 'src/routes/index.tsx': ts` + import { component$ } from '@builder.io/qwik' + + export default component$(() => { + return

Hello World!

+ }) + `, + }, + }, + async ({ fs, spawn, expect }) => { + let process = await spawn('pnpm vite --mode ssr') + await process.onStdout((m) => m.includes('ready in')) + + let url = '' + await process.onStdout((m) => { + console.log(m) + let match = /Local:\s*(http.*)\//.exec(m) + if (match) url = match[1] + return Boolean(url) + }) + + await retryAssertion(async () => { + let css = await fetchStyles(url) + expect(css).toContain(candidate`underline`) + }) + + await retryAssertion(async () => { + await fs.write( + 'src/routes/index.tsx', + ts` + import { component$ } from '@builder.io/qwik' + + export default component$(() => { + return

Hello World!

+ }) + `, + ) + + let css = await fetchStyles(url) + expect(css).toContain(candidate`underline`) + expect(css).toContain(candidate`flex`) + }) + }, +) diff --git a/integrations/vite/react-router.test.ts b/integrations/vite/react-router.test.ts new file mode 100644 index 000000000000..98b0c928d943 --- /dev/null +++ b/integrations/vite/react-router.test.ts @@ -0,0 +1,266 @@ +import { candidate, css, fetchStyles, json, retryAssertion, test, ts, txt } from '../utils' + +const WORKSPACE = { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@react-router/dev": "^7", + "@react-router/node": "^7", + "@react-router/serve": "^7", + "@tailwindcss/vite": "workspace:^", + "@types/node": "^20", + "@types/react-dom": "^19", + "@types/react": "^19", + "isbot": "^5", + "react-dom": "^19", + "react-router": "^7", + "react": "^19", + "tailwindcss": "workspace:^", + "vite": "^5" + } + } + `, + 'react-router.config.ts': ts` + import type { Config } from '@react-router/dev/config' + export default { ssr: true } satisfies Config + `, + 'vite.config.ts': ts` + import { defineConfig } from 'vite' + import { reactRouter } from '@react-router/dev/vite' + import tailwindcss from '@tailwindcss/vite' + + export default defineConfig({ + plugins: [tailwindcss(), reactRouter()], + }) + `, + 'app/routes/home.tsx': ts` + export default function Home() { + return

Welcome to React Router

+ } + `, + 'app/app.css': css`@import 'tailwindcss';`, + 'app/routes.ts': ts` + import { type RouteConfig, index } from '@react-router/dev/routes' + export default [index('routes/home.tsx')] satisfies RouteConfig + `, + 'app/root.tsx': ts` + import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router' + import './app.css' + export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ) + } + + export default function App() { + return + } + `, +} + +test('dev mode', { fs: WORKSPACE }, async ({ fs, spawn, expect }) => { + let process = await spawn('pnpm react-router dev') + + let url = '' + await process.onStdout((m) => { + let match = /Local:\s*(http.*)\//.exec(m) + if (match) url = match[1] + return Boolean(url) + }) + + await retryAssertion(async () => { + let css = await fetchStyles(url) + expect(css).toContain(candidate`font-bold`) + }) + + await retryAssertion(async () => { + await fs.write( + 'app/routes/home.tsx', + ts` + export default function Home() { + return

Welcome to React Router

+ } + `, + ) + + let css = await fetchStyles(url) + expect(css).toContain(candidate`underline`) + expect(css).toContain(candidate`font-bold`) + }) +}) + +test( + // cf. https://github.com/remix-run/react-router/blob/00cb4d7b310663b2e84152700c05d3b503005e83/integration/vite-hmr-hdr-test.ts#L311-L318 + 'dev mode, editing a server-only loader dependency triggers HDR instead of a full reload', + { + fs: { + ...WORKSPACE, + 'package.json': json` + { + "type": "module", + "dependencies": { + "@react-router/dev": "^7", + "@react-router/node": "^7", + "@react-router/serve": "^7", + "@tailwindcss/vite": "workspace:^", + "@types/node": "^20", + "@types/react-dom": "^19", + "@types/react": "^19", + "isbot": "^5", + "react-dom": "^19", + "react-router": "^7", + "react": "^19", + "tailwindcss": "workspace:^", + "vite": "^7" + } + } + `, + 'app/routes/home.tsx': ts` + import type { Route } from './+types/home' + import { direct } from '../direct-hdr-dep' + + export async function loader() { + return { message: direct } + } + + export default function Home({ loaderData }: Route.ComponentProps) { + return ( +
+

{loaderData.message}

+ +
+ ) + } + `, + 'app/direct-hdr-dep.ts': ts` export const direct = 'HDR: 0' `, + }, + }, + async ({ fs, spawn, expect }) => { + let process = await spawn('pnpm react-router dev') + + let url = '' + await process.onStdout((m) => { + let match = /Local:\s*(http.*)\//.exec(m) + if (match) url = match[1] + return Boolean(url) + }) + + // check initial state + await retryAssertion(async () => { + let html = await (await fetch(url)).text() + expect(html).toContain('HDR: 0') + + let css = await fetchStyles(url) + expect(css).toContain(candidate`font-bold`) + }) + + // Flush stdout so we only see messages triggered by the edit below. + process.flush() + + // Edit the server-only module. The client environment watches this file + // but it only exists in the server module graph. Without the fix, the + // Tailwind CSS plugin would trigger a full page reload on the client + // instead of letting react-router handle HDR. + await fs.write('app/direct-hdr-dep.ts', ts` export const direct = 'HDR: 1' `) + + // check update + await retryAssertion(async () => { + let html = await (await fetch(url)).text() + expect(html).toContain('HDR: 1') + + let css = await fetchStyles(url) + expect(css).toContain(candidate`font-bold`) + }) + + // Assert the client receives an HMR update (not a full page reload). + await process.onStdout((m) => m.includes('(client) hmr update')) + }, +) + +test('build mode', { fs: WORKSPACE }, async ({ spawn, exec, expect }) => { + await exec('pnpm react-router build') + let process = await spawn('pnpm react-router-serve ./build/server/index.js') + + let url = '' + await process.onStdout((m) => { + let match = /\[react-router-serve\]\s*(http.*) \/?/.exec(m) + if (match) url = match[1] + return url != '' + }) + + await retryAssertion(async () => { + let css = await fetchStyles(url) + expect(css).toContain(candidate`font-bold`) + }) +}) + +test( + 'build mode using ?url stylesheet imports should only build one stylesheet (requires `file-system` scanner)', + { + fs: { + ...WORKSPACE, + 'app/root.tsx': ts` + import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router' + import styles from './app.css?url' + export const links: Route.LinksFunction = () => [{ rel: 'stylesheet', href: styles }] + export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ) + } + + export default function App() { + return + } + `, + 'vite.config.ts': ts` + import { defineConfig } from 'vite' + import { reactRouter } from '@react-router/dev/vite' + import tailwindcss from '@tailwindcss/vite' + + export default defineConfig({ + plugins: [tailwindcss(), reactRouter()], + }) + `, + '.gitignore': txt` + node_modules/ + build/ + `, + }, + }, + async ({ fs, exec, expect }) => { + await exec('pnpm react-router build') + + let files = await fs.glob('build/client/assets/**/*.css') + + expect(files).toHaveLength(1) + let [filename] = files[0] + + await fs.expectFileToContain(filename, [candidate`font-bold`]) + }, +) diff --git a/integrations/vite/resolvers.test.ts b/integrations/vite/resolvers.test.ts new file mode 100644 index 000000000000..79ce57553555 --- /dev/null +++ b/integrations/vite/resolvers.test.ts @@ -0,0 +1,311 @@ +import { describe } from 'vitest' +import { + candidate, + css, + fetchStyles, + html, + js, + json, + retryAssertion, + test, + ts, + txt, +} from '../utils' + +test( + 'resolves tsconfig paths in production build', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "vite": "^8" + } + } + `, + 'tsconfig.json': json` + { + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + build: { cssMinify: false }, + plugins: [tailwindcss()], + resolve: { + tsconfigPaths: true, + }, + }) + `, + 'index.html': html` + + + + +
Hello, world!
+ + `, + 'src/index.css': css` + @import '@/styles/base.css'; + @plugin '@/plugin.js'; + `, + 'src/styles/base.css': css` + @reference 'tailwindcss/theme'; + @import 'tailwindcss/utilities'; + `, + 'src/plugin.js': js` + export default function ({ addUtilities }) { + addUtilities({ '.custom-underline': { 'border-bottom': '1px solid green' } }) + } + `, + }, + }, + async ({ fs, exec, expect }) => { + await exec('pnpm vite build') + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] + + await fs.expectFileToContain(filename, [candidate`underline`, candidate`custom-underline`]) + }, +) + +test( + 'resolves tsconfig paths in dev mode', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "vite": "^8" + } + } + `, + 'tsconfig.json': json` + { + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + build: { cssMinify: false }, + plugins: [tailwindcss()], + resolve: { + tsconfigPaths: true, + }, + }) + `, + 'index.html': html` + + + + +
Hello, world!
+ + `, + 'src/index.css': css` + @import '@/styles/base.css'; + @plugin '@/plugin.js'; + `, + 'src/styles/base.css': css` + @reference 'tailwindcss/theme'; + @import 'tailwindcss/utilities'; + `, + 'src/plugin.js': js` + export default function ({ addUtilities }) { + addUtilities({ '.custom-underline': { 'border-bottom': '1px solid green' } }) + } + `, + }, + }, + async ({ spawn, expect }) => { + let process = await spawn('pnpm vite dev') + await process.onStdout((m) => m.includes('ready in')) + + let url = '' + await process.onStdout((m) => { + let match = /Local:\s*(http.*)\//.exec(m) + if (match) url = match[1] + return Boolean(url) + }) + + await retryAssertion(async () => { + let styles = await fetchStyles(url, '/index.html') + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`custom-underline`) + }) + }, +) + +describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { + test( + 'resolves aliases in production build', + { + fs: { + 'package.json': txt` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} + "vite": "^5.3.5" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + import { fileURLToPath } from 'node:url' + + export default defineConfig({ + css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, + build: { cssMinify: false }, + plugins: [tailwindcss()], + resolve: { + alias: { + '#css-alias': fileURLToPath(new URL('./src/alias.css', import.meta.url)), + '#js-alias': fileURLToPath(new URL('./src/plugin.js', import.meta.url)), + }, + }, + }) + `, + 'index.html': html` + + + + +
Hello, world!
+ + `, + 'src/index.css': css` + @import '#css-alias'; + @plugin '#js-alias'; + `, + 'src/alias.css': css` + @reference 'tailwindcss/theme'; + @import 'tailwindcss/utilities'; + `, + 'src/plugin.js': js` + export default function ({ addUtilities }) { + addUtilities({ '.custom-underline': { 'border-bottom': '1px solid green' } }) + } + `, + }, + }, + async ({ fs, exec, expect }) => { + await exec('pnpm vite build') + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] + + await fs.expectFileToContain(filename, [candidate`underline`, candidate`custom-underline`]) + }, + ) + + test( + 'resolves aliases in dev mode', + { + fs: { + 'package.json': txt` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} + "vite": "^5.3.5" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + import { fileURLToPath } from 'node:url' + + export default defineConfig({ + css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, + build: { cssMinify: false }, + plugins: [tailwindcss()], + resolve: { + alias: { + '#css-alias': fileURLToPath(new URL('./src/alias.css', import.meta.url)), + '#js-alias': fileURLToPath(new URL('./src/plugin.js', import.meta.url)), + }, + }, + }) + `, + 'index.html': html` + + + + +
Hello, world!
+ + `, + 'src/index.css': css` + @import '#css-alias'; + @plugin '#js-alias'; + `, + 'src/alias.css': css` + @reference 'tailwindcss/theme'; + @import 'tailwindcss/utilities'; + `, + 'src/plugin.js': js` + export default function ({ addUtilities }) { + addUtilities({ '.custom-underline': { 'border-bottom': '1px solid green' } }) + } + `, + }, + }, + async ({ spawn, expect }) => { + let process = await spawn('pnpm vite dev') + await process.onStdout((m) => m.includes('ready in')) + + let url = '' + await process.onStdout((m) => { + let match = /Local:\s*(http.*)\//.exec(m) + if (match) url = match[1] + return Boolean(url) + }) + + await retryAssertion(async () => { + let styles = await fetchStyles(url, '/index.html') + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`custom-underline`) + }) + }, + ) +}) diff --git a/integrations/vite/solidstart.test.ts b/integrations/vite/solidstart.test.ts new file mode 100644 index 000000000000..bbe9ccc8868d --- /dev/null +++ b/integrations/vite/solidstart.test.ts @@ -0,0 +1,108 @@ +import { candidate, css, fetchStyles, js, json, retryAssertion, test, ts } from '../utils' + +const WORKSPACE = { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@solidjs/start": "^1", + "solid-js": "^1", + "vinxi": "^0", + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + } + } + `, + 'jsconfig.json': json` + { + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js" + } + } + `, + 'app.config.js': ts` + import { defineConfig } from '@solidjs/start/config' + import tailwindcss from '@tailwindcss/vite' + + export default defineConfig({ + vite: { + plugins: [tailwindcss()], + }, + }) + `, + 'src/entry-server.jsx': js` + // @refresh reload + import { createHandler, StartServer } from '@solidjs/start/server' + + export default createHandler(() => ( + ( + + {assets} + +
{children}
+ {scripts} + + + )} + /> + )) + `, + 'src/entry-client.jsx': js` + // @refresh reload + import { mount, StartClient } from '@solidjs/start/client' + + mount(() => , document.getElementById('app')) + `, + 'src/app.jsx': js` + import './app.css' + export default function App() { + return

Hello world!

+ } + `, + 'src/app.css': css`@import 'tailwindcss';`, +} + +test( + 'dev mode', + { + fs: WORKSPACE, + }, + async ({ fs, spawn, expect }) => { + let process = await spawn('pnpm vinxi dev', { + env: { + TEST: 'false', // VERY IMPORTANT OTHERWISE YOU WON'T GET OUTPUT + NODE_ENV: 'development', + }, + }) + + let url = '' + await process.onStdout((m) => { + let match = /Local:\s*(http.*)\//.exec(m) + if (match) url = match[1] + return Boolean(url) + }) + + await retryAssertion(async () => { + let css = await fetchStyles(url) + expect(css).toContain(candidate`underline`) + }) + + await retryAssertion(async () => { + await fs.write( + 'src/app.jsx', + js` + import './app.css' + export default function App() { + return

Hello world!

+ } + `, + ) + + let css = await fetchStyles(url) + expect(css).toContain(candidate`underline`) + expect(css).toContain(candidate`font-bold`) + }) + }, +) diff --git a/integrations/vite/source-maps.test.ts b/integrations/vite/source-maps.test.ts new file mode 100644 index 000000000000..0e259469077a --- /dev/null +++ b/integrations/vite/source-maps.test.ts @@ -0,0 +1,93 @@ +import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts } from '../utils' + +test( + `dev build`, + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "lightningcss": "^1", + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + plugins: [tailwindcss()], + css: { + devSourcemap: true, + }, + }) + `, + 'index.html': html` + + + + +
Hello, world!
+ + `, + 'src/index.css': css` + @import 'tailwindcss/utilities'; + /* */ + `, + }, + }, + async ({ fs, spawn, expect, parseSourceMap }) => { + // Source maps only work in development mode in Vite + let process = await spawn('pnpm vite dev') + await process.onStdout((m) => m.includes('ready in')) + + let url = '' + await process.onStdout((m) => { + let match = /Local:\s*(http.*)\//.exec(m) + if (match) url = match[1] + return Boolean(url) + }) + + let styles = await retryAssertion(async () => { + let styles = await fetchStyles(url, '/index.html') + + // Wait until we have the right CSS + expect(styles).toContain(candidate`flex`) + + return styles + }) + + // Make sure we can find a source map + let map = parseSourceMap(styles) + + expect(map.at(1, 0)).toMatchObject({ + source: null, + original: '(none)', + generated: '/*! tailwi...', + }) + + expect(map.at(2, 0)).toMatchObject({ + source: expect.stringContaining('utilities.css'), + original: '@tailwind...', + generated: '.flex {...', + }) + + expect(map.at(3, 2)).toMatchObject({ + source: expect.stringContaining('utilities.css'), + original: '@tailwind...', + generated: 'display: f...', + }) + + expect(map.at(4, 0)).toMatchObject({ + source: null, + original: '(none)', + generated: '}...', + }) + }, +) diff --git a/integrations/vite/ssr.test.ts b/integrations/vite/ssr.test.ts new file mode 100644 index 000000000000..a1d6608161cf --- /dev/null +++ b/integrations/vite/ssr.test.ts @@ -0,0 +1,72 @@ +import { describe } from 'vitest' +import { candidate, css, html, json, test, ts } from '../utils' + +describe.each([['^5.3'], ['^6.0'], ['^7'], ['^8']])('Using Vite %s', (version) => { + test( + `SSR build`, + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "vite": "${version}" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + build: { + cssMinify: false, + ssrEmitAssets: true, + }, + plugins: [tailwindcss()], + }) + `, + 'index.html': html` + +
+ + + `, + 'src/index.css': css`@import 'tailwindcss';`, + 'src/index.ts': ts` + import './index.css' + + document.querySelector('#app').innerHTML = \` +
Hello, world!
+ \` + `, + 'server.ts': ts` + import css from './src/index.css?url' + + document.querySelector('#app').innerHTML = \` + +
Hello, world!
+ \` + `, + }, + }, + async ({ fs, exec, expect }) => { + await exec('pnpm vite build --ssr server.ts') + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] + + await fs.expectFileToContain(filename, [ + candidate`underline`, + candidate`m-2`, + candidate`overline`, + candidate`m-3`, + ]) + }, + ) +}) diff --git a/integrations/vite/svelte.test.ts b/integrations/vite/svelte.test.ts new file mode 100644 index 000000000000..e186c3f85185 --- /dev/null +++ b/integrations/vite/svelte.test.ts @@ -0,0 +1,253 @@ +import { candidate, css, html, json, retryAssertion, test, ts } from '../utils' + +test( + 'production build', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "svelte": "^5", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^5", + "@tailwindcss/vite": "workspace:^", + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import { defineConfig } from 'vite' + import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte' + import tailwindcss from '@tailwindcss/vite' + + export default defineConfig({ + plugins: [ + svelte({ + preprocess: [vitePreprocess()], + }), + tailwindcss(), + ], + }) + `, + 'index.html': html` + + + +
+ + + + `, + 'src/main.ts': ts` + import App from './App.svelte' + const app = new App({ + target: document.body, + }) + `, + 'src/index.css': css`@import 'tailwindcss';`, + 'src/App.svelte': html` + + +

Hello {name}!

+ + + `, + 'src/other.css': css` + .local { + @apply text-red-500; + animation: 2s ease-in-out infinite localKeyframes; + } + + :global(.global) { + @apply text-green-500; + animation: 2s ease-in-out infinite globalKeyframes; + } + + @keyframes -global-globalKeyframes { + 0% { + opacity: 0; + } + 100% { + opacity: 100%; + } + } + + @keyframes localKeyframes { + 0% { + opacity: 0; + } + 100% { + opacity: 100%; + } + } + `, + }, + }, + async ({ exec, fs, expect }) => { + let output = await exec('pnpm vite build') + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + + await fs.expectFileToContain(files[0][0], [ + candidate`underline`, + '.global{color:var(--color-green-500,oklch(72.3% .219 149.579));animation:2s ease-in-out infinite globalKeyframes}', + /\.local.svelte-.*\{color:var\(--color-red-500,oklch\(63\.7% \.237 25\.331\)\);animation:2s ease-in-out infinite svelte-.*-localKeyframes\}/, + /@keyframes globalKeyframes\{/, + /@keyframes svelte-.*-localKeyframes\{/, + ]) + + // Should not print any warnings + expect(output).not.toContain('vite-plugin-svelte') + }, +) + +test( + 'watch mode', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "svelte": "^5", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^5", + "@tailwindcss/vite": "workspace:^", + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import { defineConfig } from 'vite' + import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte' + import tailwindcss from '@tailwindcss/vite' + + export default defineConfig({ + plugins: [ + svelte({ + preprocess: [vitePreprocess()], + }), + tailwindcss(), + ], + }) + `, + 'index.html': html` + + + +
+ + + + `, + 'src/main.ts': ts` + import App from './App.svelte' + const app = new App({ + target: document.body, + }) + `, + 'src/App.svelte': html` + + +

Hello {name}!

+ + + `, + 'src/index.css': css` @import 'tailwindcss'; `, + 'src/other.css': css` + .local { + @apply text-red-500; + animation: 2s ease-in-out infinite localKeyframes; + } + + :global(.global) { + @apply text-green-500; + animation: 2s ease-in-out infinite globalKeyframes; + } + + @keyframes -global-globalKeyframes { + 0% { + opacity: 0; + } + 100% { + opacity: 100%; + } + } + + @keyframes localKeyframes { + 0% { + opacity: 0; + } + 100% { + opacity: 100%; + } + } + `, + }, + }, + async ({ fs, spawn, expect }) => { + let process = await spawn(`pnpm vite build --watch`) + await process.onStdout((m) => m.includes('built in')) + + await retryAssertion(async () => { + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + let [, css] = files[0] + expect(css).toContain(candidate`underline`) + expect(css).toContain( + '.global{color:var(--color-green-500,oklch(72.3% .219 149.579));animation:2s ease-in-out infinite globalKeyframes}', + ) + expect(css).toMatch( + /\.local.svelte-.*\{color:var\(--color-red-500,oklch\(63\.7% \.237 25\.331\)\);animation:2s ease-in-out infinite svelte-.*-localKeyframes\}/, + ) + expect(css).toMatch(/@keyframes globalKeyframes\{/) + expect(css).toMatch(/@keyframes svelte-.*-localKeyframes\{/) + }) + + await fs.write( + 'src/App.svelte', + (await fs.read('src/App.svelte')).replace('underline', 'font-bold bar'), + ) + + await fs.write( + 'src/other.css', + `${await fs.read('src/other.css')}\n.bar { @apply text-pink-500; }`, + ) + + await retryAssertion(async () => { + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + let [, css] = files[0] + expect(css).toContain(candidate`font-bold`) + expect(css).toContain( + '.global{color:var(--color-green-500,oklch(72.3% .219 149.579));animation:2s ease-in-out infinite globalKeyframes}', + ) + expect(css).toMatch( + /\.local.svelte-.*\{color:var\(--color-red-500,oklch\(63\.7% \.237 25\.331\)\);animation:2s ease-in-out infinite svelte-.*-localKeyframes\}/, + ) + expect(css).toMatch(/@keyframes globalKeyframes\{/) + expect(css).toMatch(/@keyframes svelte-.*-localKeyframes\{/) + expect(css).toMatch( + /\.bar.svelte-.*\{color:var\(--color-pink-500,oklch\(65\.6% \.241 354\.308\)\)\}/, + ) + }) + }, +) diff --git a/integrations/vite/url-rewriting.test.ts b/integrations/vite/url-rewriting.test.ts new file mode 100644 index 000000000000..500fe6d71fdf --- /dev/null +++ b/integrations/vite/url-rewriting.test.ts @@ -0,0 +1,108 @@ +import { describe } from 'vitest' +import { binary, css, html, svg, test, ts, txt } from '../utils' + +const SIMPLE_IMAGE = `iVBORw0KGgoAAAANSUhEUgAAADAAAAAlAQAAAAAsYlcCAAAACklEQVR4AWMYBQABAwABRUEDtQAAAABJRU5ErkJggg==` + +describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { + test( + 'can rewrite urls in production builds', + { + fs: { + 'package.json': txt` + { + "type": "module", + "dependencies": { + "tailwindcss": "workspace:^" + }, + "devDependencies": { + ${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''} + "@tailwindcss/vite": "workspace:^", + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + plugins: [tailwindcss()], + build: { + assetsInlineLimit: 256, + cssMinify: false, + }, + css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, + }) + `, + 'index.html': html` + + + + + + +
+ + + `, + 'src/app.css': css` + @reference 'tailwindcss'; + @import './dir-1/bar.css'; + @import './dir-1/dir-2/baz.css'; + @import './dir-1/dir-2/vector.css'; + `, + 'src/dir-1/bar.css': css` + .test1 { + background-image: url('../../resources/image.png'); + } + `, + 'src/dir-1/dir-2/baz.css': css` + .test2 { + background-image: url('../../../resources/image.png'); + } + `, + 'src/dir-1/dir-2/vector.css': css` + @import './dir-3/vector.css'; + .test3 { + background-image: url('../../../resources/vector.svg'); + } + `, + 'src/dir-1/dir-2/dir-3/vector.css': css` + .test4 { + background-image: url('./vector-2.svg'); + } + `, + 'resources/image.png': binary(SIMPLE_IMAGE), + 'resources/vector.svg': svg` + + + + + + + `, + 'src/dir-1/dir-2/dir-3/vector-2.svg': svg` + + + + + + + `, + }, + }, + async ({ fs, exec, expect }) => { + await exec('pnpm vite build') + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + + await fs.expectFileToContain(files[0][0], [SIMPLE_IMAGE]) + + let images = await fs.glob('dist/**/*.svg') + expect(images).toHaveLength(2) + + await fs.expectFileToContain(files[0][0], [/\/assets\/vector-.*?\.svg/]) + }, + ) +}) diff --git a/integrations/vite/virtual-modules.test.ts b/integrations/vite/virtual-modules.test.ts new file mode 100644 index 000000000000..12eadb6c715b Binary files /dev/null and b/integrations/vite/virtual-modules.test.ts differ diff --git a/integrations/vite/vue.test.ts b/integrations/vite/vue.test.ts new file mode 100644 index 000000000000..3fea0db9aa4c --- /dev/null +++ b/integrations/vite/vue.test.ts @@ -0,0 +1,146 @@ +import { stripVTControlCharacters } from 'node:util' +import { candidate, html, json, test, ts } from '../utils' + +test( + 'production build', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "vue": "^3.4.37", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.1.2", + "@tailwindcss/vite": "workspace:^", + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import { defineConfig } from 'vite' + import vue from '@vitejs/plugin-vue' + import tailwindcss from '@tailwindcss/vite' + + export default defineConfig({ + plugins: [vue(), tailwindcss()], + }) + `, + 'index.html': html` + + + +
+ + + + `, + 'src/main.ts': ts` + import { createApp } from 'vue' + import App from './App.vue' + + createApp(App).mount('#app') + `, + 'src/App.vue': html` + + + + `, + }, + }, + async ({ fs, exec, expect }) => { + await exec('pnpm vite build') + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + + await fs.expectFileToContain(files[0][0], [candidate`underline`, candidate`foo`]) + await fs.expectFileToContain(files[0][0], ['.bar{']) + }, +) + +test( + 'error when using `@apply` without `@reference`', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "vue": "^3.4.37", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.1.2", + "@tailwindcss/vite": "workspace:^", + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import { defineConfig } from 'vite' + import vue from '@vitejs/plugin-vue' + import tailwindcss from '@tailwindcss/vite' + + export default defineConfig({ + plugins: [vue(), tailwindcss()], + }) + `, + 'index.html': html` + + + +
+ + + + `, + 'src/main.ts': ts` + import { createApp } from 'vue' + import App from './App.vue' + + createApp(App).mount('#app') + `, + 'src/App.vue': html` + + + + `, + }, + }, + async ({ exec, expect }) => { + expect.assertions(1) + + try { + await exec('pnpm vite build') + } catch (error) { + let [, message] = + /error during build:([\s\S]*?)file:/g.exec( + stripVTControlCharacters(error.message.replace(/\r?\n/g, '\n')), + ) ?? [] + expect(message.trim()).toMatchInlineSnapshot( + `"[@tailwindcss/vite:generate:build] Cannot apply unknown utility class \`text-red-500\`. Are you using CSS modules or similar and missing \`@reference\`? https://tailwindcss.com/docs/functions-and-directives#reference-directive"`, + ) + } + }, +) diff --git a/integrations/vitest.config.ts b/integrations/vitest.config.ts new file mode 100644 index 000000000000..6855b146be78 --- /dev/null +++ b/integrations/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineProject } from 'vitest/config' + +export default defineProject({ + test: { + hideSkippedTests: true, + }, +}) diff --git a/integrations/webpack/index.test.ts b/integrations/webpack/index.test.ts new file mode 100644 index 000000000000..2a91411d2494 --- /dev/null +++ b/integrations/webpack/index.test.ts @@ -0,0 +1,110 @@ +import { css, html, js, json, test } from '../utils' + +test( + 'webpack + PostCSS (watch)', + { + fs: { + 'package.json': json` + { + "main": "./src/index.js", + "browser": "./src/index.js", + "dependencies": { + "css-loader": "^6", + "postcss": "^8", + "postcss-loader": "^7", + "webpack": "^5", + "webpack-cli": "^5", + "mini-css-extract-plugin": "^2", + "tailwindcss": "workspace:^", + "@tailwindcss/postcss": "workspace:^" + } + } + `, + 'postcss.config.js': js` + /** @type {import('postcss-load-config').Config} */ + module.exports = { + plugins: { + '@tailwindcss/postcss': {}, + }, + } + `, + 'webpack.config.js': js` + let MiniCssExtractPlugin = require('mini-css-extract-plugin') + + module.exports = { + output: { + clean: true, + }, + plugins: [new MiniCssExtractPlugin()], + module: { + rules: [ + { + test: /.css$/i, + use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'], + }, + ], + }, + } + `, + 'src/index.js': js`import './index.css'`, + 'src/index.html': html` +
+ `, + 'src/index.css': css` + @import 'tailwindcss/theme'; + @import 'tailwindcss/utilities'; + `, + 'src/unrelated.module.css': css` + .module { + color: var(--color-blue-500); + } + `, + }, + }, + async ({ fs, spawn, exec, expect }) => { + // Generate the initial build so output CSS files exist on disk + await exec('pnpm webpack --mode=development') + + // NOTE: We are writing to an output CSS file which is not being ignored by + // `.gitignore` nor marked with `@source not`. This should not result in an + // infinite loop. + let process = await spawn('pnpm webpack --mode=development --watch') + await process.onStdout((m) => m.includes('compiled successfully in')) + + expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(` + " + --- ./dist/main.css --- + :root, :host { + --color-blue-500: oklch(62.3% 0.214 259.815); + } + .flex { + display: flex; + } + " + `) + + await fs.write( + 'src/unrelated.module.css', + css` + .module { + color: var(--color-blue-500); + background-color: var(--color-red-500); + } + `, + ) + await process.onStdout((m) => m.includes('compiled successfully in')) + + expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(` + " + --- ./dist/main.css --- + :root, :host { + --color-red-500: oklch(63.7% 0.237 25.331); + --color-blue-500: oklch(62.3% 0.214 259.815); + } + .flex { + display: flex; + } + " + `) + }, +) diff --git a/integrations/webpack/loader.test.ts b/integrations/webpack/loader.test.ts new file mode 100644 index 000000000000..9c3d870afcbd --- /dev/null +++ b/integrations/webpack/loader.test.ts @@ -0,0 +1,495 @@ +import { css, html, js, json, test } from '../utils' + +test( + '@tailwindcss/webpack loader (build)', + { + fs: { + 'package.json': json` + { + "main": "./src/index.js", + "browser": "./src/index.js", + "dependencies": { + "css-loader": "^6", + "webpack": "^5", + "webpack-cli": "^5", + "mini-css-extract-plugin": "^2", + "tailwindcss": "workspace:^", + "@tailwindcss/webpack": "workspace:^" + } + } + `, + 'webpack.config.js': js` + let MiniCssExtractPlugin = require('mini-css-extract-plugin') + + module.exports = { + output: { + clean: true, + }, + plugins: [new MiniCssExtractPlugin()], + module: { + rules: [ + { + test: /.css$/i, + use: [MiniCssExtractPlugin.loader, 'css-loader', '@tailwindcss/webpack'], + }, + ], + }, + } + `, + 'src/index.js': js`import './index.css'`, + 'src/index.html': html` +
+ `, + 'src/index.css': css` + @import 'tailwindcss/theme'; + @import 'tailwindcss/utilities'; + `, + }, + }, + async ({ fs, exec, expect }) => { + await exec('pnpm webpack --mode=development') + + expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(` + " + --- ./dist/main.css --- + .flex { + display: flex; + } + " + `) + }, +) + +test( + '@tailwindcss/webpack loader (watch)', + { + fs: { + 'package.json': json` + { + "main": "./src/index.js", + "browser": "./src/index.js", + "dependencies": { + "css-loader": "^6", + "webpack": "^5", + "webpack-cli": "^5", + "mini-css-extract-plugin": "^2", + "tailwindcss": "workspace:^", + "@tailwindcss/webpack": "workspace:^" + } + } + `, + 'webpack.config.js': js` + let MiniCssExtractPlugin = require('mini-css-extract-plugin') + + module.exports = { + output: { + clean: true, + }, + plugins: [new MiniCssExtractPlugin()], + module: { + rules: [ + { + test: /.css$/i, + use: [MiniCssExtractPlugin.loader, 'css-loader', '@tailwindcss/webpack'], + }, + ], + }, + } + `, + 'src/index.js': js`import './index.css'`, + 'src/index.html': html` +
+ `, + 'src/index.css': css` + @import 'tailwindcss/theme'; + @import 'tailwindcss/utilities'; + `, + }, + }, + async ({ fs, spawn, exec, expect }) => { + // Generate the initial build so output CSS files exist on disk + await exec('pnpm webpack --mode=development') + + let process = await spawn('pnpm webpack --mode=development --watch') + await process.onStdout((m) => m.includes('compiled successfully in')) + + expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(` + " + --- ./dist/main.css --- + .flex { + display: flex; + } + " + `) + + // Add a new Tailwind class to the HTML file + await fs.write('src/index.html', html` +
+ `) + await process.onStdout((m) => m.includes('compiled successfully in')) + + expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(` + " + --- ./dist/main.css --- + .flex { + display: flex; + } + .underline { + text-decoration-line: underline; + } + " + `) + }, +) + +test( + '@tailwindcss/webpack loader with @apply', + { + fs: { + 'package.json': json` + { + "main": "./src/index.js", + "browser": "./src/index.js", + "dependencies": { + "css-loader": "^6", + "webpack": "^5", + "webpack-cli": "^5", + "mini-css-extract-plugin": "^2", + "tailwindcss": "workspace:^", + "@tailwindcss/webpack": "workspace:^" + } + } + `, + 'webpack.config.js': js` + let MiniCssExtractPlugin = require('mini-css-extract-plugin') + + module.exports = { + output: { + clean: true, + }, + plugins: [new MiniCssExtractPlugin()], + module: { + rules: [ + { + test: /.css$/i, + use: [MiniCssExtractPlugin.loader, 'css-loader', '@tailwindcss/webpack'], + }, + ], + }, + } + `, + 'src/index.js': js`import './index.css'`, + 'src/index.css': css` + @import 'tailwindcss/theme'; + + .btn { + @apply flex items-center px-4 py-2 rounded-md; + } + `, + }, + }, + async ({ fs, exec, expect }) => { + await exec('pnpm webpack --mode=development') + + expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(` + " + --- ./dist/main.css --- + :root, :host { + --spacing: 0.25rem; + --radius-md: 0.375rem; + } + .btn { + display: flex; + align-items: center; + border-radius: var(--radius-md); + padding-inline: calc(var(--spacing) * 4); + padding-block: calc(var(--spacing) * 2); + } + " + `) + }, +) + +test( + '@tailwindcss/webpack loader with optimization', + { + fs: { + 'package.json': json` + { + "main": "./src/index.js", + "browser": "./src/index.js", + "dependencies": { + "css-loader": "^6", + "webpack": "^5", + "webpack-cli": "^5", + "mini-css-extract-plugin": "^2", + "tailwindcss": "workspace:^", + "@tailwindcss/webpack": "workspace:^" + } + } + `, + 'webpack.config.js': js` + let MiniCssExtractPlugin = require('mini-css-extract-plugin') + + module.exports = { + output: { + clean: true, + }, + plugins: [new MiniCssExtractPlugin()], + module: { + rules: [ + { + test: /.css$/i, + use: [ + MiniCssExtractPlugin.loader, + 'css-loader', + { + loader: '@tailwindcss/webpack', + options: { + optimize: true, + }, + }, + ], + }, + ], + }, + } + `, + 'src/index.js': js`import './index.css'`, + 'src/index.html': html` +
+ `, + 'src/index.css': css` + @import 'tailwindcss/theme'; + @import 'tailwindcss/utilities'; + `, + }, + }, + async ({ fs, exec, expect }) => { + await exec('pnpm webpack --mode=development') + + expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(` + " + --- ./dist/main.css --- + .flex{display:flex} + " + `) + }, +) + +test( + '@tailwindcss/webpack loader with CSS @import', + { + fs: { + 'package.json': json` + { + "main": "./src/index.js", + "browser": "./src/index.js", + "dependencies": { + "css-loader": "^6", + "webpack": "^5", + "webpack-cli": "^5", + "mini-css-extract-plugin": "^2", + "tailwindcss": "workspace:^", + "@tailwindcss/webpack": "workspace:^" + } + } + `, + 'webpack.config.js': js` + let MiniCssExtractPlugin = require('mini-css-extract-plugin') + + module.exports = { + output: { + clean: true, + }, + plugins: [new MiniCssExtractPlugin()], + module: { + rules: [ + { + test: /.css$/i, + use: [MiniCssExtractPlugin.loader, 'css-loader', '@tailwindcss/webpack'], + }, + ], + }, + } + `, + 'src/index.js': js`import './index.css'`, + 'src/index.html': html` +
+ `, + 'src/index.css': css` + @import './custom.css'; + @import 'tailwindcss/theme'; + @import 'tailwindcss/utilities'; + `, + 'src/custom.css': css` + /**/ + @utility custom-util { + color: var(--color-red-500); + } + `, + }, + }, + async ({ fs, exec, expect }) => { + await exec('pnpm webpack --mode=development') + + expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(` + " + --- ./dist/main.css --- + :root, :host { + --color-red-500: oklch(63.7% 0.237 25.331); + } + .flex { + display: flex; + } + .custom-util { + color: var(--color-red-500); + } + " + `) + }, +) + +test( + '@tailwindcss/webpack loader with @plugin', + { + fs: { + 'package.json': json` + { + "main": "./src/index.js", + "browser": "./src/index.js", + "dependencies": { + "css-loader": "^6", + "webpack": "^5", + "webpack-cli": "^5", + "mini-css-extract-plugin": "^2", + "tailwindcss": "workspace:^", + "@tailwindcss/webpack": "workspace:^" + } + } + `, + 'webpack.config.js': js` + let MiniCssExtractPlugin = require('mini-css-extract-plugin') + + module.exports = { + output: { + clean: true, + }, + plugins: [new MiniCssExtractPlugin()], + module: { + rules: [ + { + test: /.css$/i, + use: [MiniCssExtractPlugin.loader, 'css-loader', '@tailwindcss/webpack'], + }, + ], + }, + } + `, + 'src/index.js': js`import './index.css'`, + 'src/index.html': html` +
+ `, + 'src/index.css': css` + @import 'tailwindcss/utilities'; + @plugin './plugin.js'; + `, + 'src/plugin.js': js` + export default function ({ addUtilities }) { + addUtilities({ + '.custom-underline': { + 'border-bottom': '1px solid green', + }, + }) + } + `, + }, + }, + async ({ fs, exec, expect }) => { + await exec('pnpm webpack --mode=development') + + expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(` + " + --- ./dist/main.css --- + .custom-underline { + border-bottom: 1px solid green; + } + " + `) + }, +) + +test( + '@tailwindcss/webpack loader isolates cache by resource including query', + { + fs: { + 'package.json': json` + { + "main": "./src/index.js", + "browser": "./src/index.js", + "dependencies": { + "css-loader": "^6", + "webpack": "^5", + "webpack-cli": "^5", + "mini-css-extract-plugin": "^2", + "tailwindcss": "workspace:^", + "@tailwindcss/webpack": "workspace:^" + } + } + `, + 'webpack.config.js': js` + let MiniCssExtractPlugin = require('mini-css-extract-plugin') + let path = require('node:path') + + module.exports = { + mode: 'development', + entry: { + a: './src/a.js', + b: './src/b.js', + }, + output: { + clean: true, + }, + plugins: [new MiniCssExtractPlugin()], + module: { + rules: [ + { + test: /.css$/i, + use: [ + MiniCssExtractPlugin.loader, + 'css-loader', + '@tailwindcss/webpack', + path.resolve(__dirname, 'query-loader.js'), + ], + }, + ], + }, + } + `, + 'query-loader.js': js` + module.exports = function (source) { + if (this.resourceQuery.includes('a')) { + return '@import "tailwindcss/utilities"; @utility only-a {color: var(--color-red-500);}' + } + + if (this.resourceQuery.includes('b')) { + return '@import "tailwindcss/utilities"; @utility only-b {color: var(--color-blue-500);}' + } + + return source + } + `, + 'src/a.js': js`import './index.css?a'`, + 'src/b.js': js`import './index.css?b'`, + 'src/index.css': css``, + }, + }, + async ({ fs, exec }) => { + await exec('pnpm webpack --mode=development') + + await fs.expectFileToContain('dist/a.css', ['only-a', '--color-red-500']) + await fs.expectFileToContain('dist/b.css', ['only-b', '--color-blue-500']) + await fs.expectFileNotToContain('dist/a.css', ['only-b', '--color-blue-500']) + await fs.expectFileNotToContain('dist/b.css', ['only-a', '--color-red-500']) + }, +) diff --git a/jest/customMatchers.js b/jest/customMatchers.js deleted file mode 100644 index 23372befcd2a..000000000000 --- a/jest/customMatchers.js +++ /dev/null @@ -1,21 +0,0 @@ -expect.extend({ - // Compare two CSS strings with all whitespace removed - // This is probably naive but it's fast and works well enough. - toMatchCss(received, argument) { - function stripped(str) { - return str.replace(/\s/g, '') - } - - if (stripped(received) === stripped(argument)) { - return { - message: () => `expected ${received} not to match CSS ${argument}`, - pass: true, - } - } else { - return { - message: () => `expected ${received} to match CSS ${argument}`, - pass: false, - } - } - }, -}) diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 6cade71d33b0..000000000000 --- a/package-lock.json +++ /dev/null @@ -1,7059 +0,0 @@ -{ - "name": "tailwindcss", - "version": "0.6.6", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "abab": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", - "integrity": "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=", - "dev": true - }, - "acorn": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz", - "integrity": "sha512-jd5MkIUlbbmb07nXH0DT3y7rDVtkzDi4XZOUVWAer8ajmF/DTSSbl5oNFyDOl/OXA33Bl79+ypHhl2pN20VeOQ==", - "dev": true - }, - "acorn-globals": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-3.1.0.tgz", - "integrity": "sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8=", - "dev": true, - "requires": { - "acorn": "4.0.13" - }, - "dependencies": { - "acorn": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", - "dev": true - } - } - }, - "acorn-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", - "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", - "dev": true, - "requires": { - "acorn": "3.3.0" - }, - "dependencies": { - "acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", - "dev": true - } - } - }, - "agent-base": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.0.tgz", - "integrity": "sha512-c+R/U5X+2zz2+UCrCFv6odQzJdoqI+YecuhnAJLa1zYaMc13zPfwMwZrr91Pd1DYNo/yPRbiM4WVf9whgwFsIg==", - "dev": true, - "requires": { - "es6-promisify": "5.0.0" - } - }, - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "dev": true, - "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.1.0", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.3.1" - } - }, - "ajv-keywords": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", - "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", - "dev": true - }, - "align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "dev": true, - "requires": { - "kind-of": "3.2.2", - "longest": "1.0.1", - "repeat-string": "1.6.1" - } - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true - }, - "ansi-escapes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", - "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==", - "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "anymatch": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", - "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", - "dev": true, - "requires": { - "micromatch": "2.3.11", - "normalize-path": "2.1.1" - } - }, - "append-transform": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-0.4.0.tgz", - "integrity": "sha1-126/jKlNJ24keja61EpLdKthGZE=", - "dev": true, - "requires": { - "default-require-extensions": "1.0.0" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "1.0.3" - } - }, - "arr-diff": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", - "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", - "dev": true, - "requires": { - "arr-flatten": "1.1.0" - } - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", - "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", - "dev": true - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "1.0.3" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true - }, - "array-unique": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", - "dev": true - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", - "dev": true - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", - "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", - "dev": true, - "requires": { - "lodash": "4.17.10" - } - }, - "async-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", - "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", - "dev": true, - "optional": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "atob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", - "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=", - "dev": true - }, - "autoprefixer": { - "version": "7.2.6", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-7.2.6.tgz", - "integrity": "sha512-Iq8TRIB+/9eQ8rbGhcP7ct5cYb/3qjNYAR2SnzLCEcwF6rvVOax8+9+fccgXk4bEhQGjOZd5TLhsksmAdsbGqQ==", - "dev": true, - "requires": { - "browserslist": "2.11.3", - "caniuse-lite": "1.0.30000846", - "normalize-range": "0.1.2", - "num2fraction": "1.2.2", - "postcss": "6.0.22", - "postcss-value-parser": "3.3.0" - } - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", - "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==", - "dev": true - }, - "babel-cli": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-cli/-/babel-cli-6.26.0.tgz", - "integrity": "sha1-UCq1SHTX24itALiHoGODzgPQAvE=", - "dev": true, - "requires": { - "babel-core": "6.26.3", - "babel-polyfill": "6.26.0", - "babel-register": "6.26.0", - "babel-runtime": "6.26.0", - "chokidar": "1.7.0", - "commander": "2.15.1", - "convert-source-map": "1.5.1", - "fs-readdir-recursive": "1.1.0", - "glob": "7.1.2", - "lodash": "4.17.10", - "output-file-sync": "1.1.2", - "path-is-absolute": "1.0.1", - "slash": "1.0.0", - "source-map": "0.5.7", - "v8flags": "2.1.1" - } - }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "esutils": "2.0.2", - "js-tokens": "3.0.2" - } - }, - "babel-core": { - "version": "6.26.3", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", - "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", - "dev": true, - "requires": { - "babel-code-frame": "6.26.0", - "babel-generator": "6.26.1", - "babel-helpers": "6.24.1", - "babel-messages": "6.23.0", - "babel-register": "6.26.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "convert-source-map": "1.5.1", - "debug": "2.6.9", - "json5": "0.5.1", - "lodash": "4.17.10", - "minimatch": "3.0.4", - "path-is-absolute": "1.0.1", - "private": "0.1.8", - "slash": "1.0.0", - "source-map": "0.5.7" - } - }, - "babel-generator": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", - "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", - "dev": true, - "requires": { - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "detect-indent": "4.0.0", - "jsesc": "1.3.0", - "lodash": "4.17.10", - "source-map": "0.5.7", - "trim-right": "1.0.1" - } - }, - "babel-helper-bindify-decorators": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz", - "integrity": "sha1-FMGeXxQte0fxmlJDHlKxzLxAozA=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-builder-binary-assignment-operator-visitor": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", - "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=", - "dev": true, - "requires": { - "babel-helper-explode-assignable-expression": "6.24.1", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-builder-react-jsx": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz", - "integrity": "sha1-Of+DE7dci2Xc7/HzHTg+D/KkCKA=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "esutils": "2.0.2" - } - }, - "babel-helper-call-delegate": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", - "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", - "dev": true, - "requires": { - "babel-helper-hoist-variables": "6.24.1", - "babel-runtime": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-define-map": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", - "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", - "dev": true, - "requires": { - "babel-helper-function-name": "6.24.1", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "lodash": "4.17.10" - } - }, - "babel-helper-explode-assignable-expression": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", - "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-explode-class": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz", - "integrity": "sha1-fcKjkQ3uAHBW4eMdZAztPVTqqes=", - "dev": true, - "requires": { - "babel-helper-bindify-decorators": "6.24.1", - "babel-runtime": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-function-name": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", - "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", - "dev": true, - "requires": { - "babel-helper-get-function-arity": "6.24.1", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-get-function-arity": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", - "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-hoist-variables": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", - "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-optimise-call-expression": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", - "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-regex": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", - "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "lodash": "4.17.10" - } - }, - "babel-helper-remap-async-to-generator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", - "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", - "dev": true, - "requires": { - "babel-helper-function-name": "6.24.1", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helper-replace-supers": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", - "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", - "dev": true, - "requires": { - "babel-helper-optimise-call-expression": "6.24.1", - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-helpers": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", - "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-template": "6.26.0" - } - }, - "babel-jest": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-20.0.3.tgz", - "integrity": "sha1-5KA7E9wQOJ4UD8ZF0J/8TO0wFnE=", - "dev": true, - "requires": { - "babel-core": "6.26.3", - "babel-plugin-istanbul": "4.1.6", - "babel-preset-jest": "20.0.3" - } - }, - "babel-messages": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-check-es2015-constants": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", - "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-istanbul": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz", - "integrity": "sha512-PWP9FQ1AhZhS01T/4qLSKoHGY/xvkZdVBGlKM/HuxxS3+sC66HhTNR7+MpbO/so/cz/wY94MeSWJuP1hXIPfwQ==", - "dev": true, - "requires": { - "babel-plugin-syntax-object-rest-spread": "6.13.0", - "find-up": "2.1.0", - "istanbul-lib-instrument": "1.10.1", - "test-exclude": "4.2.1" - } - }, - "babel-plugin-jest-hoist": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-20.0.3.tgz", - "integrity": "sha1-r+3IU70/jcNUjqZx++adA8wsF2c=", - "dev": true - }, - "babel-plugin-syntax-async-functions": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", - "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=", - "dev": true - }, - "babel-plugin-syntax-async-generators": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz", - "integrity": "sha1-a8lj67FuzLrmuStZbrfzXDQqi5o=", - "dev": true - }, - "babel-plugin-syntax-class-properties": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", - "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=", - "dev": true - }, - "babel-plugin-syntax-decorators": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz", - "integrity": "sha1-MSVjtNvePMgGzuPkFszurd0RrAs=", - "dev": true - }, - "babel-plugin-syntax-dynamic-import": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", - "integrity": "sha1-jWomIpyDdFqZgqRBBRVyyqF5sdo=", - "dev": true - }, - "babel-plugin-syntax-exponentiation-operator": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", - "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=", - "dev": true - }, - "babel-plugin-syntax-flow": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz", - "integrity": "sha1-TDqyCiryaqIM0lmVw5jE63AxDI0=", - "dev": true - }, - "babel-plugin-syntax-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=", - "dev": true - }, - "babel-plugin-syntax-object-rest-spread": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", - "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=", - "dev": true - }, - "babel-plugin-syntax-trailing-function-commas": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", - "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=", - "dev": true - }, - "babel-plugin-transform-async-generator-functions": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz", - "integrity": "sha1-8FiQAUX9PpkHpt3yjaWfIVJYpds=", - "dev": true, - "requires": { - "babel-helper-remap-async-to-generator": "6.24.1", - "babel-plugin-syntax-async-generators": "6.13.0", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-async-to-generator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", - "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=", - "dev": true, - "requires": { - "babel-helper-remap-async-to-generator": "6.24.1", - "babel-plugin-syntax-async-functions": "6.13.0", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-class-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", - "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=", - "dev": true, - "requires": { - "babel-helper-function-name": "6.24.1", - "babel-plugin-syntax-class-properties": "6.13.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0" - } - }, - "babel-plugin-transform-decorators": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz", - "integrity": "sha1-eIAT2PjGtSIr33s0Q5Df13Vp4k0=", - "dev": true, - "requires": { - "babel-helper-explode-class": "6.24.1", - "babel-plugin-syntax-decorators": "6.13.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-arrow-functions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", - "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-block-scoped-functions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", - "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-block-scoping": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", - "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "lodash": "4.17.10" - } - }, - "babel-plugin-transform-es2015-classes": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", - "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", - "dev": true, - "requires": { - "babel-helper-define-map": "6.26.0", - "babel-helper-function-name": "6.24.1", - "babel-helper-optimise-call-expression": "6.24.1", - "babel-helper-replace-supers": "6.24.1", - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-computed-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", - "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-template": "6.26.0" - } - }, - "babel-plugin-transform-es2015-destructuring": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", - "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-duplicate-keys": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", - "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-for-of": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", - "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-function-name": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", - "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", - "dev": true, - "requires": { - "babel-helper-function-name": "6.24.1", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-literals": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", - "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-modules-amd": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", - "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=", - "dev": true, - "requires": { - "babel-plugin-transform-es2015-modules-commonjs": "6.26.2", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0" - } - }, - "babel-plugin-transform-es2015-modules-commonjs": { - "version": "6.26.2", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz", - "integrity": "sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==", - "dev": true, - "requires": { - "babel-plugin-transform-strict-mode": "6.24.1", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-modules-systemjs": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", - "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=", - "dev": true, - "requires": { - "babel-helper-hoist-variables": "6.24.1", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0" - } - }, - "babel-plugin-transform-es2015-modules-umd": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", - "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=", - "dev": true, - "requires": { - "babel-plugin-transform-es2015-modules-amd": "6.24.1", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0" - } - }, - "babel-plugin-transform-es2015-object-super": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", - "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", - "dev": true, - "requires": { - "babel-helper-replace-supers": "6.24.1", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-parameters": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", - "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", - "dev": true, - "requires": { - "babel-helper-call-delegate": "6.24.1", - "babel-helper-get-function-arity": "6.24.1", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-shorthand-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", - "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-spread": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", - "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-sticky-regex": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", - "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", - "dev": true, - "requires": { - "babel-helper-regex": "6.26.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-plugin-transform-es2015-template-literals": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", - "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-typeof-symbol": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", - "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-es2015-unicode-regex": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", - "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", - "dev": true, - "requires": { - "babel-helper-regex": "6.26.0", - "babel-runtime": "6.26.0", - "regexpu-core": "2.0.0" - } - }, - "babel-plugin-transform-exponentiation-operator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", - "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=", - "dev": true, - "requires": { - "babel-helper-builder-binary-assignment-operator-visitor": "6.24.1", - "babel-plugin-syntax-exponentiation-operator": "6.13.0", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-flow-strip-types": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz", - "integrity": "sha1-hMtnKTXUNxT9wyvOhFaNh0Qc988=", - "dev": true, - "requires": { - "babel-plugin-syntax-flow": "6.18.0", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-object-rest-spread": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", - "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=", - "dev": true, - "requires": { - "babel-plugin-syntax-object-rest-spread": "6.13.0", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-react-display-name": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz", - "integrity": "sha1-Z+K/Hx6ck6sI25Z5LgU5K/LMKNE=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-react-jsx": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz", - "integrity": "sha1-hAoCjn30YN/DotKfDA2R9jduZqM=", - "dev": true, - "requires": { - "babel-helper-builder-react-jsx": "6.26.0", - "babel-plugin-syntax-jsx": "6.18.0", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-react-jsx-self": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.22.0.tgz", - "integrity": "sha1-322AqdomEqEh5t3XVYvL7PBuY24=", - "dev": true, - "requires": { - "babel-plugin-syntax-jsx": "6.18.0", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-react-jsx-source": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz", - "integrity": "sha1-ZqwSFT9c0tF7PBkmj0vwGX9E7NY=", - "dev": true, - "requires": { - "babel-plugin-syntax-jsx": "6.18.0", - "babel-runtime": "6.26.0" - } - }, - "babel-plugin-transform-regenerator": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", - "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=", - "dev": true, - "requires": { - "regenerator-transform": "0.10.1" - } - }, - "babel-plugin-transform-strict-mode": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", - "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0" - } - }, - "babel-polyfill": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", - "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "core-js": "2.5.6", - "regenerator-runtime": "0.10.5" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", - "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=", - "dev": true - } - } - }, - "babel-preset-env": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.7.0.tgz", - "integrity": "sha512-9OR2afuKDneX2/q2EurSftUYM0xGu4O2D9adAhVfADDhrYDaxXV0rBbevVYoY9n6nyX1PmQW/0jtpJvUNr9CHg==", - "dev": true, - "requires": { - "babel-plugin-check-es2015-constants": "6.22.0", - "babel-plugin-syntax-trailing-function-commas": "6.22.0", - "babel-plugin-transform-async-to-generator": "6.24.1", - "babel-plugin-transform-es2015-arrow-functions": "6.22.0", - "babel-plugin-transform-es2015-block-scoped-functions": "6.22.0", - "babel-plugin-transform-es2015-block-scoping": "6.26.0", - "babel-plugin-transform-es2015-classes": "6.24.1", - "babel-plugin-transform-es2015-computed-properties": "6.24.1", - "babel-plugin-transform-es2015-destructuring": "6.23.0", - "babel-plugin-transform-es2015-duplicate-keys": "6.24.1", - "babel-plugin-transform-es2015-for-of": "6.23.0", - "babel-plugin-transform-es2015-function-name": "6.24.1", - "babel-plugin-transform-es2015-literals": "6.22.0", - "babel-plugin-transform-es2015-modules-amd": "6.24.1", - "babel-plugin-transform-es2015-modules-commonjs": "6.26.2", - "babel-plugin-transform-es2015-modules-systemjs": "6.24.1", - "babel-plugin-transform-es2015-modules-umd": "6.24.1", - "babel-plugin-transform-es2015-object-super": "6.24.1", - "babel-plugin-transform-es2015-parameters": "6.24.1", - "babel-plugin-transform-es2015-shorthand-properties": "6.24.1", - "babel-plugin-transform-es2015-spread": "6.22.0", - "babel-plugin-transform-es2015-sticky-regex": "6.24.1", - "babel-plugin-transform-es2015-template-literals": "6.22.0", - "babel-plugin-transform-es2015-typeof-symbol": "6.23.0", - "babel-plugin-transform-es2015-unicode-regex": "6.24.1", - "babel-plugin-transform-exponentiation-operator": "6.24.1", - "babel-plugin-transform-regenerator": "6.26.0", - "browserslist": "3.2.8", - "invariant": "2.2.4", - "semver": "5.5.0" - }, - "dependencies": { - "browserslist": { - "version": "3.2.8", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-3.2.8.tgz", - "integrity": "sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ==", - "dev": true, - "requires": { - "caniuse-lite": "1.0.30000846", - "electron-to-chromium": "1.3.48" - } - } - } - }, - "babel-preset-flow": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-preset-flow/-/babel-preset-flow-6.23.0.tgz", - "integrity": "sha1-5xIYiHCFrpoktb5Baa/7WZgWxJ0=", - "dev": true, - "requires": { - "babel-plugin-transform-flow-strip-types": "6.22.0" - } - }, - "babel-preset-jest": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-20.0.3.tgz", - "integrity": "sha1-y6yq3stdaJyh4d4TYOv8ZoYsF4o=", - "dev": true, - "requires": { - "babel-plugin-jest-hoist": "20.0.3" - } - }, - "babel-preset-react": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-react/-/babel-preset-react-6.24.1.tgz", - "integrity": "sha1-umnfrqRfw+xjm2pOzqbhdwLJE4A=", - "dev": true, - "requires": { - "babel-plugin-syntax-jsx": "6.18.0", - "babel-plugin-transform-react-display-name": "6.25.0", - "babel-plugin-transform-react-jsx": "6.24.1", - "babel-plugin-transform-react-jsx-self": "6.22.0", - "babel-plugin-transform-react-jsx-source": "6.22.0", - "babel-preset-flow": "6.23.0" - } - }, - "babel-preset-stage-2": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz", - "integrity": "sha1-2eKWD7PXEYfw5k7sYrwHdnIZvcE=", - "dev": true, - "requires": { - "babel-plugin-syntax-dynamic-import": "6.18.0", - "babel-plugin-transform-class-properties": "6.24.1", - "babel-plugin-transform-decorators": "6.24.1", - "babel-preset-stage-3": "6.24.1" - } - }, - "babel-preset-stage-3": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz", - "integrity": "sha1-g2raCp56f6N8sTj7kyb4eTSkg5U=", - "dev": true, - "requires": { - "babel-plugin-syntax-trailing-function-commas": "6.22.0", - "babel-plugin-transform-async-generator-functions": "6.24.1", - "babel-plugin-transform-async-to-generator": "6.24.1", - "babel-plugin-transform-exponentiation-operator": "6.24.1", - "babel-plugin-transform-object-rest-spread": "6.26.0" - } - }, - "babel-register": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", - "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", - "dev": true, - "requires": { - "babel-core": "6.26.3", - "babel-runtime": "6.26.0", - "core-js": "2.5.6", - "home-or-tmp": "2.0.0", - "lodash": "4.17.10", - "mkdirp": "0.5.1", - "source-map-support": "0.4.18" - } - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "dev": true, - "requires": { - "core-js": "2.5.6", - "regenerator-runtime": "0.11.1" - } - }, - "babel-template": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", - "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "lodash": "4.17.10" - } - }, - "babel-traverse": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", - "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", - "dev": true, - "requires": { - "babel-code-frame": "6.26.0", - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "debug": "2.6.9", - "globals": "9.18.0", - "invariant": "2.2.4", - "lodash": "4.17.10" - } - }, - "babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "esutils": "2.0.2", - "lodash": "4.17.10", - "to-fast-properties": "1.0.3" - } - }, - "babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "1.0.1", - "class-utils": "0.3.6", - "component-emitter": "1.2.1", - "define-property": "1.0.0", - "isobject": "3.0.1", - "mixin-deep": "1.3.1", - "pascalcase": "0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "1.0.2" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "6.0.2" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "6.0.2" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "dev": true, - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "binary-extensions": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", - "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", - "dev": true, - "optional": true - }, - "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", - "dev": true, - "requires": { - "hoek": "4.2.1" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", - "dev": true, - "requires": { - "expand-range": "1.8.2", - "preserve": "0.2.0", - "repeat-element": "1.1.2" - } - }, - "browser-resolve": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.2.tgz", - "integrity": "sha1-j/CbCixCFxihBRwmCzLkj0QpOM4=", - "dev": true, - "requires": { - "resolve": "1.1.7" - }, - "dependencies": { - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - } - } - }, - "browserslist": { - "version": "2.11.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.11.3.tgz", - "integrity": "sha512-yWu5cXT7Av6mVwzWc8lMsJMHWn4xyjSuGYi4IozbVTLUOEYPSagUB8kiMDUHA1fS3zjr8nkxkn9jdvug4BBRmA==", - "dev": true, - "requires": { - "caniuse-lite": "1.0.30000846", - "electron-to-chromium": "1.3.48" - } - }, - "bser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz", - "integrity": "sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=", - "dev": true, - "requires": { - "node-int64": "0.4.0" - } - }, - "buffer-from": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz", - "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==", - "dev": true - }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "1.0.0", - "component-emitter": "1.2.1", - "get-value": "2.0.6", - "has-value": "1.0.0", - "isobject": "3.0.1", - "set-value": "2.0.0", - "to-object-path": "0.3.0", - "union-value": "1.0.0", - "unset-value": "1.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "caller-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", - "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", - "dev": true, - "requires": { - "callsites": "0.2.0" - } - }, - "callsites": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", - "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", - "dev": true - }, - "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", - "dev": true, - "optional": true - }, - "camelcase-css": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-1.0.1.tgz", - "integrity": "sha1-FXxCOCZfXPlKHf/ehkRlUsvz9wU=" - }, - "caniuse-lite": { - "version": "1.0.30000846", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000846.tgz", - "integrity": "sha512-qxUOHr5mTaadWH1ap0ueivHd8x42Bnemcn+JutVr7GWmm2bU4zoBhjuv5QdXgALQnnT626lOQros7cCDf8PwCg==", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", - "dev": true, - "optional": true, - "requires": { - "align-text": "0.1.4", - "lazy-cache": "1.0.4" - } - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "chardet": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", - "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", - "dev": true - }, - "chokidar": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", - "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", - "dev": true, - "optional": true, - "requires": { - "anymatch": "1.3.2", - "async-each": "1.0.1", - "fsevents": "1.2.4", - "glob-parent": "2.0.0", - "inherits": "2.0.3", - "is-binary-path": "1.0.1", - "is-glob": "2.0.1", - "path-is-absolute": "1.0.1", - "readdirp": "2.1.0" - } - }, - "ci-info": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.3.tgz", - "integrity": "sha512-SK/846h/Rcy8q9Z9CAwGBLfCJ6EkjJWdpelWDufQpqVDYq2Wnnv8zlSO6AMQap02jvhVruKKpEtQOufo3pFhLg==", - "dev": true - }, - "circular-json": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", - "dev": true - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "3.1.0", - "define-property": "0.2.5", - "isobject": "3.0.1", - "static-extend": "0.1.2" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "0.1.6" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "clean-css": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.11.tgz", - "integrity": "sha1-Ls3xRaujj1R0DybO/Q/z4D4SXWo=", - "dev": true, - "requires": { - "source-map": "0.5.7" - } - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "requires": { - "restore-cursor": "2.0.0" - } - }, - "cli-table2": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/cli-table2/-/cli-table2-0.2.0.tgz", - "integrity": "sha1-LR738hig54biFFQFYtS9F3/jLZc=", - "dev": true, - "requires": { - "colors": "1.3.0", - "lodash": "3.10.1", - "string-width": "1.0.2" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", - "dev": true - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - } - } - }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", - "dev": true - }, - "cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "dev": true, - "optional": true, - "requires": { - "center-align": "0.1.3", - "right-align": "0.1.3", - "wordwrap": "0.0.2" - }, - "dependencies": { - "wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", - "dev": true, - "optional": true - } - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "1.0.0", - "object-visit": "1.0.1" - } - }, - "color-convert": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", - "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "colors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.0.tgz", - "integrity": "sha512-EDpX3a7wHMWFA7PUHWPHNWqOxIIRSJetuwl0AS5Oi/5FMV8kWm69RTlgm00GKjBO1xFHMtBbL49yRtMMdticBw==", - "dev": true, - "optional": true - }, - "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "dev": true, - "requires": { - "delayed-stream": "1.0.0" - } - }, - "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" - }, - "comment-regex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/comment-regex/-/comment-regex-1.0.1.tgz", - "integrity": "sha512-IWlN//Yfby92tOIje7J18HkNmWRR7JESA/BK8W7wqY/akITpU5B0JQWnbTjCfdChSrDNb0DrdA9jfAxiiBXyiQ==" - }, - "compare-versions": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.2.1.tgz", - "integrity": "sha512-2y2nHcopMG/NAyk6vWXlLs86XeM9sik4jmx1tKIgzMi9/RQ2eo758RGpxQO3ErihHmg0RlQITPqgz73y6s7quA==", - "dev": true - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "requires": { - "buffer-from": "1.0.0", - "inherits": "2.0.3", - "readable-stream": "2.3.6", - "typedarray": "0.0.6" - } - }, - "content-type-parser": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/content-type-parser/-/content-type-parser-1.0.2.tgz", - "integrity": "sha512-lM4l4CnMEwOLHAHr/P6MEZwZFPJFtAAKgL6pogbXmVZggIqXhdB6RbBtPOTsw2FcXwYhehRGERJmRrjOiIB8pQ==", - "dev": true - }, - "convert-source-map": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", - "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=", - "dev": true - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "core-js": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.6.tgz", - "integrity": "sha512-lQUVfQi0aLix2xpyjrrJEvfuYCqPc/HwmTKsC/VNf8q0zsjX7SQZtp4+oRONN5Tsur9GDETPjj+Ub2iDiGZfSQ==", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "dev": true, - "requires": { - "lru-cache": "4.1.3", - "shebang-command": "1.2.0", - "which": "1.3.0" - } - }, - "cssom": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.2.tgz", - "integrity": "sha1-uANhcMefB6kP8vFuIihAJ6JDhIs=", - "dev": true - }, - "cssstyle": { - "version": "0.2.37", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.2.37.tgz", - "integrity": "sha1-VBCXI0yyUTyDzu06zdwn/yeYfVQ=", - "dev": true, - "requires": { - "cssom": "0.3.2" - } - }, - "cvss": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/cvss/-/cvss-1.0.3.tgz", - "integrity": "sha512-1FfNhEFVfeC+fgZpEr6oCOOTXifJicZS+Lq/mmUKI4Om+2O8zYspc/uhw51He+CTM5givI1dqIw5JUqyi1BWtA==", - "dev": true - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "1.0.0" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "default-require-extensions": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-1.0.0.tgz", - "integrity": "sha1-836hXT4T/9m0N9M+GnW1+5eHTLg=", - "dev": true, - "requires": { - "strip-bom": "2.0.0" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "1.0.2", - "isobject": "3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "6.0.2" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "6.0.2" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "defined": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", - "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" - }, - "del": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", - "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", - "dev": true, - "requires": { - "globby": "5.0.0", - "is-path-cwd": "1.0.0", - "is-path-in-cwd": "1.0.1", - "object-assign": "4.1.1", - "pify": "2.3.0", - "pinkie-promise": "2.0.1", - "rimraf": "2.6.2" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "detect-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", - "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", - "dev": true, - "requires": { - "repeating": "2.0.1" - } - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true - }, - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "2.0.2" - } - }, - "dot-prop": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", - "requires": { - "is-obj": "1.0.1" - } - }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "dev": true, - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "electron-to-chromium": { - "version": "1.3.48", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.48.tgz", - "integrity": "sha1-07DYWTgUBE4JLs4hCPw6ya6kuQA=", - "dev": true - }, - "errno": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", - "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", - "dev": true, - "requires": { - "prr": "1.0.1" - } - }, - "error-ex": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", - "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", - "dev": true, - "requires": { - "is-arrayish": "0.2.1" - } - }, - "es6-promise": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", - "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==", - "dev": true - }, - "es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "dev": true, - "requires": { - "es6-promise": "4.2.4" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "escodegen": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.9.1.tgz", - "integrity": "sha512-6hTjO1NAWkHnDk3OqQ4YrCuwwmGHL9S3nPlzBOUG/R44rda3wLNrfvQ5fkSGjyhHFKM7ALPKcKGrwvCLe0lC7Q==", - "dev": true, - "requires": { - "esprima": "3.1.3", - "estraverse": "4.2.0", - "esutils": "2.0.2", - "optionator": "0.8.2", - "source-map": "0.6.1" - }, - "dependencies": { - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - } - } - }, - "eslint": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz", - "integrity": "sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==", - "dev": true, - "requires": { - "ajv": "5.5.2", - "babel-code-frame": "6.26.0", - "chalk": "2.4.1", - "concat-stream": "1.6.2", - "cross-spawn": "5.1.0", - "debug": "3.1.0", - "doctrine": "2.1.0", - "eslint-scope": "3.7.1", - "eslint-visitor-keys": "1.0.0", - "espree": "3.5.4", - "esquery": "1.0.1", - "esutils": "2.0.2", - "file-entry-cache": "2.0.0", - "functional-red-black-tree": "1.0.1", - "glob": "7.1.2", - "globals": "11.5.0", - "ignore": "3.3.8", - "imurmurhash": "0.1.4", - "inquirer": "3.3.0", - "is-resolvable": "1.1.0", - "js-yaml": "3.11.0", - "json-stable-stringify-without-jsonify": "1.0.1", - "levn": "0.3.0", - "lodash": "4.17.10", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "natural-compare": "1.4.0", - "optionator": "0.8.2", - "path-is-inside": "1.0.2", - "pluralize": "7.0.0", - "progress": "2.0.0", - "regexpp": "1.1.0", - "require-uncached": "1.0.3", - "semver": "5.5.0", - "strip-ansi": "4.0.0", - "strip-json-comments": "2.0.1", - "table": "4.0.2", - "text-table": "0.2.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "3.2.1", - "escape-string-regexp": "1.0.5", - "supports-color": "5.4.0" - } - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "globals": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.5.0.tgz", - "integrity": "sha512-hYyf+kI8dm3nORsiiXUQigOU62hDLfJ9G01uyGMxhc6BKsircrUhC4uJPQPUSuq2GrTmiiEt7ewxlMdBewfmKQ==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "3.0.0" - } - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "3.0.0" - } - } - } - }, - "eslint-config-postcss": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/eslint-config-postcss/-/eslint-config-postcss-2.0.2.tgz", - "integrity": "sha1-yuHGCTzteFCJSluF++HR4jK3Kvs=", - "dev": true - }, - "eslint-config-prettier": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-2.9.0.tgz", - "integrity": "sha512-ag8YEyBXsm3nmOv1Hz991VtNNDMRa+MNy8cY47Pl4bw6iuzqKbJajXdqUpiw13STdLLrznxgm1hj9NhxeOYq0A==", - "dev": true, - "requires": { - "get-stdin": "5.0.1" - } - }, - "eslint-plugin-prettier": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-2.6.0.tgz", - "integrity": "sha512-floiaI4F7hRkTrFe8V2ItOK97QYrX75DjmdzmVITZoAP6Cn06oEDPQRsO6MlHEP/u2SxI3xQ52Kpjw6j5WGfeQ==", - "dev": true, - "requires": { - "fast-diff": "1.1.2", - "jest-docblock": "21.2.0" - } - }, - "eslint-scope": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", - "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", - "dev": true, - "requires": { - "esrecurse": "4.2.1", - "estraverse": "4.2.0" - } - }, - "eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", - "dev": true - }, - "espree": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", - "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", - "dev": true, - "requires": { - "acorn": "5.5.3", - "acorn-jsx": "3.0.1" - } - }, - "esprima": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", - "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", - "dev": true - }, - "esquery": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", - "dev": true, - "requires": { - "estraverse": "4.2.0" - } - }, - "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", - "dev": true, - "requires": { - "estraverse": "4.2.0" - } - }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "exec-sh": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.1.tgz", - "integrity": "sha512-aLt95pexaugVtQerpmE51+4QfWrNc304uez7jvj6fWnN8GeEHpttB8F36n8N7uVhUMbH/1enbxQ9HImZ4w/9qg==", - "dev": true, - "requires": { - "merge": "1.2.0" - } - }, - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "dev": true, - "requires": { - "cross-spawn": "5.1.0", - "get-stream": "3.0.0", - "is-stream": "1.1.0", - "npm-run-path": "2.0.2", - "p-finally": "1.0.0", - "signal-exit": "3.0.2", - "strip-eof": "1.0.0" - } - }, - "expand-brackets": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", - "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", - "dev": true, - "requires": { - "is-posix-bracket": "0.1.1" - } - }, - "expand-range": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", - "dev": true, - "requires": { - "fill-range": "2.2.4" - } - }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", - "dev": true - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "2.0.4" - } - } - } - }, - "external-editor": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", - "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", - "dev": true, - "requires": { - "chardet": "0.4.2", - "iconv-lite": "0.4.23", - "tmp": "0.0.33" - } - }, - "extglob": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", - "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", - "dev": true, - "requires": { - "is-extglob": "1.0.0" - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", - "dev": true - }, - "fast-diff": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", - "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "fb-watchman": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", - "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", - "dev": true, - "requires": { - "bser": "2.0.0" - } - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "dev": true, - "requires": { - "escape-string-regexp": "1.0.5" - } - }, - "file-entry-cache": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", - "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", - "dev": true, - "requires": { - "flat-cache": "1.3.0", - "object-assign": "4.1.1" - } - }, - "filename-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", - "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", - "dev": true - }, - "fileset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", - "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=", - "dev": true, - "requires": { - "glob": "7.1.2", - "minimatch": "3.0.4" - } - }, - "fill-range": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", - "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", - "dev": true, - "requires": { - "is-number": "2.1.0", - "isobject": "2.1.0", - "randomatic": "3.0.0", - "repeat-element": "1.1.2", - "repeat-string": "1.6.1" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "2.0.0" - } - }, - "flat-cache": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", - "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", - "dev": true, - "requires": { - "circular-json": "0.3.3", - "del": "2.2.2", - "graceful-fs": "4.1.11", - "write": "0.2.1" - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", - "dev": true, - "requires": { - "for-in": "1.0.2" - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", - "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", - "dev": true, - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.6", - "mime-types": "2.1.18" - } - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "0.2.2" - } - }, - "fs-extra": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", - "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", - "requires": { - "graceful-fs": "4.1.11", - "jsonfile": "4.0.0", - "universalify": "0.1.1" - } - }, - "fs-readdir-recursive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", - "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", - "dev": true, - "optional": true, - "requires": { - "nan": "2.10.0", - "node-pre-gyp": "0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.21", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": "2.1.2" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "minipass": { - "version": "2.2.4", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "minizlib": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "2.6.9", - "iconv-lite": "0.4.21", - "sax": "1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "1.0.3", - "mkdirp": "0.5.1", - "needle": "2.2.0", - "nopt": "4.0.1", - "npm-packlist": "1.1.10", - "npmlog": "4.1.2", - "rc": "1.2.7", - "rimraf": "2.6.2", - "semver": "5.5.0", - "tar": "4.4.1" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1.1.1", - "osenv": "0.1.5" - } - }, - "npm-bundled": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.1.10", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "3.0.1", - "npm-bundled": "1.0.3" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "0.5.1", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.1", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.1.1", - "bundled": true, - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.5.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "1.0.1", - "fs-minipass": "1.2.5", - "minipass": "2.2.4", - "minizlib": "1.1.0", - "mkdirp": "0.5.1", - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "yallist": { - "version": "3.0.2", - "bundled": true, - "dev": true - } - } - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "gather-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gather-stream/-/gather-stream-1.0.0.tgz", - "integrity": "sha1-szmUr0V6gRVwDUEPMXczy+egkEs=" - }, - "get-caller-file": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", - "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=", - "dev": true - }, - "get-stdin": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz", - "integrity": "sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=", - "dev": true - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "1.0.0" - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "glob-base": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", - "dev": true, - "requires": { - "glob-parent": "2.0.0", - "is-glob": "2.0.1" - } - }, - "glob-parent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", - "dev": true, - "requires": { - "is-glob": "2.0.1" - } - }, - "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", - "dev": true - }, - "globby": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", - "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", - "dev": true, - "requires": { - "array-union": "1.0.2", - "arrify": "1.0.1", - "glob": "7.1.2", - "object-assign": "4.1.1", - "pify": "2.3.0", - "pinkie-promise": "2.0.1" - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" - }, - "growly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true - }, - "handlebars": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", - "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", - "dev": true, - "requires": { - "async": "1.5.2", - "optimist": "0.6.1", - "source-map": "0.4.4", - "uglify-js": "2.8.29" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - }, - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "dev": true, - "requires": { - "amdefine": "1.0.1" - } - } - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "dev": true, - "requires": { - "ajv": "5.5.2", - "har-schema": "2.0.0" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "2.1.1" - } - }, - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "2.0.6", - "has-values": "1.0.0", - "isobject": "3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "3.0.0", - "kind-of": "4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "hoek": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", - "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", - "dev": true - }, - "home-or-tmp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", - "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", - "dev": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "hosted-git-info": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", - "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==", - "dev": true - }, - "html-encoding-sniffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", - "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", - "dev": true, - "requires": { - "whatwg-encoding": "1.0.3" - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.14.1" - } - }, - "https-proxy-agent": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", - "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", - "dev": true, - "requires": { - "agent-base": "4.2.0", - "debug": "3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } - } - }, - "iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", - "dev": true, - "requires": { - "safer-buffer": "2.1.2" - } - }, - "ignore": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.8.tgz", - "integrity": "sha512-pUh+xUQQhQzevjRHHFqqcTy0/dP/kS9I8HSrUydhihjuD09W6ldVWFtIrwhXdUJHis3i2rZNqEHpZH/cbinFbg==", - "dev": true - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "indexes-of": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", - "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "inquirer": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", - "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", - "dev": true, - "requires": { - "ansi-escapes": "3.1.0", - "chalk": "2.4.1", - "cli-cursor": "2.1.0", - "cli-width": "2.2.0", - "external-editor": "2.2.0", - "figures": "2.0.0", - "lodash": "4.17.10", - "mute-stream": "0.0.7", - "run-async": "2.3.0", - "rx-lite": "4.0.8", - "rx-lite-aggregates": "4.0.8", - "string-width": "2.1.1", - "strip-ansi": "4.0.0", - "through": "2.3.8" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "3.2.1", - "escape-string-regexp": "1.0.5", - "supports-color": "5.4.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "3.0.0" - } - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "3.0.0" - } - } - } - }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, - "requires": { - "loose-envify": "1.3.1" - } - }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", - "dev": true - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "optional": true, - "requires": { - "binary-extensions": "1.11.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-builtin-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", - "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", - "dev": true, - "requires": { - "builtin-modules": "1.1.1" - } - }, - "is-ci": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.1.0.tgz", - "integrity": "sha512-c7TnwxLePuqIlxHgr7xtxzycJPegNHFuIrBkwbf8hc58//+Op1CqFkyS+xnIMkwn9UsJIwc174BIjkyBmSpjKg==", - "dev": true, - "requires": { - "ci-info": "1.1.3" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-dotfile": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", - "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", - "dev": true - }, - "is-equal-shallow": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", - "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", - "dev": true, - "requires": { - "is-primitive": "2.0.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true - }, - "is-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "dev": true, - "requires": { - "is-extglob": "1.0.0" - } - }, - "is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - } - }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" - }, - "is-odd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", - "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", - "dev": true, - "requires": { - "is-number": "4.0.0" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true - } - } - }, - "is-path-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", - "dev": true - }, - "is-path-in-cwd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", - "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", - "dev": true, - "requires": { - "is-path-inside": "1.0.1" - } - }, - "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "dev": true, - "requires": { - "path-is-inside": "1.0.2" - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "is-posix-bracket": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", - "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", - "dev": true - }, - "is-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", - "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", - "dev": true - }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", - "dev": true - }, - "is-resolvable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", - "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "istanbul-api": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.1.tgz", - "integrity": "sha512-duj6AlLcsWNwUpfyfHt0nWIeRiZpuShnP40YTxOGQgtaN8fd6JYSxsvxUphTDy8V5MfDXo4s/xVCIIvVCO808g==", - "dev": true, - "requires": { - "async": "2.6.1", - "compare-versions": "3.2.1", - "fileset": "2.0.3", - "istanbul-lib-coverage": "1.2.0", - "istanbul-lib-hook": "1.2.0", - "istanbul-lib-instrument": "1.10.1", - "istanbul-lib-report": "1.1.4", - "istanbul-lib-source-maps": "1.2.4", - "istanbul-reports": "1.3.0", - "js-yaml": "3.11.0", - "mkdirp": "0.5.1", - "once": "1.4.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "istanbul-lib-source-maps": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.4.tgz", - "integrity": "sha512-UzuK0g1wyQijiaYQxj/CdNycFhAd2TLtO2obKQMTZrZ1jzEMRY3rvpASEKkaxbRR6brvdovfA03znPa/pXcejg==", - "dev": true, - "requires": { - "debug": "3.1.0", - "istanbul-lib-coverage": "1.2.0", - "mkdirp": "0.5.1", - "rimraf": "2.6.2", - "source-map": "0.5.7" - } - } - } - }, - "istanbul-lib-coverage": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz", - "integrity": "sha512-GvgM/uXRwm+gLlvkWHTjDAvwynZkL9ns15calTrmhGgowlwJBbWMYzWbKqE2DT6JDP1AFXKa+Zi0EkqNCUqY0A==", - "dev": true - }, - "istanbul-lib-hook": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.0.tgz", - "integrity": "sha512-p3En6/oGkFQV55Up8ZPC2oLxvgSxD8CzA0yBrhRZSh3pfv3OFj9aSGVC0yoerAi/O4u7jUVnOGVX1eVFM+0tmQ==", - "dev": true, - "requires": { - "append-transform": "0.4.0" - } - }, - "istanbul-lib-instrument": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.1.tgz", - "integrity": "sha512-1dYuzkOCbuR5GRJqySuZdsmsNKPL3PTuyPevQfoCXJePT9C8y1ga75neU+Tuy9+yS3G/dgx8wgOmp2KLpgdoeQ==", - "dev": true, - "requires": { - "babel-generator": "6.26.1", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "istanbul-lib-coverage": "1.2.0", - "semver": "5.5.0" - } - }, - "istanbul-lib-report": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.4.tgz", - "integrity": "sha512-Azqvq5tT0U09nrncK3q82e/Zjkxa4tkFZv7E6VcqP0QCPn6oNljDPfrZEC/umNXds2t7b8sRJfs6Kmpzt8m2kA==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "1.2.0", - "mkdirp": "0.5.1", - "path-parse": "1.0.5", - "supports-color": "3.2.3" - } - }, - "istanbul-lib-source-maps": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.3.tgz", - "integrity": "sha512-fDa0hwU/5sDXwAklXgAoCJCOsFsBplVQ6WBldz5UwaqOzmDhUK4nfuR7/G//G2lERlblUNJB8P6e8cXq3a7MlA==", - "dev": true, - "requires": { - "debug": "3.1.0", - "istanbul-lib-coverage": "1.2.0", - "mkdirp": "0.5.1", - "rimraf": "2.6.2", - "source-map": "0.5.7" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } - } - }, - "istanbul-reports": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.3.0.tgz", - "integrity": "sha512-y2Z2IMqE1gefWUaVjrBm0mSKvUkaBy9Vqz8iwr/r40Y9hBbIteH5wqHG/9DLTfJ9xUnUT2j7A3+VVJ6EaYBllA==", - "dev": true, - "requires": { - "handlebars": "4.0.11" - } - }, - "jest": { - "version": "20.0.4", - "resolved": "https://registry.npmjs.org/jest/-/jest-20.0.4.tgz", - "integrity": "sha1-PdJgwpidba1nix6cxNkZRPbWAqw=", - "dev": true, - "requires": { - "jest-cli": "20.0.4" - }, - "dependencies": { - "ansi-escapes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", - "dev": true - }, - "callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", - "dev": true - }, - "jest-cli": { - "version": "20.0.4", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-20.0.4.tgz", - "integrity": "sha1-5TKxnYiuW8bEF+iwWTpv6VSx3JM=", - "dev": true, - "requires": { - "ansi-escapes": "1.4.0", - "callsites": "2.0.0", - "chalk": "1.1.3", - "graceful-fs": "4.1.11", - "is-ci": "1.1.0", - "istanbul-api": "1.3.1", - "istanbul-lib-coverage": "1.2.0", - "istanbul-lib-instrument": "1.10.1", - "istanbul-lib-source-maps": "1.2.3", - "jest-changed-files": "20.0.3", - "jest-config": "20.0.4", - "jest-docblock": "20.0.3", - "jest-environment-jsdom": "20.0.3", - "jest-haste-map": "20.0.5", - "jest-jasmine2": "20.0.4", - "jest-message-util": "20.0.3", - "jest-regex-util": "20.0.3", - "jest-resolve-dependencies": "20.0.3", - "jest-runtime": "20.0.4", - "jest-snapshot": "20.0.3", - "jest-util": "20.0.3", - "micromatch": "2.3.11", - "node-notifier": "5.2.1", - "pify": "2.3.0", - "slash": "1.0.0", - "string-length": "1.0.1", - "throat": "3.2.0", - "which": "1.3.0", - "worker-farm": "1.6.0", - "yargs": "7.1.0" - } - }, - "jest-docblock": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-20.0.3.tgz", - "integrity": "sha1-F76phDQswz2DxQ++FUXqDvqkRxI=", - "dev": true - } - } - }, - "jest-changed-files": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-20.0.3.tgz", - "integrity": "sha1-k5TVzGXEOEBhSb7xv01Sto4D4/g=", - "dev": true - }, - "jest-config": { - "version": "20.0.4", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-20.0.4.tgz", - "integrity": "sha1-43kwqyIXyRNgXv8T5712PsSPruo=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "glob": "7.1.2", - "jest-environment-jsdom": "20.0.3", - "jest-environment-node": "20.0.3", - "jest-jasmine2": "20.0.4", - "jest-matcher-utils": "20.0.3", - "jest-regex-util": "20.0.3", - "jest-resolve": "20.0.4", - "jest-validate": "20.0.3", - "pretty-format": "20.0.3" - } - }, - "jest-diff": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-20.0.3.tgz", - "integrity": "sha1-gfKI/Z5nXw+yPHXxwrGURf5YZhc=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "diff": "3.5.0", - "jest-matcher-utils": "20.0.3", - "pretty-format": "20.0.3" - } - }, - "jest-docblock": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-21.2.0.tgz", - "integrity": "sha512-5IZ7sY9dBAYSV+YjQ0Ovb540Ku7AO9Z5o2Cg789xj167iQuZ2cG+z0f3Uct6WeYLbU6aQiM2pCs7sZ+4dotydw==", - "dev": true - }, - "jest-environment-jsdom": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-20.0.3.tgz", - "integrity": "sha1-BIqKwS7iJfcZBBdxODS7mZeH3pk=", - "dev": true, - "requires": { - "jest-mock": "20.0.3", - "jest-util": "20.0.3", - "jsdom": "9.12.0" - } - }, - "jest-environment-node": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-20.0.3.tgz", - "integrity": "sha1-1Ii8RhKvLCRumG6K52caCZFj1AM=", - "dev": true, - "requires": { - "jest-mock": "20.0.3", - "jest-util": "20.0.3" - } - }, - "jest-haste-map": { - "version": "20.0.5", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-20.0.5.tgz", - "integrity": "sha512-0IKAQjUvuZjMCNi/0VNQQF74/H9KB67hsHJqGiwTWQC6XO5Azs7kLWm+6Q/dwuhvDUvABDOBMFK2/FwZ3sZ07Q==", - "dev": true, - "requires": { - "fb-watchman": "2.0.0", - "graceful-fs": "4.1.11", - "jest-docblock": "20.0.3", - "micromatch": "2.3.11", - "sane": "1.6.0", - "worker-farm": "1.6.0" - }, - "dependencies": { - "jest-docblock": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-20.0.3.tgz", - "integrity": "sha1-F76phDQswz2DxQ++FUXqDvqkRxI=", - "dev": true - } - } - }, - "jest-jasmine2": { - "version": "20.0.4", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-20.0.4.tgz", - "integrity": "sha1-/MWxQReA2RHQQpAu8YWehS5g1eE=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "graceful-fs": "4.1.11", - "jest-diff": "20.0.3", - "jest-matcher-utils": "20.0.3", - "jest-matchers": "20.0.3", - "jest-message-util": "20.0.3", - "jest-snapshot": "20.0.3", - "once": "1.4.0", - "p-map": "1.2.0" - } - }, - "jest-matcher-utils": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-20.0.3.tgz", - "integrity": "sha1-s6a443yld4A7CDKpixZPRLeBVhI=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "pretty-format": "20.0.3" - } - }, - "jest-matchers": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jest-matchers/-/jest-matchers-20.0.3.tgz", - "integrity": "sha1-ymnbHDLbWm9wf6XgQBq7VXAN/WA=", - "dev": true, - "requires": { - "jest-diff": "20.0.3", - "jest-matcher-utils": "20.0.3", - "jest-message-util": "20.0.3", - "jest-regex-util": "20.0.3" - } - }, - "jest-message-util": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-20.0.3.tgz", - "integrity": "sha1-auwoRDBvyw5udNV5bBAG2W/dgxw=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "micromatch": "2.3.11", - "slash": "1.0.0" - } - }, - "jest-mock": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-20.0.3.tgz", - "integrity": "sha1-i8Bw6QQUqhVcEajWTIaaDVxx2lk=", - "dev": true - }, - "jest-regex-util": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-20.0.3.tgz", - "integrity": "sha1-hburXRM+RGJbGfr4xqpRItCF12I=", - "dev": true - }, - "jest-resolve": { - "version": "20.0.4", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-20.0.4.tgz", - "integrity": "sha1-lEiz6La6/BVHlETGSZBFt//ll6U=", - "dev": true, - "requires": { - "browser-resolve": "1.11.2", - "is-builtin-module": "1.0.0", - "resolve": "1.7.1" - } - }, - "jest-resolve-dependencies": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-20.0.3.tgz", - "integrity": "sha1-bhSntxevDyyzZnxUneQK8Bexcjo=", - "dev": true, - "requires": { - "jest-regex-util": "20.0.3" - } - }, - "jest-runtime": { - "version": "20.0.4", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-20.0.4.tgz", - "integrity": "sha1-osgCIZxCA/dU3xQE5JAYYWnRJNg=", - "dev": true, - "requires": { - "babel-core": "6.26.3", - "babel-jest": "20.0.3", - "babel-plugin-istanbul": "4.1.6", - "chalk": "1.1.3", - "convert-source-map": "1.5.1", - "graceful-fs": "4.1.11", - "jest-config": "20.0.4", - "jest-haste-map": "20.0.5", - "jest-regex-util": "20.0.3", - "jest-resolve": "20.0.4", - "jest-util": "20.0.3", - "json-stable-stringify": "1.0.1", - "micromatch": "2.3.11", - "strip-bom": "3.0.0", - "yargs": "7.1.0" - }, - "dependencies": { - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - } - } - }, - "jest-snapshot": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-20.0.3.tgz", - "integrity": "sha1-W4R+GtsaTZCFKn+fElCG4YfHZWY=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "jest-diff": "20.0.3", - "jest-matcher-utils": "20.0.3", - "jest-util": "20.0.3", - "natural-compare": "1.4.0", - "pretty-format": "20.0.3" - } - }, - "jest-util": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-20.0.3.tgz", - "integrity": "sha1-DAf32A2C9OWmfG+LnD/n9lz9Mq0=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "graceful-fs": "4.1.11", - "jest-message-util": "20.0.3", - "jest-mock": "20.0.3", - "jest-validate": "20.0.3", - "leven": "2.1.0", - "mkdirp": "0.5.1" - } - }, - "jest-validate": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-20.0.3.tgz", - "integrity": "sha1-0M/R3k9XnymEhJJcKA+PHZTsPKs=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "jest-matcher-utils": "20.0.3", - "leven": "2.1.0", - "pretty-format": "20.0.3" - } - }, - "js-base64": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.5.tgz", - "integrity": "sha512-aUnNwqMOXw3yvErjMPSQu6qIIzUmT1e5KcU1OZxRDU1g/am6mzBvcrmLAYwzmB59BHPrh5/tKaiF4OPhqRWESQ==" - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true - }, - "js-yaml": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.11.0.tgz", - "integrity": "sha512-saJstZWv7oNeOyBh3+Dx1qWzhW0+e6/8eDzo7p5rDFqxntSztloLtuKu+Ejhtq82jsilwOIZYsCz+lIjthg1Hw==", - "dev": true, - "requires": { - "argparse": "1.0.10", - "esprima": "4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true, - "optional": true - }, - "jsdom": { - "version": "9.12.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-9.12.0.tgz", - "integrity": "sha1-6MVG//ywbADUgzyoRBD+1/igl9Q=", - "dev": true, - "requires": { - "abab": "1.0.4", - "acorn": "4.0.13", - "acorn-globals": "3.1.0", - "array-equal": "1.0.0", - "content-type-parser": "1.0.2", - "cssom": "0.3.2", - "cssstyle": "0.2.37", - "escodegen": "1.9.1", - "html-encoding-sniffer": "1.0.2", - "nwmatcher": "1.4.4", - "parse5": "1.5.1", - "request": "2.87.0", - "sax": "1.2.4", - "symbol-tree": "3.2.2", - "tough-cookie": "2.3.4", - "webidl-conversions": "4.0.2", - "whatwg-encoding": "1.0.3", - "whatwg-url": "4.8.0", - "xml-name-validator": "2.0.1" - }, - "dependencies": { - "acorn": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", - "dev": true - } - } - }, - "jsesc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", - "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", - "dev": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "dev": true, - "requires": { - "jsonify": "0.0.0" - } - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", - "dev": true - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "requires": { - "graceful-fs": "4.1.11" - } - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "1.1.6" - } - }, - "lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", - "dev": true, - "optional": true - }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "dev": true, - "requires": { - "invert-kv": "1.0.0" - } - }, - "leven": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", - "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "1.1.2", - "type-check": "0.3.2" - } - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "parse-json": "2.2.0", - "pify": "2.3.0", - "pinkie-promise": "2.0.1", - "strip-bom": "2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "2.0.0", - "path-exists": "3.0.0" - } - }, - "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" - }, - "longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true - }, - "loose-envify": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", - "dev": true, - "requires": { - "js-tokens": "3.0.2" - } - }, - "lru-cache": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", - "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", - "dev": true, - "requires": { - "pseudomap": "1.0.2", - "yallist": "2.1.2" - } - }, - "makeerror": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", - "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", - "dev": true, - "requires": { - "tmpl": "1.0.4" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "1.0.1" - } - }, - "math-random": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", - "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=", - "dev": true - }, - "mem": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", - "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", - "dev": true, - "requires": { - "mimic-fn": "1.2.0" - } - }, - "merge": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz", - "integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=", - "dev": true - }, - "micromatch": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", - "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", - "dev": true, - "requires": { - "arr-diff": "2.0.0", - "array-unique": "0.2.1", - "braces": "1.8.5", - "expand-brackets": "0.1.5", - "extglob": "0.3.2", - "filename-regex": "2.0.1", - "is-extglob": "1.0.0", - "is-glob": "2.0.1", - "kind-of": "3.2.2", - "normalize-path": "2.1.1", - "object.omit": "2.0.1", - "parse-glob": "3.0.4", - "regex-cache": "0.4.4" - } - }, - "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", - "dev": true - }, - "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "dev": true, - "requires": { - "mime-db": "1.33.0" - } - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - }, - "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", - "dev": true, - "requires": { - "for-in": "1.0.2", - "is-extendable": "1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - } - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", - "dev": true - }, - "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", - "dev": true, - "optional": true - }, - "nanomatch": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", - "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", - "dev": true, - "requires": { - "arr-diff": "4.0.0", - "array-unique": "0.3.2", - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "fragment-cache": "0.2.1", - "is-odd": "2.0.0", - "is-windows": "1.0.2", - "kind-of": "6.0.2", - "object.pick": "1.3.0", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", - "dev": true - }, - "node-notifier": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.2.1.tgz", - "integrity": "sha512-MIBs+AAd6dJ2SklbbE8RUDRlIVhU8MaNLh1A9SUZDUHPiZkWLFde6UNwG41yQHZEToHgJMXqyVZ9UcS/ReOVTg==", - "dev": true, - "requires": { - "growly": "1.3.0", - "semver": "5.5.0", - "shellwords": "0.1.1", - "which": "1.3.0" - } - }, - "nodesecurity-npm-utils": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/nodesecurity-npm-utils/-/nodesecurity-npm-utils-6.0.0.tgz", - "integrity": "sha512-NLRle1woNaT2orR6fue2jNqkhxDTktgJj3sZxvR/8kp21pvOY7Gwlx5wvo0H8ZVPqdgd2nE2ADB9wDu5Cl8zNg==", - "dev": true - }, - "normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", - "dev": true, - "requires": { - "hosted-git-info": "2.6.0", - "is-builtin-module": "1.0.0", - "semver": "5.5.0", - "validate-npm-package-license": "3.0.3" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "1.1.0" - } - }, - "normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", - "dev": true - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "2.0.1" - } - }, - "nsp": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/nsp/-/nsp-3.2.1.tgz", - "integrity": "sha512-dLmGi7IGixJEHKetErIH460MYiYIzAoxuVsloZFu9e1p9U8K0yULx7YQ1+VzrjZbB+wqq67ES1SfOvKVb/qMDQ==", - "dev": true, - "requires": { - "chalk": "2.4.1", - "cli-table2": "0.2.0", - "cvss": "1.0.3", - "https-proxy-agent": "2.2.1", - "inquirer": "3.3.0", - "nodesecurity-npm-utils": "6.0.0", - "semver": "5.5.0", - "wreck": "12.5.1", - "yargs": "9.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "3.2.1", - "escape-string-regexp": "1.0.5", - "supports-color": "5.4.0" - } - }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "dev": true, - "requires": { - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wrap-ansi": "2.1.0" - }, - "dependencies": { - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - } - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "load-json-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "parse-json": "2.2.0", - "pify": "2.3.0", - "strip-bom": "3.0.0" - } - }, - "os-locale": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", - "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", - "dev": true, - "requires": { - "execa": "0.7.0", - "lcid": "1.0.0", - "mem": "1.1.0" - } - }, - "path-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", - "dev": true, - "requires": { - "pify": "2.3.0" - } - }, - "read-pkg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", - "dev": true, - "requires": { - "load-json-file": "2.0.0", - "normalize-package-data": "2.4.0", - "path-type": "2.0.0" - } - }, - "read-pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", - "dev": true, - "requires": { - "find-up": "2.1.0", - "read-pkg": "2.0.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "3.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "yargs": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-9.0.1.tgz", - "integrity": "sha1-UqzCP+7Kw0BCB47njAwAf1CF20w=", - "dev": true, - "requires": { - "camelcase": "4.1.0", - "cliui": "3.2.0", - "decamelize": "1.2.0", - "get-caller-file": "1.0.2", - "os-locale": "2.1.0", - "read-pkg-up": "2.0.0", - "require-directory": "2.1.1", - "require-main-filename": "1.0.1", - "set-blocking": "2.0.0", - "string-width": "2.1.1", - "which-module": "2.0.0", - "y18n": "3.2.1", - "yargs-parser": "7.0.0" - } - }, - "yargs-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", - "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", - "dev": true, - "requires": { - "camelcase": "4.1.0" - } - } - } - }, - "num2fraction": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", - "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", - "dev": true - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "nwmatcher": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.4.4.tgz", - "integrity": "sha512-3iuY4N5dhgMpCUrOVnuAdGrgxVqV2cJpM+XNccjR2DKOB1RUP0aA+wGXEiNziG/UKboFyGBIoKOaNlJxx8bciQ==", - "dev": true - }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "0.1.1", - "define-property": "0.2.5", - "kind-of": "3.2.2" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "0.1.6" - } - } - } - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "object.omit": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", - "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", - "dev": true, - "requires": { - "for-own": "0.1.5", - "is-extendable": "0.1.1" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1.0.2" - } - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "dev": true, - "requires": { - "mimic-fn": "1.2.0" - } - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "0.0.10", - "wordwrap": "0.0.3" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", - "dev": true - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - } - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, - "requires": { - "deep-is": "0.1.3", - "fast-levenshtein": "2.0.6", - "levn": "0.3.0", - "prelude-ls": "1.1.2", - "type-check": "0.3.2", - "wordwrap": "1.0.0" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", - "dev": true, - "requires": { - "lcid": "1.0.0" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "output-file-sync": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/output-file-sync/-/output-file-sync-1.1.2.tgz", - "integrity": "sha1-0KM+7+YaIF+suQCS6CZZjVJFznY=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "mkdirp": "0.5.1", - "object-assign": "4.1.1" - } - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-limit": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", - "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", - "dev": true, - "requires": { - "p-try": "1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "1.2.0" - } - }, - "p-map": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", - "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", - "dev": true - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "parse-glob": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", - "dev": true, - "requires": { - "glob-base": "0.3.0", - "is-dotfile": "1.0.3", - "is-extglob": "1.0.0", - "is-glob": "2.0.1" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "1.3.1" - } - }, - "parse5": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-1.5.1.tgz", - "integrity": "sha1-m387DeMr543CQBsXVzzK8Pb1nZQ=", - "dev": true - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "path-parse": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", - "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", - "dev": true - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "pify": "2.3.0", - "pinkie-promise": "2.0.1" - } - }, - "perfectionist": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/perfectionist/-/perfectionist-2.4.0.tgz", - "integrity": "sha1-wUetNxThJkZ/F2QSnuct+GHUfqA=", - "requires": { - "comment-regex": "1.0.1", - "defined": "1.0.0", - "minimist": "1.2.0", - "postcss": "5.2.18", - "postcss-scss": "0.3.1", - "postcss-value-parser": "3.3.0", - "read-file-stdin": "0.2.1", - "string.prototype.repeat": "0.2.0", - "vendors": "1.0.2", - "write-file-stdout": "0.0.2" - }, - "dependencies": { - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.5", - "source-map": "0.5.7", - "supports-color": "3.2.3" - } - } - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "2.0.4" - } - }, - "pluralize": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", - "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", - "dev": true - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "postcss": { - "version": "6.0.22", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.22.tgz", - "integrity": "sha512-Toc9lLoUASwGqxBSJGTVcOQiDqjK+Z2XlWBg+IgYwQMY9vA2f7iMpXVc1GpPcfTSyM5lkxNo0oDwDRO+wm7XHA==", - "requires": { - "chalk": "2.4.1", - "source-map": "0.6.1", - "supports-color": "5.4.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "1.9.1" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "requires": { - "ansi-styles": "3.2.1", - "escape-string-regexp": "1.0.5", - "supports-color": "5.4.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "requires": { - "has-flag": "3.0.0" - } - } - } - }, - "postcss-functions": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-functions/-/postcss-functions-3.0.0.tgz", - "integrity": "sha1-DpTQFERwCkgd4g3k1V+yZAVkJQ4=", - "requires": { - "glob": "7.1.2", - "object-assign": "4.1.1", - "postcss": "6.0.22", - "postcss-value-parser": "3.3.0" - } - }, - "postcss-js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-1.0.1.tgz", - "integrity": "sha512-smhUUMF5o5W1ZCQSyh5A3lNOXFLdNrxqyhWbLsGolZH2AgVmlyhxhYbIixfsdKE6r1vG5i7O40DPcvEvE1mvjw==", - "requires": { - "camelcase-css": "1.0.1", - "postcss": "6.0.22" - } - }, - "postcss-nested": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-3.0.0.tgz", - "integrity": "sha512-1xxmLHSfubuUi6xZZ0zLsNoiKfk3BWQj6fkNMaBJC529wKKLcdeCxXt6KJmDLva+trNyQNwEaE/ZWMA7cve1fA==", - "requires": { - "postcss": "6.0.22", - "postcss-selector-parser": "3.1.1" - } - }, - "postcss-scss": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-0.3.1.tgz", - "integrity": "sha1-ZcYQ2OKn7g5isYNbcbiHBzSBbks=", - "requires": { - "postcss": "5.2.18" - }, - "dependencies": { - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.5", - "source-map": "0.5.7", - "supports-color": "3.2.3" - } - } - } - }, - "postcss-selector-parser": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", - "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", - "requires": { - "dot-prop": "4.2.0", - "indexes-of": "1.0.1", - "uniq": "1.0.1" - } - }, - "postcss-value-parser": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz", - "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=" - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "preserve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", - "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", - "dev": true - }, - "prettier": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.12.1.tgz", - "integrity": "sha1-wa0g6APndJ+vkFpAnSNn4Gu+cyU=", - "dev": true - }, - "pretty-format": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-20.0.3.tgz", - "integrity": "sha1-Ag41ClYKH+GpjcO+tsz/s4beixQ=", - "dev": true, - "requires": { - "ansi-regex": "2.1.1", - "ansi-styles": "3.2.1" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - } - } - }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true - }, - "progress": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", - "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", - "dev": true - }, - "prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", - "dev": true - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - }, - "randomatic": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.0.0.tgz", - "integrity": "sha512-VdxFOIEY3mNO5PtSRkkle/hPJDHvQhK21oa73K4yAc9qmp6N429gAyF1gZMOTMeS0/AYzaV/2Trcef+NaIonSA==", - "dev": true, - "requires": { - "is-number": "4.0.0", - "kind-of": "6.0.2", - "math-random": "1.0.1" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "read-file-stdin": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/read-file-stdin/-/read-file-stdin-0.2.1.tgz", - "integrity": "sha1-JezP86FTtoCa+ssj7hU4fbng7mE=", - "requires": { - "gather-stream": "1.0.0" - } - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "1.1.0", - "normalize-package-data": "2.4.0", - "path-type": "1.1.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "1.1.2", - "read-pkg": "1.1.0" - }, - "dependencies": { - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "2.1.0", - "pinkie-promise": "2.0.1" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "2.0.1" - } - } - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.2", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" - } - }, - "readdirp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", - "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", - "dev": true, - "optional": true, - "requires": { - "graceful-fs": "4.1.11", - "minimatch": "3.0.4", - "readable-stream": "2.3.6", - "set-immediate-shim": "1.0.1" - } - }, - "regenerate": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", - "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", - "dev": true - }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true - }, - "regenerator-transform": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", - "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "private": "0.1.8" - } - }, - "regex-cache": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", - "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", - "dev": true, - "requires": { - "is-equal-shallow": "0.1.3" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "3.0.2", - "safe-regex": "1.1.0" - } - }, - "regexpp": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz", - "integrity": "sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw==", - "dev": true - }, - "regexpu-core": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", - "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", - "dev": true, - "requires": { - "regenerate": "1.4.0", - "regjsgen": "0.2.0", - "regjsparser": "0.1.5" - } - }, - "regjsgen": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", - "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", - "dev": true - }, - "regjsparser": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", - "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", - "dev": true, - "requires": { - "jsesc": "0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", - "dev": true - } - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "repeat-element": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", - "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "dev": true, - "requires": { - "is-finite": "1.0.2" - } - }, - "request": { - "version": "2.87.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", - "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", - "dev": true, - "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.7.0", - "caseless": "0.12.0", - "combined-stream": "1.0.6", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.3.2", - "har-validator": "5.0.3", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.18", - "oauth-sign": "0.8.2", - "performance-now": "2.1.0", - "qs": "6.5.2", - "safe-buffer": "5.1.2", - "tough-cookie": "2.3.4", - "tunnel-agent": "0.6.0", - "uuid": "3.2.1" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "require-uncached": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", - "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", - "dev": true, - "requires": { - "caller-path": "0.1.0", - "resolve-from": "1.0.1" - } - }, - "resolve": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz", - "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", - "dev": true, - "requires": { - "path-parse": "1.0.5" - } - }, - "resolve-from": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", - "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", - "dev": true - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "dev": true, - "requires": { - "onetime": "2.0.1", - "signal-exit": "3.0.2" - } - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", - "dev": true, - "optional": true, - "requires": { - "align-text": "0.1.4" - } - }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "dev": true, - "requires": { - "glob": "7.1.2" - } - }, - "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "dev": true, - "requires": { - "is-promise": "2.1.0" - } - }, - "rx-lite": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", - "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=", - "dev": true - }, - "rx-lite-aggregates": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", - "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", - "dev": true, - "requires": { - "rx-lite": "4.0.8" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "0.1.15" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sane": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/sane/-/sane-1.6.0.tgz", - "integrity": "sha1-lhDEUjB6E10pwf3+JUcDQYDEZ3U=", - "dev": true, - "requires": { - "anymatch": "1.3.2", - "exec-sh": "0.2.1", - "fb-watchman": "1.9.2", - "minimatch": "3.0.4", - "minimist": "1.2.0", - "walker": "1.0.7", - "watch": "0.10.0" - }, - "dependencies": { - "bser": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bser/-/bser-1.0.2.tgz", - "integrity": "sha1-OBEWlwsqbe6lZG3RXdcnhES1YWk=", - "dev": true, - "requires": { - "node-int64": "0.4.0" - } - }, - "fb-watchman": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-1.9.2.tgz", - "integrity": "sha1-okz0eCf4LTj7Waaa1wt247auc4M=", - "dev": true, - "requires": { - "bser": "1.0.2" - } - } - } - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true - }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "set-immediate-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", - "dev": true, - "optional": true - }, - "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", - "dev": true, - "requires": { - "extend-shallow": "2.0.1", - "is-extendable": "0.1.1", - "is-plain-object": "2.0.4", - "split-string": "3.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "shellwords": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true - }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", - "dev": true - }, - "slice-ansi": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", - "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "2.0.0" - } - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "0.11.2", - "debug": "2.6.9", - "define-property": "0.2.5", - "extend-shallow": "2.0.1", - "map-cache": "0.2.2", - "source-map": "0.5.7", - "source-map-resolve": "0.5.2", - "use": "3.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "0.1.6" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "1.0.0", - "isobject": "3.0.1", - "snapdragon-util": "3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "1.0.2" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "6.0.2" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "6.0.2" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "3.2.2" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "dev": true, - "requires": { - "atob": "2.1.1", - "decode-uri-component": "0.2.0", - "resolve-url": "0.2.1", - "source-map-url": "0.4.0", - "urix": "0.1.0" - } - }, - "source-map-support": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", - "dev": true, - "requires": { - "source-map": "0.5.7" - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, - "spdx-correct": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", - "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", - "dev": true, - "requires": { - "spdx-expression-parse": "3.0.0", - "spdx-license-ids": "3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", - "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "dev": true, - "requires": { - "spdx-exceptions": "2.1.0", - "spdx-license-ids": "3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", - "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", - "dev": true - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "3.0.2" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "sshpk": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", - "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", - "dev": true, - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - } - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "0.2.5", - "object-copy": "0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "0.1.6" - } - } - } - }, - "string-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz", - "integrity": "sha1-VpcPscOFWOnnC3KL894mmsRa36w=", - "dev": true, - "requires": { - "strip-ansi": "3.0.1" - } - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "2.0.0", - "strip-ansi": "4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "3.0.0" - } - } - } - }, - "string.prototype.repeat": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-0.2.0.tgz", - "integrity": "sha1-q6Nt4I3O5qWjN9SbLqHaGyj8Ds8=" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "5.1.2" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "0.2.1" - } - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "requires": { - "has-flag": "1.0.0" - } - }, - "symbol-tree": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", - "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", - "dev": true - }, - "table": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", - "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==", - "dev": true, - "requires": { - "ajv": "5.5.2", - "ajv-keywords": "2.1.1", - "chalk": "2.4.1", - "lodash": "4.17.10", - "slice-ansi": "1.0.0", - "string-width": "2.1.1" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "3.2.1", - "escape-string-regexp": "1.0.5", - "supports-color": "5.4.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "3.0.0" - } - } - } - }, - "test-exclude": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.1.tgz", - "integrity": "sha512-qpqlP/8Zl+sosLxBcVKl9vYy26T9NPalxSzzCP/OY6K7j938ui2oKgo+kRZYfxAeIpLqpbVnsHq1tyV70E4lWQ==", - "dev": true, - "requires": { - "arrify": "1.0.1", - "micromatch": "3.1.10", - "object-assign": "4.1.1", - "read-pkg-up": "1.0.1", - "require-main-filename": "1.0.1" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "1.1.0", - "array-unique": "0.3.2", - "extend-shallow": "2.0.1", - "fill-range": "4.0.0", - "isobject": "3.0.1", - "repeat-element": "1.1.2", - "snapdragon": "0.8.2", - "snapdragon-node": "2.1.1", - "split-string": "3.1.0", - "to-regex": "3.0.2" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "2.6.9", - "define-property": "0.2.5", - "extend-shallow": "2.0.1", - "posix-character-classes": "0.1.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "0.1.6" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "0.1.1" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "0.3.2", - "define-property": "1.0.0", - "expand-brackets": "2.1.4", - "extend-shallow": "2.0.1", - "fragment-cache": "0.2.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "1.0.2" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "2.0.1", - "is-number": "3.0.0", - "repeat-string": "1.6.1", - "to-regex-range": "2.1.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "6.0.2" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "6.0.2" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "4.0.0", - "array-unique": "0.3.2", - "braces": "2.3.2", - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "extglob": "2.0.4", - "fragment-cache": "0.2.1", - "kind-of": "6.0.2", - "nanomatch": "1.2.9", - "object.pick": "1.3.0", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - } - } - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "throat": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-3.2.0.tgz", - "integrity": "sha512-/EY8VpvlqJ+sFtLPeOgc8Pl7kQVOWv0woD87KTXVHPIAE842FGT+rokxIhe8xIUP1cfgrkt0as0vDLjDiMtr8w==", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "1.0.2" - } - }, - "tmpl": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", - "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", - "dev": true - }, - "to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", - "dev": true - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "regex-not": "1.0.2", - "safe-regex": "1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "3.0.0", - "repeat-string": "1.6.1" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - } - } - } - }, - "tough-cookie": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", - "dev": true, - "requires": { - "punycode": "1.4.1" - } - }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", - "dev": true - }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", - "dev": true - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "5.1.2" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true, - "optional": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "1.1.2" - } - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true - }, - "uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", - "dev": true, - "optional": true, - "requires": { - "source-map": "0.5.7", - "uglify-to-browserify": "1.0.2", - "yargs": "3.10.0" - }, - "dependencies": { - "yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "dev": true, - "optional": true, - "requires": { - "camelcase": "1.2.1", - "cliui": "2.1.0", - "decamelize": "1.2.0", - "window-size": "0.1.0" - } - } - } - }, - "uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", - "dev": true, - "optional": true - }, - "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", - "dev": true, - "requires": { - "arr-union": "3.1.0", - "get-value": "2.0.6", - "is-extendable": "0.1.1", - "set-value": "0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "0.1.1" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "dev": true, - "requires": { - "extend-shallow": "2.0.1", - "is-extendable": "0.1.1", - "is-plain-object": "2.0.4", - "to-object-path": "0.3.0" - } - } - } - }, - "uniq": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", - "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=" - }, - "universalify": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", - "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=" - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "0.3.1", - "isobject": "3.0.1" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "2.0.6", - "has-values": "0.1.4", - "isobject": "2.1.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "use": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", - "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", - "dev": true, - "requires": { - "kind-of": "6.0.2" - }, - "dependencies": { - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "user-home": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", - "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=", - "dev": true - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", - "dev": true - }, - "v8flags": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz", - "integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=", - "dev": true, - "requires": { - "user-home": "1.1.1" - } - }, - "validate-npm-package-license": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", - "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", - "dev": true, - "requires": { - "spdx-correct": "3.0.0", - "spdx-expression-parse": "3.0.0" - } - }, - "vendors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.2.tgz", - "integrity": "sha512-w/hry/368nO21AN9QljsaIhb9ZiZtZARoVH5f3CsFbawdLdayCgKRPup7CggujvySMxx0I91NOyxdVENohprLQ==" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "1.3.0" - } - }, - "walker": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", - "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", - "dev": true, - "requires": { - "makeerror": "1.0.11" - } - }, - "watch": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/watch/-/watch-0.10.0.tgz", - "integrity": "sha1-d3mLLaD5kQ1ZXxrOWwwiWFIfIdw=", - "dev": true - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - }, - "whatwg-encoding": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.3.tgz", - "integrity": "sha512-jLBwwKUhi8WtBfsMQlL4bUUcT8sMkAtQinscJAe/M4KHCkHuUJAF6vuB0tueNIw4c8ziO6AkRmgY+jL3a0iiPw==", - "dev": true, - "requires": { - "iconv-lite": "0.4.19" - }, - "dependencies": { - "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", - "dev": true - } - } - }, - "whatwg-url": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-4.8.0.tgz", - "integrity": "sha1-0pgaqRSMHgCkHFphMRZqtGg7vMA=", - "dev": true, - "requires": { - "tr46": "0.0.3", - "webidl-conversions": "3.0.1" - }, - "dependencies": { - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", - "dev": true - } - } - }, - "which": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", - "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", - "dev": true, - "requires": { - "isexe": "2.0.0" - } - }, - "which-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", - "dev": true - }, - "window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", - "dev": true, - "optional": true - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - }, - "worker-farm": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.6.0.tgz", - "integrity": "sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ==", - "dev": true, - "requires": { - "errno": "0.1.7" - } - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, - "requires": { - "string-width": "1.0.2", - "strip-ansi": "3.0.1" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "wreck": { - "version": "12.5.1", - "resolved": "https://registry.npmjs.org/wreck/-/wreck-12.5.1.tgz", - "integrity": "sha512-l5DUGrc+yDyIflpty1x9XuMj1ehVjC/dTbF3/BasOO77xk0EdEa4M/DuOY8W88MQDAD0fEDqyjc8bkIMHd2E9A==", - "dev": true, - "requires": { - "boom": "5.2.0", - "hoek": "4.2.1" - } - }, - "write": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", - "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", - "dev": true, - "requires": { - "mkdirp": "0.5.1" - } - }, - "write-file-stdout": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/write-file-stdout/-/write-file-stdout-0.0.2.tgz", - "integrity": "sha1-wlLXx8WxtAKJdjDjRTx7/mkNnKE=" - }, - "xml-name-validator": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz", - "integrity": "sha1-TYuPHszTQZqjYgYb7O9RXh5VljU=", - "dev": true - }, - "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", - "dev": true - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - }, - "yargs": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", - "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", - "dev": true, - "requires": { - "camelcase": "3.0.0", - "cliui": "3.2.0", - "decamelize": "1.2.0", - "get-caller-file": "1.0.2", - "os-locale": "1.4.0", - "read-pkg-up": "1.0.1", - "require-directory": "2.1.1", - "require-main-filename": "1.0.1", - "set-blocking": "2.0.0", - "string-width": "1.0.2", - "which-module": "1.0.0", - "y18n": "3.2.1", - "yargs-parser": "5.0.0" - }, - "dependencies": { - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true - }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "dev": true, - "requires": { - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wrap-ansi": "2.1.0" - } - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - } - } - }, - "yargs-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", - "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", - "dev": true, - "requires": { - "camelcase": "3.0.0" - }, - "dependencies": { - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true - } - } - } - } -} diff --git a/package.json b/package.json index 307ad9f658d7..c06e7c606009 100644 --- a/package.json +++ b/package.json @@ -1,88 +1,70 @@ { - "name": "tailwindcss", - "version": "0.6.6", - "description": "A utility-first CSS framework for rapidly building custom user interfaces.", - "license": "MIT", - "main": "lib/index.js", - "style": "dist/tailwind.css", - "repository": "https://github.com/tailwindcss/tailwindcss.git", - "bugs": "https://github.com/tailwindcss/tailwindcss/issues", - "homepage": "https://tailwindcss.com", - "bin": { - "tailwind": "lib/cli.js" + "name": "@tailwindcss/root", + "private": true, + "version": "1.0.0", + "prettier": { + "semi": false, + "singleQuote": true, + "printWidth": 100, + "plugins": [ + "prettier-plugin-organize-imports" + ], + "overrides": [ + { + "files": [ + "tsconfig.json" + ], + "options": { + "parser": "jsonc" + } + }, + { + "files": [ + "integrations/**/*.ts" + ], + "options": { + "plugins": [ + "prettier-plugin-embed", + "prettier-plugin-organize-imports" + ] + } + } + ] }, - "contributors": [ - "Adam Wathan ", - "Jonathan Reinink ", - "David Hemphill " - ], "scripts": { - "prebabelify": "rimraf lib", - "babelify": "babel src --out-dir lib", - "prepare": "npm run babelify && babel-node src/build.js", - "style": "eslint .", - "test": "jest && eslint . && nsp check" + "format": "prettier --write .", + "lint": "prettier --check . && turbo lint", + "build": "turbo build --filter=!./playgrounds/*", + "postbuild": "node ./scripts/pack-packages.mjs", + "dev": "turbo dev --filter=!./playgrounds/*", + "test": "cargo test && vitest run --hideSkippedTests", + "test:integrations": "vitest --root=./integrations", + "test:ui": "pnpm run --filter=tailwindcss test:ui && pnpm run --filter=@tailwindcss/browser test:ui", + "tdd": "vitest --hideSkippedTests", + "bench": "vitest bench", + "version-packages": "node ./scripts/version-packages.mjs", + "vite": "pnpm run --filter=vite-playground dev", + "nextjs": "pnpm run --filter=nextjs-playground dev" }, + "license": "MIT", "devDependencies": { - "babel-cli": "^6.6.5", - "babel-core": "^6.7.2", - "babel-jest": "^20.0.3", - "babel-preset-env": "^1.0.0", - "babel-preset-react": "^6.24.1", - "babel-preset-stage-2": "^6.24.1", - "babel-preset-stage-3": "^6.24.1", - "clean-css": "^4.1.9", - "eslint": "^4.10.0", - "eslint-config-postcss": "^2.0.2", - "eslint-config-prettier": "^2.7.0", - "eslint-plugin-prettier": "^2.3.1", - "jest": "^20.0.4", - "nsp": "^3.2.1", - "prettier": "^1.7.4", - "rimraf": "^2.6.1" - }, - "dependencies": { - "autoprefixer": "^7.1.6", - "bytes": "^3.0.0", - "chalk": "^2.4.1", - "css.escape": "^1.5.1", - "fs-extra": "^4.0.2", - "lodash": "^4.17.5", - "node-emoji": "^1.8.1", - "perfectionist": "^2.4.0", - "postcss": "^6.0.9", - "postcss-functions": "^3.0.0", - "postcss-js": "^1.0.1", - "postcss-nested": "^3.0.0", - "postcss-selector-parser": "^3.1.1", - "pretty-hrtime": "^1.0.3", - "strip-comments": "^1.0.2" - }, - "browserslist": [ - "> 1%" - ], - "babel": { - "presets": [ - [ - "env", - { - "targets": { - "node": "6.9.0" - } - } - ], - "stage-2", - "stage-3", - "react" - ] - }, - "jest": { - "setupTestFrameworkScriptFile": "/jest/customMatchers.js", - "testPathIgnorePatterns": [ - "/__tests__/fixtures/" - ] + "@playwright/test": "^1.58.0", + "@types/node": "catalog:", + "postcss": "8.5.6", + "postcss-import": "^16.1.1", + "prettier": "catalog:", + "prettier-plugin-embed": "^0.5.1", + "prettier-plugin-organize-imports": "^4.3.0", + "tsup": "^8.5.1", + "turbo": "^2.7.6", + "typescript": "^5.5.4", + "vitest": "^4.0.18" }, - "engines": { - "node": ">=6.9.0" + "packageManager": "pnpm@9.6.0", + "pnpm": { + "patchedDependencies": { + "@parcel/watcher@2.5.1": "patches/@parcel__watcher@2.5.1.patch", + "lightningcss@1.32.0": "patches/lightningcss@1.32.0.patch" + } } } diff --git a/packages/@tailwindcss-browser/README.md b/packages/@tailwindcss-browser/README.md new file mode 100644 index 000000000000..5f532607d00a --- /dev/null +++ b/packages/@tailwindcss-browser/README.md @@ -0,0 +1,36 @@ +

+ + + + + Tailwind CSS + + +

+ +

+ A utility-first CSS framework for rapidly building custom user interfaces. +

+ +

+ Build Status + Total Downloads + Latest Release + License +

+ +--- + +## Documentation + +For full documentation, visit [tailwindcss.com](https://tailwindcss.com). + +## Community + +For help, discussion about best practices, or feature ideas: + +[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/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**. diff --git a/packages/@tailwindcss-browser/package.json b/packages/@tailwindcss-browser/package.json new file mode 100644 index 000000000000..a5a7287e7e79 --- /dev/null +++ b/packages/@tailwindcss-browser/package.json @@ -0,0 +1,37 @@ +{ + "name": "@tailwindcss/browser", + "version": "4.2.2", + "description": "A utility-first CSS framework for rapidly building custom user interfaces.", + "license": "MIT", + "main": "./dist/index.global.js", + "browser": "./dist/index.global.js", + "repository": { + "type": "git", + "url": "https://github.com/tailwindlabs/tailwindcss.git", + "directory": "packages/@tailwindcss-browser" + }, + "bugs": "https://github.com/tailwindlabs/tailwindcss/issues", + "homepage": "https://tailwindcss.com", + "scripts": { + "lint": "tsc --noEmit", + "build": "tsup-node", + "dev": "pnpm run build -- --watch", + "test:ui": "playwright test" + }, + "exports": { + ".": "./dist/index.global.js", + "./package.json": "./package.json" + }, + "files": [ + "dist" + ], + "publishConfig": { + "provenance": true, + "access": "public" + }, + "devDependencies": { + "h3": "^1.15.5", + "listhen": "^1.9.0", + "tailwindcss": "workspace:*" + } +} diff --git a/packages/@tailwindcss-browser/playwright.config.ts b/packages/@tailwindcss-browser/playwright.config.ts new file mode 100644 index 000000000000..c3ad05b918d4 --- /dev/null +++ b/packages/@tailwindcss-browser/playwright.config.ts @@ -0,0 +1,66 @@ +import { defineConfig, devices } from '@playwright/test' + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + { + name: 'firefox', + use: { + ...devices['Desktop Firefox'], + // https://playwright.dev/docs/test-use-options#more-browser-and-context-options + launchOptions: { + // https://playwright.dev/docs/api/class-browsertype#browser-type-launch-option-firefox-user-prefs + firefoxUserPrefs: { + // By default, headless Firefox runs as though no pointers + // capabilities are available. + // https://github.com/microsoft/playwright/issues/7769#issuecomment-966098074 + // + // This impacts our `hover` variant implementation which uses an + // '(hover: hover)' media query to determine if hover is available. + // + // Available values for pointer capabilities: + // NO_POINTER = 0x00; + // COARSE_POINTER = 0x01; + // FINE_POINTER = 0x02; + // HOVER_CAPABLE_POINTER = 0x04; + // + // Setting to 0x02 | 0x04 says the system supports a mouse + 'ui.primaryPointerCapabilities': 0x02 | 0x04, + 'ui.allPointerCapabilities': 0x02 | 0x04, + }, + }, + }, + }, + ], +}) diff --git a/packages/@tailwindcss-browser/src/assets.ts b/packages/@tailwindcss-browser/src/assets.ts new file mode 100644 index 000000000000..e72d47658c4e --- /dev/null +++ b/packages/@tailwindcss-browser/src/assets.ts @@ -0,0 +1,11 @@ +import index from 'tailwindcss/index.css' +import preflight from 'tailwindcss/preflight.css' +import theme from 'tailwindcss/theme.css' +import utilities from 'tailwindcss/utilities.css' + +export const css = { + index, + preflight, + theme, + utilities, +} diff --git a/packages/@tailwindcss-browser/src/index.ts b/packages/@tailwindcss-browser/src/index.ts new file mode 100644 index 000000000000..49c7e4675fc8 --- /dev/null +++ b/packages/@tailwindcss-browser/src/index.ts @@ -0,0 +1,294 @@ +import * as tailwindcss from 'tailwindcss' +import * as assets from './assets' +import { Instrumentation } from './instrumentation' + +/** + * The type used by ` + `, + body: html`
`, + }) + + await expect(page.locator('[data-test]')).toHaveCSS('background-color', 'rgb(255, 0, 0)') + + await page.evaluate(() => { + document.querySelector('[data-css]')!.textContent = ` + .foo { + @apply bg-blue; + } + ` + }) + + await expect(page.locator('[data-test]')).toHaveCSS('background-color', 'rgb(0, 0, 255)') +}) + +test('changes to `@theme`', async ({ page }) => { + await server.render({ + page, + head: html` + + `, + body: html`
`, + }) + + await expect(page.locator('[data-test]')).toHaveCSS('background-color', 'rgb(255, 0, 0)') + + await page.evaluate(() => { + document.querySelector('[data-css]')!.textContent = ` + @theme { + --color-primary: #0000ff; + } + ` + }) + + await expect(page.locator('[data-test]')).toHaveCSS('background-color', 'rgb(0, 0, 255)') +}) + +test('no classes', async ({ page }) => { + await server.render({ + page, + body: html`
test
`, + }) + + await expect(page.locator('body')).toHaveCSS('margin', '0px') +}) + +test('html classes', async ({ page }) => { + await server.render({ + page, + htmlClasses: 'h-4', + }) + + await expect(page.locator('html')).toHaveCSS('height', '16px') +}) + +async function createServer() { + const { createApp, createRouter, defineEventHandler, toNodeListener } = await import('h3') + const { listen } = await import('listhen') + + interface PageOptions { + page: Page + head?: string + body?: string + htmlClasses?: string + } + + async function render({ page, htmlClasses, head, body }: PageOptions) { + let content = html` + + + + + + + Document + + + ${head ?? ''} + + + ${body ?? ''} + + + ` + + router.get( + '/', + defineEventHandler(() => content), + ) + + await page.goto(server.url) + } + + const app = createApp() + const router = createRouter() + + router.get( + '/tailwindcss.js', + defineEventHandler(() => readFile(require.resolve('@tailwindcss/browser'))), + ) + + app.use(router) + + let workerIndex = Number(process.env.TEST_WORKER_INDEX ?? 0) + + let listener = await listen(toNodeListener(app), { + port: 3000 + workerIndex, + showURL: false, + open: false, + }) + + return { + app, + url: listener.url, + render, + } +} diff --git a/packages/@tailwindcss-browser/tsconfig.json b/packages/@tailwindcss-browser/tsconfig.json new file mode 100644 index 000000000000..8ea040daf8d3 --- /dev/null +++ b/packages/@tailwindcss-browser/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "lib": ["es2022", "esnext.disposable", "dom", "dom.iterable"], + }, +} diff --git a/packages/@tailwindcss-browser/tsup.config.ts b/packages/@tailwindcss-browser/tsup.config.ts new file mode 100644 index 000000000000..10a73f2b7e09 --- /dev/null +++ b/packages/@tailwindcss-browser/tsup.config.ts @@ -0,0 +1,32 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + format: ['iife'], + clean: true, + minify: true, + entry: ['src/index.ts'], + noExternal: [/.*/], + loader: { + '.css': 'text', + }, + define: { + 'process.env.NODE_ENV': '"production"', + 'process.env.FEATURES_ENV': '"stable"', + }, + esbuildPlugins: [ + { + name: 'patch-intellisense-apis', + setup(build) { + build.onLoad({ filter: /intellisense.ts$/ }, () => { + return { + contents: ` + export function getClassList() { return [] } + export function getVariants() { return [] } + export function canonicalizeCandidates() { return [] } + `, + } + }) + }, + }, + ], +}) diff --git a/packages/@tailwindcss-browser/vitest.config.ts b/packages/@tailwindcss-browser/vitest.config.ts new file mode 100644 index 000000000000..f6f91227da41 --- /dev/null +++ b/packages/@tailwindcss-browser/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + exclude: ['**/*.spec.?(c|m)[jt]s?(x)', '**/node_modules/**'], + }, +}) diff --git a/packages/@tailwindcss-cli/README.md b/packages/@tailwindcss-cli/README.md new file mode 100644 index 000000000000..5f532607d00a --- /dev/null +++ b/packages/@tailwindcss-cli/README.md @@ -0,0 +1,36 @@ +

+ + + + + Tailwind CSS + + +

+ +

+ A utility-first CSS framework for rapidly building custom user interfaces. +

+ +

+ Build Status + Total Downloads + Latest Release + License +

+ +--- + +## Documentation + +For full documentation, visit [tailwindcss.com](https://tailwindcss.com). + +## Community + +For help, discussion about best practices, or feature ideas: + +[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/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**. diff --git a/packages/@tailwindcss-cli/package.json b/packages/@tailwindcss-cli/package.json new file mode 100644 index 000000000000..01e7b28d789e --- /dev/null +++ b/packages/@tailwindcss-cli/package.json @@ -0,0 +1,40 @@ +{ + "name": "@tailwindcss/cli", + "version": "4.2.2", + "description": "A utility-first CSS framework for rapidly building custom user interfaces.", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/tailwindlabs/tailwindcss.git", + "directory": "packages/@tailwindcss-cli" + }, + "bugs": "https://github.com/tailwindlabs/tailwindcss/issues", + "homepage": "https://tailwindcss.com", + "scripts": { + "lint": "tsc --noEmit", + "build": "tsup-node", + "dev": "pnpm run build -- --watch" + }, + "bin": { + "tailwindcss": "./dist/index.mjs" + }, + "exports": { + "./package.json": "./package.json" + }, + "files": [ + "dist" + ], + "publishConfig": { + "provenance": true, + "access": "public" + }, + "dependencies": { + "@parcel/watcher": "^2.5.1", + "@tailwindcss/node": "workspace:*", + "@tailwindcss/oxide": "workspace:*", + "enhanced-resolve": "^5.19.0", + "mri": "^1.2.0", + "picocolors": "^1.1.1", + "tailwindcss": "workspace:*" + } +} diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts new file mode 100644 index 000000000000..eabd2a1de022 --- /dev/null +++ b/packages/@tailwindcss-cli/src/commands/build/index.ts @@ -0,0 +1,514 @@ +import watcher from '@parcel/watcher' +import { + compile, + env, + Instrumentation, + optimize, + toSourceMap, + type SourceMap, +} from '@tailwindcss/node' +import { clearRequireCache } from '@tailwindcss/node/require-cache' +import { Scanner, type ChangedContent } from '@tailwindcss/oxide' +import { existsSync, type Stats } from 'node:fs' +import fs from 'node:fs/promises' +import path from 'node:path' +import type { Arg, Result } from '../../utils/args' +import { Disposables } from '../../utils/disposables' +import { + eprintln, + formatDuration, + header, + highlight, + println, + relative, +} from '../../utils/renderer' +import { drainStdin, outputFile } from './utils' + +const css = String.raw +const DEBUG = env.DEBUG + +export function options() { + return { + '--input': { + type: 'string', + description: 'Input file', + alias: '-i', + }, + '--output': { + type: 'string', + description: 'Output file', + alias: '-o', + default: '-', + }, + '--watch': { + type: 'boolean | string', + description: + 'Watch for changes and rebuild as needed, and use `always` to keep watching when stdin is closed', + alias: '-w', + values: ['always'], + }, + '--minify': { + type: 'boolean', + description: 'Optimize and minify the output', + alias: '-m', + }, + '--optimize': { + type: 'boolean', + description: 'Optimize the output without minifying', + }, + '--cwd': { + type: 'string', + description: 'The current working directory', + default: '.', + }, + '--map': { + type: 'boolean | string', + description: 'Generate a source map', + default: false, + }, + } satisfies Arg +} + +async function handleError(fn: () => T): Promise { + try { + return await fn() + } catch (err) { + if (err instanceof Error) { + eprintln(err.toString()) + } + process.exit(1) + } +} + +export async function handle(args: Result>) { + eprintln(header()) + eprintln() + + using I = new Instrumentation() + DEBUG && I.start('[@tailwindcss/cli] (initial build)') + + let base = path.resolve(args['--cwd']) + + // Resolve the output as an absolute path. If the output is a `-`, then we + // don't need to resolve it because this is a flag to indicate that we want to + // use `stdout` instead. + if (args['--output'] && args['--output'] !== '-') { + args['--output'] = path.resolve(base, args['--output']) + } + + // Resolve the input as an absolute path. If the input is a `-`, then we don't + // need to resolve it because this is a flag to indicate that we want to use + // `stdin` instead. + if (args['--input'] && args['--input'] !== '-') { + args['--input'] = path.resolve(base, args['--input']) + + // Ensure the provided `--input` exists. + if (!existsSync(args['--input'])) { + eprintln(`Specified input file ${highlight(relative(args['--input']))} does not exist.`) + process.exit(1) + } + } + + // Check if the input and output file paths are identical, otherwise return an + // error to the user. + if (args['--input'] === args['--output'] && args['--input'] !== '-') { + eprintln( + `Specified input file ${highlight(relative(args['--input']))} and output file ${highlight(relative(args['--output']))} are identical.`, + ) + process.exit(1) + } + + // If the user passes `{bin} build --map -` then this likely means they want to output the map inline + // this is the default behavior of `{bin build} --map` to inform the user of that + if (args['--map'] === '-') { + eprintln(`Use --map without a value to inline the source map`) + process.exit(1) + } + + // Resolve the map as an absolute path. If the output is true then we + // don't need to resolve it because it'll be an inline source map + if (args['--map'] && args['--map'] !== true) { + args['--map'] = path.resolve(base, args['--map']) + } + + let start = process.hrtime.bigint() + + let input = args['--input'] + ? args['--input'] === '-' + ? await drainStdin() + : await fs.readFile(args['--input'], 'utf-8') + : css` + @import 'tailwindcss'; + ` + + let previous = { + css: '', + optimizedCss: '', + } + + async function write( + css: string, + map: SourceMap | null, + args: Result>, + I: Instrumentation, + ) { + let output = css + + // Optimize the output + if (args['--minify'] || args['--optimize']) { + if (css !== previous.css) { + DEBUG && I.start('Optimize CSS') + let optimized = optimize(css, { + file: args['--input'] ?? 'input.css', + minify: args['--minify'] ?? false, + map: map?.raw ?? undefined, + }) + DEBUG && I.end('Optimize CSS') + previous.css = css + previous.optimizedCss = optimized.code + if (optimized.map) { + map = toSourceMap(optimized.map) + } + output = optimized.code + } else { + output = previous.optimizedCss + } + } + + // Write the output + if (map) { + // Inline the source map + if (args['--map'] === true) { + output += `\n` + output += map.inline + } else if (typeof args['--map'] === 'string') { + let basePath = + args['--output'] && args['--output'] !== '-' + ? path.dirname(path.resolve(args['--output'])) + : process.cwd() + + let mapPath = path.resolve(args['--map']) + + let relativePath = path.relative(basePath, mapPath) + + output += `\n` + output += map.comment(relativePath) + + DEBUG && I.start('Write source map') + await outputFile(args['--map'], map.raw) + DEBUG && I.end('Write source map') + } + } + + DEBUG && I.start('Write output') + if (args['--output'] && args['--output'] !== '-') { + await outputFile(args['--output'], output) + } else { + println(output) + } + DEBUG && I.end('Write output') + } + + let inputFilePath = + args['--input'] && args['--input'] !== '-' ? path.resolve(args['--input']) : null + + let inputBasePath = inputFilePath ? path.dirname(inputFilePath) : process.cwd() + + let fullRebuildPaths: string[] = inputFilePath ? [inputFilePath] : [] + + async function createCompiler(css: string, I: Instrumentation) { + DEBUG && I.start('Setup compiler') + let compiler = await compile(css, { + from: args['--output'] ? (inputFilePath ?? 'stdin.css') : undefined, + base: inputBasePath, + onDependency(path) { + fullRebuildPaths.push(path) + }, + }) + + let sources = (() => { + // Disable auto source detection + if (compiler.root === 'none') { + return [] + } + + // No root specified, use the base directory + if (compiler.root === null) { + return [{ base, pattern: '**/*', negated: false }] + } + + // Use the specified root + return [{ ...compiler.root, negated: false }] + })().concat(compiler.sources) + + let scanner = new Scanner({ sources }) + DEBUG && I.end('Setup compiler') + + return [compiler, scanner] as const + } + + let [compiler, scanner] = await handleError(() => createCompiler(input, I)) + + // Watch for changes + if (args['--watch']) { + let cleanupWatchers: (() => Promise)[] = [] + cleanupWatchers.push( + await createWatchers(watchDirectories(scanner), async function handle(files) { + try { + // If the only change happened to the output file, then we don't want to + // trigger a rebuild because that will result in an infinite loop. + if (files.length === 1 && files[0] === args['--output']) return + + using I = new Instrumentation() + DEBUG && I.start('[@tailwindcss/cli] (watcher)') + + // Re-compile the input + let start = process.hrtime.bigint() + + let changedFiles: ChangedContent[] = [] + let rebuildStrategy: 'incremental' | 'full' = 'incremental' + + let resolvedFullRebuildPaths = fullRebuildPaths + + for (let file of files) { + // If one of the changed files is related to the input CSS or JS + // config/plugin files, then we need to do a full rebuild because + // the theme might have changed. + if (resolvedFullRebuildPaths.includes(file)) { + rebuildStrategy = 'full' + + // No need to check the rest of the events, because we already know we + // need to do a full rebuild. + break + } + + // Track new and updated files for incremental rebuilds. + changedFiles.push({ + file, + extension: path.extname(file).slice(1), + } satisfies ChangedContent) + } + + // Track the compiled CSS + let compiledCss = '' + let compiledMap: SourceMap | null = null + + // Scan the entire `base` directory for full rebuilds. + if (rebuildStrategy === 'full') { + // Read the new `input`. + let input = args['--input'] + ? args['--input'] === '-' + ? await drainStdin() + : await fs.readFile(args['--input'], 'utf-8') + : css` + @import 'tailwindcss'; + ` + clearRequireCache(resolvedFullRebuildPaths) + fullRebuildPaths = inputFilePath ? [inputFilePath] : [] + + // Create a new compiler, given the new `input` + ;[compiler, scanner] = await createCompiler(input, I) + + // Scan the directory for candidates + DEBUG && I.start('Scan for candidates') + let candidates = scanner.scan() + DEBUG && I.end('Scan for candidates') + + // Setup new watchers + DEBUG && I.start('Setup new watchers') + let newCleanupFunction = await createWatchers(watchDirectories(scanner), handle) + DEBUG && I.end('Setup new watchers') + + // Clear old watchers + DEBUG && I.start('Cleanup old watchers') + await Promise.all(cleanupWatchers.splice(0).map((cleanup) => cleanup())) + DEBUG && I.end('Cleanup old watchers') + + cleanupWatchers.push(newCleanupFunction) + + // Re-compile the CSS + DEBUG && I.start('Build CSS') + compiledCss = compiler.build(candidates) + DEBUG && I.end('Build CSS') + + if (args['--map']) { + DEBUG && I.start('Build Source Map') + compiledMap = toSourceMap(compiler.buildSourceMap()) + DEBUG && I.end('Build Source Map') + } + } + + // Scan changed files only for incremental rebuilds. + else if (rebuildStrategy === 'incremental') { + DEBUG && I.start('Scan for candidates') + let newCandidates = scanner.scanFiles(changedFiles) + DEBUG && I.end('Scan for candidates') + + // No new candidates found which means we don't need to write to + // disk, and can return early. + if (newCandidates.length <= 0) { + let end = process.hrtime.bigint() + eprintln(`Done in ${formatDuration(end - start)}`) + return + } + + DEBUG && I.start('Build CSS') + compiledCss = compiler.build(newCandidates) + DEBUG && I.end('Build CSS') + + if (args['--map']) { + DEBUG && I.start('Build Source Map') + compiledMap = toSourceMap(compiler.buildSourceMap()) + DEBUG && I.end('Build Source Map') + } + } + + await write(compiledCss, compiledMap, args, I) + + let end = process.hrtime.bigint() + eprintln(`Done in ${formatDuration(end - start)}`) + } catch (err) { + // Catch any errors and print them to stderr, but don't exit the process + // and keep watching. + if (err instanceof Error) { + eprintln(err.toString()) + } + } + }), + ) + + // Abort the watcher if `stdin` is closed to avoid zombie processes. You can + // disable this behavior with `--watch=always`. + if (args['--watch'] !== 'always') { + process.stdin.on('end', () => { + Promise.all(cleanupWatchers.map((fn) => fn())).then( + () => process.exit(0), + () => process.exit(1), + ) + }) + } + + // Keep the process running + process.stdin.resume() + } + + DEBUG && I.start('Scan for candidates') + let candidates = scanner.scan() + DEBUG && I.end('Scan for candidates') + DEBUG && I.start('Build CSS') + let output = await handleError(() => compiler.build(candidates)) + DEBUG && I.end('Build CSS') + + let map: SourceMap | null = null + + if (args['--map']) { + DEBUG && I.start('Build Source Map') + map = await handleError(() => toSourceMap(compiler.buildSourceMap())) + DEBUG && I.end('Build Source Map') + } + + await write(output, map, args, I) + + let end = process.hrtime.bigint() + eprintln(`Done in ${formatDuration(end - start)}`) +} + +async function createWatchers(dirs: string[], cb: (files: string[]) => void) { + // Remove any directories that are children of an already watched directory. + // If we don't we may not get notified of certain filesystem events regardless + // of whether or not they are for the directory that is duplicated. + + // 1. Sort in asc by length + dirs = dirs.sort((a, z) => a.length - z.length) + + // 2. Remove any directories that are children of another directory + let toRemove = [] + + // /project-a 0 + // /project-a/src 1 + + for (let i = 0; i < dirs.length; ++i) { + for (let j = 0; j < i; ++j) { + if (!dirs[i].startsWith(`${dirs[j]}/`)) continue + + toRemove.push(dirs[i]) + } + } + + dirs = dirs.filter((dir) => !toRemove.includes(dir)) + + // Track all Parcel watchers for each glob. + // + // When we encounter a change in a CSS file, we need to setup new watchers and + // we want to cleanup the old ones we captured here. + let watchers = new Disposables() + + // Track all files that were added or changed. + let files = new Set() + + // Keep track of the debounce queue to avoid multiple rebuilds. + let debounceQueue = new Disposables() + + // A changed file can be watched by multiple watchers, but we only want to + // handle the file once. We debounce the handle function with the collected + // files to handle them in a single batch and to avoid multiple rebuilds. + async function enqueueCallback() { + // Dispose all existing macrotasks. + await debounceQueue.dispose() + + // Setup a new macrotask to handle the files in batch. + debounceQueue.queueMacrotask(() => { + cb(Array.from(files)) + files.clear() + }) + } + + // Setup a watcher for every directory. + for (let dir of dirs) { + let { unsubscribe } = await watcher.subscribe(dir, async (err, events) => { + // Whenever an error occurs we want to let the user know about it but we + // want to keep watching for changes. + if (err) { + console.error(err) + return + } + + await Promise.all( + events.map(async (event) => { + // We currently don't handle deleted files because it doesn't influence + // the CSS output. This is because we currently keep all scanned + // candidates in a cache for performance reasons. + if (event.type === 'delete') return + + // Ignore directory changes. We only care about file changes + let stats: Stats | null = null + try { + stats = await fs.lstat(event.path) + } catch {} + if (!stats?.isFile() && !stats?.isSymbolicLink()) { + return + } + + // Track the changed file. + files.add(event.path) + }), + ) + + // Handle the tracked files at some point in the future. + await enqueueCallback() + }) + + // Ensure we cleanup the watcher when we're done. + watchers.add(unsubscribe) + } + + // Cleanup + return async () => { + await watchers.dispose() + await debounceQueue.dispose() + } +} + +function watchDirectories(scanner: Scanner) { + return [...new Set(scanner.normalizedSources.flatMap((globEntry) => globEntry.base))] +} diff --git a/packages/@tailwindcss-cli/src/commands/build/utils.ts b/packages/@tailwindcss-cli/src/commands/build/utils.ts new file mode 100644 index 000000000000..ecdd96726f96 --- /dev/null +++ b/packages/@tailwindcss-cli/src/commands/build/utils.ts @@ -0,0 +1,35 @@ +import fs from 'node:fs/promises' +import path from 'node:path' + +export function drainStdin() { + return new Promise((resolve, reject) => { + let result = '' + process.stdin.on('data', (chunk) => { + result += chunk + }) + process.stdin.on('end', () => resolve(result)) + process.stdin.on('error', (err) => reject(err)) + }) +} + +export async function outputFile(file: string, contents: string) { + // Check for special files like `/dev/stdout` or pipes. We don't want to read from these as that + // will hang the process until the file descriptors are closed. + let isSpecialFile = await fs + .stat(file) + .then((stats) => stats.isCharacterDevice() || stats.isFIFO()) + .catch(() => false) + + if (!isSpecialFile) { + try { + let currentContents = await fs.readFile(file, 'utf8') + if (currentContents === contents) return // Skip writing the file + } catch {} + } + + // Ensure the parent directories exist + await fs.mkdir(path.dirname(file), { recursive: true }) + + // Write the file + await fs.writeFile(file, contents, 'utf8') +} diff --git a/packages/@tailwindcss-cli/src/commands/canonicalize/canonicalize.test.ts b/packages/@tailwindcss-cli/src/commands/canonicalize/canonicalize.test.ts new file mode 100644 index 000000000000..4392380a574d --- /dev/null +++ b/packages/@tailwindcss-cli/src/commands/canonicalize/canonicalize.test.ts @@ -0,0 +1,175 @@ +import path from 'node:path' +import { PassThrough, Readable } from 'node:stream' +import { fileURLToPath } from 'node:url' +import { describe, expect, test } from 'vitest' +import { runCommandLine, streamStdin } from '.' +import { normalizeWindowsSeparators } from '../../utils/test-helpers' + +let css = normalizeWindowsSeparators( + path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'fixtures/input.css'), +) + +describe('runCommandLine', { timeout: 30_000 }, () => { + test('canonicalizes, collapses, and sorts candidate groups from positional arguments', async () => { + let result = await runCommandLine({ + argv: ['--css', css, 'py-3 p-1 px-3'], + stdinIsTTY: true, + stdoutIsTTY: false, + }) + + expect(result).toEqual({ + exitCode: 0, + stdout: 'p-3', + stderr: '', + }) + }) + + test('falls back to the default tailwind import when --css is omitted', async () => { + let result = await runCommandLine({ + argv: ['py-3 p-1 px-3'], + cwd: path.dirname(css), + stdinIsTTY: true, + stdoutIsTTY: false, + }) + + expect(result).toEqual({ + exitCode: 0, + stdout: 'p-3', + stderr: '', + }) + }) + + test('canonicalizes, collapses, and sorts multiple groups from stdin lines', async () => { + let result = await runCommandLine({ + argv: ['--css', css], + stdin: '[display:_flex_] py-3 p-1 px-3\nmt-2 mr-2 mb-2 ml-2 focus:hover:p-3 hover:p-1 py-3\n', + stdinIsTTY: false, + stdoutIsTTY: false, + }) + + expect(result).toEqual({ + exitCode: 0, + stdout: 'flex p-3\nm-2 py-3 hover:p-1 focus:hover:p-3', + stderr: '', + }) + }) + + test('collapses equivalent candidates', async () => { + let result = await runCommandLine({ + argv: ['--css', css, 'mt-2 mr-2 mb-2 ml-2'], + stdinIsTTY: true, + stdoutIsTTY: false, + }) + + expect(result).toEqual({ + exitCode: 0, + stdout: 'm-2', + stderr: '', + }) + }) + + test('renders json output for processed candidate groups', async () => { + let result = await runCommandLine({ + argv: ['--css', css, '--format', 'json', 'py-3 p-1 px-3'], + stdinIsTTY: true, + stdoutIsTTY: false, + }) + + expect(result.exitCode).toBe(0) + expect(JSON.parse(result.stdout)).toEqual([ + { + input: 'py-3 p-1 px-3', + output: 'p-3', + changed: true, + }, + ]) + expect(result.stderr).toBe('') + }) + + test('splits candidate lists with segment-aware spacing', async () => { + let result = await runCommandLine({ + argv: ['--css', css, '--format', 'json', "content-['hello world'] p-1"], + stdinIsTTY: true, + stdoutIsTTY: false, + }) + + expect(result.exitCode).toBe(0) + expect(JSON.parse(result.stdout)).toEqual([ + { + input: "content-['hello world'] p-1", + output: "p-1 content-['hello_world']", + changed: true, + }, + ]) + expect(result.stderr).toBe('') + }) + + test('shows a usage error when no candidate groups are provided', async () => { + let result = await runCommandLine({ + argv: ['--css', css], + stdinIsTTY: true, + stdoutIsTTY: false, + }) + + expect(result.exitCode).toBe(1) + expect(result.stderr).toBe('No candidate groups provided') + expect(result.stdout).toContain('Usage:') + }) + + test('streams canonicalized output line by line', async () => { + let input = Readable.from('py-3 p-1 px-3\nmt-2 mr-2 mb-2 ml-2\n') + let { stream: output, collect: collectOutput } = createOutput() + + await streamStdin({ css, cwd: path.dirname(css), format: 'text', input, output }) + + expect(collectOutput()).toBe('p-3\nm-2\n') + }) + + test('streams empty lines as empty lines', async () => { + let input = Readable.from('py-3 p-1 px-3\n\nmt-2 mr-2 mb-2 ml-2\n') + let { stream: output, collect: collectOutput } = createOutput() + + await streamStdin({ css, cwd: path.dirname(css), format: 'text', input, output }) + + expect(collectOutput()).toBe('p-3\n\nm-2\n') + }) + + test('streams json output when requested', async () => { + let input = Readable.from('py-3 p-1 px-3\nmt-2 mr-2 mb-2 ml-2\n') + let { stream: output, collect: collectOutput } = createOutput() + + await streamStdin({ css, cwd: path.dirname(css), format: 'json', input, output }) + + expect(JSON.parse(collectOutput())).toEqual([ + { + input: 'py-3 p-1 px-3', + output: 'p-3', + changed: true, + }, + { + input: 'mt-2 mr-2 mb-2 ml-2', + output: 'm-2', + changed: true, + }, + ]) + }) + + test('streams jsonl output when requested', async () => { + let input = Readable.from('py-3 p-1 px-3\nmt-2 mr-2 mb-2 ml-2\n') + let { stream: output, collect: collectOutput } = createOutput() + + await streamStdin({ css, cwd: path.dirname(css), format: 'jsonl', input, output }) + + expect(collectOutput()).toBe( + '{"input":"py-3 p-1 px-3","output":"p-3","changed":true}\n' + + '{"input":"mt-2 mr-2 mb-2 ml-2","output":"m-2","changed":true}\n', + ) + }) +}) + +function createOutput() { + let stream = new PassThrough() + let chunks: Buffer[] = [] + stream.on('data', (chunk) => chunks.push(chunk)) + return { stream, collect: () => Buffer.concat(chunks).toString() } +} diff --git a/packages/@tailwindcss-cli/src/commands/canonicalize/fixtures/input.css b/packages/@tailwindcss-cli/src/commands/canonicalize/fixtures/input.css new file mode 100644 index 000000000000..d4b5078586e2 --- /dev/null +++ b/packages/@tailwindcss-cli/src/commands/canonicalize/fixtures/input.css @@ -0,0 +1 @@ +@import 'tailwindcss'; diff --git a/packages/@tailwindcss-cli/src/commands/canonicalize/index.ts b/packages/@tailwindcss-cli/src/commands/canonicalize/index.ts new file mode 100644 index 000000000000..5aecfe51edad --- /dev/null +++ b/packages/@tailwindcss-cli/src/commands/canonicalize/index.ts @@ -0,0 +1,362 @@ +import { __unstable__loadDesignSystem } from '@tailwindcss/node' +import fs from 'node:fs/promises' +import path from 'node:path' +import { createInterface } from 'node:readline' +import type { Readable, Writable } from 'node:stream' +import { compare } from '../../../../tailwindcss/src/utils/compare' +import { segment } from '../../../../tailwindcss/src/utils/segment' +import { args, type Arg } from '../../utils/args' +import { help } from '../help' + +const css = String.raw +export type OutputFormat = 'text' | 'json' | 'jsonl' + +export interface CandidateGroupResult { + input: string + output: string + changed: boolean +} + +export interface RunCommandLineOptions { + argv?: string[] + cwd?: string + stdin?: string | null + stdinIsTTY?: boolean + stdoutIsTTY?: boolean +} + +export interface RunCommandLineResult { + exitCode: number + stdout: string + stderr: string +} + +export function usage() { + return 'tailwindcss canonicalize [classes...]' +} + +function usageWithCss() { + return 'tailwindcss canonicalize --css input.css [classes...]' +} + +function usageWithStream() { + return 'tailwindcss canonicalize --stream [--css input.css]' +} + +export function options() { + return { + '--css': { + type: 'string', + description: + 'CSS entry file used to load the Tailwind design system (defaults to `@import "tailwindcss";`)', + }, + '--format': { + type: 'string', + description: 'Output format', + default: 'text', + values: ['text', 'json', 'jsonl'], + }, + '--stream': { + type: 'boolean', + description: 'Read candidate groups from stdin line by line and write results to stdout', + default: false, + }, + } satisfies Arg +} + +const sharedOptions = { + '--help': { + type: 'boolean', + description: 'Display usage information', + alias: '-h', + default: false, + }, +} satisfies Arg + +export async function runCommandLine({ + argv = process.argv.slice(2), + cwd = process.cwd(), + stdin = null, + stdinIsTTY = process.stdin.isTTY, + stdoutIsTTY = process.stdout.isTTY, +}: RunCommandLineOptions = {}): Promise { + try { + let flags = args( + { + ...options(), + ...sharedOptions, + }, + argv, + ) + + let format = parseFormat(flags['--format'] ?? 'text') + + if ((stdoutIsTTY && argv.length === 0) || flags['--help']) { + return { + exitCode: 0, + stdout: helpMessage() ?? '', + stderr: '', + } + } + + if (flags['--stream']) { + await streamStdin({ + css: flags['--css'], + cwd, + format, + input: process.stdin, + output: process.stdout, + }) + return { exitCode: 0, stdout: '', stderr: '' } + } + + let inputs = flags._.length > 0 ? flags._ : await readCandidateGroups({ stdin, stdinIsTTY }) + + if (inputs.length === 0) { + return usageError('No candidate groups provided') + } + + let results = await processCandidateGroups({ + css: flags['--css'], + cwd, + inputs, + }) + + return { + exitCode: 0, + stdout: formatCandidateResults(results, format), + stderr: '', + } + } catch (error) { + return { + exitCode: 1, + stdout: '', + stderr: error instanceof Error ? error.message : String(error), + } + } +} + +export async function streamStdin({ + css: cssFile, + cwd, + format, + input, + output, +}: { + css: string | null + cwd: string + format: OutputFormat + input: Readable + output: Writable +}): Promise { + let designSystem = await loadDesignSystem(cssFile, cwd) + let rl = createInterface({ input }) + let first = true + + if (format === 'json') { + output.write('[') + } + + for await (let line of rl) { + let result = createCandidateGroupResult(designSystem, line) + + switch (format) { + case 'text': { + output.write(result.output + '\n') + break + } + + case 'jsonl': { + output.write(JSON.stringify(result) + '\n') + break + } + + case 'json': { + if (first) { + output.write('\n') + } else { + output.write(',\n') + } + + output.write(indent(JSON.stringify(result, null, 2), 2)) + first = false + break + } + } + } + + if (format === 'json') { + output.write(first ? ']' : '\n]') + } +} + +export function readCandidateGroups({ + stdin, + stdinIsTTY, +}: { + stdin: string | null + stdinIsTTY: boolean +}) { + if (stdin !== null) { + return Promise.resolve(splitCandidateGroups(stdin)) + } + + if (stdinIsTTY) { + return Promise.resolve([]) + } + + return drainStdin().then(splitCandidateGroups) +} + +export async function drainStdin() { + return new Promise((resolve, reject) => { + let result = '' + + process.stdin.on('data', (chunk) => { + result += chunk + }) + + process.stdin.on('end', () => resolve(result)) + process.stdin.on('error', (err) => reject(err)) + }) +} + +export async function processCandidateGroups({ + css, + cwd = process.cwd(), + inputs, +}: { + css: string | null + cwd?: string + inputs: string[] +}): Promise { + let designSystem = await loadDesignSystem(css, cwd) + + return inputs.map((input) => createCandidateGroupResult(designSystem, input)) +} + +export function formatCandidateResults(results: CandidateGroupResult[], format: OutputFormat) { + switch (format) { + case 'json': + return JSON.stringify(results, null, 2) + case 'jsonl': + return results.map((result) => JSON.stringify(result)).join('\n') + case 'text': + return results.map((result) => result.output).join('\n') + } +} + +function helpMessage() { + return help({ + render: false, + usage: [usage(), usageWithCss(), usageWithStream()], + options: { + ...options(), + ...sharedOptions, + }, + }) +} + +async function loadDesignSystem(cssFile: string | null, cwd: string) { + if (cssFile === null) { + return __unstable__loadDesignSystem( + css` + @import 'tailwindcss'; + `, + { base: cwd }, + ) + } + + let resolvedCssFile = path.resolve(cwd, cssFile) + let content = await fs.readFile(resolvedCssFile, 'utf8') + return __unstable__loadDesignSystem(content, { + base: path.dirname(resolvedCssFile), + }) +} + +function splitCandidateGroups(input: string) { + return input + .split(/\r?\n/g) + .map((line) => line.trim()) + .filter((line) => line.length > 0) +} + +function parseFormat(input: string): OutputFormat { + if (input === 'text' || input === 'json' || input === 'jsonl') { + return input + } + + throw new Error(`Invalid value for --format: ${input}`) +} + +function usageError(message: string): RunCommandLineResult { + return { + exitCode: 1, + stdout: helpMessage() ?? '', + stderr: message, + } +} + +function splitCandidates(input: string) { + let trimmedInput = input.trim() + if (trimmedInput.length === 0) return [] + + return segment(trimmedInput, ' ') + .map((candidate) => candidate.trim()) + .filter((candidate) => candidate.length > 0) +} + +function canonicalize( + designSystem: Awaited>, + input: string, +) { + let candidates = splitCandidates(input) + candidates = designSystem.canonicalizeCandidates(candidates, { + collapse: true, + logicalToPhysical: true, + }) + return defaultSort(designSystem.getClassOrder(candidates)).join(' ') +} + +function createCandidateGroupResult( + designSystem: Awaited>, + input: string, +): CandidateGroupResult { + let output = canonicalize(designSystem, input) + + return { + input, + output, + changed: output !== input, + } +} + +function indent(input: string, size: number) { + let prefix = ' '.repeat(size) + return input + .split('\n') + .map((line) => prefix + line) + .join('\n') +} + +function defaultSort(entries: [string, bigint | null][]) { + return entries + .slice() + .sort(([candidateA, orderA], [candidateZ, orderZ]) => { + if (orderA === orderZ) return compare(candidateA, candidateZ) + if (orderA === null) return -1 + if (orderZ === null) return 1 + return bigSign(orderA - orderZ) || compare(candidateA, candidateZ) + }) + .map(([candidate]) => candidate) +} + +function bigSign(value: bigint) { + if (value > 0n) { + return 1 + } else if (value === 0n) { + return 0 + } else { + return -1 + } +} diff --git a/packages/@tailwindcss-cli/src/commands/help/index.ts b/packages/@tailwindcss-cli/src/commands/help/index.ts new file mode 100644 index 000000000000..b557d03e0acc --- /dev/null +++ b/packages/@tailwindcss-cli/src/commands/help/index.ts @@ -0,0 +1,182 @@ +import pc from 'picocolors' +import type { Arg } from '../../utils/args' +import { UI, header, highlight, indent, println, wordWrap } from '../../utils/renderer' + +export function help({ + render = true, + invalid, + usage, + options, +}: { + render?: boolean + invalid?: string + usage?: string[] + options?: Arg +}) { + // Available terminal width + let width = process.stdout.columns ?? 80 + let lines: string[] = [] + let writeLine = render ? println : (value = '') => void lines.push(value) + + // Render header + writeLine(header()) + + // Render the invalid command + if (invalid) { + writeLine() + writeLine(`${pc.dim('Invalid command:')} ${invalid}`) + } + + // Render usage + if (usage && usage.length > 0) { + writeLine() + writeLine(pc.dim('Usage:')) + for (let [idx, example] of usage.entries()) { + // Split the usage example into the command and its options. This allows + // us to wrap the options based on the available width of the terminal. + let command = example.slice(0, example.indexOf('[')) + let options = example.slice(example.indexOf('[')) + + // Make the options dimmed, to make them stand out less than the command + // itself. + options = options.replace(/\[.*?\]/g, (option) => pc.dim(option)) + + // The space between the command and the options. + let space = 1 + + // Wrap the options based on the available width of the terminal. + let lines = wordWrap(options, width - UI.indent - command.length - space) + + // Print an empty line between the usage examples if we need to split due + // to width constraints. This ensures that the usage examples are visually + // separated. + // + // E.g.: when enough space is available + // + // ``` + // Usage: + // tailwindcss build [--input input.css] [--output output.css] [--watch] [options...] + // tailwindcss other [--watch] [options...] + // ``` + // + // E.g.: when not enough space is available + // + // ``` + // Usage: + // tailwindcss build [--input input.css] [--output output.css] + // [--watch] [options...] + // + // tailwindcss other [--watch] [options...] + // ``` + if (lines.length > 1 && idx !== 0) { + writeLine() + } + + // Print the usage examples based on available width of the terminal. + // + // E.g.: when enough space is available + // + // ``` + // Usage: + // tailwindcss [--input input.css] [--output output.css] [--watch] [options...] + // ``` + // + // E.g.: when not enough space is available + // + // ``` + // Usage: + // tailwindcss [--input input.css] [--output output.css] + // [--watch] [options...] + // ``` + // + // > Note how the second line is indented to align with the first line. + writeLine(indent(`${command}${lines.shift()}`)) + for (let line of lines) { + writeLine(indent(line, command.length)) + } + } + } + + // Render options + if (options) { + // Track the max alias length, this is used to indent the options that don't + // have an alias such that everything is aligned properly. + let maxAliasLength = 0 + for (let { alias } of Object.values(options)) { + if (alias) { + maxAliasLength = Math.max(maxAliasLength, alias.length) + } + } + + // The option strings, which are the combination of the `alias` and the + // `flag`, with the correct spacing. + let optionStrings: string[] = [] + + // Track the max option length, which is the longest combination of an + // `alias` followed by `, ` and followed by the `flag`. + let maxOptionLength = 0 + + for (let [flag, { alias, values }] of Object.entries(options)) { + if (values?.length) { + flag += `[=${values.join(', ')}]` + } + + // The option string, which is the combination of the alias and the flag + // but already properly indented based on the other aliases to ensure + // everything is aligned properly. + let option = [ + alias ? `${alias.padStart(maxAliasLength)}` : alias, + alias ? flag : ' '.repeat(maxAliasLength + 2 /* `, `.length */) + flag, + ] + .filter(Boolean) + .join(', ') + + optionStrings.push(option) + maxOptionLength = Math.max(maxOptionLength, option.length) + } + + writeLine() + writeLine(pc.dim('Options:')) + + // The minimum amount of dots between the option and the description. + let minimumGap = 8 + + for (let { description, default: defaultValue = null } of Object.values(options)) { + // The option to render + let option = optionStrings.shift() as string + + // The amount of dots to show between the option and the description. + let dotCount = minimumGap + (maxOptionLength - option.length) + + // To account for the space before and after the dots. + let spaces = 2 + + // The available width remaining for the description. + let availableWidth = width - option.length - dotCount - spaces - UI.indent + + // Wrap the description and the default value (if present), based on the + // available width. + let lines = wordWrap( + defaultValue !== null + ? `${description} ${pc.dim(`[default:\u202F${highlight(`${defaultValue}`)}]`)}` + : description, + availableWidth, + ) + + // Print the option, the spacer dots and the start of the description. + writeLine( + indent(`${pc.blue(option)} ${pc.dim(pc.gray('\u00B7')).repeat(dotCount)} ${lines.shift()}`), + ) + + // Print the remaining lines of the description, indenting them to align + // with the start of the description. + for (let line of lines) { + writeLine(indent(`${' '.repeat(option.length + dotCount + spaces)}${line}`)) + } + } + } + + if (!render) { + return lines.join('\n') + } +} diff --git a/packages/@tailwindcss-cli/src/index.ts b/packages/@tailwindcss-cli/src/index.ts new file mode 100644 index 000000000000..4b308e694646 --- /dev/null +++ b/packages/@tailwindcss-cli/src/index.ts @@ -0,0 +1,82 @@ +#!/usr/bin/env node + +import { args, type Arg } from './utils/args' + +import * as build from './commands/build' +import * as canonicalize from './commands/canonicalize' +import { help } from './commands/help' + +const sharedOptions = { + '--help': { type: 'boolean', description: 'Display usage information', alias: '-h' }, +} satisfies Arg + +function buildUsage(command = 'tailwindcss') { + return `${command} [--input input.css] [--output output.css] [--watch] [options…]` +} + +function rootHelp({ invalid }: { invalid?: string } = {}) { + help({ + invalid, + usage: [buildUsage('tailwindcss'), buildUsage('tailwindcss build'), canonicalize.usage()], + commands: { + build: 'Build your CSS', + canonicalize: 'Canonicalize candidate lists', + }, + options: { ...build.options(), ...sharedOptions }, + }) +} + +async function run() { + let argv = process.argv.slice(2) + let command = argv[0] + let rootFlags = args({ ...build.options(), ...sharedOptions }, argv) + + if (command === 'build') { + let flags = args({ ...build.options(), ...sharedOptions }, argv.slice(1)) + + if ((process.stdout.isTTY && argv.length === 1) || flags['--help']) { + help({ + usage: [buildUsage('tailwindcss build')], + options: { ...build.options(), ...sharedOptions }, + }) + process.exit(0) + } + + await build.handle(flags) + return + } + + if (command === 'canonicalize') { + let result = await canonicalize.runCommandLine({ argv: argv.slice(1) }) + + if (result.stdout.length > 0) { + process.stdout.write(`${result.stdout}\n`) + } + + if (result.stderr.length > 0) { + process.stderr.write(`${result.stderr}\n`) + } + + process.exitCode = result.exitCode + return + } + + if ((process.stdout.isTTY && command === undefined) || rootFlags['--help']) { + rootHelp() + process.exit(0) + } + + if (command && !command.startsWith('-')) { + rootHelp({ invalid: command }) + process.exit(1) + } + + let flags = args({ + ...build.options(), + ...sharedOptions, + }) + + await build.handle(flags) +} + +await run() diff --git a/packages/@tailwindcss-cli/src/utils/args.test.ts b/packages/@tailwindcss-cli/src/utils/args.test.ts new file mode 100644 index 000000000000..4ac2e8778b9a --- /dev/null +++ b/packages/@tailwindcss-cli/src/utils/args.test.ts @@ -0,0 +1,155 @@ +import { expect, it } from 'vitest' +import { args, type Arg } from './args' + +it('should be possible to parse a single argument', () => { + expect( + args( + { + '--input': { type: 'string', description: 'Input file' }, + }, + ['--input', 'input.css'], + ), + ).toMatchInlineSnapshot(` + { + "--input": "input.css", + "_": [], + } + `) +}) + +it('should only return the last value for duplicate arguments', () => { + expect( + args( + { + '--output': { type: 'string', description: 'Output file' }, + }, + ['--output', 'output.css', '--output', 'override.css'], + ), + ).toMatchInlineSnapshot(` + { + "--output": "override.css", + "_": [], + } + `) +}) + +it('uses last value when flag with "-" is supplied multiple times', () => { + let result = args( + { + '--output': { type: 'string', description: 'Output file', alias: '-o' }, + }, + ['--output', 'output.css', '--output', '-'], + ) + + expect(result).toMatchInlineSnapshot(` + { + "--output": "-", + "_": [], + } + `) +}) + +it('should fallback to the default value if no flag is passed', () => { + expect( + args( + { + '--input': { type: 'string', description: 'Input file', default: 'input.css' }, + }, + ['--other'], + ), + ).toMatchInlineSnapshot(` + { + "--input": "input.css", + "_": [], + } + `) +}) + +it('should fallback to null if no flag is passed and no default value is provided', () => { + expect( + args( + { + '--input': { type: 'string', description: 'Input file' }, + }, + ['--other'], + ), + ).toMatchInlineSnapshot(` + { + "--input": null, + "_": [], + } + `) +}) + +it('should be possible to parse a single argument using the shorthand alias', () => { + expect( + args( + { + '--input': { type: 'string', description: 'Input file', alias: '-i' }, + }, + ['-i', 'input.css'], + ), + ).toMatchInlineSnapshot(` + { + "--input": "input.css", + "_": [], + } + `) +}) + +it('should convert the incoming value to the correct type', () => { + expect( + args( + { + '--input': { type: 'string', description: 'Input file' }, + '--watch': { type: 'boolean', description: 'Watch mode' }, + '--retries': { type: 'number', description: 'Amount of retries' }, + }, + ['--input', 'input.css', '--watch', '--retries', '3'], + ), + ).toMatchInlineSnapshot(` + { + "--input": "input.css", + "--retries": 3, + "--watch": true, + "_": [], + } + `) +}) + +it('should be possible to provide multiple types, and convert the value to that type', () => { + let options = { + '--retries': { type: 'boolean | number | string', description: 'Retries' }, + } satisfies Arg + + expect(args(options, ['--retries'])).toMatchInlineSnapshot(` + { + "--retries": true, + "_": [], + } + `) + expect(args(options, ['--retries', 'true'])).toMatchInlineSnapshot(` + { + "--retries": true, + "_": [], + } + `) + expect(args(options, ['--retries', 'false'])).toMatchInlineSnapshot(` + { + "--retries": false, + "_": [], + } + `) + expect(args(options, ['--retries', '5'])).toMatchInlineSnapshot(` + { + "--retries": 5, + "_": [], + } + `) + expect(args(options, ['--retries', 'indefinitely'])).toMatchInlineSnapshot(` + { + "--retries": "indefinitely", + "_": [], + } + `) +}) diff --git a/packages/@tailwindcss-cli/src/utils/args.ts b/packages/@tailwindcss-cli/src/utils/args.ts new file mode 100644 index 000000000000..88b306b0514a --- /dev/null +++ b/packages/@tailwindcss-cli/src/utils/args.ts @@ -0,0 +1,181 @@ +import parse from 'mri' + +// Definition of the arguments for a command in the CLI. +export type Arg = { + [key: `--${string}`]: { + type: keyof Types + description: string + alias?: `-${string}` + default?: Types[keyof Types] + values?: string[] + } +} + +// Each argument will have a type and we want to convert the incoming raw string +// based value to the correct type. We can't use pure TypeScript types because +// these don't exist at runtime. Instead, we define a string-based type that +// maps to a TypeScript type. +type Types = { + boolean: boolean + number: number | null + string: string | null + 'boolean | string': boolean | string | null + 'number | string': number | string | null + 'boolean | number': boolean | number | null + 'boolean | number | string': boolean | number | string | null +} + +// Convert the `Arg` type to a type that can be used at runtime. +// +// E.g.: +// +// Arg: +// ``` +// { '--input': { type: 'string', description: 'Input file', alias: '-i' } } +// ``` +// +// Command: +// ``` +// ./tailwindcss -i input.css +// ./tailwindcss --input input.css +// ``` +// +// Result type: +// ``` +// { +// _: string[], // All non-flag arguments +// '--input': string | null // The `--input` flag will be filled with `null`, if the flag is not used. +// // The `null` type will not be there if `default` is provided. +// } +// ``` +// +// Result runtime object: +// ``` +// { +// _: [], +// '--input': 'input.css' +// } +// ``` +export type Result = { + [K in keyof T]: T[K] extends { type: keyof Types; default?: any } + ? undefined extends T[K]['default'] + ? Types[T[K]['type']] + : NonNullable + : never +} & { + // All non-flag arguments + _: string[] +} + +export function args(options: T, argv = process.argv.slice(2)): Result { + for (let [idx, value] of argv.entries()) { + if (value === '-') { + argv[idx] = '__IO_DEFAULT_VALUE__' + } + } + + let parsed = parse(argv) + + for (let key in parsed) { + let value = parsed[key] + + if (key !== '_' && Array.isArray(value)) { + value = value[value.length - 1] + } + + if (value === '__IO_DEFAULT_VALUE__') { + value = '-' + } + + parsed[key] = value + } + + let result: { _: string[]; [key: string]: unknown } = { + _: parsed._, + } + + for (let [ + flag, + { type, alias, default: defaultValue = type === 'boolean' ? false : null }, + ] of Object.entries(options)) { + // Start with the default value + result[flag] = defaultValue + + // Try to find the `alias`, and map it to long form `flag` + if (alias) { + let key = alias.slice(1) + if (parsed[key] !== undefined) { + result[flag] = convert(parsed[key], type) + } + } + + // Try to find the long form `flag` + { + let key = flag.slice(2) + if (parsed[key] !== undefined) { + result[flag] = convert(parsed[key], type) + } + } + } + + return result as Result +} + +// --- + +type ArgumentType = string | boolean + +// Try to convert the raw incoming `value` (which will be a string or a boolean, +// this is coming from `mri`'s parse function'), to the correct type based on +// the `type` of the argument. +function convert(value: string | boolean, type: T) { + switch (type) { + case 'string': + return convertString(value) + case 'boolean': + return convertBoolean(value) + case 'number': + return convertNumber(value) + case 'boolean | string': + return convertBoolean(value) ?? convertString(value) + case 'number | string': + return convertNumber(value) ?? convertString(value) + case 'boolean | number': + return convertBoolean(value) ?? convertNumber(value) + case 'boolean | number | string': + return convertBoolean(value) ?? convertNumber(value) ?? convertString(value) + default: + throw new Error(`Unhandled type: ${type}`) + } +} + +function convertBoolean(value: ArgumentType) { + if (value === true || value === false) { + return value + } + + if (value === 'true') { + return true + } + + if (value === 'false') { + return false + } +} + +function convertNumber(value: ArgumentType) { + if (typeof value === 'number') { + return value + } + + { + let valueAsNumber = Number(value) + if (!Number.isNaN(valueAsNumber)) { + return valueAsNumber + } + } +} + +function convertString(value: ArgumentType) { + return `${value}` +} diff --git a/packages/@tailwindcss-cli/src/utils/disposables.ts b/packages/@tailwindcss-cli/src/utils/disposables.ts new file mode 100644 index 000000000000..737f3ed1e0a1 --- /dev/null +++ b/packages/@tailwindcss-cli/src/utils/disposables.ts @@ -0,0 +1,45 @@ +/** + * Disposables allow you to manage resources that can be cleaned up. Each helper + * function returns a dispose function to clean up the resource. + * + * The `dispose` method can be called to clean up all resources at once. + */ +export class Disposables { + // Track all disposables + #disposables = new Set([]) + + /** + * Enqueue a callback in the macrotasks queue. + */ + queueMacrotask(cb: () => void) { + let timer = setTimeout(cb, 0) + + return this.add(() => { + clearTimeout(timer) + }) + } + + /** + * General purpose disposable function that can be cleaned up. + */ + add(dispose: () => void) { + this.#disposables.add(dispose) + + return () => { + this.#disposables.delete(dispose) + + dispose() + } + } + + /** + * Dispose all disposables at once. + */ + async dispose() { + for (let dispose of this.#disposables) { + await dispose() + } + + this.#disposables.clear() + } +} diff --git a/packages/@tailwindcss-cli/src/utils/format-ns.test.ts b/packages/@tailwindcss-cli/src/utils/format-ns.test.ts new file mode 100644 index 000000000000..a5382d21772e --- /dev/null +++ b/packages/@tailwindcss-cli/src/utils/format-ns.test.ts @@ -0,0 +1,28 @@ +import { expect, it } from 'vitest' +import { formatNanoseconds } from './format-ns' + +it.each([ + [0, '0ns'], + [1, '1ns'], + [999, '999ns'], + [1000, '1µs'], + [1001, '1µs'], + [999999, '999µs'], + [1000000, '1ms'], + [1000001, '1ms'], + [999999999, '999ms'], + [1000000000, '1s'], + [1000000001, '1s'], + [59999999999, '59s'], + [60000000000, '1m'], + [60000000001, '1m'], + [3599999999999n, '59m'], + [3600000000000n, '1h'], + [3600000000001n, '1h'], + [86399999999999n, '23h'], + [86400000000000n, '1d'], + [86400000000001n, '1d'], + [8640000000000000n, '100d'], +])('should format %s nanoseconds as %s', (ns, expected) => { + expect(formatNanoseconds(ns)).toBe(expected) +}) diff --git a/packages/@tailwindcss-cli/src/utils/format-ns.ts b/packages/@tailwindcss-cli/src/utils/format-ns.ts new file mode 100644 index 000000000000..39889d401149 --- /dev/null +++ b/packages/@tailwindcss-cli/src/utils/format-ns.ts @@ -0,0 +1,23 @@ +export function formatNanoseconds(input: bigint | number) { + let ns = typeof input === 'number' ? BigInt(input) : input + + if (ns < 1_000n) return `${ns}ns` + ns /= 1_000n + + if (ns < 1_000n) return `${ns}µs` + ns /= 1_000n + + if (ns < 1_000n) return `${ns}ms` + ns /= 1_000n + + if (ns < 60n) return `${ns}s` + ns /= 60n + + if (ns < 60n) return `${ns}m` + ns /= 60n + + if (ns < 24n) return `${ns}h` + ns /= 24n + + return `${ns}d` +} diff --git a/packages/@tailwindcss-cli/src/utils/renderer.test.ts b/packages/@tailwindcss-cli/src/utils/renderer.test.ts new file mode 100644 index 000000000000..7acef455175f --- /dev/null +++ b/packages/@tailwindcss-cli/src/utils/renderer.test.ts @@ -0,0 +1,70 @@ +import path from 'node:path' +import { describe, expect, it } from 'vitest' +import { relative, wordWrap } from './renderer' +import { normalizeWindowsSeparators } from './test-helpers' + +describe('relative', () => { + it('should print an absolute path relative to the current working directory', () => { + expect(normalizeWindowsSeparators(relative(path.resolve('index.css')))).toMatchInlineSnapshot( + `"./index.css"`, + ) + }) + + it('should prefer the shortest value by default', () => { + // Shortest between absolute and relative paths + expect(normalizeWindowsSeparators(relative('index.css'))).toMatchInlineSnapshot(`"index.css"`) + }) + + it('should be possible to override the current working directory', () => { + expect(normalizeWindowsSeparators(relative('../utils/index.css', '..'))).toMatchInlineSnapshot( + `"./utils/index.css"`, + ) + }) + + it('should be possible to always prefer the relative path', () => { + expect( + normalizeWindowsSeparators( + relative('index.css', process.cwd(), { preferAbsoluteIfShorter: false }), + ), + ).toMatchInlineSnapshot(`"./index.css"`) + }) +}) + +describe('word wrap', () => { + it('should wrap a sentence', () => { + expect(wordWrap('The quick brown fox jumps over the lazy dog', 10)).toMatchInlineSnapshot(` + [ + "The quick", + "brown fox", + "jumps over", + "the lazy", + "dog", + ] + `) + expect(wordWrap('The quick brown fox jumps over the lazy dog', 30)).toMatchInlineSnapshot(` + [ + "The quick brown fox jumps over", + "the lazy dog", + ] + `) + }) + + it('should wrap a sentence with ANSI escape codes', () => { + // The ANSI escape codes are not counted in the length, but they should + // still be rendered correctly. + expect( + wordWrap( + '\x1B[31mThe\x1B[39m \x1B[32mquick\x1B[39m \x1B[34mbrown\x1B[39m \x1B[35mfox\x1B[39m jumps over the lazy dog', + 10, + ), + ).toMatchInlineSnapshot(` + [ + "The quick", + "brown fox", + "jumps over", + "the lazy", + "dog", + ] + `) + }) +}) diff --git a/packages/@tailwindcss-cli/src/utils/renderer.ts b/packages/@tailwindcss-cli/src/utils/renderer.ts new file mode 100644 index 000000000000..6b00985866cb --- /dev/null +++ b/packages/@tailwindcss-cli/src/utils/renderer.ts @@ -0,0 +1,102 @@ +import fs from 'node:fs' +import path from 'node:path' +import { stripVTControlCharacters } from 'node:util' +import pc from 'picocolors' +import { resolve } from '../utils/resolve' +import { formatNanoseconds } from './format-ns' + +export const UI = { + indent: 2, +} +export function header() { + return `${pc.italic(pc.bold(pc.blue('\u2248')))} tailwindcss ${pc.blue(`v${getVersion()}`)}` +} + +export function highlight(file: string) { + return `${pc.dim(pc.blue('`'))}${pc.blue(file)}${pc.dim(pc.blue('`'))}` +} + +/** + * Convert an `absolute` path to a `relative` path from the current working + * directory. + */ +export function relative( + to: string, + from = process.cwd(), + { preferAbsoluteIfShorter = true } = {}, +) { + let result = path.relative(from, to) + if (!result.startsWith('..')) { + result = `.${path.sep}${result}` + } + + if (preferAbsoluteIfShorter && result.length > to.length) { + return to + } + + return result +} + +/** + * Wrap `text` into multiple lines based on the `width`. + */ +export function wordWrap(text: string, width: number) { + let words = text.split(' ') + let lines = [] + + let line = '' + let lineLength = 0 + for (let word of words) { + let wordLength = stripVTControlCharacters(word).length + + if (lineLength + wordLength + 1 > width) { + lines.push(line) + line = '' + lineLength = 0 + } + + line += (lineLength ? ' ' : '') + word + lineLength += wordLength + (lineLength ? 1 : 0) + } + + if (lineLength) { + lines.push(line) + } + + return lines +} + +/** + * Format a duration in nanoseconds to a more human readable format. + */ +export function formatDuration(ns: bigint) { + let formatted = formatNanoseconds(ns) + + if (ns <= 50 * 1e6) return pc.green(formatted) + if (ns <= 300 * 1e6) return pc.blue(formatted) + if (ns <= 1000 * 1e6) return pc.yellow(formatted) + + return pc.red(formatted) +} + +export function indent(value: string, offset = 0) { + return `${' '.repeat(offset + UI.indent)}${value}` +} + +// Rust inspired functions to print to the console: + +export function eprintln(value = '') { + process.stderr.write(`${value}\n`) +} + +export function println(value = '') { + process.stdout.write(`${value}\n`) +} + +function getVersion(): string { + if (typeof globalThis.__tw_version === 'string') { + return globalThis.__tw_version + } + let { version } = JSON.parse(fs.readFileSync(resolve('tailwindcss/package.json'), 'utf-8')) + return version +} diff --git a/packages/@tailwindcss-cli/src/utils/resolve.ts b/packages/@tailwindcss-cli/src/utils/resolve.ts new file mode 100644 index 000000000000..e9197e8741ab --- /dev/null +++ b/packages/@tailwindcss-cli/src/utils/resolve.ts @@ -0,0 +1,32 @@ +import EnhancedResolve from 'enhanced-resolve' +import fs from 'node:fs' +import { createRequire } from 'node:module' + +const localResolve = createRequire(import.meta.url).resolve +export function resolve(id: string) { + if (typeof globalThis.__tw_resolve === 'function') { + let resolved = globalThis.__tw_resolve(id) + if (resolved) { + return resolved + } + } + return localResolve(id) +} + +const resolver = EnhancedResolve.ResolverFactory.createResolver({ + fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000), + useSyncFileSystemCalls: true, + extensions: ['.css'], + mainFields: ['style'], + conditionNames: ['style'], +}) +export function resolveCssId(id: string, base: string) { + if (typeof globalThis.__tw_resolve === 'function') { + let resolved = globalThis.__tw_resolve(id, base) + if (resolved) { + return resolved + } + } + + return resolver.resolveSync({}, base, id) +} diff --git a/packages/@tailwindcss-cli/src/utils/test-helpers.ts b/packages/@tailwindcss-cli/src/utils/test-helpers.ts new file mode 100644 index 000000000000..30f4fc777fb8 --- /dev/null +++ b/packages/@tailwindcss-cli/src/utils/test-helpers.ts @@ -0,0 +1,5 @@ +import path from 'node:path' + +export function normalizeWindowsSeparators(p: string) { + return path.sep === '\\' ? p.replaceAll('\\', '/') : p +} diff --git a/packages/@tailwindcss-cli/tsconfig.json b/packages/@tailwindcss-cli/tsconfig.json new file mode 100644 index 000000000000..6ae022f65bf0 --- /dev/null +++ b/packages/@tailwindcss-cli/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.base.json", +} diff --git a/packages/@tailwindcss-cli/tsup.config.ts b/packages/@tailwindcss-cli/tsup.config.ts new file mode 100644 index 000000000000..7d82eee2c882 --- /dev/null +++ b/packages/@tailwindcss-cli/tsup.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + format: ['esm'], + clean: true, + minify: true, + entry: ['src/index.ts'], +}) diff --git a/packages/@tailwindcss-node/README.md b/packages/@tailwindcss-node/README.md new file mode 100644 index 000000000000..5f532607d00a --- /dev/null +++ b/packages/@tailwindcss-node/README.md @@ -0,0 +1,36 @@ +

+ + + + + Tailwind CSS + + +

+ +

+ A utility-first CSS framework for rapidly building custom user interfaces. +

+ +

+ Build Status + Total Downloads + Latest Release + License +

+ +--- + +## Documentation + +For full documentation, visit [tailwindcss.com](https://tailwindcss.com). + +## Community + +For help, discussion about best practices, or feature ideas: + +[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/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**. diff --git a/packages/@tailwindcss-node/package.json b/packages/@tailwindcss-node/package.json new file mode 100644 index 000000000000..a8763e934564 --- /dev/null +++ b/packages/@tailwindcss-node/package.json @@ -0,0 +1,48 @@ +{ + "name": "@tailwindcss/node", + "version": "4.2.2", + "description": "A utility-first CSS framework for rapidly building custom user interfaces.", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/tailwindlabs/tailwindcss.git", + "directory": "packages/@tailwindcss-node" + }, + "bugs": "https://github.com/tailwindlabs/tailwindcss/issues", + "homepage": "https://tailwindcss.com", + "scripts": { + "build": "tsup-node", + "dev": "pnpm run build -- --watch" + }, + "files": [ + "dist/" + ], + "publishConfig": { + "provenance": true, + "access": "public" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./require-cache": { + "types": "./dist/require-cache.d.ts", + "default": "./dist/require-cache.js" + }, + "./esm-cache-loader": { + "types": "./dist/esm-cache.loader.d.mts", + "default": "./dist/esm-cache.loader.mjs" + } + }, + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "catalog:", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "workspace:*" + } +} diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts new file mode 100644 index 000000000000..7eb959d3bb1b --- /dev/null +++ b/packages/@tailwindcss-node/src/compile.ts @@ -0,0 +1,278 @@ +import EnhancedResolve from 'enhanced-resolve' +import { createJiti, type Jiti } from 'jiti' +import fs from 'node:fs' +import fsPromises from 'node:fs/promises' +import path from 'node:path' +import { pathToFileURL } from 'node:url' +import { + __unstable__loadDesignSystem as ___unstable__loadDesignSystem, + compile as _compile, + compileAst as _compileAst, + Features, + Polyfills, +} from 'tailwindcss' +import type { AstNode } from '../../tailwindcss/src/ast' +import { getModuleDependencies } from './get-module-dependencies' +import { rewriteUrls } from './urls' + +export { Features, Polyfills } + +export type Resolver = (id: string, base: string) => Promise + +export interface CompileOptions { + base: string + from?: string + onDependency: (path: string) => void + shouldRewriteUrls?: boolean + polyfills?: Polyfills + + customCssResolver?: Resolver + customJsResolver?: Resolver +} + +function createCompileOptions({ + base, + from, + polyfills, + onDependency, + shouldRewriteUrls, + + customCssResolver, + customJsResolver, +}: CompileOptions) { + return { + base, + polyfills, + from, + async loadModule(id: string, base: string) { + return loadModule(id, base, onDependency, customJsResolver) + }, + async loadStylesheet(id: string, sheetBase: string) { + let sheet = await loadStylesheet(id, sheetBase, onDependency, customCssResolver) + + if (shouldRewriteUrls) { + sheet.content = await rewriteUrls({ + css: sheet.content, + root: base, + base: sheet.base, + }) + } + + return sheet + }, + } +} + +async function ensureSourceDetectionRootExists(compiler: { + root: Awaited>['root'] +}) { + // Verify if the `source(…)` path exists (until the glob pattern starts) + if (compiler.root && compiler.root !== 'none') { + let globSymbols = /[*{]/ + let basePath = [] + for (let segment of compiler.root.pattern.split('/')) { + if (globSymbols.test(segment)) { + break + } + + basePath.push(segment) + } + + let exists = await fsPromises + .stat(path.resolve(compiler.root.base, basePath.join('/'))) + .then((stat) => stat.isDirectory()) + .catch(() => false) + + if (!exists) { + throw new Error( + `The \`source(${compiler.root.pattern})\` does not exist or is not a directory.`, + ) + } + } +} + +export async function compileAst(ast: AstNode[], options: CompileOptions) { + let compiler = await _compileAst(ast, createCompileOptions(options)) + await ensureSourceDetectionRootExists(compiler) + return compiler +} + +export async function compile(css: string, options: CompileOptions) { + let compiler = await _compile(css, createCompileOptions(options)) + await ensureSourceDetectionRootExists(compiler) + return compiler +} + +export async function __unstable__loadDesignSystem(css: string, { base }: { base: string }) { + return ___unstable__loadDesignSystem(css, { + base, + async loadModule(id, base) { + return loadModule(id, base, () => {}) + }, + async loadStylesheet(id, base) { + return loadStylesheet(id, base, () => {}) + }, + }) +} + +export async function loadModule( + id: string, + base: string, + onDependency: (path: string) => void, + customJsResolver?: Resolver, +) { + if (id[0] !== '.') { + let resolvedPath = await resolveJsId(id, base, customJsResolver) + if (!resolvedPath) { + throw new Error(`Could not resolve '${id}' from '${base}'`) + } + + let module = await importModule(pathToFileURL(resolvedPath).href) + return { + path: resolvedPath, + base: path.dirname(resolvedPath), + module: module.default ?? module, + } + } + + let resolvedPath = await resolveJsId(id, base, customJsResolver) + if (!resolvedPath) { + throw new Error(`Could not resolve '${id}' from '${base}'`) + } + + let [module, moduleDependencies] = await Promise.all([ + importModule(pathToFileURL(resolvedPath).href + '?id=' + Date.now()), + getModuleDependencies(resolvedPath), + ]) + + for (let file of moduleDependencies) { + onDependency(file) + } + return { + path: resolvedPath, + base: path.dirname(resolvedPath), + module: module.default ?? module, + } +} + +async function loadStylesheet( + id: string, + base: string, + onDependency: (path: string) => void, + cssResolver?: Resolver, +) { + let resolvedPath = await resolveCssId(id, base, cssResolver) + if (!resolvedPath) throw new Error(`Could not resolve '${id}' from '${base}'`) + + onDependency(resolvedPath) + + let file = await fsPromises.readFile(resolvedPath, 'utf-8') + return { + path: resolvedPath, + base: path.dirname(resolvedPath), + content: file, + } +} + +// Attempts to import the module using the native `import()` function. If this +// fails, it sets up `jiti` and attempts to import this way so that `.ts` files +// can be resolved properly. +let jiti: null | Jiti = null +async function importModule(path: string): Promise { + if (typeof globalThis.__tw_load === 'function') { + let module = await globalThis.__tw_load(path) + if (module) { + return module + } + } + + try { + return await import(path) + } catch (error) { + jiti ??= createJiti(import.meta.url, { moduleCache: false, fsCache: false }) + return await jiti.import(path) + } +} + +const modules = ['node_modules', ...(process.env.NODE_PATH ? [process.env.NODE_PATH] : [])] + +const cssResolver = EnhancedResolve.ResolverFactory.createResolver({ + fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000), + useSyncFileSystemCalls: true, + extensions: ['.css'], + mainFields: ['style'], + conditionNames: ['style'], + modules, +}) +async function resolveCssId( + id: string, + base: string, + customCssResolver?: Resolver, +): Promise { + if (typeof globalThis.__tw_resolve === 'function') { + let resolved = globalThis.__tw_resolve(id, base) + if (resolved) { + return Promise.resolve(resolved) + } + } + + if (customCssResolver) { + let customResolution = await customCssResolver(id, base) + if (customResolution) { + return customResolution + } + } + + return runResolver(cssResolver, id, base) +} + +const esmResolver = EnhancedResolve.ResolverFactory.createResolver({ + fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000), + useSyncFileSystemCalls: true, + extensions: ['.js', '.json', '.node', '.ts'], + conditionNames: ['node', 'import'], + modules, +}) + +const cjsResolver = EnhancedResolve.ResolverFactory.createResolver({ + fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000), + useSyncFileSystemCalls: true, + extensions: ['.js', '.json', '.node', '.ts'], + conditionNames: ['node', 'require'], + modules, +}) + +async function resolveJsId( + id: string, + base: string, + customJsResolver?: Resolver, +): Promise { + if (typeof globalThis.__tw_resolve === 'function') { + let resolved = globalThis.__tw_resolve(id, base) + if (resolved) { + return Promise.resolve(resolved) + } + } + + if (customJsResolver) { + let customResolution = await customJsResolver(id, base) + if (customResolution) { + return customResolution + } + } + + return runResolver(esmResolver, id, base).catch(() => runResolver(cjsResolver, id, base)) +} + +function runResolver( + resolver: EnhancedResolve.Resolver, + id: string, + base: string, +): Promise { + return new Promise((resolve, reject) => + resolver.resolve({}, base, id, {}, (err, result) => { + if (err) return reject(err) + resolve(result) + }), + ) +} diff --git a/packages/@tailwindcss-node/src/env.ts b/packages/@tailwindcss-node/src/env.ts new file mode 100644 index 000000000000..3b1a1b726fa9 --- /dev/null +++ b/packages/@tailwindcss-node/src/env.ts @@ -0,0 +1,44 @@ +export const DEBUG = resolveDebug(process.env.DEBUG) + +function resolveDebug(debug: typeof process.env.DEBUG) { + if (typeof debug === 'boolean') { + return debug + } + + if (debug === undefined) { + return false + } + + // Environment variables are strings, so convert to boolean + if (debug === 'true' || debug === '1') { + return true + } + + if (debug === 'false' || debug === '0') { + return false + } + + // Keep the debug convention into account: + // DEBUG=* -> This enables all debug modes + // DEBUG=projectA,projectB,projectC -> This enables debug for projectA, projectB and projectC + // DEBUG=projectA:* -> This enables all debug modes for projectA (if you have sub-types) + // DEBUG=projectA,-projectB -> This enables debug for projectA and explicitly disables it for projectB + + if (debug === '*') { + return true + } + + let debuggers = debug.split(',').map((d) => d.split(':')[0]) + + // Ignoring tailwindcss + if (debuggers.includes('-tailwindcss')) { + return false + } + + // Including tailwindcss + if (debuggers.includes('tailwindcss')) { + return true + } + + return false +} diff --git a/packages/@tailwindcss-node/src/esm-cache.loader.mts b/packages/@tailwindcss-node/src/esm-cache.loader.mts new file mode 100644 index 000000000000..edcfc8d778ba --- /dev/null +++ b/packages/@tailwindcss-node/src/esm-cache.loader.mts @@ -0,0 +1,22 @@ +import { isBuiltin, type ResolveHook } from 'node:module' + +export let resolve: ResolveHook = async (specifier, context, nextResolve) => { + let result = await nextResolve(specifier, context) + + if (result.url === import.meta.url) return result + if (isBuiltin(result.url)) return result + if (!context.parentURL) return result + + let parent = new URL(context.parentURL) + + let id = parent.searchParams.get('id') + if (id === null) return result + + let url = new URL(result.url) + url.searchParams.set('id', id) + + return { + ...result, + url: `${url}`, + } +} diff --git a/packages/@tailwindcss-node/src/get-module-dependencies.ts b/packages/@tailwindcss-node/src/get-module-dependencies.ts new file mode 100644 index 000000000000..34dedff20c3f --- /dev/null +++ b/packages/@tailwindcss-node/src/get-module-dependencies.ts @@ -0,0 +1,106 @@ +import fs from 'node:fs/promises' +import path from 'node:path' + +// Patterns we use to match dependencies in a file whether in CJS, ESM, or TypeScript +const DEPENDENCY_PATTERNS = [ + /import[\s\S]*?['"](.{3,}?)['"]/gi, + /import[\s\S]*from[\s\S]*?['"](.{3,}?)['"]/gi, + /export[\s\S]*from[\s\S]*?['"](.{3,}?)['"]/gi, + /require\(['"`](.+)['"`]\)/gi, +] + +// Given the current file `a.ts`, we want to make sure that when importing `b` that we resolve +// `b.ts` before `b.js` +// +// E.g.: +// +// a.ts +// b // .ts +// c // .ts +// a.js +// b // .js or .ts +const JS_EXTENSIONS = ['.js', '.cjs', '.mjs'] +const JS_RESOLUTION_ORDER = ['', '.js', '.cjs', '.mjs', '.ts', '.cts', '.mts', '.jsx', '.tsx'] +const TS_RESOLUTION_ORDER = ['', '.ts', '.cts', '.mts', '.tsx', '.js', '.cjs', '.mjs', '.jsx'] + +async function resolveWithExtension(file: string, extensions: string[]) { + // Try to find `./a.ts`, `./a.cts`, ... from `./a` + for (let ext of extensions) { + let full = `${file}${ext}` + + let stats = await fs.stat(full).catch(() => null) + if (stats?.isFile()) return full + } + + // Try to find `./a/index.js` from `./a` + for (let ext of extensions) { + let full = `${file}/index${ext}` + + let exists = await fs.access(full).then( + () => true, + () => false, + ) + if (exists) { + return full + } + } + + return null +} + +async function traceDependencies( + seen: Set, + filename: string, + base: string, + ext: string, +): Promise { + // Try to find the file + let extensions = JS_EXTENSIONS.includes(ext) ? JS_RESOLUTION_ORDER : TS_RESOLUTION_ORDER + let absoluteFile = await resolveWithExtension(path.resolve(base, filename), extensions) + if (absoluteFile === null) return // File doesn't exist + + // Prevent infinite loops when there are circular dependencies + if (seen.has(absoluteFile)) return // Already seen + + // Mark the file as a dependency + seen.add(absoluteFile) + + // Resolve new base for new imports/requires + base = path.dirname(absoluteFile) + ext = path.extname(absoluteFile) + + let contents = await fs.readFile(absoluteFile, 'utf-8') + + // Recursively trace dependencies in parallel + let promises = [] + + for (let pattern of DEPENDENCY_PATTERNS) { + for (let match of contents.matchAll(pattern)) { + // Bail out if it's not a relative file + if (!match[1].startsWith('.')) continue + + promises.push(traceDependencies(seen, match[1], base, ext)) + } + } + + await Promise.all(promises) +} + +/** + * Trace all dependencies of a module recursively + * + * The result is an unordered set of absolute file paths. Meaning that the order + * is not guaranteed to be equal to source order or across runs. + **/ +export async function getModuleDependencies(absoluteFilePath: string) { + let seen = new Set() + + await traceDependencies( + seen, + absoluteFilePath, + path.dirname(absoluteFilePath), + path.extname(absoluteFilePath), + ) + + return Array.from(seen) +} diff --git a/packages/@tailwindcss-node/src/index.cts b/packages/@tailwindcss-node/src/index.cts new file mode 100644 index 000000000000..4bca0a5e11de --- /dev/null +++ b/packages/@tailwindcss-node/src/index.cts @@ -0,0 +1,19 @@ +import * as Module from 'node:module' +import { pathToFileURL } from 'node:url' +import * as env from './env' +export * from './compile' +export * from './instrumentation' +export * from './normalize-path' +export * from './optimize' +export * from './source-maps' +export { env } + +// In Bun, ESM modules will also populate `require.cache`, so the module hook is +// not necessary. +if (!process.versions.bun) { + // `Module#register` was added in Node v18.19.0 and v20.6.0 + // + // Not calling it means that while ESM dependencies don't get reloaded, the + // actual included files will because they cache bust directly via `?id=…` + Module.register?.(pathToFileURL(require.resolve('@tailwindcss/node/esm-cache-loader'))) +} diff --git a/packages/@tailwindcss-node/src/index.ts b/packages/@tailwindcss-node/src/index.ts new file mode 100644 index 000000000000..a5e7bf5b4bc3 --- /dev/null +++ b/packages/@tailwindcss-node/src/index.ts @@ -0,0 +1,21 @@ +import * as Module from 'node:module' +import { pathToFileURL } from 'node:url' +import * as env from './env' +export * from './compile' +export * from './instrumentation' +export * from './normalize-path' +export * from './optimize' +export * from './source-maps' +export { env } + +// In Bun, ESM modules will also populate `require.cache`, so the module hook is +// not necessary. +if (!process.versions.bun) { + let localRequire = Module.createRequire(import.meta.url) + + // `Module#register` was added in Node v18.19.0 and v20.6.0 + // + // Not calling it means that while ESM dependencies don't get reloaded, the + // actual included files will because they cache bust directly via `?id=…` + Module.register?.(pathToFileURL(localRequire.resolve('@tailwindcss/node/esm-cache-loader'))) +} diff --git a/packages/@tailwindcss-node/src/instrumentation.test.ts b/packages/@tailwindcss-node/src/instrumentation.test.ts new file mode 100644 index 000000000000..1f06796fdd3e --- /dev/null +++ b/packages/@tailwindcss-node/src/instrumentation.test.ts @@ -0,0 +1,61 @@ +import { stripVTControlCharacters } from 'util' +import { expect, it } from 'vitest' +import { Instrumentation } from './instrumentation' + +it('should add instrumentation', () => { + let I = new Instrumentation() + + I.start('Foo') + let x = 1 + for (let i = 0; i < 100; i++) { + I.start('Bar') + x **= 2 + I.end('Bar') + } + I.end('Foo') + + I.hit('Potato') + I.hit('Potato') + I.hit('Potato') + I.hit('Potato') + + expect.assertions(1) + + I.report((output) => { + expect(stripVTControlCharacters(output).replace(/\[.*\]/g, '[0.xxms]')).toMatchInlineSnapshot(` + " + Hits: + Potato × 4 + + Timers: + [0.xxms] Foo + [0.xxms] ↳ Bar × 100 + " + `) + }) +}) + +it('should auto end pending timers when reporting', () => { + let I = new Instrumentation() + + I.start('Foo') + let x = 1 + for (let i = 0; i < 100; i++) { + I.start('Bar') + x **= 2 + I.end('Bar') + } + I.start('Baz') + + expect.assertions(1) + + I.report((output) => { + expect(stripVTControlCharacters(output).replace(/\[.*\]/g, '[0.xxms]')).toMatchInlineSnapshot(` + " + [0.xxms] Foo + [0.xxms] ↳ Bar × 100 + [0.xxms] ↳ Baz + " + `) + }) +}) diff --git a/packages/@tailwindcss-node/src/instrumentation.ts b/packages/@tailwindcss-node/src/instrumentation.ts new file mode 100644 index 000000000000..d27858d888bd --- /dev/null +++ b/packages/@tailwindcss-node/src/instrumentation.ts @@ -0,0 +1,113 @@ +import { DefaultMap } from '../../tailwindcss/src/utils/default-map' +import * as env from './env' + +// See: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#:~:text=Symbol.dispose,-??=%20Symbol(%22Symbol.dispose +// @ts-expect-error — Ensure Symbol.dispose exists +Symbol.dispose ??= Symbol('Symbol.dispose') +// @ts-expect-error — Ensure Symbol.asyncDispose exists +Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose') + +export class Instrumentation implements Disposable { + #hits = new DefaultMap(() => ({ value: 0 })) + #timers = new DefaultMap(() => ({ value: 0n })) + #timerStack: { id: string; label: string; namespace: string; value: bigint }[] = [] + + constructor( + private defaultFlush = (message: string) => void process.stderr.write(`${message}\n`), + ) {} + + hit(label: string) { + this.#hits.get(label).value++ + } + + start(label: string) { + let namespace = this.#timerStack.map((t) => t.label).join('//') + let id = `${namespace}${namespace.length === 0 ? '' : '//'}${label}` + + this.#hits.get(id).value++ + + // Create the timer if it doesn't exist yet + this.#timers.get(id) + + this.#timerStack.push({ id, label, namespace, value: process.hrtime.bigint() }) + } + + end(label: string) { + let end = process.hrtime.bigint() + + if (this.#timerStack[this.#timerStack.length - 1].label !== label) { + throw new Error( + `Mismatched timer label: \`${label}\`, expected \`${ + this.#timerStack[this.#timerStack.length - 1].label + }\``, + ) + } + + let parent = this.#timerStack.pop()! + let elapsed = end - parent.value + this.#timers.get(parent.id).value += elapsed + } + + reset() { + this.#hits.clear() + this.#timers.clear() + this.#timerStack.splice(0) + } + + report(flush = this.defaultFlush) { + let output: string[] = [] + let hasHits = false + + // Auto end any pending timers + for (let i = this.#timerStack.length - 1; i >= 0; i--) { + this.end(this.#timerStack[i].label) + } + + for (let [label, { value: count }] of this.#hits.entries()) { + if (this.#timers.has(label)) continue + if (output.length === 0) { + hasHits = true + output.push('Hits:') + } + + let depth = label.split('//').length + output.push(`${' '.repeat(depth)}${label} ${dim(blue(`× ${count}`))}`) + } + + if (this.#timers.size > 0 && hasHits) { + output.push('\nTimers:') + } + + let max = -Infinity + let computed = new Map() + for (let [label, { value }] of this.#timers) { + let x = `${(Number(value) / 1e6).toFixed(2)}ms` + computed.set(label, x) + max = Math.max(max, x.length) + } + + for (let label of this.#timers.keys()) { + let depth = label.split('//').length + output.push( + `${dim(`[${computed.get(label)!.padStart(max, ' ')}]`)}${' '.repeat(depth - 1)}${depth === 1 ? ' ' : dim(' ↳ ')}${label.split('//').pop()} ${ + this.#hits.get(label).value === 1 ? '' : dim(blue(`× ${this.#hits.get(label).value}`)) + }`.trimEnd(), + ) + } + + flush(`\n${output.join('\n')}\n`) + this.reset() + } + + [Symbol.dispose]() { + env.DEBUG && this.report() + } +} + +function dim(input: string) { + return `\u001b[2m${input}\u001b[22m` +} + +function blue(input: string) { + return `\u001b[34m${input}\u001b[39m` +} diff --git a/packages/@tailwindcss-node/src/normalize-path.ts b/packages/@tailwindcss-node/src/normalize-path.ts new file mode 100644 index 000000000000..a8184ef2317c --- /dev/null +++ b/packages/@tailwindcss-node/src/normalize-path.ts @@ -0,0 +1,47 @@ +// Inlined version of `normalize-path` +// Copyright (c) 2014-2018, Jon Schlinkert. +// Released under the MIT License. +function normalizePathBase(path: string, stripTrailing?: boolean) { + if (typeof path !== 'string') { + throw new TypeError('expected path to be a string') + } + + if (path === '\\' || path === '/') return '/' + + var len = path.length + if (len <= 1) return path + + // ensure that win32 namespaces has two leading slashes, so that the path is + // handled properly by the win32 version of path.parse() after being normalized + // https://msdn.microsoft.com/library/windows/desktop/aa365247(v=vs.85).aspx#namespaces + var prefix = '' + if (len > 4 && path[3] === '\\') { + var ch = path[2] + if ((ch === '?' || ch === '.') && path.slice(0, 2) === '\\\\') { + path = path.slice(2) + prefix = '//' + } + } + + var segs = path.split(/[/\\]+/) + if (stripTrailing !== false && segs[segs.length - 1] === '') { + segs.pop() + } + return prefix + segs.join('/') +} + +export function normalizePath(originalPath: string) { + let normalized = normalizePathBase(originalPath) + + // Make sure Windows network share paths are normalized properly + // They have to begin with two slashes or they won't resolve correctly + if ( + originalPath.startsWith('\\\\') && + normalized.startsWith('/') && + !normalized.startsWith('//') + ) { + return `/${normalized}` + } + + return normalized +} diff --git a/packages/@tailwindcss-node/src/optimize.ts b/packages/@tailwindcss-node/src/optimize.ts new file mode 100644 index 000000000000..6e60f8b873f6 --- /dev/null +++ b/packages/@tailwindcss-node/src/optimize.ts @@ -0,0 +1,151 @@ +import remapping from '@jridgewell/remapping' +import { Features, transform } from 'lightningcss' +import MagicString from 'magic-string' + +export interface OptimizeOptions { + /** + * The file being transformed + */ + file?: string + + /** + * Enabled minified output + */ + minify?: boolean + + /** + * The output source map before optimization + * + * If omitted a resulting source map will not be available + */ + map?: string +} + +export interface TransformResult { + code: string + map: string | undefined +} + +export function optimize( + input: string, + { file = 'input.css', minify = false, map }: OptimizeOptions = {}, +): TransformResult { + function optimize(code: Buffer | Uint8Array, map: string | undefined) { + return transform({ + filename: file, + code, + minify, + sourceMap: typeof map !== 'undefined', + inputSourceMap: map, + drafts: { + customMedia: true, + }, + nonStandard: { + deepSelectorCombinator: true, + }, + include: Features.Nesting | Features.MediaQueries, + exclude: Features.LogicalProperties | Features.DirSelector | Features.LightDark, + targets: { + safari: (16 << 16) | (4 << 8), + ios_saf: (16 << 16) | (4 << 8), + firefox: 128 << 16, + chrome: 111 << 16, + }, + errorRecovery: true, + }) + } + + // Running Lightning CSS twice to ensure that adjacent rules are merged after + // nesting is applied. This creates a more optimized output. + let result = optimize(Buffer.from(input), map) + map = result.map?.toString() + + result.warnings = result.warnings.filter((warning) => { + // Ignore warnings about unknown pseudo-classes as they are likely caused + // by the use of `:deep()`, `:slotted()`, and `:global()` which are not + // standard CSS but are commonly used in frameworks like Vue. + if (/'(deep|slotted|global)' is not recognized as a valid pseudo-/.test(warning.message)) { + return false + } + + return true + }) + + // Because of `errorRecovery: true`, there could be warnings, so let's let the + // user know about them. + if (process.env.NODE_ENV !== 'test' && result.warnings.length > 0) { + let lines = input.split('\n') + + let output = [ + `Found ${result.warnings.length} ${result.warnings.length === 1 ? 'warning' : 'warnings'} while optimizing generated CSS:`, + ] + + for (let [idx, warning] of result.warnings.entries()) { + output.push('') + if (result.warnings.length > 1) { + output.push(`Issue #${idx + 1}:`) + } + + let context = 2 + + let start = Math.max(0, warning.loc.line - context - 1) + let end = Math.min(lines.length, warning.loc.line + context) + + let snippet = lines.slice(start, end).map((line, idx) => { + if (start + idx + 1 === warning.loc.line) { + return `${dim(`\u2502`)} ${line}` + } else { + return dim(`\u2502 ${line}`) + } + }) + + snippet.splice( + warning.loc.line - start, + 0, + `${dim('\u2506')}${' '.repeat(warning.loc.column - 1)} ${yellow(`${dim('^--')} ${warning.message}`)}`, + `${dim('\u2506')}`, + ) + + output.push(...snippet) + } + output.push('') + + console.warn(output.join('\n')) + } + + result = optimize(result.code, map) + map = result.map?.toString() + + let code = result.code.toString() + + // Work around an issue where the media query range syntax transpilation + // generates code that is invalid with `@media` queries level 3. + let magic = new MagicString(code) + magic.replaceAll('@media not (', '@media not all and (') + + // We have to use a source-map-preserving method of replacing the content + // which requires the use of Magic String + remapping(…) to make sure + // the resulting map is correct + if (map !== undefined && magic.hasChanged()) { + let magicMap = magic.generateMap({ source: 'original', hires: 'boundary' }).toString() + + let remapped = remapping([magicMap, map], () => null) + + map = remapped.toString() + } + + code = magic.toString() + + return { + code, + map, + } +} + +function dim(str: string) { + return `\x1B[2m${str}\x1B[22m` +} + +function yellow(str: string) { + return `\x1B[33m${str}\x1B[39m` +} diff --git a/packages/@tailwindcss-node/src/require-cache.cts b/packages/@tailwindcss-node/src/require-cache.cts new file mode 100644 index 000000000000..ea0562019468 --- /dev/null +++ b/packages/@tailwindcss-node/src/require-cache.cts @@ -0,0 +1,5 @@ +export function clearRequireCache(files: string[]) { + for (let key of files) { + delete require.cache[key] + } +} diff --git a/packages/@tailwindcss-node/src/source-maps.test.ts b/packages/@tailwindcss-node/src/source-maps.test.ts new file mode 100644 index 000000000000..96ff8f05cc5b --- /dev/null +++ b/packages/@tailwindcss-node/src/source-maps.test.ts @@ -0,0 +1,8 @@ +import { expect, it } from 'vitest' +import { toSourceMap } from './source-maps' + +it('should emit source maps', () => { + let map = toSourceMap('{"version":3,"sources":[],"names":[],"mappings":""}') + + expect(map.comment('app.css.map')).toBe('/*# sourceMappingURL=app.css.map */\n') +}) diff --git a/packages/@tailwindcss-node/src/source-maps.ts b/packages/@tailwindcss-node/src/source-maps.ts new file mode 100644 index 000000000000..58b79cb253c3 --- /dev/null +++ b/packages/@tailwindcss-node/src/source-maps.ts @@ -0,0 +1,60 @@ +import { SourceMapGenerator } from 'source-map-js' +import type { DecodedSource, DecodedSourceMap } from '../../tailwindcss/src/source-maps/source-map' +import { DefaultMap } from '../../tailwindcss/src/utils/default-map' + +export type { DecodedSource, DecodedSourceMap } +export interface SourceMap { + readonly raw: string + readonly inline: string + comment(url: string): string +} + +function serializeSourceMap(map: DecodedSourceMap): string { + let generator = new SourceMapGenerator() + + let id = 1 + let sourceTable = new DefaultMap< + DecodedSource | null, + { + url: string + content: string + } + >((src) => { + return { + url: src?.url ?? ``, + content: src?.content ?? '', + } + }) + + for (let mapping of map.mappings) { + let original = sourceTable.get(mapping.originalPosition?.source ?? null) + + generator.addMapping({ + generated: mapping.generatedPosition, + original: mapping.originalPosition, + source: original.url, + name: mapping.name, + }) + + generator.setSourceContent(original.url, original.content) + } + + return generator.toString() +} + +export function toSourceMap(map: DecodedSourceMap | string): SourceMap { + let raw = typeof map === 'string' ? map : serializeSourceMap(map) + + function comment(url: string) { + return `/*# sourceMappingURL\x3d${url} */\n` + } + + return { + raw, + get inline() { + let inlined = Buffer.from(raw, 'utf-8').toString('base64') + return comment(`data:application/json;base64,${inlined}`) + }, + comment, + } +} diff --git a/packages/@tailwindcss-node/src/urls.test.ts b/packages/@tailwindcss-node/src/urls.test.ts new file mode 100644 index 000000000000..16ba352a7f66 --- /dev/null +++ b/packages/@tailwindcss-node/src/urls.test.ts @@ -0,0 +1,163 @@ +import { expect, test } from 'vitest' +import { rewriteUrls } from './urls' + +const css = String.raw + +test('URLs can be rewritten', async () => { + let root = '/root' + + let result = await rewriteUrls({ + root, + base: '/root/foo/bar', + // prettier-ignore + css: css` + .foo { + /* Relative URLs: replaced */ + background: url(./image.jpg); + background: url(../image.jpg); + background: url('./image.jpg'); + background: url("./image.jpg"); + + /* Absolute URLs: ignored */ + background: url(/image.jpg); + background: url(/foo/image.jpg); + background: url('/image.jpg'); + background: url("/image.jpg"); + + /* Potentially Vite-aliased URLs: ignored */ + background: url(~/image.jpg); + background: url(~/foo/image.jpg); + background: url('~/image.jpg'); + background: url("~/image.jpg"); + background: url(#/image.jpg); + background: url(#/foo/image.jpg); + background: url('#/image.jpg'); + background: url("#/image.jpg"); + background: url(@/image.jpg); + background: url(@/foo/image.jpg); + background: url('@/image.jpg'); + background: url("@/image.jpg"); + + /* External URL: ignored */ + background: url(http://example.com/image.jpg); + background: url('http://example.com/image.jpg'); + background: url("http://example.com/image.jpg"); + + /* Data URI: ignored */ + /* background: url(data:image/png;base64,abc==); */ + background: url('data:image/png;base64,abc=='); + background: url("data:image/png;base64,abc=="); + + /* Function calls: ignored */ + background: url(var(--foo)); + background: url(var(--foo, './image.jpg')); + background: url(var(--foo, "./image.jpg")); + + /* Fragments: ignored */ + background: url(#dont-touch-this); + + /* Image Sets - Raw URL: replaced */ + background: image-set( + image1.jpg 1x, + image2.jpg 2x + ); + background: image-set( + 'image1.jpg' 1x, + 'image2.jpg' 2x + ); + background: image-set( + "image1.jpg" 1x, + "image2.jpg" 2x + ); + + /* Image Sets - Relative URLs: replaced */ + background: image-set( + url('image1.jpg') 1x, + url('image2.jpg') 2x + ); + background: image-set( + url("image1.jpg") 1x, + url("image2.jpg") 2x + ); + background: image-set( + url('image1.avif') type('image/avif'), + url('image2.jpg') type('image/jpeg') + ); + background: image-set( + url("image1.avif") type('image/avif'), + url("image2.jpg") type('image/jpeg') + ); + + /* Image Sets - Function calls: ignored */ + background: image-set( + linear-gradient(blue, white) 1x, + linear-gradient(blue, green) 2x + ); + + /* Image Sets - Mixed: replaced */ + background: image-set( + linear-gradient(blue, white) 1x, + url("image2.jpg") 2x + ); + } + + /* Fonts - Multiple URLs: replaced */ + @font-face { + font-family: "Newman"; + src: + local("Newman"), + url("newman-COLRv1.otf") format("opentype") tech(color-COLRv1), + url("newman-outline.otf") format("opentype"), + url("newman-outline.woff") format("woff"); + } + `, + }) + + expect(result).toMatchInlineSnapshot(` + ".foo { + background: url(./foo/bar/image.jpg); + background: url(./foo/image.jpg); + background: url('./foo/bar/image.jpg'); + background: url("./foo/bar/image.jpg"); + background: url(/image.jpg); + background: url(/foo/image.jpg); + background: url('/image.jpg'); + background: url("/image.jpg"); + background: url(~/image.jpg); + background: url(~/foo/image.jpg); + background: url('~/image.jpg'); + background: url("~/image.jpg"); + background: url(#/image.jpg); + background: url(#/foo/image.jpg); + background: url('#/image.jpg'); + background: url("#/image.jpg"); + background: url(@/image.jpg); + background: url(@/foo/image.jpg); + background: url('@/image.jpg'); + background: url("@/image.jpg"); + background: url(http://example.com/image.jpg); + background: url('http://example.com/image.jpg'); + background: url("http://example.com/image.jpg"); + background: url('data:image/png;base64,abc=='); + background: url("data:image/png;base64,abc=="); + background: url(var(--foo)); + background: url(var(--foo, './image.jpg')); + background: url(var(--foo, "./image.jpg")); + background: url(#dont-touch-this); + background: image-set(url(./foo/bar/image1.jpg) 1x, url(./foo/bar/image2.jpg) 2x); + background: image-set(url('./foo/bar/image1.jpg') 1x, url('./foo/bar/image2.jpg') 2x); + background: image-set(url("./foo/bar/image1.jpg") 1x, url("./foo/bar/image2.jpg") 2x); + background: image-set(url('./foo/bar/image1.jpg') 1x, url('./foo/bar/image2.jpg') 2x); + background: image-set(url("./foo/bar/image1.jpg") 1x, url("./foo/bar/image2.jpg") 2x); + background: image-set(url('./foo/bar/image1.avif') type('image/avif'), url('./foo/bar/image2.jpg') type('image/jpeg')); + background: image-set(url("./foo/bar/image1.avif") type('image/avif'), url("./foo/bar/image2.jpg") type('image/jpeg')); + background: image-set(linear-gradient(blue, white) 1x, linear-gradient(blue, green) 2x); + background: image-set(linear-gradient(blue, white) 1x, url("./foo/bar/image2.jpg") 2x); + } + @font-face { + font-family: "Newman"; + src: local("Newman"), url("./foo/bar/newman-COLRv1.otf") format("opentype") tech(color-COLRv1), url("./foo/bar/newman-outline.otf") format("opentype"), url("./foo/bar/newman-outline.woff") format("woff"); + } + " + `) +}) diff --git a/packages/@tailwindcss-node/src/urls.ts b/packages/@tailwindcss-node/src/urls.ts new file mode 100644 index 000000000000..61695a9d10e8 --- /dev/null +++ b/packages/@tailwindcss-node/src/urls.ts @@ -0,0 +1,207 @@ +// Inlined version of code from Vite +// Copyright (c) 2019-present, VoidZero Inc. and Vite contributors +// Released under the MIT License. +// +// Minor modifications have been made to work with the Tailwind CSS codebase + +import * as path from 'node:path' +import { toCss } from '../../tailwindcss/src/ast' +import { parse } from '../../tailwindcss/src/css-parser' +import { walk } from '../../tailwindcss/src/walk' +import { normalizePath } from './normalize-path' + +const cssUrlRE = + /(?[\w-]+\([^)]*\)|"[^"]*"|'[^']*'|[^,]\S*[^,])\s*(?:\s(?\w[^,]+))?(?:,|$)/g +const nonEscapedDoubleQuoteRE = /(? dataUrlRE.test(url) +const isExternalUrl = (url: string): boolean => externalRE.test(url) + +type CssUrlReplacer = (url: string, importer?: string) => string | Promise + +interface ImageCandidate { + url: string + descriptor: string +} + +export async function rewriteUrls({ + css, + base, + root, +}: { + css: string + base: string + root: string +}) { + if (!css.includes('url(') && !css.includes('image-set(')) { + return css + } + + let ast = parse(css) + + let promises: Promise[] = [] + + function replacerForDeclaration(url: string) { + if (url[0] === '/') return url + + let absoluteUrl = path.posix.join(normalizePath(base), url) + let relativeUrl = path.posix.relative(normalizePath(root), absoluteUrl) + + // If the path points to a file in the same directory, `path.relative` will + // remove the leading `./` and we need to add it back in order to still + // consider the path relative + if (!relativeUrl.startsWith('.')) { + relativeUrl = './' + relativeUrl + } + + return relativeUrl + } + + walk(ast, (node) => { + if (node.kind !== 'declaration') return + if (!node.value) return + + let isCssUrl = cssUrlRE.test(node.value) + let isCssImageSet = cssImageSetRE.test(node.value) + + if (isCssUrl || isCssImageSet) { + let rewriterToUse = isCssImageSet ? rewriteCssImageSet : rewriteCssUrls + + promises.push( + rewriterToUse(node.value, replacerForDeclaration).then((url) => { + node.value = url + }), + ) + } + }) + + if (promises.length) { + await Promise.all(promises) + } + + return toCss(ast) +} + +function rewriteCssUrls(css: string, replacer: CssUrlReplacer): Promise { + return asyncReplace(css, cssUrlRE, async (match) => { + const [matched, rawUrl] = match + return await doUrlReplace(rawUrl.trim(), matched, replacer) + }) +} + +async function rewriteCssImageSet(css: string, replacer: CssUrlReplacer): Promise { + return await asyncReplace(css, cssImageSetRE, async (match) => { + const [, rawUrl] = match + const url = await processSrcSet(rawUrl, async ({ url }) => { + // the url maybe url(...) + if (cssUrlRE.test(url)) { + return await rewriteCssUrls(url, replacer) + } + if (!cssNotProcessedRE.test(url)) { + return await doUrlReplace(url, url, replacer) + } + return url + }) + return url + }) +} + +async function doUrlReplace( + rawUrl: string, + matched: string, + replacer: CssUrlReplacer, + funcName: string = 'url', +) { + let wrap = '' + const first = rawUrl[0] + if (first === `"` || first === `'`) { + wrap = first + rawUrl = rawUrl.slice(1, -1) + } + + if (skipUrlReplacer(rawUrl)) { + return matched + } + + let newUrl = await replacer(rawUrl) + // The new url might need wrapping even if the original did not have it, e.g. if a space was added during replacement + if (wrap === '' && newUrl !== encodeURI(newUrl)) { + wrap = '"' + } + // If wrapping in single quotes and newUrl also contains single quotes, switch to double quotes. + // Give preference to double quotes since SVG inlining converts double quotes to single quotes. + if (wrap === "'" && newUrl.includes("'")) { + wrap = '"' + } + // Escape double quotes if they exist (they also tend to be rarer than single quotes) + if (wrap === '"' && newUrl.includes('"')) { + newUrl = newUrl.replace(nonEscapedDoubleQuoteRE, '\\"') + } + return `${funcName}(${wrap}${newUrl}${wrap})` +} + +function skipUrlReplacer(rawUrl: string, aliases?: string[]) { + return ( + isExternalUrl(rawUrl) || + isDataUrl(rawUrl) || + !rawUrl[0].match(/[.a-zA-Z0-9_]/) || + functionCallRE.test(rawUrl) + ) +} + +function processSrcSet( + srcs: string, + replacer: (arg: ImageCandidate) => Promise, +): Promise { + return Promise.all( + parseSrcset(srcs).map(async ({ url, descriptor }) => ({ + url: await replacer({ url, descriptor }), + descriptor, + })), + ).then(joinSrcset) +} + +function parseSrcset(string: string): ImageCandidate[] { + const matches = string + .trim() + .replace(escapedSpaceCharactersRE, ' ') + .replace(/\r?\n/, '') + .replace(/,\s+/, ', ') + .replaceAll(/\s+/g, ' ') + .matchAll(imageCandidateRE) + return Array.from(matches, ({ groups }) => ({ + url: groups?.url?.trim() ?? '', + descriptor: groups?.descriptor?.trim() ?? '', + })).filter(({ url }) => !!url) +} + +function joinSrcset(ret: ImageCandidate[]) { + return ret.map(({ url, descriptor }) => url + (descriptor ? ` ${descriptor}` : '')).join(', ') +} + +async function asyncReplace( + input: string, + re: RegExp, + replacer: (match: RegExpExecArray) => string | Promise, +): Promise { + let match: RegExpExecArray | null + let remaining = input + let rewritten = '' + while ((match = re.exec(remaining))) { + rewritten += remaining.slice(0, match.index) + rewritten += await replacer(match) + remaining = remaining.slice(match.index + match[0].length) + } + rewritten += remaining + return rewritten +} diff --git a/packages/@tailwindcss-node/tsconfig.json b/packages/@tailwindcss-node/tsconfig.json new file mode 100644 index 000000000000..6ae022f65bf0 --- /dev/null +++ b/packages/@tailwindcss-node/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.base.json", +} diff --git a/packages/@tailwindcss-node/tsup.config.ts b/packages/@tailwindcss-node/tsup.config.ts new file mode 100644 index 000000000000..3f2d184d54be --- /dev/null +++ b/packages/@tailwindcss-node/tsup.config.ts @@ -0,0 +1,40 @@ +import { defineConfig } from 'tsup' + +export default defineConfig([ + { + format: ['cjs'], + minify: true, + dts: true, + entry: ['src/index.cts'], + define: { + 'process.env.NODE_ENV': '"production"', + }, + }, + { + format: ['esm'], + minify: true, + dts: true, + entry: ['src/index.ts'], + define: { + 'process.env.NODE_ENV': '"production"', + }, + }, + { + format: ['esm'], + minify: true, + dts: true, + entry: ['src/esm-cache.loader.mts'], + define: { + 'process.env.NODE_ENV': '"production"', + }, + }, + { + format: ['cjs'], + minify: true, + dts: true, + entry: ['src/require-cache.cts'], + define: { + 'process.env.NODE_ENV': '"production"', + }, + }, +]) diff --git a/packages/@tailwindcss-postcss/README.md b/packages/@tailwindcss-postcss/README.md new file mode 100644 index 000000000000..6aec3fe253de --- /dev/null +++ b/packages/@tailwindcss-postcss/README.md @@ -0,0 +1,126 @@ +

+ + + + + Tailwind CSS + + +

+ +

+ A utility-first CSS framework for rapidly building custom user interfaces. +

+ +

+ Build Status + Total Downloads + Latest Release + License +

+ +--- + +## Documentation + +For full documentation, visit [tailwindcss.com](https://tailwindcss.com). + +## Community + +For help, discussion about best practices, or feature ideas: + +[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/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**. + +--- + +## `@tailwindcss/postcss` plugin API + +### Changing where the plugin searches for source files + +You can use the `base` option (defaults to the current working directory) to change the directory in which the plugin searches for source files: + +```js +import tailwindcss from '@tailwindcss/postcss' + +export default { + plugins: [ + tailwindcss({ + base: path.resolve(__dirname, './path'), + }), + ], +} +``` + +### Enabling or disabling Lightning CSS + +By default, this plugin detects whether or not the CSS is being built for production by checking the `NODE_ENV` environment variable. When building for production Lightning CSS will be enabled otherwise it is disabled. + +If you want to always enable or disable Lightning CSS the `optimize` option may be used: + +```js +import tailwindcss from '@tailwindcss/postcss' + +export default { + plugins: [ + tailwindcss({ + // Enable or disable Lightning CSS + optimize: false, + }), + ], +} +``` + +It's also possible to keep Lightning CSS enabled but disable minification: + +```js +import tailwindcss from '@tailwindcss/postcss' + +export default { + plugins: [ + tailwindcss({ + optimize: { minify: false }, + }), + ], +} +``` + +### Enabling or disabling `url(…)` rewriting + +Our PostCSS plugin can rewrite `url(…)`s for you since it also handles `@import` (no `postcss-import` is needed). This feature is enabled by default. + +In some situations the bundler or framework you're using may provide this feature itself. In this case you can set `transformAssetUrls` to `false` to disable this feature: + +```js +import tailwindcss from '@tailwindcss/postcss' + +export default { + plugins: [ + tailwindcss({ + // Disable `url(…)` rewriting + transformAssetUrls: false, + + // Enable `url(…)` rewriting (the default) + transformAssetUrls: true, + }), + ], +} +``` + +You may also pass options to `optimize` to enable Lighting CSS but prevent minification: + +```js +import tailwindcss from '@tailwindcss/postcss' + +export default { + plugins: [ + tailwindcss({ + // Enables Lightning CSS but disables minification + optimize: { minify: false }, + }), + ], +} +``` diff --git a/packages/@tailwindcss-postcss/package.json b/packages/@tailwindcss-postcss/package.json new file mode 100644 index 000000000000..922857a9031d --- /dev/null +++ b/packages/@tailwindcss-postcss/package.json @@ -0,0 +1,46 @@ +{ + "name": "@tailwindcss/postcss", + "version": "4.2.2", + "description": "PostCSS plugin for Tailwind CSS, a utility-first CSS framework for rapidly building custom user interfaces", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/tailwindlabs/tailwindcss.git", + "directory": "packages/@tailwindcss-postcss" + }, + "bugs": "https://github.com/tailwindlabs/tailwindcss/issues", + "homepage": "https://tailwindcss.com", + "scripts": { + "lint": "tsc --noEmit", + "build": "tsup-node", + "dev": "pnpm run build -- --watch" + }, + "files": [ + "dist/" + ], + "publishConfig": { + "provenance": true, + "access": "public" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "workspace:*", + "@tailwindcss/oxide": "workspace:*", + "postcss": "^8.5.6", + "tailwindcss": "workspace:*" + }, + "devDependencies": { + "@types/node": "catalog:", + "@types/postcss-import": "14.0.3", + "dedent": "1.7.1", + "internal-example-plugin": "workspace:*", + "postcss-import": "^16.1.1" + } +} diff --git a/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap b/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap new file mode 100644 index 000000000000..fd6db912cf4a --- /dev/null +++ b/packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap @@ -0,0 +1,308 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = ` +"@layer properties { + @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { + *, :before, :after, ::backdrop { + --tw-font-weight: initial; + } + } +} + +@layer theme { + :root, :host { + --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", + "Noto Color Emoji"; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", + monospace; + --color-black: #000; + --text-2xl: 1.5rem; + --text-2xl--line-height: calc(2 / 1.5); + --font-weight-bold: 700; + --default-font-family: var(--font-sans); + --default-mono-font-family: var(--font-mono); + } +} + +@layer base { + *, :after, :before, ::backdrop { + box-sizing: border-box; + border: 0 solid; + margin: 0; + padding: 0; + } + + ::file-selector-button { + box-sizing: border-box; + border: 0 solid; + margin: 0; + padding: 0; + } + + html, :host { + -webkit-text-size-adjust: 100%; + tab-size: 4; + line-height: 1.5; + font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); + font-feature-settings: var(--default-font-feature-settings, normal); + font-variation-settings: var(--default-font-variation-settings, normal); + -webkit-tap-highlight-color: transparent; + } + + hr { + height: 0; + color: inherit; + border-top-width: 1px; + } + + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + } + + h1, h2, h3, h4, h5, h6 { + font-size: inherit; + font-weight: inherit; + } + + a { + color: inherit; + -webkit-text-decoration: inherit; + -webkit-text-decoration: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; + } + + b, strong { + font-weight: bolder; + } + + code, kbd, samp, pre { + font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); + font-feature-settings: var(--default-mono-font-feature-settings, normal); + font-variation-settings: var(--default-mono-font-variation-settings, normal); + font-size: 1em; + } + + small { + font-size: 80%; + } + + sub, sup { + vertical-align: baseline; + font-size: 75%; + line-height: 0; + position: relative; + } + + sub { + bottom: -.25em; + } + + sup { + top: -.5em; + } + + table { + text-indent: 0; + border-color: inherit; + border-collapse: collapse; + } + + :-moz-focusring { + outline: auto; + } + + progress { + vertical-align: baseline; + } + + summary { + display: list-item; + } + + ol, ul, menu { + list-style: none; + } + + img, svg, video, canvas, audio, iframe, embed, object { + vertical-align: middle; + display: block; + } + + img, video { + max-width: 100%; + height: auto; + } + + button, input, select, optgroup, textarea { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; + opacity: 1; + background-color: #0000; + border-radius: 0; + } + + ::file-selector-button { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; + opacity: 1; + background-color: #0000; + border-radius: 0; + } + + :where(select:is([multiple], [size])) optgroup { + font-weight: bolder; + } + + :where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; + } + + ::file-selector-button { + margin-inline-end: 4px; + } + + ::placeholder { + opacity: 1; + } + + @supports (not ((-webkit-appearance: -apple-pay-button))) or (contain-intrinsic-size: 1px) { + ::placeholder { + color: currentColor; + } + + @supports (color: color-mix(in lab, red, red)) { + ::placeholder { + color: color-mix(in oklab, currentcolor 50%, transparent); + } + } + } + + textarea { + resize: vertical; + } + + ::-webkit-search-decoration { + -webkit-appearance: none; + } + + ::-webkit-date-and-time-value { + min-height: 1lh; + text-align: inherit; + } + + ::-webkit-datetime-edit { + display: inline-flex; + } + + ::-webkit-datetime-edit-fields-wrapper { + padding: 0; + } + + ::-webkit-datetime-edit { + padding-block: 0; + } + + ::-webkit-datetime-edit-year-field { + padding-block: 0; + } + + ::-webkit-datetime-edit-month-field { + padding-block: 0; + } + + ::-webkit-datetime-edit-day-field { + padding-block: 0; + } + + ::-webkit-datetime-edit-hour-field { + padding-block: 0; + } + + ::-webkit-datetime-edit-minute-field { + padding-block: 0; + } + + ::-webkit-datetime-edit-second-field { + padding-block: 0; + } + + ::-webkit-datetime-edit-millisecond-field { + padding-block: 0; + } + + ::-webkit-datetime-edit-meridiem-field { + padding-block: 0; + } + + ::-webkit-calendar-picker-indicator { + line-height: 1; + } + + :-moz-ui-invalid { + box-shadow: none; + } + + button, input:where([type="button"], [type="reset"], [type="submit"]) { + appearance: button; + } + + ::file-selector-button { + appearance: button; + } + + ::-webkit-inner-spin-button { + height: auto; + } + + ::-webkit-outer-spin-button { + height: auto; + } + + [hidden]:where(:not([hidden="until-found"])) { + display: none !important; + } +} + +@layer components; + +@layer utilities { + .text-2xl { + font-size: var(--text-2xl); + line-height: var(--tw-leading, var(--text-2xl--line-height)); + } + + .text-black\\/50 { + color: #00000080; + } + + @supports (color: color-mix(in lab, red, red)) { + .text-black\\/50 { + color: color-mix(in oklab, var(--color-black) 50%, transparent); + } + } + + .underline { + text-decoration-line: underline; + } + + @media (min-width: 96rem) { + .\\32 xl\\:font-bold { + --tw-font-weight: var(--font-weight-bold); + font-weight: var(--font-weight-bold); + } + } +} + +@property --tw-font-weight { + syntax: "*"; + inherits: false +}" +`; diff --git a/packages/@tailwindcss-postcss/src/ast.test.ts b/packages/@tailwindcss-postcss/src/ast.test.ts new file mode 100644 index 000000000000..8a2a5b019f9f --- /dev/null +++ b/packages/@tailwindcss-postcss/src/ast.test.ts @@ -0,0 +1,107 @@ +import dedent from 'dedent' +import postcss from 'postcss' +import { expect, it } from 'vitest' +import { toCss } from '../../tailwindcss/src/ast' +import { parse } from '../../tailwindcss/src/css-parser' +import { cssAstToPostCssAst, postCssAstToCssAst } from './ast' + +let css = dedent + +it('should convert a PostCSS AST into a Tailwind CSS AST', () => { + let input = css` + @charset "UTF-8"; + + @layer foo, bar, baz; + + @import 'tailwindcss'; + + .foo { + color: red; + + &:hover { + color: blue; + } + + .bar { + color: green !important; + background-color: yellow; + + @media (min-width: 640px) { + color: orange; + } + } + } + ` + + let ast = postcss.parse(input) + let transformedAst = postCssAstToCssAst(ast) + + expect(toCss(transformedAst)).toMatchInlineSnapshot(` + "@charset "UTF-8"; + @layer foo, bar, baz; + @import 'tailwindcss'; + .foo { + color: red; + &:hover { + color: blue; + } + .bar { + color: green !important; + background-color: yellow; + @media (min-width: 640px) { + color: orange; + } + } + } + " + `) +}) + +it('should convert a Tailwind CSS AST into a PostCSS AST', () => { + let input = css` + @charset "UTF-8"; + + @layer foo, bar, baz; + + @import 'tailwindcss'; + + .foo { + color: red; + + &:hover { + color: blue; + } + + .bar { + color: green !important; + background-color: yellow; + + @media (min-width: 640px) { + color: orange; + } + } + } + ` + + let ast = parse(input) + let transformedAst = cssAstToPostCssAst(postcss, ast) + + expect(transformedAst.toString()).toMatchInlineSnapshot(` + "@charset "UTF-8"; + @layer foo, bar, baz; + @import 'tailwindcss'; + .foo { + color: red; + &:hover { + color: blue; + } + .bar { + color: green !important; + background-color: yellow; + @media (min-width: 640px) { + color: orange; + } + } + }" + `) +}) diff --git a/packages/@tailwindcss-postcss/src/ast.ts b/packages/@tailwindcss-postcss/src/ast.ts new file mode 100644 index 000000000000..7ca9e2addedf --- /dev/null +++ b/packages/@tailwindcss-postcss/src/ast.ts @@ -0,0 +1,189 @@ +import type * as postcss from 'postcss' +import { atRule, comment, decl, rule, type AstNode } from '../../tailwindcss/src/ast' +import { createLineTable, type LineTable } from '../../tailwindcss/src/source-maps/line-table' +import type { Source, SourceLocation } from '../../tailwindcss/src/source-maps/source' +import { DefaultMap } from '../../tailwindcss/src/utils/default-map' + +const EXCLAMATION_MARK = 0x21 + +export function cssAstToPostCssAst( + postcss: postcss.Postcss, + ast: AstNode[], + source?: postcss.Source, +): postcss.Root { + let inputMap = new DefaultMap((src) => { + return new postcss.Input(src.code, { + map: source?.input.map, + from: src.file ?? undefined, + }) + }) + + let lineTables = new DefaultMap((src) => createLineTable(src.code)) + + let root = postcss.root() + root.source = source + + function toSource(loc: SourceLocation | undefined): postcss.Source | undefined { + // Use the fallback if this node has no location info in the AST + if (!loc) return + if (!loc[0]) return + + let table = lineTables.get(loc[0]) + let start = table.find(loc[1]) + let end = table.find(loc[2]) + + return { + input: inputMap.get(loc[0]), + start: { + line: start.line, + column: start.column + 1, + offset: loc[1], + }, + end: { + line: end.line, + column: end.column + 1, + offset: loc[2], + }, + } + } + + function updateSource(astNode: postcss.ChildNode, loc: SourceLocation | undefined) { + let source = toSource(loc) + + // The `source` property on PostCSS nodes must be defined if present because + // `toJSON()` reads each property and tries to read from source.input if it + // sees a `source` property. This means for a missing or otherwise absent + // source it must be *missing* from the object rather than just `undefined` + if (source) { + astNode.source = source + } else { + delete astNode.source + } + } + + function transform(node: AstNode, parent: postcss.Container) { + // Declaration + if (node.kind === 'declaration') { + let astNode = postcss.decl({ + prop: node.property, + value: node.value ?? '', + important: node.important, + }) + updateSource(astNode, node.src) + parent.append(astNode) + } + + // Rule + else if (node.kind === 'rule') { + let astNode = postcss.rule({ selector: node.selector }) + updateSource(astNode, node.src) + astNode.raws.semicolon = true + parent.append(astNode) + for (let child of node.nodes) { + transform(child, astNode) + } + } + + // AtRule + else if (node.kind === 'at-rule') { + let astNode = postcss.atRule({ name: node.name.slice(1), params: node.params }) + updateSource(astNode, node.src) + astNode.raws.semicolon = true + parent.append(astNode) + for (let child of node.nodes) { + transform(child, astNode) + } + } + + // Comment + else if (node.kind === 'comment') { + let astNode = postcss.comment({ text: node.value }) + // Spaces are encoded in our node.value already, no need to add additional + // spaces. + astNode.raws.left = '' + astNode.raws.right = '' + updateSource(astNode, node.src) + parent.append(astNode) + } + + // AtRoot & Context should not happen + else if (node.kind === 'at-root' || node.kind === 'context') { + } + + // Unknown + else { + node satisfies never + } + } + + for (let node of ast) { + transform(node, root) + } + + return root +} + +export function postCssAstToCssAst(root: postcss.Root): AstNode[] { + let inputMap = new DefaultMap((input) => ({ + file: input.file ?? input.id ?? null, + code: input.css, + })) + + function toSource(node: postcss.ChildNode): SourceLocation | undefined { + let source = node.source + if (!source) return + + let input = source.input + if (!input) return + if (source.start === undefined) return + if (source.end === undefined) return + + return [inputMap.get(input), source.start.offset, source.end.offset] + } + + function transform( + node: postcss.ChildNode, + parent: Extract['nodes'], + ) { + // Declaration + if (node.type === 'decl') { + let astNode = decl(node.prop, node.value, node.important) + astNode.src = toSource(node) + parent.push(astNode) + } + + // Rule + else if (node.type === 'rule') { + let astNode = rule(node.selector) + astNode.src = toSource(node) + node.each((child) => transform(child, astNode.nodes)) + parent.push(astNode) + } + + // AtRule + else if (node.type === 'atrule') { + let astNode = atRule(`@${node.name}`, node.params) + astNode.src = toSource(node) + node.each((child) => transform(child, astNode.nodes)) + parent.push(astNode) + } + + // Comment + else if (node.type === 'comment') { + if (node.text.charCodeAt(0) !== EXCLAMATION_MARK) return + let astNode = comment(node.text) + astNode.src = toSource(node) + parent.push(astNode) + } + + // Unknown + else { + node satisfies never + } + } + + let ast: AstNode[] = [] + root.each((node) => transform(node, ast)) + + return ast +} diff --git a/packages/@tailwindcss-postcss/src/fixtures/example-project/index.html b/packages/@tailwindcss-postcss/src/fixtures/example-project/index.html new file mode 100644 index 000000000000..9d8328479188 --- /dev/null +++ b/packages/@tailwindcss-postcss/src/fixtures/example-project/index.html @@ -0,0 +1 @@ +
diff --git a/packages/@tailwindcss-postcss/src/fixtures/example-project/input.css b/packages/@tailwindcss-postcss/src/fixtures/example-project/input.css new file mode 100644 index 000000000000..f1af498fc1c6 --- /dev/null +++ b/packages/@tailwindcss-postcss/src/fixtures/example-project/input.css @@ -0,0 +1 @@ +/* the content for this file is set in the tests */ diff --git a/packages/@tailwindcss-postcss/src/fixtures/example-project/plugin.js b/packages/@tailwindcss-postcss/src/fixtures/example-project/plugin.js new file mode 100644 index 000000000000..0852126714ec --- /dev/null +++ b/packages/@tailwindcss-postcss/src/fixtures/example-project/plugin.js @@ -0,0 +1,4 @@ +module.exports = function ({ addVariant }) { + addVariant('inverted', '@media (inverted-colors: inverted)') + addVariant('hocus', ['&:focus', '&:hover']) +} diff --git a/packages/@tailwindcss-postcss/src/fixtures/example-project/src/index.js b/packages/@tailwindcss-postcss/src/fixtures/example-project/src/index.js new file mode 100644 index 000000000000..8258dcbd3cff --- /dev/null +++ b/packages/@tailwindcss-postcss/src/fixtures/example-project/src/index.js @@ -0,0 +1 @@ +const className = 'text-2xl text-black/50' diff --git a/packages/@tailwindcss-postcss/src/fixtures/example-project/src/relative-import.css b/packages/@tailwindcss-postcss/src/fixtures/example-project/src/relative-import.css new file mode 100644 index 000000000000..48a30ab4dca7 --- /dev/null +++ b/packages/@tailwindcss-postcss/src/fixtures/example-project/src/relative-import.css @@ -0,0 +1 @@ +@plugin '../plugin.js'; diff --git a/packages/@tailwindcss-postcss/src/index.cts b/packages/@tailwindcss-postcss/src/index.cts new file mode 100644 index 000000000000..abc7c2e25bf1 --- /dev/null +++ b/packages/@tailwindcss-postcss/src/index.cts @@ -0,0 +1,7 @@ +import tailwindcss from './index.ts' + +// This is used instead of `export default` to work around a bug in +// `postcss-load-config` + +// @ts-ignore +export = tailwindcss diff --git a/packages/@tailwindcss-postcss/src/index.test.ts b/packages/@tailwindcss-postcss/src/index.test.ts new file mode 100644 index 000000000000..ff12e09ef6af --- /dev/null +++ b/packages/@tailwindcss-postcss/src/index.test.ts @@ -0,0 +1,444 @@ +import dedent from 'dedent' +import { mkdir, mkdtemp, readFile, rm, unlink, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'path' +import postcss from 'postcss' +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' +import tailwindcss from './index' + +// We give this file path to PostCSS for processing. +// This file doesn't exist, but the path is used to resolve imports. +// We place it in packages/ because Vitest runs in the monorepo root, +// and packages/tailwindcss must be a sub-folder for +// @import 'tailwindcss' to work. +function inputCssFilePath() { + // Including the current test name to ensure that the cache is invalidated per + // test otherwise the cache will be used across tests. + return `${__dirname}/fixtures/example-project/input.css?test=${expect.getState().currentTestName}` +} + +const css = dedent + +test("`@import 'tailwindcss'` is replaced with the generated CSS", async () => { + let processor = postcss([ + tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), + ]) + + let result = await processor.process(`@import 'tailwindcss'`, { from: inputCssFilePath() }) + + expect(result.css.trim()).toMatchSnapshot() + + // Check for dependency messages + expect(result.messages).toContainEqual({ + type: 'dependency', + file: expect.stringMatching(/index.html$/g), + parent: expect.any(String), + plugin: expect.any(String), + }) + expect(result.messages).toContainEqual({ + type: 'dependency', + file: expect.stringMatching(/index.js$/g), + parent: expect.any(String), + plugin: expect.any(String), + }) + expect(result.messages).toContainEqual({ + type: 'dir-dependency', + dir: expect.stringMatching(/example-project[/|\\]src$/g), + glob: expect.stringMatching(/^\*\*\/\*/g), + parent: expect.any(String), + plugin: expect.any(String), + }) +}) + +test('output is optimized by Lightning CSS', async () => { + let processor = postcss([ + tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), + ]) + + let result = await processor.process( + css` + @layer utilities { + .foo { + @apply text-[black]; + } + } + + @layer utilities { + .bar { + color: red; + } + } + `, + { from: inputCssFilePath() }, + ) + + expect(result.css.trim()).toMatchInlineSnapshot(` + "@layer utilities { + .foo { + color: #000; + } + + .bar { + color: red; + } + }" + `) +}) + +test('@apply can be used without emitting the theme in the CSS file', async () => { + let processor = postcss([ + tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), + ]) + + let result = await processor.process( + css` + @reference 'tailwindcss/theme.css'; + .foo { + @apply text-red-500; + } + `, + { from: inputCssFilePath() }, + ) + + expect(result.css.trim()).toMatchInlineSnapshot(` + ".foo { + color: var(--color-red-500, oklch(63.7% .237 25.331)); + }" + `) +}) + +describe('processing without specifying a base path', () => { + let filepath: string + let dir: string + + beforeEach(async () => { + dir = await mkdtemp(path.join(tmpdir(), 'tw-postcss')) + await mkdir(dir, { recursive: true }) + filepath = path.join(dir, 'my-test-file.html') + await writeFile(filepath, `
`) + }) + afterEach(() => unlink(filepath)) + + test('the current working directory is used by default', async () => { + const spy = vi.spyOn(process, 'cwd') + spy.mockReturnValue(dir) + + let processor = postcss([tailwindcss({ optimize: { minify: false } })]) + + let result = await processor.process(`@import "tailwindcss"`, { from: inputCssFilePath() }) + + expect(result.css).toContain( + ".md\\:\\[\\&\\:hover\\]\\:content-\\[\\'testing_default_base_path\\'\\]", + ) + + expect(result.messages).toContainEqual({ + type: 'dependency', + file: expect.stringMatching(/my-test-file.html$/g), + parent: expect.any(String), + plugin: expect.any(String), + }) + }) +}) + +describe('plugins', () => { + test('local CJS plugin', async () => { + let processor = postcss([ + tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), + ]) + + let result = await processor.process( + css` + @import 'tailwindcss/utilities'; + @plugin './plugin.js'; + `, + { from: inputCssFilePath() }, + ) + + expect(result.css.trim()).toMatchInlineSnapshot(` + ".underline { + text-decoration-line: underline; + } + + @media (inverted-colors: inverted) { + .inverted\\:flex { + display: flex; + } + } + + .hocus\\:underline:focus, .hocus\\:underline:hover { + text-decoration-line: underline; + }" + `) + }) + + test('local CJS plugin from `@import`-ed file', async () => { + let processor = postcss([ + tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), + ]) + + let result = await processor.process( + css` + @import 'tailwindcss/utilities'; + @import '../example-project/src/relative-import.css'; + `, + { from: `${__dirname}/fixtures/another-project/input.css` }, + ) + + expect(result.css.trim()).toMatchInlineSnapshot(` + ".underline { + text-decoration-line: underline; + } + + @media (inverted-colors: inverted) { + .inverted\\:flex { + display: flex; + } + } + + .hocus\\:underline:focus, .hocus\\:underline:hover { + text-decoration-line: underline; + }" + `) + }) + + test('published CJS plugin', async () => { + let processor = postcss([ + tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), + ]) + + let result = await processor.process( + css` + @import 'tailwindcss/utilities'; + @plugin 'internal-example-plugin'; + `, + { from: inputCssFilePath() }, + ) + + expect(result.css.trim()).toMatchInlineSnapshot(` + ".underline { + text-decoration-line: underline; + } + + @media (inverted-colors: inverted) { + .inverted\\:flex { + display: flex; + } + } + + .hocus\\:underline:focus, .hocus\\:underline:hover { + text-decoration-line: underline; + }" + `) + }) +}) + +test('bail early when Tailwind is not used', async () => { + let processor = postcss([ + tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), + ]) + + let result = await processor.process( + css` + .custom-css { + color: red; + } + `, + { from: inputCssFilePath() }, + ) + + // `fixtures/example-project` includes an `underline` candidate. But since we + // didn't use `@tailwind utilities` we didn't scan for utilities. + expect(result.css).not.toContain('.underline {') + + expect(result.css.trim()).toMatchInlineSnapshot(` + ".custom-css { + color: red; + }" + `) +}) + +test('handle CSS when only using a `@reference` (we should not bail early)', async () => { + let processor = postcss([ + tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), + ]) + + let result = await processor.process( + css` + @reference "tailwindcss/theme.css"; + + .foo { + @variant md { + bar: baz; + } + } + `, + { from: inputCssFilePath() }, + ) + + expect(result.css.trim()).toMatchInlineSnapshot(` + "@media (min-width: 48rem) { + .foo { + bar: baz; + } + }" + `) +}) + +test('handle CSS when using a `@variant` using variants that do not rely on the `@theme`', async () => { + let processor = postcss([ + tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), + ]) + + let result = await processor.process( + css` + .foo { + @variant data-is-hoverable { + bar: baz; + } + } + `, + { from: inputCssFilePath() }, + ) + + expect(result.css.trim()).toMatchInlineSnapshot(` + ".foo[data-is-hoverable] { + bar: baz; + }" + `) +}) + +test('runs `Once` plugins in the right order', async () => { + let before = '' + let after = '' + let processor = postcss([ + { + postcssPlugin: 'before', + Once(root) { + before = root.toString() + }, + }, + tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), + { + postcssPlugin: 'after', + Once(root) { + after = root.toString() + }, + }, + ]) + + let result = await processor.process( + css` + @theme { + --color-red-500: red; + } + .custom-css { + color: theme(--color-red-500); + } + `, + { from: inputCssFilePath() }, + ) + + expect(result.css.trim()).toMatchInlineSnapshot(` + ".custom-css { + color: red; + }" + `) + expect(before).toMatchInlineSnapshot(` + "@theme { + --color-red-500: red; + } + .custom-css { + color: theme(--color-red-500); + }" + `) + expect(after).toMatchInlineSnapshot(` + ".custom-css { + color: red; + }" + `) +}) + +describe('concurrent builds', () => { + let dir: string + beforeEach(async () => { + dir = await mkdtemp(path.join(tmpdir(), 'tw-postcss')) + await writeFile(path.join(dir, 'index.html'), `
`) + await writeFile( + path.join(dir, 'index.css'), + css` + @import './dependency.css'; + `, + ) + await writeFile( + path.join(dir, 'dependency.css'), + css` + @tailwind utilities; + `, + ) + }) + afterEach(() => rm(dir, { recursive: true, force: true })) + + test('does experience a race-condition when calling the plugin two times for the same change', async () => { + const spy = vi.spyOn(process, 'cwd') + spy.mockReturnValue(dir) + + let from = path.join(dir, 'index.css') + let input = (await readFile(path.join(dir, 'index.css'))).toString() + + let plugin = tailwindcss({ optimize: { minify: false } }) + + async function run(input: string): Promise { + let ast = postcss.parse(input) + for (let runner of (plugin as any).plugins) { + if (runner.Once) { + await runner.Once(ast, { postcss, result: { opts: { from }, messages: [] } }) + } + } + return ast.toString() + } + + let result = await run(input) + + expect(result).toContain('.underline') + + // Ensure that the mtime is updated + await new Promise((resolve) => setTimeout(resolve, 100)) + await writeFile( + path.join(dir, 'dependency.css'), + css` + @tailwind utilities; + .red { + color: red; + } + `, + ) + + let promise1 = run(input) + let promise2 = run(input) + + expect(await promise1).toContain('.red') + expect(await promise2).toContain('.red') + }) +}) + +test('does not register the input file as a dependency, even if it is passed in as relative path', async () => { + let processor = postcss([ + tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), + ]) + + let result = await processor.process(`@tailwind utilities`, { from: './input.css' }) + + expect(result.css.trim()).toMatchInlineSnapshot(` + ".underline { + text-decoration-line: underline; + }" + `) + + // Check for dependency messages + expect(result.messages).not.toContainEqual({ + type: 'dependency', + file: expect.stringMatching(/input.css$/g), + parent: expect.any(String), + plugin: expect.any(String), + }) +}) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts new file mode 100644 index 000000000000..2ff4405b66b6 --- /dev/null +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -0,0 +1,365 @@ +import QuickLRU from '@alloc/quick-lru' +import { + compileAst, + env, + Features, + Instrumentation, + optimize as optimizeCss, + Polyfills, +} from '@tailwindcss/node' +import { clearRequireCache } from '@tailwindcss/node/require-cache' +import { Scanner } from '@tailwindcss/oxide' +import fs from 'node:fs' +import path, { relative } from 'node:path' +import type { AcceptedPlugin, PluginCreator, Postcss, Root } from 'postcss' +import { toCss, type AstNode } from '../../tailwindcss/src/ast' +import { cssAstToPostCssAst, postCssAstToCssAst } from './ast' +import fixRelativePathsPlugin from './postcss-fix-relative-paths' + +const DEBUG = env.DEBUG + +interface CacheEntry { + mtimes: Map + compiler: null | ReturnType + scanner: null | Scanner + tailwindCssAst: AstNode[] + cachedPostCssAst: Root + optimizedPostCssAst: Root + fullRebuildPaths: string[] +} +const cache = new QuickLRU({ maxSize: 50 }) + +function getContextFromCache(postcss: Postcss, inputFile: string, opts: PluginOptions): CacheEntry { + let key = `${inputFile}:${opts.base ?? ''}:${JSON.stringify(opts.optimize)}` + if (cache.has(key)) return cache.get(key)! + let entry = { + mtimes: new Map(), + compiler: null, + scanner: null, + + tailwindCssAst: [], + cachedPostCssAst: postcss.root(), + optimizedPostCssAst: postcss.root(), + + fullRebuildPaths: [] as string[], + } + cache.set(key, entry) + return entry +} + +export type PluginOptions = { + /** + * The base directory to scan for class candidates. + * + * Defaults to the current working directory. + */ + base?: string + + /** + * Optimize and minify the output CSS. + */ + optimize?: boolean | { minify?: boolean } + + /** + * Enable or disable asset URL rewriting. + * + * Defaults to `true`. + */ + transformAssetUrls?: boolean +} + +function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { + let base = opts.base ?? process.cwd() + let optimize = opts.optimize ?? process.env.NODE_ENV === 'production' + let shouldRewriteUrls = opts.transformAssetUrls ?? true + + return { + postcssPlugin: '@tailwindcss/postcss', + plugins: [ + // We need to handle the case where `postcss-import` might have run before + // the Tailwind CSS plugin is run. In this case, we need to manually fix + // relative paths before processing it in core. + fixRelativePathsPlugin(), + + { + postcssPlugin: 'tailwindcss', + async Once(root, { result, postcss }) { + using I = new Instrumentation() + + let inputFile = result.opts.from ?? '' + let isCSSModuleFile = inputFile.endsWith('.module.css') + + DEBUG && I.start(`[@tailwindcss/postcss] ${relative(base, inputFile)}`) + + // Bail out early if this is guaranteed to be a non-Tailwind CSS file. + { + DEBUG && I.start('Quick bail check') + let canBail = true + root.walkAtRules((node) => { + if ( + node.name === 'import' || + node.name === 'reference' || + node.name === 'theme' || + node.name === 'variant' || + node.name === 'config' || + node.name === 'plugin' || + node.name === 'apply' || + node.name === 'tailwind' + ) { + canBail = false + return false + } + }) + if (canBail) return + DEBUG && I.end('Quick bail check') + } + + let context = getContextFromCache(postcss, inputFile, opts) + let inputBasePath = path.dirname(path.resolve(inputFile)) + + // Whether this is the first build or not, if it is, then we can + // optimize the build by not creating the compiler until we need it. + let isInitialBuild = context.compiler === null + + async function createCompiler() { + DEBUG && I.start('Setup compiler') + if (context.fullRebuildPaths.length > 0 && !isInitialBuild) { + clearRequireCache(context.fullRebuildPaths) + } + + context.fullRebuildPaths = [] + + DEBUG && I.start('PostCSS AST -> Tailwind CSS AST') + let ast = postCssAstToCssAst(root) + DEBUG && I.end('PostCSS AST -> Tailwind CSS AST') + + DEBUG && I.start('Create compiler') + let compiler = await compileAst(ast, { + from: result.opts.from, + base: inputBasePath, + shouldRewriteUrls, + onDependency: (path) => context.fullRebuildPaths.push(path), + // In CSS Module files, we have to disable the `@property` polyfill since these will + // emit global `*` rules which are considered to be non-pure and will cause builds + // to fail. + polyfills: isCSSModuleFile ? Polyfills.All ^ Polyfills.AtProperty : Polyfills.All, + }) + DEBUG && I.end('Create compiler') + + DEBUG && I.end('Setup compiler') + return compiler + } + + try { + // Setup the compiler if it doesn't exist yet. This way we can + // guarantee a `build()` function is available. + context.compiler ??= createCompiler() + + if ((await context.compiler).features === Features.None) { + return + } + + let rebuildStrategy: 'full' | 'incremental' = 'incremental' + + // Track file modification times to CSS files + DEBUG && I.start('Register full rebuild paths') + { + for (let file of context.fullRebuildPaths) { + result.messages.push({ + type: 'dependency', + plugin: '@tailwindcss/postcss', + file: path.resolve(file), + parent: result.opts.from, + }) + } + + let files = result.messages.flatMap((message) => { + if (message.type !== 'dependency') return [] + return message.file + }) + files.push(inputFile) + + for (let file of files) { + let changedTime = fs.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? null + if (changedTime === null) { + if (file === inputFile) { + rebuildStrategy = 'full' + } + continue + } + + let prevTime = context.mtimes.get(file) + if (prevTime === changedTime) continue + + rebuildStrategy = 'full' + context.mtimes.set(file, changedTime) + } + } + DEBUG && I.end('Register full rebuild paths') + + if ( + rebuildStrategy === 'full' && + // We can re-use the compiler if it was created during the + // initial build. If it wasn't, we need to create a new one. + !isInitialBuild + ) { + context.compiler = createCompiler() + } + + let compiler = await context.compiler + + if (context.scanner === null || rebuildStrategy === 'full') { + DEBUG && I.start('Setup scanner') + let sources = (() => { + // Disable auto source detection + if (compiler.root === 'none') { + return [] + } + + // No root specified, use the base directory + if (compiler.root === null) { + return [{ base, pattern: '**/*', negated: false }] + } + + // Use the specified root + return [{ ...compiler.root, negated: false }] + })().concat(compiler.sources) + + // Look for candidates used to generate the CSS + context.scanner = new Scanner({ sources }) + DEBUG && I.end('Setup scanner') + } + + DEBUG && I.start('Scan for candidates') + let candidates = compiler.features & Features.Utilities ? context.scanner.scan() : [] + DEBUG && I.end('Scan for candidates') + + if (compiler.features & Features.Utilities) { + DEBUG && I.start('Register dependency messages') + // Add all found files as direct dependencies + // Note: With Turbopack, the input file might not be a resolved path + let resolvedInputFile = path.resolve(base, inputFile) + for (let file of context.scanner.files) { + let absolutePath = path.resolve(file) + // The CSS file cannot be a dependency of itself + if (absolutePath === resolvedInputFile) { + continue + } + result.messages.push({ + type: 'dependency', + plugin: '@tailwindcss/postcss', + file: absolutePath, + parent: result.opts.from, + }) + } + + // Register dependencies so changes in `base` cause a rebuild while + // giving tools like Vite or Parcel a glob that can be used to limit + // the files that cause a rebuild to only those that match it. + for (let { base: globBase, pattern } of context.scanner.globs) { + // Avoid adding a dependency on the base directory itself, since it + // causes Next.js to start an endless recursion if the `distDir` is + // configured to anything other than the default `.next` dir. + if (pattern === '*' && base === globBase) { + continue + } + + if (pattern === '') { + result.messages.push({ + type: 'dependency', + plugin: '@tailwindcss/postcss', + file: path.resolve(globBase), + parent: result.opts.from, + }) + } else { + result.messages.push({ + type: 'dir-dependency', + plugin: '@tailwindcss/postcss', + dir: path.resolve(globBase), + glob: pattern, + parent: result.opts.from, + }) + } + } + DEBUG && I.end('Register dependency messages') + } + + DEBUG && I.start('Build utilities') + let tailwindCssAst = compiler.build(candidates) + DEBUG && I.end('Build utilities') + + if (context.tailwindCssAst !== tailwindCssAst) { + if (optimize) { + DEBUG && I.start('Optimization') + + DEBUG && I.start('AST -> CSS') + let css = toCss(tailwindCssAst) + DEBUG && I.end('AST -> CSS') + + DEBUG && I.start('Lightning CSS') + let optimized = optimizeCss(css, { + minify: typeof optimize === 'object' ? optimize.minify : true, + }) + DEBUG && I.end('Lightning CSS') + + DEBUG && I.start('CSS -> PostCSS AST') + context.optimizedPostCssAst = postcss.parse(optimized.code, result.opts) + DEBUG && I.end('CSS -> PostCSS AST') + + DEBUG && I.end('Optimization') + } else { + // Convert our AST to a PostCSS AST + DEBUG && I.start('Transform Tailwind CSS AST into PostCSS AST') + context.cachedPostCssAst = cssAstToPostCssAst(postcss, tailwindCssAst, root.source) + DEBUG && I.end('Transform Tailwind CSS AST into PostCSS AST') + } + } + + context.tailwindCssAst = tailwindCssAst + + DEBUG && I.start('Update PostCSS AST') + root.removeAll() + root.append( + optimize + ? context.optimizedPostCssAst.clone().nodes + : context.cachedPostCssAst.clone().nodes, + ) + + // Trick PostCSS into thinking the indent is 2 spaces, so it uses that + // as the default instead of 4. + root.raws.indent = ' ' + DEBUG && I.end('Update PostCSS AST') + + DEBUG && I.end(`[@tailwindcss/postcss] ${relative(base, inputFile)}`) + } catch (error) { + // An error requires a full rebuild to fix + context.compiler = null + + // Ensure all dependencies we have collected thus far are included so that the rebuild + // is correctly triggered + for (let file of context.fullRebuildPaths) { + result.messages.push({ + type: 'dependency', + plugin: '@tailwindcss/postcss', + file: path.resolve(file), + parent: result.opts.from, + }) + } + + // We found that throwing the error will cause PostCSS to no longer watch for changes + // in some situations so we instead log the error and continue with an empty stylesheet. + console.error(error) + + if (error && typeof error === 'object' && 'message' in error) { + throw root.error(`${error.message}`) + } + + throw root.error(`${error}`) + } + }, + }, + ], + } +} + +export default Object.assign(tailwindcss, { postcss: true }) as PluginCreator diff --git a/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/example-project/src/index.css b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/example-project/src/index.css new file mode 100644 index 000000000000..b8c4fc5f1e12 --- /dev/null +++ b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/example-project/src/index.css @@ -0,0 +1,4 @@ +@source "./**/*.ts"; +@source "!./**/*.ts"; +@plugin "./plugin.js"; +@plugin "./what\"s-this.js"; diff --git a/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/example-project/src/invalid.css b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/example-project/src/invalid.css new file mode 100644 index 000000000000..9eeb9353e577 --- /dev/null +++ b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/example-project/src/invalid.css @@ -0,0 +1,4 @@ +@plugin "/absolute/paths"; +@plugin "C:\Program Files\HAL 9000"; +@plugin "\\Media\Pictures\Worth\1000 words"; +@plugin "some-node-dep"; diff --git a/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/index.css b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/index.css new file mode 100644 index 000000000000..2c014767b514 --- /dev/null +++ b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/index.css @@ -0,0 +1 @@ +@import '../../example-project/src/index.css'; diff --git a/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/invalid.css b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/invalid.css new file mode 100644 index 000000000000..b69d455c0db2 --- /dev/null +++ b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/invalid.css @@ -0,0 +1 @@ +@import '../../example-project/src/invalid.css'; diff --git a/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/plugins-in-root.css b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/plugins-in-root.css new file mode 100644 index 000000000000..d6d5f082c364 --- /dev/null +++ b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/plugins-in-root.css @@ -0,0 +1,5 @@ +@import './plugins-in-sibling.css'; + +@plugin './plugin-in-root.ts'; +@plugin '../plugin-in-root.ts'; +@plugin 'plugin-in-root'; diff --git a/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/plugins-in-sibling.css b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/plugins-in-sibling.css new file mode 100644 index 000000000000..5df3cb06176a --- /dev/null +++ b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/plugins-in-sibling.css @@ -0,0 +1,3 @@ +@plugin './plugin-in-sibling.ts'; +@plugin '../plugin-in-sibling.ts'; +@plugin 'plugin-in-sibling'; diff --git a/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.test.ts b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.test.ts new file mode 100644 index 000000000000..d2f72f66481f --- /dev/null +++ b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.test.ts @@ -0,0 +1,58 @@ +import fs from 'node:fs' +import path from 'node:path' +import postcss from 'postcss' +import atImport from 'postcss-import' +import { describe, expect, test } from 'vitest' +import fixRelativePathsPlugin from '.' + +describe('fixRelativePathsPlugin', () => { + test('rewrites @source and @plugin to be relative to the initial css file', async () => { + let cssPath = path.join(__dirname, 'fixtures', 'external-import', 'src', 'index.css') + let css = fs.readFileSync(cssPath, 'utf-8') + + let processor = postcss([atImport(), fixRelativePathsPlugin()]) + + let result = await processor.process(css, { from: cssPath }) + + expect(result.css.trim()).toMatchInlineSnapshot(` + "@source "../../example-project/src/**/*.ts"; + @source "!../../example-project/src/**/*.ts"; + @plugin "../../example-project/src/plugin.js"; + @plugin "../../example-project/src/what\\"s-this.js";" + `) + }) + + test('should not rewrite non-relative paths', async () => { + let cssPath = path.join(__dirname, 'fixtures', 'external-import', 'src', 'invalid.css') + let css = fs.readFileSync(cssPath, 'utf-8') + + let processor = postcss([atImport(), fixRelativePathsPlugin()]) + + let result = await processor.process(css, { from: cssPath }) + + expect(result.css.trim()).toMatchInlineSnapshot(` + "@plugin "/absolute/paths"; + @plugin "C:\\Program Files\\HAL 9000"; + @plugin "\\\\Media\\Pictures\\Worth\\1000 words"; + @plugin "some-node-dep";" + `) + }) + + test('should return relative paths even if the file is resolved in the same basedir as the root stylesheet', async () => { + let cssPath = path.join(__dirname, 'fixtures', 'external-import', 'src', 'plugins-in-root.css') + let css = fs.readFileSync(cssPath, 'utf-8') + + let processor = postcss([atImport(), fixRelativePathsPlugin()]) + + let result = await processor.process(css, { from: cssPath }) + + expect(result.css.trim()).toMatchInlineSnapshot(` + "@plugin './plugin-in-sibling.ts'; + @plugin '../plugin-in-sibling.ts'; + @plugin 'plugin-in-sibling'; + @plugin './plugin-in-root.ts'; + @plugin '../plugin-in-root.ts'; + @plugin 'plugin-in-root';" + `) + }) +}) diff --git a/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.ts b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.ts new file mode 100644 index 000000000000..68dcb5551b7d --- /dev/null +++ b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.ts @@ -0,0 +1,74 @@ +import { normalizePath } from '@tailwindcss/node' +import path from 'node:path' +import type { AtRule, Plugin } from 'postcss' + +const SINGLE_QUOTE = "'" +const DOUBLE_QUOTE = '"' + +export default function fixRelativePathsPlugin(): Plugin { + // Retain a list of touched at-rules to avoid infinite loops + let touched: WeakSet = new WeakSet() + + function fixRelativePath(atRule: AtRule) { + let rootPath = atRule.root().source?.input.file + if (!rootPath) { + return + } + + let inputFilePath = atRule.source?.input.file + if (!inputFilePath) { + return + } + + if (touched.has(atRule)) { + return + } + + let value = atRule.params[0] + + let quote = + value[0] === DOUBLE_QUOTE && value[value.length - 1] === DOUBLE_QUOTE + ? DOUBLE_QUOTE + : value[0] === SINGLE_QUOTE && value[value.length - 1] === SINGLE_QUOTE + ? SINGLE_QUOTE + : null + if (!quote) { + return + } + let glob = atRule.params.slice(1, -1) + + // Handle eventual negative rules. We only support one level of negation. + let negativePrefix = '' + if (glob.startsWith('!')) { + glob = glob.slice(1) + negativePrefix = '!' + } + + // We only want to rewrite relative paths. + if (!glob.startsWith('./') && !glob.startsWith('../')) { + return + } + + let absoluteGlob = path.posix.join(normalizePath(path.dirname(inputFilePath)), glob) + let absoluteRootPosixPath = path.posix.dirname(normalizePath(rootPath)) + + let relative = path.posix.relative(absoluteRootPosixPath, absoluteGlob) + + // If the path points to a file in the same directory, `path.relative` will + // remove the leading `./` and we need to add it back in order to still + // consider the path relative + if (!relative.startsWith('.')) { + relative = './' + relative + } + + atRule.params = quote + negativePrefix + relative + quote + touched.add(atRule) + } + + return { + postcssPlugin: 'tailwindcss-postcss-fix-relative-paths', + Once(root) { + root.walkAtRules(/source|plugin|config/, fixRelativePath) + }, + } +} diff --git a/packages/@tailwindcss-postcss/tsconfig.json b/packages/@tailwindcss-postcss/tsconfig.json new file mode 100644 index 000000000000..6ae022f65bf0 --- /dev/null +++ b/packages/@tailwindcss-postcss/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.base.json", +} diff --git a/packages/@tailwindcss-postcss/tsup.config.ts b/packages/@tailwindcss-postcss/tsup.config.ts new file mode 100644 index 000000000000..684c072ac854 --- /dev/null +++ b/packages/@tailwindcss-postcss/tsup.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'tsup' + +export default defineConfig([ + { + format: ['esm'], + minify: true, + cjsInterop: true, + dts: true, + entry: ['src/index.ts'], + }, + { + format: ['cjs'], + minify: true, + cjsInterop: true, + dts: true, + entry: ['src/index.cts'], + }, +]) diff --git a/packages/@tailwindcss-standalone/package.json b/packages/@tailwindcss-standalone/package.json new file mode 100644 index 000000000000..4372188d2c3e --- /dev/null +++ b/packages/@tailwindcss-standalone/package.json @@ -0,0 +1,55 @@ +{ + "name": "@tailwindcss/standalone", + "version": "4.2.2", + "private": true, + "description": "Standalone CLI for Tailwind CSS", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/tailwindlabs/tailwindcss.git", + "directory": "packages/@tailwindcss-standalone" + }, + "bugs": "https://github.com/tailwindlabs/tailwindcss/issues", + "homepage": "https://tailwindcss.com", + "scripts": { + "lint": "tsc --noEmit", + "build": "bun ./scripts/build.ts" + }, + "bin": { + "tailwindcss": "./dist/index.mjs" + }, + "exports": { + "./package.json": "./package.json" + }, + "files": [ + "dist" + ], + "dependencies": { + "@tailwindcss/aspect-ratio": "^0.4.2", + "@tailwindcss/cli": "workspace:*", + "@tailwindcss/forms": "^0.5.11", + "@tailwindcss/typography": "^0.5.19", + "detect-libc": "1.0.3", + "enhanced-resolve": "^5.19.0", + "tailwindcss": "workspace:*" + }, + "__notes": "These binary packages must be included so Bun can build the CLI for all supported platforms. We also rely on Lightning CSS and Parcel being patched so Bun can statically analyze the executables.", + "devDependencies": { + "@parcel/watcher-darwin-arm64": "^2.5.6", + "@parcel/watcher-darwin-x64": "^2.5.6", + "@parcel/watcher-linux-arm64-glibc": "^2.5.6", + "@parcel/watcher-linux-arm64-musl": "^2.5.6", + "@parcel/watcher-linux-x64-glibc": "^2.5.6", + "@parcel/watcher-linux-x64-musl": "^2.5.6", + "@parcel/watcher-win32-x64": "^2.5.6", + "@types/bun": "^1.3.9", + "bun": "^1.3.9", + "lightningcss-darwin-arm64": "catalog:", + "lightningcss-darwin-x64": "catalog:", + "lightningcss-linux-arm64-gnu": "catalog:", + "lightningcss-linux-arm64-musl": "catalog:", + "lightningcss-linux-x64-gnu": "catalog:", + "lightningcss-linux-x64-musl": "catalog:", + "lightningcss-win32-x64-msvc": "catalog:" + } +} diff --git a/packages/@tailwindcss-standalone/scripts/build.ts b/packages/@tailwindcss-standalone/scripts/build.ts new file mode 100644 index 000000000000..7ff572d3b0b6 --- /dev/null +++ b/packages/@tailwindcss-standalone/scripts/build.ts @@ -0,0 +1,114 @@ +import { createHash } from 'node:crypto' +import { mkdir, readFile, writeFile } from 'node:fs/promises' +import * as path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = fileURLToPath(new URL('.', import.meta.url)) + +// Workaround for Bun binary downloads failing on Windows CI when +// USERPROFILE is passed through by Turborepo. +// +// Unfortunately, setting this at runtime doesn't appear to work so we have to +// spawn a new process without the env var. +if (process.env.NESTED_BUILD !== '1' && process.env.USERPROFILE && process.env.USERPROFILE !== '') { + let result = await Bun.$`bun ${fileURLToPath(import.meta.url)}`.env({ + USERPROFILE: '', + NESTED_BUILD: '1', + }) + + process.exit(result.exitCode) +} + +// We use baseline builds for all x64 platforms to ensure compatibility with +// older hardware. +let builds: { target: Bun.Build.Target; name: string }[] = [ + { name: 'tailwindcss-linux-arm64', target: 'bun-linux-arm64' }, + { name: 'tailwindcss-linux-arm64-musl', target: 'bun-linux-arm64-musl' }, + // @ts-expect-error: Either the types are wrong or the runtime needs to be updated + // to accept a `-glibc` at the end like the types suggest. + { name: 'tailwindcss-linux-x64', target: 'bun-linux-x64-baseline' }, + { name: 'tailwindcss-linux-x64-musl', target: 'bun-linux-x64-baseline-musl' }, + { name: 'tailwindcss-macos-arm64', target: 'bun-darwin-arm64' }, + { name: 'tailwindcss-macos-x64', target: 'bun-darwin-x64-baseline' }, + { name: 'tailwindcss-windows-x64.exe', target: 'bun-windows-x64-baseline' }, +] + +let summary: { target: Bun.Build.Target; name: string; sum: string }[] = [] + +// Build platform binaries and checksum them. +let start = process.hrtime.bigint() +for (let { target, name } of builds) { + let outfile = path.resolve(__dirname, `../dist/${name}`) + + let result = await Bun.build({ + entrypoints: ['./src/index.ts'], + target: 'node', + minify: { + whitespace: false, + syntax: true, + identifiers: false, + keepNames: true, + }, + + define: { + // This ensures only necessary binaries are bundled for linux targets + // It reduces binary size since no runtime selection is necessary + 'process.env.PLATFORM_LIBC': JSON.stringify(target.includes('-musl') ? 'musl' : 'glibc'), + + // This prevents the WASI build from being bundled with the binary + 'process.env.NAPI_RS_FORCE_WASI': JSON.stringify(''), + + // This simplifies the Oxide loading code a small amount + 'process.env.NAPI_RS_NATIVE_LIBRARY_PATH': JSON.stringify(''), + + // No need to support additional NODE_PATHs in the standalone build + 'process.env.NODE_PATH': JSON.stringify(''), + }, + + compile: { + target, + outfile, + + // Disable .env loading + autoloadDotenv: false, + + // Disable bunfig.toml loading + autoloadBunfig: false, + }, + + plugins: [ + { + name: 'tailwindcss-plugin', + setup(build) { + build.onLoad({ filter: /tailwindcss-oxide\.wasi\.cjs$/ }, async (args) => { + return { contents: '' } + }) + }, + }, + ], + }) + + let entry = result.outputs.find((output) => output.kind === 'entry-point') + if (!entry) throw new Error(`Build failed for ${target}`) + + let content = await readFile(outfile) + + summary.push({ + target, + name, + sum: createHash('sha256').update(content).digest('hex'), + }) +} + +await mkdir(path.resolve(__dirname, '../dist'), { recursive: true }) + +// Write the checksums to a file +let sumsFile = path.resolve(__dirname, '../dist/sha256sums.txt') +let sums = summary.map(({ name, sum }) => `${sum} ./${name}`) + +await writeFile(sumsFile, sums.join('\n') + '\n') + +console.table(summary.map(({ target, sum }) => ({ target, sum }))) + +let elapsed = process.hrtime.bigint() - start +console.log(`Build completed in ${(Number(elapsed) / 1e6).toFixed(0)}ms`) diff --git a/packages/@tailwindcss-standalone/src/index.ts b/packages/@tailwindcss-standalone/src/index.ts new file mode 100644 index 000000000000..23a928f8b68b --- /dev/null +++ b/packages/@tailwindcss-standalone/src/index.ts @@ -0,0 +1,98 @@ +import { createRequire } from 'node:module' +import packageJson from 'tailwindcss/package.json' + +import indexCss from 'tailwindcss/index.css' with { type: 'file' } +import preflightCss from 'tailwindcss/preflight.css' with { type: 'file' } +import themeCss from 'tailwindcss/theme.css' with { type: 'file' } +import utilitiesCss from 'tailwindcss/utilities.css' with { type: 'file' } + +const localResolve = createRequire(import.meta.url).resolve + +globalThis.__tw_resolve = (id, baseDir) => { + let isEmbeddedFileBase = baseDir === '/$bunfs/root' || baseDir?.includes(':/~BUN/root') + const likelyEmbeddedFile = + id === 'tailwindcss' || + id.startsWith('tailwindcss/') || + id.startsWith('@tailwindcss/') || + isEmbeddedFileBase + + if (!likelyEmbeddedFile) { + return false + } + + id = id.startsWith('tailwindcss/') + ? id.slice(12) + : isEmbeddedFileBase && id.startsWith('./') + ? id.slice(2) + : id + + switch (id) { + case 'index': + case 'index.css': + case 'tailwindcss': + return localResolve(indexCss) + case 'theme': + case 'theme.css': + return localResolve(themeCss) + case 'preflight': + case 'preflight.css': + return localResolve(preflightCss) + case 'utilities': + case 'utilities.css': + return localResolve(utilitiesCss) + case '@tailwindcss/forms': + case '@tailwindcss/typography': + case '@tailwindcss/aspect-ratio': + return id + default: + return false + } +} +globalThis.__tw_load = async (id) => { + if (id.endsWith('@tailwindcss/forms')) { + return require('@tailwindcss/forms') + } else if (id.endsWith('@tailwindcss/typography')) { + return require('@tailwindcss/typography') + } else if (id.endsWith('@tailwindcss/aspect-ratio')) { + return require('@tailwindcss/aspect-ratio') + } else { + return undefined + } +} +globalThis.__tw_version = packageJson.version + +// We use a plugin to make sure that the JS APIs are bundled with the standalone +// CLI and can be imported inside configs and plugins +Bun.plugin({ + name: 'bundle-tailwindcss-apis', + target: 'bun', + async setup(build) { + // These imports must be static strings otherwise they won't be bundled + let bundled = { + tailwindcss: await import('tailwindcss'), + 'tailwindcss/colors': await import('tailwindcss/colors'), + 'tailwindcss/colors.js': await import('tailwindcss/colors'), + 'tailwindcss/plugin': await import('tailwindcss/plugin'), + 'tailwindcss/plugin.js': await import('tailwindcss/plugin'), + 'tailwindcss/package.json': await import('tailwindcss/package.json'), + 'tailwindcss/lib/util/flattenColorPalette': + await import('tailwindcss/lib/util/flattenColorPalette'), + 'tailwindcss/lib/util/flattenColorPalette.js': + await import('tailwindcss/lib/util/flattenColorPalette'), + 'tailwindcss/defaultTheme': await import('tailwindcss/defaultTheme'), + 'tailwindcss/defaultTheme.js': await import('tailwindcss/defaultTheme'), + } + + for (let [id, exports] of Object.entries(bundled)) { + build.module(id, () => ({ + loader: 'object', + exports: { + ...exports, + __esModule: true, + }, + })) + } + }, +}) + +await import('../../@tailwindcss-cli/src/index.ts') diff --git a/packages/@tailwindcss-standalone/src/types.d.ts b/packages/@tailwindcss-standalone/src/types.d.ts new file mode 100644 index 000000000000..3212ac5b66e8 --- /dev/null +++ b/packages/@tailwindcss-standalone/src/types.d.ts @@ -0,0 +1,11 @@ +declare module '*.css' { + const content: string + export default content +} + +declare var __tw_version: string | undefined +declare var __tw_resolve: undefined | ((id: string, base?: string) => string | false) +declare var __tw_readFile: + | undefined + | ((path: string, encoding: BufferEncoding) => Promise) +declare var __tw_load: undefined | ((path: string) => Promise) diff --git a/packages/@tailwindcss-standalone/tsconfig.json b/packages/@tailwindcss-standalone/tsconfig.json new file mode 100644 index 000000000000..6ae022f65bf0 --- /dev/null +++ b/packages/@tailwindcss-standalone/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.base.json", +} diff --git a/packages/@tailwindcss-upgrade/README.md b/packages/@tailwindcss-upgrade/README.md new file mode 100644 index 000000000000..5f532607d00a --- /dev/null +++ b/packages/@tailwindcss-upgrade/README.md @@ -0,0 +1,36 @@ +

+ + + + + Tailwind CSS + + +

+ +

+ A utility-first CSS framework for rapidly building custom user interfaces. +

+ +

+ Build Status + Total Downloads + Latest Release + License +

+ +--- + +## Documentation + +For full documentation, visit [tailwindcss.com](https://tailwindcss.com). + +## Community + +For help, discussion about best practices, or feature ideas: + +[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/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**. diff --git a/packages/@tailwindcss-upgrade/package.json b/packages/@tailwindcss-upgrade/package.json new file mode 100644 index 000000000000..8f6bbb127ef9 --- /dev/null +++ b/packages/@tailwindcss-upgrade/package.json @@ -0,0 +1,52 @@ +{ + "name": "@tailwindcss/upgrade", + "version": "4.2.2", + "description": "A utility-first CSS framework for rapidly building custom user interfaces.", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/tailwindlabs/tailwindcss.git", + "directory": "packages/@tailwindcss-cli" + }, + "bugs": "https://github.com/tailwindlabs/tailwindcss/issues", + "homepage": "https://tailwindcss.com", + "scripts": { + "lint": "tsc --noEmit", + "build": "tsup-node", + "dev": "pnpm run build -- --watch" + }, + "bin": "./dist/index.mjs", + "exports": { + "./package.json": "./package.json" + }, + "files": [ + "dist" + ], + "publishConfig": { + "provenance": true, + "access": "public" + }, + "dependencies": { + "@tailwindcss/node": "workspace:*", + "@tailwindcss/oxide": "workspace:*", + "dedent": "1.7.1", + "enhanced-resolve": "^5.19.0", + "globby": "^15.0.0", + "jiti": "^2.0.0-beta.3", + "mri": "^1.2.0", + "picocolors": "^1.1.1", + "postcss": "^8.5.6", + "postcss-import": "^16.1.1", + "postcss-selector-parser": "^7.1.1", + "prettier": "catalog:", + "semver": "^7.7.4", + "tailwindcss": "workspace:*", + "tree-sitter": "^0.22.4", + "tree-sitter-typescript": "^0.23.2" + }, + "devDependencies": { + "@types/node": "catalog:", + "@types/postcss-import": "^14.0.3", + "@types/semver": "^7.7.1" + } +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/config/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/codemods/config/migrate-js-config.ts new file mode 100644 index 000000000000..6a451333e098 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/config/migrate-js-config.ts @@ -0,0 +1,553 @@ +import { Scanner } from '@tailwindcss/oxide' +import fs from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { loadModule } from '../../../../@tailwindcss-node/src/compile' +import defaultTheme from '../../../../tailwindcss/dist/default-theme' +import { atRule, toCss, type AstNode } from '../../../../tailwindcss/src/ast' +import { + keyPathToCssProperty, + themeableValues, +} from '../../../../tailwindcss/src/compat/apply-config-to-theme' +import { keyframesToRules } from '../../../../tailwindcss/src/compat/apply-keyframes-to-theme' +import { + resolveConfig, + type ConfigFile, +} from '../../../../tailwindcss/src/compat/config/resolve-config' +import type { ResolvedConfig, ThemeConfig } from '../../../../tailwindcss/src/compat/config/types' +import { buildCustomContainerUtilityRules } from '../../../../tailwindcss/src/compat/container' +import { darkModePlugin } from '../../../../tailwindcss/src/compat/dark-mode' +import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' +import type { DesignSystem } from '../../../../tailwindcss/src/design-system' +import { escape } from '../../../../tailwindcss/src/utils/escape' +import { + isValidOpacityValue, + isValidSpacingMultiplier, +} from '../../../../tailwindcss/src/utils/infer-data-type' +import * as ValueParser from '../../../../tailwindcss/src/value-parser' +import { findStaticPlugins, type StaticPluginOptions } from '../../utils/extract-static-plugins' +import { highlight, info, relative } from '../../utils/renderer' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +export type JSConfigMigration = + // Could not convert the config file, need to inject it as-is in a @config directive + null | { + sources: { base: string; pattern: string }[] + plugins: { base: string; path: string; options: null | StaticPluginOptions }[] + css: string + } + +export async function migrateJsConfig( + designSystem: DesignSystem, + fullConfigPath: string, + base: string, +): Promise { + let [unresolvedConfig, source] = await Promise.all([ + loadModule(fullConfigPath, __dirname, () => {}).then((result) => result.module) as Config, + fs.readFile(fullConfigPath, 'utf-8'), + ]) + + if (!canMigrateConfig(unresolvedConfig, source)) { + info( + `The configuration file at ${highlight(relative(fullConfigPath, base))} could not be automatically migrated to the new CSS configuration format, so your CSS has been updated to load your existing configuration file.`, + { prefix: '↳ ' }, + ) + return null + } + + let sources: { base: string; pattern: string }[] = [] + let plugins: { base: string; path: string; options: null | StaticPluginOptions }[] = [] + let cssConfigs: string[] = [] + + if ('darkMode' in unresolvedConfig) { + cssConfigs.push(migrateDarkMode(unresolvedConfig as any)) + } + + if ('content' in unresolvedConfig) { + sources = await migrateContent(unresolvedConfig as any, fullConfigPath, base) + } + + if ('theme' in unresolvedConfig) { + let themeConfig = await migrateTheme(designSystem, unresolvedConfig, base) + if (themeConfig) cssConfigs.push(themeConfig) + } + + if ('corePlugins' in unresolvedConfig) { + info( + `The \`corePlugins\` option is no longer supported as of Tailwind CSS v4.0, so it's been removed from your configuration.`, + ) + } + + let simplePlugins = findStaticPlugins(source) + if (simplePlugins !== null) { + for (let [path, options] of simplePlugins) { + plugins.push({ base, path, options }) + } + } + + return { + sources, + plugins, + css: cssConfigs.join('\n'), + } +} + +async function migrateTheme( + designSystem: DesignSystem, + unresolvedConfig: Config, + base: string, +): Promise { + // Resolve the config file without applying plugins and presets, as these are + // migrated to CSS separately. + let configToResolve: ConfigFile = { + base, + config: { ...unresolvedConfig, plugins: [], presets: undefined }, + reference: false, + src: undefined, + } + let { resolvedConfig, replacedThemeKeys } = resolveConfig(designSystem, [configToResolve]) + + let resetNamespaces = new Map( + Array.from(replacedThemeKeys.entries()).map(([key]) => [key, false]), + ) + + removeUnnecessarySpacingKeys(designSystem, resolvedConfig, replacedThemeKeys) + + let css = '' + let prevSectionKey = '' + let themeSection: string[] = [] + let keyframesCss = '' + let variants = new Map() + + // Special handling of specific theme keys: + { + if ('keyframes' in resolvedConfig.theme) { + keyframesCss += keyframesToCss(resolvedConfig.theme.keyframes) + delete resolvedConfig.theme.keyframes + } + + if ('container' in resolvedConfig.theme) { + let rules = buildCustomContainerUtilityRules(resolvedConfig.theme.container, designSystem) + if (rules.length > 0) { + // Using `theme` instead of `utility` so it sits before the `@layer + // base` with compatibility CSS. While this is technically a utility, it + // makes a bit more sense to emit this closer to the `@theme` values + // since it is needed for backwards compatibility. + css += `\n@tw-bucket theme {\n` + css += toCss([atRule('@utility', 'container', rules)]) + css += '}\n' // @tw-bucket + } + delete resolvedConfig.theme.container + } + + if ('aria' in resolvedConfig.theme) { + for (let [key, value] of Object.entries(resolvedConfig.theme.aria ?? {})) { + // Will be handled by bare values if the names match. + // E.g.: `aria-foo:flex` should produce `[aria-foo="true"]` + if (new RegExp(`^${key}=(['"]?)true\\1$`).test(`${value}`)) continue + + // Create custom variant + variants.set(`aria-${key}`, `&[aria-${value}]`) + } + delete resolvedConfig.theme.aria + } + + if ('data' in resolvedConfig.theme) { + for (let [key, value] of Object.entries(resolvedConfig.theme.data ?? {})) { + // Will be handled by bare values if the names match. + // E.g.: `data-foo:flex` should produce `[data-foo]` + if (key === value) continue + + // Create custom variant + variants.set(`data-${key}`, `&[data-${value}]`) + } + delete resolvedConfig.theme.data + } + + if ('supports' in resolvedConfig.theme) { + for (let [key, value] of Object.entries(resolvedConfig.theme.supports ?? {})) { + // Will be handled by bare values if the value of the declaration is a + // CSS variable. + let parsed = ValueParser.parse(`${value}`) + + // Unwrap the parens, e.g.: `(foo: var(--bar))` → `foo: var(--bar)` + if (parsed.length === 1 && parsed[0].kind === 'function' && parsed[0].value === '') { + parsed = parsed[0].nodes + } + + // Verify structure: `foo: var(--bar)` + // ^^^ ← must match the `key` + if ( + parsed.length === 3 && + parsed[0].kind === 'word' && + parsed[0].value === key && + parsed[2].kind === 'function' && + parsed[2].value === 'var' + ) { + continue + } + + // Create custom variant + variants.set(`supports-${key}`, `{@supports(${value}){@slot;}}`) + } + delete resolvedConfig.theme.supports + } + } + + // Convert theme values to CSS custom properties + for (let [key, value] of themeableValues(resolvedConfig.theme)) { + if (typeof value !== 'string' && typeof value !== 'number') { + continue + } + + if (typeof value === 'string') { + // This is more advanced than the version in core as ideally something + // like `rgba(0 0 0 / )` becomes `rgba(0 0 0)`. Since we know + // from the `/` that it's used in an alpha channel and we can remove it. + // + // In other cases we may not know exactly how its used, so we'll just + // replace it with `1` like core does. + value = value.replace(/\s*\/\s*/, '').replace(//, '1') + } + + // Convert `opacity` namespace from decimal to percentage values. + // Additionally we can drop values that resolve to the same value as the + // named modifier with the same name. + if (key[0] === 'opacity' && (typeof value === 'number' || typeof value === 'string')) { + let numValue = typeof value === 'string' ? parseFloat(value) : value + + if (numValue >= 0 && numValue <= 1) { + value = numValue * 100 + '%' + } + + if ( + typeof value === 'string' && + key[1] === value.replace(/%$/, '') && + isValidOpacityValue(key[1]) + ) { + continue + } + } + + let sectionKey = createSectionKey(key) + if (sectionKey !== prevSectionKey) { + themeSection.push('') + prevSectionKey = sectionKey + } + + let property = keyPathToCssProperty(key) + + if (property !== null) { + if ( + !property.startsWith('default-') && + resetNamespaces.has(key[0]) && + resetNamespaces.get(key[0]) === false + ) { + resetNamespaces.set(key[0], true) + let ns = keyPathToCssProperty([key[0]]) + if (ns !== null) { + themeSection.push(` ${escape(`--${ns}`)}-*: initial;`) + } + } + + themeSection.push(` ${escape(`--${property}`)}: ${value};`) + } + } + + if (keyframesCss) { + themeSection.push('', keyframesCss) + } + + if (themeSection.length > 0) { + css += `\n@tw-bucket theme {\n` + css += `\n@theme {\n` + css += themeSection.join('\n') + '\n' + css += '}\n' // @theme + css += '}\n' // @tw-bucket + } + + if (variants.size > 0) { + css += '\n@tw-bucket custom-variant {\n' + + let previousRoot = '' + for (let [name, selector] of variants) { + let root = name.split('-')[0] + if (previousRoot !== root) css += '\n' + previousRoot = root + + if (selector.startsWith('{')) { + css += `@custom-variant ${name} ${selector}\n` + } else { + css += `@custom-variant ${name} (${selector});\n` + } + } + css += '}\n' + } + + return css +} + +function migrateDarkMode(unresolvedConfig: Config & { darkMode: any }): string { + let variant: string | string[] = '' + let addVariant = (_name: string, _variant: string) => (variant = _variant) + let config = () => unresolvedConfig.darkMode + darkModePlugin({ config, addVariant }) + + if (variant === '') { + return '' + } + + if (!Array.isArray(variant)) { + variant = [variant] + } + + if (variant.length === 1 && !variant[0].includes('{')) { + return `\n@tw-bucket custom-variant {\n@custom-variant dark (${variant[0]});\n}\n` + } + + let customVariant = '' + for (let variantName of variant) { + // Convert to the block syntax if a block is used + if (variantName.includes('{')) { + customVariant += variantName.replace('}', '{ @slot }}') + '\n' + } else { + customVariant += variantName + '{ @slot }\n' + } + } + + if (customVariant !== '') { + return `\n@tw-bucket custom-variant {\n@custom-variant dark {${customVariant}};\n}\n` + } + + return '' +} + +// Returns a string identifier used to section theme declarations +function createSectionKey(key: string[]): string { + let sectionSegments = [] + for (let i = 0; i < key.length - 1; i++) { + let segment = key[i] + // Ignore tuples + if (key[i + 1][0] === '-') { + break + } + sectionSegments.push(segment) + } + return sectionSegments.join('-') +} + +async function migrateContent( + unresolvedConfig: Config, + configPath: string, + base: string, +): Promise<{ base: string; pattern: string }[]> { + let autoContentFiles = autodetectedSourceFiles(base) + + let sources = [] + let contentIsRelative = (() => { + if (!unresolvedConfig.content) return false + if (Array.isArray(unresolvedConfig.content)) return false + if (unresolvedConfig.content.relative) return true + if (unresolvedConfig.future === 'all') return false + return unresolvedConfig.future?.relativeContentPathsByDefault ?? false + })() + + let sourceGlobs = Array.isArray(unresolvedConfig.content) + ? unresolvedConfig.content.map((pattern) => ({ base, pattern })) + : (unresolvedConfig.content?.files ?? []).map((pattern) => { + if (typeof pattern === 'string' && contentIsRelative) { + return { base: path.dirname(configPath), pattern: pattern } + } + return { base, pattern } + }) + + for (let { base, pattern } of sourceGlobs) { + if (typeof pattern !== 'string') { + throw new Error('Unsupported content value: ' + pattern) + } + + let sourceFiles = patternSourceFiles({ + base, + pattern: pattern[0] === '!' ? pattern.slice(1) : pattern, + negated: pattern[0] === '!', + }) + + let autoContentContainsAllSourceFiles = true + for (let sourceFile of sourceFiles) { + if (!autoContentFiles.includes(sourceFile)) { + autoContentContainsAllSourceFiles = false + break + } + } + + if (!autoContentContainsAllSourceFiles) { + sources.push({ base, pattern }) + } + } + return sources +} + +// Applies heuristics to determine if we can attempt to migrate the config +function canMigrateConfig(unresolvedConfig: Config, source: string): boolean { + // The file may not contain non-serializable values + function isSimpleValue(value: unknown): boolean { + if (typeof value === 'function') return false + if (Array.isArray(value)) return value.every(isSimpleValue) + if (typeof value === 'object' && value !== null) { + return Object.values(value).every(isSimpleValue) + } + return ['string', 'number', 'boolean', 'undefined'].includes(typeof value) + } + + // `theme` and `plugins` are handled separately and allowed to be more complex + let { plugins, theme, ...remainder } = unresolvedConfig + if (!isSimpleValue(remainder)) { + return false + } + + // The file may only contain known-migratable top-level properties + let knownProperties = [ + 'darkMode', + 'content', + 'theme', + 'plugins', + 'presets', + 'prefix', // Prefix is handled in the dedicated prefix migrator + 'corePlugins', + 'future', + 'experimental', + ] + + if (Object.keys(unresolvedConfig).some((key) => !knownProperties.includes(key))) { + return false + } + + if (findStaticPlugins(source) === null) { + return false + } + + if (unresolvedConfig.presets && unresolvedConfig.presets.length > 0) { + return false + } + + // If there are unknown "future" flags we should bail + if (unresolvedConfig.future && unresolvedConfig.future !== 'all') { + let knownFlags = [ + 'hoverOnlyWhenSupported', + 'respectDefaultRingColorOpacity', + 'disableColorOpacityUtilitiesByDefault', + 'relativeContentPathsByDefault', + ] + + if (Object.keys(unresolvedConfig.future).some((key) => !knownFlags.includes(key))) { + return false + } + } + + // If there are unknown "experimental" flags we should bail + if (unresolvedConfig.experimental && unresolvedConfig.experimental !== 'all') { + let knownFlags = ['generalizedModifiers'] + + if (Object.keys(unresolvedConfig.experimental).some((key) => !knownFlags.includes(key))) { + return false + } + } + + // Only migrate the config file if all top-level theme keys are allowed to be + // migrated + if (theme && typeof theme === 'object') { + if (theme.extend && !onlyAllowedThemeValues(theme.extend)) return false + let { extend: _extend, ...themeCopy } = theme + if (!onlyAllowedThemeValues(themeCopy)) return false + } + + return true +} + +const ALLOWED_THEME_KEYS = [ + ...Object.keys(defaultTheme), + // Used by @tailwindcss/container-queries + 'containers', +] +function onlyAllowedThemeValues(theme: ThemeConfig): boolean { + for (let key of Object.keys(theme)) { + if (!ALLOWED_THEME_KEYS.includes(key)) { + return false + } + } + + if ('screens' in theme && typeof theme.screens === 'object' && theme.screens !== null) { + for (let screen of Object.values(theme.screens)) { + if (typeof screen === 'object' && screen !== null && ('max' in screen || 'raw' in screen)) { + return false + } + } + } + return true +} + +function keyframesToCss(keyframes: Record): string { + let ast: AstNode[] = keyframesToRules({ theme: { keyframes } }) + return toCss(ast).trim() + '\n' +} + +function autodetectedSourceFiles(base: string) { + let scanner = new Scanner({ + sources: [ + { + base, + pattern: '**/*', + negated: false, + }, + ], + }) + scanner.scan() + return scanner.files +} + +function patternSourceFiles(source: { base: string; pattern: string; negated: boolean }): string[] { + let scanner = new Scanner({ sources: [source] }) + scanner.scan() + return scanner.files +} + +function removeUnnecessarySpacingKeys( + designSystem: DesignSystem, + resolvedConfig: ResolvedConfig, + replacedThemeKeys: Set, +) { + // We want to keep the spacing scale as-is if the user is overwriting + if (replacedThemeKeys.has('spacing')) return + + // Ensure we have a spacing multiplier + let spacingScale = designSystem.theme.get(['--spacing']) + if (!spacingScale) return + + let [spacingMultiplier, spacingUnit] = splitNumberAndUnit(spacingScale) + if (!spacingMultiplier || !spacingUnit) return + + if (spacingScale && !replacedThemeKeys.has('spacing')) { + for (let [key, value] of Object.entries(resolvedConfig.theme.spacing ?? {})) { + let [multiplier, unit] = splitNumberAndUnit(value as string) + if (multiplier === null) continue + + if (!isValidSpacingMultiplier(key)) continue + if (unit !== spacingUnit) continue + + if (parseFloat(multiplier) === Number(key) * parseFloat(spacingMultiplier)) { + delete resolvedConfig.theme.spacing[key] + designSystem.theme.clearNamespace(escape(`--spacing-${key.replaceAll('.', '_')}`), 0) + } + } + } +} + +function splitNumberAndUnit(value: string): [string, string] | [null, null] { + let match = value.match(/^([0-9.]+)(.*)$/) + if (!match) { + return [null, null] + } + return [match[1], match[2]] +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/config/migrate-postcss.ts b/packages/@tailwindcss-upgrade/src/codemods/config/migrate-postcss.ts new file mode 100644 index 000000000000..512f3df59612 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/config/migrate-postcss.ts @@ -0,0 +1,348 @@ +import fs from 'node:fs/promises' +import path from 'node:path' +import { pkg } from '../../utils/packages' +import { highlight, info, relative, success, warn } from '../../utils/renderer' + +// Migrates simple PostCSS setups. This is to cover non-dynamic config files +// similar to the ones we have all over our docs: +// +// ```js +// module.exports = { +// plugins: { +// 'postcss-import': {}, +// 'tailwindcss/nesting': 'postcss-nesting', +// tailwindcss: {}, +// autoprefixer: {}, +// } +// } +// ``` +export async function migratePostCSSConfig(base: string) { + let ranMigration = false + let didMigrate = false + let didAddPostcssClient = false + let didRemoveAutoprefixer = false + let didRemovePostCSSImport = false + + let packageJsonPath = path.resolve(base, 'package.json') + let packageJson + try { + packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8')) + } catch {} + + // Priority 1: Handle JS config files + let jsConfigPath = await detectJSConfigPath(base) + if (jsConfigPath) { + let result = await migratePostCSSJSConfig(jsConfigPath) + ranMigration = true + + if (result) { + didMigrate = true + didAddPostcssClient = result.didAddPostcssClient + didRemoveAutoprefixer = result.didRemoveAutoprefixer + didRemovePostCSSImport = result.didRemovePostCSSImport + } + } + + // Priority 2: Handle package.json config + if (!ranMigration) { + if (packageJson && 'postcss' in packageJson) { + let result = await migratePostCSSJsonConfig(packageJson.postcss) + ranMigration = true + + if (result) { + await fs.writeFile( + packageJsonPath, + JSON.stringify({ ...packageJson, postcss: result?.json }, null, 2), + ) + + didMigrate = true + didAddPostcssClient = result.didAddPostcssClient + didRemoveAutoprefixer = result.didRemoveAutoprefixer + didRemovePostCSSImport = result.didRemovePostCSSImport + } + } + } + + // Priority 3: JSON based postcss config files + if (!ranMigration) { + let jsonConfigPath = await detectJSONConfigPath(base) + let jsonConfig: null | any = null + if (jsonConfigPath) { + try { + jsonConfig = JSON.parse(await fs.readFile(jsonConfigPath, 'utf-8')) + } catch {} + if (jsonConfig) { + let result = await migratePostCSSJsonConfig(jsonConfig) + ranMigration = true + + if (result) { + await fs.writeFile(jsonConfigPath, JSON.stringify(result.json, null, 2)) + + didMigrate = true + didAddPostcssClient = result.didAddPostcssClient + didRemoveAutoprefixer = result.didRemoveAutoprefixer + didRemovePostCSSImport = result.didRemovePostCSSImport + } + } + } + } + + if (!ranMigration) { + info('No PostCSS config found, skipping migration.', { + prefix: '↳ ', + }) + return + } + + if (didAddPostcssClient) { + let location = Object.hasOwn(packageJson?.dependencies ?? {}, 'tailwindcss') + ? ('dependencies' as const) + : Object.hasOwn(packageJson?.devDependencies ?? {}, 'tailwindcss') + ? ('devDependencies' as const) + : null + + if (location !== null) { + try { + await pkg(base).add(['@tailwindcss/postcss@latest'], location) + success(`Installed package: ${highlight('@tailwindcss/postcss')}`, { prefix: '↳ ' }) + } catch {} + } + } + + if (didRemoveAutoprefixer) { + try { + await pkg(base).remove(['autoprefixer']) + success(`Removed package: ${highlight('autoprefixer')}`, { prefix: '↳ ' }) + } catch {} + } + + if (didRemovePostCSSImport) { + try { + await pkg(base).remove(['postcss-import']) + success(`Removed package: ${highlight('postcss-import')}`, { prefix: '↳ ' }) + } catch {} + } + + if (didMigrate && jsConfigPath) { + success(`Migrated PostCSS configuration: ${highlight(relative(jsConfigPath, base))}`, { + prefix: '↳ ', + }) + } +} + +async function migratePostCSSJSConfig(configPath: string): Promise<{ + didAddPostcssClient: boolean + didRemoveAutoprefixer: boolean + didRemovePostCSSImport: boolean +} | null> { + function isTailwindCSSPlugin(line: string) { + return /['"]?tailwindcss['"]?: ?\{\}/.test(line) + } + function isPostCSSImportPlugin(line: string) { + return /['"]?postcss-import['"]?: ?\{\}/.test(line) + } + function isAutoprefixerPlugin(line: string) { + return /['"]?autoprefixer['"]?: ?\{\}/.test(line) + } + function isTailwindCSSNestingPlugin(line: string) { + return /['"]tailwindcss\/nesting['"]: ?(\{\}|['"]postcss-nesting['"])/.test(line) + } + + info('Migrating PostCSS configuration…') + + let isSimpleConfig = await isSimplePostCSSConfig(configPath) + if (!isSimpleConfig) { + warn('The PostCSS config contains dynamic JavaScript and can not be automatically migrated.', { + prefix: '↳ ', + }) + return null + } + + let didAddPostcssClient = false + let didRemoveAutoprefixer = false + let didRemovePostCSSImport = false + + let content = await fs.readFile(configPath, 'utf-8') + let lines = content.split('\n') + let newLines: string[] = [] + for (let i = 0; i < lines.length; i++) { + let line = lines[i] + + if (isTailwindCSSPlugin(line)) { + didAddPostcssClient = true + newLines.push(line.replace('tailwindcss:', `'@tailwindcss/postcss':`)) + } else if (isAutoprefixerPlugin(line)) { + didRemoveAutoprefixer = true + } else if (isPostCSSImportPlugin(line)) { + // Check that there are no unknown plugins before the tailwindcss plugin + let hasUnknownPluginsBeforeTailwindCSS = false + for (let j = i + 1; j < lines.length; j++) { + let nextLine = lines[j] + if (isTailwindCSSPlugin(nextLine)) { + break + } + if (isTailwindCSSNestingPlugin(nextLine)) { + continue + } + hasUnknownPluginsBeforeTailwindCSS = true + break + } + + if (!hasUnknownPluginsBeforeTailwindCSS) { + didRemovePostCSSImport = true + } else { + newLines.push(line) + } + } else if (isTailwindCSSNestingPlugin(line)) { + // Check if the following rule is the tailwindcss plugin + let nextLine = lines[i + 1] + if (isTailwindCSSPlugin(nextLine)) { + // Since this plugin is bundled with `tailwindcss`, we don't need to + // clean up a package when deleting this line. + } else { + newLines.push(line) + } + } else { + newLines.push(line) + } + } + await fs.writeFile(configPath, newLines.join('\n')) + + return { didAddPostcssClient, didRemoveAutoprefixer, didRemovePostCSSImport } +} + +async function migratePostCSSJsonConfig(json: any): Promise<{ + json: any + didAddPostcssClient: boolean + didRemoveAutoprefixer: boolean + didRemovePostCSSImport: boolean +} | null> { + function isTailwindCSSPlugin(plugin: string, options: any) { + return plugin === 'tailwindcss' && isEmptyObject(options) + } + function isPostCSSImportPlugin(plugin: string, options: any) { + return plugin === 'postcss-import' && isEmptyObject(options) + } + function isAutoprefixerPlugin(plugin: string, options: any) { + return plugin === 'autoprefixer' && isEmptyObject(options) + } + function isTailwindCSSNestingPlugin(plugin: string, options: any) { + return ( + plugin === 'tailwindcss/nesting' && (options === 'postcss-nesting' || isEmptyObject(options)) + ) + } + + let didAddPostcssClient = false + let didRemoveAutoprefixer = false + let didRemovePostCSSImport = false + + let plugins = Object.entries(json.plugins || {}) + + let newPlugins: [string, any][] = [] + for (let i = 0; i < plugins.length; i++) { + let [plugin, options] = plugins[i] + + if (isTailwindCSSPlugin(plugin, options)) { + didAddPostcssClient = true + newPlugins.push(['@tailwindcss/postcss', options]) + } else if (isAutoprefixerPlugin(plugin, options)) { + didRemoveAutoprefixer = true + } else if (isPostCSSImportPlugin(plugin, options)) { + // Check that there are no unknown plugins before the tailwindcss plugin + let hasUnknownPluginsBeforeTailwindCSS = false + for (let j = i + 1; j < plugins.length; j++) { + let [nextPlugin, nextOptions] = plugins[j] + if (isTailwindCSSPlugin(nextPlugin, nextOptions)) { + break + } + if (isTailwindCSSNestingPlugin(nextPlugin, nextOptions)) { + continue + } + hasUnknownPluginsBeforeTailwindCSS = true + break + } + + if (!hasUnknownPluginsBeforeTailwindCSS) { + didRemovePostCSSImport = true + } else { + newPlugins.push([plugin, options]) + } + } else if (isTailwindCSSNestingPlugin(plugin, options)) { + // Check if the following rule is the tailwindcss plugin + let [nextPlugin, nextOptions] = plugins[i + 1] + if (isTailwindCSSPlugin(nextPlugin, nextOptions)) { + // Since this plugin is bundled with `tailwindcss`, we don't need to + // clean up a package when deleting this line. + } else { + newPlugins.push([plugin, options]) + } + } else { + newPlugins.push([plugin, options]) + } + } + + return { + json: { ...json, plugins: Object.fromEntries(newPlugins) }, + didAddPostcssClient, + didRemoveAutoprefixer, + didRemovePostCSSImport, + } +} + +const JS_CONFIG_FILE_LOCATIONS = [ + '.postcssrc.js', + '.postcssrc.mjs', + '.postcssrc.cjs', + '.postcssrc.ts', + '.postcssrc.mts', + '.postcssrc.cts', + 'postcss.config.js', + 'postcss.config.mjs', + 'postcss.config.cjs', + 'postcss.config.ts', + 'postcss.config.mts', + 'postcss.config.cts', +] +async function detectJSConfigPath(base: string): Promise { + for (let file of JS_CONFIG_FILE_LOCATIONS) { + let fullPath = path.resolve(base, file) + try { + await fs.access(fullPath) + return fullPath + } catch {} + } + return null +} + +const JSON_CONFIG_FILE_LOCATIONS = [ + '.postcssrc', + '.postcssrc.json', + // yaml syntax is not supported + // '.postcssrc.yml' +] +async function detectJSONConfigPath(base: string): Promise { + for (let file of JSON_CONFIG_FILE_LOCATIONS) { + let fullPath = path.resolve(base, file) + try { + await fs.access(fullPath) + return fullPath + } catch {} + } + return null +} + +async function isSimplePostCSSConfig(configPath: string): Promise { + let content = await fs.readFile(configPath, 'utf-8') + return ( + content.includes('tailwindcss:') && + !( + content.includes('require') || + // Adding a space at the end to not match `'postcss-import'` + content.includes('import ') + ) + ) +} + +function isEmptyObject(obj: any) { + return typeof obj === 'object' && obj !== null && Object.keys(obj).length === 0 +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/analyze.ts b/packages/@tailwindcss-upgrade/src/codemods/css/analyze.ts new file mode 100644 index 000000000000..c9af6a7a38cf --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/analyze.ts @@ -0,0 +1,300 @@ +import { isGitIgnored } from 'globby' +import path from 'node:path' +import postcss, { type Result } from 'postcss' +import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map' +import { segment } from '../../../../tailwindcss/src/utils/segment' +import { Stylesheet, type StylesheetConnection } from '../../stylesheet' +import { error, highlight, relative } from '../../utils/renderer' +import { resolveCssId } from '../../utils/resolve' + +export async function analyze(stylesheets: Stylesheet[]) { + let isIgnored = await isGitIgnored() + let processingQueue: (() => Promise)[] = [] + let stylesheetsByFile = new DefaultMap((file) => { + // We don't want to process ignored files (like node_modules) + try { + if (isIgnored(file)) { + return null + } + } catch { + // If the file is not part of the current working directory (which can + // happen if you import `tailwindcss` and it's loading a shared file from + // pnpm) then this will throw. + return null + } + + try { + let sheet = Stylesheet.loadSync(file) + + // Mutate incoming stylesheets to include the newly discovered sheet + stylesheets.push(sheet) + + // Queue up the processing of this stylesheet + processingQueue.push(() => processor.process(sheet.root, { from: sheet.file! })) + + return sheet + } catch { + return null + } + }) + + // Step 1: Record which `@import` rules point to which stylesheets + // and which stylesheets are parents/children of each other + let processor = postcss([ + { + postcssPlugin: 'mark-import-nodes', + AtRule: { + import(node) { + // Find what the import points to + let id = node.params.match(/['"](.*?)['"]/)?.[1] + if (!id) return + + let basePath = node.source?.input.file + ? path.dirname(node.source.input.file) + : process.cwd() + + // Resolve the import to a file path + let resolvedPath: string | false = false + try { + // We first try to resolve the file as relative to the current file + // to mimic the behavior of `postcss-import` since that's what was + // used to resolve imports in Tailwind CSS v3. + if (id[0] !== '.') { + try { + resolvedPath = resolveCssId(`./${id}`, basePath) + } catch {} + } + + if (!resolvedPath) { + resolvedPath = resolveCssId(id, basePath) + } + } catch (err) { + // Import is a URL, we don't want to process these, but also don't + // want to show an error message for them. + if (id.startsWith('http://') || id.startsWith('https://') || id.startsWith('//')) { + return + } + + // Something went wrong, we can't resolve the import. + error( + `Failed to resolve import: ${highlight(id)} in ${highlight(relative(node.source?.input.file!, basePath))}. Skipping.`, + { prefix: '↳ ' }, + ) + return + } + + if (!resolvedPath) return + + // Find the stylesheet pointing to the resolved path + let stylesheet = stylesheetsByFile.get(resolvedPath) + + // If it _does not_ exist in stylesheets we don't care and skip it + // this is likely because its in node_modules or a workspace package + // that we don't want to modify + if (!stylesheet) return + + // Mark the import node with the ID of the stylesheet it points to + // We will use these later to build lookup tables and modify the AST + node.raws.tailwind_destination_sheet_id = stylesheet.id + + let parent = node.source?.input.file + ? stylesheetsByFile.get(node.source.input.file) + : undefined + + let layers: string[] = [] + + for (let part of segment(node.params, ' ')) { + if (!part.startsWith('layer(')) continue + if (!part.endsWith(')')) continue + + layers.push(part.slice(6, -1).trim()) + } + + // Connect sheets together in a dependency graph + if (parent) { + let meta = { layers } + stylesheet.parents.add({ item: parent, meta }) + parent.children.add({ item: stylesheet, meta }) + } + }, + }, + }, + ]) + + // Seed the map with all the known stylesheets, and queue up the processing of + // each incoming stylesheet. + for (let sheet of stylesheets) { + if (sheet.file) { + stylesheetsByFile.set(sheet.file, sheet) + processingQueue.push(() => processor.process(sheet.root, { from: sheet.file ?? undefined })) + } + } + + // Process all the stylesheets from step 1 + while (processingQueue.length > 0) { + let task = processingQueue.shift()! + await task() + } + + // --- + + let commonPath = process.cwd() + + function pathToString(path: StylesheetConnection[]) { + let parts: string[] = [] + + for (let connection of path) { + if (!connection.item.file) continue + + let filePath = connection.item.file.replace(commonPath, '') + let layers = connection.meta.layers.join(', ') + + if (layers.length > 0) { + parts.push(`${filePath} (layers: ${layers})`) + } else { + parts.push(filePath) + } + } + + return parts.join(' <- ') + } + + let lines: string[] = [] + + for (let sheet of stylesheets) { + if (!sheet.file) continue + + let { convertiblePaths, nonConvertiblePaths } = sheet.analyzeImportPaths() + let isAmbiguous = convertiblePaths.length > 0 && nonConvertiblePaths.length > 0 + + if (!isAmbiguous) continue + + sheet.canMigrate = false + + let filePath = sheet.file.replace(commonPath, '') + + for (let path of convertiblePaths) { + lines.push(`- ${filePath} <- ${pathToString(path)}`) + } + + for (let path of nonConvertiblePaths) { + lines.push(`- ${filePath} <- ${pathToString(path)}`) + } + } + + if (lines.length === 0) { + let tailwindRootLeafs = new Set() + + for (let sheet of stylesheets) { + // If the current file already contains `@config`, then we can assume it's + // a Tailwind CSS root file. + sheet.root.walkAtRules('config', () => { + sheet.isTailwindRoot = true + return false + }) + if (sheet.isTailwindRoot) continue + + // If an `@tailwind` at-rule, or `@import "tailwindcss"` is present, + // then we can assume it's a file where Tailwind CSS might be configured. + // + // However, if 2 or more stylesheets exist with these rules that share a + // common parent, then we want to mark the common parent as the root + // stylesheet instead. + sheet.root.walkAtRules((node) => { + if ( + node.name === 'tailwind' || + (node.name === 'import' && node.params.match(/^["']tailwindcss["']/)) || + (node.name === 'import' && node.params.match(/^["']tailwindcss\/.*?["']$/)) + ) { + sheet.isTailwindRoot = true + tailwindRootLeafs.add(sheet) + } + }) + } + + // Only a single Tailwind CSS root file exists, no need to do anything else. + if (tailwindRootLeafs.size <= 1) { + return + } + + // Mark the common parent as the root file + { + // Group each sheet from tailwindRootLeafs by their common parent + let commonParents = new DefaultMap>(() => new Set()) + + // Seed common parents with leafs + for (let sheet of tailwindRootLeafs) { + commonParents.get(sheet).add(sheet) + } + + // If any 2 common parents come from the same tree, then all children of + // parent A and parent B will be moved to the parent of parent A and + // parent B. Parent A and parent B will be removed. + let repeat = true + repeat: while (repeat) { + repeat = false + + for (let [sheetA, childrenA] of commonParents) { + for (let [sheetB, childrenB] of commonParents) { + if (sheetA === sheetB) continue + + // Ancestors from self to root. Reversed order so we find the + // nearest common parent first + // + // Including self because if you compare a sheet with its parent, + // then the parent is still the common sheet between the two. In + // this case, the parent is the root file. + let ancestorsA = [sheetA].concat(Array.from(sheetA.ancestors()).reverse()) + let ancestorsB = [sheetB].concat(Array.from(sheetB.ancestors()).reverse()) + + for (let parentA of ancestorsA) { + for (let parentB of ancestorsB) { + if (parentA !== parentB) continue + + // Found the parent + let parent = parentA + + commonParents.delete(sheetA) + commonParents.delete(sheetB) + + for (let child of childrenA) { + commonParents.get(parent).add(child) + } + + for (let child of childrenB) { + commonParents.get(parent).add(child) + } + + // Found a common parent between sheet A and sheet B. We can + // stop looking for more common parents between A and B, and + // continue with the next sheet. + repeat = true + continue repeat + } + } + } + } + } + + // Mark the common parent as the Tailwind CSS root file, and remove the + // flag from each leaf. + for (let [parent, children] of commonParents) { + parent.isTailwindRoot = true + + for (let child of children) { + if (parent === child) continue + + child.isTailwindRoot = false + } + } + return + } + } + + { + let error = `You have one or more stylesheets that are imported into a utility layer and non-utility layer.\n` + error += `We cannot convert stylesheets under these conditions. Please look at the following stylesheets:\n` + + throw new Error(error + lines.join('\n')) + } +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/fixtures/test.css b/packages/@tailwindcss-upgrade/src/codemods/css/fixtures/test.css new file mode 100644 index 000000000000..a15c877ac01f --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/fixtures/test.css @@ -0,0 +1,3 @@ +.foo { + color: red; +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/format-nodes.test.ts b/packages/@tailwindcss-upgrade/src/codemods/css/format-nodes.test.ts new file mode 100644 index 000000000000..9d6393a02b82 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/format-nodes.test.ts @@ -0,0 +1,45 @@ +import postcss, { type Plugin } from 'postcss' +import { expect, it } from 'vitest' +import { formatNodes } from './format-nodes' +import { sortBuckets } from './sort-buckets' + +function markPretty(): Plugin { + return { + postcssPlugin: '@tailwindcss/upgrade/mark-pretty', + OnceExit(root) { + root.walkAtRules('tw-format', (atRule) => { + atRule.raws.tailwind_pretty = true + }) + }, + } +} + +function migrate(input: string) { + return postcss() + .use(markPretty()) + .use(sortBuckets()) + .use(formatNodes()) + .process(input, { from: expect.getState().testPath }) + .then((result) => result.css) +} + +it('should format PostCSS nodes', async () => { + expect(await migrate(`@utility .foo { .foo { color: red; } }`)).toMatchInlineSnapshot(` + "@utility .foo { + .foo { + color: red; + } + }" + `) +}) + +it('should format PostCSS nodes in the `user` bucket', async () => { + expect(await migrate(`@tw-bucket user { @tw-format .bar { .foo { color: red; } } }`)) + .toMatchInlineSnapshot(` + "@tw-format .bar { + .foo { + color: red; + } + }" + `) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/format-nodes.ts b/packages/@tailwindcss-upgrade/src/codemods/css/format-nodes.ts new file mode 100644 index 000000000000..489f2d349c73 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/format-nodes.ts @@ -0,0 +1,84 @@ +import postcss, { type ChildNode, type Plugin, type Root } from 'postcss' +import { format, type Options } from 'prettier' +import { walk } from '../../utils/walk' + +const FORMAT_OPTIONS: Options = { + parser: 'css', + semi: true, + singleQuote: true, +} + +// Prettier is used to generate cleaner output, but it's only used on the nodes +// that were marked as `pretty` during the migration. +export function formatNodes(): Plugin { + async function migrate(root: Root) { + // Find the nodes to format + let nodesToFormat: ChildNode[] = [] + walk(root, (child, _idx, parent) => { + // Always print semicolons after at-rules + if (child.type === 'atrule') { + child.raws.semicolon = true + } + + if (child.type === 'atrule' && child.name === 'tw-bucket') { + nodesToFormat.push(child) + } else if (child.raws.tailwind_pretty) { + // @ts-expect-error We might not have a parent + child.parent ??= parent + nodesToFormat.unshift(child) + } + }) + + let output: string[] = [] + + // Format the nodes + for (let node of nodesToFormat) { + let contents = (() => { + if (node.type === 'atrule' && node.name === 'tw-bucket') { + // Remove the `@tw-bucket` wrapping, and use the contents directly. + return node + .toString() + .trim() + .replace(/@tw-bucket(.*?){([\s\S]*)}/, '$2') + } + + return node.toString() + })() + + // Do not format the user bucket to ensure we keep the user's formatting + // intact. + if (node.type === 'atrule' && node.name === 'tw-bucket' && node.params === 'user') { + output.push(contents) + continue + } + + // Format buckets + if (node.type === 'atrule' && node.name === 'tw-bucket') { + output.push(await format(contents, FORMAT_OPTIONS)) + continue + } + + // Format any other nodes + node.replaceWith( + postcss.parse( + `${node.raws.before ?? ''}${(await format(contents, FORMAT_OPTIONS)).trim()}`, + ), + ) + } + + root.removeAll() + root.append( + postcss.parse( + output + .map((bucket) => bucket.trim()) + .filter(Boolean) + .join('\n\n'), + ), + ) + } + + return { + postcssPlugin: '@tailwindcss/upgrade/format-nodes', + OnceExit: migrate, + } +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/link.ts b/packages/@tailwindcss-upgrade/src/codemods/css/link.ts new file mode 100644 index 000000000000..04c2dd770039 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/link.ts @@ -0,0 +1,122 @@ +import { normalizePath } from '@tailwindcss/node' +import path from 'node:path' +import postcss from 'postcss' +import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map' +import { Stylesheet } from '../../stylesheet' +import { error, highlight, relative, success } from '../../utils/renderer' +import { detectConfigPath } from '../template/prepare-config' + +export async function linkConfigs( + stylesheets: Stylesheet[], + { configPath, base }: { configPath: string | null; base: string }, +) { + let rootStylesheets = stylesheets.filter((sheet) => sheet.isTailwindRoot) + if (rootStylesheets.length === 0) { + throw new Error( + `Cannot find any CSS files that reference Tailwind CSS.\nBefore your project can be upgraded you need to create a CSS file that imports Tailwind CSS or uses ${highlight('@tailwind')}.`, + ) + } + let withoutAtConfig = rootStylesheets.filter((sheet) => { + let hasConfig = false + sheet.root.walkAtRules('config', (node) => { + let configPath = path.resolve(path.dirname(sheet.file!), node.params.slice(1, -1)) + sheet.linkedConfigPath = configPath + hasConfig = true + return false + }) + return !hasConfig + }) + + // All stylesheets have a `@config` directives + if (withoutAtConfig.length === 0) return + + // Find the config file path for each stylesheet + let configPathBySheet = new Map() + let sheetByConfigPath = new DefaultMap>(() => new Set()) + for (let sheet of withoutAtConfig) { + if (!sheet.file) continue + + let localConfigPath = configPath as string + if (configPath === null) { + localConfigPath = await detectConfigPath(path.dirname(sheet.file), base) + } else if (!path.isAbsolute(localConfigPath)) { + localConfigPath = path.resolve(base, localConfigPath) + } + + configPathBySheet.set(sheet, localConfigPath) + sheetByConfigPath.get(localConfigPath).add(sheet) + } + + let problematicStylesheets = new Set() + for (let sheets of sheetByConfigPath.values()) { + if (sheets.size > 1) { + for (let sheet of sheets) { + problematicStylesheets.add(sheet) + } + } + } + + // There are multiple "root" files without `@config` directives. Manual + // intervention is needed to link to the correct Tailwind config files. + if (problematicStylesheets.size > 1) { + for (let sheet of problematicStylesheets) { + error( + `Could not determine configuration file for: ${highlight(relative(sheet.file!, base))}\nUpdate your stylesheet to use ${highlight('@config')} to specify the correct configuration file explicitly and then run the upgrade tool again.`, + { prefix: '↳ ' }, + ) + } + + process.exit(1) + } + + let relativePath = relative + for (let [sheet, configPath] of configPathBySheet) { + try { + if (!sheet || !sheet.file) return + success( + `Linked ${highlight(relativePath(configPath, base))} to ${highlight(relativePath(sheet.file, base))}`, + { prefix: '↳ ' }, + ) + + // Link the `@config` directive to the root stylesheets + + // Track the config file path on the stylesheet itself for easy access + // without traversing the CSS ast and finding the corresponding + // `@config` later. + sheet.linkedConfigPath = configPath + + // Create a relative path from the current file to the config file. + let relative = path.relative(path.dirname(sheet.file), configPath) + + // If the path points to a file in the same directory, `path.relative` will + // remove the leading `./` and we need to add it back in order to still + // consider the path relative + if (!relative.startsWith('.') && !path.isAbsolute(relative)) { + relative = './' + relative + } + + relative = normalizePath(relative) + + // Add the `@config` directive to the root stylesheet. + { + let target = sheet.root as postcss.Root | postcss.AtRule + let atConfig = postcss.atRule({ name: 'config', params: `'${relative}'` }) + + sheet.root.walkAtRules((node) => { + if (node.name === 'tailwind' || node.name === 'import') { + target = node + } + }) + + if (target.type === 'root') { + sheet.root.prepend(atConfig) + } else if (target.type === 'atrule') { + target.after(atConfig) + } + } + } catch (e: any) { + error('Could not load the configuration file: ' + e.message, { prefix: '↳ ' }) + process.exit(1) + } + } +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.test.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.test.ts new file mode 100644 index 000000000000..b8b309e54a4e --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.test.ts @@ -0,0 +1,133 @@ +import { __unstable__loadDesignSystem } from '@tailwindcss/node' +import dedent from 'dedent' +import postcss from 'postcss' +import { expect, it, vi } from 'vitest' +import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' +import * as versions from '../../utils/version' +import { migrateAtApply } from './migrate-at-apply' +vi.spyOn(versions, 'isMajor').mockReturnValue(true) + +const css = dedent + +async function migrate(input: string, config: Config = {}) { + let designSystem = await __unstable__loadDesignSystem( + css` + @import 'tailwindcss'; + + /* TODO(perf): Only here to speed up the tests */ + @theme { + --*: initial; + } + `, + { base: __dirname }, + ) + + return postcss() + .use( + migrateAtApply({ + designSystem, + userConfig: config, + }), + ) + .process(input, { from: expect.getState().testPath }) + .then((result) => result.css) +} + +it('should not migrate `@apply`, when there are no issues', async () => { + expect( + await migrate(css` + .foo { + @apply flex flex-col items-center; + } + `), + ).toMatchInlineSnapshot(` + ".foo { + @apply flex flex-col items-center; + }" + `) +}) + +it('should append `!` to each utility, when using `!important`', async () => { + expect( + await migrate(css` + .foo { + @apply flex flex-col !important; + } + `), + ).toMatchInlineSnapshot(` + ".foo { + @apply flex! flex-col!; + }" + `) +}) + +// TODO: Handle SCSS syntax +it.skip('should append `!` to each utility, when using `#{!important}`', async () => { + expect( + await migrate(css` + .foo { + @apply flex flex-col #{!important}; + } + `), + ).toMatchInlineSnapshot(` + ".foo { + @apply flex! flex-col!; + }" + `) +}) + +it('should move the legacy `!` prefix, to the new `!` postfix notation', async () => { + expect( + await migrate(css` + .foo { + @apply !flex flex-col! hover:!items-start items-center; + } + `), + ).toMatchInlineSnapshot(` + ".foo { + @apply flex! flex-col! hover:items-start! items-center; + }" + `) +}) + +it( + 'should apply all candidate migration when migrating with a config', + { timeout: 10_000 }, + async () => { + async function migrateWithPrefix(input: string) { + return postcss() + .use( + migrateAtApply({ + designSystem: await __unstable__loadDesignSystem( + css` + @import 'tailwindcss' prefix(tw); + + /* TODO(perf): Only here to speed up the tests */ + @theme { + --*: initial; + } + `, + { base: __dirname }, + ), + userConfig: { + prefix: 'tw_', + }, + }), + ) + .process(input, { from: expect.getState().testPath }) + .then((result) => result.css) + } + + expect( + await migrateWithPrefix(css` + .foo { + @apply !tw_flex [color:--my-color] tw_bg-gradient-to-t; + } + `), + ).toMatchInlineSnapshot(` + ".foo { + @apply tw:flex! tw:text-(--my-color) tw:bg-linear-to-t; + }" + `) + }, +) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.ts new file mode 100644 index 000000000000..69f92688dff5 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-apply.ts @@ -0,0 +1,61 @@ +import type { AtRule, Plugin } from 'postcss' +import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' +import type { DesignSystem } from '../../../../tailwindcss/src/design-system' +import { segment } from '../../../../tailwindcss/src/utils/segment' +import { migrateCandidate } from '../template/migrate' + +export function migrateAtApply({ + designSystem, + userConfig, +}: { + designSystem: DesignSystem | null + userConfig: Config | null +}): Plugin { + function migrate(atRule: AtRule) { + let utilities = atRule.params.split(/(\s+)/) + let important = + utilities[utilities.length - 1] === '!important' || + utilities[utilities.length - 1] === '#{!important}' // Sass/SCSS + + if (important) utilities.pop() // Remove `!important` + + let params = utilities.map((part) => { + // Keep whitespace + if (part.trim() === '') return part + let variants = segment(part, ':') + let utility = variants.pop()! + + // Apply the important modifier to all the rules if necessary + if (important && utility[0] !== '!' && utility[utility.length - 1] !== '!') { + utility += '!' + } + + // Reconstruct the utility with the variants + return [...variants, utility].join(':') + }) + + return async () => { + if (!designSystem) return + + // If we have a valid designSystem and config setup, we can run all + // candidate migrations on each utility + params = await Promise.all( + params.map(async (param) => await migrateCandidate(designSystem, userConfig, param)), + ) + + atRule.params = params.join('').trim() + } + } + + return { + postcssPlugin: '@tailwindcss/upgrade/migrate-at-apply', + async OnceExit(root) { + let migrations: (() => void)[] = [] + root.walkAtRules('apply', (atRule) => { + migrations.push(migrate(atRule)) + }) + + await Promise.allSettled(migrations.map((m) => m())) + }, + } +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.test.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.test.ts new file mode 100644 index 000000000000..ba1d069471ea --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.test.ts @@ -0,0 +1,1059 @@ +import dedent from 'dedent' +import postcss from 'postcss' +import { describe, expect, it, vi } from 'vitest' +import { Stylesheet } from '../../stylesheet' +import * as versions from '../../utils/version' +import { formatNodes } from './format-nodes' +import { migrateAtLayerUtilities } from './migrate-at-layer-utilities' +import { sortBuckets } from './sort-buckets' +vi.spyOn(versions, 'isMajor').mockReturnValue(true) + +const css = dedent + +async function migrate( + data: + | string + | { + root: postcss.Root + layers?: string[] + }, +) { + let stylesheet: Stylesheet + + if (typeof data === 'string') { + stylesheet = await Stylesheet.fromString(data) + } else { + stylesheet = await Stylesheet.fromRoot(data.root) + + if (data.layers) { + let meta = { layers: data.layers } + let parent = await Stylesheet.fromString('.placeholder {}') + + stylesheet.parents.add({ item: parent, meta }) + parent.children.add({ item: stylesheet, meta }) + } + } + + return postcss() + .use(migrateAtLayerUtilities(stylesheet)) + .use(sortBuckets()) + .use(formatNodes()) + .process(stylesheet.root!, { from: expect.getState().testPath }) + .then((result) => result.css) +} + +it('should migrate simple `@layer utilities` to `@utility`', async () => { + expect( + await migrate(css` + @layer utilities { + .foo { + color: red; + } + } + `), + ).toMatchInlineSnapshot(` + "@utility foo { + color: red; + }" + `) +}) + +it('should split multiple selectors in separate utilities', async () => { + expect( + await migrate(css` + @layer utilities { + .foo, + .bar { + color: red; + } + } + `), + ).toMatchInlineSnapshot(` + "@utility foo { + color: red; + } + + @utility bar { + color: red; + }" + `) +}) + +it('should merge `@utility` with the same name', async () => { + expect( + await migrate(css` + @layer utilities { + .foo { + color: red; + } + } + + .bar { + color: blue; + } + + @layer utilities { + .foo { + font-weight: bold; + } + } + `), + ).toMatchInlineSnapshot(` + "@utility foo { + color: red; + font-weight: bold; + } + + .bar { + color: blue; + }" + `) +}) + +it('should leave non-class utilities alone', async () => { + expect( + await migrate(css` + @layer utilities { + /* 1. */ + #before { + /* 1.1. */ + color: red; + /* 1.2. */ + .bar { + /* 1.2.1. */ + font-weight: bold; + } + } + + /* 2. */ + .foo { + /* 2.1. */ + color: red; + /* 2.2. */ + .bar { + /* 2.2.1. */ + font-weight: bold; + } + } + + /* 3. */ + #after { + /* 3.1. */ + color: blue; + /* 3.2. */ + .bar { + /* 3.2.1. */ + font-weight: bold; + } + } + } + `), + ).toMatchInlineSnapshot(` + "@utility foo { + /* 2. */ + /* 2.1. */ + color: red; + /* 2.2. */ + .bar { + /* 2.2.1. */ + font-weight: bold; + } + } + + @layer utilities { + /* 1. */ + #before { + /* 1.1. */ + color: red; + /* 1.2. */ + .bar { + /* 1.2.1. */ + font-weight: bold; + } + } + + /* 3. */ + #after { + /* 3.1. */ + color: blue; + /* 3.2. */ + .bar { + /* 3.2.1. */ + font-weight: bold; + } + } + }" + `) +}) + +it('should migrate simple `@layer utilities` with nesting to `@utility`', async () => { + expect( + await migrate(css` + @layer utilities { + .foo { + color: red; + + &:hover { + color: blue; + } + + &:focus { + color: green; + } + } + } + `), + ).toMatchInlineSnapshot(` + "@utility foo { + color: red; + + &:hover { + color: blue; + } + + &:focus { + color: green; + } + }" + `) +}) + +it('should migrate multiple simple `@layer utilities` to `@utility`', async () => { + expect( + await migrate(css` + @layer utilities { + .foo { + color: red; + } + + .bar { + color: blue; + } + } + `), + ).toMatchInlineSnapshot(` + "@utility foo { + color: red; + } + + @utility bar { + color: blue; + }" + `) +}) + +it('should not migrate Rules inside of Rules to a `@utility`', async () => { + expect( + await migrate(css` + @layer utilities { + .foo { + color: red; + } + + .bar { + color: blue; + + .baz { + color: green; + } + } + } + `), + ).toMatchInlineSnapshot(` + "@utility foo { + color: red; + } + + @utility bar { + color: blue; + + .baz { + color: green; + } + }" + `) +}) + +it('should invert at-rules to make them migrate-able', async () => { + expect( + await migrate(css` + @layer utilities { + @media (min-width: 640px) { + .foo { + color: red; + } + } + } + `), + ).toMatchInlineSnapshot(` + "@utility foo { + @media (min-width: 640px) { + color: red; + } + }" + `) +}) + +it('should migrate at-rules with multiple utilities and invert them', async () => { + expect( + await migrate(css` + @layer utilities { + @media (min-width: 640px) { + .foo { + color: red; + } + } + } + + @layer utilities { + @media (min-width: 640px) { + .bar { + color: blue; + } + } + } + `), + ).toMatchInlineSnapshot(` + "@utility foo { + @media (min-width: 640px) { + color: red; + } + } + + @utility bar { + @media (min-width: 640px) { + color: blue; + } + }" + `) +}) + +it('should migrate deeply nested at-rules with multiple utilities and invert them', async () => { + expect( + await migrate(css` + @layer utilities { + @media (min-width: 640px) { + .foo { + color: red; + } + + .bar { + color: blue; + } + + @media (min-width: 1024px) { + .baz { + color: green; + } + + @media (min-width: 1280px) { + .qux { + color: yellow; + } + } + } + } + } + `), + ).toMatchInlineSnapshot(` + "@utility foo { + @media (min-width: 640px) { + color: red; + } + } + + @utility bar { + @media (min-width: 640px) { + color: blue; + } + } + + @utility baz { + @media (min-width: 640px) { + @media (min-width: 1024px) { + color: green; + } + } + } + + @utility qux { + @media (min-width: 640px) { + @media (min-width: 1024px) { + @media (min-width: 1280px) { + color: yellow; + } + } + } + }" + `) +}) + +it('should migrate classes with pseudo elements', async () => { + expect( + await migrate(css` + @layer utilities { + .no-scrollbar::-webkit-scrollbar { + display: none; + } + } + `), + ).toMatchInlineSnapshot(` + "@utility no-scrollbar { + &::-webkit-scrollbar { + display: none; + } + }" + `) +}) + +it('should migrate classes with attribute selectors', async () => { + expect( + await migrate(css` + @layer utilities { + .no-scrollbar[data-checked=''] { + display: none; + } + } + `), + ).toMatchInlineSnapshot(` + "@utility no-scrollbar { + &[data-checked=''] { + display: none; + } + }" + `) +}) + +it('should migrate classes with element selectors', async () => { + expect( + await migrate(css` + @layer utilities { + .no-scrollbar main { + display: none; + } + } + `), + ).toMatchInlineSnapshot(` + "@utility no-scrollbar { + & main { + display: none; + } + }" + `) +}) + +it('should migrate classes attached to an element selector', async () => { + expect( + await migrate(css` + @layer utilities { + main.no-scrollbar { + display: none; + } + } + `), + ).toMatchInlineSnapshot(` + "@utility no-scrollbar { + &main { + display: none; + } + }" + `) +}) + +it('should migrate classes with id selectors', async () => { + expect( + await migrate(css` + @layer utilities { + .no-scrollbar#main { + display: none; + } + } + `), + ).toMatchInlineSnapshot(` + "@utility no-scrollbar { + &#main { + display: none; + } + }" + `) +}) + +it('should migrate classes with another attached class', async () => { + expect( + await migrate(css` + @layer utilities { + .no-scrollbar.main { + display: none; + } + } + `), + ).toMatchInlineSnapshot(` + "@utility no-scrollbar { + &.main { + display: none; + } + } + + @utility main { + &.no-scrollbar { + display: none; + } + }" + `) +}) + +it('should migrate a selector with multiple classes to multiple @utility definitions', async () => { + expect( + await migrate(css` + @layer utilities { + .foo .bar:hover .baz:focus { + display: none; + } + } + `), + ).toMatchInlineSnapshot(` + "@utility foo { + & .bar:hover .baz:focus { + display: none; + } + } + + @utility bar { + .foo &:hover .baz:focus { + display: none; + } + } + + @utility baz { + .foo .bar:hover &:focus { + display: none; + } + }" + `) +}) + +it('should merge `@utility` definitions with the same name', async () => { + expect( + await migrate(css` + @layer utilities { + .step { + counter-increment: step; + } + + .step:before { + @apply absolute w-7 h-7 bg-default-100 rounded-full font-medium text-center text-base inline-flex items-center justify-center -indent-px; + @apply ml-[-41px]; + content: counter(step); + } + } + `), + ).toMatchInlineSnapshot(` + "@utility step { + counter-increment: step; + + &:before { + @apply absolute w-7 h-7 bg-default-100 rounded-full font-medium text-center text-base inline-flex items-center justify-center -indent-px; + @apply ml-[-41px]; + content: counter(step); + } + }" + `) +}) + +it('should not migrate nested classes inside a `:not(…)`', async () => { + expect( + await migrate(css` + @layer utilities { + .foo .bar:not(.qux):has(.baz) { + display: none; + } + } + `), + ).toMatchInlineSnapshot(` + "@utility foo { + & .bar:not(.qux):has(.baz) { + display: none; + } + } + + @utility bar { + .foo &:not(.qux):has(.baz) { + display: none; + } + } + + @utility baz { + .foo .bar:not(.qux):has(&) { + display: none; + } + }" + `) +}) + +it('should migrate advanced combinations', async () => { + expect( + await migrate(css` + @layer utilities { + @media (width >= 100px) { + @supports (display: none) { + .foo .bar:not(.qux):has(.baz) { + display: none; + } + } + + .bar { + color: red; + } + } + + @media (width >= 200px) { + .foo { + &:hover { + @apply bg-red-500; + + .bar { + color: red; + } + } + } + } + } + `), + ).toMatchInlineSnapshot(` + "@utility foo { + @media (width >= 100px) { + @supports (display: none) { + & .bar:not(.qux):has(.baz) { + display: none; + } + } + } + + @media (width >= 200px) { + &:hover { + @apply bg-red-500; + + .bar { + color: red; + } + } + } + } + + @utility bar { + @media (width >= 100px) { + @supports (display: none) { + .foo &:not(.qux):has(.baz) { + display: none; + } + } + color: red; + } + } + + @utility baz { + @media (width >= 100px) { + @supports (display: none) { + .foo .bar:not(.qux):has(&) { + display: none; + } + } + } + }" + `) +}) + +describe('comments', () => { + it('should preserve comment location for a simple utility', async () => { + expect( + await migrate(css` + /* Start of utilities: */ + @layer utilities { + /* Utility #1 */ + .foo { + /* Declarations: */ + color: red; + } + } + `), + ).toMatchInlineSnapshot(` + "/* Start of utilities: */ + @utility foo { + /* Utility #1 */ + /* Declarations: */ + color: red; + }" + `) + }) + + it('should copy comments when creating multiple utilities from a single selector', async () => { + expect( + await migrate(css` + /* Start of utilities: */ + @layer utilities { + /* Foo & Bar */ + .foo .bar { + /* Declarations: */ + color: red; + } + } + `), + ).toMatchInlineSnapshot(` + "/* Start of utilities: */ + @utility foo { + /* Foo & Bar */ + & .bar { + /* Declarations: */ + color: red; + } + } + @utility bar { + /* Foo & Bar */ + .foo & { + /* Declarations: */ + color: red; + } + }" + `) + }) + + it('should preserve comments for utilities wrapped in at-rules', async () => { + expect( + await migrate(css` + /* Start of utilities: */ + @layer utilities { + /* Mobile only */ + @media (width <= 640px) { + /* Utility #1 */ + .foo { + /* Declarations: */ + color: red; + } + } + } + `), + ).toMatchInlineSnapshot(` + "/* Start of utilities: */ + @utility foo { + /* Mobile only */ + @media (width <= 640px) { + /* Utility #1 */ + /* Declarations: */ + color: red; + } + }" + `) + }) + + it('should preserve comment locations as best as possible', async () => { + expect( + await migrate(css` + /* Above */ + .before { + /* Inside */ + } + /* After */ + + /* Tailwind Utilities: */ + @layer utilities { + /* Chrome, Safari and Opera */ + /* Second comment */ + @media (min-width: 640px) { + /* Foobar */ + .no-scrollbar::-webkit-scrollbar { + display: none; + } + } + + /* Firefox, IE and Edge */ + /* Second comment */ + .no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } + } + + /* Above */ + .after { + /* Inside */ + } + /* After */ + `), + ).toMatchInlineSnapshot(` + "/* Tailwind Utilities: */ + @utility no-scrollbar { + /* Chrome, Safari and Opera */ + /* Second comment */ + @media (min-width: 640px) { + /* Foobar */ + &::-webkit-scrollbar { + display: none; + } + } + + /* Firefox, IE and Edge */ + /* Second comment */ + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } + + /* Above */ + .before { + /* Inside */ + } + /* After */ + + /* Above */ + .after { + /* Inside */ + } + /* After */" + `) + }) +}) + +// Saw this when testing codemods on https://github.com/docker/docs +it('should not lose attribute selectors', async () => { + expect( + await migrate(css` + @layer components { + #TableOfContents { + .toc a { + @apply block max-w-full truncate py-1 pl-2 hover:font-medium hover:no-underline; + &[aria-current='true'], + &:hover { + @apply border-l-2 border-l-gray-light bg-gradient-to-r from-gray-light-100 font-medium text-black dark:border-l-gray-dark dark:from-gray-dark-200 dark:text-white; + } + &:not([aria-current='true']) { + @apply text-gray-light-600 hover:text-black dark:text-gray-dark-700 dark:hover:text-white; + } + } + } + } + `), + ).toMatchInlineSnapshot(` + "@layer components { + #TableOfContents { + .toc a { + @apply block max-w-full truncate py-1 pl-2 hover:font-medium hover:no-underline; + &[aria-current='true'], + &:hover { + @apply border-l-2 border-l-gray-light bg-gradient-to-r from-gray-light-100 font-medium text-black dark:border-l-gray-dark dark:from-gray-dark-200 dark:text-white; + } + &:not([aria-current='true']) { + @apply text-gray-light-600 hover:text-black dark:text-gray-dark-700 dark:hover:text-white; + } + } + } + }" + `) +}) + +describe('layered stylesheets', () => { + it('should transform classes to utilities inside a layered stylesheet (utilities)', async () => { + expect( + await migrate({ + root: postcss.parse(css` + /* Utility #1 */ + .foo { + /* Declarations: */ + color: red; + } + `), + layers: ['utilities'], + }), + ).toMatchInlineSnapshot(` + "@utility foo { + /* Utility #1 */ + /* Declarations: */ + color: red; + }" + `) + }) + + it('should transform classes to utilities inside a layered stylesheet (components)', async () => { + expect( + await migrate({ + root: postcss.parse(css` + /* Utility #1 */ + .foo { + /* Declarations: */ + color: red; + } + `), + layers: ['components'], + }), + ).toMatchInlineSnapshot(` + "@utility foo { + /* Utility #1 */ + /* Declarations: */ + color: red; + }" + `) + }) + + it('should NOT transform classes to utilities inside a non-utility, layered stylesheet', async () => { + expect( + await migrate({ + root: postcss.parse(css` + /* Utility #1 */ + .foo { + /* Declarations: */ + color: red; + } + `), + layers: ['foo'], + }), + ).toMatchInlineSnapshot(` + "/* Utility #1 */ + .foo { + /* Declarations: */ + color: red; + }" + `) + }) + + it('should handle non-classes in utility-layered stylesheets', async () => { + expect( + await migrate({ + root: postcss.parse(css` + /* Utility #1 */ + .foo { + /* Declarations: */ + color: red; + } + #main { + color: red; + } + `), + layers: ['utilities'], + }), + ).toMatchInlineSnapshot(` + "@utility foo { + /* Utility #1 */ + /* Declarations: */ + color: red; + } + + #main { + color: red; + }" + `) + }) + + it('should handle non-classes in utility-layered stylesheets', async () => { + expect( + await migrate({ + root: postcss.parse(css` + @layer utilities { + @layer utilities { + /* Utility #1 */ + .foo { + /* Declarations: */ + color: red; + } + } + + /* Utility #2 */ + .bar { + /* Declarations: */ + color: red; + } + + #main { + color: red; + } + } + + /* Utility #3 */ + .baz { + /* Declarations: */ + color: red; + } + + #secondary { + color: red; + } + `), + layers: ['utilities'], + }), + ).toMatchInlineSnapshot(` + "@utility foo { + @layer utilities { + @layer utilities { + /* Utility #1 */ + /* Declarations: */ + color: red; + } + } + } + + @utility bar { + @layer utilities { + /* Utility #2 */ + /* Declarations: */ + color: red; + } + } + + @utility baz { + /* Utility #3 */ + /* Declarations: */ + color: red; + } + + @layer utilities { + + #main { + color: red; + } + } + + #secondary { + color: red; + }" + `) + }) + + it('imports are preserved in layered stylesheets', async () => { + expect( + await migrate({ + root: postcss.parse(css` + @import 'thing'; + + .foo { + color: red; + } + `), + layers: ['utilities'], + }), + ).toMatchInlineSnapshot(` + "@import 'thing'; + + @utility foo { + color: red; + }" + `) + }) + + it('charset is preserved in layered stylesheets', async () => { + expect( + await migrate({ + root: postcss.parse(css` + @charset "utf-8"; + + .foo { + color: red; + } + `), + layers: ['utilities'], + }), + ).toMatchInlineSnapshot(` + "@charset "utf-8"; + + @utility foo { + color: red; + }" + `) + }) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.ts new file mode 100644 index 000000000000..9b03e9d1658f --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-at-layer-utilities.ts @@ -0,0 +1,318 @@ +import { type AtRule, type Comment, type Plugin, type Rule } from 'postcss' +import SelectorParser from 'postcss-selector-parser' +import { segment } from '../../../../tailwindcss/src/utils/segment' +import { Stylesheet } from '../../stylesheet' +import * as version from '../../utils/version' +import { walk, WalkAction, walkDepth } from '../../utils/walk' + +export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin { + function migrate(atRule: AtRule) { + // Migrating `@layer utilities` to `@utility` is only supported in Tailwind + // CSS v3 projects. Tailwind CSS v4 projects could also have `@layer + // utilities` but those aren't actual utilities. + if (!version.isMajor(3)) return + + // Only migrate `@layer utilities` and `@layer components`. + if (atRule.params !== 'utilities' && atRule.params !== 'components') return + + // Keep rules that should not be turned into utilities as is. This will + // include rules with element or ID selectors. + let defaultsAtRule = atRule.clone() + + // Clone each rule with multiple selectors into their own rule with a single + // selector. + walk(atRule, (node) => { + if (node.type !== 'rule') return + + // Clone the node for each selector + let selectors = segment(node.selector, ',') + if (selectors.length > 1) { + let clonedNodes: Rule[] = [] + for (let selector of selectors) { + let clone = node.clone({ selector }) + clonedNodes.push(clone) + } + node.replaceWith(clonedNodes) + } + + return WalkAction.Skip + }) + + // Track all the classes that we want to create an `@utility` for. + let classes = new Set() + + walk(atRule, (node) => { + if (node.type !== 'rule') return + + // Find all the classes in the selector + SelectorParser((selectors) => { + selectors.each((selector) => { + walk(selector, (selectorNode) => { + // Ignore everything in `:not(…)` + if (selectorNode.type === 'pseudo' && selectorNode.value === ':not') { + return WalkAction.Skip + } + + if (selectorNode.type === 'class') { + classes.add(selectorNode.value) + } + }) + }) + }).processSync(node.selector, { updateSelector: false }) + + return WalkAction.Skip + }) + + // Remove all the nodes from the default `@layer utilities` that we know + // should be turned into `@utility` at-rules. + walk(defaultsAtRule, (node) => { + if (node.type !== 'rule') return + + SelectorParser((selectors) => { + selectors.each((selector) => { + walk(selector, (selectorNode) => { + // Ignore everything in `:not(…)` + if (selectorNode.type === 'pseudo' && selectorNode.value === ':not') { + return WalkAction.Skip + } + + // Remove the node if the class is in the list + if (selectorNode.type === 'class' && classes.has(selectorNode.value)) { + node.remove() + return WalkAction.Stop + } + }) + }) + }).processSync(node, { updateSelector: true }) + }) + + // Upgrade every Rule in `@layer utilities` to an `@utility` at-rule. + let clones: AtRule[] = [defaultsAtRule] + for (let cls of classes) { + let clone = atRule.clone() + clones.push(clone) + + walk(clone, (node) => { + if (node.type === 'atrule') { + if (!node.nodes || node.nodes?.length === 0) { + node.remove() + } + } + + if (node.type !== 'rule') return + + // Fan out each utility into its own rule. + // + // E.g.: + // ```css + // .foo .bar:hover .baz { + // color: red; + // } + // ``` + // + // Becomes: + // ```css + // @utility foo { + // & .bar:hover .baz { + // color: red; + // } + // } + // + // @utility bar { + // .foo &:hover .baz { + // color: red; + // } + // } + // + // @utility baz { + // .foo .bar:hover & { + // color: red; + // } + // } + // ``` + let containsClass = false + SelectorParser((selectors) => { + selectors.each((selector) => { + walk(selector, (selectorNode) => { + // Ignore everything in `:not(…)` + if (selectorNode.type === 'pseudo' && selectorNode.value === ':not') { + return WalkAction.Skip + } + + // Replace the class with `&` and track the new selector + if (selectorNode.type === 'class' && selectorNode.value === cls) { + containsClass = true + + // Find the node in the clone based on the position of the + // original node. + let target = selector.atPosition( + selectorNode.source!.start!.line, + selectorNode.source!.start!.column, + ) + + // Keep moving the target to the front until we hit the start or + // find a combinator. This is to prevent `.foo.bar` from + // becoming `.bar&`. Instead we want `&.bar`. + let parent = target.parent! + let idx = (target.parent?.index(target) ?? 0) - 1 + while (idx >= 0 && parent.at(idx)?.type !== 'combinator') { + let current = parent.at(idx + 1) + let previous = parent.at(idx) + parent.at(idx + 1).replaceWith(previous) + parent.at(idx).replaceWith(current) + + idx-- + } + + // Replace the class with `&` + target.replaceWith(SelectorParser.nesting()) + } + }) + }) + }).processSync(node, { updateSelector: true }) + + // Cleanup all the nodes that should not be part of the `@utility` rule. + if (!containsClass) { + let toRemove: (Comment | Rule)[] = [node] + let idx = node.parent?.index(node) ?? null + if (idx !== null) { + for (let i = idx - 1; i >= 0; i--) { + if (node.parent?.nodes.at(i)?.type === 'rule') { + break + } + if (node.parent?.nodes.at(i)?.type === 'comment') { + toRemove.push(node.parent?.nodes.at(i) as Comment) + } + } + } + for (let node of toRemove) { + node.remove() + } + } + + return WalkAction.Skip + }) + + // Migrate the `@layer utilities` to `@utility ` + clone.name = 'utility' + clone.params = cls + + clone.raws.before = `${clone.raws.before ?? ''}\n\n` + } + + // Cleanup + for (let idx = clones.length - 1; idx >= 0; idx--) { + let clone = clones[idx] + + walkDepth(clone, (node) => { + // Remove comments from the main `@layer utilities` we want to keep, + // that are part of any of the other clones. + if (clone === defaultsAtRule) { + if (node.type === 'comment') { + let found = false + for (let other of clones) { + if (other === defaultsAtRule) continue + + walk(other, (child) => { + if ( + child.type === 'comment' && + child.source?.start?.offset === node.source?.start?.offset + ) { + node.remove() + found = true + return WalkAction.Stop + } + }) + + if (found) { + return WalkAction.Skip + } + } + } + } + + // Remove empty rules + if ((node.type === 'rule' || node.type === 'atrule') && node.nodes?.length === 0) { + node.remove() + } + + // Replace `&` selectors with its children + else if (node.type === 'rule' && node.selector === '&') { + interface PostCSSNode { + type: string + parent?: PostCSSNode + } + + let parent: PostCSSNode | undefined = node.parent + let skip = false + while (parent) { + if (parent.type === 'rule') { + skip = true + break + } + + parent = parent.parent + } + + if (!skip) node.replaceWith(node.nodes) + } + }) + + // Remove empty clones entirely + if (clone.nodes?.length === 0) { + clones.splice(idx, 1) + } + } + + // Finally, replace the original `@layer utilities` with the new rules. + atRule.replaceWith(clones) + } + + return { + postcssPlugin: '@tailwindcss/upgrade/migrate-at-layer-utilities', + OnceExit: (root, { atRule }) => { + let layers = stylesheet.layers() + let isUtilityStylesheet = layers.has('utilities') || layers.has('components') + + if (isUtilityStylesheet) { + let rule = atRule({ name: 'layer', params: 'utilities' }) + rule.append(root.nodes) + root.append(rule) + } + + // Migrate `@layer utilities` and `@layer components` into `@utility`. + // Using this instead of the visitor API in case we want to use + // postcss-nesting in the future. + root.walkAtRules('layer', migrate) + + // Merge `@utility ` with the same name into a single rule. This can + // happen when the same classes is used in multiple `@layer utilities` + // blocks. + { + let utilities = new Map() + walk(root, (child) => { + if (child.type === 'atrule' && child.name === 'utility') { + let existing = utilities.get(child.params) + if (existing) { + existing.append(child.nodes!) + child.remove() + } else { + utilities.set(child.params, child) + } + } + }) + } + + // If the stylesheet is inside a layered import then we can remove the top-level layer directive we added + if (isUtilityStylesheet) { + root.each((node) => { + if (node.type !== 'atrule') return + if (node.name !== 'layer') return + if (node.params !== 'utilities') return + + node.replaceWith(node.nodes ?? []) + }) + } + }, + } +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-config.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-config.ts new file mode 100644 index 000000000000..2004d096903d --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-config.ts @@ -0,0 +1,110 @@ +import path from 'node:path' +import postcss, { AtRule, type Plugin } from 'postcss' +import { normalizePath } from '../../../../@tailwindcss-node/src/normalize-path' +import type { Stylesheet } from '../../stylesheet' +import type { JSConfigMigration } from '../config/migrate-js-config' + +const ALREADY_INJECTED = new WeakMap() + +export function migrateConfig( + sheet: Stylesheet, + { + configFilePath, + jsConfigMigration, + }: { configFilePath: string | null; jsConfigMigration: JSConfigMigration | null }, +): Plugin { + function migrate() { + if (!sheet.isTailwindRoot) return + if (!configFilePath) return + + let alreadyInjected = ALREADY_INJECTED.get(sheet) + if (alreadyInjected && alreadyInjected.includes(configFilePath)) { + return + } else if (alreadyInjected) { + alreadyInjected.push(configFilePath) + } else { + ALREADY_INJECTED.set(sheet, [configFilePath]) + } + + let root = sheet.root + + // We don't have a sheet with a file path + if (!sheet.file) return + + let cssConfig = new AtRule() + + // Remove the `@config` directive if it exists and we couldn't migrate the + // config file. + if (jsConfigMigration !== null) { + root.walkAtRules('config', (node) => { + node.remove() + }) + + let css = '\n\n' + css += '\n@tw-bucket source {' + for (let source of jsConfigMigration.sources) { + let absolute = path.resolve(source.base, source.pattern) + css += `@source '${relativeToStylesheet(sheet, absolute)}';\n` + } + css += '}\n' + + css += '\n@tw-bucket plugin {\n' + for (let plugin of jsConfigMigration.plugins) { + let relative = + plugin.path[0] === '.' + ? relativeToStylesheet(sheet, path.resolve(plugin.base, plugin.path)) + : plugin.path + + if (plugin.options === null) { + css += `@plugin '${relative}';\n` + } else { + css += `@plugin '${relative}' {\n` + for (let [property, value] of Object.entries(plugin.options)) { + let cssValue = '' + if (typeof value === 'string') { + cssValue = quoteString(value) + } else if (Array.isArray(value)) { + cssValue = value + .map((v) => (typeof v === 'string' ? quoteString(v) : '' + v)) + .join(', ') + } else { + cssValue = '' + value + } + + css += ` ${property}: ${cssValue};\n` + } + css += '}\n' // @plugin + } + } + css += '}\n' // @tw-bucket + + cssConfig.append(postcss.parse(css + jsConfigMigration.css)) + } + + // Inject the `@config` directive + root.append(cssConfig.nodes) + } + + return { + postcssPlugin: '@tailwindcss/upgrade/migrate-config', + OnceExit: migrate, + } +} + +function relativeToStylesheet(sheet: Stylesheet, absolute: string) { + if (!sheet.file) throw new Error('Can not find a path for the stylesheet') + + let sheetPath = sheet.file + + let relative = path.relative(path.dirname(sheetPath), absolute) + if (relative[0] !== '.') { + relative = `./${relative}` + } + // Ensure relative is a POSIX style path since we will merge it with the + // glob. + return normalizePath(relative) +} + +function quoteString(value: string): string { + return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'` +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-import.test.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-import.test.ts new file mode 100644 index 000000000000..4fba6de529b1 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-import.test.ts @@ -0,0 +1,97 @@ +import dedent from 'dedent' +import postcss from 'postcss' +import { expect, it } from 'vitest' +import { migrateImport } from './migrate-import' + +const css = dedent + +async function migrate(input: string) { + return postcss() + .use(migrateImport()) + .process(input, { from: expect.getState().testPath }) + .then((result) => result.css) +} + +it('prints relative file imports as relative paths', async () => { + expect( + await migrate(css` + @import url('https://example.com'); + + @import 'fixtures/test'; + @import 'fixtures/test.css'; + @import './fixtures/test.css'; + @import './fixtures/test'; + + @import 'fixtures/test' screen; + @import 'fixtures/test.css' screen; + @import './fixtures/test.css' screen; + @import './fixtures/test' screen; + + @import 'fixtures/test' supports(display: grid); + @import 'fixtures/test.css' supports(display: grid); + @import './fixtures/test.css' supports(display: grid); + @import './fixtures/test' supports(display: grid); + + @import 'fixtures/test' layer(utilities); + @import 'fixtures/test.css' layer(utilities); + @import './fixtures/test.css' layer(utilities); + @import './fixtures/test' layer(utilities); + + @import 'fixtures/test' theme(inline); + @import 'fixtures/test.css' theme(inline); + @import './fixtures/test.css' theme(inline); + @import './fixtures/test' theme(inline); + + @import 'fixtures/test' layer(utilities) supports(display: grid) screen and (min-width: 600px); + @import 'fixtures/test.css' layer(utilities) supports(display: grid) screen and + (min-width: 600px); + @import './fixtures/test.css' layer(utilities) supports(display: grid) screen and + (min-width: 600px); + @import './fixtures/test' layer(utilities) supports(display: grid) screen and + (min-width: 600px); + + @import 'tailwindcss'; + @import 'tailwindcss/theme.css'; + @import 'tailwindcss/theme'; + `), + ).toMatchInlineSnapshot(` + "@import url('https://example.com'); + + @import './fixtures/test.css'; + @import './fixtures/test.css'; + @import './fixtures/test.css'; + @import './fixtures/test.css'; + + @import './fixtures/test.css' screen; + @import './fixtures/test.css' screen; + @import './fixtures/test.css' screen; + @import './fixtures/test.css' screen; + + @import './fixtures/test.css' supports(display: grid); + @import './fixtures/test.css' supports(display: grid); + @import './fixtures/test.css' supports(display: grid); + @import './fixtures/test.css' supports(display: grid); + + @import './fixtures/test.css' layer(utilities); + @import './fixtures/test.css' layer(utilities); + @import './fixtures/test.css' layer(utilities); + @import './fixtures/test.css' layer(utilities); + + @import './fixtures/test.css' theme(inline); + @import './fixtures/test.css' theme(inline); + @import './fixtures/test.css' theme(inline); + @import './fixtures/test.css' theme(inline); + + @import './fixtures/test.css' layer(utilities) supports(display: grid) screen and (min-width: 600px); + @import './fixtures/test.css' layer(utilities) supports(display: grid) screen and + (min-width: 600px); + @import './fixtures/test.css' layer(utilities) supports(display: grid) screen and + (min-width: 600px); + @import './fixtures/test.css' layer(utilities) supports(display: grid) screen and + (min-width: 600px); + + @import 'tailwindcss'; + @import 'tailwindcss/theme.css'; + @import 'tailwindcss/theme';" + `) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-import.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-import.ts new file mode 100644 index 000000000000..419f5aa32feb --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-import.ts @@ -0,0 +1,52 @@ +import fs from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import { type Plugin, type Root } from 'postcss' +import { parseImportParams } from '../../../../tailwindcss/src/at-import' +import { segment } from '../../../../tailwindcss/src/utils/segment' +import * as ValueParser from '../../../../tailwindcss/src/value-parser' + +export function migrateImport(): Plugin { + async function migrate(root: Root) { + let file = root.source?.input.file + if (!file) return + + let promises: Promise[] = [] + root.walkAtRules('import', (rule) => { + try { + let [firstParam, ...rest] = segment(rule.params, ' ') + + let params = parseImportParams(ValueParser.parse(firstParam)) + if (!params) return + + let isRelative = params.uri[0] === '.' + let hasCssExtension = params.uri.endsWith('.css') + + if (isRelative && hasCssExtension) { + return + } + + let fullPath = resolve(dirname(file), params.uri) + if (!hasCssExtension) fullPath += '.css' + + promises.push( + fs.stat(fullPath).then(() => { + let ext = hasCssExtension ? '' : '.css' + let path = isRelative ? params.uri : `./${params.uri}` + rule.params = [`'${path}${ext}'`, ...rest].join(' ') + }), + ) + } catch { + // When an error occurs while parsing the `@import` statement, we skip + // the import. This will happen in cases where you import an external + // URL. + } + }) + + await Promise.allSettled(promises) + } + + return { + postcssPlugin: '@tailwindcss/upgrade/migrate-import', + OnceExit: migrate, + } +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.test.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.test.ts new file mode 100644 index 000000000000..18ce72e8b7c0 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.test.ts @@ -0,0 +1,197 @@ +import { __unstable__loadDesignSystem } from '@tailwindcss/node' +import dedent from 'dedent' +import postcss from 'postcss' +import { expect, it } from 'vitest' +import type { UserConfig } from '../../../../tailwindcss/src/compat/config/types' +import { formatNodes } from './format-nodes' +import { migrateMediaScreen } from './migrate-media-screen' +import { sortBuckets } from './sort-buckets' + +const css = dedent + +async function migrate(input: string, userConfig: UserConfig = {}) { + return postcss() + .use( + migrateMediaScreen({ + designSystem: await __unstable__loadDesignSystem(`@import 'tailwindcss';`, { + base: __dirname, + }), + userConfig, + }), + ) + .use(sortBuckets()) + .use(formatNodes()) + .process(input, { from: expect.getState().testPath }) + .then((result) => result.css) +} + +it('should migrate a built-in breakpoint', async () => { + expect( + await migrate(css` + @media screen(md) { + .foo { + color: red; + } + } + `), + ).toMatchInlineSnapshot(` + "@media (width >= theme(--breakpoint-md)) { + .foo { + color: red; + } + }" + `) +}) + +it('should migrate `@screen` with a built-in breakpoint', async () => { + expect( + await migrate(css` + @screen md { + .foo { + color: red; + } + } + `), + ).toMatchInlineSnapshot(` + "@media (width >= theme(--breakpoint-md)) { + .foo { + color: red; + } + }" + `) +}) + +it('should migrate a custom min-width screen (string)', async () => { + expect( + await migrate( + css` + @media screen(foo) { + .foo { + color: red; + } + } + `, + { + theme: { + screens: { + foo: '123px', + }, + }, + }, + ), + ).toMatchInlineSnapshot(` + "@media (width >= theme(--breakpoint-foo)) { + .foo { + color: red; + } + }" + `) +}) + +it('should migrate a custom min-width screen (object)', async () => { + expect( + await migrate( + css` + @media screen(foo) { + .foo { + color: red; + } + } + `, + { + theme: { + screens: { + foo: { min: '123px' }, + }, + }, + }, + ), + ).toMatchInlineSnapshot(` + "@media (width >= theme(--breakpoint-foo)) { + .foo { + color: red; + } + }" + `) +}) + +it('should migrate a custom max-width screen', async () => { + expect( + await migrate( + css` + @media screen(foo) { + .foo { + color: red; + } + } + `, + { + theme: { + screens: { + foo: { max: '123px' }, + }, + }, + }, + ), + ).toMatchInlineSnapshot(` + "@media (123px >= width) { + .foo { + color: red; + } + }" + `) +}) + +it('should migrate a custom min and max-width screen', async () => { + expect( + await migrate( + css` + @media screen(foo) { + .foo { + color: red; + } + } + `, + { + theme: { + screens: { + foo: { min: '100px', max: '123px' }, + }, + }, + }, + ), + ).toMatchInlineSnapshot(` + "@media (123px >= width >= 100px) { + .foo { + color: red; + } + }" + `) +}) + +it('should migrate a raw media query', async () => { + expect( + await migrate( + css` + @media screen(foo) { + .foo { + color: red; + } + } + `, + { + theme: { + screens: { + foo: { raw: 'only screen and (min-width: 123px)' }, + }, + }, + }, + ), + ).toMatchInlineSnapshot(` + "@media only screen and (min-width: 123px) { + .foo { + color: red; + } + }" + `) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.ts new file mode 100644 index 000000000000..c10e9ee99094 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-media-screen.ts @@ -0,0 +1,53 @@ +import { type Plugin, type Root } from 'postcss' +import { resolveConfig } from '../../../../tailwindcss/src/compat/config/resolve-config' +import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' +import { buildMediaQuery } from '../../../../tailwindcss/src/compat/screens-config' +import type { DesignSystem } from '../../../../tailwindcss/src/design-system' +import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map' + +export function migrateMediaScreen({ + designSystem, + userConfig, +}: { + designSystem?: DesignSystem | null + userConfig?: Config | null +} = {}): Plugin { + function migrate(root: Root) { + if (!designSystem || !userConfig) return + + let { resolvedConfig } = resolveConfig(designSystem, [ + { base: '', config: userConfig, reference: false, src: undefined }, + ]) + let screens = resolvedConfig?.theme?.screens || {} + + let mediaQueries = new DefaultMap((name) => { + let value = designSystem?.resolveThemeValue(`--breakpoint-${name}`, true) ?? screens?.[name] + if (typeof value === 'string') return `(width >= theme(--breakpoint-${name}))` + return value ? buildMediaQuery(value) : null + }) + + // First migrate `@screen md` to `@media screen(md)` + root.walkAtRules('screen', (node) => { + node.name = 'media' + node.params = `screen(${node.params})` + }) + + // Then migrate the `screen(…)` function + root.walkAtRules((rule) => { + if (rule.name !== 'media') return + + let screen = rule.params.match(/screen\(([^)]+)\)/) + if (!screen) return + + let value = mediaQueries.get(screen[1]) + if (!value) return + + rule.params = value + }) + } + + return { + postcssPlugin: '@tailwindcss/upgrade/migrate-media-screen', + OnceExit: migrate, + } +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-missing-layers.test.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-missing-layers.test.ts new file mode 100644 index 000000000000..595275874dd6 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-missing-layers.test.ts @@ -0,0 +1,227 @@ +import dedent from 'dedent' +import postcss from 'postcss' +import { expect, it } from 'vitest' +import { formatNodes } from './format-nodes' +import { migrateMissingLayers } from './migrate-missing-layers' +import { sortBuckets } from './sort-buckets' + +const css = dedent + +function migrate(input: string) { + return postcss() + .use(migrateMissingLayers()) + .use(sortBuckets()) + .use(formatNodes()) + .process(input, { from: expect.getState().testPath }) + .then((result) => result.css) +} + +it('should not migrate already migrated `@import` at-rules', async () => { + expect( + await migrate(css` + @import 'tailwindcss'; + `), + ).toMatchInlineSnapshot(`"@import 'tailwindcss';"`) +}) + +it('should add missing `layer(…)` to imported files', async () => { + expect( + await migrate(css` + @import 'tailwindcss/utilities'; /* Expected no layer */ + @import './foo.css'; /* Expected layer(utilities) */ + @import './bar.css'; /* Expected layer(utilities) */ + @import 'tailwindcss/components'; /* Expected no layer */ + `), + ).toMatchInlineSnapshot(` + "@import 'tailwindcss/utilities'; /* Expected no layer */ + @import './foo.css' layer(utilities); /* Expected layer(utilities) */ + @import './bar.css' layer(utilities); /* Expected layer(utilities) */ + @import 'tailwindcss/components'; /* Expected no layer */" + `) +}) + +it('should add missing `layer(…)` as the first param after the import itself', async () => { + expect( + await migrate(css` + @import 'tailwindcss/utilities' supports(--foo); /* Expected no layer */ + @import './foo.css' supports(--foo); /* Expected layer(utilities) supports(--foo) */ + @import './bar.css' supports(--foo); /* Expected layer(utilities) supports(--foo) */ + @import 'tailwindcss/components' supports(--foo); /* Expected no layer */ + `), + ).toMatchInlineSnapshot(` + "@import 'tailwindcss/utilities' supports(--foo); /* Expected no layer */ + @import './foo.css' layer(utilities) supports(--foo); /* Expected layer(utilities) supports(--foo) */ + @import './bar.css' layer(utilities) supports(--foo); /* Expected layer(utilities) supports(--foo) */ + @import 'tailwindcss/components' supports(--foo); /* Expected no layer */" + `) +}) + +it('should not migrate anything if no `@tailwind` directives (or imports) are found', async () => { + expect( + await migrate(css` + /* Base */ + html { + color: red; + } + + /* Utilities */ + .foo { + color: blue; + } + `), + ).toMatchInlineSnapshot(` + "/* Base */ + html { + color: red; + } + + /* Utilities */ + .foo { + color: blue; + }" + `) +}) + +it('should not wrap comments in a layer, if they are the only nodes', async () => { + expect( + await migrate(css` + @tailwind base; + + /* BASE */ + + @tailwind components; + + /* COMPONENTS */ + + @tailwind utilities; + + /** UTILITIES */ + /** - Another comment */ + `), + ).toMatchInlineSnapshot(` + "@tailwind base; + + /* BASE */ + + @tailwind components; + + /* COMPONENTS */ + + @tailwind utilities; + + /** UTILITIES */ + /** - Another comment */" + `) +}) + +it('should migrate rules above the `@tailwind base` directive in an `@layer base`', async () => { + expect( + await migrate(css` + @charset "UTF-8"; + @layer foo, bar, baz; + + /**! + * License header + */ + + html { + color: red; + } + + @tailwind base; + @tailwind components; + @tailwind utilities; + `), + ).toMatchInlineSnapshot(` + "@charset "UTF-8"; + @layer foo, bar, baz; + + /**! + * License header + */ + + @tailwind base; + @tailwind components; + @tailwind utilities; + + @layer base { + html { + color: red; + } + }" + `) +}) + +it('should migrate rules between tailwind directives', async () => { + expect( + await migrate(css` + @tailwind base; + + .base { + } + + @tailwind components; + + .component-a { + } + .component-b { + } + + @tailwind utilities; + + .utility-a { + } + .utility-b { + } + `), + ).toMatchInlineSnapshot(` + "@tailwind base; + + @tailwind components; + + @tailwind utilities; + + @layer base { + .base { + } + } + + @layer components { + .component-a { + } + .component-b { + } + } + + .utility-a { + } + .utility-b { + }" + `) +}) + +it('should keep CSS above a layer unlayered', async () => { + expect( + await migrate(css` + .foo { + color: red; + } + + @layer components { + .bar { + color: blue; + } + } + `), + ).toMatchInlineSnapshot(` + ".foo { + color: red; + } + + @layer components { + .bar { + color: blue; + } + }" + `) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-missing-layers.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-missing-layers.ts new file mode 100644 index 000000000000..10c9d55de9ca --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-missing-layers.ts @@ -0,0 +1,161 @@ +import { AtRule, type ChildNode, type Plugin, type Root } from 'postcss' +import { segment } from '../../../../tailwindcss/src/utils/segment' + +export function migrateMissingLayers(): Plugin { + function migrate(root: Root) { + let lastLayer = '' + let bucket: ChildNode[] = [] + let buckets: [layer: string, bucket: typeof bucket][] = [] + let firstLayerName: string | null = null + + root.each((node) => { + if (node.type === 'atrule') { + // Known Tailwind directives that should not be inside a layer. + if ( + node.name === 'config' || + node.name === 'source' || + node.name === 'theme' || + node.name === 'utility' || + node.name === 'custom-variant' || + node.name === 'variant' + ) { + if (bucket.length > 0) { + buckets.push([lastLayer, bucket.splice(0)]) + } + return + } + + // Base + if ( + (node.name === 'tailwind' && node.params === 'base') || + (node.name === 'import' && node.params.match(/^["']tailwindcss\/base["']/)) + ) { + if (bucket.length > 0) { + buckets.push([lastLayer, bucket.splice(0)]) + } + + firstLayerName ??= 'base' + lastLayer = 'base' + return + } + + // Components + if ( + (node.name === 'tailwind' && node.params === 'components') || + (node.name === 'import' && node.params.match(/^["']tailwindcss\/components["']/)) + ) { + if (bucket.length > 0) { + buckets.push([lastLayer, bucket.splice(0)]) + } + + firstLayerName ??= 'components' + lastLayer = 'components' + return + } + + // Utilities + if ( + (node.name === 'tailwind' && node.params === 'utilities') || + (node.name === 'import' && node.params.match(/^["']tailwindcss\/utilities["']/)) + ) { + if (bucket.length > 0) { + buckets.push([lastLayer, bucket.splice(0)]) + } + + firstLayerName ??= 'utilities' + lastLayer = 'utilities' + return + } + + // Already in a layer + if (node.name === 'layer') { + if (bucket.length > 0) { + buckets.push([lastLayer, bucket.splice(0)]) + } + return + } + + // Add layer to `@import` at-rules + if (node.name === 'import') { + if (bucket.length > 0) { + buckets.push([lastLayer, bucket.splice(0)]) + } + + // Create new bucket just for the import. This way every import exists + // in its own layer which allows us to add the `layer(…)` parameter + // later on. + buckets.push([lastLayer, [node]]) + return + } + } + + // (License) comments, body-less `@layer` and `@charset` can stay at the + // top, when we haven't found any `@tailwind` at-rules yet. + if ( + lastLayer === '' && + (node.type === 'comment' /* Comment */ || + (node.type === 'atrule' && !node.nodes) || // @layer foo, bar, baz; + (node.type === 'atrule' && node.name === 'charset')) // @charset "UTF-8"; + ) { + return + } + + // Track the node + bucket.push(node) + }) + + for (let [layerName, nodes] of buckets) { + let targetLayerName = layerName || firstLayerName || '' + if (targetLayerName === '') { + continue + } + + // Do not wrap comments in a layer, if they are the only nodes. + if (nodes.every((node) => node.type === 'comment')) { + continue + } + + // Add `layer(…)` to `@import` at-rules + if (nodes.every((node) => node.type === 'atrule' && node.name === 'import')) { + for (let node of nodes) { + if (node.type !== 'atrule' || node.name !== 'import') continue + + if (!node.params.includes('layer(')) { + let params = segment(node.params, ' ') + params.splice(1, 0, `layer(${targetLayerName})`) + node.params = params.join(' ') + node.raws.tailwind_injected_layer = true + } + } + continue + } + + // Wrap each bucket in an `@layer` at-rule + let target = nodes[0] + let layerNode = new AtRule({ + name: 'layer', + params: targetLayerName, + nodes: nodes.map((node) => { + // Keep the target node as-is, because we will be replacing that one + // with the new layer node. + if (node === target) { + return node + } + + // Every other node should be removed from its original position. They + // will be added to the new layer node. + return node.remove() + }), + raws: { + tailwind_pretty: true, + }, + }) + target.replaceWith(layerNode) + } + } + + return { + postcssPlugin: '@tailwindcss/upgrade/migrate-missing-layers', + OnceExit: migrate, + } +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.test.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.test.ts new file mode 100644 index 000000000000..2d8b700411b1 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.test.ts @@ -0,0 +1,325 @@ +import { __unstable__loadDesignSystem } from '@tailwindcss/node' +import dedent from 'dedent' +import postcss from 'postcss' +import { expect, it, vi } from 'vitest' +import * as versions from '../../utils/version' +import { formatNodes } from './format-nodes' +import { migratePreflight } from './migrate-preflight' +import { sortBuckets } from './sort-buckets' +vi.spyOn(versions, 'isMajor').mockReturnValue(true) + +const css = dedent + +async function migrate(input: string) { + let designSystem = await __unstable__loadDesignSystem( + css` + @import 'tailwindcss'; + `, + { base: __dirname }, + ) + + return postcss() + .use(migratePreflight({ designSystem })) + .use(sortBuckets()) + .use(formatNodes()) + .process(input, { from: expect.getState().testPath }) + .then((result) => result.css) +} + +it("should add compatibility CSS after the `@import 'tailwindcss'`", async () => { + expect( + await migrate(css` + @import 'tailwindcss'; + `), + ).toMatchInlineSnapshot(` + "@import 'tailwindcss'; + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + }" + `) +}) + +it('should add the compatibility CSS after the last `@import`', async () => { + expect( + await migrate(css` + @import 'tailwindcss'; + @import './foo.css'; + @import './bar.css'; + `), + ).toMatchInlineSnapshot(` + "@import 'tailwindcss'; + @import './foo.css'; + @import './bar.css'; + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + }" + `) +}) + +it('should add the compatibility CSS after the last import, even if a body-less `@layer` exists', async () => { + expect( + await migrate(css` + @charset "UTF-8"; + @layer foo, bar, baz, base; + + /**! + * License header + */ + + @import 'tailwindcss'; + @import './foo.css'; + @import './bar.css'; + `), + ).toMatchInlineSnapshot(` + "@charset "UTF-8"; + @layer foo, bar, baz, base; + + /**! + * License header + */ + + @import 'tailwindcss'; + @import './foo.css'; + @import './bar.css'; + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + }" + `) +}) + +it('should add the compatibility CSS before the first `@layer base` (if the "tailwindcss" import exists)', async () => { + expect( + await migrate(css` + @import 'tailwindcss'; + + @custom-variant foo { + } + + @utility bar { + } + + @layer base { + } + + @utility baz { + } + + @layer base { + } + `), + ).toMatchInlineSnapshot(` + "@import 'tailwindcss'; + + @custom-variant foo { + } + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + + @utility bar { + } + + @utility baz { + } + + @layer base { + } + + @layer base { + }" + `) +}) + +it('should add the compatibility CSS before the first `@layer base` (if the "tailwindcss/preflight" import exists)', async () => { + expect( + await migrate(css` + @import 'tailwindcss/preflight'; + + @custom-variant foo { + } + + @utility bar { + } + + @layer base { + } + + @utility baz { + } + + @layer base { + } + `), + ).toMatchInlineSnapshot(` + "@import 'tailwindcss/preflight'; + + @custom-variant foo { + } + + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + } + + @utility bar { + } + + @utility baz { + } + + @layer base { + } + + @layer base { + }" + `) +}) + +it('should not add the backwards compatibility CSS when no `@import "tailwindcss"` or `@import "tailwindcss/preflight"` exists', async () => { + expect( + await migrate(css` + @custom-variant foo { + } + + @utility bar { + } + + @layer base { + } + + @utility baz { + } + + @layer base { + } + `), + ).toMatchInlineSnapshot(` + "@custom-variant foo { + } + + @utility bar { + } + + @utility baz { + } + + @layer base { + } + + @layer base { + }" + `) +}) + +it('should not add the backwards compatibility CSS when another `@import "tailwindcss"` import exists such as theme or utilities', async () => { + expect( + await migrate(css` + @import 'tailwindcss/theme'; + + @custom-variant foo { + } + + @utility bar { + } + + @layer base { + } + + @utility baz { + } + + @layer base { + } + `), + ).toMatchInlineSnapshot(` + "@import 'tailwindcss/theme'; + + @custom-variant foo { + } + + @utility bar { + } + + @utility baz { + } + + @layer base { + } + + @layer base { + }" + `) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.ts new file mode 100644 index 000000000000..d32d96979cb0 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-preflight.ts @@ -0,0 +1,184 @@ +import dedent from 'dedent' +import postcss, { type Plugin, type Root } from 'postcss' +import { keyPathToCssProperty } from '../../../../tailwindcss/src/compat/apply-config-to-theme' +import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' +import type { DesignSystem } from '../../../../tailwindcss/src/design-system' +import { toKeyPath } from '../../../../tailwindcss/src/utils/to-key-path' +import * as ValueParser from '../../../../tailwindcss/src/value-parser' +import { walk, WalkAction } from '../../../../tailwindcss/src/walk' +import * as version from '../../utils/version' + +// Defaults in v4 +const DEFAULT_BORDER_COLOR = 'currentcolor' + +const css = dedent +const BORDER_COLOR_COMPATIBILITY_CSS = css` + /* + The default border color has changed to \`currentcolor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: theme(borderColor.DEFAULT); + } + } +` + +export function migratePreflight({ + designSystem, + userConfig, +}: { + designSystem: DesignSystem | null + userConfig?: Config | null +}): Plugin { + // @ts-expect-error + let defaultBorderColor = userConfig?.theme?.borderColor?.DEFAULT + + function canResolveThemeValue(path: string) { + if (!designSystem) return false + let variable = `--${keyPathToCssProperty(toKeyPath(path))}` as const + return Boolean(designSystem.theme.get([variable])) + } + + function migrate(root: Root) { + // CSS for backwards compatibility with v3 should only injected in v3 + // projects and not v4 projects. + if (!version.isMajor(3)) return + + let isTailwindRoot = false + root.walkAtRules('import', (node) => { + if ( + /['"]tailwindcss['"]/.test(node.params) || + /['"]tailwindcss\/preflight['"]/.test(node.params) + ) { + isTailwindRoot = true + return false + } + }) + + if (!isTailwindRoot) return + + // Figure out the compatibility CSS to inject + let compatibilityCssString = '' + if (defaultBorderColor !== DEFAULT_BORDER_COLOR) { + compatibilityCssString += BORDER_COLOR_COMPATIBILITY_CSS + compatibilityCssString += '\n\n' + } + + compatibilityCssString = `\n@tw-bucket compatibility {\n${compatibilityCssString}\n}\n` + let compatibilityCss = postcss.parse(compatibilityCssString) + + // Replace the `theme(…)` with v3 values if we can't resolve the theme + // value. + compatibilityCss.walkDecls((decl) => { + if (decl.value.includes('theme(')) { + decl.value = substituteFunctionsInValue(ValueParser.parse(decl.value), (path) => { + if (canResolveThemeValue(path)) { + return defaultBorderColor + } else { + if (path === 'borderColor.DEFAULT') { + return 'var(--color-gray-200, currentcolor)' + } + } + return null + }) + } + }) + + // Cleanup `--border-color` definition in `theme(…)` + root.walkAtRules('theme', (node) => { + node.walkDecls('--border-color', (decl) => { + decl.remove() + }) + + if (node.nodes?.length === 0) { + node.remove() + } + }) + + // Inject the compatibility CSS + root.append(compatibilityCss) + } + + return { + postcssPlugin: '@tailwindcss/upgrade/migrate-preflight', + OnceExit: migrate, + } +} + +function substituteFunctionsInValue( + ast: ValueParser.ValueAstNode[], + handle: (value: string, fallback?: string) => string | null, +) { + walk(ast, (node) => { + if (node.kind === 'function' && node.value === 'theme') { + if (node.nodes.length < 1) return + + // Ignore whitespace before the first argument + if (node.nodes[0].kind === 'separator' && node.nodes[0].value.trim() === '') { + node.nodes.shift() + } + + let pathNode = node.nodes[0] + if (pathNode.kind !== 'word') return + + let path = pathNode.value + + // For the theme function arguments, we require all separators to contain + // comma (`,`), spaces alone should be merged into the previous word to + // avoid splitting in this case: + // + // theme(--color-red-500 / 75%) theme(--color-red-500 / 75%, foo, bar) + // + // We only need to do this for the first node, as the fallback values are + // passed through as-is. + let skipUntilIndex = 1 + for (let i = skipUntilIndex; i < node.nodes.length; i++) { + if (node.nodes[i].value.includes(',')) { + break + } + path += ValueParser.toCss([node.nodes[i]]) + skipUntilIndex = i + 1 + } + + path = eventuallyUnquote(path) + let fallbackValues = node.nodes.slice(skipUntilIndex + 1) + + let replacement = + fallbackValues.length > 0 ? handle(path, ValueParser.toCss(fallbackValues)) : handle(path) + if (replacement === null) return + + return WalkAction.Replace(ValueParser.parse(replacement)) + } + }) + + return ValueParser.toCss(ast) +} + +function eventuallyUnquote(value: string) { + if (value[0] !== "'" && value[0] !== '"') return value + + let unquoted = '' + let quoteChar = value[0] + for (let i = 1; i < value.length - 1; i++) { + let currentChar = value[i] + let nextChar = value[i + 1] + + if (currentChar === '\\' && (nextChar === quoteChar || nextChar === '\\')) { + unquoted += nextChar + i++ + } else { + unquoted += currentChar + } + } + + return unquoted +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-tailwind-directives.test.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-tailwind-directives.test.ts new file mode 100644 index 000000000000..fdf480b3ef3d --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-tailwind-directives.test.ts @@ -0,0 +1,420 @@ +import dedent from 'dedent' +import postcss from 'postcss' +import { expect, it } from 'vitest' +import { formatNodes } from './format-nodes' +import { migrateTailwindDirectives } from './migrate-tailwind-directives' +import { sortBuckets } from './sort-buckets' + +const css = dedent + +function migrate(input: string, options: { newPrefix: string | null } = { newPrefix: null }) { + return postcss() + .use(migrateTailwindDirectives(options)) + .use(sortBuckets()) + .use(formatNodes()) + .process(input, { from: expect.getState().testPath }) + .then((result) => result.css) +} + +it("should not migrate `@import 'tailwindcss'`", async () => { + expect( + await migrate(css` + @import 'tailwindcss'; + `), + ).toEqual(css` + @import 'tailwindcss'; + `) +}) + +it("should append a prefix to `@import 'tailwindcss'`", async () => { + expect( + await migrate( + css` + @import 'tailwindcss'; + `, + { + newPrefix: 'tw', + }, + ), + ).toEqual(css` + @import 'tailwindcss' prefix(tw); + `) +}) + +it('should migrate the tailwind.css import', async () => { + expect( + await migrate(css` + @import 'tailwindcss/tailwind.css'; + `), + ).toEqual(css` + @import 'tailwindcss'; + `) +}) + +it('should migrate the tailwind.css import with a prefix', async () => { + expect( + await migrate( + css` + @import 'tailwindcss/tailwind.css'; + `, + { + newPrefix: 'tw', + }, + ), + ).toEqual(css` + @import 'tailwindcss' prefix(tw); + `) +}) + +it('should migrate the default @tailwind directives to a single import', async () => { + expect( + await migrate(css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `), + ).toEqual(css` + @import 'tailwindcss'; + `) +}) + +it('should migrate the default @tailwind directives to a single import with a prefix', async () => { + expect( + await migrate( + css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + { + newPrefix: 'tw', + }, + ), + ).toEqual(css` + @import 'tailwindcss' prefix(tw); + `) +}) + +it('should migrate the default @tailwind directives as imports to a single import', async () => { + expect( + await migrate(css` + @import 'tailwindcss/base'; + @import 'tailwindcss/components'; + @import 'tailwindcss/utilities'; + `), + ).toEqual(css` + @import 'tailwindcss'; + `) +}) + +it('should migrate the default @tailwind directives to a single import in a valid location', async () => { + expect( + await migrate(css` + @charset "UTF-8"; + @layer foo, bar, baz; + + /**! + * License header + */ + + html { + color: red; + } + + @tailwind base; + @tailwind components; + @tailwind utilities; + `), + ) + // NOTE: The `html {}` is not wrapped in a `@layer` directive, because that + // is handled by another migration step. See ../index.test.ts for a + // dedicated test. + .toEqual(css` + @charset "UTF-8"; + @layer foo, bar, baz; + + /**! + * License header + */ + + @import 'tailwindcss'; + + html { + color: red; + } + `) +}) + +it('should migrate the default @tailwind directives as imports to a single import in a valid location', async () => { + expect( + await migrate(css` + @charset "UTF-8"; + @layer foo, bar, baz; + + /**! + * License header + */ + + @import 'tailwindcss/base'; + @import 'tailwindcss/components'; + @import 'tailwindcss/utilities'; + `), + ).toEqual(css` + @charset "UTF-8"; + @layer foo, bar, baz; + + /**! + * License header + */ + + @import 'tailwindcss'; + `) +}) + +it('should migrate the default @tailwind directives as imports to a single import with a prefix', async () => { + expect( + await migrate( + css` + @import 'tailwindcss/base'; + @import 'tailwindcss/components'; + @import 'tailwindcss/utilities'; + `, + { + newPrefix: 'tw', + }, + ), + ).toEqual(css` + @import 'tailwindcss' prefix(tw); + `) +}) + +it.each([ + [ + // The default order + css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + css` + @import 'tailwindcss'; + `, + ], + + // @tailwind components moved, but has no effect in v4. Therefore `base` and + // `utilities` are still in the correct order. + [ + css` + @tailwind base; + @tailwind utilities; + @tailwind components; + `, + css` + @import 'tailwindcss'; + `, + ], + + // Same as previous comment + [ + css` + @tailwind components; + @tailwind base; + @tailwind utilities; + `, + css` + @import 'tailwindcss'; + `, + ], + + // `base` and `utilities` swapped order, thus the `@layer` directives are + // needed. The `components` directive is still ignored. + [ + css` + @tailwind components; + @tailwind utilities; + @tailwind base; + `, + css` + @layer theme, components, utilities, base; + @import 'tailwindcss'; + `, + ], + [ + css` + @tailwind utilities; + @tailwind base; + @tailwind components; + `, + css` + @layer theme, components, utilities, base; + @import 'tailwindcss'; + `, + ], + [ + css` + @tailwind utilities; + @tailwind components; + @tailwind base; + `, + css` + @layer theme, components, utilities, base; + @import 'tailwindcss'; + `, + ], +])( + 'should migrate the default directives (but in different order) to a single import, order %#', + async (input, expected) => { + expect(await migrate(input)).toEqual(expected) + }, +) + +it('should migrate `@tailwind base` to theme and preflight imports', async () => { + expect( + await migrate(css` + @tailwind base; + `), + ).toEqual(css` + @import 'tailwindcss/theme' layer(theme); + @import 'tailwindcss/preflight' layer(base); + `) +}) + +it('should migrate `@tailwind base` to theme and preflight imports with a prefix', async () => { + expect( + await migrate( + css` + @tailwind base; + `, + { + newPrefix: 'tw', + }, + ), + ).toEqual(css` + @import 'tailwindcss/theme' layer(theme) prefix(tw); + @import 'tailwindcss/preflight' layer(base); + `) +}) + +it('should migrate `@import "tailwindcss/base"` to theme and preflight imports', async () => { + expect( + await migrate(css` + @import 'tailwindcss/base'; + `), + ).toEqual(css` + @import 'tailwindcss/theme' layer(theme); + @import 'tailwindcss/preflight' layer(base); + `) +}) + +it('should migrate `@import "tailwindcss/base"` to theme and preflight imports with a prefix', async () => { + expect( + await migrate( + css` + @import 'tailwindcss/base'; + `, + { + newPrefix: 'tw', + }, + ), + ).toEqual(css` + @import 'tailwindcss/theme' layer(theme) prefix(tw); + @import 'tailwindcss/preflight' layer(base); + `) +}) + +it('should migrate `@tailwind utilities` to an import', async () => { + expect( + await migrate(css` + @tailwind utilities; + `), + ).toEqual(css` + @import 'tailwindcss/utilities' layer(utilities); + `) +}) + +it('should migrate `@import "tailwindcss/utilities"` to an import', async () => { + expect( + await migrate(css` + @import 'tailwindcss/utilities'; + `), + ).toEqual(css` + @import 'tailwindcss/utilities' layer(utilities); + `) +}) + +it('should not migrate existing imports using a custom layer', async () => { + expect( + await migrate(css` + @import 'tailwindcss/utilities' layer(my-utilities); + `), + ).toEqual(css` + @import 'tailwindcss/utilities' layer(my-utilities); + `) +}) + +// We don't have a `@layer components` anymore, so omitting it should result +// in the full import as well. Alternatively, we could expand to: +// +// ```css +// @import 'tailwindcss/theme' layer(theme); +// @import 'tailwindcss/preflight' layer(base); +// @import 'tailwindcss/utilities' layer(utilities); +// ``` +it('should migrate `@tailwind base` and `@tailwind utilities` to a single import', async () => { + expect( + await migrate(css` + @tailwind base; + @tailwind utilities; + `), + ).toEqual(css` + @import 'tailwindcss'; + `) +}) + +it('should migrate `@tailwind base` and `@tailwind utilities` to a single import with a prefix', async () => { + expect( + await migrate( + css` + @import 'tailwindcss/base'; + @import 'tailwindcss/utilities'; + `, + { + newPrefix: 'tw', + }, + ), + ).toEqual(css` + @import 'tailwindcss' prefix(tw); + `) +}) + +it('should drop `@tailwind screens;`', async () => { + expect( + await migrate(css` + @tailwind screens; + `), + ).toEqual('') +}) + +it('should drop `@tailwind variants;`', async () => { + expect( + await migrate(css` + @tailwind variants; + `), + ).toEqual('') +}) + +it('should replace `@responsive` with its children', async () => { + expect( + await migrate(css` + @responsive { + .foo { + color: red; + } + } + `), + ).toMatchInlineSnapshot(` + ".foo { + color: red; + }" + `) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-tailwind-directives.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-tailwind-directives.ts new file mode 100644 index 000000000000..d3d74a65e386 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-tailwind-directives.ts @@ -0,0 +1,188 @@ +import { AtRule, type ChildNode, type Plugin, type Root } from 'postcss' + +const DEFAULT_LAYER_ORDER = ['theme', 'base', 'components', 'utilities'] + +export function migrateTailwindDirectives(options: { newPrefix: string | null }): Plugin { + let prefixParams = options.newPrefix ? ` prefix(${options.newPrefix})` : '' + + function migrate(root: Root) { + let baseNode = null as AtRule | null + let utilitiesNode = null as AtRule | null + let orderedNodes: AtRule[] = [] + + let defaultImportNode = null as AtRule | null + let utilitiesImportNode = null as AtRule | null + let preflightImportNode = null as AtRule | null + let themeImportNode = null as AtRule | null + + let layerOrder: string[] = [] + + root.walkAtRules((node) => { + // Migrate legacy `@import "tailwindcss/tailwind.css"` + if (node.name === 'import' && node.params.match(/^["']tailwindcss\/tailwind\.css["']$/)) { + node.params = node.params.replace('tailwindcss/tailwind.css', 'tailwindcss') + } + + // Append any new prefix() param to existing `@import 'tailwindcss'` directives + if (node.name === 'import' && node.params.match(/^["']tailwindcss["']/)) { + node.params += prefixParams + } + + // Track old imports and directives + else if ( + (node.name === 'tailwind' && node.params === 'base') || + (node.name === 'import' && node.params.match(/^["']tailwindcss\/base["']$/)) + ) { + layerOrder.push('base') + orderedNodes.push(node) + baseNode = node + } else if ( + (node.name === 'tailwind' && node.params === 'utilities') || + (node.name === 'import' && node.params.match(/^["']tailwindcss\/utilities["']$/)) + ) { + layerOrder.push('utilities') + orderedNodes.push(node) + utilitiesNode = node + } + + // Remove directives that are not needed anymore + else if ( + (node.name === 'tailwind' && node.params === 'components') || + (node.name === 'tailwind' && node.params === 'screens') || + (node.name === 'tailwind' && node.params === 'variants') || + (node.name === 'import' && node.params.match(/^["']tailwindcss\/components["']$/)) + ) { + node.remove() + } + + // Replace Tailwind CSS v2 directives that still worked in v3. + else if (node.name === 'responsive') { + if (node.nodes) { + for (let child of node.nodes) { + child.raws.tailwind_pretty = true + } + node.replaceWith(node.nodes) + } else { + node.remove() + } + } + }) + + // Insert default import if all directives are present + if (baseNode !== null && utilitiesNode !== null) { + if (!defaultImportNode) { + findTargetNode(orderedNodes).before( + new AtRule({ name: 'import', params: `'tailwindcss'${prefixParams}` }), + ) + } + baseNode?.remove() + utilitiesNode?.remove() + } + + // Insert individual imports if not all directives are present + else if (utilitiesNode !== null) { + if (!utilitiesImportNode) { + findTargetNode(orderedNodes).before( + new AtRule({ name: 'import', params: "'tailwindcss/utilities' layer(utilities)" }), + ) + } + utilitiesNode?.remove() + } else if (baseNode !== null) { + if (!themeImportNode) { + findTargetNode(orderedNodes).before( + new AtRule({ name: 'import', params: `'tailwindcss/theme' layer(theme)${prefixParams}` }), + ) + } + + if (!preflightImportNode) { + findTargetNode(orderedNodes).before( + new AtRule({ name: 'import', params: "'tailwindcss/preflight' layer(base)" }), + ) + } + + baseNode?.remove() + } + + // Insert `@layer …;` at the top when the order in the CSS was different + // from the default. + { + // Determine if the order is different from the default. + let sortedLayerOrder = layerOrder.toSorted((a, z) => { + return DEFAULT_LAYER_ORDER.indexOf(a) - DEFAULT_LAYER_ORDER.indexOf(z) + }) + + if (layerOrder.some((layer, index) => layer !== sortedLayerOrder[index])) { + // Create a new `@layer` rule with the sorted order. + let newLayerOrder = DEFAULT_LAYER_ORDER.toSorted((a, z) => { + return layerOrder.indexOf(a) - layerOrder.indexOf(z) + }) + root.prepend({ name: 'layer', params: newLayerOrder.join(', ') }) + } + } + } + + return { + postcssPlugin: '@tailwindcss/upgrade/migrate-tailwind-directives', + OnceExit: migrate, + } +} + +// Finds the location where we can inject the new `@import` at-rule. This +// guarantees that the `@import` is inserted at the most expected location. +// +// Ideally it's replacing the existing Tailwind directives, but we have to +// ensure that the `@import` is valid in this location or not. If not, we move +// the `@import` up until we find a valid location. +function findTargetNode(nodes: AtRule[]) { + // Start at the `base` or `utilities` node (whichever comes first), and find + // the spot where we can insert the new import. + let target: ChildNode = nodes.at(0)! + + // Only allowed nodes before the `@import` are: + // + // - `@charset` at-rule. + // - `@layer foo, bar, baz;` at-rule to define the order of the layers. + // - `@import` at-rule to import other CSS files. + // - Comments. + // + // Nodes that cannot exist before the `@import` are: + // + // - Any other at-rule. + // - Any rule. + let previous = target.prev() + while (previous) { + // Rules are not allowed before the `@import`, so we have to at least inject + // the `@import` before this rule. + if (previous.type === 'rule') { + target = previous + } + + // Some at-rules are allowed before the `@import`. + if (previous.type === 'atrule') { + // `@charset` and `@import` are allowed before the `@import`. + if (previous.name === 'charset' || previous.name === 'import') { + // Allowed + previous = previous.prev() + continue + } + + // `@layer` without any nodes is allowed before the `@import`. + else if (previous.name === 'layer' && !previous.nodes) { + // Allowed + previous = previous.prev() + continue + } + + // Anything other at-rule (`@media`, `@supports`, etc.) is not allowed + // before the `@import`. + else { + target = previous + } + } + + // Keep checking the previous node. + previous = previous.prev() + } + + return target +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-theme-to-var.test.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-theme-to-var.test.ts new file mode 100644 index 000000000000..f0b4844ade07 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-theme-to-var.test.ts @@ -0,0 +1,46 @@ +import { __unstable__loadDesignSystem } from '@tailwindcss/node' +import dedent from 'dedent' +import postcss from 'postcss' +import { expect, it } from 'vitest' +import { formatNodes } from './format-nodes' +import { migrateThemeToVar } from './migrate-theme-to-var' +import { sortBuckets } from './sort-buckets' + +const css = dedent + +async function migrate(input: string) { + return postcss() + .use( + migrateThemeToVar({ + designSystem: await __unstable__loadDesignSystem(`@import 'tailwindcss';`, { + base: __dirname, + }), + }), + ) + .use(sortBuckets()) + .use(formatNodes()) + .process(input, { from: expect.getState().testPath }) + .then((result) => result.css) +} + +it('should migrate `theme(…)` to `var(…)`', async () => { + expect( + await migrate(css` + @media theme(screens.sm) { + .foo { + background-color: theme(colors.red.900); + color: theme(colors.red.900 / 75%); + border-color: theme(colors.red.200/75%); + } + } + `), + ).toMatchInlineSnapshot(` + "@media --theme(--breakpoint-sm) { + .foo { + background-color: var(--color-red-900); + color: --theme(--color-red-900 / 75%); + border-color: --theme(--color-red-200 / 75%); + } + }" + `) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-theme-to-var.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-theme-to-var.ts new file mode 100644 index 000000000000..8285def1e432 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-theme-to-var.ts @@ -0,0 +1,34 @@ +import { type Plugin } from 'postcss' +import type { DesignSystem } from '../../../../tailwindcss/src/design-system' +import { Convert, createConverter } from '../template/migrate-theme-to-var' + +export function migrateThemeToVar({ + designSystem, +}: { + designSystem?: DesignSystem | null +} = {}): Plugin { + return { + postcssPlugin: '@tailwindcss/upgrade/migrate-theme-to-var', + OnceExit(root) { + if (!designSystem) return + let convert = createConverter(designSystem, { prettyPrint: true }) + + root.walkDecls((decl) => { + let [newValue] = convert(decl.value) + decl.value = newValue + }) + + root.walkAtRules((atRule) => { + if ( + atRule.name === 'media' || + atRule.name === 'custom-media' || + atRule.name === 'container' || + atRule.name === 'supports' + ) { + let [newValue] = convert(atRule.params, Convert.MigrateThemeOnly) + atRule.params = newValue + } + }) + }, + } +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-variants-directive.test.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-variants-directive.test.ts new file mode 100644 index 000000000000..3521d34f7039 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-variants-directive.test.ts @@ -0,0 +1,35 @@ +import dedent from 'dedent' +import postcss from 'postcss' +import { expect, it } from 'vitest' +import { formatNodes } from './format-nodes' +import { migrateVariantsDirective } from './migrate-variants-directive' +import { sortBuckets } from './sort-buckets' + +const css = dedent + +function migrate(input: string) { + return postcss() + .use(migrateVariantsDirective()) + .use(sortBuckets()) + .use(formatNodes()) + .process(input, { from: expect.getState().testPath }) + .then((result) => result.css) +} + +it('should replace `@variants` with `@layer utilities`', async () => { + expect( + await migrate(css` + @variants hover, focus { + .foo { + color: red; + } + } + `), + ).toMatchInlineSnapshot(` + "@layer utilities { + .foo { + color: red; + } + }" + `) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate-variants-directive.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-variants-directive.ts new file mode 100644 index 000000000000..1592c0059dc2 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate-variants-directive.ts @@ -0,0 +1,35 @@ +import { type Plugin, type Root } from 'postcss' + +export function migrateVariantsDirective(): Plugin { + function migrate(root: Root) { + root.walkAtRules('variants', (node) => { + // Migrate `@variants` to `@utility` because `@variants` make the classes + // an actual utility. + // ```css + // @variants hover { + // .foo {} + // } + // ``` + // + // Means that you can do this in your HTML: + // ```html + //
+ // ``` + // + // Notice the `focus:`, even though we _only_ configured the `hover` + // variant. + // + // This means that we can convert it to an `@layer utilities` rule. Later, + // this will get converted to an `@utility` rule. + if (node.name === 'variants') { + node.name = 'layer' + node.params = 'utilities' + } + }) + } + + return { + postcssPlugin: '@tailwindcss/upgrade/migrate-variants-directive', + OnceExit: migrate, + } +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/migrate.ts b/packages/@tailwindcss-upgrade/src/codemods/css/migrate.ts new file mode 100644 index 000000000000..cb91508513fe --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/migrate.ts @@ -0,0 +1,57 @@ +import postcss from 'postcss' +import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' +import type { DesignSystem } from '../../../../tailwindcss/src/design-system' +import { Stylesheet } from '../../stylesheet' +import type { JSConfigMigration } from '../config/migrate-js-config' +import { migrateAtApply } from './migrate-at-apply' +import { migrateAtLayerUtilities } from './migrate-at-layer-utilities' +import { migrateConfig } from './migrate-config' +import { migrateImport } from './migrate-import' +import { migrateMediaScreen } from './migrate-media-screen' +import { migrateMissingLayers } from './migrate-missing-layers' +import { migratePreflight } from './migrate-preflight' +import { migrateTailwindDirectives } from './migrate-tailwind-directives' +import { migrateThemeToVar } from './migrate-theme-to-var' +import { migrateVariantsDirective } from './migrate-variants-directive' + +export interface MigrateOptions { + newPrefix: string | null + designSystem: DesignSystem | null + userConfig: Config | null + configFilePath: string | null + jsConfigMigration: JSConfigMigration | null +} + +export async function migrate(stylesheet: Stylesheet, options: MigrateOptions) { + if (!stylesheet.file) { + throw new Error('Cannot migrate a stylesheet without a file path') + } + + if (!stylesheet.canMigrate) return + + await migrateContents(stylesheet, options) +} + +export async function migrateContents( + stylesheet: Stylesheet | string, + options: MigrateOptions, + file?: string, +) { + if (typeof stylesheet === 'string') { + stylesheet = await Stylesheet.fromString(stylesheet) + stylesheet.file = file ?? null + } + + return postcss() + .use(migrateImport()) + .use(migrateAtApply(options)) + .use(migrateMediaScreen(options)) + .use(migrateVariantsDirective()) + .use(migrateAtLayerUtilities(stylesheet)) + .use(migrateMissingLayers()) + .use(migrateTailwindDirectives(options)) + .use(migrateConfig(stylesheet, options)) + .use(migratePreflight(options)) + .use(migrateThemeToVar(options)) + .process(stylesheet.root, { from: stylesheet.file ?? undefined }) +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/sort-buckets.ts b/packages/@tailwindcss-upgrade/src/codemods/css/sort-buckets.ts new file mode 100644 index 000000000000..22b0581ddb5a --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/sort-buckets.ts @@ -0,0 +1,161 @@ +import postcss, { type AtRule, type ChildNode, type Comment, type Plugin, type Root } from 'postcss' +import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map' +import { walk, WalkAction } from '../../utils/walk' + +const BUCKET_ORDER = [ + // Imports + 'import', // @import + + // Configuration + 'config', // @config + 'plugin', // @plugin + 'source', // @source + 'custom-variant', // @custom-variant + 'theme', // @theme + + // Styles + 'compatibility', // @layer base with compatibility CSS + 'utility', // @utility + + // User CSS + 'user', +] + +export function sortBuckets(): Plugin { + async function migrate(root: Root) { + // 1. Move items that are not in a bucket, into a bucket + { + let comments: Comment[] = [] + + let buckets = new DefaultMap((name) => { + let bucket = postcss.atRule({ name: 'tw-bucket', params: name, nodes: [] }) + root.append(bucket) + return bucket + }) + + // Seed the buckets with existing buckets + root.walkAtRules('tw-bucket', (node) => { + buckets.set(node.params, node) + }) + + let lastLayer = 'user' + function injectInto(name: string, ...nodes: ChildNode[]) { + lastLayer = name + buckets.get(name).nodes?.push(...comments.splice(0), ...nodes) + } + + walk(root, (node) => { + // Already in a bucket, skip it + if (node.type === 'atrule' && node.name === 'tw-bucket') { + return WalkAction.Skip + } + + // Comments belong to the bucket of the nearest node, which is typically + // in the "next" bucket. + if (node.type === 'comment') { + // We already have comments, which means that we already have nodes + // that belong in the next bucket, so we should move the current + // comment into the next bucket as well. + if (comments.length > 0) { + comments.push(node) + return + } + + // Figure out the closest node to the comment + let prevDistance = distance(node.prev(), node) ?? Infinity + let nextDistance = distance(node, node.next()) ?? Infinity + + if (prevDistance < nextDistance) { + buckets.get(lastLayer).nodes?.push(node) + } else { + comments.push(node) + } + } + + // Known at-rules + else if ( + node.type === 'atrule' && + ['config', 'plugin', 'source', 'theme', 'utility', 'custom-variant'].includes(node.name) + ) { + injectInto(node.name, node) + } + + // Imports bucket, which also contains the `@charset` and body-less `@layer` + else if ( + (node.type === 'atrule' && node.name === 'layer' && !node.nodes) || // @layer foo, bar; + (node.type === 'atrule' && node.name === 'import') || + (node.type === 'atrule' && node.name === 'charset') || // @charset "UTF-8"; + (node.type === 'atrule' && node.name === 'tailwind') + ) { + injectInto('import', node) + } + + // User CSS + else if (node.type === 'rule' || node.type === 'atrule') { + injectInto('user', node) + } + + // Fallback + else { + injectInto('user', node) + } + + return WalkAction.Skip + }) + + if (comments.length > 0) { + injectInto(lastLayer) + } + } + + // 2. Merge `@tw-bucket` with the same name together + let firstBuckets = new Map() + root.walkAtRules('tw-bucket', (node) => { + let firstBucket = firstBuckets.get(node.params) + if (!firstBucket) { + firstBuckets.set(node.params, node) + return + } + + if (node.nodes) { + firstBucket.append(...node.nodes) + } + }) + + // 3. Remove empty `@tw-bucket` + root.walkAtRules('tw-bucket', (node) => { + if (!node.nodes?.length) { + node.remove() + } + }) + + // 4. Sort the `@tw-bucket` themselves + { + let sorted = Array.from(firstBuckets.values()).sort((a, z) => { + let aIndex = BUCKET_ORDER.indexOf(a.params) + let zIndex = BUCKET_ORDER.indexOf(z.params) + return aIndex - zIndex + }) + + // Re-inject the sorted buckets + root.removeAll() + root.append(sorted) + } + } + + return { + postcssPlugin: '@tailwindcss/upgrade/sort-buckets', + OnceExit: migrate, + } +} + +function distance(before?: ChildNode, after?: ChildNode): number | null { + if (!before || !after) return null + if (!before.source || !after.source) return null + if (!before.source.start || !after.source.start) return null + if (!before.source.end || !after.source.end) return null + + // Compare end of Before, to start of After + let d = Math.abs(before.source.end.line - after.source.start.line) + return d +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/css/split.ts b/packages/@tailwindcss-upgrade/src/codemods/css/split.ts new file mode 100644 index 000000000000..146d30733cbb --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/css/split.ts @@ -0,0 +1,257 @@ +import postcss from 'postcss' +import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map' +import { Stylesheet, type StylesheetId } from '../../stylesheet' +import { walk, WalkAction } from '../../utils/walk' + +export async function split(stylesheets: Stylesheet[]) { + let stylesheetsById = new Map() + let stylesheetsByFile = new Map() + + for (let sheet of stylesheets) { + stylesheetsById.set(sheet.id, sheet) + + if (sheet.file) { + stylesheetsByFile.set(sheet.file, sheet) + } + } + + // Keep track of sheets that contain `@utility` rules + let requiresSplit = new Set() + + for (let sheet of stylesheets) { + // Root files don't need to be split + if (sheet.isTailwindRoot) continue + + let containsUtility = false + let containsUnsafe = sheet.layers().size > 0 + + walk(sheet.root, (node) => { + if (node.type === 'atrule' && node.name === 'utility') { + containsUtility = true + } + + // Safe to keep without splitting + else if ( + // An `@import "…" layer(…)` is safe + (node.type === 'atrule' && node.name === 'import' && node.params.includes('layer(')) || + // @layer blocks are safe + (node.type === 'atrule' && node.name === 'layer') || + // Comments are safe + node.type === 'comment' + ) { + return WalkAction.Skip + } + + // Everything else is not safe, and requires a split + else { + containsUnsafe = true + } + + // We already know we need to split this sheet + if (containsUtility && containsUnsafe) { + return WalkAction.Stop + } + + return WalkAction.Skip + }) + + if (containsUtility && containsUnsafe) { + requiresSplit.add(sheet) + } + } + + // Split every imported stylesheet into two parts + let utilitySheets = new Map() + + for (let sheet of stylesheets) { + // Ignore stylesheets that were not imported + if (!sheet.file) continue + if (sheet.parents.size === 0) continue + + // Skip stylesheets that don't have utilities + // and don't have any children that have utilities + if (!requiresSplit.has(sheet)) { + if (!Array.from(sheet.descendants()).some((child) => requiresSplit.has(child))) { + continue + } + } + + let utilities = postcss.root() + + walk(sheet.root, (node) => { + if (node.type !== 'atrule') return + if (node.name !== 'utility') return + + // `append` will move this node from the original sheet + // to the new utilities sheet + utilities.append(node) + + return WalkAction.Skip + }) + + let newFileName = sheet.file.replace(/\.css$/, '.utilities.css') + + let counter = 0 + + // If we already have a utility sheet with this name, we need to rename it + while (stylesheetsByFile.has(newFileName)) { + counter += 1 + newFileName = sheet.file.replace(/\.css$/, `.utilities.${counter}.css`) + } + + let utilitySheet = await Stylesheet.fromRoot(utilities, newFileName) + + utilitySheet.extension = counter > 0 ? `.utilities.${counter}.css` : `.utilities.css` + + utilitySheets.set(sheet, utilitySheet) + stylesheetsById.set(utilitySheet.id, utilitySheet) + } + + // Make sure the utility sheets are linked to one another + for (let [normalSheet, utilitySheet] of utilitySheets) { + for (let parent of normalSheet.parents) { + let utilityParent = utilitySheets.get(parent.item) + if (!utilityParent) continue + utilitySheet.parents.add({ + item: utilityParent, + meta: parent.meta, + }) + } + + for (let child of normalSheet.children) { + let utilityChild = utilitySheets.get(child.item) + if (!utilityChild) continue + utilitySheet.children.add({ + item: utilityChild, + meta: child.meta, + }) + } + } + + for (let sheet of stylesheets) { + let utilitySheet = utilitySheets.get(sheet) + let utilityImports: Set = new Set() + + for (let node of sheet.importRules) { + let sheetId = node.raws.tailwind_destination_sheet_id as StylesheetId | undefined + + // This import rule does not point to a stylesheet + // which likely means it points to `node_modules` + if (!sheetId) continue + + let originalDestination = stylesheetsById.get(sheetId) + + // This import points to a stylesheet that no longer exists which likely + // means it was removed by the optimizer this will be cleaned up later + if (!originalDestination) continue + + let utilityDestination = utilitySheets.get(originalDestination) + + // A utility sheet doesn't exist for this import so it doesn't need + // to be processed + if (!utilityDestination) continue + + let match = node.params.match(/(['"])(.*)\1/) + if (!match) return + + let quote = match[1] + let id = match[2] + + let newFile = id.replace(/\.css$/, utilityDestination.extension!) + + // The import will just point to the new file without any media queries, + // layers, or other conditions because `@utility` MUST be top-level. + let newImport = node.clone({ + params: `${quote}${newFile}${quote}`, + raws: { + tailwind_injected_layer: node.raws.tailwind_injected_layer, + tailwind_original_params: `${quote}${id}${quote}`, + tailwind_destination_sheet_id: utilityDestination.id, + }, + }) + + if (utilitySheet) { + // If this import is intended to go into the utility sheet + // we'll collect it into a list to add later. If we don't' + // we'll end up adding them in reverse order. + utilityImports.add(newImport) + } else { + // This import will go immediately after the original import + node.after(newImport) + } + } + + // Add imports to the top of the utility sheet if necessary + if (utilitySheet && utilityImports.size > 0) { + utilitySheet.root.prepend(Array.from(utilityImports)) + } + } + + // Tracks the at rules that import a given stylesheet + let importNodes = new DefaultMap>(() => new Set()) + + for (let sheet of stylesheetsById.values()) { + for (let node of sheet.importRules) { + let sheetId = node.raws.tailwind_destination_sheet_id as StylesheetId | undefined + + // This import rule does not point to a stylesheet + if (!sheetId) continue + + let destination = stylesheetsById.get(sheetId) + + // This import rule does not point to a stylesheet that exists + // We'll remove it later + if (!destination) continue + + importNodes.get(destination).add(node) + } + } + + // At this point we've created many `{name}.utilities.css` files. + // If the original file _becomes_ empty after splitting that means that + // dedicated utility file is not required and we can move the utilities + // back to the original file. + // + // This could be done in one step but separating them makes it easier to + // reason about since the stylesheets are in a consistent state before we + // perform any cleanup tasks. + let list: Stylesheet[] = [] + + for (let sheet of stylesheets.slice()) { + for (let child of sheet.descendants()) { + list.push(child) + } + + list.push(sheet) + } + + for (let sheet of list) { + let utilitySheet = utilitySheets.get(sheet) + + // This sheet was not split so there's nothing to do + if (!utilitySheet) continue + + // This sheet did not become empty + if (!sheet.isEmpty) continue + + // We have a sheet that became empty after splitting + // 1. Replace the sheet with it's utility sheet content + sheet.root = utilitySheet.root + + // 2. Rewrite imports in parent sheets to point to the original sheet + // Ideally this wouldn't need to be _undone_ but instead only done once at the end + for (let node of importNodes.get(utilitySheet)) { + node.params = node.raws.tailwind_original_params as any + } + + // 3. Remove the original import from the non-utility sheet + for (let node of importNodes.get(sheet)) { + node.remove() + } + + // 3. Mark the utility sheet for removal + utilitySheets.delete(sheet) + } + + stylesheets.push(...utilitySheets.values()) +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/candidates.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/candidates.test.ts new file mode 100644 index 000000000000..2e4d150aa55a --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/template/candidates.test.ts @@ -0,0 +1,84 @@ +import { __unstable__loadDesignSystem } from '@tailwindcss/node' +import { expect, test } from 'vitest' +import { spliceChangesIntoString } from '../../utils/splice-changes-into-string' +import { extractRawCandidates } from './candidates' + +let html = String.raw + +test('extracts candidates with positions from a template', async () => { + let content = html` +
+ +
+ ` + let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', { + base: __dirname, + }) + + let candidates = await extractRawCandidates(content, 'html') + let validCandidates = candidates.filter( + ({ rawCandidate }) => designSystem.parseCandidate(rawCandidate).length > 0, + ) + + expect(validCandidates).toMatchInlineSnapshot(` + [ + { + "end": 28, + "rawCandidate": "bg-blue-500", + "start": 17, + }, + { + "end": 51, + "rawCandidate": "hover:focus:text-white", + "start": 29, + }, + { + "end": 63, + "rawCandidate": "[color:red]", + "start": 52, + }, + { + "end": 98, + "rawCandidate": "bg-blue-500", + "start": 87, + }, + { + "end": 109, + "rawCandidate": "text-white", + "start": 99, + }, + ] + `) +}) + +test('replaces the right positions for a candidate', async () => { + let content = html` +

🤠👋

+
+ ` + + let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', { + base: __dirname, + }) + + let candidates = await extractRawCandidates(content, 'html') + + let candidate = candidates.find( + ({ rawCandidate }) => designSystem.parseCandidate(rawCandidate).length > 0, + )! + + let migrated = spliceChangesIntoString(content, [ + { + start: candidate.start, + end: candidate.end, + replacement: 'flex', + }, + ]) + + expect(migrated).toMatchInlineSnapshot(` + " +

🤠👋

+
+ " + `) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/candidates.ts b/packages/@tailwindcss-upgrade/src/codemods/template/candidates.ts new file mode 100644 index 000000000000..88b1e4e0307f --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/template/candidates.ts @@ -0,0 +1,43 @@ +import { Scanner } from '@tailwindcss/oxide' +import { cloneCandidate, type Candidate } from '../../../../tailwindcss/src/candidate' +import type { DesignSystem } from '../../../../tailwindcss/src/design-system' + +export async function extractRawCandidates( + content: string, + extension: string = 'html', +): Promise<{ rawCandidate: string; start: number; end: number }[]> { + let scanner = new Scanner({}) + let result = scanner.getCandidatesWithPositions({ content, extension }) + + let candidates: { rawCandidate: string; start: number; end: number }[] = [] + for (let { candidate: rawCandidate, position: start } of result) { + candidates.push({ rawCandidate, start, end: start + rawCandidate.length }) + } + return candidates +} + +// Create a basic stripped candidate without variants or important flag +export function baseCandidate(candidate: T) { + let base = cloneCandidate(candidate) + + base.important = false + base.variants = [] + + return base +} + +export function parseCandidate(designSystem: DesignSystem, input: string) { + return designSystem.parseCandidate( + designSystem.theme.prefix && !input.startsWith(`${designSystem.theme.prefix}:`) + ? `${designSystem.theme.prefix}:${input}` + : input, + ) +} + +export function printUnprefixedCandidate(designSystem: DesignSystem, candidate: Candidate) { + let candidateString = designSystem.printCandidate(candidate) + + return designSystem.theme.prefix && candidateString.startsWith(`${designSystem.theme.prefix}:`) + ? candidateString.slice(designSystem.theme.prefix.length + 1) + : candidateString +} diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.test.ts b/packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.test.ts new file mode 100644 index 000000000000..ff12fc114c13 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.test.ts @@ -0,0 +1,150 @@ +import { __unstable__loadDesignSystem } from '@tailwindcss/node' +import { describe, expect, test, vi } from 'vitest' +import * as versions from '../../utils/version' +import { migrateCandidate } from './migrate' +vi.spyOn(versions, 'isMajor').mockReturnValue(true) + +const css = String.raw + +describe('is-safe-migration', async () => { + let designSystem = await __unstable__loadDesignSystem( + css` + @import 'tailwindcss'; + + /* TODO(perf): Only here to speed up the tests */ + @theme { + --*: initial; + --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + } + `, + { base: __dirname }, + ) + + test.each([ + [`let notBorder = !border \n`, '!border'], + [`{ "foo": !border.something + ""}\n`, '!border'], + [`
\n`, '!border'], + [`
\n`, '!border'], + [`
\n`, '!border'], + [`
\n`, '!border'], + [`
\n`, '!border'], + [`
\n`, '!border'], + [`
\n`, '!border'], + [`
\n`, '!border'], + [`
\n`, '!border'], + [`
\n`, '!border'], + + [`let notShadow = shadow \n`, 'shadow'], + [`{ "foo": shadow.something + ""}\n`, 'shadow'], + [`
\n`, 'shadow'], + [`
\n`, 'shadow'], + [`
\n`, 'shadow'], + [`
\n`, 'shadow'], + [`
\n`, 'shadow'], + [`
\n`, 'shadow'], + [`
\n`, 'shadow'], + [`
\n`, 'shadow'], + [`
\n`, 'shadow'], + [`
\n`, 'shadow'], + [`
\n`, 'shadow'], + + // Next.js Image placeholder cases + [``, 'blur'], + [``, 'blur'], + [``, 'blur'], + + // https://github.com/tailwindlabs/tailwindcss/issues/17974 + ['
', '!duration'], + ['
', '!duration'], + ['
', '!visible'], + + // Alpine/Livewire wire:… + ['', 'blur'], + + // Vue 3 events + [`emit('blur', props.modelValue)\n`, 'blur'], + [`$emit('blur', props.modelValue)\n`, 'blur'], + + // JavaScript / TypeScript + [`document.addEventListener('blur',handleBlur)`, 'blur'], + [`document.addEventListener('blur', handleBlur)`, 'blur'], + + [`function foo({ outline = true })`, 'outline'], + [`function foo({ before = false, outline = true })`, 'outline'], + [`function foo({before=false,outline=true })`, 'outline'], + [`function foo({outline=true })`, 'outline'], + // https://github.com/tailwindlabs/tailwindcss/issues/18675 + [ + // With default value + `function foo({ size = "1.25rem", digit, outline = true, textClass = "", className = "" })`, + 'outline', + ], + [ + // Without default value + `function foo({ size = "1.25rem", digit, outline, textClass = "", className = "" })`, + 'outline', + ], + [ + // As the last argument + `function foo({ size = "1.25rem", digit, outline })`, + 'outline', + ], + [ + // As the last argument, but there is technically another `"` on the same line + `function foo({ size = "1.25rem", digit, outline }): { return "foo" }`, + 'outline', + ], + [ + // Tricky quote balancing + `function foo({ before = "'", outline, after = "'" }): { return "foo" }`, + 'outline', + ], + + [`function foo(blur, foo)`, 'blur'], + [`function foo(blur,foo)`, 'blur'], + + // shadcn/ui variants + [`