├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md ├── logo-dark.svg ├── logo-light.svg └── workflows │ ├── build-cli.yml │ ├── integration-tests.yml │ ├── nodejs.yml │ ├── release-insiders.yml │ └── release.yml ├── .gitignore ├── .swcrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── base.css ├── colors.d.ts ├── colors.js ├── components.css ├── defaultConfig.d.ts ├── defaultConfig.js ├── defaultTheme.d.ts ├── defaultTheme.js ├── integrations ├── .gitignore ├── execute.js ├── io.js ├── package-lock.json ├── package.json ├── parcel │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── src │ │ ├── glob │ │ │ └── index.html │ │ ├── index.css │ │ └── index.html │ ├── tailwind.config.js │ └── tests │ │ └── integration.test.js ├── postcss-cli │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── src │ │ ├── glob │ │ │ └── index.html │ │ ├── index.css │ │ └── index.html │ ├── tailwind.config.js │ └── tests │ │ └── integration.test.js ├── resolve-tool-root.js ├── rollup-sass │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── rollup.config.js │ ├── src │ │ ├── glob │ │ │ └── index.html │ │ ├── imported.scss │ │ ├── index.html │ │ ├── index.js │ │ └── index.scss │ ├── tailwind.config.js │ └── tests │ │ └── integration.test.js ├── rollup │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── rollup.config.js │ ├── src │ │ ├── glob │ │ │ └── index.html │ │ ├── index.css │ │ ├── index.html │ │ └── index.js │ ├── tailwind.config.js │ └── tests │ │ └── integration.test.js ├── syntax.js ├── tailwindcss-cli │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── src │ │ ├── glob │ │ │ └── index.html │ │ ├── index.css │ │ └── index.html │ ├── tailwind.config.js │ └── tests │ │ ├── cli.test.js │ │ └── integration.test.js ├── vite │ ├── .gitignore │ ├── glob │ │ └── index.html │ ├── index.css │ ├── index.html │ ├── index.js │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── tailwind.config.js │ └── tests │ │ └── integration.test.js ├── webpack-4 │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── src │ │ ├── glob │ │ │ └── index.html │ │ ├── index.css │ │ ├── index.html │ │ └── index.js │ ├── tailwind.config.js │ ├── tests │ │ └── integration.test.js │ └── webpack.config.js └── webpack-5 │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── src │ ├── glob │ │ └── index.html │ ├── index.css │ ├── index.html │ └── index.js │ ├── tailwind.config.js │ ├── tests │ └── integration.test.js │ └── webpack.config.js ├── jest ├── customMatchers.js └── runInTempDirectory.js ├── nesting └── index.js ├── package-lock.json ├── package.json ├── perf ├── .gitignore ├── fixture.css ├── script.sh └── tailwind.config.js ├── plugin.d.ts ├── plugin.js ├── prettier.config.js ├── resolveConfig.js ├── screens.css ├── scripts ├── create-plugin-list.js ├── generate-types.js ├── install-integrations.js └── rebuildFixtures.js ├── src ├── cli-peer-dependencies.js ├── cli.js ├── constants.js ├── corePlugins.js ├── css │ ├── LICENSE │ └── preflight.css ├── featureFlags.js ├── index.js ├── lib │ ├── cacheInvalidation.js │ ├── collapseAdjacentRules.js │ ├── collapseDuplicateDeclarations.js │ ├── defaultExtractor.js │ ├── detectNesting.js │ ├── evaluateTailwindFunctions.js │ ├── expandApplyAtRules.js │ ├── expandTailwindAtRules.js │ ├── generateRules.js │ ├── getModuleDependencies.js │ ├── normalizeTailwindDirectives.js │ ├── partitionApplyAtRules.js │ ├── regex.js │ ├── resolveDefaultsAtRules.js │ ├── setupContextUtils.js │ ├── setupTrackingContext.js │ ├── sharedState.js │ └── substituteScreenAtRules.js ├── postcss-plugins │ └── nesting │ │ ├── README.md │ │ ├── index.js │ │ └── plugin.js ├── processTailwindFeatures.js ├── public │ ├── colors.js │ ├── create-plugin.js │ ├── default-config.js │ ├── default-theme.js │ └── resolve-config.js └── util │ ├── bigSign.js │ ├── buildMediaQuery.js │ ├── cloneDeep.js │ ├── cloneNodes.js │ ├── color.js │ ├── configurePlugins.js │ ├── createPlugin.js │ ├── createUtilityPlugin.js │ ├── dataTypes.js │ ├── defaults.js │ ├── escapeClassName.js │ ├── escapeCommas.js │ ├── flattenColorPalette.js │ ├── formatVariantSelector.js │ ├── getAllConfigs.js │ ├── hashConfig.js │ ├── isKeyframeRule.js │ ├── isPlainObject.js │ ├── isValidArbitraryValue.js │ ├── log.js │ ├── nameClass.js │ ├── negateValue.js │ ├── normalizeConfig.js │ ├── normalizeScreens.js │ ├── parseAnimationValue.js │ ├── parseBoxShadowValue.js │ ├── parseDependency.js │ ├── parseObjectStyles.js │ ├── pluginUtils.js │ ├── prefixSelector.js │ ├── resolveConfig.js │ ├── resolveConfigPath.js │ ├── responsive.js │ ├── splitAtTopLevelOnly.js │ ├── tap.js │ ├── toColorValue.js │ ├── toPath.js │ ├── transformThemeValue.js │ ├── validateConfig.js │ └── withAlphaVariable.js ├── standalone-cli ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── standalone.js └── tests │ ├── fixtures │ ├── basic.html │ ├── plugins.html │ └── test.config.js │ └── test.js ├── stubs ├── .prettierrc.json ├── defaultConfig.stub.js ├── defaultPostCssConfig.stub.js └── simpleConfig.stub.js ├── tailwind.css ├── tests ├── __snapshots__ │ └── source-maps.test.js.snap ├── animations.test.js ├── apply.test.css ├── apply.test.html ├── apply.test.js ├── arbitrary-properties.test.js ├── arbitrary-values.test.css ├── arbitrary-values.test.html ├── arbitrary-values.test.js ├── arbitrary-variants.test.js ├── basic-usage.test.css ├── basic-usage.test.html ├── basic-usage.test.js ├── collapse-adjacent-rules.test.css ├── collapse-adjacent-rules.test.html ├── collapse-adjacent-rules.test.js ├── collapse-duplicate-declarations.test.js ├── color-opacity-modifiers.test.js ├── color.test.js ├── combined-selectors.test.js ├── configurePlugins.test.js ├── containerPlugin.test.js ├── context-reuse.test.html ├── context-reuse.test.js ├── custom-extractors.test.css ├── custom-extractors.test.html ├── custom-extractors.test.js ├── custom-plugins.test.js ├── custom-separator.test.css ├── custom-separator.test.html ├── custom-separator.test.js ├── custom-transformers.test.html ├── custom-transformers.test.js ├── custom-transformers.test.php ├── customConfig.test.js ├── dark-mode.test.js ├── default-extractor.test.js ├── defaultConfig.test.js ├── defaultTheme.test.js ├── detect-nesting.test.js ├── escapeClassName.test.js ├── evaluateTailwindFunctions.test.js ├── experimental.test.js ├── extractor-edge-cases.test.js ├── fixtures │ ├── custom-config.js │ ├── custom-purge-config.js │ ├── esm-package.json │ └── purge-example.html ├── flattenColorPalette.test.js ├── format-variant-selector.test.js ├── getClassList.test.js ├── getSortOrder.test.js ├── import-syntax.test.css ├── import-syntax.test.html ├── import-syntax.test.js ├── important-boolean.test.css ├── important-boolean.test.html ├── important-boolean.test.js ├── important-modifier-prefix.test.css ├── important-modifier-prefix.test.html ├── important-modifier-prefix.test.js ├── important-modifier.test.js ├── important-selector.test.css ├── important-selector.test.html ├── important-selector.test.js ├── kitchen-sink.test.css ├── kitchen-sink.test.html ├── kitchen-sink.test.js ├── layer-at-rules.test.js ├── layer-without-tailwind.test.css ├── layer-without-tailwind.test.html ├── layer-without-tailwind.test.js ├── match-components.test.js ├── match-variants.test.js ├── minimum-impact-selector.test.js ├── modify-selectors.test.css ├── modify-selectors.test.html ├── modify-selectors.test.js ├── mutable.test.css ├── mutable.test.html ├── mutable.test.js ├── negateValue.test.js ├── negated-content-ignore.test.html ├── negated-content-include.test.html ├── negated-content.test.js ├── negative-prefix.test.js ├── normalize-config.test.js ├── normalize-data-types.test.js ├── normalize-screens.test.js ├── opacity.test.js ├── parallel-variants.test.js ├── parseAnimationValue.test.js ├── parseObjectStyles.test.js ├── plugins │ ├── divide.test.js │ ├── fontSize.test.js │ └── gradientColorStops.test.js ├── postcss-plugins │ └── nesting │ │ ├── index.test.js │ │ └── plugins.js ├── prefix.test.css ├── prefix.test.html ├── prefix.test.js ├── prefixSelector.test.js ├── preflight.test.js ├── raw-content.test.css ├── raw-content.test.html ├── raw-content.test.js ├── relative-purge-paths.test.html ├── relative-purge-paths.test.js ├── resolve-defaults-at-rules.test.js ├── resolveConfig.test.js ├── responsive-and-variants-atrules.test.css ├── responsive-and-variants-atrules.test.html ├── responsive-and-variants-atrules.test.js ├── safelist.test.js ├── screenAtRule.test.js ├── shared-state.test.js ├── source-maps.test.js ├── syntax-lit-html.test.js ├── syntax-svelte.test.js ├── syntax-svelte.test.svelte ├── tailwind-screens.test.js ├── to-path.test.js ├── util │ ├── defaults.js │ ├── run.js │ ├── source-maps.js │ └── strings.js ├── variants.test.css ├── variants.test.html ├── variants.test.js ├── warnings.test.js └── withAlphaVariable.test.js ├── types.d.ts ├── types ├── config.d.ts ├── generated │ └── .gitkeep └── index.d.ts ├── utilities.css └── variants.css /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /cli 2 | /lib 3 | /docs 4 | /peers 5 | /tests/fixtures/cli-utils.js 6 | /stubs/* 7 | /src/corePluginList.js 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | }, 5 | "parserOptions": { 6 | "ecmaVersion": 2020, 7 | "sourceType": "module" 8 | }, 9 | "extends": ["prettier"], 10 | "plugins": ["prettier"], 11 | "rules": { 12 | "camelcase": ["error", { "allow": ["^unstable_"] }], 13 | "no-unused-vars": [2, { "args": "all", "argsIgnorePattern": "^_" }], 14 | "no-warning-comments": 0, 15 | "prettier/prettier": "error" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Tailwind CSS Community Guidelines 2 | 3 | The following community guidelines are based on [The Ruby Community Conduct Guidelines](https://www.ruby-lang.org/en/conduct/). 4 | 5 | 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.). 6 | 7 | - Participants will be tolerant of opposing views. 8 | - Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks. 9 | - When interpreting the words and actions of others, participants should always assume good intentions. 10 | - Behaviour which can be reasonably considered harassment will not be tolerated. 11 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for your interest in contributing to Tailwind CSS! Please take a moment to review this document **before submitting a pull request**. 4 | 5 | ## Pull requests 6 | 7 | **Please ask first before starting work on any significant new features.** 8 | 9 | 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 feature request](https://github.com/tailwindlabs/tailwindcss/discussions/new?category=ideas) to first discuss any significant new ideas. This includes things like adding new utilities, creating new at-rules, etc. 10 | 11 | ## Coding standards 12 | 13 | Our code formatting rules are defined in [.eslintrc](https://github.com/tailwindcss/tailwindcss/blob/master/.eslintrc.json). You can check your code against these standards by running: 14 | 15 | ```sh 16 | npm run style 17 | ``` 18 | 19 | To automatically fix any style violations in your code, you can run: 20 | 21 | ```sh 22 | npm run style -- --fix 23 | ``` 24 | 25 | ## Running tests 26 | 27 | You can run the test suite using the following commands: 28 | 29 | ```sh 30 | npm run swcify && npm test 31 | ``` 32 | 33 | Please ensure that the tests are passing when submitting a pull request. If you're adding new features to Tailwind, please include tests. 34 | 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Get Help 4 | url: https://github.com/tailwindlabs/tailwindcss/discussions/new?category=help 5 | about: If you can't get something to work the way you expect, open a question in our discussion forums. 6 | - name: Feature Request 7 | url: https://github.com/tailwindlabs/tailwindcss/discussions/new?category=ideas 8 | about: 'Suggest any ideas you have using our discussion forums.' 9 | - name: Bug Report 10 | url: https://github.com/tailwindlabs/tailwindcss/issues/new?body=%3C!--%20Please%20provide%20all%20of%20the%20information%20requested%20below.%20We%27re%20a%20small%20team%20and%20without%20all%20of%20this%20information%20it%27s%20not%20possible%20for%20us%20to%20help%20and%20your%20bug%20report%20will%20be%20closed.%20--%3E%0A%0A**What%20version%20of%20Tailwind%20CSS%20are%20you%20using%3F**%0A%0AFor%20example%3A%20v2.0.4%0A%0A**What%20build%20tool%20(or%20framework%20if%20it%20abstracts%20the%20build%20tool)%20are%20you%20using%3F**%0A%0AFor%20example%3A%20postcss-cli%208.3.1%2C%20Next.js%2010.0.9%2C%20webpack%205.28.0%0A%0A**What%20version%20of%20Node.js%20are%20you%20using%3F**%0A%0AFor%20example%3A%20v12.0.0%0A%0A**What%20browser%20are%20you%20using%3F**%0A%0AFor%20example%3A%20Chrome%2C%20Safari%2C%20or%20N%2FA%0A%0A**What%20operating%20system%20are%20you%20using%3F**%0A%0AFor%20example%3A%20macOS%2C%20Windows%0A%0A**Reproduction%20URL**%0A%0AA%20Tailwind%20Play%20link%20or%20public%20GitHub%20repo%20that%20includes%20a%20minimal%20reproduction%20of%20the%20bug.%20**Please%20do%20not%20link%20to%20your%20actual%20project**%2C%20what%20we%20need%20instead%20is%20a%20_minimal_%20reproduction%20in%20a%20fresh%20project%20without%20any%20unnecessary%20code.%20This%20means%20it%20doesn%27t%20matter%20if%20your%20real%20project%20is%20private%2Fconfidential%2C%20since%20we%20want%20a%20link%20to%20a%20separate%2C%20isolated%20reproduction%20anyways.%20Unfortunately%20we%20can%27t%20provide%20support%20without%20a%20reproduction%2C%20and%20your%20issue%20will%20be%20closed%20with%20no%20comment%20if%20this%20is%20not%20provided.%0A%0A**Describe%20your%20issue**%0A%0ADescribe%20the%20problem%20you%27re%20seeing%2C%20any%20important%20steps%20to%20reproduce%20and%20what%20behavior%20you%20expect%20instead. 11 | about: If you've already asked for help with a problem and confirmed something is broken with Tailwind CSS itself, create a bug report. 12 | - name: Documentation Issue 13 | url: https://github.com/tailwindlabs/tailwindcss.com 14 | about: 'For documentation issues, suggest changes on our documentation repository.' 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /.github/workflows/build-cli.yml: -------------------------------------------------------------------------------- 1 | name: Build Standalone CLI 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | env: 9 | CI: true 10 | 11 | jobs: 12 | build_cli: 13 | runs-on: macos-11 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - run: git fetch --tags -f 18 | 19 | - name: Resolve version 20 | id: vars 21 | run: echo "::set-output name=tag_name::$(git describe --tags --abbrev=0)" 22 | 23 | - name: Use Node.js 16 24 | uses: actions/setup-node@v2 25 | with: 26 | node-version: '16' 27 | registry-url: 'https://registry.npmjs.org' 28 | 29 | - name: Build tailwindcss 30 | run: npm run prepublishOnly 31 | 32 | - name: Install standalone cli dependencies 33 | run: npm install 34 | working-directory: standalone-cli 35 | 36 | - name: Build standalone cli 37 | run: npm run build 38 | working-directory: standalone-cli 39 | 40 | - name: Test 41 | run: npm test 42 | working-directory: standalone-cli 43 | 44 | - name: Release 45 | uses: softprops/action-gh-release@v1 46 | with: 47 | draft: true 48 | tag_name: ${{ steps.vars.outputs.tag_name }} 49 | body: | 50 | * [Linux (arm64)](https://github.com/tailwindlabs/tailwindcss/releases/download/${{ steps.vars.outputs.tag_name }}/tailwindcss-linux-arm64) 51 | * [Linux (x64)](https://github.com/tailwindlabs/tailwindcss/releases/download/${{ steps.vars.outputs.tag_name }}/tailwindcss-linux-x64) 52 | * [macOS (arm64)](https://github.com/tailwindlabs/tailwindcss/releases/download/${{ steps.vars.outputs.tag_name }}/tailwindcss-macos-arm64) 53 | * [macOS (x64)](https://github.com/tailwindlabs/tailwindcss/releases/download/${{ steps.vars.outputs.tag_name }}/tailwindcss-macos-x64) 54 | * [Windows (x64)](https://github.com/tailwindlabs/tailwindcss/releases/download/${{ steps.vars.outputs.tag_name }}/tailwindcss-windows-x64.exe) 55 | files: | 56 | standalone-cli/dist/tailwindcss-linux-arm64 57 | standalone-cli/dist/tailwindcss-linux-x64 58 | standalone-cli/dist/tailwindcss-macos-arm64 59 | standalone-cli/dist/tailwindcss-macos-x64 60 | standalone-cli/dist/tailwindcss-windows-x64.exe 61 | -------------------------------------------------------------------------------- /.github/workflows/integration-tests.yml: -------------------------------------------------------------------------------- 1 | name: Integration Tests 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | integration: [parcel, postcss-cli, rollup, rollup-sass, tailwindcss-cli, vite, webpack-4, webpack-5] 16 | node-version: [16] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v2 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | 25 | - name: Use cached node_modules (tailwindcss) 26 | id: cache-tailwindcss 27 | uses: actions/cache@v2 28 | with: 29 | path: node_modules 30 | key: nodeModules-${{ hashFiles('./package-lock.json') }}-${{ matrix.node-version }}-tailwindcss 31 | restore-keys: | 32 | nodeModules- 33 | 34 | - name: Install dependencies 35 | if: steps.cache-tailwindcss.outputs.cache-hit != 'true' 36 | run: npm install 37 | env: 38 | CI: true 39 | 40 | - name: Build Tailwind CSS 41 | run: npm run prepublishOnly 42 | 43 | - name: Use cached node_modules (integrations) 44 | id: cache-integrations 45 | uses: actions/cache@v2 46 | with: 47 | path: ./integrations/**/node_modules 48 | key: nodeModules-${{ hashFiles('./integrations/**/package-lock.json') }}-${{ matrix.node-version }}-${{ matrix.integration }}-integrations 49 | restore-keys: | 50 | nodeModules- 51 | 52 | - name: Install shared dependencies 53 | if: steps.cache-integrations.outputs.cache-hit != 'true' 54 | run: npm run install:integrations 55 | env: 56 | CI: true 57 | 58 | - name: Test ${{ matrix.integration }} 59 | run: npm test --prefix ./integrations/${{ matrix.integration }} 60 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [12, 14, 16] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v2 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | 27 | - name: Use cached node_modules 28 | id: cache 29 | uses: actions/cache@v2 30 | with: 31 | path: node_modules 32 | key: nodeModules-${{ hashFiles('./package-lock.json') }}-${{ matrix.node-version }} 33 | restore-keys: | 34 | nodeModules- 35 | - name: Install dependencies 36 | if: steps.cache.outputs.cache-hit != 'true' 37 | run: npm install 38 | env: 39 | CI: true 40 | 41 | - name: Test 42 | run: npm test 43 | env: 44 | CI: true 45 | -------------------------------------------------------------------------------- /.github/workflows/release-insiders.yml: -------------------------------------------------------------------------------- 1 | name: Release Insiders 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | node-version: [12] 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | registry-url: 'https://registry.npmjs.org' 23 | 24 | - name: Use cached node_modules 25 | id: cache 26 | uses: actions/cache@v2 27 | with: 28 | path: node_modules 29 | key: nodeModules-${{ hashFiles('**/package-lock.json') }}-${{ matrix.node-version }} 30 | restore-keys: | 31 | nodeModules- 32 | 33 | - name: Install dependencies 34 | if: steps.cache.outputs.cache-hit != 'true' 35 | run: npm install 36 | env: 37 | CI: true 38 | 39 | - name: Test 40 | run: npm test 41 | env: 42 | CI: true 43 | 44 | - name: Resolve version 45 | id: vars 46 | run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)" 47 | 48 | - name: "Version based on commit: 0.0.0-insiders.${{ steps.vars.outputs.sha_short }}" 49 | run: npm version 0.0.0-insiders.${{ steps.vars.outputs.sha_short }} --force --no-git-tag-version 50 | 51 | - name: Publish 52 | run: npm publish --tag insiders 53 | env: 54 | CI: true 55 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 56 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | node-version: [12] 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | registry-url: 'https://registry.npmjs.org' 23 | 24 | - name: Use cached node_modules 25 | id: cache 26 | uses: actions/cache@v2 27 | with: 28 | path: node_modules 29 | key: nodeModules-${{ hashFiles('**/package-lock.json') }}-${{ matrix.node-version }} 30 | restore-keys: | 31 | nodeModules- 32 | 33 | - name: Install dependencies 34 | if: steps.cache.outputs.cache-hit != 'true' 35 | run: npm install 36 | env: 37 | CI: true 38 | 39 | - name: Test 40 | run: npm test 41 | env: 42 | CI: true 43 | 44 | - name: Publish 45 | run: npm publish 46 | env: 47 | CI: true 48 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /coverage 3 | /cli 4 | /lib 5 | /peers 6 | /example 7 | .vscode 8 | .DS_Store 9 | tailwind.config.js 10 | index.html 11 | yarn.lock 12 | yarn-error.log 13 | types/generated/*.d.ts 14 | 15 | # Perf related files 16 | isolate*.log 17 | 18 | # Generated files 19 | /src/corePluginList.js 20 | -------------------------------------------------------------------------------- /.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "module": { 3 | "type": "commonjs" 4 | }, 5 | "env": { 6 | "targets": { 7 | "node": "12.13.0" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Tailwind Labs, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Tailwind CSS 4 | 5 | 6 | Tailwind CSS 7 | 8 |

9 | 10 | A utility-first CSS framework for rapidly building custom user interfaces. 11 | 12 |

13 | Build Status 14 | Total Downloads 15 | Latest Release 16 | License 17 |

