├── .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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | A utility-first CSS framework for rapidly building custom user interfaces.
11 |
12 |
13 |
14 |
15 |
16 |
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 |
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``,
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 |
--------------------------------------------------------------------------------