18 | 19 | ------ 20 | 21 | ## Documentation 22 | 23 | For full documentation, visit [tailwindcss.com](https://tailwindcss.com/). 24 | 25 | ## Community 26 | 27 | For help, discussion about best practices, or any other conversation that would benefit from being searchable: 28 | 29 | [Discuss Tailwind CSS on GitHub](https://github.com/tailwindcss/tailwindcss/discussions) 30 | 31 | For casual chit-chat with others using the framework: 32 | 33 | [Join the Tailwind CSS Discord Server](https://discord.gg/7NF8GNe) 34 | 35 | ## Contributing 36 | 37 | If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/master/.github/CONTRIBUTING.md) **before submitting a pull request**. 38 | -------------------------------------------------------------------------------- /base.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | -------------------------------------------------------------------------------- /colors.d.ts: -------------------------------------------------------------------------------- 1 | import type { DefaultColors } from './types/generated/colors' 2 | declare const colors: DefaultColors 3 | export = colors 4 | -------------------------------------------------------------------------------- /colors.js: -------------------------------------------------------------------------------- 1 | let colors = require('./lib/public/colors') 2 | module.exports = (colors.__esModule ? colors : { default: colors }).default 3 | -------------------------------------------------------------------------------- /components.css: -------------------------------------------------------------------------------- 1 | @tailwind components; 2 | -------------------------------------------------------------------------------- /defaultConfig.d.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from './types/config' 2 | declare const config: Config 3 | export = config 4 | -------------------------------------------------------------------------------- /defaultConfig.js: -------------------------------------------------------------------------------- 1 | let defaultConfig = require('./lib/public/default-config') 2 | module.exports = (defaultConfig.__esModule ? defaultConfig : { default: defaultConfig }).default 3 | -------------------------------------------------------------------------------- /defaultTheme.d.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from './types/config' 2 | declare const theme: Config['theme'] 3 | export = theme 4 | -------------------------------------------------------------------------------- /defaultTheme.js: -------------------------------------------------------------------------------- 1 | let defaultTheme = require('./lib/public/default-theme') 2 | module.exports = (defaultTheme.__esModule ? defaultTheme : { default: defaultTheme }).default 3 | -------------------------------------------------------------------------------- /integrations/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /integrations/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "integrations", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "test": "jest --runInBand --forceExit" 7 | }, 8 | "jest": { 9 | "testTimeout": 30000, 10 | "projects": [ 11 | "/*/package.json" 12 | ] 13 | }, 14 | "devDependencies": { 15 | "isomorphic-fetch": "^3.0.0", 16 | "jest": "^26.6.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /integrations/parcel/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | .parcel-cache/ 4 | !tailwind.config.js 5 | !index.html 6 | -------------------------------------------------------------------------------- /integrations/parcel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parcel", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "build": "parcel build ./src/index.html --no-cache", 7 | "dev": "parcel watch ./src/index.html --no-cache", 8 | "test": "jest --runInBand --forceExit" 9 | }, 10 | "jest": { 11 | "testTimeout": 10000, 12 | "displayName": "parcel", 13 | "setupFilesAfterEnv": [ 14 | "/../../jest/customMatchers.js" 15 | ] 16 | }, 17 | "devDependencies": { 18 | "parcel": "^2.5.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /integrations/parcel/postcss.config.js: -------------------------------------------------------------------------------- 1 | let path = require('path') 2 | 3 | module.exports = { 4 | plugins: [require(path.resolve('..', '..'))], 5 | } 6 | -------------------------------------------------------------------------------- /integrations/parcel/src/glob/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /integrations/parcel/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /integrations/parcel/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /integrations/parcel/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./src/index.html', './src/glob/*.{js,html}'], 3 | theme: { 4 | extend: {}, 5 | }, 6 | corePlugins: { 7 | preflight: false, 8 | }, 9 | plugins: [], 10 | } 11 | -------------------------------------------------------------------------------- /integrations/postcss-cli/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | !tailwind.config.js 4 | !index.html 5 | -------------------------------------------------------------------------------- /integrations/postcss-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-cli", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "build": "NODE_ENV=production postcss ./src/index.css -o ./dist/main.css --verbose", 7 | "test": "jest --runInBand --forceExit" 8 | }, 9 | "jest": { 10 | "testTimeout": 10000, 11 | "displayName": "PostCSS CLI", 12 | "setupFilesAfterEnv": [ 13 | "/../../jest/customMatchers.js" 14 | ] 15 | }, 16 | "devDependencies": { 17 | "postcss": "^8.4.14", 18 | "postcss-cli": "^9.1.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /integrations/postcss-cli/postcss.config.js: -------------------------------------------------------------------------------- 1 | let path = require('path') 2 | 3 | module.exports = { 4 | plugins: [require(path.resolve('..', '..'))], 5 | } 6 | -------------------------------------------------------------------------------- /integrations/postcss-cli/src/glob/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /integrations/postcss-cli/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /integrations/postcss-cli/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /integrations/postcss-cli/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./src/index.html', './src/glob/*.{js,html}'], 3 | theme: { 4 | extend: {}, 5 | }, 6 | corePlugins: { 7 | preflight: false, 8 | }, 9 | plugins: [], 10 | } 11 | -------------------------------------------------------------------------------- /integrations/resolve-tool-root.js: -------------------------------------------------------------------------------- 1 | let path = require('path') 2 | 3 | module.exports = function resolveToolRoot() { 4 | let { testPath } = expect.getState() 5 | let separator = '/' // TODO: Does this resolve correctly on windows, or should we use `path.sep` instead. 6 | 7 | return path.resolve( 8 | __dirname, 9 | testPath 10 | .replace(__dirname + separator, '') 11 | .split(separator) 12 | .shift() 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /integrations/rollup-sass/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | !tailwind.config.js 4 | !index.html 5 | -------------------------------------------------------------------------------- /integrations/rollup-sass/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rollup.js", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "build": "rollup -c", 7 | "test": "jest --runInBand --forceExit" 8 | }, 9 | "jest": { 10 | "testTimeout": 10000, 11 | "displayName": "rollup.js", 12 | "setupFilesAfterEnv": ["/../../jest/customMatchers.js"] 13 | }, 14 | "devDependencies": { 15 | "rollup": "^2.74.1", 16 | "rollup-plugin-postcss": "^4.0.2", 17 | "sass": "^1.51.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /integrations/rollup-sass/postcss.config.js: -------------------------------------------------------------------------------- 1 | let path = require('path') 2 | 3 | module.exports = { 4 | plugins: [require(path.resolve('..', '..'))], 5 | } 6 | -------------------------------------------------------------------------------- /integrations/rollup-sass/rollup.config.js: -------------------------------------------------------------------------------- 1 | import postcss from 'rollup-plugin-postcss' 2 | 3 | export default { 4 | input: './src/index.js', 5 | output: { 6 | file: './dist/index.js', 7 | format: 'cjs', 8 | }, 9 | plugins: [ 10 | postcss({ 11 | extract: true, 12 | }), 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /integrations/rollup-sass/src/glob/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /integrations/rollup-sass/src/imported.scss: -------------------------------------------------------------------------------- 1 | // Stub 2 | -------------------------------------------------------------------------------- /integrations/rollup-sass/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /integrations/rollup-sass/src/index.js: -------------------------------------------------------------------------------- 1 | import './index.scss' 2 | -------------------------------------------------------------------------------- /integrations/rollup-sass/src/index.scss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | @import "./imported"; 5 | -------------------------------------------------------------------------------- /integrations/rollup-sass/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./src/index.html', './src/glob/*.{js,html}'], 3 | theme: { 4 | extend: {}, 5 | }, 6 | corePlugins: { 7 | preflight: false, 8 | }, 9 | plugins: [], 10 | } 11 | -------------------------------------------------------------------------------- /integrations/rollup/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | !tailwind.config.js 4 | !index.html 5 | -------------------------------------------------------------------------------- /integrations/rollup/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rollup.js", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "build": "rollup -c", 7 | "test": "jest --runInBand --forceExit" 8 | }, 9 | "jest": { 10 | "testTimeout": 10000, 11 | "displayName": "rollup.js", 12 | "setupFilesAfterEnv": ["/../../jest/customMatchers.js"] 13 | }, 14 | "devDependencies": { 15 | "rollup": "^2.74.1", 16 | "rollup-plugin-postcss": "^4.0.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /integrations/rollup/postcss.config.js: -------------------------------------------------------------------------------- 1 | let path = require('path') 2 | 3 | module.exports = { 4 | plugins: [require(path.resolve('..', '..'))], 5 | } 6 | -------------------------------------------------------------------------------- /integrations/rollup/rollup.config.js: -------------------------------------------------------------------------------- 1 | import postcss from 'rollup-plugin-postcss' 2 | 3 | export default { 4 | input: './src/index.js', 5 | output: { 6 | file: './dist/index.js', 7 | format: 'cjs', 8 | }, 9 | plugins: [ 10 | postcss({ 11 | extract: true, 12 | }), 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /integrations/rollup/src/glob/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /integrations/rollup/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /integrations/rollup/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /integrations/rollup/src/index.js: -------------------------------------------------------------------------------- 1 | import './index.css' 2 | -------------------------------------------------------------------------------- /integrations/rollup/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./src/index.html', './src/glob/*.{js,html}'], 3 | theme: { 4 | extend: {}, 5 | }, 6 | corePlugins: { 7 | preflight: false, 8 | }, 9 | plugins: [], 10 | } 11 | -------------------------------------------------------------------------------- /integrations/syntax.js: -------------------------------------------------------------------------------- 1 | // Small helper to allow for css, html and JavaScript highlighting / formatting in most editors. 2 | module.exports = { css: String.raw, html: String.raw, javascript: String.raw } 3 | -------------------------------------------------------------------------------- /integrations/tailwindcss-cli/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | !tailwind.config.js 4 | !index.html 5 | -------------------------------------------------------------------------------- /integrations/tailwindcss-cli/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tailwindcss-cli", 3 | "version": "0.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "tailwindcss-cli", 9 | "version": "0.0.0" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /integrations/tailwindcss-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tailwindcss-cli", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "build": "NODE_ENV=production node ../../lib/cli.js -i ./src/index.css -o ./dist/main.css", 7 | "test": "jest --runInBand --forceExit" 8 | }, 9 | "jest": { 10 | "testTimeout": 10000, 11 | "displayName": "Tailwind CSS CLI", 12 | "setupFilesAfterEnv": [ 13 | "/../../jest/customMatchers.js" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /integrations/tailwindcss-cli/postcss.config.js: -------------------------------------------------------------------------------- 1 | let path = require('path') 2 | 3 | module.exports = { 4 | plugins: [require(path.resolve('..', '..'))], 5 | } 6 | -------------------------------------------------------------------------------- /integrations/tailwindcss-cli/src/glob/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /integrations/tailwindcss-cli/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /integrations/tailwindcss-cli/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /integrations/tailwindcss-cli/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./src/index.html', './src/glob/*.{js,html}'], 3 | theme: { 4 | extend: {}, 5 | }, 6 | corePlugins: { 7 | preflight: false, 8 | }, 9 | plugins: [], 10 | } 11 | -------------------------------------------------------------------------------- /integrations/vite/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | !tailwind.config.js 4 | !index.html 5 | -------------------------------------------------------------------------------- /integrations/vite/glob/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /integrations/vite/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /integrations/vite/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /integrations/vite/index.js: -------------------------------------------------------------------------------- 1 | import './index.css' 2 | -------------------------------------------------------------------------------- /integrations/vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite", 3 | "private": true, 4 | "version": "0.0.0", 5 | "main": "./src/index.js", 6 | "browser": "./src/index.js", 7 | "scripts": { 8 | "build": "vite build", 9 | "test": "jest --runInBand --forceExit" 10 | }, 11 | "jest": { 12 | "testTimeout": 10000, 13 | "displayName": "vite", 14 | "setupFilesAfterEnv": [ 15 | "/../../jest/customMatchers.js" 16 | ] 17 | }, 18 | "devDependencies": { 19 | "vite": "^2.9.9" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /integrations/vite/postcss.config.js: -------------------------------------------------------------------------------- 1 | let path = require('path') 2 | 3 | module.exports = { 4 | plugins: [require(path.resolve('..', '..'))], 5 | } 6 | -------------------------------------------------------------------------------- /integrations/vite/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./index.html', './glob/*.{js,html}'], 3 | theme: { 4 | extend: {}, 5 | }, 6 | corePlugins: { 7 | preflight: false, 8 | }, 9 | plugins: [], 10 | } 11 | -------------------------------------------------------------------------------- /integrations/webpack-4/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | !tailwind.config.js 4 | !index.html 5 | -------------------------------------------------------------------------------- /integrations/webpack-4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-4", 3 | "private": true, 4 | "version": "0.0.0", 5 | "main": "./src/index.js", 6 | "browser": "./src/index.js", 7 | "scripts": { 8 | "build": "webpack --mode=production", 9 | "dev": "webpack --mode=development --watch", 10 | "test": "jest --runInBand --forceExit" 11 | }, 12 | "jest": { 13 | "testTimeout": 10000, 14 | "displayName": "webpack 4", 15 | "setupFilesAfterEnv": [ 16 | "/../../jest/customMatchers.js" 17 | ] 18 | }, 19 | "devDependencies": { 20 | "css-loader": "^5.2.7", 21 | "mini-css-extract-plugin": "^1.6.2", 22 | "postcss-loader": "^4.3.0", 23 | "webpack": "^4.46.0", 24 | "webpack-cli": "^4.9.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /integrations/webpack-4/postcss.config.js: -------------------------------------------------------------------------------- 1 | let path = require('path') 2 | 3 | module.exports = { 4 | plugins: [require(path.resolve('..', '..'))], 5 | } 6 | -------------------------------------------------------------------------------- /integrations/webpack-4/src/glob/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /integrations/webpack-4/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /integrations/webpack-4/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /integrations/webpack-4/src/index.js: -------------------------------------------------------------------------------- 1 | import './index.css' 2 | -------------------------------------------------------------------------------- /integrations/webpack-4/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./src/index.html', './src/glob/*.{js,html}'], 3 | theme: { 4 | extend: {}, 5 | }, 6 | corePlugins: { 7 | preflight: false, 8 | }, 9 | plugins: [], 10 | } 11 | -------------------------------------------------------------------------------- /integrations/webpack-4/webpack.config.js: -------------------------------------------------------------------------------- 1 | let MiniCssExtractPlugin = require('mini-css-extract-plugin') 2 | 3 | module.exports = { 4 | plugins: [new MiniCssExtractPlugin()], 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.css$/i, 9 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'], 10 | }, 11 | ], 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /integrations/webpack-5/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | !tailwind.config.js 4 | !index.html 5 | -------------------------------------------------------------------------------- /integrations/webpack-5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-5", 3 | "private": true, 4 | "version": "0.0.0", 5 | "main": "./src/index.js", 6 | "browser": "./src/index.js", 7 | "scripts": { 8 | "build": "webpack --mode=production", 9 | "dev": "webpack --mode=development", 10 | "test": "jest --runInBand --forceExit" 11 | }, 12 | "jest": { 13 | "testTimeout": 10000, 14 | "displayName": "webpack 5", 15 | "setupFilesAfterEnv": [ 16 | "/../../jest/customMatchers.js" 17 | ] 18 | }, 19 | "devDependencies": { 20 | "css-loader": "^6.7.1", 21 | "mini-css-extract-plugin": "^2.6.0", 22 | "postcss-loader": "^6.2.1", 23 | "webpack": "^5.72.1", 24 | "webpack-cli": "^4.9.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /integrations/webpack-5/postcss.config.js: -------------------------------------------------------------------------------- 1 | let path = require('path') 2 | 3 | module.exports = { 4 | plugins: [require(path.resolve('..', '..'))], 5 | } 6 | -------------------------------------------------------------------------------- /integrations/webpack-5/src/glob/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /integrations/webpack-5/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /integrations/webpack-5/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /integrations/webpack-5/src/index.js: -------------------------------------------------------------------------------- 1 | import './index.css' 2 | -------------------------------------------------------------------------------- /integrations/webpack-5/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./src/index.html', './src/glob/*.{js,html}'], 3 | theme: { 4 | extend: {}, 5 | }, 6 | corePlugins: { 7 | preflight: false, 8 | }, 9 | plugins: [], 10 | } 11 | -------------------------------------------------------------------------------- /integrations/webpack-5/webpack.config.js: -------------------------------------------------------------------------------- 1 | let MiniCssExtractPlugin = require('mini-css-extract-plugin') 2 | 3 | module.exports = { 4 | plugins: [new MiniCssExtractPlugin()], 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.css$/i, 9 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'], 10 | }, 11 | ], 12 | }, 13 | output: { 14 | clean: true, 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /jest/runInTempDirectory.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | import rimraf from 'rimraf' 5 | 6 | let id = 0 7 | 8 | export default function (callback) { 9 | return new Promise((resolve) => { 10 | const workerId = `${process.env.JEST_WORKER_ID}-${id++}` 11 | const tmpPath = path.resolve(__dirname, `../__tmp_${workerId}`) 12 | const currentPath = process.cwd() 13 | 14 | rimraf.sync(tmpPath) 15 | fs.mkdirSync(tmpPath) 16 | process.chdir(tmpPath) 17 | 18 | callback().then(() => { 19 | process.chdir(currentPath) 20 | rimraf(tmpPath, resolve) 21 | }) 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /nesting/index.js: -------------------------------------------------------------------------------- 1 | let nesting = require('../lib/postcss-plugins/nesting') 2 | module.exports = (nesting.__esModule ? nesting : { default: nesting }).default 3 | -------------------------------------------------------------------------------- /perf/.gitignore: -------------------------------------------------------------------------------- 1 | output*.css 2 | v8.json -------------------------------------------------------------------------------- /perf/fixture.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | @tailwind components; 4 | 5 | @tailwind utilities; 6 | 7 | 8 | .btn-1-xl { 9 | @apply sm:space-x-0; 10 | @apply xl:space-x-0; 11 | @apply sm:space-x-1; 12 | @apply xl:space-x-1; 13 | @apply sm:space-y-0; 14 | @apply xl:space-y-0; 15 | @apply sm:space-y-1; 16 | @apply xl:space-y-1; 17 | } 18 | .btn-2-xl { 19 | @apply sm:space-x-0; 20 | @apply xl:space-x-0; 21 | @apply sm:space-x-1; 22 | @apply xl:space-x-1; 23 | @apply sm:space-y-0; 24 | @apply xl:space-y-0; 25 | @apply sm:space-y-1; 26 | @apply xl:space-y-1; 27 | @apply btn-1-xl; 28 | } 29 | .btn-3-xl { 30 | @apply sm:space-x-0; 31 | @apply xl:space-x-0; 32 | @apply sm:space-x-1; 33 | @apply xl:space-x-1; 34 | @apply sm:space-y-0; 35 | @apply xl:space-y-0; 36 | @apply sm:space-y-1; 37 | @apply xl:space-y-1; 38 | @apply btn-2-xl; 39 | } 40 | .btn-4-xl { 41 | @apply sm:space-x-0; 42 | @apply xl:space-x-0; 43 | @apply sm:space-x-1; 44 | @apply xl:space-x-1; 45 | @apply sm:space-y-0; 46 | @apply xl:space-y-0; 47 | @apply sm:space-y-1; 48 | @apply xl:space-y-1; 49 | @apply btn-3-xl; 50 | } 51 | .btn-5-xl { 52 | @apply sm:space-x-0; 53 | @apply xl:space-x-0; 54 | @apply sm:space-x-1; 55 | @apply xl:space-x-1; 56 | @apply sm:space-y-0; 57 | @apply xl:space-y-0; 58 | @apply sm:space-y-1; 59 | @apply xl:space-y-1; 60 | @apply btn-4-xl; 61 | } 62 | -------------------------------------------------------------------------------- /perf/script.sh: -------------------------------------------------------------------------------- 1 | # Cleanup existing perf stuff 2 | rm isolate-*.log 3 | 4 | # Ensure we use the latest build version 5 | npm run babelify 6 | 7 | # Run Tailwind on the big fixture file & profile it 8 | node --prof lib/cli.js build ./perf/fixture.css -c ./perf/tailwind.config.js -o ./perf/output.css 9 | 10 | # Generate flame graph 11 | node --prof-process --preprocess -j isolate*.log > ./perf/v8.json 12 | 13 | # Now visit: https://mapbox.github.io/flamebearer/ 14 | # And drag that v8.json file in there! 15 | # You can put "./lib" in the search box which will highlight all our code in green. -------------------------------------------------------------------------------- /perf/tailwind.config.js: -------------------------------------------------------------------------------- 1 | let colors = require('../colors') 2 | module.exports = { 3 | purge: [], 4 | darkMode: 'class', 5 | theme: { 6 | extend: { colors }, 7 | }, 8 | variants: [ 9 | 'responsive', 10 | 'group-hover', 11 | 'group-focus', 12 | 'hover', 13 | 'focus-within', 14 | 'focus-visible', 15 | 'focus', 16 | 'active', 17 | 'visited', 18 | 'disabled', 19 | 'checked', 20 | ], 21 | plugins: [], 22 | } 23 | -------------------------------------------------------------------------------- /plugin.d.ts: -------------------------------------------------------------------------------- 1 | import type { Config, PluginCreator } from './types/config' 2 | declare function createPlugin( 3 | plugin: PluginCreator, 4 | config?: Config 5 | ): { handler: PluginCreator; config?: Config } 6 | export = createPlugin 7 | -------------------------------------------------------------------------------- /plugin.js: -------------------------------------------------------------------------------- 1 | let createPlugin = require('./lib/public/create-plugin') 2 | module.exports = (createPlugin.__esModule ? createPlugin : { default: createPlugin }).default 3 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // These settings are duplicated in .editorconfig: 3 | tabWidth: 2, // indent_size = 2 4 | useTabs: false, // indent_style = space 5 | endOfLine: 'lf', // end_of_line = lf 6 | semi: false, // default: true 7 | singleQuote: true, // default: false 8 | printWidth: 100, // default: 80 9 | trailingComma: 'es5', 10 | bracketSpacing: true, 11 | overrides: [ 12 | { 13 | files: '*.js', 14 | options: { 15 | parser: 'flow', 16 | }, 17 | }, 18 | ], 19 | } 20 | -------------------------------------------------------------------------------- /resolveConfig.js: -------------------------------------------------------------------------------- 1 | let resolveConfig = require('./lib/public/resolve-config') 2 | module.exports = (resolveConfig.__esModule ? resolveConfig : { default: resolveConfig }).default 3 | -------------------------------------------------------------------------------- /screens.css: -------------------------------------------------------------------------------- 1 | @tailwind screens; 2 | -------------------------------------------------------------------------------- /scripts/create-plugin-list.js: -------------------------------------------------------------------------------- 1 | import { corePlugins } from '../src/corePlugins' 2 | import fs from 'fs' 3 | import path from 'path' 4 | 5 | let corePluginList = Object.keys(corePlugins) 6 | 7 | fs.writeFileSync( 8 | path.join(process.cwd(), 'src', 'corePluginList.js'), 9 | `export default ${JSON.stringify(corePluginList)}` 10 | ) 11 | -------------------------------------------------------------------------------- /scripts/generate-types.js: -------------------------------------------------------------------------------- 1 | import prettier from 'prettier' 2 | import { corePlugins } from '../src/corePlugins' 3 | import colors from '../src/public/colors' 4 | import fs from 'fs' 5 | import path from 'path' 6 | 7 | fs.writeFileSync( 8 | path.join(process.cwd(), 'types', 'generated', 'corePluginList.d.ts'), 9 | `export type CorePluginList = ${Object.keys(corePlugins) 10 | .map((p) => `'${p}'`) 11 | .join(' | ')}` 12 | ) 13 | 14 | let colorsWithoutDeprecatedColors = Object.fromEntries( 15 | Object.entries(Object.getOwnPropertyDescriptors(colors)) 16 | .filter(([_, { value }]) => { 17 | return typeof value !== 'undefined' 18 | }) 19 | .map(([name, definition]) => [name, definition.value]) 20 | ) 21 | 22 | let deprecatedColors = Object.entries(Object.getOwnPropertyDescriptors(colors)) 23 | .filter(([_, { value }]) => { 24 | return typeof value === 'undefined' 25 | }) 26 | .map(([name, definition]) => { 27 | let warn = console.warn 28 | let messages = [] 29 | console.warn = (...args) => messages.push(args.pop()) 30 | definition.get() 31 | console.warn = warn 32 | let message = messages.join(' ').trim() 33 | let newColor = message.match(/renamed to `(.*)`/)[1] 34 | return `/** @deprecated ${message} */${name}: DefaultColors['${newColor}'],` 35 | }) 36 | .join('\n') 37 | 38 | fs.writeFileSync( 39 | path.join(process.cwd(), 'types', 'generated', 'colors.d.ts'), 40 | prettier.format( 41 | `export interface DefaultColors { ${JSON.stringify(colorsWithoutDeprecatedColors).slice( 42 | 1, 43 | -1 44 | )}\n${deprecatedColors}\n}`, 45 | { 46 | semi: false, 47 | singleQuote: true, 48 | printWidth: 100, 49 | parser: 'typescript', 50 | } 51 | ) 52 | ) 53 | -------------------------------------------------------------------------------- /scripts/install-integrations.js: -------------------------------------------------------------------------------- 1 | let fs = require('fs/promises') 2 | let { spawn } = require('child_process') 3 | let path = require('path') 4 | let root = process.cwd() 5 | 6 | function npmInstall(cwd) { 7 | return new Promise((resolve) => { 8 | let childProcess = spawn('npm', ['install'], { cwd }) 9 | childProcess.on('exit', resolve) 10 | }) 11 | } 12 | 13 | async function install() { 14 | let base = path.resolve(root, 'integrations') 15 | let ignoreFolders = ['node_modules'] 16 | let integrations = (await fs.readdir(base, { withFileTypes: true })) 17 | .filter((integration) => integration.isDirectory()) 18 | .filter((integration) => !ignoreFolders.includes(integration.name)) 19 | .map((folder) => path.resolve(base, folder.name)) 20 | .concat([base]) 21 | .map((integration) => npmInstall(integration)) 22 | 23 | await Promise.all(integrations) 24 | console.log('Done!') 25 | } 26 | 27 | install() 28 | -------------------------------------------------------------------------------- /scripts/rebuildFixtures.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import postcss from 'postcss' 3 | import tailwind from '..' 4 | 5 | function build({ from, to, config }) { 6 | return new Promise((resolve, reject) => { 7 | console.log(`Processing ./${from}...`) 8 | 9 | fs.readFile(`./${from}`, (err, css) => { 10 | if (err) throw err 11 | 12 | return postcss([tailwind(config)]) 13 | .process(css, { 14 | from: undefined, 15 | }) 16 | .then((result) => { 17 | fs.writeFileSync(`./${to}`, result.css) 18 | return result 19 | }) 20 | .then(resolve) 21 | .catch((error) => { 22 | console.log(error) 23 | reject() 24 | }) 25 | }) 26 | }) 27 | } 28 | 29 | console.info('\nRebuilding fixtures...\n') 30 | 31 | Promise.all([ 32 | build({ 33 | from: 'tests/fixtures/tailwind-input.css', 34 | to: 'tests/fixtures/tailwind-output.css', 35 | config: {}, 36 | }), 37 | build({ 38 | from: 'tests/fixtures/tailwind-input.css', 39 | to: 'tests/fixtures/tailwind-output-important.css', 40 | config: { important: true }, 41 | }), 42 | build({ 43 | from: 'tests/fixtures/tailwind-input.css', 44 | to: 'tests/fixtures/tailwind-output-no-color-opacity.css', 45 | config: { 46 | corePlugins: { 47 | textOpacity: false, 48 | backgroundOpacity: false, 49 | borderOpacity: false, 50 | placeholderOpacity: false, 51 | divideOpacity: false, 52 | }, 53 | }, 54 | }), 55 | build({ 56 | from: 'tests/fixtures/tailwind-input.css', 57 | to: 'tests/fixtures/tailwind-output-flagged.css', 58 | config: { 59 | future: 'all', 60 | experimental: 'all', 61 | }, 62 | }), 63 | ]).then(() => { 64 | console.log('\nFinished rebuilding fixtures.') 65 | console.log( 66 | '\nPlease triple check that the fixture output matches what you expect before committing this change.' 67 | ) 68 | }) 69 | -------------------------------------------------------------------------------- /src/cli-peer-dependencies.js: -------------------------------------------------------------------------------- 1 | export function lazyPostcss() { 2 | return require('postcss') 3 | } 4 | 5 | export function lazyAutoprefixer() { 6 | return require('autoprefixer') 7 | } 8 | 9 | export function lazyCssnano() { 10 | return require('cssnano') 11 | } 12 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | export const cli = 'tailwind' 4 | export const defaultConfigFile = './tailwind.config.js' 5 | export const defaultPostCssConfigFile = './postcss.config.js' 6 | export const cjsConfigFile = './tailwind.config.cjs' 7 | export const cjsPostCssConfigFile = './postcss.config.cjs' 8 | 9 | export const supportedConfigFiles = [cjsConfigFile, defaultConfigFile] 10 | export const supportedPostCssConfigFile = [cjsPostCssConfigFile, defaultPostCssConfigFile] 11 | 12 | export const defaultConfigStubFile = path.resolve(__dirname, '../stubs/defaultConfig.stub.js') 13 | export const simpleConfigStubFile = path.resolve(__dirname, '../stubs/simpleConfig.stub.js') 14 | export const defaultPostCssConfigStubFile = path.resolve( 15 | __dirname, 16 | '../stubs/defaultPostCssConfig.stub.js' 17 | ) 18 | -------------------------------------------------------------------------------- /src/css/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Nicolas Gallagher 4 | Copyright (c) Jonathan Neal 5 | Copyright (c) Sindre Sorhus (sindresorhus.com) 6 | Copyright (c) Adam Wathan 7 | Copyright (c) Jonathan Reinink 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | -------------------------------------------------------------------------------- /src/featureFlags.js: -------------------------------------------------------------------------------- 1 | import colors from 'picocolors' 2 | import log from './util/log' 3 | 4 | let defaults = { 5 | optimizeUniversalDefaults: false, 6 | } 7 | 8 | let featureFlags = { 9 | future: ['hoverOnlyWhenSupported'], 10 | experimental: ['optimizeUniversalDefaults'], 11 | } 12 | 13 | export function flagEnabled(config, flag) { 14 | if (featureFlags.future.includes(flag)) { 15 | return config.future === 'all' || (config?.future?.[flag] ?? defaults[flag] ?? false) 16 | } 17 | 18 | if (featureFlags.experimental.includes(flag)) { 19 | return ( 20 | config.experimental === 'all' || (config?.experimental?.[flag] ?? defaults[flag] ?? false) 21 | ) 22 | } 23 | 24 | return false 25 | } 26 | 27 | function experimentalFlagsEnabled(config) { 28 | if (config.experimental === 'all') { 29 | return featureFlags.experimental 30 | } 31 | 32 | return Object.keys(config?.experimental ?? {}).filter( 33 | (flag) => featureFlags.experimental.includes(flag) && config.experimental[flag] 34 | ) 35 | } 36 | 37 | export function issueFlagNotices(config) { 38 | if (process.env.JEST_WORKER_ID !== undefined) { 39 | return 40 | } 41 | 42 | if (experimentalFlagsEnabled(config).length > 0) { 43 | let changes = experimentalFlagsEnabled(config) 44 | .map((s) => colors.yellow(s)) 45 | .join(', ') 46 | 47 | log.warn('experimental-flags-enabled', [ 48 | `You have enabled experimental features: ${changes}`, 49 | 'Experimental features in Tailwind CSS are not covered by semver, may introduce breaking changes, and can change at any time.', 50 | ]) 51 | } 52 | } 53 | 54 | export default featureFlags 55 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import setupTrackingContext from './lib/setupTrackingContext' 2 | import processTailwindFeatures from './processTailwindFeatures' 3 | import { env } from './lib/sharedState' 4 | 5 | module.exports = function tailwindcss(configOrPath) { 6 | return { 7 | postcssPlugin: 'tailwindcss', 8 | plugins: [ 9 | env.DEBUG && 10 | function (root) { 11 | console.log('\n') 12 | console.time('JIT TOTAL') 13 | return root 14 | }, 15 | function (root, result) { 16 | let context = setupTrackingContext(configOrPath) 17 | 18 | if (root.type === 'document') { 19 | let roots = root.nodes.filter((node) => node.type === 'root') 20 | 21 | for (const root of roots) { 22 | if (root.type === 'root') { 23 | processTailwindFeatures(context)(root, result) 24 | } 25 | } 26 | 27 | return 28 | } 29 | 30 | processTailwindFeatures(context)(root, result) 31 | }, 32 | env.DEBUG && 33 | function (root) { 34 | console.timeEnd('JIT TOTAL') 35 | console.log('\n') 36 | return root 37 | }, 38 | ].filter(Boolean), 39 | } 40 | } 41 | 42 | module.exports.postcss = true 43 | -------------------------------------------------------------------------------- /src/lib/cacheInvalidation.js: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | import * as sharedState from './sharedState' 3 | 4 | /** 5 | * Calculate the hash of a string. 6 | * 7 | * This doesn't need to be cryptographically secure or 8 | * anything like that since it's used only to detect 9 | * when the CSS changes to invalidate the context. 10 | * 11 | * This is wrapped in a try/catch because it's really dependent 12 | * on how Node itself is build and the environment and OpenSSL 13 | * version / build that is installed on the user's machine. 14 | * 15 | * Based on the environment this can just outright fail. 16 | * 17 | * See https://github.com/nodejs/node/issues/40455 18 | * 19 | * @param {string} str 20 | */ 21 | function getHash(str) { 22 | try { 23 | return crypto.createHash('md5').update(str, 'utf-8').digest('binary') 24 | } catch (err) { 25 | return '' 26 | } 27 | } 28 | 29 | /** 30 | * Determine if the CSS tree is different from the 31 | * previous version for the given `sourcePath`. 32 | * 33 | * @param {string} sourcePath 34 | * @param {import('postcss').Node} root 35 | */ 36 | export function hasContentChanged(sourcePath, root) { 37 | let css = root.toString() 38 | 39 | // We only care about files with @tailwind directives 40 | // Other files use an existing context 41 | if (!css.includes('@tailwind')) { 42 | return false 43 | } 44 | 45 | let existingHash = sharedState.sourceHashMap.get(sourcePath) 46 | let rootHash = getHash(css) 47 | let didChange = existingHash !== rootHash 48 | 49 | sharedState.sourceHashMap.set(sourcePath, rootHash) 50 | 51 | return didChange 52 | } 53 | -------------------------------------------------------------------------------- /src/lib/collapseAdjacentRules.js: -------------------------------------------------------------------------------- 1 | let comparisonMap = { 2 | atrule: ['name', 'params'], 3 | rule: ['selector'], 4 | } 5 | let types = new Set(Object.keys(comparisonMap)) 6 | 7 | export default function collapseAdjacentRules() { 8 | function collapseRulesIn(root) { 9 | let currentRule = null 10 | root.each((node) => { 11 | if (!types.has(node.type)) { 12 | currentRule = null 13 | return 14 | } 15 | 16 | if (currentRule === null) { 17 | currentRule = node 18 | return 19 | } 20 | 21 | let properties = comparisonMap[node.type] 22 | 23 | if (node.type === 'atrule' && node.name === 'font-face') { 24 | currentRule = node 25 | } else if ( 26 | properties.every( 27 | (property) => 28 | (node[property] ?? '').replace(/\s+/g, ' ') === 29 | (currentRule[property] ?? '').replace(/\s+/g, ' ') 30 | ) 31 | ) { 32 | // An AtRule may not have children (for example if we encounter duplicate @import url(…) rules) 33 | if (node.nodes) { 34 | currentRule.append(node.nodes) 35 | } 36 | 37 | node.remove() 38 | } else { 39 | currentRule = node 40 | } 41 | }) 42 | 43 | // After we've collapsed adjacent rules & at-rules, we need to collapse 44 | // adjacent rules & at-rules that are children of at-rules. 45 | // We do not care about nesting rules because Tailwind CSS 46 | // explicitly does not handle rule nesting on its own as 47 | // the user is expected to use a nesting plugin 48 | root.each((node) => { 49 | if (node.type === 'atrule') { 50 | collapseRulesIn(node) 51 | } 52 | }) 53 | } 54 | 55 | return (root) => { 56 | collapseRulesIn(root) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/lib/detectNesting.js: -------------------------------------------------------------------------------- 1 | export default function (_context) { 2 | return (root, result) => { 3 | let found = false 4 | 5 | root.walkAtRules('tailwind', (node) => { 6 | if (found) return false 7 | 8 | if (node.parent && node.parent.type !== 'root') { 9 | found = true 10 | node.warn( 11 | result, 12 | [ 13 | 'Nested @tailwind rules were detected, but are not supported.', 14 | "Consider using a prefix to scope Tailwind's classes: https://tailwindcss.com/docs/configuration#prefix", 15 | 'Alternatively, use the important selector strategy: https://tailwindcss.com/docs/configuration#selector-strategy', 16 | ].join('\n') 17 | ) 18 | return false 19 | } 20 | }) 21 | 22 | root.walkRules((rule) => { 23 | if (found) return false 24 | 25 | rule.walkRules((nestedRule) => { 26 | found = true 27 | nestedRule.warn( 28 | result, 29 | [ 30 | 'Nested CSS was detected, but CSS nesting has not been configured correctly.', 31 | 'Please enable a CSS nesting plugin *before* Tailwind in your configuration.', 32 | 'See how here: https://tailwindcss.com/docs/using-with-preprocessors#nesting', 33 | ].join('\n') 34 | ) 35 | return false 36 | }) 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/getModuleDependencies.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import resolve from 'resolve' 4 | import detective from 'detective' 5 | 6 | function createModule(file) { 7 | const source = fs.readFileSync(file, 'utf-8') 8 | const requires = detective(source) 9 | 10 | return { file, requires } 11 | } 12 | 13 | export default function getModuleDependencies(entryFile) { 14 | const rootModule = createModule(entryFile) 15 | const modules = [rootModule] 16 | 17 | // Iterate over the modules, even when new 18 | // ones are being added 19 | for (const mdl of modules) { 20 | mdl.requires 21 | .filter((dep) => { 22 | // Only track local modules, not node_modules 23 | return dep.startsWith('./') || dep.startsWith('../') 24 | }) 25 | .forEach((dep) => { 26 | try { 27 | const basedir = path.dirname(mdl.file) 28 | const depPath = resolve.sync(dep, { basedir }) 29 | const depModule = createModule(depPath) 30 | 31 | modules.push(depModule) 32 | } catch (_err) { 33 | // eslint-disable-next-line no-empty 34 | } 35 | }) 36 | } 37 | 38 | return modules 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/partitionApplyAtRules.js: -------------------------------------------------------------------------------- 1 | function partitionRules(root) { 2 | if (!root.walkAtRules) return 3 | 4 | let applyParents = new Set() 5 | 6 | root.walkAtRules('apply', (rule) => { 7 | applyParents.add(rule.parent) 8 | }) 9 | 10 | if (applyParents.size === 0) { 11 | return 12 | } 13 | 14 | for (let rule of applyParents) { 15 | let nodeGroups = [] 16 | let lastGroup = [] 17 | 18 | for (let node of rule.nodes) { 19 | if (node.type === 'atrule' && node.name === 'apply') { 20 | if (lastGroup.length > 0) { 21 | nodeGroups.push(lastGroup) 22 | lastGroup = [] 23 | } 24 | nodeGroups.push([node]) 25 | } else { 26 | lastGroup.push(node) 27 | } 28 | } 29 | 30 | if (lastGroup.length > 0) { 31 | nodeGroups.push(lastGroup) 32 | } 33 | 34 | if (nodeGroups.length === 1) { 35 | continue 36 | } 37 | 38 | for (let group of [...nodeGroups].reverse()) { 39 | let clone = rule.clone({ nodes: [] }) 40 | clone.append(group) 41 | rule.after(clone) 42 | } 43 | 44 | rule.remove() 45 | } 46 | } 47 | 48 | export default function expandApplyAtRules() { 49 | return (root) => { 50 | partitionRules(root) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/lib/regex.js: -------------------------------------------------------------------------------- 1 | const REGEX_SPECIAL = /[\\^$.*+?()[\]{}|]/g 2 | const REGEX_HAS_SPECIAL = RegExp(REGEX_SPECIAL.source) 3 | 4 | /** 5 | * @param {string|RegExp|Array} source 6 | */ 7 | function toSource(source) { 8 | source = Array.isArray(source) ? source : [source] 9 | 10 | source = source.map((item) => (item instanceof RegExp ? item.source : item)) 11 | 12 | return source.join('') 13 | } 14 | 15 | /** 16 | * @param {string|RegExp|Array} source 17 | */ 18 | export function pattern(source) { 19 | return new RegExp(toSource(source), 'g') 20 | } 21 | 22 | /** 23 | * @param {string|RegExp|Array} source 24 | */ 25 | export function withoutCapturing(source) { 26 | return new RegExp(`(?:${toSource(source)})`, 'g') 27 | } 28 | 29 | /** 30 | * @param {Array} sources 31 | */ 32 | export function any(sources) { 33 | return `(?:${sources.map(toSource).join('|')})` 34 | } 35 | 36 | /** 37 | * @param {string|RegExp} source 38 | */ 39 | export function optional(source) { 40 | return `(?:${toSource(source)})?` 41 | } 42 | 43 | /** 44 | * @param {string|RegExp|Array} source 45 | */ 46 | export function zeroOrMore(source) { 47 | return `(?:${toSource(source)})*` 48 | } 49 | 50 | /** 51 | * Generate a RegExp that matches balanced brackets for a given depth 52 | * We have to specify a depth because JS doesn't support recursive groups using ?R 53 | * 54 | * Based on https://stackoverflow.com/questions/17759004/how-to-match-string-within-parentheses-nested-in-java/17759264#17759264 55 | * 56 | * @param {string|RegExp|Array} source 57 | */ 58 | export function nestedBrackets(open, close, depth = 1) { 59 | return withoutCapturing([ 60 | escape(open), 61 | /[^\s]*/, 62 | depth === 1 63 | ? `[^${escape(open)}${escape(close)}\s]*` 64 | : any([`[^${escape(open)}${escape(close)}\s]*`, nestedBrackets(open, close, depth - 1)]), 65 | /[^\s]*/, 66 | escape(close), 67 | ]) 68 | } 69 | 70 | export function escape(string) { 71 | return string && REGEX_HAS_SPECIAL.test(string) 72 | ? string.replace(REGEX_SPECIAL, '\\$&') 73 | : string || '' 74 | } 75 | -------------------------------------------------------------------------------- /src/lib/sharedState.js: -------------------------------------------------------------------------------- 1 | export const env = { 2 | NODE_ENV: process.env.NODE_ENV, 3 | DEBUG: resolveDebug(process.env.DEBUG), 4 | } 5 | export const contextMap = new Map() 6 | export const configContextMap = new Map() 7 | export const contextSourcesMap = new Map() 8 | export const sourceHashMap = new Map() 9 | export const NOT_ON_DEMAND = new String('*') 10 | 11 | export function resolveDebug(debug) { 12 | if (debug === undefined) { 13 | return false 14 | } 15 | 16 | // Environment variables are strings, so convert to boolean 17 | if (debug === 'true' || debug === '1') { 18 | return true 19 | } 20 | 21 | if (debug === 'false' || debug === '0') { 22 | return false 23 | } 24 | 25 | // Keep the debug convention into account: 26 | // DEBUG=* -> This enables all debug modes 27 | // DEBUG=projectA,projectB,projectC -> This enables debug for projectA, projectB and projectC 28 | // DEBUG=projectA:* -> This enables all debug modes for projectA (if you have sub-types) 29 | // DEBUG=projectA,-projectB -> This enables debug for projectA and explicitly disables it for projectB 30 | 31 | if (debug === '*') { 32 | return true 33 | } 34 | 35 | let debuggers = debug.split(',').map((d) => d.split(':')[0]) 36 | 37 | // Ignoring tailwindcss 38 | if (debuggers.includes('-tailwindcss')) { 39 | return false 40 | } 41 | 42 | // Including tailwindcss 43 | if (debuggers.includes('tailwindcss')) { 44 | return true 45 | } 46 | 47 | return false 48 | } 49 | -------------------------------------------------------------------------------- /src/lib/substituteScreenAtRules.js: -------------------------------------------------------------------------------- 1 | import { normalizeScreens } from '../util/normalizeScreens' 2 | import buildMediaQuery from '../util/buildMediaQuery' 3 | 4 | export default function ({ tailwindConfig: { theme } }) { 5 | return function (css) { 6 | css.walkAtRules('screen', (atRule) => { 7 | let screen = atRule.params 8 | let screens = normalizeScreens(theme.screens) 9 | let screenDefinition = screens.find(({ name }) => name === screen) 10 | 11 | if (!screenDefinition) { 12 | throw atRule.error(`No \`${screen}\` screen found.`) 13 | } 14 | 15 | atRule.name = 'media' 16 | atRule.params = buildMediaQuery(screenDefinition) 17 | }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/postcss-plugins/nesting/README.md: -------------------------------------------------------------------------------- 1 | # tailwindcss/nesting 2 | 3 | This is a PostCSS plugin that wraps [postcss-nested](https://github.com/postcss/postcss-nested) or [postcss-nesting](https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-nesting) and acts as a compatibility layer to make sure your nesting plugin of choice properly understands Tailwind's custom syntax like `@apply` and `@screen`. 4 | 5 | Add it to your PostCSS configuration, somewhere before Tailwind itself: 6 | 7 | ```js 8 | // postcss.config.js 9 | module.exports = { 10 | plugins: [ 11 | require('postcss-import'), 12 | require('tailwindcss/nesting'), 13 | require('tailwindcss'), 14 | require('autoprefixer'), 15 | ] 16 | } 17 | ``` 18 | 19 | By default, it uses the [postcss-nested](https://github.com/postcss/postcss-nested) plugin under the hood, which uses a Sass-like syntax and is the plugin that powers nesting support in the [Tailwind CSS plugin API](https://tailwindcss.com/docs/plugins#css-in-js-syntax). 20 | 21 | If you'd rather use [postcss-nesting](https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-nesting) (which is based on the work-in-progress [CSS Nesting](https://drafts.csswg.org/css-nesting-1/) specification), first install the plugin alongside: 22 | 23 | ```shell 24 | npm install postcss-nesting 25 | ``` 26 | 27 | Then pass the plugin itself as an argument to `tailwindcss/nesting` in your PostCSS configuration: 28 | 29 | ```js 30 | // postcss.config.js 31 | module.exports = { 32 | plugins: [ 33 | require('postcss-import'), 34 | require('tailwindcss/nesting')(require('postcss-nesting')), 35 | require('tailwindcss'), 36 | require('autoprefixer'), 37 | ] 38 | } 39 | ``` 40 | 41 | This can also be helpful if for whatever reason you need to use a very specific version of `postcss-nested` and want to override the version we bundle with `tailwindcss/nesting` itself. 42 | 43 | -------------------------------------------------------------------------------- /src/postcss-plugins/nesting/index.js: -------------------------------------------------------------------------------- 1 | import { nesting } from './plugin' 2 | 3 | export default Object.assign( 4 | function (opts) { 5 | return { 6 | postcssPlugin: 'tailwindcss/nesting', 7 | Once(root, { result }) { 8 | return nesting(opts)(root, result) 9 | }, 10 | } 11 | }, 12 | { postcss: true } 13 | ) 14 | -------------------------------------------------------------------------------- /src/postcss-plugins/nesting/plugin.js: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss' 2 | import postcssNested from 'postcss-nested' 3 | 4 | export function nesting(opts = postcssNested) { 5 | return (root, result) => { 6 | root.walkAtRules('screen', (rule) => { 7 | rule.name = 'media' 8 | rule.params = `screen(${rule.params})` 9 | }) 10 | 11 | root.walkAtRules('apply', (rule) => { 12 | rule.before(postcss.decl({ prop: '__apply', value: rule.params, source: rule.source })) 13 | rule.remove() 14 | }) 15 | 16 | let plugin = (() => { 17 | if ( 18 | typeof opts === 'function' || 19 | (typeof opts === 'object' && opts?.hasOwnProperty?.('postcssPlugin')) 20 | ) { 21 | return opts 22 | } 23 | 24 | if (typeof opts === 'string') { 25 | return require(opts) 26 | } 27 | 28 | if (Object.keys(opts).length <= 0) { 29 | return postcssNested 30 | } 31 | 32 | throw new Error('tailwindcss/nesting should be loaded with a nesting plugin.') 33 | })() 34 | 35 | postcss([plugin]).process(root, result.opts).sync() 36 | 37 | root.walkDecls('__apply', (decl) => { 38 | decl.before(postcss.atRule({ name: 'apply', params: decl.value, source: decl.source })) 39 | decl.remove() 40 | }) 41 | 42 | /** 43 | * Use a private PostCSS API to remove the "clean" flag from the entire AST. 44 | * This is done because running process() on the AST will set the "clean" 45 | * flag on all nodes, which we don't want. 46 | * 47 | * This causes downstream plugins using the visitor API to be skipped. 48 | * 49 | * This is guarded because the PostCSS API is not public 50 | * and may change in future versions of PostCSS. 51 | * 52 | * See https://github.com/postcss/postcss/issues/1712 for more details 53 | * 54 | * @param {import('postcss').Node} node 55 | */ 56 | function markDirty(node) { 57 | if (!('markDirty' in node)) { 58 | return 59 | } 60 | 61 | // Traverse the tree down to the leaf nodes 62 | if (node.nodes) { 63 | node.nodes.forEach((n) => markDirty(n)) 64 | } 65 | 66 | // If it's a leaf node mark it as dirty 67 | // We do this here because marking a node as dirty 68 | // will walk up the tree and mark all parents as dirty 69 | // resulting in a lot of unnecessary work if we did this 70 | // for every single node 71 | if (!node.nodes) { 72 | node.markDirty() 73 | } 74 | } 75 | 76 | markDirty(root) 77 | 78 | return root 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/processTailwindFeatures.js: -------------------------------------------------------------------------------- 1 | import normalizeTailwindDirectives from './lib/normalizeTailwindDirectives' 2 | import expandTailwindAtRules from './lib/expandTailwindAtRules' 3 | import expandApplyAtRules from './lib/expandApplyAtRules' 4 | import evaluateTailwindFunctions from './lib/evaluateTailwindFunctions' 5 | import substituteScreenAtRules from './lib/substituteScreenAtRules' 6 | import resolveDefaultsAtRules from './lib/resolveDefaultsAtRules' 7 | import collapseAdjacentRules from './lib/collapseAdjacentRules' 8 | import collapseDuplicateDeclarations from './lib/collapseDuplicateDeclarations' 9 | import partitionApplyAtRules from './lib/partitionApplyAtRules' 10 | import detectNesting from './lib/detectNesting' 11 | import { createContext } from './lib/setupContextUtils' 12 | import { issueFlagNotices } from './featureFlags' 13 | 14 | export default function processTailwindFeatures(setupContext) { 15 | return function (root, result) { 16 | let { tailwindDirectives, applyDirectives } = normalizeTailwindDirectives(root) 17 | 18 | detectNesting()(root, result) 19 | 20 | // Partition apply rules that are found in the css 21 | // itself. 22 | partitionApplyAtRules()(root, result) 23 | 24 | let context = setupContext({ 25 | tailwindDirectives, 26 | applyDirectives, 27 | registerDependency(dependency) { 28 | result.messages.push({ 29 | plugin: 'tailwindcss', 30 | parent: result.opts.from, 31 | ...dependency, 32 | }) 33 | }, 34 | createContext(tailwindConfig, changedContent) { 35 | return createContext(tailwindConfig, changedContent, root) 36 | }, 37 | })(root, result) 38 | 39 | if (context.tailwindConfig.separator === '-') { 40 | throw new Error( 41 | "The '-' character cannot be used as a custom separator in JIT mode due to parsing ambiguity. Please use another character like '_' instead." 42 | ) 43 | } 44 | 45 | issueFlagNotices(context.tailwindConfig) 46 | 47 | expandTailwindAtRules(context)(root, result) 48 | // Partition apply rules that are generated by 49 | // addComponents, addUtilities and so on. 50 | partitionApplyAtRules()(root, result) 51 | expandApplyAtRules(context)(root, result) 52 | evaluateTailwindFunctions(context)(root, result) 53 | substituteScreenAtRules(context)(root, result) 54 | resolveDefaultsAtRules(context)(root, result) 55 | collapseAdjacentRules(context)(root, result) 56 | collapseDuplicateDeclarations(context)(root, result) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/public/create-plugin.js: -------------------------------------------------------------------------------- 1 | import createPlugin from '../util/createPlugin' 2 | export default createPlugin 3 | -------------------------------------------------------------------------------- /src/public/default-config.js: -------------------------------------------------------------------------------- 1 | import { cloneDeep } from '../util/cloneDeep' 2 | import defaultConfig from '../../stubs/defaultConfig.stub' 3 | 4 | export default cloneDeep(defaultConfig) 5 | -------------------------------------------------------------------------------- /src/public/default-theme.js: -------------------------------------------------------------------------------- 1 | import { cloneDeep } from '../util/cloneDeep' 2 | import defaultConfig from '../../stubs/defaultConfig.stub' 3 | 4 | export default cloneDeep(defaultConfig.theme) 5 | -------------------------------------------------------------------------------- /src/public/resolve-config.js: -------------------------------------------------------------------------------- 1 | import resolveConfigObjects from '../util/resolveConfig' 2 | import getAllConfigs from '../util/getAllConfigs' 3 | 4 | export default function resolveConfig(...configs) { 5 | let [, ...defaultConfigs] = getAllConfigs(configs[0]) 6 | return resolveConfigObjects([...configs, ...defaultConfigs]) 7 | } 8 | -------------------------------------------------------------------------------- /src/util/bigSign.js: -------------------------------------------------------------------------------- 1 | export default function bigSign(bigIntValue) { 2 | return (bigIntValue > 0n) - (bigIntValue < 0n) 3 | } 4 | -------------------------------------------------------------------------------- /src/util/buildMediaQuery.js: -------------------------------------------------------------------------------- 1 | export default function buildMediaQuery(screens) { 2 | screens = Array.isArray(screens) ? screens : [screens] 3 | 4 | return screens 5 | .map((screen) => 6 | screen.values.map((screen) => { 7 | if (screen.raw !== undefined) { 8 | return screen.raw 9 | } 10 | 11 | return [ 12 | screen.min && `(min-width: ${screen.min})`, 13 | screen.max && `(max-width: ${screen.max})`, 14 | ] 15 | .filter(Boolean) 16 | .join(' and ') 17 | }) 18 | ) 19 | .join(', ') 20 | } 21 | -------------------------------------------------------------------------------- /src/util/cloneDeep.js: -------------------------------------------------------------------------------- 1 | export function cloneDeep(value) { 2 | if (Array.isArray(value)) { 3 | return value.map((child) => cloneDeep(child)) 4 | } 5 | 6 | if (typeof value === 'object' && value !== null) { 7 | return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, cloneDeep(v)])) 8 | } 9 | 10 | return value 11 | } 12 | -------------------------------------------------------------------------------- /src/util/cloneNodes.js: -------------------------------------------------------------------------------- 1 | export default function cloneNodes(nodes, source = undefined, raws = undefined) { 2 | return nodes.map((node) => { 3 | let cloned = node.clone() 4 | 5 | if (source !== undefined) { 6 | cloned.source = source 7 | 8 | if ('walk' in cloned) { 9 | cloned.walk((child) => { 10 | child.source = source 11 | }) 12 | } 13 | } 14 | 15 | if (raws !== undefined) { 16 | cloned.raws.tailwind = { 17 | ...cloned.raws.tailwind, 18 | ...raws, 19 | } 20 | } 21 | 22 | return cloned 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /src/util/color.js: -------------------------------------------------------------------------------- 1 | import namedColors from 'color-name' 2 | 3 | let HEX = /^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i 4 | let SHORT_HEX = /^#([a-f\d])([a-f\d])([a-f\d])([a-f\d])?$/i 5 | let VALUE = /(?:\d+|\d*\.\d+)%?/ 6 | let SEP = /(?:\s*,\s*|\s+)/ 7 | let ALPHA_SEP = /\s*[,/]\s*/ 8 | let CUSTOM_PROPERTY = /var\(--(?:[^ )]*?)\)/ 9 | 10 | let RGB = new RegExp( 11 | `^(rgb)a?\\(\\s*(${VALUE.source}|${CUSTOM_PROPERTY.source})(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${ALPHA_SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?\\s*\\)$` 12 | ) 13 | let HSL = new RegExp( 14 | `^(hsl)a?\\(\\s*((?:${VALUE.source})(?:deg|rad|grad|turn)?|${CUSTOM_PROPERTY.source})(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${ALPHA_SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?\\s*\\)$` 15 | ) 16 | 17 | // In "loose" mode the color may contain fewer than 3 parts, as long as at least 18 | // one of the parts is variable. 19 | export function parseColor(value, { loose = false } = {}) { 20 | if (typeof value !== 'string') { 21 | return null 22 | } 23 | 24 | value = value.trim() 25 | if (value === 'transparent') { 26 | return { mode: 'rgb', color: ['0', '0', '0'], alpha: '0' } 27 | } 28 | 29 | if (value in namedColors) { 30 | return { mode: 'rgb', color: namedColors[value].map((v) => v.toString()) } 31 | } 32 | 33 | let hex = value 34 | .replace(SHORT_HEX, (_, r, g, b, a) => ['#', r, r, g, g, b, b, a ? a + a : ''].join('')) 35 | .match(HEX) 36 | 37 | if (hex !== null) { 38 | return { 39 | mode: 'rgb', 40 | color: [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)].map((v) => 41 | v.toString() 42 | ), 43 | alpha: hex[4] ? (parseInt(hex[4], 16) / 255).toString() : undefined, 44 | } 45 | } 46 | 47 | let match = value.match(RGB) ?? value.match(HSL) 48 | 49 | if (match === null) { 50 | return null 51 | } 52 | 53 | let color = [match[2], match[3], match[4]].filter(Boolean).map((v) => v.toString()) 54 | 55 | if (!loose && color.length !== 3) { 56 | return null 57 | } 58 | 59 | if (color.length < 3 && !color.some((part) => /^var\(.*?\)$/.test(part))) { 60 | return null 61 | } 62 | 63 | return { 64 | mode: match[1], 65 | color, 66 | alpha: match[5]?.toString?.(), 67 | } 68 | } 69 | 70 | export function formatColor({ mode, color, alpha }) { 71 | let hasAlpha = alpha !== undefined 72 | return `${mode}(${color.join(' ')}${hasAlpha ? ` / ${alpha}` : ''})` 73 | } 74 | -------------------------------------------------------------------------------- /src/util/configurePlugins.js: -------------------------------------------------------------------------------- 1 | export default function (pluginConfig, plugins) { 2 | if (pluginConfig === undefined) { 3 | return plugins 4 | } 5 | 6 | const pluginNames = Array.isArray(pluginConfig) 7 | ? pluginConfig 8 | : [ 9 | ...new Set( 10 | plugins 11 | .filter((pluginName) => { 12 | return pluginConfig !== false && pluginConfig[pluginName] !== false 13 | }) 14 | .concat( 15 | Object.keys(pluginConfig).filter((pluginName) => { 16 | return pluginConfig[pluginName] !== false 17 | }) 18 | ) 19 | ), 20 | ] 21 | 22 | return pluginNames 23 | } 24 | -------------------------------------------------------------------------------- /src/util/createPlugin.js: -------------------------------------------------------------------------------- 1 | function createPlugin(plugin, config) { 2 | return { 3 | handler: plugin, 4 | config, 5 | } 6 | } 7 | 8 | createPlugin.withOptions = function (pluginFunction, configFunction = () => ({})) { 9 | const optionsFunction = function (options) { 10 | return { 11 | __options: options, 12 | handler: pluginFunction(options), 13 | config: configFunction(options), 14 | } 15 | } 16 | 17 | optionsFunction.__isOptionsFunction = true 18 | 19 | // Expose plugin dependencies so that `object-hash` returns a different 20 | // value if anything here changes, to ensure a rebuild is triggered. 21 | optionsFunction.__pluginFunction = pluginFunction 22 | optionsFunction.__configFunction = configFunction 23 | 24 | return optionsFunction 25 | } 26 | 27 | export default createPlugin 28 | -------------------------------------------------------------------------------- /src/util/createUtilityPlugin.js: -------------------------------------------------------------------------------- 1 | import transformThemeValue from './transformThemeValue' 2 | 3 | export default function createUtilityPlugin( 4 | themeKey, 5 | utilityVariations = [[themeKey, [themeKey]]], 6 | { filterDefault = false, ...options } = {} 7 | ) { 8 | let transformValue = transformThemeValue(themeKey) 9 | return function ({ matchUtilities, theme }) { 10 | for (let utilityVariation of utilityVariations) { 11 | let group = Array.isArray(utilityVariation[0]) ? utilityVariation : [utilityVariation] 12 | 13 | matchUtilities( 14 | group.reduce((obj, [classPrefix, properties]) => { 15 | return Object.assign(obj, { 16 | [classPrefix]: (value) => { 17 | return properties.reduce((obj, name) => { 18 | if (Array.isArray(name)) { 19 | return Object.assign(obj, { [name[0]]: name[1] }) 20 | } 21 | return Object.assign(obj, { [name]: transformValue(value) }) 22 | }, {}) 23 | }, 24 | }) 25 | }, {}), 26 | { 27 | ...options, 28 | values: filterDefault 29 | ? Object.fromEntries( 30 | Object.entries(theme(themeKey) ?? {}).filter(([modifier]) => modifier !== 'DEFAULT') 31 | ) 32 | : theme(themeKey), 33 | } 34 | ) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/util/defaults.js: -------------------------------------------------------------------------------- 1 | export function defaults(target, ...sources) { 2 | for (let source of sources) { 3 | for (let k in source) { 4 | if (!target?.hasOwnProperty?.(k)) { 5 | target[k] = source[k] 6 | } 7 | } 8 | 9 | for (let k of Object.getOwnPropertySymbols(source)) { 10 | if (!target?.hasOwnProperty?.(k)) { 11 | target[k] = source[k] 12 | } 13 | } 14 | } 15 | 16 | return target 17 | } 18 | -------------------------------------------------------------------------------- /src/util/escapeClassName.js: -------------------------------------------------------------------------------- 1 | import parser from 'postcss-selector-parser' 2 | import escapeCommas from './escapeCommas' 3 | 4 | export default function escapeClassName(className) { 5 | let node = parser.className() 6 | node.value = className 7 | return escapeCommas(node?.raws?.value ?? node.value) 8 | } 9 | -------------------------------------------------------------------------------- /src/util/escapeCommas.js: -------------------------------------------------------------------------------- 1 | export default function escapeCommas(className) { 2 | return className.replace(/\\,/g, '\\2c ') 3 | } 4 | -------------------------------------------------------------------------------- /src/util/flattenColorPalette.js: -------------------------------------------------------------------------------- 1 | const flattenColorPalette = (colors) => 2 | Object.assign( 3 | {}, 4 | ...Object.entries(colors ?? {}).flatMap(([color, values]) => 5 | typeof values == 'object' 6 | ? Object.entries(flattenColorPalette(values)).map(([number, hex]) => ({ 7 | [color + (number === 'DEFAULT' ? '' : `-${number}`)]: hex, 8 | })) 9 | : [{ [`${color}`]: values }] 10 | ) 11 | ) 12 | 13 | export default flattenColorPalette 14 | -------------------------------------------------------------------------------- /src/util/getAllConfigs.js: -------------------------------------------------------------------------------- 1 | import defaultConfig from '../../stubs/defaultConfig.stub.js' 2 | import { flagEnabled } from '../featureFlags' 3 | 4 | export default function getAllConfigs(config) { 5 | const configs = (config?.presets ?? [defaultConfig]) 6 | .slice() 7 | .reverse() 8 | .flatMap((preset) => getAllConfigs(preset instanceof Function ? preset() : preset)) 9 | 10 | const features = { 11 | // Add experimental configs here... 12 | } 13 | 14 | const experimentals = Object.keys(features) 15 | .filter((feature) => flagEnabled(config, feature)) 16 | .map((feature) => features[feature]) 17 | 18 | return [config, ...experimentals, ...configs] 19 | } 20 | -------------------------------------------------------------------------------- /src/util/hashConfig.js: -------------------------------------------------------------------------------- 1 | import hash from 'object-hash' 2 | 3 | export default function hashConfig(config) { 4 | return hash(config, { ignoreUnknown: true }) 5 | } 6 | -------------------------------------------------------------------------------- /src/util/isKeyframeRule.js: -------------------------------------------------------------------------------- 1 | export default function isKeyframeRule(rule) { 2 | return rule.parent && rule.parent.type === 'atrule' && /keyframes$/.test(rule.parent.name) 3 | } 4 | -------------------------------------------------------------------------------- /src/util/isPlainObject.js: -------------------------------------------------------------------------------- 1 | export default function isPlainObject(value) { 2 | if (Object.prototype.toString.call(value) !== '[object Object]') { 3 | return false 4 | } 5 | 6 | const prototype = Object.getPrototypeOf(value) 7 | return prototype === null || prototype === Object.prototype 8 | } 9 | -------------------------------------------------------------------------------- /src/util/isValidArbitraryValue.js: -------------------------------------------------------------------------------- 1 | let matchingBrackets = new Map([ 2 | ['{', '}'], 3 | ['[', ']'], 4 | ['(', ')'], 5 | ]) 6 | let inverseMatchingBrackets = new Map( 7 | Array.from(matchingBrackets.entries()).map(([k, v]) => [v, k]) 8 | ) 9 | 10 | let quotes = new Set(['"', "'", '`']) 11 | 12 | // Arbitrary values must contain balanced brackets (), [] and {}. Escaped 13 | // values don't count, and brackets inside quotes also don't count. 14 | // 15 | // E.g.: w-[this-is]w-[weird-and-invalid] 16 | // E.g.: w-[this-is\\]w-\\[weird-but-valid] 17 | // E.g.: content-['this-is-also-valid]-weirdly-enough'] 18 | export default function isValidArbitraryValue(value) { 19 | let stack = [] 20 | let inQuotes = false 21 | 22 | for (let i = 0; i < value.length; i++) { 23 | let char = value[i] 24 | 25 | if (char === ':' && !inQuotes && stack.length === 0) { 26 | return false 27 | } 28 | 29 | // Non-escaped quotes allow us to "allow" anything in between 30 | if (quotes.has(char) && value[i - 1] !== '\\') { 31 | inQuotes = !inQuotes 32 | } 33 | 34 | if (inQuotes) continue 35 | if (value[i - 1] === '\\') continue // Escaped 36 | 37 | if (matchingBrackets.has(char)) { 38 | stack.push(char) 39 | } else if (inverseMatchingBrackets.has(char)) { 40 | let inverse = inverseMatchingBrackets.get(char) 41 | 42 | // Nothing to pop from, therefore it is unbalanced 43 | if (stack.length <= 0) { 44 | return false 45 | } 46 | 47 | // Popped value must match the inverse value, otherwise it is unbalanced 48 | if (stack.pop() !== inverse) { 49 | return false 50 | } 51 | } 52 | } 53 | 54 | // If there is still something on the stack, it is also unbalanced 55 | if (stack.length > 0) { 56 | return false 57 | } 58 | 59 | // All good, totally balanced! 60 | return true 61 | } 62 | -------------------------------------------------------------------------------- /src/util/log.js: -------------------------------------------------------------------------------- 1 | import colors from 'picocolors' 2 | 3 | let alreadyShown = new Set() 4 | 5 | function log(type, messages, key) { 6 | if (process.env.JEST_WORKER_ID !== undefined) return 7 | 8 | if (key && alreadyShown.has(key)) return 9 | if (key) alreadyShown.add(key) 10 | 11 | console.warn('') 12 | messages.forEach((message) => console.warn(type, '-', message)) 13 | } 14 | 15 | export function dim(input) { 16 | return colors.dim(input) 17 | } 18 | 19 | export default { 20 | info(key, messages) { 21 | log(colors.bold(colors.cyan('info')), ...(Array.isArray(key) ? [key] : [messages, key])) 22 | }, 23 | warn(key, messages) { 24 | log(colors.bold(colors.yellow('warn')), ...(Array.isArray(key) ? [key] : [messages, key])) 25 | }, 26 | risk(key, messages) { 27 | log(colors.bold(colors.magenta('risk')), ...(Array.isArray(key) ? [key] : [messages, key])) 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /src/util/nameClass.js: -------------------------------------------------------------------------------- 1 | import escapeClassName from './escapeClassName' 2 | import escapeCommas from './escapeCommas' 3 | 4 | export function asClass(name) { 5 | return escapeCommas(`.${escapeClassName(name)}`) 6 | } 7 | 8 | export default function nameClass(classPrefix, key) { 9 | return asClass(formatClass(classPrefix, key)) 10 | } 11 | 12 | export function formatClass(classPrefix, key) { 13 | if (key === 'DEFAULT') { 14 | return classPrefix 15 | } 16 | 17 | if (key === '-' || key === '-DEFAULT') { 18 | return `-${classPrefix}` 19 | } 20 | 21 | if (key.startsWith('-')) { 22 | return `-${classPrefix}${key}` 23 | } 24 | 25 | return `${classPrefix}-${key}` 26 | } 27 | -------------------------------------------------------------------------------- /src/util/negateValue.js: -------------------------------------------------------------------------------- 1 | export default function (value) { 2 | value = `${value}` 3 | 4 | if (value === '0') { 5 | return '0' 6 | } 7 | 8 | // Flip sign of numbers 9 | if (/^[+-]?(\d+|\d*\.\d+)(e[+-]?\d+)?(%|\w+)?$/.test(value)) { 10 | return value.replace(/^[+-]?/, (sign) => (sign === '-' ? '' : '-')) 11 | } 12 | 13 | if (value.includes('var(') || value.includes('calc(')) { 14 | return `calc(${value} * -1)` 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/util/normalizeScreens.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A function that normalizes the various forms that the screens object can be 3 | * provided in. 4 | * 5 | * Input(s): 6 | * - ['100px', '200px'] // Raw strings 7 | * - { sm: '100px', md: '200px' } // Object with string values 8 | * - { sm: { min: '100px' }, md: { max: '100px' } } // Object with object values 9 | * - { sm: [{ min: '100px' }, { max: '200px' }] } // Object with object array (multiple values) 10 | * 11 | * Output(s): 12 | * - [{ name: 'sm', values: [{ min: '100px', max: '200px' }] }] // List of objects, that contains multiple values 13 | */ 14 | export function normalizeScreens(screens, root = true) { 15 | if (Array.isArray(screens)) { 16 | return screens.map((screen) => { 17 | if (root && Array.isArray(screen)) { 18 | throw new Error('The tuple syntax is not supported for `screens`.') 19 | } 20 | 21 | if (typeof screen === 'string') { 22 | return { name: screen.toString(), values: [{ min: screen, max: undefined }] } 23 | } 24 | 25 | let [name, options] = screen 26 | name = name.toString() 27 | 28 | if (typeof options === 'string') { 29 | return { name, values: [{ min: options, max: undefined }] } 30 | } 31 | 32 | if (Array.isArray(options)) { 33 | return { name, values: options.map((option) => resolveValue(option)) } 34 | } 35 | 36 | return { name, values: [resolveValue(options)] } 37 | }) 38 | } 39 | 40 | return normalizeScreens(Object.entries(screens ?? {}), false) 41 | } 42 | 43 | function resolveValue({ 'min-width': _minWidth, min = _minWidth, max, raw } = {}) { 44 | return { min, max, raw } 45 | } 46 | -------------------------------------------------------------------------------- /src/util/parseAnimationValue.js: -------------------------------------------------------------------------------- 1 | const DIRECTIONS = new Set(['normal', 'reverse', 'alternate', 'alternate-reverse']) 2 | const PLAY_STATES = new Set(['running', 'paused']) 3 | const FILL_MODES = new Set(['none', 'forwards', 'backwards', 'both']) 4 | const ITERATION_COUNTS = new Set(['infinite']) 5 | const TIMINGS = new Set([ 6 | 'linear', 7 | 'ease', 8 | 'ease-in', 9 | 'ease-out', 10 | 'ease-in-out', 11 | 'step-start', 12 | 'step-end', 13 | ]) 14 | const TIMING_FNS = ['cubic-bezier', 'steps'] 15 | 16 | const COMMA = /\,(?![^(]*\))/g // Comma separator that is not located between brackets. E.g.: `cubiz-bezier(a, b, c)` these don't count. 17 | const SPACE = /\ +(?![^(]*\))/g // Similar to the one above, but with spaces instead. 18 | const TIME = /^(-?[\d.]+m?s)$/ 19 | const DIGIT = /^(\d+)$/ 20 | 21 | export default function parseAnimationValue(input) { 22 | let animations = input.split(COMMA) 23 | return animations.map((animation) => { 24 | let value = animation.trim() 25 | let result = { value } 26 | let parts = value.split(SPACE) 27 | let seen = new Set() 28 | 29 | for (let part of parts) { 30 | if (!seen.has('DIRECTIONS') && DIRECTIONS.has(part)) { 31 | result.direction = part 32 | seen.add('DIRECTIONS') 33 | } else if (!seen.has('PLAY_STATES') && PLAY_STATES.has(part)) { 34 | result.playState = part 35 | seen.add('PLAY_STATES') 36 | } else if (!seen.has('FILL_MODES') && FILL_MODES.has(part)) { 37 | result.fillMode = part 38 | seen.add('FILL_MODES') 39 | } else if ( 40 | !seen.has('ITERATION_COUNTS') && 41 | (ITERATION_COUNTS.has(part) || DIGIT.test(part)) 42 | ) { 43 | result.iterationCount = part 44 | seen.add('ITERATION_COUNTS') 45 | } else if (!seen.has('TIMING_FUNCTION') && TIMINGS.has(part)) { 46 | result.timingFunction = part 47 | seen.add('TIMING_FUNCTION') 48 | } else if (!seen.has('TIMING_FUNCTION') && TIMING_FNS.some((f) => part.startsWith(`${f}(`))) { 49 | result.timingFunction = part 50 | seen.add('TIMING_FUNCTION') 51 | } else if (!seen.has('DURATION') && TIME.test(part)) { 52 | result.duration = part 53 | seen.add('DURATION') 54 | } else if (!seen.has('DELAY') && TIME.test(part)) { 55 | result.delay = part 56 | seen.add('DELAY') 57 | } else if (!seen.has('NAME')) { 58 | result.name = part 59 | seen.add('NAME') 60 | } else { 61 | if (!result.unknown) result.unknown = [] 62 | result.unknown.push(part) 63 | } 64 | } 65 | 66 | return result 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /src/util/parseBoxShadowValue.js: -------------------------------------------------------------------------------- 1 | import { splitAtTopLevelOnly } from './splitAtTopLevelOnly' 2 | 3 | let KEYWORDS = new Set(['inset', 'inherit', 'initial', 'revert', 'unset']) 4 | let SPACE = /\ +(?![^(]*\))/g // Similar to the one above, but with spaces instead. 5 | let LENGTH = /^-?(\d+|\.\d+)(.*?)$/g 6 | 7 | export function parseBoxShadowValue(input) { 8 | let shadows = Array.from(splitAtTopLevelOnly(input, ',')) 9 | return shadows.map((shadow) => { 10 | let value = shadow.trim() 11 | let result = { raw: value } 12 | let parts = value.split(SPACE) 13 | let seen = new Set() 14 | 15 | for (let part of parts) { 16 | // Reset index, since the regex is stateful. 17 | LENGTH.lastIndex = 0 18 | 19 | // Keyword 20 | if (!seen.has('KEYWORD') && KEYWORDS.has(part)) { 21 | result.keyword = part 22 | seen.add('KEYWORD') 23 | } 24 | 25 | // Length value 26 | else if (LENGTH.test(part)) { 27 | if (!seen.has('X')) { 28 | result.x = part 29 | seen.add('X') 30 | } else if (!seen.has('Y')) { 31 | result.y = part 32 | seen.add('Y') 33 | } else if (!seen.has('BLUR')) { 34 | result.blur = part 35 | seen.add('BLUR') 36 | } else if (!seen.has('SPREAD')) { 37 | result.spread = part 38 | seen.add('SPREAD') 39 | } 40 | } 41 | 42 | // Color or unknown 43 | else { 44 | if (!result.color) { 45 | result.color = part 46 | } else { 47 | if (!result.unknown) result.unknown = [] 48 | result.unknown.push(part) 49 | } 50 | } 51 | } 52 | 53 | // Check if valid 54 | result.valid = result.x !== undefined && result.y !== undefined 55 | 56 | return result 57 | }) 58 | } 59 | 60 | export function formatBoxShadowValue(shadows) { 61 | return shadows 62 | .map((shadow) => { 63 | if (!shadow.valid) { 64 | return shadow.raw 65 | } 66 | 67 | return [shadow.keyword, shadow.x, shadow.y, shadow.blur, shadow.spread, shadow.color] 68 | .filter(Boolean) 69 | .join(' ') 70 | }) 71 | .join(', ') 72 | } 73 | -------------------------------------------------------------------------------- /src/util/parseDependency.js: -------------------------------------------------------------------------------- 1 | import isGlob from 'is-glob' 2 | import globParent from 'glob-parent' 3 | import path from 'path' 4 | 5 | // Based on `glob-base` 6 | // https://github.com/micromatch/glob-base/blob/master/index.js 7 | function parseGlob(pattern) { 8 | let glob = pattern 9 | let base = globParent(pattern) 10 | 11 | if (base !== '.') { 12 | glob = pattern.substr(base.length) 13 | if (glob.charAt(0) === '/') { 14 | glob = glob.substr(1) 15 | } 16 | } 17 | 18 | if (glob.substr(0, 2) === './') { 19 | glob = glob.substr(2) 20 | } 21 | if (glob.charAt(0) === '/') { 22 | glob = glob.substr(1) 23 | } 24 | 25 | return { base, glob } 26 | } 27 | 28 | export default function parseDependency(normalizedFileOrGlob) { 29 | if (normalizedFileOrGlob.startsWith('!')) { 30 | return null 31 | } 32 | 33 | let message 34 | 35 | if (isGlob(normalizedFileOrGlob)) { 36 | let { base, glob } = parseGlob(normalizedFileOrGlob) 37 | message = { type: 'dir-dependency', dir: path.resolve(base), glob } 38 | } else { 39 | message = { type: 'dependency', file: path.resolve(normalizedFileOrGlob) } 40 | } 41 | 42 | // rollup-plugin-postcss does not support dir-dependency messages 43 | // but directories can be watched in the same way as files 44 | if (message.type === 'dir-dependency' && process.env.ROLLUP_WATCH === 'true') { 45 | message = { type: 'dependency', file: message.dir } 46 | } 47 | 48 | return message 49 | } 50 | -------------------------------------------------------------------------------- /src/util/parseObjectStyles.js: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss' 2 | import postcssNested from 'postcss-nested' 3 | import postcssJs from 'postcss-js' 4 | 5 | export default function parseObjectStyles(styles) { 6 | if (!Array.isArray(styles)) { 7 | return parseObjectStyles([styles]) 8 | } 9 | 10 | return styles.flatMap((style) => { 11 | return postcss([ 12 | postcssNested({ 13 | bubble: ['screen'], 14 | }), 15 | ]).process(style, { 16 | parser: postcssJs, 17 | }).root.nodes 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /src/util/prefixSelector.js: -------------------------------------------------------------------------------- 1 | import parser from 'postcss-selector-parser' 2 | 3 | export default function (prefix, selector, prependNegative = false) { 4 | return parser((selectors) => { 5 | selectors.walkClasses((classSelector) => { 6 | let baseClass = classSelector.value 7 | let shouldPlaceNegativeBeforePrefix = prependNegative && baseClass.startsWith('-') 8 | 9 | classSelector.value = shouldPlaceNegativeBeforePrefix 10 | ? `-${prefix}${baseClass.slice(1)}` 11 | : `${prefix}${baseClass}` 12 | }) 13 | }).processSync(selector) 14 | } 15 | -------------------------------------------------------------------------------- /src/util/resolveConfigPath.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | function isObject(value) { 5 | return typeof value === 'object' && value !== null 6 | } 7 | 8 | function isEmpty(obj) { 9 | return Object.keys(obj).length === 0 10 | } 11 | 12 | function isString(value) { 13 | return typeof value === 'string' || value instanceof String 14 | } 15 | 16 | export default function resolveConfigPath(pathOrConfig) { 17 | // require('tailwindcss')({ theme: ..., variants: ... }) 18 | if (isObject(pathOrConfig) && pathOrConfig.config === undefined && !isEmpty(pathOrConfig)) { 19 | return null 20 | } 21 | 22 | // require('tailwindcss')({ config: 'custom-config.js' }) 23 | if ( 24 | isObject(pathOrConfig) && 25 | pathOrConfig.config !== undefined && 26 | isString(pathOrConfig.config) 27 | ) { 28 | return path.resolve(pathOrConfig.config) 29 | } 30 | 31 | // require('tailwindcss')({ config: { theme: ..., variants: ... } }) 32 | if ( 33 | isObject(pathOrConfig) && 34 | pathOrConfig.config !== undefined && 35 | isObject(pathOrConfig.config) 36 | ) { 37 | return null 38 | } 39 | 40 | // require('tailwindcss')('custom-config.js') 41 | if (isString(pathOrConfig)) { 42 | return path.resolve(pathOrConfig) 43 | } 44 | 45 | // require('tailwindcss') 46 | for (const configFile of ['./tailwind.config.js', './tailwind.config.cjs']) { 47 | try { 48 | const configPath = path.resolve(configFile) 49 | fs.accessSync(configPath) 50 | return configPath 51 | } catch (err) {} 52 | } 53 | 54 | return null 55 | } 56 | -------------------------------------------------------------------------------- /src/util/responsive.js: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss' 2 | import cloneNodes from './cloneNodes' 3 | 4 | export default function responsive(rules) { 5 | return postcss 6 | .atRule({ 7 | name: 'responsive', 8 | }) 9 | .append(cloneNodes(Array.isArray(rules) ? rules : [rules])) 10 | } 11 | -------------------------------------------------------------------------------- /src/util/splitAtTopLevelOnly.js: -------------------------------------------------------------------------------- 1 | import * as regex from '../lib/regex' 2 | 3 | /** 4 | * This splits a string on a top-level character. 5 | * 6 | * Regex doesn't support recursion (at least not the JS-flavored version). 7 | * So we have to use a tiny state machine to keep track of paren placement. 8 | * 9 | * Expected behavior using commas: 10 | * var(--a, 0 0 1px rgb(0, 0, 0)), 0 0 1px rgb(0, 0, 0) 11 | * ─┬─ ┬ ┬ ┬ 12 | * x x x ╰──────── Split because top-level 13 | * ╰──────────────┴──┴───────────── Ignored b/c inside >= 1 levels of parens 14 | * 15 | * @param {string} input 16 | * @param {string} separator 17 | */ 18 | export function* splitAtTopLevelOnly(input, separator) { 19 | let SPECIALS = new RegExp(`[(){}\\[\\]${regex.escape(separator)}]`, 'g') 20 | 21 | let depth = 0 22 | let lastIndex = 0 23 | let found = false 24 | let separatorIndex = 0 25 | let separatorStart = 0 26 | let separatorLength = separator.length 27 | 28 | // Find all paren-like things & character 29 | // And only split on commas if they're top-level 30 | for (let match of input.matchAll(SPECIALS)) { 31 | let matchesSeparator = match[0] === separator[separatorIndex] 32 | let atEndOfSeparator = separatorIndex === separatorLength - 1 33 | let matchesFullSeparator = matchesSeparator && atEndOfSeparator 34 | 35 | if (match[0] === '(') depth++ 36 | if (match[0] === ')') depth-- 37 | if (match[0] === '[') depth++ 38 | if (match[0] === ']') depth-- 39 | if (match[0] === '{') depth++ 40 | if (match[0] === '}') depth-- 41 | 42 | if (matchesSeparator && depth === 0) { 43 | if (separatorStart === 0) { 44 | separatorStart = match.index 45 | } 46 | 47 | separatorIndex++ 48 | } 49 | 50 | if (matchesFullSeparator && depth === 0) { 51 | found = true 52 | 53 | yield input.substring(lastIndex, separatorStart) 54 | lastIndex = separatorStart + separatorLength 55 | } 56 | 57 | if (separatorIndex === separatorLength) { 58 | separatorIndex = 0 59 | separatorStart = 0 60 | } 61 | } 62 | 63 | // Provide the last segment of the string if available 64 | // Otherwise the whole string since no `char`s were found 65 | // This mirrors the behavior of string.split() 66 | if (found) { 67 | yield input.substring(lastIndex) 68 | } else { 69 | yield input 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/util/tap.js: -------------------------------------------------------------------------------- 1 | export function tap(value, mutator) { 2 | mutator(value) 3 | return value 4 | } 5 | -------------------------------------------------------------------------------- /src/util/toColorValue.js: -------------------------------------------------------------------------------- 1 | export default function toColorValue(maybeFunction) { 2 | return typeof maybeFunction === 'function' ? maybeFunction({}) : maybeFunction 3 | } 4 | -------------------------------------------------------------------------------- /src/util/toPath.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Parse a path string into an array of path segments. 3 | * 4 | * Square bracket notation `a[b]` may be used to "escape" dots that would otherwise be interpreted as path separators. 5 | * 6 | * Example: 7 | * a -> ['a] 8 | * a.b.c -> ['a', 'b', 'c'] 9 | * a[b].c -> ['a', 'b', 'c'] 10 | * a[b.c].e.f -> ['a', 'b.c', 'e', 'f'] 11 | * a[b][c][d] -> ['a', 'b', 'c', 'd'] 12 | * 13 | * @param {string|string[]} path 14 | **/ 15 | export function toPath(path) { 16 | if (Array.isArray(path)) return path 17 | 18 | let openBrackets = path.split('[').length - 1 19 | let closedBrackets = path.split(']').length - 1 20 | 21 | if (openBrackets !== closedBrackets) { 22 | throw new Error(`Path is invalid. Has unbalanced brackets: ${path}`) 23 | } 24 | 25 | return path.split(/\.(?![^\[]*\])|[\[\]]/g).filter(Boolean) 26 | } 27 | -------------------------------------------------------------------------------- /src/util/transformThemeValue.js: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss' 2 | 3 | export default function transformThemeValue(themeSection) { 4 | if (['fontSize', 'outline'].includes(themeSection)) { 5 | return (value) => { 6 | if (typeof value === 'function') value = value({}) 7 | if (Array.isArray(value)) value = value[0] 8 | 9 | return value 10 | } 11 | } 12 | 13 | if ( 14 | [ 15 | 'fontFamily', 16 | 'boxShadow', 17 | 'transitionProperty', 18 | 'transitionDuration', 19 | 'transitionDelay', 20 | 'transitionTimingFunction', 21 | 'backgroundImage', 22 | 'backgroundSize', 23 | 'backgroundColor', 24 | 'cursor', 25 | 'animation', 26 | ].includes(themeSection) 27 | ) { 28 | return (value) => { 29 | if (typeof value === 'function') value = value({}) 30 | if (Array.isArray(value)) value = value.join(', ') 31 | 32 | return value 33 | } 34 | } 35 | 36 | // For backwards compatibility reasons, before we switched to underscores 37 | // instead of commas for arbitrary values. 38 | if (['gridTemplateColumns', 'gridTemplateRows', 'objectPosition'].includes(themeSection)) { 39 | return (value) => { 40 | if (typeof value === 'function') value = value({}) 41 | if (typeof value === 'string') value = postcss.list.comma(value).join(' ') 42 | 43 | return value 44 | } 45 | } 46 | 47 | return (value) => { 48 | if (typeof value === 'function') value = value({}) 49 | 50 | return value 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/util/validateConfig.js: -------------------------------------------------------------------------------- 1 | import log from './log' 2 | 3 | export function validateConfig(config) { 4 | if (config.content.files.length === 0) { 5 | log.warn('content-problems', [ 6 | 'The `content` option in your Tailwind CSS configuration is missing or empty.', 7 | 'Configure your content sources or your generated CSS will be missing styles.', 8 | 'https://tailwindcss.com/docs/content-configuration', 9 | ]) 10 | } 11 | 12 | return config 13 | } 14 | -------------------------------------------------------------------------------- /src/util/withAlphaVariable.js: -------------------------------------------------------------------------------- 1 | import { parseColor, formatColor } from './color' 2 | 3 | export function withAlphaValue(color, alphaValue, defaultValue) { 4 | if (typeof color === 'function') { 5 | return color({ opacityValue: alphaValue }) 6 | } 7 | 8 | let parsed = parseColor(color) 9 | 10 | if (parsed === null) { 11 | return defaultValue 12 | } 13 | 14 | return formatColor({ ...parsed, alpha: alphaValue }) 15 | } 16 | 17 | export default function withAlphaVariable({ color, property, variable }) { 18 | let properties = [].concat(property) 19 | if (typeof color === 'function') { 20 | return { 21 | [variable]: '1', 22 | ...Object.fromEntries( 23 | properties.map((p) => { 24 | return [p, color({ opacityVariable: variable, opacityValue: `var(${variable})` })] 25 | }) 26 | ), 27 | } 28 | } 29 | 30 | const parsed = parseColor(color) 31 | 32 | if (parsed === null) { 33 | return Object.fromEntries(properties.map((p) => [p, color])) 34 | } 35 | 36 | if (parsed.alpha !== undefined) { 37 | // Has an alpha value, return color as-is 38 | return Object.fromEntries(properties.map((p) => [p, color])) 39 | } 40 | 41 | return { 42 | [variable]: '1', 43 | ...Object.fromEntries( 44 | properties.map((p) => { 45 | return [p, formatColor({ ...parsed, alpha: `var(${variable})` })] 46 | }) 47 | ), 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /standalone-cli/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /dist 3 | -------------------------------------------------------------------------------- /standalone-cli/README.md: -------------------------------------------------------------------------------- 1 | # Tailwind Standalone CLI 2 | 3 | > Standalone version of [Tailwind CLI](https://tailwindcss.com/docs/installation#using-tailwind-cli) 4 | 5 | ## Installation 6 | 7 | Download the appropriate platform-specific version of the `tailwindcss` cli from the [latest release](https://github.com/tailwindlabs/tailwindcss/releases). 8 | 9 | ## Usage 10 | 11 | ``` 12 | ./tailwindcss-macos -o tailwind.css 13 | ``` 14 | 15 | Check out the [Tailwind CLI documentation](https://tailwindcss.com/docs/installation#using-tailwind-cli) for details on the available options. 16 | -------------------------------------------------------------------------------- /standalone-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tailwindcss-standalone", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "build": "pkg standalone.js --out-path dist --targets node16-macos-x64,node16-macos-arm64,node16-win-x64,node16-linuxstatic-x64,node16-linuxstatic-arm64 --compress Brotli --no-bytecode --public-packages \"*\" --public", 6 | "prebuild": "rimraf dist", 7 | "postbuild": "move-file dist/standalone-macos-x64 dist/tailwindcss-macos-x64 && move-file dist/standalone-macos-arm64 dist/tailwindcss-macos-arm64 && move-file dist/standalone-win-x64.exe dist/tailwindcss-windows-x64.exe && move-file dist/standalone-linuxstatic-x64 dist/tailwindcss-linux-x64 && move-file dist/standalone-linuxstatic-arm64 dist/tailwindcss-linux-arm64", 8 | "test": "jest" 9 | }, 10 | "devDependencies": { 11 | "@tailwindcss/aspect-ratio": "^0.4.0", 12 | "@tailwindcss/forms": "^0.4.0", 13 | "@tailwindcss/line-clamp": "^0.3.0", 14 | "@tailwindcss/typography": "^0.5.0", 15 | "jest": "^27.2.5", 16 | "move-file-cli": "^3.0.0", 17 | "pkg": "^5.3.3", 18 | "rimraf": "^3.0.2", 19 | "tailwindcss": "file:.." 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /standalone-cli/standalone.js: -------------------------------------------------------------------------------- 1 | let Module = require('module') 2 | let origRequire = Module.prototype.require 3 | 4 | let localModules = { 5 | 'tailwindcss/colors': require('tailwindcss/colors'), 6 | 'tailwindcss/defaultConfig': require('tailwindcss/defaultConfig'), 7 | 'tailwindcss/defaultTheme': require('tailwindcss/defaultTheme'), 8 | 'tailwindcss/resolveConfig': require('tailwindcss/resolveConfig'), 9 | 'tailwindcss/plugin': require('tailwindcss/plugin'), 10 | 11 | '@tailwindcss/aspect-ratio': require('@tailwindcss/aspect-ratio'), 12 | '@tailwindcss/forms': require('@tailwindcss/forms'), 13 | '@tailwindcss/line-clamp': require('@tailwindcss/line-clamp'), 14 | '@tailwindcss/typography': require('@tailwindcss/typography'), 15 | } 16 | 17 | Module.prototype.require = function (id) { 18 | if (localModules.hasOwnProperty(id)) { 19 | return localModules[id] 20 | } 21 | return origRequire.apply(this, arguments) 22 | } 23 | 24 | require('tailwindcss/lib/cli') 25 | -------------------------------------------------------------------------------- /standalone-cli/tests/fixtures/basic.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /standalone-cli/tests/fixtures/plugins.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | -------------------------------------------------------------------------------- /standalone-cli/tests/fixtures/test.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('@tailwindcss/aspect-ratio'), 4 | require('@tailwindcss/forms')({ strategy: 'class' }), 5 | require('@tailwindcss/line-clamp'), 6 | require('@tailwindcss/typography'), 7 | ], 8 | } 9 | -------------------------------------------------------------------------------- /standalone-cli/tests/test.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process') 2 | 3 | const platformMap = { 4 | darwin: 'macos', 5 | win32: 'windows', 6 | linux: 'linux', 7 | } 8 | 9 | function exec(args) { 10 | return execSync( 11 | `./dist/tailwindcss-${platformMap[process.platform]}-${process.arch} ${args}` 12 | ).toString() 13 | } 14 | 15 | it('works', () => { 16 | expect(exec('--content tests/fixtures/basic.html')).toContain('.uppercase') 17 | }) 18 | 19 | it('supports first-party plugins', () => { 20 | let result = exec('--content tests/fixtures/plugins.html --config tests/fixtures/test.config.js') 21 | expect(result).toContain('.aspect-w-1') 22 | expect(result).toContain('.form-input') 23 | expect(result).toContain('.line-clamp-2') 24 | expect(result).toContain('.prose') 25 | }) 26 | -------------------------------------------------------------------------------- /stubs/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "semi": false, 4 | "singleQuote": true, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /stubs/defaultPostCssConfig.stub.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /stubs/simpleConfig.stub.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | } 8 | -------------------------------------------------------------------------------- /tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | @tailwind components; 4 | 5 | @tailwind utilities; 6 | -------------------------------------------------------------------------------- /tests/apply.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Title 8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /tests/collapse-adjacent-rules.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Title 8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/collapse-adjacent-rules.test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | import { run, html, css } from './util/run' 5 | 6 | test('collapse adjacent rules', () => { 7 | let config = { 8 | content: [path.resolve(__dirname, './collapse-adjacent-rules.test.html')], 9 | corePlugins: { preflight: false }, 10 | theme: {}, 11 | plugins: [ 12 | function ({ addVariant }) { 13 | addVariant('foo-bar', '@supports (foo: bar)') 14 | }, 15 | ], 16 | } 17 | 18 | let input = css` 19 | @tailwind base; 20 | @font-face { 21 | font-family: 'Inter'; 22 | src: url('/fonts/Inter.woff2') format('woff2'), url('/fonts/Inter.woff') format('woff'); 23 | } 24 | @font-face { 25 | font-family: 'Gilroy'; 26 | src: url('/fonts/Gilroy.woff2') format('woff2'), url('/fonts/Gilroy.woff') format('woff'); 27 | } 28 | @page { 29 | margin: 1cm; 30 | } 31 | @tailwind components; 32 | @tailwind utilities; 33 | @layer base { 34 | @font-face { 35 | font-family: 'Poppins'; 36 | src: url('/fonts/Poppins.woff2') format('woff2'), url('/fonts/Poppins.woff') format('woff'); 37 | } 38 | @font-face { 39 | font-family: 'Proxima Nova'; 40 | src: url('/fonts/ProximaNova.woff2') format('woff2'), 41 | url('/fonts/ProximaNova.woff') format('woff'); 42 | } 43 | } 44 | .foo, 45 | .bar { 46 | color: black; 47 | } 48 | .foo, 49 | .bar { 50 | font-weight: 700; 51 | } 52 | .some-apply-thing { 53 | @apply foo-bar:md:text-black foo-bar:md:font-bold foo-bar:text-black foo-bar:font-bold md:font-bold md:text-black; 54 | } 55 | ` 56 | 57 | return run(input, config).then((result) => { 58 | let expectedPath = path.resolve(__dirname, './collapse-adjacent-rules.test.css') 59 | let expected = fs.readFileSync(expectedPath, 'utf8') 60 | 61 | expect(result.css).toMatchFormattedCss(expected) 62 | }) 63 | }) 64 | 65 | test('duplicate url imports does not break rule collapsing', () => { 66 | let config = { 67 | content: [{ raw: html`` }], 68 | corePlugins: { preflight: false }, 69 | } 70 | 71 | let input = css` 72 | @import url('https://example.com'); 73 | @import url('https://example.com'); 74 | ` 75 | 76 | return run(input, config).then((result) => { 77 | expect(result.css).toMatchFormattedCss(css` 78 | @import url('https://example.com'); 79 | `) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /tests/combined-selectors.test.js: -------------------------------------------------------------------------------- 1 | import { run, html, css, defaults } from './util/run' 2 | 3 | it('should generate the partial selector, if only a partial is used (base layer)', () => { 4 | let config = { 5 | content: [{ raw: html`
` }], 6 | corePlugins: { preflight: false }, 7 | } 8 | 9 | let input = css` 10 | @tailwind base; 11 | 12 | @layer base { 13 | :root { 14 | font-weight: bold; 15 | } 16 | 17 | /* --- */ 18 | 19 | :root, 20 | .a { 21 | color: black; 22 | } 23 | } 24 | ` 25 | 26 | return run(input, config).then((result) => { 27 | return expect(result.css).toMatchFormattedCss(css` 28 | :root { 29 | font-weight: bold; 30 | } 31 | 32 | /* --- */ 33 | 34 | :root, 35 | .a { 36 | color: black; 37 | } 38 | 39 | ${defaults} 40 | `) 41 | }) 42 | }) 43 | 44 | it('should generate the partial selector, if only a partial is used (utilities layer)', () => { 45 | let config = { 46 | content: [{ raw: html`
` }], 47 | corePlugins: { preflight: false }, 48 | } 49 | 50 | let input = css` 51 | @tailwind utilities; 52 | 53 | @layer utilities { 54 | :root { 55 | font-weight: bold; 56 | } 57 | 58 | /* --- */ 59 | 60 | :root, 61 | .a { 62 | color: black; 63 | } 64 | } 65 | ` 66 | 67 | return run(input, config).then((result) => { 68 | return expect(result.css).toMatchFormattedCss(css` 69 | :root { 70 | font-weight: bold; 71 | } 72 | 73 | /* --- */ 74 | 75 | :root, 76 | .a { 77 | color: black; 78 | } 79 | `) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /tests/configurePlugins.test.js: -------------------------------------------------------------------------------- 1 | import configurePlugins from '../src/util/configurePlugins' 2 | 3 | test('setting a plugin to false removes it', () => { 4 | const plugins = ['fontSize', 'display', 'backgroundPosition'] 5 | 6 | const configuredPlugins = configurePlugins({ display: false }, plugins) 7 | 8 | expect(configuredPlugins).toEqual(['fontSize', 'backgroundPosition']) 9 | }) 10 | 11 | test('passing only false removes all plugins', () => { 12 | const plugins = ['fontSize', 'display', 'backgroundPosition'] 13 | 14 | const configuredPlugins = configurePlugins(false, plugins) 15 | 16 | expect(configuredPlugins).toEqual([]) 17 | }) 18 | 19 | test('passing an array whitelists plugins', () => { 20 | const plugins = ['fontSize', 'display', 'backgroundPosition'] 21 | 22 | const configuredPlugins = configurePlugins(['display'], plugins) 23 | 24 | expect(configuredPlugins).toEqual(['display']) 25 | }) 26 | -------------------------------------------------------------------------------- /tests/context-reuse.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Title 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/custom-extractors.test.css: -------------------------------------------------------------------------------- 1 | .bg-white { 2 | --tw-bg-opacity: 1; 3 | background-color: rgb(255 255 255 / var(--tw-bg-opacity)); 4 | } 5 | .text-indigo-500 { 6 | --tw-text-opacity: 1; 7 | color: rgb(99 102 241 / var(--tw-text-opacity)); 8 | } 9 | -------------------------------------------------------------------------------- /tests/custom-extractors.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Title 8 | 9 | 10 | 11 |
hello world
12 | text-red-500 shouldn't appear in the output 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/custom-extractors.test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | import { run } from './util/run' 5 | 6 | function customExtractor(content) { 7 | let matches = content.match(/class="([^"]+)"/) 8 | return matches ? matches[1].split(/\s+/) : [] 9 | } 10 | 11 | let expectedPath = path.resolve(__dirname, './custom-extractors.test.css') 12 | let expected = fs.readFileSync(expectedPath, 'utf8') 13 | 14 | describe('modern', () => { 15 | test('extract.DEFAULT', () => { 16 | let config = { 17 | content: { 18 | files: [path.resolve(__dirname, './custom-extractors.test.html')], 19 | extract: { 20 | DEFAULT: customExtractor, 21 | }, 22 | }, 23 | } 24 | 25 | return run('@tailwind utilities', config).then((result) => { 26 | expect(result.css).toMatchFormattedCss(expected) 27 | }) 28 | }) 29 | 30 | test('extract.{extension}', () => { 31 | let config = { 32 | content: { 33 | files: [path.resolve(__dirname, './custom-extractors.test.html')], 34 | extract: { 35 | html: customExtractor, 36 | }, 37 | }, 38 | } 39 | 40 | return run('@tailwind utilities', config).then((result) => { 41 | expect(result.css).toMatchFormattedCss(expected) 42 | }) 43 | }) 44 | 45 | test('extract function', () => { 46 | let config = { 47 | content: { 48 | files: [path.resolve(__dirname, './custom-extractors.test.html')], 49 | extract: customExtractor, 50 | }, 51 | } 52 | 53 | return run('@tailwind utilities', config).then((result) => { 54 | expect(result.css).toMatchFormattedCss(expected) 55 | }) 56 | }) 57 | }) 58 | 59 | describe('legacy', () => { 60 | test('defaultExtractor', () => { 61 | let config = { 62 | content: { 63 | files: [path.resolve(__dirname, './custom-extractors.test.html')], 64 | options: { 65 | defaultExtractor: customExtractor, 66 | }, 67 | }, 68 | } 69 | 70 | return run('@tailwind utilities', config).then((result) => { 71 | expect(result.css).toMatchFormattedCss(expected) 72 | }) 73 | }) 74 | 75 | test('extractors array', () => { 76 | let config = { 77 | content: { 78 | files: [path.resolve(__dirname, './custom-extractors.test.html')], 79 | options: { 80 | extractors: [ 81 | { 82 | extractor: customExtractor, 83 | extensions: ['html'], 84 | }, 85 | ], 86 | }, 87 | }, 88 | } 89 | 90 | return run('@tailwind utilities', config).then((result) => { 91 | expect(result.css).toMatchFormattedCss(expected) 92 | }) 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /tests/custom-separator.test.css: -------------------------------------------------------------------------------- 1 | .group:hover .group-hover_focus-within_text-left:focus-within { 2 | text-align: left; 3 | } 4 | [dir='rtl'] .rtl_active_text-center:active { 5 | text-align: center; 6 | } 7 | @media (prefers-reduced-motion: no-preference) { 8 | .motion-safe_hover_text-center:hover { 9 | text-align: center; 10 | } 11 | } 12 | .dark .dark_focus_text-left:focus { 13 | text-align: left; 14 | } 15 | @media (min-width: 768px) { 16 | .md_hover_text-right:hover { 17 | text-align: right; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/custom-separator.test.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | -------------------------------------------------------------------------------- /tests/custom-separator.test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | import { run } from './util/run' 5 | 6 | test('custom separator', () => { 7 | let config = { 8 | darkMode: 'class', 9 | content: [path.resolve(__dirname, './custom-separator.test.html')], 10 | separator: '_', 11 | } 12 | 13 | return run('@tailwind utilities', config).then((result) => { 14 | let expectedPath = path.resolve(__dirname, './custom-separator.test.css') 15 | let expected = fs.readFileSync(expectedPath, 'utf8') 16 | 17 | expect(result.css).toMatchFormattedCss(expected) 18 | }) 19 | }) 20 | 21 | test('dash is not supported', () => { 22 | let config = { 23 | darkMode: 'class', 24 | content: [{ raw: 'lg-hover-font-bold' }], 25 | separator: '-', 26 | } 27 | 28 | return expect(run('@tailwind utilities', config)).rejects.toThrowError( 29 | "The '-' character cannot be used as a custom separator in JIT mode due to parsing ambiguity. Please use another character like '_' instead." 30 | ) 31 | }) 32 | -------------------------------------------------------------------------------- /tests/custom-transformers.test.html: -------------------------------------------------------------------------------- 1 | blah blah blah 2 | -------------------------------------------------------------------------------- /tests/custom-transformers.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { run, html, css } from './util/run' 3 | 4 | function customTransformer(content) { 5 | return content.replace(/uppercase/g, 'lowercase') 6 | } 7 | 8 | test('transform function', () => { 9 | let config = { 10 | content: { 11 | files: [{ raw: html`
` }], 12 | transform: customTransformer, 13 | }, 14 | } 15 | 16 | return run('@tailwind utilities', config).then((result) => { 17 | expect(result.css).toMatchFormattedCss(css` 18 | .lowercase { 19 | text-transform: lowercase; 20 | } 21 | `) 22 | }) 23 | }) 24 | 25 | test('transform.DEFAULT', () => { 26 | let config = { 27 | content: { 28 | files: [{ raw: html`
` }], 29 | transform: { 30 | DEFAULT: customTransformer, 31 | }, 32 | }, 33 | } 34 | 35 | return run('@tailwind utilities', config).then((result) => { 36 | expect(result.css).toMatchFormattedCss(css` 37 | .lowercase { 38 | text-transform: lowercase; 39 | } 40 | `) 41 | }) 42 | }) 43 | 44 | test('transform.{extension}', () => { 45 | let config = { 46 | content: { 47 | files: [ 48 | path.resolve(__dirname, './custom-transformers.test.html'), 49 | path.resolve(__dirname, './custom-transformers.test.php'), 50 | ], 51 | transform: { 52 | html: () => 'uppercase', 53 | php: () => 'lowercase', 54 | }, 55 | }, 56 | } 57 | 58 | return run('@tailwind utilities', config).then((result) => { 59 | expect(result.css).toMatchFormattedCss(css` 60 | .uppercase { 61 | text-transform: uppercase; 62 | } 63 | .lowercase { 64 | text-transform: lowercase; 65 | } 66 | `) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /tests/custom-transformers.test.php: -------------------------------------------------------------------------------- 1 | blah blah blah -------------------------------------------------------------------------------- /tests/defaultConfig.test.js: -------------------------------------------------------------------------------- 1 | import config from '../src/public/default-config' 2 | import configStub from '../stubs/defaultConfig.stub.js' 3 | 4 | test('the default config matches the stub', () => { 5 | expect(config).toEqual(configStub) 6 | }) 7 | 8 | test('modifying the default config does not affect the stub', () => { 9 | config.theme = {} 10 | expect(config).not.toEqual(configStub) 11 | }) 12 | -------------------------------------------------------------------------------- /tests/defaultTheme.test.js: -------------------------------------------------------------------------------- 1 | import theme from '../src/public/default-theme' 2 | import configStub from '../stubs/defaultConfig.stub.js' 3 | 4 | test('the default theme matches the stub', () => { 5 | expect(theme).toEqual(configStub.theme) 6 | }) 7 | 8 | test('modifying the default theme does not affect the stub', () => { 9 | theme.colors = {} 10 | expect(theme).not.toEqual(configStub.theme) 11 | }) 12 | -------------------------------------------------------------------------------- /tests/detect-nesting.test.js: -------------------------------------------------------------------------------- 1 | import { run, html, css } from './util/run' 2 | 3 | it('should warn when we detect nested css', () => { 4 | let config = { 5 | content: [{ raw: html`
` }], 6 | } 7 | 8 | let input = css` 9 | @tailwind utilities; 10 | 11 | .nested { 12 | .example { 13 | } 14 | } 15 | ` 16 | 17 | return run(input, config).then((result) => { 18 | expect(result.messages).toHaveLength(1) 19 | expect(result.messages).toMatchObject([ 20 | { 21 | type: 'warning', 22 | text: [ 23 | 'Nested CSS was detected, but CSS nesting has not been configured correctly.', 24 | 'Please enable a CSS nesting plugin *before* Tailwind in your configuration.', 25 | 'See how here: https://tailwindcss.com/docs/using-with-preprocessors#nesting', 26 | ].join('\n'), 27 | }, 28 | ]) 29 | }) 30 | }) 31 | 32 | it('should warn when we detect namespaced @tailwind at rules', () => { 33 | let config = { 34 | content: [{ raw: html`
` }], 35 | } 36 | 37 | let input = css` 38 | .namespace { 39 | @tailwind utilities; 40 | } 41 | ` 42 | 43 | return run(input, config).then((result) => { 44 | expect(result.messages).toHaveLength(1) 45 | expect(result.messages).toMatchObject([ 46 | { 47 | type: 'warning', 48 | text: [ 49 | 'Nested @tailwind rules were detected, but are not supported.', 50 | "Consider using a prefix to scope Tailwind's classes: https://tailwindcss.com/docs/configuration#prefix", 51 | 'Alternatively, use the important selector strategy: https://tailwindcss.com/docs/configuration#selector-strategy', 52 | ].join('\n'), 53 | }, 54 | ]) 55 | }) 56 | }) 57 | 58 | it('should not warn when nesting a single rule inside a media query', () => { 59 | let config = { 60 | content: [{ raw: html`
` }], 61 | } 62 | 63 | let input = css` 64 | @tailwind utilities; 65 | 66 | @media (min-width: 768px) { 67 | .nested { 68 | } 69 | } 70 | ` 71 | 72 | return run(input, config).then((result) => { 73 | expect(result.messages).toHaveLength(0) 74 | expect(result.messages).toEqual([]) 75 | }) 76 | }) 77 | 78 | it('should only warn for the first detected nesting ', () => { 79 | let config = { 80 | content: [{ raw: html`
` }], 81 | } 82 | 83 | let input = css` 84 | @tailwind utilities; 85 | 86 | .nested { 87 | .example { 88 | } 89 | 90 | .other { 91 | } 92 | } 93 | 94 | .other { 95 | .example { 96 | } 97 | } 98 | ` 99 | 100 | return run(input, config).then((result) => { 101 | expect(result.messages).toHaveLength(1) 102 | }) 103 | }) 104 | -------------------------------------------------------------------------------- /tests/escapeClassName.test.js: -------------------------------------------------------------------------------- 1 | import escapeClassName from '../src/util/escapeClassName' 2 | 3 | test('invalid characters are escaped', () => { 4 | expect(escapeClassName('w:_$-1/2')).toEqual('w\\:_\\$-1\\/2') 5 | }) 6 | -------------------------------------------------------------------------------- /tests/extractor-edge-cases.test.js: -------------------------------------------------------------------------------- 1 | import { run, html, css } from './util/run' 2 | 3 | test('PHP arrays', async () => { 4 | let config = { 5 | content: [ 6 | { raw: html`

">Hello world

` }, 7 | ], 8 | } 9 | 10 | return run('@tailwind utilities', config).then((result) => { 11 | expect(result.css).toMatchFormattedCss(css` 12 | .max-w-\[16rem\] { 13 | max-width: 16rem; 14 | } 15 | `) 16 | }) 17 | }) 18 | 19 | test('arbitrary values with quotes', async () => { 20 | let config = { content: [{ raw: html`
` }] } 21 | 22 | return run('@tailwind utilities', config).then((result) => { 23 | expect(result.css).toMatchFormattedCss(css` 24 | .content-\[\'hello\]\'\] { 25 | --tw-content: 'hello]'; 26 | content: var(--tw-content); 27 | } 28 | `) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /tests/fixtures/custom-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [{ raw: '
' }], 3 | theme: { 4 | screens: { 5 | mobile: '400px', 6 | }, 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /tests/fixtures/custom-purge-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./tests/fixtures/*.html'], 3 | theme: { 4 | extend: { 5 | colors: { 6 | 'black!': '#000', 7 | }, 8 | spacing: { 9 | 1.5: '0.375rem', 10 | '(1/2+8)': 'calc(50% + 2rem)', 11 | }, 12 | minHeight: { 13 | '(screen-4)': 'calc(100vh - 1rem)', 14 | }, 15 | fontFamily: { 16 | '%#$@': 'Comic Sans', 17 | }, 18 | }, 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /tests/fixtures/esm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/purge-example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | My Page 5 | 6 | 7 |
8 |
9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | 24 |
25 | 26 | 27 | span.inline-grid.grid-cols-3(class="px-1.5") 28 | .col-span-2 29 | Hello 30 | .col-span-1.text-center 31 | World! 32 | 33 | 34 | .flow-root 35 | .text-green-700.bg-green-100 36 | .text-left= content 37 | %samp.font-mono{:data-foo => "bar"}= output 38 | .col-span-4[aria-hidden=true] 39 | .tracking-tight#headline 40 | 41 | 42 | { 43 | "helloThere": "Hello there, Mr. Jones" 44 | } -------------------------------------------------------------------------------- /tests/flattenColorPalette.test.js: -------------------------------------------------------------------------------- 1 | import flattenColorPalette from '../src/util/flattenColorPalette' 2 | 3 | test('it flattens nested color objects', () => { 4 | expect( 5 | flattenColorPalette({ 6 | purple: 'purple', 7 | white: { 8 | 25: 'rgba(255,255,255,.25)', 9 | 50: 'rgba(255,255,255,.5)', 10 | 75: 'rgba(255,255,255,.75)', 11 | DEFAULT: '#fff', 12 | }, 13 | red: { 14 | 1: 'rgb(33,0,0)', 15 | 2: 'rgb(67,0,0)', 16 | 3: 'rgb(100,0,0)', 17 | }, 18 | green: { 19 | 1: 'rgb(0,33,0)', 20 | 2: 'rgb(0,67,0)', 21 | 3: 'rgb(0,100,0)', 22 | }, 23 | blue: { 24 | 1: 'rgb(0,0,33)', 25 | 2: 'rgb(0,0,67)', 26 | 3: 'rgb(0,0,100)', 27 | }, 28 | }) 29 | ).toEqual({ 30 | purple: 'purple', 31 | 'white-25': 'rgba(255,255,255,.25)', 32 | 'white-50': 'rgba(255,255,255,.5)', 33 | 'white-75': 'rgba(255,255,255,.75)', 34 | white: '#fff', 35 | 'red-1': 'rgb(33,0,0)', 36 | 'red-2': 'rgb(67,0,0)', 37 | 'red-3': 'rgb(100,0,0)', 38 | 'green-1': 'rgb(0,33,0)', 39 | 'green-2': 'rgb(0,67,0)', 40 | 'green-3': 'rgb(0,100,0)', 41 | 'blue-1': 'rgb(0,0,33)', 42 | 'blue-2': 'rgb(0,0,67)', 43 | 'blue-3': 'rgb(0,0,100)', 44 | }) 45 | }) 46 | 47 | test('it flattens deeply nested color objects', () => { 48 | expect( 49 | flattenColorPalette({ 50 | primary: 'purple', 51 | secondary: { 52 | DEFAULT: 'blue', 53 | hover: 'cyan', 54 | focus: 'red', 55 | }, 56 | button: { 57 | primary: { 58 | DEFAULT: 'magenta', 59 | hover: 'green', 60 | focus: { 61 | DEFAULT: 'yellow', 62 | variant: 'orange', 63 | }, 64 | }, 65 | }, 66 | }) 67 | ).toEqual({ 68 | primary: 'purple', 69 | secondary: 'blue', 70 | 'secondary-hover': 'cyan', 71 | 'secondary-focus': 'red', 72 | 'button-primary': 'magenta', 73 | 'button-primary-hover': 'green', 74 | 'button-primary-focus': 'yellow', 75 | 'button-primary-focus-variant': 'orange', 76 | }) 77 | }) 78 | 79 | test('it handles empty objects', () => { 80 | expect(flattenColorPalette({})).toEqual({}) 81 | }) 82 | -------------------------------------------------------------------------------- /tests/getClassList.test.js: -------------------------------------------------------------------------------- 1 | import resolveConfig from '../src/public/resolve-config' 2 | import { createContext } from '../src/lib/setupContextUtils' 3 | 4 | it('should generate every possible class, without variants', () => { 5 | let config = {} 6 | 7 | let context = createContext(resolveConfig(config)) 8 | let classes = context.getClassList() 9 | expect(classes).toBeInstanceOf(Array) 10 | 11 | // Verify we have a `container` for the 'components' section. 12 | expect(classes).toContain('container') 13 | 14 | // Verify we handle the DEFAULT case correctly 15 | expect(classes).toContain('border') 16 | 17 | // Verify we handle negative values correctly 18 | expect(classes).toContain('-inset-1/4') 19 | expect(classes).toContain('-m-0') 20 | expect(classes).not.toContain('-uppercase') 21 | expect(classes).not.toContain('-opacity-50') 22 | 23 | config = { theme: { extend: { margin: { DEFAULT: '5px' } } } } 24 | context = createContext(resolveConfig(config)) 25 | classes = context.getClassList() 26 | 27 | expect(classes).not.toContain('-m-DEFAULT') 28 | }) 29 | 30 | it('should generate every possible class while handling negatives and prefixes', () => { 31 | let config = { prefix: 'tw-' } 32 | let context = createContext(resolveConfig(config)) 33 | let classes = context.getClassList() 34 | expect(classes).toBeInstanceOf(Array) 35 | 36 | // Verify we have a `container` for the 'components' section. 37 | expect(classes).toContain('tw-container') 38 | 39 | // Verify we handle the DEFAULT case correctly 40 | expect(classes).toContain('tw-border') 41 | 42 | // Verify we handle negative values correctly 43 | expect(classes).toContain('-tw-inset-1/4') 44 | expect(classes).toContain('-tw-m-0') 45 | expect(classes).not.toContain('-tw-uppercase') 46 | expect(classes).not.toContain('-tw-opacity-50') 47 | 48 | // These utilities do work but there's no reason to generate 49 | // them alongside the `-{prefix}-{utility}` versions 50 | expect(classes).not.toContain('tw--inset-1/4') 51 | expect(classes).not.toContain('tw--m-0') 52 | 53 | config = { 54 | prefix: 'tw-', 55 | theme: { extend: { margin: { DEFAULT: '5px' } } }, 56 | } 57 | context = createContext(resolveConfig(config)) 58 | classes = context.getClassList() 59 | 60 | expect(classes).not.toContain('-tw-m-DEFAULT') 61 | }) 62 | -------------------------------------------------------------------------------- /tests/import-syntax.test.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | font-size: 32px; 3 | } 4 | *, 5 | ::before, 6 | ::after { 7 | --tw-border-spacing-x: 0; 8 | --tw-border-spacing-y: 0; 9 | --tw-translate-x: 0; 10 | --tw-translate-y: 0; 11 | --tw-rotate: 0; 12 | --tw-skew-x: 0; 13 | --tw-skew-y: 0; 14 | --tw-scale-x: 1; 15 | --tw-scale-y: 1; 16 | --tw-pan-x: ; 17 | --tw-pan-y: ; 18 | --tw-pinch-zoom: ; 19 | --tw-scroll-snap-strictness: proximity; 20 | --tw-ordinal: ; 21 | --tw-slashed-zero: ; 22 | --tw-numeric-figure: ; 23 | --tw-numeric-spacing: ; 24 | --tw-numeric-fraction: ; 25 | --tw-ring-inset: ; 26 | --tw-ring-offset-width: 0px; 27 | --tw-ring-offset-color: #fff; 28 | --tw-ring-color: rgb(59 130 246 / 0.5); 29 | --tw-ring-offset-shadow: 0 0 #0000; 30 | --tw-ring-shadow: 0 0 #0000; 31 | --tw-shadow: 0 0 #0000; 32 | --tw-shadow-colored: 0 0 #0000; 33 | --tw-blur: ; 34 | --tw-brightness: ; 35 | --tw-contrast: ; 36 | --tw-grayscale: ; 37 | --tw-hue-rotate: ; 38 | --tw-invert: ; 39 | --tw-saturate: ; 40 | --tw-sepia: ; 41 | --tw-drop-shadow: ; 42 | --tw-backdrop-blur: ; 43 | --tw-backdrop-brightness: ; 44 | --tw-backdrop-contrast: ; 45 | --tw-backdrop-grayscale: ; 46 | --tw-backdrop-hue-rotate: ; 47 | --tw-backdrop-invert: ; 48 | --tw-backdrop-opacity: ; 49 | --tw-backdrop-saturate: ; 50 | --tw-backdrop-sepia: ; 51 | } 52 | .container { 53 | width: 100%; 54 | } 55 | @media (min-width: 640px) { 56 | .container { 57 | max-width: 640px; 58 | } 59 | } 60 | @media (min-width: 768px) { 61 | .container { 62 | max-width: 768px; 63 | } 64 | } 65 | @media (min-width: 1024px) { 66 | .container { 67 | max-width: 1024px; 68 | } 69 | } 70 | @media (min-width: 1280px) { 71 | .container { 72 | max-width: 1280px; 73 | } 74 | } 75 | @media (min-width: 1536px) { 76 | .container { 77 | max-width: 1536px; 78 | } 79 | } 80 | .mt-6 { 81 | margin-top: 1.5rem; 82 | } 83 | .bg-black { 84 | --tw-bg-opacity: 1; 85 | background-color: rgb(0 0 0 / var(--tw-bg-opacity)); 86 | } 87 | @media (min-width: 768px) { 88 | .md\:hover\:text-center:hover { 89 | text-align: center; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/import-syntax.test.html: -------------------------------------------------------------------------------- 1 |

Hello world!

2 |
3 |
4 |
5 |
6 | -------------------------------------------------------------------------------- /tests/import-syntax.test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | import { run, css } from './util/run' 5 | 6 | test('using @import instead of @tailwind', () => { 7 | let config = { 8 | content: [path.resolve(__dirname, './import-syntax.test.html')], 9 | corePlugins: { preflight: false }, 10 | plugins: [ 11 | function ({ addBase }) { 12 | addBase({ 13 | h1: { 14 | fontSize: '32px', 15 | }, 16 | }) 17 | }, 18 | ], 19 | } 20 | 21 | let input = css` 22 | @import 'tailwindcss/base'; 23 | @import 'tailwindcss/components'; 24 | @import 'tailwindcss/utilities'; 25 | ` 26 | 27 | return run(input, config).then((result) => { 28 | let expectedPath = path.resolve(__dirname, './import-syntax.test.css') 29 | let expected = fs.readFileSync(expectedPath, 'utf8') 30 | 31 | expect(result.css).toMatchFormattedCss(expected) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /tests/important-boolean.test.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /tests/important-boolean.test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | import { run, css } from './util/run' 5 | 6 | test('important boolean', () => { 7 | let config = { 8 | important: true, 9 | darkMode: 'class', 10 | content: [path.resolve(__dirname, './important-boolean.test.html')], 11 | corePlugins: { preflight: false }, 12 | plugins: [ 13 | function ({ addComponents, addUtilities }) { 14 | addComponents( 15 | { 16 | '.btn': { 17 | button: 'yes', 18 | }, 19 | }, 20 | { respectImportant: true } 21 | ) 22 | addComponents( 23 | { 24 | '@font-face': { 25 | 'font-family': 'Inter', 26 | }, 27 | '@page': { 28 | margin: '1cm', 29 | }, 30 | }, 31 | { respectImportant: true } 32 | ) 33 | addUtilities( 34 | { 35 | '.custom-util': { 36 | button: 'no', 37 | }, 38 | }, 39 | { respectImportant: false } 40 | ) 41 | }, 42 | ], 43 | } 44 | 45 | let input = css` 46 | @tailwind base; 47 | @tailwind components; 48 | @layer components { 49 | .custom-component { 50 | @apply font-bold; 51 | } 52 | .custom-important-component { 53 | @apply text-center !important; 54 | } 55 | } 56 | @tailwind utilities; 57 | ` 58 | 59 | return run(input, config).then((result) => { 60 | let expectedPath = path.resolve(__dirname, './important-boolean.test.css') 61 | let expected = fs.readFileSync(expectedPath, 'utf8') 62 | 63 | expect(result.css).toMatchFormattedCss(expected) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /tests/important-modifier-prefix.test.css: -------------------------------------------------------------------------------- 1 | *, 2 | ::before, 3 | ::after { 4 | --tw-border-spacing-x: 0; 5 | --tw-border-spacing-y: 0; 6 | --tw-translate-x: 0; 7 | --tw-translate-y: 0; 8 | --tw-rotate: 0; 9 | --tw-skew-x: 0; 10 | --tw-skew-y: 0; 11 | --tw-scale-x: 1; 12 | --tw-scale-y: 1; 13 | --tw-pan-x: ; 14 | --tw-pan-y: ; 15 | --tw-pinch-zoom: ; 16 | --tw-scroll-snap-strictness: proximity; 17 | --tw-ordinal: ; 18 | --tw-slashed-zero: ; 19 | --tw-numeric-figure: ; 20 | --tw-numeric-spacing: ; 21 | --tw-numeric-fraction: ; 22 | --tw-ring-inset: ; 23 | --tw-ring-offset-width: 0px; 24 | --tw-ring-offset-color: #fff; 25 | --tw-ring-color: rgb(59 130 246 / 0.5); 26 | --tw-ring-offset-shadow: 0 0 #0000; 27 | --tw-ring-shadow: 0 0 #0000; 28 | --tw-shadow: 0 0 #0000; 29 | --tw-shadow-colored: 0 0 #0000; 30 | --tw-blur: ; 31 | --tw-brightness: ; 32 | --tw-contrast: ; 33 | --tw-grayscale: ; 34 | --tw-hue-rotate: ; 35 | --tw-invert: ; 36 | --tw-saturate: ; 37 | --tw-sepia: ; 38 | --tw-drop-shadow: ; 39 | --tw-backdrop-blur: ; 40 | --tw-backdrop-brightness: ; 41 | --tw-backdrop-contrast: ; 42 | --tw-backdrop-grayscale: ; 43 | --tw-backdrop-hue-rotate: ; 44 | --tw-backdrop-invert: ; 45 | --tw-backdrop-opacity: ; 46 | --tw-backdrop-saturate: ; 47 | --tw-backdrop-sepia: ; 48 | } 49 | .\!tw-container { 50 | width: 100% !important; 51 | } 52 | @media (min-width: 640px) { 53 | .\!tw-container { 54 | max-width: 640px !important; 55 | } 56 | } 57 | @media (min-width: 768px) { 58 | .\!tw-container { 59 | max-width: 768px !important; 60 | } 61 | } 62 | @media (min-width: 1024px) { 63 | .\!tw-container { 64 | max-width: 1024px !important; 65 | } 66 | } 67 | @media (min-width: 1280px) { 68 | .\!tw-container { 69 | max-width: 1280px !important; 70 | } 71 | } 72 | @media (min-width: 1536px) { 73 | .\!tw-container { 74 | max-width: 1536px !important; 75 | } 76 | } 77 | .\!tw-font-bold { 78 | font-weight: 700 !important; 79 | } 80 | .hover\:\!tw-text-center:hover { 81 | text-align: center !important; 82 | } 83 | @media (min-width: 1024px) { 84 | .lg\:\!tw-opacity-50 { 85 | opacity: 0.5 !important; 86 | } 87 | } 88 | @media (min-width: 1280px) { 89 | .xl\:focus\:disabled\:\!tw-float-right:disabled:focus { 90 | float: right !important; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/important-modifier-prefix.test.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |
6 |
7 |
8 | -------------------------------------------------------------------------------- /tests/important-modifier-prefix.test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | import { run, css } from './util/run' 5 | 6 | test('important modifier with prefix', () => { 7 | let config = { 8 | important: false, 9 | prefix: 'tw-', 10 | darkMode: 'class', 11 | content: [path.resolve(__dirname, './important-modifier-prefix.test.html')], 12 | corePlugins: { preflight: false }, 13 | } 14 | 15 | let input = css` 16 | @tailwind base; 17 | @tailwind components; 18 | @tailwind utilities; 19 | ` 20 | 21 | return run(input, config).then((result) => { 22 | let expectedPath = path.resolve(__dirname, './important-modifier-prefix.test.css') 23 | let expected = fs.readFileSync(expectedPath, 'utf8') 24 | 25 | expect(result.css).toMatchFormattedCss(expected) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /tests/important-modifier.test.js: -------------------------------------------------------------------------------- 1 | import { run, css, html } from './util/run' 2 | 3 | test('important modifier', () => { 4 | let config = { 5 | important: false, 6 | darkMode: 'class', 7 | content: [ 8 | { 9 | raw: html` 10 |
11 |
12 |
13 |
14 |
15 |
16 | `, 17 | }, 18 | ], 19 | corePlugins: { preflight: false }, 20 | plugins: [ 21 | function ({ theme, matchUtilities }) { 22 | matchUtilities( 23 | { 24 | 'custom-parent': (value) => { 25 | return { 26 | '.custom-child': { 27 | margin: value, 28 | }, 29 | } 30 | }, 31 | }, 32 | { values: theme('spacing') } 33 | ) 34 | }, 35 | ], 36 | } 37 | 38 | let input = css` 39 | @tailwind components; 40 | @tailwind utilities; 41 | ` 42 | 43 | return run(input, config).then((result) => { 44 | expect(result.css).toMatchFormattedCss(css` 45 | .\!container { 46 | width: 100% !important; 47 | } 48 | @media (min-width: 640px) { 49 | .\!container { 50 | max-width: 640px !important; 51 | } 52 | } 53 | @media (min-width: 768px) { 54 | .\!container { 55 | max-width: 768px !important; 56 | } 57 | } 58 | @media (min-width: 1024px) { 59 | .\!container { 60 | max-width: 1024px !important; 61 | } 62 | } 63 | @media (min-width: 1280px) { 64 | .\!container { 65 | max-width: 1280px !important; 66 | } 67 | } 68 | @media (min-width: 1536px) { 69 | .\!container { 70 | max-width: 1536px !important; 71 | } 72 | } 73 | .\!font-bold { 74 | font-weight: 700 !important; 75 | } 76 | .\!custom-parent-5 .custom-child { 77 | margin: 1.25rem !important; 78 | } 79 | .hover\:\!text-center:hover { 80 | text-align: center !important; 81 | } 82 | @media (min-width: 1024px) { 83 | .lg\:\!opacity-50 { 84 | opacity: 0.5 !important; 85 | } 86 | } 87 | @media (min-width: 1280px) { 88 | .xl\:focus\:disabled\:\!float-right:disabled:focus { 89 | float: right !important; 90 | } 91 | } 92 | `) 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /tests/important-selector.test.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /tests/important-selector.test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | import { run, css } from './util/run' 5 | 6 | test('important selector', () => { 7 | let config = { 8 | important: '#app', 9 | darkMode: 'class', 10 | content: [path.resolve(__dirname, './important-selector.test.html')], 11 | corePlugins: { preflight: false }, 12 | plugins: [ 13 | function ({ addComponents, addUtilities }) { 14 | addComponents( 15 | { 16 | '.btn': { 17 | button: 'yes', 18 | }, 19 | }, 20 | { respectImportant: true } 21 | ) 22 | addComponents( 23 | { 24 | '@font-face': { 25 | 'font-family': 'Inter', 26 | }, 27 | '@page': { 28 | margin: '1cm', 29 | }, 30 | }, 31 | { respectImportant: true } 32 | ) 33 | addUtilities( 34 | { 35 | '.custom-util': { 36 | button: 'no', 37 | }, 38 | }, 39 | { respectImportant: false } 40 | ) 41 | }, 42 | ], 43 | } 44 | 45 | let input = css` 46 | @tailwind base; 47 | @tailwind components; 48 | @layer components { 49 | .custom-component { 50 | @apply font-bold; 51 | } 52 | .custom-important-component { 53 | @apply text-center !important; 54 | } 55 | } 56 | @tailwind utilities; 57 | ` 58 | 59 | return run(input, config).then((result) => { 60 | let expectedPath = path.resolve(__dirname, './important-selector.test.css') 61 | let expected = fs.readFileSync(expectedPath, 'utf8') 62 | 63 | expect(result.css).toMatchFormattedCss(expected) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /tests/layer-without-tailwind.test.css: -------------------------------------------------------------------------------- 1 | @layer components { 2 | .foo { 3 | color: black; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/layer-without-tailwind.test.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /tests/layer-without-tailwind.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import { run, css } from './util/run' 4 | 5 | test('using @layer without @tailwind', async () => { 6 | let config = { 7 | content: [path.resolve(__dirname, './layer-without-tailwind.test.html')], 8 | } 9 | 10 | let input = css` 11 | @layer components { 12 | .foo { 13 | color: black; 14 | } 15 | } 16 | ` 17 | 18 | await expect(run(input, config)).rejects.toThrowError( 19 | '`@layer components` is used but no matching `@tailwind components` directive is present.' 20 | ) 21 | }) 22 | 23 | test('using @responsive without @tailwind', async () => { 24 | let config = { 25 | content: [path.resolve(__dirname, './layer-without-tailwind.test.html')], 26 | } 27 | 28 | let input = css` 29 | @responsive { 30 | .foo { 31 | color: black; 32 | } 33 | } 34 | ` 35 | 36 | await expect(run(input, config)).rejects.toThrowError( 37 | '`@responsive` is used but `@tailwind utilities` is missing.' 38 | ) 39 | }) 40 | 41 | test('using @variants without @tailwind', async () => { 42 | let config = { 43 | content: [path.resolve(__dirname, './layer-without-tailwind.test.html')], 44 | } 45 | 46 | let input = css` 47 | @variants hover { 48 | .foo { 49 | color: black; 50 | } 51 | } 52 | ` 53 | 54 | await expect(run(input, config)).rejects.toThrowError( 55 | '`@variants` is used but `@tailwind utilities` is missing.' 56 | ) 57 | }) 58 | 59 | test('non-Tailwind @layer rules are okay', async () => { 60 | let config = { 61 | content: [path.resolve(__dirname, './layer-without-tailwind.test.html')], 62 | } 63 | 64 | let input = css` 65 | @layer custom { 66 | .foo { 67 | color: black; 68 | } 69 | } 70 | ` 71 | 72 | return run(input, config).then((result) => { 73 | expect(result.css).toMatchFormattedCss(css` 74 | @layer custom { 75 | .foo { 76 | color: black; 77 | } 78 | } 79 | `) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /tests/match-components.test.js: -------------------------------------------------------------------------------- 1 | import { run, html, css, defaults } from './util/run' 2 | 3 | it('should be possible to matchComponents', () => { 4 | let config = { 5 | content: [ 6 | { 7 | raw: html`
8 |
9 |
10 | 11 |
`, 12 | }, 13 | ], 14 | corePlugins: { 15 | preflight: false, 16 | }, 17 | plugins: [ 18 | function ({ matchComponents }) { 19 | matchComponents({ 20 | card: (value) => { 21 | return [ 22 | { color: value }, 23 | { 24 | '.card-header': { 25 | borderTopWidth: 3, 26 | borderTopColor: value, 27 | }, 28 | }, 29 | { 30 | '.card-footer': { 31 | borderBottomWidth: 3, 32 | borderBottomColor: value, 33 | }, 34 | }, 35 | ] 36 | }, 37 | }) 38 | }, 39 | ], 40 | } 41 | 42 | return run('@tailwind base; @tailwind components; @tailwind utilities', config).then((result) => { 43 | return expect(result.css).toMatchFormattedCss(css` 44 | ${defaults} 45 | 46 | .card-\[\#0088cc\] { 47 | color: #0088cc; 48 | } 49 | 50 | .card-\[\#0088cc\] .card-header { 51 | border-top-width: 3px; 52 | border-top-color: #0088cc; 53 | } 54 | 55 | .card-\[\#0088cc\] .card-footer { 56 | border-bottom-width: 3px; 57 | border-bottom-color: #0088cc; 58 | } 59 | 60 | .text-center { 61 | text-align: center; 62 | } 63 | 64 | .font-bold { 65 | font-weight: 700; 66 | } 67 | 68 | .shadow { 69 | --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); 70 | --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 71 | 0 1px 2px -1px var(--tw-shadow-color); 72 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), 73 | var(--tw-shadow); 74 | } 75 | 76 | .hover\:card-\[\#f0f\]:hover { 77 | color: #f0f; 78 | } 79 | 80 | .hover\:card-\[\#f0f\]:hover .card-header { 81 | border-top-width: 3px; 82 | border-top-color: #f0f; 83 | } 84 | 85 | .hover\:card-\[\#f0f\]:hover .card-footer { 86 | border-bottom-width: 3px; 87 | border-bottom-color: #f0f; 88 | } 89 | `) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /tests/minimum-impact-selector.test.js: -------------------------------------------------------------------------------- 1 | import { elementSelectorParser } from '../src/lib/resolveDefaultsAtRules' 2 | 3 | it.each` 4 | before | after 5 | ${'*'} | ${'*'} 6 | ${'*:hover'} | ${'*'} 7 | ${'* > *'} | ${'* > *'} 8 | ${'.foo'} | ${'.foo'} 9 | ${'.foo:hover'} | ${'.foo'} 10 | ${'.foo:focus:hover'} | ${'.foo'} 11 | ${'li:first-child'} | ${'li'} 12 | ${'li:before'} | ${'li:before'} 13 | ${'li::before'} | ${'li::before'} 14 | ${'#app .foo'} | ${'.foo'} 15 | ${'#app'} | ${'[id=app]'} 16 | ${'#app.other'} | ${'.other'} 17 | ${'input[type="text"]'} | ${'[type="text"]'} 18 | ${'input[type="text"].foo'} | ${'.foo'} 19 | ${'.group .group\\:foo'} | ${'.group\\:foo'} 20 | ${'.group:hover .group-hover\\:foo'} | ${'.group-hover\\:foo'} 21 | ${'.owl > * + *'} | ${'.owl > *'} 22 | ${'.owl > :not([hidden]) + :not([hidden])'} | ${'.owl > *'} 23 | ${'.group:hover .group-hover\\:owl > :not([hidden]) + :not([hidden])'} | ${'.group-hover\\:owl > *'} 24 | ${'.peer:first-child ~ .peer-first\\:shadow-md'} | ${'.peer-first\\:shadow-md'} 25 | ${'.whats ~ .next > span:hover'} | ${'span'} 26 | ${'.foo .bar ~ .baz > .next > span > article:hover'} | ${'article'} 27 | `('should generate "$after" from "$before"', ({ before, after }) => { 28 | expect(elementSelectorParser.transformSync(before).join(', ')).toEqual(after) 29 | }) 30 | -------------------------------------------------------------------------------- /tests/modify-selectors.test.css: -------------------------------------------------------------------------------- 1 | .markdown > p { 2 | margin-top: 12px; 3 | } 4 | .font-bold { 5 | font-weight: 700; 6 | } 7 | .foo .foo\:markdown > p { 8 | margin-top: 12px; 9 | } 10 | .foo .foo\:font-bold { 11 | font-weight: 700; 12 | } 13 | .foo .foo\:visited\:markdown:visited > p { 14 | margin-top: 12px; 15 | } 16 | .foo .foo\:hover\:font-bold:hover { 17 | font-weight: 700; 18 | } 19 | @media (min-width: 640px) { 20 | .foo .sm\:foo\:font-bold { 21 | font-weight: 700; 22 | } 23 | } 24 | @media (min-width: 768px) { 25 | .foo .md\:foo\:focus\:font-bold:focus { 26 | font-weight: 700; 27 | } 28 | } 29 | @media (min-width: 1024px) { 30 | .foo .lg\:foo\:disabled\:markdown:disabled > p { 31 | margin-top: 12px; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/modify-selectors.test.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |

Lorem ipsum dolor sit amet...

8 |
9 |
10 |

Lorem ipsum dolor sit amet...

11 |
12 |
13 |

Lorem ipsum dolor sit amet...

14 |
15 |
16 |

Lorem ipsum dolor sit amet...

17 |
18 | -------------------------------------------------------------------------------- /tests/modify-selectors.test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import selectorParser from 'postcss-selector-parser' 4 | 5 | import { run, css } from './util/run' 6 | 7 | test('modify selectors', () => { 8 | let config = { 9 | darkMode: 'class', 10 | content: [path.resolve(__dirname, './modify-selectors.test.html')], 11 | corePlugins: { preflight: false }, 12 | theme: {}, 13 | plugins: [ 14 | function ({ addVariant }) { 15 | addVariant('foo', ({ modifySelectors, separator }) => { 16 | modifySelectors(({ selector }) => { 17 | return selectorParser((selectors) => { 18 | selectors.walkClasses((classNode) => { 19 | classNode.value = `foo${separator}${classNode.value}` 20 | classNode.parent.insertBefore(classNode, selectorParser().astSync(`.foo `)) 21 | }) 22 | }).processSync(selector) 23 | }) 24 | }) 25 | }, 26 | ], 27 | } 28 | 29 | let input = css` 30 | @tailwind components; 31 | @tailwind utilities; 32 | 33 | @layer components { 34 | .markdown > p { 35 | margin-top: 12px; 36 | } 37 | } 38 | ` 39 | 40 | return run(input, config).then((result) => { 41 | let expectedPath = path.resolve(__dirname, './modify-selectors.test.css') 42 | let expected = fs.readFileSync(expectedPath, 'utf8') 43 | 44 | expect(result.css).toMatchFormattedCss(expected) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /tests/mutable.test.css: -------------------------------------------------------------------------------- 1 | .bg-foo { 2 | background-image: url("./foo.png"); 3 | } 4 | -------------------------------------------------------------------------------- /tests/mutable.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Title 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/mutable.test.js: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss' 2 | import fs from 'fs' 3 | import path from 'path' 4 | 5 | import { run } from './util/run' 6 | 7 | function pluginThatMutatesRules() { 8 | return (root) => { 9 | root.walkRules((rule) => { 10 | rule.nodes 11 | .filter((node) => node.prop === 'background-image') 12 | .forEach((node) => { 13 | node.value = 'url("./bar.png")' 14 | }) 15 | 16 | return rule 17 | }) 18 | } 19 | } 20 | 21 | test.only('plugins mutating rules after tailwind doesnt break it', async () => { 22 | let config = { 23 | content: [path.resolve(__dirname, './mutable.test.html')], 24 | theme: { 25 | backgroundImage: { 26 | foo: 'url("./foo.png")', 27 | }, 28 | }, 29 | plugins: [], 30 | } 31 | 32 | function checkResult(result) { 33 | let expectedPath = path.resolve(__dirname, './mutable.test.css') 34 | let expected = fs.readFileSync(expectedPath, 'utf8') 35 | 36 | expect(result.css).toMatchFormattedCss(expected) 37 | } 38 | 39 | // Verify the first run produces the expected result 40 | let firstRun = await run('@tailwind utilities', config) 41 | checkResult(firstRun) 42 | 43 | // Outside of the context of tailwind jit more postcss plugins may operate on the AST: 44 | // In this case we have a plugin that mutates rules directly 45 | await postcss([pluginThatMutatesRules()]).process(firstRun, { 46 | from: path.resolve(__filename), 47 | }) 48 | 49 | // Verify subsequent runs don't produce mutated rules 50 | let secondRun = await run('@tailwind utilities', config) 51 | checkResult(secondRun) 52 | }) 53 | -------------------------------------------------------------------------------- /tests/negateValue.test.js: -------------------------------------------------------------------------------- 1 | import negateValue from '../src/util/negateValue' 2 | 3 | test('it negates numeric CSS values', () => { 4 | expect(negateValue('5')).toEqual('-5') 5 | expect(negateValue('10px')).toEqual('-10px') 6 | expect(negateValue('18rem')).toEqual('-18rem') 7 | expect(negateValue('-10')).toEqual('10') 8 | expect(negateValue('-7ch')).toEqual('7ch') 9 | }) 10 | 11 | test('values that cannot be negated become undefined', () => { 12 | expect(negateValue('auto')).toBeUndefined() 13 | expect(negateValue('cover')).toBeUndefined() 14 | }) 15 | -------------------------------------------------------------------------------- /tests/negated-content-ignore.test.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /tests/negated-content-include.test.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /tests/negated-content.test.js: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import { run, css, defaults } from './util/run' 3 | 4 | it('should be possible to use negated content patterns', () => { 5 | let config = { 6 | content: [ 7 | path.resolve(__dirname, './negated-content-*.test.html'), 8 | '!' + path.resolve(__dirname, './negated-content-ignore.test.html'), 9 | ], 10 | corePlugins: { preflight: false }, 11 | } 12 | 13 | let input = css` 14 | @tailwind base; 15 | @tailwind components; 16 | @tailwind utilities; 17 | ` 18 | 19 | return run(input, config).then((result) => { 20 | expect(result.css).toMatchFormattedCss(css` 21 | ${defaults} 22 | .uppercase { 23 | text-transform: uppercase; 24 | } 25 | `) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /tests/normalize-data-types.test.js: -------------------------------------------------------------------------------- 1 | import { normalize } from '../src/util/dataTypes' 2 | 3 | let table = [ 4 | ['foo', 'foo'], 5 | ['foo-bar', 'foo-bar'], 6 | ['16/9', '16 / 9'], 7 | 8 | // '_'s are converted to spaces except when escaped 9 | ['foo_bar', 'foo bar'], 10 | ['foo__bar', 'foo bar'], 11 | ['foo\\_bar', 'foo_bar'], 12 | 13 | // Urls are preserved as-is 14 | [ 15 | 'url("https://example.com/abc+def/some-path/2022-01-01-abc/some_underscoered_path")', 16 | 'url("https://example.com/abc+def/some-path/2022-01-01-abc/some_underscoered_path")', 17 | ], 18 | 19 | // var(…) is preserved as is 20 | ['var(--foo)', 'var(--foo)'], 21 | ['var(--headings-h1-size)', 'var(--headings-h1-size)'], 22 | 23 | // calc(…) get's spaces around operators 24 | ['calc(1+2)', 'calc(1 + 2)'], 25 | ['calc(100%+1rem)', 'calc(100% + 1rem)'], 26 | ['calc(1+calc(100%-20px))', 'calc(1 + calc(100% - 20px))'], 27 | ['calc(var(--headings-h1-size)*100)', 'calc(var(--headings-h1-size) * 100)'], 28 | [ 29 | 'calc(var(--headings-h1-size)*calc(100%+50%))', 30 | 'calc(var(--headings-h1-size) * calc(100% + 50%))', 31 | ], 32 | ['var(--heading-h1-font-size)', 'var(--heading-h1-font-size)'], 33 | ['var(--my-var-with-more-than-3-words)', 'var(--my-var-with-more-than-3-words)'], 34 | ['var(--width, calc(100%+1rem))', 'var(--width, calc(100% + 1rem))'], 35 | 36 | // Misc 37 | ['color(0_0_0/1.0)', 'color(0 0 0 / 1.0)'], 38 | ] 39 | 40 | it.each(table)('normalize data: %s', (input, output) => { 41 | expect(normalize(input)).toBe(output) 42 | }) 43 | -------------------------------------------------------------------------------- /tests/normalize-screens.test.js: -------------------------------------------------------------------------------- 1 | import { normalizeScreens } from '../src/util/normalizeScreens' 2 | 3 | it('should normalize an array of string values', () => { 4 | let screens = ['768px', '1200px'] 5 | 6 | expect(normalizeScreens(screens)).toEqual([ 7 | { name: '768px', values: [{ min: '768px', max: undefined }] }, 8 | { name: '1200px', values: [{ min: '1200px', max: undefined }] }, 9 | ]) 10 | }) 11 | 12 | it('should normalize an object with string values', () => { 13 | let screens = { 14 | a: '768px', 15 | b: '1200px', 16 | } 17 | 18 | expect(normalizeScreens(screens)).toEqual([ 19 | { name: 'a', values: [{ min: '768px', max: undefined }] }, 20 | { name: 'b', values: [{ min: '1200px', max: undefined }] }, 21 | ]) 22 | }) 23 | 24 | it('should normalize an object with object values', () => { 25 | let screens = { 26 | a: { min: '768px' }, 27 | b: { max: '1200px' }, 28 | } 29 | 30 | expect(normalizeScreens(screens)).toEqual([ 31 | { name: 'a', values: [{ min: '768px', max: undefined }] }, 32 | { name: 'b', values: [{ min: undefined, max: '1200px' }] }, 33 | ]) 34 | }) 35 | 36 | it('should normalize an object with multiple object values', () => { 37 | let screens = { 38 | a: [{ min: '768px' }, { max: '1200px' }], 39 | } 40 | 41 | expect(normalizeScreens(screens)).toEqual([ 42 | { 43 | name: 'a', 44 | values: [ 45 | { max: undefined, min: '768px', raw: undefined }, 46 | { max: '1200px', min: undefined, raw: undefined }, 47 | ], 48 | }, 49 | ]) 50 | }) 51 | 52 | it('should normalize an object with object values (min-width normalized to width)', () => { 53 | let screens = { 54 | a: { 'min-width': '768px' }, 55 | b: { max: '1200px' }, 56 | } 57 | 58 | expect(normalizeScreens(screens)).toEqual([ 59 | { name: 'a', values: [{ min: '768px', max: undefined }] }, 60 | { name: 'b', values: [{ min: undefined, max: '1200px' }] }, 61 | ]) 62 | }) 63 | -------------------------------------------------------------------------------- /tests/parallel-variants.test.js: -------------------------------------------------------------------------------- 1 | import { run, html, css } from './util/run' 2 | 3 | test('basic parallel variants', async () => { 4 | let config = { 5 | content: [ 6 | { 7 | raw: html`
`, 10 | }, 11 | ], 12 | plugins: [ 13 | function test({ addVariant }) { 14 | addVariant('test', ['& *::test', '&::test']) 15 | }, 16 | ], 17 | } 18 | 19 | return run('@tailwind utilities', config).then((result) => { 20 | expect(result.css).toMatchFormattedCss(css` 21 | .font-normal { 22 | font-weight: 400; 23 | } 24 | .test\:font-bold *::test { 25 | font-weight: 700; 26 | } 27 | .test\:font-medium *::test { 28 | font-weight: 500; 29 | } 30 | .hover\:test\:font-black *:hover::test { 31 | font-weight: 900; 32 | } 33 | .test\:font-bold::test { 34 | font-weight: 700; 35 | } 36 | .test\:font-medium::test { 37 | font-weight: 500; 38 | } 39 | .hover\:test\:font-black:hover::test { 40 | font-weight: 900; 41 | } 42 | `) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /tests/plugins/fontSize.test.js: -------------------------------------------------------------------------------- 1 | import { run, html, css } from '../util/run' 2 | 3 | test('font-size utilities can include a default line-height', () => { 4 | let config = { 5 | content: [{ raw: html`
` }], 6 | theme: { 7 | fontSize: { 8 | sm: '12px', 9 | md: ['16px', '24px'], 10 | lg: ['20px', '28px'], 11 | }, 12 | }, 13 | } 14 | 15 | return run('@tailwind utilities', config).then((result) => { 16 | expect(result.css).toMatchCss(css` 17 | .text-md { 18 | font-size: 16px; 19 | line-height: 24px; 20 | } 21 | .text-sm { 22 | font-size: 12px; 23 | } 24 | .text-lg { 25 | font-size: 20px; 26 | line-height: 28px; 27 | } 28 | `) 29 | }) 30 | }) 31 | 32 | test('font-size utilities can include a default letter-spacing', () => { 33 | let config = { 34 | content: [{ raw: html`
` }], 35 | theme: { 36 | fontSize: { 37 | sm: '12px', 38 | md: ['16px', { letterSpacing: '-0.01em' }], 39 | lg: ['20px', { letterSpacing: '-0.02em' }], 40 | }, 41 | }, 42 | } 43 | 44 | return run('@tailwind utilities', config).then((result) => { 45 | expect(result.css).toMatchCss(css` 46 | .text-md { 47 | font-size: 16px; 48 | letter-spacing: -0.01em; 49 | } 50 | .text-sm { 51 | font-size: 12px; 52 | } 53 | .text-lg { 54 | font-size: 20px; 55 | letter-spacing: -0.02em; 56 | } 57 | `) 58 | }) 59 | }) 60 | 61 | test('font-size utilities can include a default line-height and letter-spacing', () => { 62 | let config = { 63 | content: [{ raw: html`
` }], 64 | theme: { 65 | fontSize: { 66 | sm: '12px', 67 | md: ['16px', { lineHeight: '24px', letterSpacing: '-0.01em' }], 68 | lg: ['20px', { lineHeight: '28px', letterSpacing: '-0.02em' }], 69 | }, 70 | }, 71 | } 72 | 73 | return run('@tailwind utilities', config).then((result) => { 74 | expect(result.css).toMatchCss(css` 75 | .text-md { 76 | font-size: 16px; 77 | line-height: 24px; 78 | letter-spacing: -0.01em; 79 | } 80 | .text-sm { 81 | font-size: 12px; 82 | } 83 | .text-lg { 84 | font-size: 20px; 85 | line-height: 28px; 86 | letter-spacing: -0.02em; 87 | } 88 | `) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /tests/plugins/gradientColorStops.test.js: -------------------------------------------------------------------------------- 1 | import { run, html, css } from '../util/run' 2 | 3 | test('opacity variables are given to colors defined as closures', () => { 4 | let config = { 5 | content: [ 6 | { 7 | raw: html`
`, 10 | }, 11 | ], 12 | theme: { 13 | colors: { 14 | primary: ({ opacityVariable, opacityValue }) => { 15 | if (opacityValue !== undefined) { 16 | return `rgba(31,31,31,${opacityValue})` 17 | } 18 | 19 | if (opacityVariable !== undefined) { 20 | return `rgba(31,31,31,var(${opacityVariable},1))` 21 | } 22 | 23 | return `rgb(31,31,31)` 24 | }, 25 | secondary: 'hsl(10, 50%, 50%)', 26 | }, 27 | opacity: { 28 | 50: '0.5', 29 | }, 30 | }, 31 | } 32 | 33 | return run('@tailwind utilities', config).then((result) => { 34 | expect(result.css).toMatchFormattedCss(css` 35 | .from-primary { 36 | --tw-gradient-from: rgb(31, 31, 31); 37 | --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(31, 31, 31, 0)); 38 | } 39 | .from-secondary { 40 | --tw-gradient-from: hsl(10, 50%, 50%); 41 | --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, hsl(10 50% 50% / 0)); 42 | } 43 | .via-primary { 44 | --tw-gradient-stops: var(--tw-gradient-from), rgb(31, 31, 31), 45 | var(--tw-gradient-to, rgba(31, 31, 31, 0)); 46 | } 47 | .via-secondary { 48 | --tw-gradient-stops: var(--tw-gradient-from), hsl(10, 50%, 50%), 49 | var(--tw-gradient-to, hsl(10 50% 50% / 0)); 50 | } 51 | .to-primary { 52 | --tw-gradient-to: rgb(31, 31, 31); 53 | } 54 | .to-secondary { 55 | --tw-gradient-to: hsl(10, 50%, 50%); 56 | } 57 | .text-primary { 58 | --tw-text-opacity: 1; 59 | color: rgba(31, 31, 31, var(--tw-text-opacity)); 60 | } 61 | .text-secondary { 62 | --tw-text-opacity: 1; 63 | color: hsl(10 50% 50% / var(--tw-text-opacity)); 64 | } 65 | .text-opacity-50 { 66 | --tw-text-opacity: 0.5; 67 | } 68 | `) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /tests/postcss-plugins/nesting/plugins.js: -------------------------------------------------------------------------------- 1 | export function visitorSpyPlugin() { 2 | let Once = jest.fn() 3 | let OnceExit = jest.fn() 4 | let Root = jest.fn() 5 | let AtRule = jest.fn() 6 | let Rule = jest.fn() 7 | let Comment = jest.fn() 8 | let Declaration = jest.fn() 9 | 10 | let plugin = Object.assign( 11 | function () { 12 | return { 13 | postcssPlugin: 'visitor-test', 14 | 15 | // These work fine 16 | Once, 17 | OnceExit, 18 | 19 | // These break 20 | Root, 21 | Rule, 22 | AtRule, 23 | Declaration, 24 | Comment, 25 | } 26 | }, 27 | { postcss: true } 28 | ) 29 | 30 | return { 31 | plugin, 32 | spies: { 33 | Once, 34 | OnceExit, 35 | Root, 36 | AtRule, 37 | Rule, 38 | Comment, 39 | Declaration, 40 | }, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/prefix.test.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /tests/prefixSelector.test.js: -------------------------------------------------------------------------------- 1 | import prefix from '../src/util/prefixSelector' 2 | 3 | test('it prefixes classes with the provided prefix', () => { 4 | expect(prefix('tw-', '.foo')).toEqual('.tw-foo') 5 | }) 6 | 7 | test('it properly prefixes selectors with non-standard characters', () => { 8 | expect(prefix('tw-', '.hello\\:world')).toEqual('.tw-hello\\:world') 9 | expect(prefix('tw-', '.foo\\/bar')).toEqual('.tw-foo\\/bar') 10 | expect(prefix('tw-', '.wew\\.lad')).toEqual('.tw-wew\\.lad') 11 | }) 12 | 13 | test('it prefixes all classes in a selector', () => { 14 | expect(prefix('tw-', '.btn-blue .w-1\\/4 > h1.text-xl + a .bar')).toEqual( 15 | '.tw-btn-blue .tw-w-1\\/4 > h1.tw-text-xl + a .tw-bar' 16 | ) 17 | }) 18 | -------------------------------------------------------------------------------- /tests/preflight.test.js: -------------------------------------------------------------------------------- 1 | import { run, html, css } from './util/run' 2 | 3 | it('preflight has a correct border color fallback', () => { 4 | let config = { 5 | content: [{ raw: html`
` }], 6 | theme: { 7 | borderColor: ({ theme }) => theme('colors'), 8 | }, 9 | plugins: [], 10 | corePlugins: { preflight: true }, 11 | } 12 | 13 | let input = css` 14 | @tailwind base; 15 | @tailwind utilities; 16 | ` 17 | 18 | return run(input, config).then((result) => { 19 | expect(result.css).toContain(`border-color: currentColor;`) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /tests/raw-content.test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | import { run, css } from './util/run' 5 | 6 | it('raw content', () => { 7 | let config = { 8 | content: [{ raw: fs.readFileSync(path.resolve(__dirname, './raw-content.test.html'), 'utf8') }], 9 | corePlugins: { preflight: false }, 10 | } 11 | 12 | let input = css` 13 | @tailwind components; 14 | @tailwind utilities; 15 | ` 16 | 17 | return run(input, config).then((result) => { 18 | let expectedPath = path.resolve(__dirname, './raw-content.test.css') 19 | let expected = fs.readFileSync(expectedPath, 'utf8') 20 | 21 | expect(result.css).toMatchFormattedCss(expected) 22 | }) 23 | }) 24 | 25 | test('raw content with extension', () => { 26 | let config = { 27 | content: { 28 | files: [ 29 | { 30 | raw: fs.readFileSync(path.resolve(__dirname, './raw-content.test.html'), 'utf8'), 31 | extension: 'html', 32 | }, 33 | ], 34 | extract: { 35 | html: () => ['invisible'], 36 | }, 37 | }, 38 | corePlugins: { preflight: false }, 39 | } 40 | 41 | return run('@tailwind utilities', config).then((result) => { 42 | expect(result.css).toMatchFormattedCss(css` 43 | .invisible { 44 | visibility: hidden; 45 | } 46 | `) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /tests/relative-purge-paths.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Title 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/relative-purge-paths.test.js: -------------------------------------------------------------------------------- 1 | import { run, css } from './util/run' 2 | 3 | test('relative purge paths', () => { 4 | let config = { 5 | content: ['./tests/relative-purge-paths.test.html'], 6 | corePlugins: { preflight: false }, 7 | } 8 | 9 | let input = css` 10 | @tailwind base; 11 | @tailwind components; 12 | @tailwind utilities; 13 | ` 14 | 15 | return run(input, config).then((result) => { 16 | expect(result.css).toIncludeCss(css` 17 | .font-bold { 18 | font-weight: 700; 19 | } 20 | `) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /tests/responsive-and-variants-atrules.test.css: -------------------------------------------------------------------------------- 1 | .responsive-in-components { 2 | color: blue; 3 | } 4 | .variants-in-components { 5 | color: red; 6 | } 7 | .both-in-components { 8 | color: green; 9 | } 10 | .responsive-in-utilities { 11 | color: blue; 12 | } 13 | .variants-in-utilities { 14 | color: red; 15 | } 16 | .both-in-utilities { 17 | color: green; 18 | } 19 | .responsive-at-root { 20 | color: white; 21 | } 22 | .variants-at-root { 23 | color: orange; 24 | } 25 | .both-at-root { 26 | color: pink; 27 | } 28 | @media (min-width: 768px) { 29 | .md\:focus\:responsive-in-components:focus { 30 | color: blue; 31 | } 32 | .md\:focus\:variants-in-components:focus { 33 | color: red; 34 | } 35 | .md\:focus\:both-in-components:focus { 36 | color: green; 37 | } 38 | .md\:focus\:responsive-in-utilities:focus { 39 | color: blue; 40 | } 41 | .md\:focus\:variants-in-utilities:focus { 42 | color: red; 43 | } 44 | .md\:focus\:both-in-utilities:focus { 45 | color: green; 46 | } 47 | .md\:focus\:responsive-at-root:focus { 48 | color: white; 49 | } 50 | .md\:focus\:variants-at-root:focus { 51 | color: orange; 52 | } 53 | .md\:focus\:both-at-root:focus { 54 | color: pink; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/responsive-and-variants-atrules.test.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /tests/responsive-and-variants-atrules.test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | import { run, css } from './util/run' 5 | 6 | test('responsive and variants atrules', () => { 7 | let config = { 8 | content: [path.resolve(__dirname, './responsive-and-variants-atrules.test.html')], 9 | corePlugins: { preflight: false }, 10 | } 11 | 12 | let input = css` 13 | @tailwind components; 14 | @tailwind utilities; 15 | 16 | @layer utilities { 17 | @responsive { 18 | .responsive-in-utilities { 19 | color: blue; 20 | } 21 | } 22 | @variants { 23 | .variants-in-utilities { 24 | color: red; 25 | } 26 | } 27 | @responsive { 28 | @variants { 29 | .both-in-utilities { 30 | color: green; 31 | } 32 | } 33 | } 34 | } 35 | 36 | @responsive { 37 | .responsive-at-root { 38 | color: white; 39 | } 40 | } 41 | @variants { 42 | .variants-at-root { 43 | color: orange; 44 | } 45 | } 46 | @responsive { 47 | @variants { 48 | .both-at-root { 49 | color: pink; 50 | } 51 | } 52 | } 53 | 54 | @layer components { 55 | @responsive { 56 | .responsive-in-components { 57 | color: blue; 58 | } 59 | } 60 | @variants { 61 | .variants-in-components { 62 | color: red; 63 | } 64 | } 65 | @responsive { 66 | @variants { 67 | .both-in-components { 68 | color: green; 69 | } 70 | } 71 | } 72 | } 73 | ` 74 | 75 | return run(input, config).then((result) => { 76 | let expectedPath = path.resolve(__dirname, './responsive-and-variants-atrules.test.css') 77 | let expected = fs.readFileSync(expectedPath, 'utf8') 78 | 79 | expect(result.css).toMatchFormattedCss(expected) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /tests/screenAtRule.test.js: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss' 2 | import plugin from '../src/lib/substituteScreenAtRules' 3 | import config from '../stubs/defaultConfig.stub.js' 4 | 5 | function run(input, opts = config) { 6 | return postcss([plugin({ tailwindConfig: opts })]).process(input, { from: undefined }) 7 | } 8 | 9 | test('it can generate media queries from configured screen sizes', () => { 10 | const input = ` 11 | @screen sm { 12 | .banana { color: yellow; } 13 | } 14 | @screen md { 15 | .banana { color: red; } 16 | } 17 | @screen lg { 18 | .banana { color: green; } 19 | } 20 | ` 21 | 22 | const output = ` 23 | @media (min-width: 500px) { 24 | .banana { color: yellow; } 25 | } 26 | @media (min-width: 750px) { 27 | .banana { color: red; } 28 | } 29 | @media (min-width: 1000px) { 30 | .banana { color: green; } 31 | } 32 | ` 33 | 34 | return run(input, { 35 | theme: { 36 | screens: { 37 | sm: '500px', 38 | md: '750px', 39 | lg: '1000px', 40 | }, 41 | }, 42 | separator: ':', 43 | }).then((result) => { 44 | expect(result.css).toMatchCss(output) 45 | expect(result.warnings().length).toBe(0) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /tests/shared-state.test.js: -------------------------------------------------------------------------------- 1 | import { resolveDebug } from '../src/lib/sharedState' 2 | 3 | it.each` 4 | value | expected 5 | ${'true'} | ${true} 6 | ${'1'} | ${true} 7 | ${'false'} | ${false} 8 | ${'0'} | ${false} 9 | ${'*'} | ${true} 10 | ${'tailwindcss'} | ${true} 11 | ${'tailwindcss:*'} | ${true} 12 | ${'other,tailwindcss'} | ${true} 13 | ${'other,tailwindcss:*'} | ${true} 14 | ${'other,-tailwindcss'} | ${false} 15 | ${'other,-tailwindcss:*'} | ${false} 16 | ${'-tailwindcss'} | ${false} 17 | ${'-tailwindcss:*'} | ${false} 18 | `('should resolve the debug ($value) flag correctly ($expected)', ({ value, expected }) => { 19 | expect(resolveDebug(value)).toBe(expected) 20 | }) 21 | -------------------------------------------------------------------------------- /tests/syntax-lit-html.test.js: -------------------------------------------------------------------------------- 1 | import { run } from './util/run' 2 | 3 | let css = String.raw 4 | 5 | test('it detects classes in lit-html templates', () => { 6 | let config = { 7 | content: [ 8 | { 9 | raw: `html\`\`;`, 10 | }, 11 | ], 12 | corePlugins: { preflight: false }, 13 | theme: {}, 14 | plugins: [], 15 | } 16 | 17 | return run('@tailwind utilities', config).then((result) => { 18 | expect(result.css).toMatchCss(css` 19 | .rounded { 20 | border-radius: 0.25rem; 21 | } 22 | .bg-blue-400 { 23 | --tw-bg-opacity: 1; 24 | background-color: rgb(96 165 250 / var(--tw-bg-opacity)); 25 | } 26 | .py-2 { 27 | padding-top: 0.5rem; 28 | padding-bottom: 0.5rem; 29 | } 30 | .px-4 { 31 | padding-left: 1rem; 32 | padding-right: 1rem; 33 | } 34 | .font-bold { 35 | font-weight: 700; 36 | } 37 | .text-white { 38 | --tw-text-opacity: 1; 39 | color: rgb(255 255 255 / var(--tw-text-opacity)); 40 | } 41 | .hover\:bg-blue-600:hover { 42 | --tw-bg-opacity: 1; 43 | background-color: rgb(37 99 235 / var(--tw-bg-opacity)); 44 | } 45 | `) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /tests/syntax-svelte.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import { run } from './util/run' 4 | 5 | let css = String.raw 6 | 7 | test('it detects svelte based on the file extension', () => { 8 | let config = { 9 | content: [path.resolve(__dirname, './syntax-svelte.test.svelte')], 10 | corePlugins: { preflight: false }, 11 | theme: {}, 12 | plugins: [], 13 | } 14 | 15 | let input = css` 16 | @tailwind components; 17 | @tailwind utilities; 18 | ` 19 | 20 | return run(input, config).then((result) => { 21 | expect(result.css).toMatchCss(css` 22 | .bg-red-500 { 23 | --tw-bg-opacity: 1; 24 | background-color: rgb(239 68 68 / var(--tw-bg-opacity)); 25 | } 26 | @media (min-width: 1024px) { 27 | .lg\:hover\:bg-blue-500:hover { 28 | --tw-bg-opacity: 1; 29 | background-color: rgb(59 130 246 / var(--tw-bg-opacity)); 30 | } 31 | } 32 | `) 33 | }) 34 | }) 35 | 36 | test('using raw with svelte extension', () => { 37 | let config = { 38 | content: [ 39 | { 40 | raw: ` 41 | 44 | 45 | 46 | 47 | 52 | `, 53 | extension: 'svelte', 54 | }, 55 | ], 56 | corePlugins: { preflight: false }, 57 | theme: {}, 58 | plugins: [], 59 | } 60 | 61 | let input = css` 62 | @tailwind components; 63 | @tailwind utilities; 64 | ` 65 | 66 | return run(input, config).then((result) => { 67 | expect(result.css).toMatchCss(css` 68 | .bg-red-500 { 69 | --tw-bg-opacity: 1; 70 | background-color: rgb(239 68 68 / var(--tw-bg-opacity)); 71 | } 72 | @media (min-width: 1024px) { 73 | .lg\:hover\:bg-blue-500:hover { 74 | --tw-bg-opacity: 1; 75 | background-color: rgb(59 130 246 / var(--tw-bg-opacity)); 76 | } 77 | } 78 | `) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /tests/syntax-svelte.test.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /tests/tailwind-screens.test.js: -------------------------------------------------------------------------------- 1 | import { run, html, css } from './util/run' 2 | 3 | test('class variants are inserted at `@tailwind variants`', async () => { 4 | let config = { 5 | content: [{ raw: html`
` }], 6 | } 7 | 8 | let input = css` 9 | @tailwind utilities; 10 | @tailwind variants; 11 | .foo { 12 | color: black; 13 | } 14 | ` 15 | 16 | return run(input, config).then((result) => { 17 | expect(result.css).toMatchFormattedCss(css` 18 | .font-bold { 19 | font-weight: 700; 20 | } 21 | .hover\:font-bold:hover { 22 | font-weight: 700; 23 | } 24 | @media (min-width: 768px) { 25 | .md\:font-bold { 26 | font-weight: 700; 27 | } 28 | } 29 | .foo { 30 | color: black; 31 | } 32 | `) 33 | }) 34 | }) 35 | 36 | test('`@tailwind screens` works as an alias for `@tailwind variants`', async () => { 37 | let config = { 38 | content: [{ raw: html`
` }], 39 | } 40 | 41 | let input = css` 42 | @tailwind utilities; 43 | @tailwind screens; 44 | .foo { 45 | color: black; 46 | } 47 | ` 48 | 49 | return run(input, config).then((result) => { 50 | expect(result.css).toMatchFormattedCss(css` 51 | .font-bold { 52 | font-weight: 700; 53 | } 54 | .hover\:font-bold:hover { 55 | font-weight: 700; 56 | } 57 | @media (min-width: 768px) { 58 | .md\:font-bold { 59 | font-weight: 700; 60 | } 61 | } 62 | .foo { 63 | color: black; 64 | } 65 | `) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /tests/to-path.test.js: -------------------------------------------------------------------------------- 1 | import { toPath } from '../src/util/toPath' 2 | 3 | it('should keep an array as an array', () => { 4 | let input = ['a', 'b', '0', 'c'] 5 | 6 | expect(toPath(input)).toBe(input) 7 | }) 8 | 9 | it.each` 10 | input | output 11 | ${'a.b.c'} | ${['a', 'b', 'c']} 12 | ${'a[0].b.c'} | ${['a', '0', 'b', 'c']} 13 | ${'.a'} | ${['a']} 14 | ${'[].a'} | ${['a']} 15 | ${'a[1.5][b][c]'} | ${['a', '1.5', 'b', 'c']} 16 | `('should convert "$input" to "$output"', ({ input, output }) => { 17 | expect(toPath(input)).toEqual(output) 18 | }) 19 | -------------------------------------------------------------------------------- /tests/util/defaults.js: -------------------------------------------------------------------------------- 1 | import { css } from './strings' 2 | 3 | /** 4 | * @param {object} param0 5 | * @param {string} [param0.defaultRingColor] 6 | * @returns {string} 7 | */ 8 | export function defaults({ defaultRingColor = `rgb(59 130 246 / 0.5)` } = {}) { 9 | return css` 10 | *, 11 | ::before, 12 | ::after { 13 | --tw-border-spacing-x: 0; 14 | --tw-border-spacing-y: 0; 15 | --tw-translate-x: 0; 16 | --tw-translate-y: 0; 17 | --tw-rotate: 0; 18 | --tw-skew-x: 0; 19 | --tw-skew-y: 0; 20 | --tw-scale-x: 1; 21 | --tw-scale-y: 1; 22 | --tw-pan-x: ; 23 | --tw-pan-y: ; 24 | --tw-pinch-zoom: ; 25 | --tw-scroll-snap-strictness: proximity; 26 | --tw-ordinal: ; 27 | --tw-slashed-zero: ; 28 | --tw-numeric-figure: ; 29 | --tw-numeric-spacing: ; 30 | --tw-numeric-fraction: ; 31 | --tw-ring-inset: ; 32 | --tw-ring-offset-width: 0px; 33 | --tw-ring-offset-color: #fff; 34 | --tw-ring-color: ${defaultRingColor}; 35 | --tw-ring-offset-shadow: 0 0 #0000; 36 | --tw-ring-shadow: 0 0 #0000; 37 | --tw-shadow: 0 0 #0000; 38 | --tw-shadow-colored: 0 0 #0000; 39 | --tw-blur: ; 40 | --tw-brightness: ; 41 | --tw-contrast: ; 42 | --tw-grayscale: ; 43 | --tw-hue-rotate: ; 44 | --tw-invert: ; 45 | --tw-saturate: ; 46 | --tw-sepia: ; 47 | --tw-drop-shadow: ; 48 | --tw-backdrop-blur: ; 49 | --tw-backdrop-brightness: ; 50 | --tw-backdrop-contrast: ; 51 | --tw-backdrop-grayscale: ; 52 | --tw-backdrop-hue-rotate: ; 53 | --tw-backdrop-invert: ; 54 | --tw-backdrop-opacity: ; 55 | --tw-backdrop-saturate: ; 56 | --tw-backdrop-sepia: ; 57 | } 58 | ` 59 | } 60 | 61 | defaults.toString = () => defaults() 62 | -------------------------------------------------------------------------------- /tests/util/run.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import postcss from 'postcss' 3 | import tailwind from '../../src' 4 | 5 | export * from './strings' 6 | export * from './defaults' 7 | 8 | let map = JSON.stringify({ 9 | version: 3, 10 | file: null, 11 | sources: [], 12 | names: [], 13 | mappings: '', 14 | }) 15 | 16 | export function run(input, config, plugin = tailwind) { 17 | let { currentTestName } = expect.getState() 18 | 19 | return postcss(plugin(config)).process(input, { 20 | from: `${path.resolve(__filename)}?test=${currentTestName}`, 21 | }) 22 | } 23 | 24 | export function runWithSourceMaps(input, config, plugin = tailwind) { 25 | let { currentTestName } = expect.getState() 26 | 27 | return postcss(plugin(config)).process(input, { 28 | from: `${path.resolve(__filename)}?test=${currentTestName}`, 29 | map: { 30 | prev: map, 31 | }, 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /tests/util/source-maps.js: -------------------------------------------------------------------------------- 1 | import { SourceMapConsumer } from 'source-map-js' 2 | 3 | /** 4 | * Parse the source maps from a PostCSS result 5 | * 6 | * @param {import('postcss').Result} result 7 | */ 8 | export function parseSourceMaps(result) { 9 | const map = result.map.toJSON() 10 | 11 | return { 12 | sources: map.sources, 13 | annotations: annotatedMappings(map), 14 | } 15 | } 16 | 17 | /** 18 | * An string annotation that represents a source map 19 | * 20 | * It's not meant to be exhaustive just enough to 21 | * verify that the source map is working and that 22 | * lines are mapped back to the original source 23 | * 24 | * Including when using @apply with multiple classes 25 | * 26 | * @param {import('source-map-js').RawSourceMap} map 27 | */ 28 | function annotatedMappings(map) { 29 | const smc = new SourceMapConsumer(map) 30 | const annotations = {} 31 | 32 | smc.eachMapping((mapping) => { 33 | let annotation = (annotations[mapping.generatedLine] = annotations[mapping.generatedLine] || { 34 | ...mapping, 35 | 36 | original: { 37 | start: [mapping.originalLine, mapping.originalColumn], 38 | end: [mapping.originalLine, mapping.originalColumn], 39 | }, 40 | 41 | generated: { 42 | start: [mapping.generatedLine, mapping.generatedColumn], 43 | end: [mapping.generatedLine, mapping.generatedColumn], 44 | }, 45 | }) 46 | 47 | annotation.generated.end[0] = mapping.generatedLine 48 | annotation.generated.end[1] = mapping.generatedColumn 49 | 50 | annotation.original.end[0] = mapping.originalLine 51 | annotation.original.end[1] = mapping.originalColumn 52 | }) 53 | 54 | return Object.values(annotations).map((annotation) => { 55 | return `${formatRange(annotation.original)} -> ${formatRange(annotation.generated)}` 56 | }) 57 | } 58 | 59 | /** 60 | * @param {object} range 61 | * @param {[number, number]} range.start 62 | * @param {[number, number]} range.end 63 | */ 64 | function formatRange(range) { 65 | if (range.start[0] === range.end[0]) { 66 | // This range is on the same line 67 | // and the columns are the same 68 | if (range.start[1] === range.end[1]) { 69 | return `${range.start[0]}:${range.start[1]}` 70 | } 71 | 72 | // This range is on the same line 73 | // but the columns are different 74 | return `${range.start[0]}:${range.start[1]}-${range.end[1]}` 75 | } 76 | 77 | // This range spans multiple lines 78 | return `${range.start[0]}:${range.start[1]}-${range.end[0]}:${range.end[1]}` 79 | } 80 | -------------------------------------------------------------------------------- /tests/util/strings.js: -------------------------------------------------------------------------------- 1 | export let css = String.raw 2 | export let html = String.raw 3 | export let javascript = String.raw 4 | -------------------------------------------------------------------------------- /tests/warnings.test.js: -------------------------------------------------------------------------------- 1 | import { html, run, css } from './util/run' 2 | 3 | let warn 4 | 5 | beforeEach(() => { 6 | let log = require('../src/util/log') 7 | warn = jest.spyOn(log.default, 'warn') 8 | }) 9 | 10 | afterEach(() => { 11 | warn.mockClear() 12 | }) 13 | 14 | test('it warns when there is no content key', async () => { 15 | let config = { 16 | corePlugins: { preflight: false }, 17 | } 18 | 19 | let input = css` 20 | @tailwind base; 21 | ` 22 | 23 | await run(input, config) 24 | 25 | expect(warn).toHaveBeenCalledTimes(1) 26 | expect(warn.mock.calls.map((x) => x[0])).toEqual(['content-problems']) 27 | }) 28 | 29 | test('it warns when there is an empty content key', async () => { 30 | let config = { 31 | content: [], 32 | corePlugins: { preflight: false }, 33 | } 34 | 35 | let input = css` 36 | @tailwind base; 37 | ` 38 | 39 | await run(input, config) 40 | 41 | expect(warn).toHaveBeenCalledTimes(1) 42 | expect(warn.mock.calls.map((x) => x[0])).toEqual(['content-problems']) 43 | }) 44 | 45 | test('it warns when there are no utilities generated', async () => { 46 | let config = { 47 | content: [{ raw: html`nothing here matching a utility` }], 48 | corePlugins: { preflight: false }, 49 | } 50 | 51 | let input = css` 52 | @tailwind utilities; 53 | ` 54 | 55 | await run(input, config) 56 | 57 | expect(warn).toHaveBeenCalledTimes(1) 58 | expect(warn.mock.calls.map((x) => x[0])).toEqual(['content-problems']) 59 | }) 60 | 61 | it('warnings are not thrown when only variant utilities are generated', async () => { 62 | let config = { 63 | content: [{ raw: html`
` }], 64 | corePlugins: { preflight: false }, 65 | } 66 | 67 | let input = css` 68 | @tailwind utilities; 69 | ` 70 | 71 | await run(input, config) 72 | 73 | expect(warn).toHaveBeenCalledTimes(0) 74 | }) 75 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | export type { Config } from './types/config' 2 | -------------------------------------------------------------------------------- /types/generated/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdo/tailwindcss/128030fcfa1b784e30474f18f88efaa41450fb51/types/generated/.gitkeep -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace tailwindcss {} 2 | -------------------------------------------------------------------------------- /utilities.css: -------------------------------------------------------------------------------- 1 | @tailwind utilities; 2 | -------------------------------------------------------------------------------- /variants.css: -------------------------------------------------------------------------------- 1 | @tailwind variants; 2 | --------------------------------------------------------------------------------