Compare commits
No commits in common. "main" and "v1.8.0" have entirely different histories.
794 changed files with 546020 additions and 196066 deletions
9
.editorconfig
Normal file
9
.editorconfig
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
4
.eslintignore
Normal file
4
.eslintignore
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
/lib
|
||||
/docs
|
||||
/__tests__/fixtures/cli-utils.js
|
||||
/stubs/*
|
||||
28
.eslintrc.json
Normal file
28
.eslintrc.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"env": {
|
||||
"jest": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"extends": ["eslint-config-postcss", "prettier"],
|
||||
"plugins": ["prettier"],
|
||||
"rules": {
|
||||
"no-unused-vars": [2, { "args": "all", "argsIgnorePattern": "^_" }],
|
||||
"no-warning-comments": 0,
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"parser": "flow"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
|
@ -1 +0,0 @@
|
|||
* @tailwindlabs/engineering
|
||||
10
.github/CODE_OF_CONDUCT.md
vendored
Normal file
10
.github/CODE_OF_CONDUCT.md
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Tailwind CSS Community Guidelines
|
||||
|
||||
The following community guidelines are based on [The Ruby Community Conduct Guidelines](https://www.ruby-lang.org/en/conduct/).
|
||||
|
||||
This document provides community guidelines for a respectful, productive, and collaborative place for any person who is willing to contribute to the Tailwind CSS project. It applies to all “collaborative space”, which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.).
|
||||
|
||||
- Participants will be tolerant of opposing views.
|
||||
- Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
||||
- When interpreting the words and actions of others, participants should always assume good intentions.
|
||||
- Behaviour which can be reasonably considered harassment will not be tolerated.
|
||||
107
.github/CONTRIBUTING.md
vendored
107
.github/CONTRIBUTING.md
vendored
|
|
@ -1,103 +1,64 @@
|
|||
# Contributing
|
||||
|
||||
## Requirements
|
||||
Thanks for your interest in contributing to Tailwind CSS! Please take a moment to review this document **before submitting a pull request**.
|
||||
|
||||
Before getting started, ensure your system has access to the following tools:
|
||||
## Pull requests
|
||||
|
||||
- [Node.js](https://nodejs.org/)
|
||||
- [Rustup](https://rustup.rs/)
|
||||
- [pnpm](https://pnpm.io/)
|
||||
**Please ask first before starting work on any significant new features.**
|
||||
|
||||
## Getting started
|
||||
|
||||
```sh
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Install Rust toolchain and WASM targets
|
||||
rustup default stable
|
||||
rustup target add wasm32-wasip1-threads
|
||||
|
||||
# Build the project
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## Development workflow
|
||||
|
||||
During development, you can run tests in watch mode:
|
||||
|
||||
```sh
|
||||
pnpm tdd
|
||||
```
|
||||
|
||||
The `playgrounds` directory contains example projects you can use to test your changes. To start the Vite playground, use:
|
||||
|
||||
```sh
|
||||
pnpm build && pnpm vite
|
||||
```
|
||||
|
||||
## Bug fixes
|
||||
|
||||
If you've found a bug in Tailwind that you'd like to fix, [submit a pull request](https://github.com/tailwindlabs/tailwindcss/pulls) with your changes. Include a helpful description of the problem and how your changes address it, and provide tests so we can verify the fix works as expected.
|
||||
|
||||
## New features
|
||||
|
||||
If there's a new feature you'd like to see added to Tailwind, [share your idea with us](https://github.com/tailwindlabs/tailwindcss/discussions/new?category=ideas) in our discussion forum to get it on our radar as something to consider for a future release before starting work on it.
|
||||
|
||||
**Please note that we don't often accept pull requests for new features.** Adding a new feature to Tailwind requires us to think through the entire problem ourselves to make sure we agree with the proposed API, which means the feature needs to be high on our own priority list for us to be able to give it the attention it needs.
|
||||
|
||||
If you open a pull request for a new feature, we're likely to close it not because it's a bad idea, but because we aren't ready to prioritize the feature and don't want the PR to sit open for months or even years.
|
||||
It's never a fun experience to have your pull request declined after investing a lot of time and effort into a new feature. To avoid this from happening, we request that contributors create [an issue](https://github.com/tailwindcss/tailwindcss/issues) to first discuss any significant new features. This includes things like adding new utilities, creating new at-rules, etc.
|
||||
|
||||
## Coding standards
|
||||
|
||||
Our code formatting rules are defined in the `"prettier"` section of [package.json](https://github.com/tailwindlabs/tailwindcss/blob/main/package.json). You can check your code against these standards by running:
|
||||
Our code formatting rules are defined in [.eslintrc](https://github.com/tailwindcss/tailwindcss/blob/master/.eslintrc.json). You can check your code against these standards by running:
|
||||
|
||||
```sh
|
||||
pnpm run lint
|
||||
npm run style
|
||||
```
|
||||
|
||||
To automatically fix any style violations in your code, you can run:
|
||||
|
||||
```sh
|
||||
pnpm run format
|
||||
npm run style -- --fix
|
||||
```
|
||||
|
||||
## Running tests
|
||||
|
||||
You can run the TypeScript and Rust test suites using the following command:
|
||||
You can run the test suite using the following commands:
|
||||
|
||||
```sh
|
||||
pnpm test
|
||||
npm test
|
||||
```
|
||||
|
||||
To run the integration tests, use:
|
||||
Please ensure that the tests are passing when submitting a pull request. If you're adding new features to Tailwind, please include tests.
|
||||
|
||||
## Building the CSS and trying your changes manually
|
||||
|
||||
In addition to the automated tests, if you'd like to test the generated CSS manually with your own test HTML file, you can generate the default build by running:
|
||||
|
||||
```sh
|
||||
pnpm build && pnpm test:integrations
|
||||
npm run prepare
|
||||
```
|
||||
|
||||
Additionally, some features require testing in browsers (i.e. to ensure CSS variable resolution works as expected). These can be run via:
|
||||
This will create new CSS files in the `/dist` folder which you can reference in your own test HTML file. We often test our own changes by creating an `index.html` file in the root of the Tailwind project itself that pulls in the `/dist/tailwind.css` stylesheet:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
<link rel="stylesheet" href="/dist/tailwind.css" />
|
||||
<title>Hello, world!</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1 class="text-2xl font-bold text-center">Hello world!</h1>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
You can then use a tool like [live-server](https://www.npmjs.com/package/live-server) to preview it in the browser:
|
||||
|
||||
```sh
|
||||
pnpm build && pnpm test:ui
|
||||
live-server .
|
||||
```
|
||||
|
||||
Please ensure that all tests are passing when submitting a pull request. If you're adding new features to Tailwind CSS, always include tests.
|
||||
|
||||
After a successful build, you can also use the npm package tarballs created inside the `dist/` folder to install your build in other local projects.
|
||||
|
||||
## Pull request process
|
||||
|
||||
When submitting a pull request:
|
||||
|
||||
- Ensure the pull request title and description explain the changes you made and why you made them.
|
||||
- Include a test plan section that outlines how you tested your contributions. We do not accept contributions without tests.
|
||||
- Ensure all tests pass. You can add the tag `[ci-all]` in your pull request description to run the test suites across all platforms.
|
||||
|
||||
When a pull request is created, Tailwind CSS maintainers will be notified automatically.
|
||||
|
||||
## Communication
|
||||
|
||||
- **GitHub discussions**: For feature ideas and general questions
|
||||
- **GitHub issues**: For bug reports
|
||||
- **GitHub pull requests**: For code contributions
|
||||
|
|
|
|||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
|
|
@ -1 +0,0 @@
|
|||
custom: ['https://tailwindcss.com/sponsor']
|
||||
13
.github/ISSUE_TEMPLATE.md
vendored
Normal file
13
.github/ISSUE_TEMPLATE.md
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!--
|
||||
|
||||
👋 Hey, thanks for taking an interest in Tailwind!
|
||||
|
||||
We don't accept issues that don't use our issue template.
|
||||
|
||||
Please use this link to create your issue:
|
||||
|
||||
https://github.com/tailwindcss/tailwindcss/issues/new/choose
|
||||
|
||||
Issues that don't follow our issue template will be closed, as it's just impossible to triage and resolve issues without some sort of system with the small team we have.
|
||||
|
||||
-->
|
||||
38
.github/ISSUE_TEMPLATE/bug-report.md
vendored
38
.github/ISSUE_TEMPLATE/bug-report.md
vendored
|
|
@ -1,39 +1,25 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: If you've already asked for help with a problem and confirmed something is broken with Tailwind CSS itself, create a bug report.
|
||||
name: 'Bug Report'
|
||||
about: 'Report a reproducible bug in the framework.'
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
<!-- Please provide all of the information requested below. We're a small team and without all of this information it's not possible for us to help and your bug report will be closed. -->
|
||||
### Describe the problem:
|
||||
|
||||
**What version of Tailwind CSS are you using?**
|
||||
<!--
|
||||
|
||||
For example: v4.0.6
|
||||
Explain the behavior you're seeing that you think is a bug, and explain how you
|
||||
think things should behave instead.
|
||||
|
||||
**What build tool (or framework if it abstracts the build tool) are you using?**
|
||||
-->
|
||||
|
||||
For example: postcss-cli 11.0.0, Next.js 15.1.7, Vite 6.1.0
|
||||
### Link to a minimal reproduction:
|
||||
|
||||
**What version of Node.js are you using?**
|
||||
<!--
|
||||
|
||||
For example: v20.0.0
|
||||
Please provide a link to a GitHub repository that reliably reproduces the issue
|
||||
with the least amount of extraneous code possible.
|
||||
|
||||
**What browser are you using?**
|
||||
|
||||
For example: Chrome, Safari, or N/A
|
||||
|
||||
**What operating system are you using?**
|
||||
|
||||
For example: macOS, Windows
|
||||
|
||||
**Reproduction URL**
|
||||
|
||||
A Tailwind Play link or public GitHub repo that includes a minimal reproduction of the bug. **Please do not link to your actual project**, what we need instead is a _minimal_ reproduction in a fresh project without any unnecessary code. This means it doesn't matter if your real project is private/confidential, since we want a link to a separate, isolated reproduction anyways.
|
||||
|
||||
A reproduction is **required** when filing an issue — any issue opened without a reproduction will be closed and you'll be asked to create a new issue that includes a reproduction. We're a small team and we can't keep up with the volume of issues we receive if we need to reproduce each issue from scratch ourselves.
|
||||
|
||||
**Describe your issue**
|
||||
|
||||
Describe the problem you're seeing, any important steps to reproduce and what behavior you expect instead.
|
||||
-->
|
||||
|
|
|
|||
10
.github/ISSUE_TEMPLATE/config.yml
vendored
10
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -1,11 +1,11 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Get Help
|
||||
url: https://github.com/tailwindlabs/tailwindcss/discussions/new?category=help
|
||||
about: If you can't get something to work the way you expect, open a question in our discussion forums.
|
||||
- name: Feature Request
|
||||
url: https://github.com/tailwindlabs/tailwindcss/discussions/new?category=ideas
|
||||
url: https://github.com/tailwindcss/tailwindcss/discussions/new?category_id=65335
|
||||
about: 'Suggest any ideas you have using our discussion forums.'
|
||||
- name: Help
|
||||
url: https://github.com/tailwindcss/tailwindcss/discussions/new?category_id=65334
|
||||
about: 'If you have a question or need help, ask a question on the discussion forums.'
|
||||
- name: Documentation Issue
|
||||
url: https://github.com/tailwindlabs/tailwindcss.com
|
||||
url: https://github.com/tailwindcss/docs
|
||||
about: 'For documentation issues, suggest changes on our documentation repository.'
|
||||
|
|
|
|||
22
.github/PULL_REQUEST_TEMPLATE.md
vendored
22
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -4,26 +4,8 @@
|
|||
|
||||
**Please ask first before starting work on any significant new features.**
|
||||
|
||||
It's never a fun experience to have your pull request declined after investing a lot of time and effort into a new feature. To avoid this from happening, we request that contributors create a discussion to first discuss any significant new features.
|
||||
It's never a fun experience to have your pull request declined after investing a lot of time and effort into a new feature. To avoid this from happening, we request that contributors create an issue to first discuss any significant new features. This includes things like adding new utilities, creating new at-rules, or adding new component examples to the documentation.
|
||||
|
||||
For more info, check out the contributing guide:
|
||||
|
||||
https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md
|
||||
|
||||
-->
|
||||
|
||||
## Summary
|
||||
|
||||
<!--
|
||||
|
||||
Provide a summary of the issue and the changes you're making. How does your change solve the problem?
|
||||
|
||||
-->
|
||||
|
||||
## Test plan
|
||||
|
||||
<!--
|
||||
|
||||
Explain how you tested your changes. Include the exact commands that you used to verify the change works and include screenshots/screen recordings of the update behavior in the browser if applicable.
|
||||
https://github.com/tailwindcss/tailwindcss/blob/master/.github/CONTRIBUTING.md
|
||||
|
||||
-->
|
||||
|
|
|
|||
1
.github/logo-dark.svg
vendored
1
.github/logo-dark.svg
vendored
|
|
@ -1 +0,0 @@
|
|||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 202 25"><path fill-rule="evenodd" clip-rule="evenodd" d="M20.832 0c-5.556 0-9.028 2.778-10.417 8.333 2.084-2.777 4.514-3.82 7.292-3.125 1.585.397 2.718 1.546 3.971 2.819 2.043 2.073 4.407 4.473 9.57 4.473 5.556 0 9.028-2.778 10.417-8.333-2.083 2.777-4.514 3.819-7.291 3.125-1.585-.397-2.718-1.546-3.972-2.82C28.359 2.4 25.995 0 20.832 0ZM10.417 12.5C4.86 12.5 1.389 15.276 0 20.832c2.083-2.778 4.514-3.82 7.292-3.125 1.584.396 2.717 1.546 3.971 2.818C13.306 22.6 15.67 25 20.833 25c5.556 0 9.028-2.777 10.417-8.333-2.084 2.778-4.514 3.82-7.292 3.125-1.585-.396-2.717-1.546-3.971-2.819-2.043-2.073-4.407-4.473-9.57-4.473Z" fill="#38BDF8"/><path fill-rule="evenodd" clip-rule="evenodd" d="M62.5 10.528h-3.636v7.037c0 1.876 1.232 1.847 3.636 1.73v2.844c-4.867.586-6.803-.762-6.803-4.574v-7.037H53v-3.05h2.697V3.54l3.167-.938v4.877H62.5v3.049ZM76.363 7.48h3.166v14.66h-3.166V20.03c-1.114 1.554-2.844 2.492-5.131 2.492-3.988 0-7.301-3.372-7.301-7.711 0-4.369 3.313-7.711 7.3-7.711 2.288 0 4.018.938 5.132 2.462V7.48ZM71.728 19.5c2.639 0 4.632-1.964 4.632-4.691s-1.993-4.691-4.632-4.691c-2.64 0-4.633 1.964-4.633 4.691s1.994 4.691 4.633 4.691ZM84.804 5.28c-1.114 0-2.023-.938-2.023-2.023 0-1.114.909-2.023 2.023-2.023s2.023.909 2.023 2.023c0 1.085-.909 2.023-2.023 2.023ZM83.22 22.14V7.478h3.167v14.66H83.22Zm6.835 0V.736h3.166V22.14h-3.166ZM113.772 7.48h3.343l-4.604 14.66h-3.108l-3.049-9.88-3.079 9.88h-3.108L95.564 7.48h3.343l2.844 10.116 3.078-10.116h3.02l3.05 10.116 2.873-10.116Zm7.27-2.199c-1.114 0-2.023-.938-2.023-2.023 0-1.114.909-2.023 2.023-2.023s2.023.909 2.023 2.023c0 1.085-.909 2.023-2.023 2.023Zm-1.58 16.86V7.478h3.167v14.66h-3.167Zm14.544-15.041c3.284 0 5.63 2.228 5.63 6.04v9.001h-3.167v-8.679c0-2.228-1.29-3.401-3.284-3.401-2.082 0-3.724 1.231-3.724 4.222v7.858h-3.166V7.48h3.166v1.876c.968-1.524 2.551-2.257 4.545-2.257Zm20.639-5.483h3.166v20.525h-3.166V20.03c-1.114 1.554-2.844 2.492-5.131 2.492-3.988 0-7.301-3.372-7.301-7.711 0-4.37 3.313-7.712 7.301-7.712 2.287 0 4.017.939 5.131 2.463V1.616Zm-4.633 17.885c2.639 0 4.633-1.964 4.633-4.691s-1.994-4.691-4.633-4.691c-2.639 0-4.633 1.964-4.633 4.691s1.994 4.691 4.633 4.691Zm18.412 3.02c-4.427 0-7.74-3.373-7.74-7.712 0-4.369 3.313-7.711 7.74-7.711 2.874 0 5.366 1.495 6.539 3.782l-2.727 1.583c-.645-1.378-2.082-2.257-3.841-2.257-2.58 0-4.545 1.964-4.545 4.603s1.965 4.603 4.545 4.603c1.759 0 3.196-.909 3.9-2.257l2.726 1.554c-1.231 2.316-3.723 3.811-6.597 3.811Zm11.819-10.996c0 2.668 7.888 1.056 7.888 6.48 0 2.932-2.551 4.515-5.718 4.515-2.932 0-5.043-1.32-5.981-3.43l2.726-1.584c.47 1.32 1.642 2.112 3.255 2.112 1.407 0 2.492-.47 2.492-1.642 0-2.61-7.887-1.144-7.887-6.392 0-2.756 2.375-4.486 5.366-4.486 2.404 0 4.398 1.114 5.424 3.049l-2.668 1.495c-.528-1.143-1.554-1.67-2.756-1.67-1.144 0-2.141.498-2.141 1.553Zm13.518 0c0 2.668 7.887 1.056 7.887 6.48 0 2.932-2.551 4.515-5.717 4.515-2.932 0-5.044-1.32-5.982-3.43l2.727-1.584c.469 1.32 1.642 2.112 3.255 2.112 1.407 0 2.492-.47 2.492-1.642 0-2.61-7.887-1.144-7.887-6.392 0-2.756 2.375-4.486 5.365-4.486 2.405 0 4.398 1.114 5.425 3.049l-2.669 1.495c-.527-1.143-1.554-1.67-2.756-1.67-1.143 0-2.14.498-2.14 1.553Z" fill="#fff"/></svg>
|
||||
|
Before Width: | Height: | Size: 3.1 KiB |
1
.github/logo-light.svg
vendored
1
.github/logo-light.svg
vendored
|
|
@ -1 +0,0 @@
|
|||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 202 25"><path fill-rule="evenodd" clip-rule="evenodd" d="M20.832 0c-5.556 0-9.028 2.778-10.417 8.333 2.084-2.777 4.514-3.82 7.292-3.125 1.585.397 2.718 1.546 3.971 2.819 2.043 2.073 4.407 4.473 9.57 4.473 5.556 0 9.028-2.778 10.417-8.333-2.083 2.777-4.514 3.819-7.291 3.125-1.585-.397-2.718-1.546-3.972-2.82C28.359 2.4 25.995 0 20.832 0ZM10.417 12.5C4.86 12.5 1.389 15.276 0 20.832c2.083-2.778 4.514-3.82 7.292-3.125 1.584.396 2.717 1.546 3.971 2.818C13.306 22.6 15.67 25 20.833 25c5.556 0 9.028-2.777 10.417-8.333-2.084 2.778-4.514 3.82-7.292 3.125-1.585-.396-2.717-1.546-3.971-2.819-2.043-2.073-4.407-4.473-9.57-4.473Z" fill="#38BDF8"/><path fill-rule="evenodd" clip-rule="evenodd" d="M62.5 10.528h-3.636v7.037c0 1.876 1.232 1.847 3.636 1.73v2.844c-4.867.586-6.803-.762-6.803-4.574v-7.037H53v-3.05h2.697V3.54l3.167-.938v4.877H62.5v3.049ZM76.363 7.48h3.166v14.66h-3.166V20.03c-1.114 1.554-2.844 2.492-5.131 2.492-3.988 0-7.301-3.372-7.301-7.711 0-4.369 3.313-7.711 7.3-7.711 2.288 0 4.018.938 5.132 2.462V7.48ZM71.728 19.5c2.639 0 4.632-1.964 4.632-4.691s-1.993-4.691-4.632-4.691c-2.64 0-4.633 1.964-4.633 4.691s1.994 4.691 4.633 4.691ZM84.804 5.28c-1.114 0-2.023-.938-2.023-2.023 0-1.114.909-2.023 2.023-2.023s2.023.909 2.023 2.023c0 1.085-.909 2.023-2.023 2.023ZM83.22 22.14V7.478h3.167v14.66H83.22Zm6.835 0V.736h3.166V22.14h-3.166ZM113.772 7.48h3.343l-4.604 14.66h-3.108l-3.049-9.88-3.079 9.88h-3.108L95.564 7.48h3.343l2.844 10.116 3.078-10.116h3.02l3.05 10.116 2.873-10.116Zm7.27-2.199c-1.114 0-2.023-.938-2.023-2.023 0-1.114.909-2.023 2.023-2.023s2.023.909 2.023 2.023c0 1.085-.909 2.023-2.023 2.023Zm-1.58 16.86V7.478h3.167v14.66h-3.167Zm14.544-15.041c3.284 0 5.63 2.228 5.63 6.04v9.001h-3.167v-8.679c0-2.228-1.29-3.401-3.284-3.401-2.082 0-3.724 1.231-3.724 4.222v7.858h-3.166V7.48h3.166v1.876c.968-1.524 2.551-2.257 4.545-2.257Zm20.639-5.483h3.166v20.525h-3.166V20.03c-1.114 1.554-2.844 2.492-5.131 2.492-3.988 0-7.301-3.372-7.301-7.711 0-4.37 3.313-7.712 7.301-7.712 2.287 0 4.017.939 5.131 2.463V1.616Zm-4.633 17.885c2.639 0 4.633-1.964 4.633-4.691s-1.994-4.691-4.633-4.691c-2.639 0-4.633 1.964-4.633 4.691s1.994 4.691 4.633 4.691Zm18.412 3.02c-4.427 0-7.74-3.373-7.74-7.712 0-4.369 3.313-7.711 7.74-7.711 2.874 0 5.366 1.495 6.539 3.782l-2.727 1.583c-.645-1.378-2.082-2.257-3.841-2.257-2.58 0-4.545 1.964-4.545 4.603s1.965 4.603 4.545 4.603c1.759 0 3.196-.909 3.9-2.257l2.726 1.554c-1.231 2.316-3.723 3.811-6.597 3.811Zm11.819-10.996c0 2.668 7.888 1.056 7.888 6.48 0 2.932-2.551 4.515-5.718 4.515-2.932 0-5.043-1.32-5.981-3.43l2.726-1.584c.47 1.32 1.642 2.112 3.255 2.112 1.407 0 2.492-.47 2.492-1.642 0-2.61-7.887-1.144-7.887-6.392 0-2.756 2.375-4.486 5.366-4.486 2.404 0 4.398 1.114 5.424 3.049l-2.668 1.495c-.528-1.143-1.554-1.67-2.756-1.67-1.144 0-2.141.498-2.141 1.553Zm13.518 0c0 2.668 7.887 1.056 7.887 6.48 0 2.932-2.551 4.515-5.717 4.515-2.932 0-5.044-1.32-5.982-3.43l2.727-1.584c.469 1.32 1.642 2.112 3.255 2.112 1.407 0 2.492-.47 2.492-1.642 0-2.61-7.887-1.144-7.887-6.392 0-2.756 2.375-4.486 5.365-4.486 2.405 0 4.398 1.114 5.425 3.049l-2.669 1.495c-.527-1.143-1.554-1.67-2.756-1.67-1.143 0-2.14.498-2.14 1.553Z" fill="#111827"/></svg>
|
||||
|
Before Width: | Height: | Size: 3.1 KiB |
124
.github/workflows/ci.yml
vendored
124
.github/workflows/ci.yml
vendored
|
|
@ -1,124 +0,0 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NODE_VERSION: 24
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
runner:
|
||||
- name: Windows
|
||||
os: windows-latest
|
||||
|
||||
- name: Linux
|
||||
os: namespace-profile-default
|
||||
|
||||
# Playwright 1.62+ dropped WebKit support for macOS 14, and hangs
|
||||
# instead of failing when launching WebKit on a macos-14 runner.
|
||||
- name: macOS
|
||||
os: macos-15
|
||||
|
||||
# Exclude windows and macos from being built on feature branches
|
||||
run-all:
|
||||
- ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.body, '[ci-all]') || github.event.pull_request.user.login == 'depfu[bot]' }}
|
||||
exclude:
|
||||
- run-all: false
|
||||
runner:
|
||||
name: Windows
|
||||
- run-all: false
|
||||
runner:
|
||||
name: macOS
|
||||
|
||||
runs-on: ${{ matrix.runner.os }}
|
||||
timeout-minutes: 30
|
||||
|
||||
name: ${{ matrix.runner.name }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
# Cache the `oxide` Rust build
|
||||
- name: Cache oxide build
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
./crates/node/*.node
|
||||
./crates/node/*.wasm
|
||||
./crates/node/index.d.ts
|
||||
./crates/node/index.js
|
||||
./crates/node/browser.js
|
||||
./crates/node/tailwindcss-oxide.wasi-browser.js
|
||||
./crates/node/tailwindcss-oxide.wasi.cjs
|
||||
./crates/node/wasi-worker-browser.mjs
|
||||
./crates/node/wasi-worker.mjs
|
||||
key: ${{ runner.os }}-oxide-${{ hashFiles('./crates/**/*') }}
|
||||
|
||||
- name: Setup WASM target
|
||||
run: rustup target add wasm32-wasip1-threads
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
env:
|
||||
CARGO_PROFILE_RELEASE_LTO: 'off'
|
||||
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER: 'lld-link'
|
||||
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
# Only lint on linux to avoid \r\n line ending errors
|
||||
if: matrix.runner.os == 'ubuntu-latest'
|
||||
|
||||
- name: Test
|
||||
run: pnpm run test
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: npm run test:ui
|
||||
|
||||
notify:
|
||||
if: ${{ always() && github.ref == 'refs/heads/main' && needs.tests.result == 'failure' }}
|
||||
needs: tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify Discord
|
||||
uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # v2
|
||||
with:
|
||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
message: 'The [most recent ${{ github.workflow }} workflow](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.'
|
||||
125
.github/workflows/integration-tests.yml
vendored
125
.github/workflows/integration-tests.yml
vendored
|
|
@ -1,125 +0,0 @@
|
|||
name: Integration Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NODE_VERSION: 24
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
runner:
|
||||
- name: Windows
|
||||
os: windows-latest
|
||||
|
||||
- name: Linux
|
||||
os: namespace-profile-default
|
||||
|
||||
- name: macOS
|
||||
os: macos-14
|
||||
|
||||
integration:
|
||||
- upgrade
|
||||
- vite
|
||||
- cli
|
||||
- postcss
|
||||
- oxide
|
||||
- webpack
|
||||
|
||||
# Exclude windows and macos from being built on feature branches
|
||||
run-all:
|
||||
- ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.body, '[ci-all]') }}
|
||||
exclude:
|
||||
- run-all: false
|
||||
runner:
|
||||
name: Windows
|
||||
- run-all: false
|
||||
runner:
|
||||
name: macOS
|
||||
|
||||
runs-on: ${{ matrix.runner.os }}
|
||||
timeout-minutes: 30
|
||||
|
||||
name: ${{ matrix.runner.name }} / ${{ matrix.integration }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
|
||||
- run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
# Cargo already skips downloading dependencies if they already exist
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
# Cache the `oxide` Rust build
|
||||
- name: Cache oxide build
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
./crates/node/*.node
|
||||
./crates/node/*.wasm
|
||||
./crates/node/index.d.ts
|
||||
./crates/node/index.js
|
||||
./crates/node/browser.js
|
||||
./crates/node/tailwindcss-oxide.wasi-browser.js
|
||||
./crates/node/tailwindcss-oxide.wasi.cjs
|
||||
./crates/node/wasi-worker-browser.mjs
|
||||
./crates/node/wasi-worker.mjs
|
||||
key: ${{ runner.os }}-oxide-${{ hashFiles('./crates/**/*') }}
|
||||
|
||||
- name: Setup WASM target
|
||||
run: rustup target add wasm32-wasip1-threads
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
env:
|
||||
CARGO_PROFILE_RELEASE_LTO: 'off'
|
||||
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER: 'lld-link'
|
||||
|
||||
- name: Test ${{ matrix.integration }}
|
||||
run: pnpm run test:integrations ./integrations/${{ matrix.integration }}
|
||||
env:
|
||||
GITHUB_WORKSPACE: ${{ github.workspace }}
|
||||
|
||||
notify:
|
||||
if: ${{ always() && github.ref == 'refs/heads/main' && needs.tests.result == 'failure' }}
|
||||
needs: tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify Discord
|
||||
uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # v2
|
||||
with:
|
||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
message: 'The [most recent ${{ github.workflow }} workflow](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `main` branch has failed.'
|
||||
42
.github/workflows/nodejs.yml
vendored
Normal file
42
.github/workflows/nodejs.yml
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [8.x, 10.x, 12.x, 14.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Use cached node_modules
|
||||
id: cache
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: node_modules
|
||||
key: nodeModules-${{ hashFiles('**/yarn.lock') }}-${{ matrix.node-version }}
|
||||
restore-keys: |
|
||||
nodeModules-
|
||||
- name: Install dependencies
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: npm install
|
||||
env:
|
||||
CI: true
|
||||
- run: npm run prepare
|
||||
- run: npm test
|
||||
env:
|
||||
CI: true
|
||||
307
.github/workflows/prepare-release.yml
vendored
307
.github/workflows/prepare-release.yml
vendored
|
|
@ -1,307 +0,0 @@
|
|||
name: Prepare Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: Skip creating the draft GitHub release
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
env:
|
||||
APP_NAME: tailwindcss-oxide
|
||||
NODE_VERSION: 24
|
||||
PNPM_VERSION: '11.9.0'
|
||||
OXIDE_LOCATION: ./crates/node
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
# Windows
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
- os: windows-latest
|
||||
target: aarch64-pc-windows-msvc
|
||||
# macOS
|
||||
- os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
strip: strip -x # Must use -x on macOS. This produces larger results on linux.
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
page-size: 14
|
||||
strip: strip -x # Must use -x on macOS. This produces larger results on linux.
|
||||
# Android
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-linux-android
|
||||
strip: ${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip
|
||||
- os: ubuntu-latest
|
||||
target: armv7-linux-androideabi
|
||||
strip: ${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip
|
||||
# Linux
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
strip: strip
|
||||
build-flags: --use-napi-cross
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-gnu
|
||||
strip: aarch64-linux-gnu-strip
|
||||
build-flags: --use-napi-cross
|
||||
- os: ubuntu-latest
|
||||
target: armv7-unknown-linux-gnueabihf
|
||||
strip: arm-linux-gnueabihf-strip
|
||||
build-flags: --use-napi-cross
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-musl
|
||||
strip-zig: true
|
||||
build-flags: -x
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
strip: strip
|
||||
build-flags: -x
|
||||
|
||||
name: Build ${{ matrix.target }} (oxide)
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Install gcc-arm-linux-gnueabihf
|
||||
if: ${{ matrix.target == 'armv7-unknown-linux-gnueabihf' }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf -y
|
||||
|
||||
- name: Install binutils-aarch64-linux-gnu
|
||||
if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install binutils-aarch64-linux-gnu -y
|
||||
|
||||
- uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
|
||||
if: ${{ contains(matrix.target, 'musl') }}
|
||||
with:
|
||||
version: 0.14.1
|
||||
use-cache: false
|
||||
|
||||
- name: Install cargo-zigbuild
|
||||
uses: taiki-e/install-action@65851e10cd6c377f11a60e600abc07cb08643468 # v2
|
||||
if: ${{ contains(matrix.target, 'musl') }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
tool: cargo-zigbuild
|
||||
|
||||
- name: Setup rust target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/*
|
||||
|
||||
- name: Build release
|
||||
run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform --target=${{ matrix.target }} ${{ matrix.build-flags }}
|
||||
env:
|
||||
RUST_TARGET: ${{ matrix.target }}
|
||||
JEMALLOC_SYS_WITH_LG_PAGE: ${{ matrix.page-size }}
|
||||
|
||||
- name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034
|
||||
if: ${{ matrix.strip || matrix.strip-zig }}
|
||||
env:
|
||||
STRIP_COMMAND: ${{ matrix.strip }}
|
||||
STRIP_ZIG: ${{ matrix.strip-zig }}
|
||||
run: |
|
||||
if [ "$STRIP_ZIG" = "true" ]; then
|
||||
for file in ${{ env.OXIDE_LOCATION }}/*.node; do
|
||||
zig objcopy --strip-all "$file" "$file.stripped"
|
||||
mv "$file.stripped" "$file"
|
||||
done
|
||||
exit 0
|
||||
fi
|
||||
|
||||
eval "$STRIP_COMMAND ${{ env.OXIDE_LOCATION }}/*.node"
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: bindings-${{ matrix.target }}
|
||||
path: ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
||||
build-freebsd:
|
||||
name: Build x86_64-unknown-freebsd (OXIDE)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Build FreeBSD
|
||||
uses: cross-platform-actions/action@cdc9ee69ef84a5f2e59c9058335d9c57bcb4ac86 # v0.25.0
|
||||
env:
|
||||
DEBUG: napi:*
|
||||
RUSTUP_HOME: /usr/local/rustup
|
||||
CARGO_HOME: /usr/local/cargo
|
||||
RUSTUP_IO_THREADS: 1
|
||||
RUST_TARGET: x86_64-unknown-freebsd
|
||||
with:
|
||||
operating_system: freebsd
|
||||
version: '14.0'
|
||||
memory: 13G
|
||||
cpu_count: 3
|
||||
environment_variables: 'DEBUG RUSTUP_IO_THREADS'
|
||||
shell: bash
|
||||
run: |
|
||||
sudo pkg install -y -f curl node libnghttp2 npm
|
||||
sudo npm install -g pnpm@${{ env.PNPM_VERSION }} --unsafe-perm=true
|
||||
curl -sSf https://static.rust-lang.org/rustup/archive/1.27.1/x86_64-unknown-freebsd/rustup-init --output rustup-init
|
||||
chmod +x rustup-init
|
||||
./rustup-init -y --profile minimal
|
||||
source "$HOME/.cargo/env"
|
||||
pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/* || true
|
||||
echo "~~~~ rustc --version ~~~~"
|
||||
rustc --version
|
||||
echo "~~~~ node -v ~~~~"
|
||||
node -v
|
||||
echo "~~~~ pnpm --version ~~~~"
|
||||
pnpm --version
|
||||
pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform
|
||||
strip -x ${{ env.OXIDE_LOCATION }}/*.node
|
||||
ls -la ${{ env.OXIDE_LOCATION }}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: bindings-x86_64-unknown-freebsd
|
||||
path: ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
||||
prepare:
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 15
|
||||
name: Build and release Tailwind CSS
|
||||
|
||||
permissions:
|
||||
contents: write # Required for creating releases
|
||||
|
||||
needs:
|
||||
- build
|
||||
- build-freebsd
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 20
|
||||
persist-credentials: false
|
||||
|
||||
- run: git fetch --tags -f
|
||||
|
||||
- name: Resolve version
|
||||
id: vars
|
||||
run: |
|
||||
echo "TAG_NAME=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
|
||||
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Setup WASM target
|
||||
run: rustup target add wasm32-wasip1-threads
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm --filter=!./playgrounds/* install --frozen-lockfile
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
path: ${{ env.OXIDE_LOCATION }}
|
||||
|
||||
- name: Move artifacts
|
||||
run: |
|
||||
cd ${{ env.OXIDE_LOCATION }}
|
||||
cp bindings-x86_64-pc-windows-msvc/* ./npm/win32-x64-msvc/
|
||||
cp bindings-aarch64-pc-windows-msvc/* ./npm/win32-arm64-msvc/
|
||||
cp bindings-x86_64-apple-darwin/* ./npm/darwin-x64/
|
||||
cp bindings-aarch64-apple-darwin/* ./npm/darwin-arm64/
|
||||
cp bindings-aarch64-linux-android/* ./npm/android-arm64/
|
||||
cp bindings-armv7-linux-androideabi/* ./npm/android-arm-eabi/
|
||||
cp bindings-aarch64-unknown-linux-gnu/* ./npm/linux-arm64-gnu/
|
||||
cp bindings-aarch64-unknown-linux-musl/* ./npm/linux-arm64-musl/
|
||||
cp bindings-armv7-unknown-linux-gnueabihf/* ./npm/linux-arm-gnueabihf/
|
||||
cp bindings-x86_64-unknown-linux-gnu/* ./npm/linux-x64-gnu/
|
||||
cp bindings-x86_64-unknown-linux-musl/* ./npm/linux-x64-musl/
|
||||
cp bindings-x86_64-unknown-freebsd/* ./npm/freebsd-x64/
|
||||
|
||||
- name: Build Tailwind CSS
|
||||
run: pnpm run build
|
||||
env:
|
||||
FEATURES_ENV: stable
|
||||
|
||||
- name: Run pre-publish optimizations scripts
|
||||
run: node ./scripts/pre-publish-optimizations.mjs
|
||||
|
||||
- name: Lock pre-release versions
|
||||
run: node ./scripts/lock-pre-release-versions.mjs
|
||||
|
||||
- name: Get release notes
|
||||
run: |
|
||||
RELEASE_NOTES=$(node ./scripts/release-notes.mjs)
|
||||
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
|
||||
echo "$RELEASE_NOTES" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload standalone artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: tailwindcss-standalone
|
||||
path: packages/@tailwindcss-standalone/dist/
|
||||
|
||||
- name: Upload npm package tarballs
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: npm-package-tarballs
|
||||
path: dist/*.tgz
|
||||
|
||||
- name: Prepare GitHub Release
|
||||
if: ${{ !inputs.dry_run }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release create "${TAG_NAME}" \
|
||||
--draft \
|
||||
--title "${TAG_NAME}" \
|
||||
--notes "${RELEASE_NOTES}" \
|
||||
packages/@tailwindcss-standalone/dist/sha256sums.txt \
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-linux-arm64 \
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-linux-arm64-musl \
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-linux-x64 \
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-linux-x64-musl \
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-macos-arm64 \
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-macos-x64 \
|
||||
packages/@tailwindcss-standalone/dist/tailwindcss-windows-x64.exe
|
||||
358
.github/workflows/release.yml
vendored
358
.github/workflows/release.yml
vendored
|
|
@ -1,358 +0,0 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
channel:
|
||||
description: Release channel to publish
|
||||
required: true
|
||||
default: insiders
|
||||
type: choice
|
||||
options:
|
||||
- insiders
|
||||
- release
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
APP_NAME: tailwindcss-oxide
|
||||
NODE_VERSION: 24
|
||||
PNPM_VERSION: '11.9.0'
|
||||
OXIDE_LOCATION: ./crates/node
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
# Windows
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
- os: windows-latest
|
||||
target: aarch64-pc-windows-msvc
|
||||
# macOS
|
||||
- os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
strip: strip -x # Must use -x on macOS. This produces larger results on linux.
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
page-size: 14
|
||||
strip: strip -x # Must use -x on macOS. This produces larger results on linux.
|
||||
# Android
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-linux-android
|
||||
strip: ${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip
|
||||
- os: ubuntu-latest
|
||||
target: armv7-linux-androideabi
|
||||
strip: ${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip
|
||||
# Linux
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
strip: strip
|
||||
build-flags: --use-napi-cross
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-gnu
|
||||
strip: aarch64-linux-gnu-strip
|
||||
build-flags: --use-napi-cross
|
||||
- os: ubuntu-latest
|
||||
target: armv7-unknown-linux-gnueabihf
|
||||
strip: arm-linux-gnueabihf-strip
|
||||
build-flags: --use-napi-cross
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-musl
|
||||
strip-zig: true
|
||||
build-flags: -x
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
strip: strip
|
||||
build-flags: -x
|
||||
|
||||
name: Build ${{ matrix.target }} (oxide)
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Install gcc-arm-linux-gnueabihf
|
||||
if: ${{ matrix.target == 'armv7-unknown-linux-gnueabihf' }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf -y
|
||||
|
||||
- name: Install binutils-aarch64-linux-gnu
|
||||
if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install binutils-aarch64-linux-gnu -y
|
||||
|
||||
- uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
|
||||
if: ${{ contains(matrix.target, 'musl') }}
|
||||
with:
|
||||
version: 0.14.1
|
||||
use-cache: false
|
||||
|
||||
- name: Install cargo-zigbuild
|
||||
uses: taiki-e/install-action@65851e10cd6c377f11a60e600abc07cb08643468 # v2
|
||||
if: ${{ contains(matrix.target, 'musl') }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
tool: cargo-zigbuild
|
||||
|
||||
- name: Setup rust target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/*
|
||||
|
||||
- name: Build release
|
||||
run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform --target=${{ matrix.target }} ${{ matrix.build-flags }}
|
||||
env:
|
||||
RUST_TARGET: ${{ matrix.target }}
|
||||
JEMALLOC_SYS_WITH_LG_PAGE: ${{ matrix.page-size }}
|
||||
|
||||
- name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034
|
||||
if: ${{ matrix.strip || matrix.strip-zig }}
|
||||
env:
|
||||
STRIP_COMMAND: ${{ matrix.strip }}
|
||||
STRIP_ZIG: ${{ matrix.strip-zig }}
|
||||
run: |
|
||||
if [ "$STRIP_ZIG" = "true" ]; then
|
||||
for file in ${{ env.OXIDE_LOCATION }}/*.node; do
|
||||
zig objcopy --strip-all "$file" "$file.stripped"
|
||||
mv "$file.stripped" "$file"
|
||||
done
|
||||
exit 0
|
||||
fi
|
||||
|
||||
eval "$STRIP_COMMAND ${{ env.OXIDE_LOCATION }}/*.node"
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: bindings-${{ matrix.target }}
|
||||
path: ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
||||
build-freebsd:
|
||||
name: Build x86_64-unknown-freebsd (OXIDE)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Build FreeBSD
|
||||
uses: cross-platform-actions/action@cdc9ee69ef84a5f2e59c9058335d9c57bcb4ac86 # v0.25.0
|
||||
env:
|
||||
DEBUG: napi:*
|
||||
RUSTUP_HOME: /usr/local/rustup
|
||||
CARGO_HOME: /usr/local/cargo
|
||||
RUSTUP_IO_THREADS: 1
|
||||
RUST_TARGET: x86_64-unknown-freebsd
|
||||
with:
|
||||
operating_system: freebsd
|
||||
version: '14.0'
|
||||
memory: 13G
|
||||
cpu_count: 3
|
||||
environment_variables: 'DEBUG RUSTUP_IO_THREADS'
|
||||
shell: bash
|
||||
run: |
|
||||
sudo pkg install -y -f curl node libnghttp2 npm
|
||||
sudo npm install -g pnpm@${{ env.PNPM_VERSION }} --unsafe-perm=true
|
||||
curl -sSf https://static.rust-lang.org/rustup/archive/1.27.1/x86_64-unknown-freebsd/rustup-init --output rustup-init
|
||||
chmod +x rustup-init
|
||||
./rustup-init -y --profile minimal
|
||||
source "$HOME/.cargo/env"
|
||||
echo "~~~~ rustc --version ~~~~"
|
||||
rustc --version
|
||||
echo "~~~~ node -v ~~~~"
|
||||
node -v
|
||||
echo "~~~~ pnpm --version ~~~~"
|
||||
pnpm --version
|
||||
pnpm install --ignore-scripts --frozen-lockfile --filter=!./playgrounds/* || true
|
||||
pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform
|
||||
strip -x ${{ env.OXIDE_LOCATION }}/*.node
|
||||
ls -la ${{ env.OXIDE_LOCATION }}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: bindings-x86_64-unknown-freebsd
|
||||
path: ${{ env.OXIDE_LOCATION }}/*.node
|
||||
|
||||
release:
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 15
|
||||
name: Build and publish Tailwind CSS
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# https://docs.npmjs.com/generating-provenance-statements#publishing-packages-with-provenance-via-github-actions
|
||||
id-token: write
|
||||
|
||||
needs:
|
||||
- build
|
||||
- build-freebsd
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 20
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
package-manager-cache: false
|
||||
|
||||
# npm trusted publishing validates the caller workflow filename, so all npm publishes live here.
|
||||
# This workflow rebuilds the publish artifacts instead of depending on prepare-release.yml.
|
||||
- name: Resolve release metadata
|
||||
env:
|
||||
INPUT_CHANNEL: ${{ github.event.inputs.channel || '' }}
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "release" || "$INPUT_CHANNEL" == "release" ]]; then
|
||||
release_channel=$(node ./scripts/release-channel.js)
|
||||
|
||||
echo "RELEASE_KIND=release" >> $GITHUB_ENV
|
||||
echo "RELEASE_CHANNEL=$release_channel" >> $GITHUB_ENV
|
||||
echo "FEATURES_ENV=stable" >> $GITHUB_ENV
|
||||
else
|
||||
sha_short=$(git rev-parse --short HEAD)
|
||||
|
||||
echo "RELEASE_KIND=insiders" >> $GITHUB_ENV
|
||||
echo "RELEASE_CHANNEL=insiders" >> $GITHUB_ENV
|
||||
echo "SHA_SHORT=$sha_short" >> $GITHUB_ENV
|
||||
echo "INSIDERS_VERSION=0.0.0-insiders.$sha_short" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Setup WASM target
|
||||
run: rustup target add wasm32-wasip1-threads
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm --filter=!./playgrounds/* install --frozen-lockfile
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
path: ${{ env.OXIDE_LOCATION }}
|
||||
|
||||
- name: Move artifacts
|
||||
run: |
|
||||
cd ${{ env.OXIDE_LOCATION }}
|
||||
cp bindings-x86_64-pc-windows-msvc/* ./npm/win32-x64-msvc/
|
||||
cp bindings-aarch64-pc-windows-msvc/* ./npm/win32-arm64-msvc/
|
||||
cp bindings-x86_64-apple-darwin/* ./npm/darwin-x64/
|
||||
cp bindings-aarch64-apple-darwin/* ./npm/darwin-arm64/
|
||||
cp bindings-aarch64-linux-android/* ./npm/android-arm64/
|
||||
cp bindings-armv7-linux-androideabi/* ./npm/android-arm-eabi/
|
||||
cp bindings-aarch64-unknown-linux-gnu/* ./npm/linux-arm64-gnu/
|
||||
cp bindings-aarch64-unknown-linux-musl/* ./npm/linux-arm64-musl/
|
||||
cp bindings-armv7-unknown-linux-gnueabihf/* ./npm/linux-arm-gnueabihf/
|
||||
cp bindings-x86_64-unknown-linux-gnu/* ./npm/linux-x64-gnu/
|
||||
cp bindings-x86_64-unknown-linux-musl/* ./npm/linux-x64-musl/
|
||||
cp bindings-x86_64-unknown-freebsd/* ./npm/freebsd-x64/
|
||||
|
||||
- name: 'Version based on commit: ${{ env.INSIDERS_VERSION }}'
|
||||
if: env.RELEASE_KIND == 'insiders'
|
||||
run: pnpm run version-packages ${INSIDERS_VERSION}
|
||||
|
||||
- name: Build Tailwind CSS
|
||||
if: env.RELEASE_KIND == 'insiders'
|
||||
run: pnpm run build
|
||||
|
||||
- name: Build Tailwind CSS
|
||||
if: env.RELEASE_KIND == 'release'
|
||||
run: pnpm run build
|
||||
env:
|
||||
FEATURES_ENV: ${{ env.FEATURES_ENV }}
|
||||
|
||||
- name: Run pre-publish optimizations scripts
|
||||
run: node ./scripts/pre-publish-optimizations.mjs
|
||||
|
||||
- name: Lock pre-release versions
|
||||
run: node ./scripts/lock-pre-release-versions.mjs
|
||||
|
||||
- name: Upload npm package tarballs
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: npm-package-tarballs
|
||||
path: dist/*.tgz
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
pnpm --recursive --filter="!@tailwindcss/oxide-wasm32-wasi" publish --tag ${RELEASE_CHANNEL} --no-git-checks
|
||||
# The wasm package needs a special npm config that isn't read when pnpm --recursive is used
|
||||
pushd crates/node/npm/wasm32-wasi; pnpm publish --tag ${RELEASE_CHANNEL} --no-git-checks --config.node-linker=hoisted; popd;
|
||||
|
||||
- name: Trigger Tailwind Play update
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }}
|
||||
script: |
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: 'tailwindlabs',
|
||||
repo: 'upgrades',
|
||||
ref: 'main',
|
||||
workflow_id: 'upgrade-tailwindcss.yml'
|
||||
})
|
||||
|
||||
notify:
|
||||
if: ${{ always() && (needs.build.result == 'failure' || needs.build-freebsd.result == 'failure' || needs.release.result == 'failure') }}
|
||||
needs:
|
||||
- build
|
||||
- build-freebsd
|
||||
- release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Resolve release label
|
||||
id: release
|
||||
env:
|
||||
INPUT_CHANNEL: ${{ github.event.inputs.channel || '' }}
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name || '' }}
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "release" ]]; then
|
||||
tag_name="${RELEASE_TAG:-${GITHUB_REF_NAME}}"
|
||||
echo "label=release ${tag_name}" >> $GITHUB_OUTPUT
|
||||
elif [[ "$INPUT_CHANNEL" == "release" ]]; then
|
||||
version=$(node -p "require('./packages/tailwindcss/package.json').version")
|
||||
echo "label=release v${version}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
sha_short=$(git rev-parse --short HEAD)
|
||||
echo "label=insiders release 0.0.0-insiders.${sha_short}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Notify Discord
|
||||
uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # v2
|
||||
with:
|
||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
message: 'The [most recent ${{ github.workflow }} workflow](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) for `${{ steps.release.outputs.label }}` has failed.'
|
||||
19
.gitignore
vendored
19
.gitignore
vendored
|
|
@ -1,10 +1,9 @@
|
|||
node_modules/
|
||||
dist/
|
||||
coverage/
|
||||
.turbo
|
||||
test-results/
|
||||
playwright-report/
|
||||
blob-report/
|
||||
playwright/.cache/
|
||||
target/
|
||||
.debug/
|
||||
/node_modules
|
||||
/lib
|
||||
/example
|
||||
index.html
|
||||
package-lock.json
|
||||
yarn-error.log
|
||||
|
||||
# Perf related files
|
||||
isolate*.log
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
coverage/
|
||||
node_modules/
|
||||
pnpm-lock.yaml
|
||||
target/
|
||||
crates/node/index.d.ts
|
||||
crates/node/index.js
|
||||
crates/ignore/
|
||||
.next
|
||||
.fingerprint
|
||||
3496
CHANGELOG.md
3496
CHANGELOG.md
File diff suppressed because it is too large
Load diff
948
Cargo.lock
generated
948
Cargo.lock
generated
|
|
@ -1,948 +0,0 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "bexpand"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "045d7d9db8390cf2c59f39f3bd138f1962ef616b096d1b9f5651c7acba19e5a7"
|
||||
dependencies = [
|
||||
"itertools",
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata 0.4.18",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "classification-macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.16.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d83cb7e7a873830708d6b02a78cd36a592c6fa14bf267b68725103b85c0d77f"
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||
|
||||
[[package]]
|
||||
name = "dunce"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fast-glob"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eca69ef247d19faa15ac0156968637440824e5ff22baa5ee0cd35b2f7ea6a0f"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
"log",
|
||||
"regex-automata 0.4.18",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "globwalk"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"ignore 0.4.23",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata 0.4.18",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.33"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"crossbeam-channel",
|
||||
"crossbeam-deque",
|
||||
"dunce",
|
||||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata 0.4.18",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "insta"
|
||||
version = "1.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82"
|
||||
dependencies = [
|
||||
"console",
|
||||
"once_cell",
|
||||
"similar",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.159"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
|
||||
dependencies = [
|
||||
"regex-automata 0.1.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "napi"
|
||||
version = "3.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de33522036981030a75c231829566bc63414e08101a6f5ff4ac6cef19c8e0941"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"ctor",
|
||||
"futures",
|
||||
"napi-build",
|
||||
"napi-sys",
|
||||
"nohash-hasher",
|
||||
"rustc-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-build"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9c366d2c8c60b86fa632df75f745509b52f9128f91a6bad4c796e44abb505e1"
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive"
|
||||
version = "3.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a49c513341a61a16a10af6efcce46b30d0822ba2d4fb197d24d33dfc199c78d5"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"ctor",
|
||||
"napi-derive-backend",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive-backend"
|
||||
version = "6.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d60b5d773ad46c698c8cc2cd9fde0b283d39cbb7f71c04bee633c7bdba4423bd"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"semver",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-sys"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85fbf1fa9f1babfe396d74bbbf52b3643770243e8f5b0b46715d4caf7f0dfc9a"
|
||||
dependencies = [
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nohash-hasher"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
|
||||
dependencies = [
|
||||
"overload",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
|
||||
|
||||
[[package]]
|
||||
name = "pretty_assertions"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
|
||||
dependencies = [
|
||||
"diff",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata 0.4.18",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
dependencies = [
|
||||
"regex-syntax 0.6.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.163"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2"
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "similar"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tailwind-oxide"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"napi",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
"rayon",
|
||||
"tailwindcss-oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tailwindcss-oxide"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bexpand",
|
||||
"bstr",
|
||||
"classification-macros",
|
||||
"dunce",
|
||||
"fast-glob",
|
||||
"globwalk",
|
||||
"ignore 0.4.33",
|
||||
"insta",
|
||||
"log",
|
||||
"pretty_assertions",
|
||||
"rayon",
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"tempfile",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"unicode-width",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["crates/*"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
3
LICENSE
3
LICENSE
|
|
@ -1,6 +1,7 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) Tailwind Labs, Inc.
|
||||
Copyright (c) Adam Wathan <adam.wathan@gmail.com>
|
||||
Copyright (c) Jonathan Reinink <jonathan@reinink.ca>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
|||
39
README.md
39
README.md
|
|
@ -1,36 +1,33 @@
|
|||
<p align="center">
|
||||
<a href="https://tailwindcss.com" target="_blank">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/tailwindlabs/tailwindcss/HEAD/.github/logo-dark.svg">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/tailwindlabs/tailwindcss/HEAD/.github/logo-light.svg">
|
||||
<img alt="Tailwind CSS" src="https://raw.githubusercontent.com/tailwindlabs/tailwindcss/HEAD/.github/logo-light.svg" width="350" height="70" style="max-width: 100%;">
|
||||
</picture>
|
||||
</a>
|
||||
<p>
|
||||
<a href="https://tailwindcss.com/" target="_blank">
|
||||
<img alt="Tailwind CSS" width="350" src="https://refactoringui.nyc3.cdn.digitaloceanspaces.com/tailwind-logo.svg">
|
||||
</a><br>
|
||||
A utility-first CSS framework for rapidly building custom user interfaces.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
A utility-first CSS framework for rapidly building custom user interfaces.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/tailwindlabs/tailwindcss/actions"><img src="https://img.shields.io/github/actions/workflow/status/tailwindlabs/tailwindcss/ci.yml?branch=main" alt="Build Status"></a>
|
||||
<p>
|
||||
<a href="https://travis-ci.org/tailwindcss/tailwindcss"><img src="https://img.shields.io/travis/tailwindcss/tailwindcss/master.svg" alt="Build Status"></a>
|
||||
<a href="https://www.npmjs.com/package/tailwindcss"><img src="https://img.shields.io/npm/dt/tailwindcss.svg" alt="Total Downloads"></a>
|
||||
<a href="https://github.com/tailwindlabs/tailwindcss/releases"><img src="https://img.shields.io/npm/v/tailwindcss.svg" alt="Latest Release"></a>
|
||||
<a href="https://github.com/tailwindlabs/tailwindcss/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/tailwindcss.svg" alt="License"></a>
|
||||
<a href="https://github.com/tailwindcss/tailwindcss/releases"><img src="https://img.shields.io/npm/v/tailwindcss.svg" alt="Latest Release"></a>
|
||||
<a href="https://github.com/tailwindcss/tailwindcss/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/tailwindcss.svg" alt="License"></a>
|
||||
</p>
|
||||
|
||||
---
|
||||
------
|
||||
|
||||
## Documentation
|
||||
|
||||
For full documentation, visit [tailwindcss.com](https://tailwindcss.com).
|
||||
For full documentation, visit [tailwindcss.com](https://tailwindcss.com/).
|
||||
|
||||
## Community
|
||||
|
||||
For help, discussion about best practices, or feature ideas:
|
||||
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
|
||||
|
||||
[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions)
|
||||
[Discuss Tailwind CSS on GitHub](https://github.com/tailwindcss/tailwindcss/discussions)
|
||||
|
||||
For casual chit-chat with others using the framework:
|
||||
|
||||
[Join the Tailwind CSS Discord Server](https://discord.gg/7NF8GNe)
|
||||
|
||||
## Contributing
|
||||
|
||||
If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**.
|
||||
If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/master/.github/CONTRIBUTING.md) **before submitting a pull request**.
|
||||
|
|
|
|||
252
__tests__/applyAtRule.test.js
Normal file
252
__tests__/applyAtRule.test.js
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
import postcss from 'postcss'
|
||||
import tailwind from '../src/index'
|
||||
|
||||
function run(input, config = {}) {
|
||||
return postcss([tailwind({ ...config })]).process(input, { from: undefined })
|
||||
}
|
||||
|
||||
test('it copies the declarations from a class into itself', () => {
|
||||
const output = '.a { color: red; } .b { color: red; }'
|
||||
|
||||
return run('.a { color: red; } .b { @apply .a; }').then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('selectors with invalid characters do not need to be manually escaped', () => {
|
||||
const input = `
|
||||
.a\\:1\\/2 { color: red; }
|
||||
.b { @apply .a:1/2; }
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.a\\:1\\/2 { color: red; }
|
||||
.b { color: red; }
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it removes important from applied classes by default', () => {
|
||||
const input = `
|
||||
.a { color: red !important; }
|
||||
.b { @apply .a; }
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.a { color: red !important; }
|
||||
.b { color: red; }
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('applied rules can be made !important', () => {
|
||||
const input = `
|
||||
.a { color: red; }
|
||||
.b { @apply .a !important; }
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.a { color: red; }
|
||||
.b { color: red !important; }
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('cssnext custom property sets are preserved', () => {
|
||||
const input = `
|
||||
.a {
|
||||
color: red;
|
||||
}
|
||||
.b {
|
||||
@apply .a --custom-property-set;
|
||||
}
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.a {
|
||||
color: red;
|
||||
}
|
||||
.b {
|
||||
color: red;
|
||||
@apply --custom-property-set;
|
||||
}
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it fails if the class does not exist', () => {
|
||||
return run('.b { @apply .a; }').catch(e => {
|
||||
expect(e).toMatchObject({ name: 'CssSyntaxError' })
|
||||
})
|
||||
})
|
||||
|
||||
test('applying classes that are defined in a media query is not supported', () => {
|
||||
const input = `
|
||||
@media (min-width: 300px) {
|
||||
.a { color: blue; }
|
||||
}
|
||||
|
||||
.b {
|
||||
@apply .a;
|
||||
}
|
||||
`
|
||||
expect.assertions(1)
|
||||
return run(input).catch(e => {
|
||||
expect(e).toMatchObject({ name: 'CssSyntaxError' })
|
||||
})
|
||||
})
|
||||
|
||||
test('applying classes that are ever used in a media query is not supported', () => {
|
||||
const input = `
|
||||
.a {
|
||||
color: red;
|
||||
}
|
||||
|
||||
@media (min-width: 300px) {
|
||||
.a { color: blue; }
|
||||
}
|
||||
|
||||
.b {
|
||||
@apply .a;
|
||||
}
|
||||
`
|
||||
expect.assertions(1)
|
||||
return run(input).catch(e => {
|
||||
expect(e).toMatchObject({ name: 'CssSyntaxError' })
|
||||
})
|
||||
})
|
||||
|
||||
test('it does not match classes that include pseudo-selectors', () => {
|
||||
const input = `
|
||||
.a:hover {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.b {
|
||||
@apply .a;
|
||||
}
|
||||
`
|
||||
expect.assertions(1)
|
||||
return run(input).catch(e => {
|
||||
expect(e).toMatchObject({ name: 'CssSyntaxError' })
|
||||
})
|
||||
})
|
||||
|
||||
test('it does not match classes that have multiple rules', () => {
|
||||
const input = `
|
||||
.a {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.b {
|
||||
@apply .a;
|
||||
}
|
||||
|
||||
.a {
|
||||
color: blue;
|
||||
}
|
||||
`
|
||||
expect.assertions(1)
|
||||
return run(input).catch(e => {
|
||||
expect(e).toMatchObject({ name: 'CssSyntaxError' })
|
||||
})
|
||||
})
|
||||
|
||||
test('you can apply utility classes that do not actually exist as long as they would exist if utilities were being generated', () => {
|
||||
const input = `
|
||||
.foo { @apply .mt-4; }
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.foo { margin-top: 1rem; }
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('you can apply utility classes without using the given prefix', () => {
|
||||
const input = `
|
||||
.foo { @apply .tw-mt-4 .mb-4; }
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.foo { margin-top: 1rem; margin-bottom: 1rem; }
|
||||
`
|
||||
|
||||
return run(input, { prefix: 'tw-' }).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('you can apply utility classes without using the given prefix when using a function for the prefix', () => {
|
||||
const input = `
|
||||
.foo { @apply .tw-mt-4 .mb-4; }
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.foo { margin-top: 1rem; margin-bottom: 1rem; }
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
prefix: () => {
|
||||
return 'tw-'
|
||||
},
|
||||
}).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('you can apply utility classes without specificity prefix even if important (selector) is used', () => {
|
||||
const input = `
|
||||
.foo { @apply .mt-8 .mb-8; }
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.foo { margin-top: 2rem; margin-bottom: 2rem; }
|
||||
`
|
||||
|
||||
return run(input, { important: '#app' }).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('you can apply utility classes without using the given prefix even if important (selector) is used', () => {
|
||||
const input = `
|
||||
.foo { @apply .tw-mt-4 .mb-4; }
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.foo { margin-top: 1rem; margin-bottom: 1rem; }
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
prefix: 'tw-',
|
||||
important: '#app',
|
||||
}).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
998
__tests__/applyComplexClasses.test.js
Normal file
998
__tests__/applyComplexClasses.test.js
Normal file
|
|
@ -0,0 +1,998 @@
|
|||
import postcss from 'postcss'
|
||||
import resolveConfig from '../src/util/resolveConfig'
|
||||
import defaultConfig from '../stubs/defaultConfig.stub.js'
|
||||
import tailwind from '../src/index'
|
||||
|
||||
function run(input, config = {}) {
|
||||
return postcss([
|
||||
tailwind({ experimental: { applyComplexClasses: true }, ...config }),
|
||||
]).process(input, { from: undefined })
|
||||
}
|
||||
|
||||
test('it copies class declarations into itself', () => {
|
||||
const output = '.a { color: red; } .b { color: red; }'
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run('.a { color: red; } .b { @apply a; }').then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('apply values can contain tabs', () => {
|
||||
const input = `
|
||||
.a {
|
||||
@apply p-4\tm-4;
|
||||
}
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.a {
|
||||
margin: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('apply values can contain newlines', () => {
|
||||
const input = `
|
||||
.a {
|
||||
@apply p-4 m-4
|
||||
flex flex-col;
|
||||
}
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.a {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('selectors with invalid characters do not need to be manually escaped', () => {
|
||||
const input = `
|
||||
.a\\:1\\/2 { color: red; }
|
||||
.b { @apply a:1/2; }
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.a\\:1\\/2 { color: red; }
|
||||
.b { color: red; }
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it removes important from applied classes by default', () => {
|
||||
const input = `
|
||||
.a { color: red !important; }
|
||||
.a:hover { color: blue !important; }
|
||||
.b { @apply a; }
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.a { color: red !important; }
|
||||
.a:hover { color: blue !important; }
|
||||
.b { color: red; }
|
||||
.b:hover { color: blue; }
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('applied rules can be made !important', () => {
|
||||
const input = `
|
||||
.a { color: red; }
|
||||
.b { @apply a !important; }
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.a { color: red; }
|
||||
.b { color: red !important; }
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('cssnext custom property sets are no longer supported', () => {
|
||||
const input = `
|
||||
.a {
|
||||
color: red;
|
||||
}
|
||||
.b {
|
||||
@apply a --custom-property-set;
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(1)
|
||||
|
||||
return run(input).catch(e => {
|
||||
expect(e).toMatchObject({ name: 'CssSyntaxError' })
|
||||
})
|
||||
})
|
||||
|
||||
test('it fails if the class does not exist', () => {
|
||||
expect.assertions(1)
|
||||
|
||||
return run('.b { @apply a; }').catch(e => {
|
||||
expect(e).toMatchObject({ name: 'CssSyntaxError' })
|
||||
})
|
||||
})
|
||||
|
||||
test('applying classes that are defined in a media query is supported', () => {
|
||||
const input = `
|
||||
@media (min-width: 300px) {
|
||||
.a { color: blue; }
|
||||
}
|
||||
|
||||
.b {
|
||||
@apply a;
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
@media (min-width: 300px) {
|
||||
.a { color: blue; }
|
||||
}
|
||||
@media (min-width: 300px) {
|
||||
.b { color: blue; }
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('applying classes that are used in a media query is supported', () => {
|
||||
const input = `
|
||||
.a {
|
||||
color: red;
|
||||
}
|
||||
|
||||
@media (min-width: 300px) {
|
||||
.a { color: blue; }
|
||||
}
|
||||
|
||||
.b {
|
||||
@apply a;
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.a {
|
||||
color: red;
|
||||
}
|
||||
|
||||
@media (min-width: 300px) {
|
||||
.a { color: blue; }
|
||||
}
|
||||
|
||||
.b {
|
||||
color: red;
|
||||
}
|
||||
|
||||
@media (min-width: 300px) {
|
||||
.b { color: blue; }
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it matches classes that include pseudo-selectors', () => {
|
||||
const input = `
|
||||
.a:hover {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.b {
|
||||
@apply a;
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.a:hover {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.b:hover {
|
||||
color: red;
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it matches classes that have multiple rules', () => {
|
||||
const input = `
|
||||
.a {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.b {
|
||||
@apply a;
|
||||
}
|
||||
|
||||
.a {
|
||||
color: blue;
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.a {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.b {
|
||||
color: red;
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.a {
|
||||
color: blue;
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('applying a class that appears multiple times in one selector', () => {
|
||||
const input = `
|
||||
.a + .a > .a {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.b {
|
||||
@apply a;
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.a + .a > .a {
|
||||
color: red;
|
||||
}
|
||||
.b + .a > .a {
|
||||
color: red;
|
||||
}
|
||||
.a + .b > .a {
|
||||
color: red;
|
||||
}
|
||||
.a + .a > .b {
|
||||
color: red;
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('you can apply utility classes that do not actually exist as long as they would exist if utilities were being generated', () => {
|
||||
const input = `
|
||||
.foo { @apply mt-4; }
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.foo { margin-top: 1rem; }
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('the shadow lookup is only used if no @tailwind rules were in the source tree', () => {
|
||||
const input = `
|
||||
@tailwind base;
|
||||
.foo { @apply mt-4; }
|
||||
`
|
||||
|
||||
expect.assertions(1)
|
||||
|
||||
return run(input).catch(e => {
|
||||
expect(e).toMatchObject({ name: 'CssSyntaxError' })
|
||||
})
|
||||
})
|
||||
|
||||
test('you can apply a class that is defined in multiple rules', () => {
|
||||
const input = `
|
||||
.foo {
|
||||
color: red;
|
||||
}
|
||||
.bar {
|
||||
@apply foo;
|
||||
}
|
||||
.foo {
|
||||
opacity: .5;
|
||||
}
|
||||
`
|
||||
const expected = `
|
||||
.foo {
|
||||
color: red;
|
||||
}
|
||||
.bar {
|
||||
color: red;
|
||||
opacity: .5;
|
||||
}
|
||||
.foo {
|
||||
opacity: .5;
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('you can apply a class that is defined in a media query', () => {
|
||||
const input = `
|
||||
.foo {
|
||||
@apply sm:text-center;
|
||||
}
|
||||
`
|
||||
const expected = `
|
||||
@media (min-width: 640px) {
|
||||
.foo {
|
||||
text-align: center
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('you can apply pseudo-class variant utilities', () => {
|
||||
const input = `
|
||||
.foo {
|
||||
@apply hover:opacity-50;
|
||||
}
|
||||
`
|
||||
const expected = `
|
||||
.foo:hover {
|
||||
opacity: 0.5
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('you can apply responsive pseudo-class variant utilities', () => {
|
||||
const input = `
|
||||
.foo {
|
||||
@apply sm:hover:opacity-50;
|
||||
}
|
||||
`
|
||||
const expected = `
|
||||
@media (min-width: 640px) {
|
||||
.foo:hover {
|
||||
opacity: 0.5
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('you can apply the container component', () => {
|
||||
const input = `
|
||||
.foo {
|
||||
@apply container;
|
||||
}
|
||||
`
|
||||
const expected = `
|
||||
.foo {
|
||||
width: 100%;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.foo {
|
||||
max-width: 640px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.foo {
|
||||
max-width: 768px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.foo {
|
||||
max-width: 1024px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1280px) {
|
||||
.foo {
|
||||
max-width: 1280px;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('classes are applied according to CSS source order, not apply order', () => {
|
||||
const input = `
|
||||
.foo {
|
||||
color: red;
|
||||
}
|
||||
.bar {
|
||||
color: blue;
|
||||
}
|
||||
.baz {
|
||||
@apply bar foo;
|
||||
}
|
||||
`
|
||||
const expected = `
|
||||
.foo {
|
||||
color: red;
|
||||
}
|
||||
.bar {
|
||||
color: blue;
|
||||
}
|
||||
.baz {
|
||||
color: red;
|
||||
color: blue;
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('you can apply utilities with multi-class selectors like group-hover variants', () => {
|
||||
const input = `
|
||||
.foo {
|
||||
@apply group-hover:bar;
|
||||
}
|
||||
.bar {
|
||||
color: blue;
|
||||
}
|
||||
.group:hover .group-hover\\:bar {
|
||||
color: blue;
|
||||
}
|
||||
`
|
||||
const expected = `
|
||||
.group:hover .foo {
|
||||
color: blue;
|
||||
}
|
||||
.bar {
|
||||
color: blue;
|
||||
}
|
||||
.group:hover .group-hover\\:bar {
|
||||
color: blue;
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('you can apply classes recursively', () => {
|
||||
const input = `
|
||||
.foo {
|
||||
@apply bar;
|
||||
}
|
||||
.bar {
|
||||
@apply baz;
|
||||
}
|
||||
.baz {
|
||||
color: blue;
|
||||
}
|
||||
`
|
||||
const expected = `
|
||||
.foo {
|
||||
color: blue;
|
||||
}
|
||||
.bar {
|
||||
color: blue;
|
||||
}
|
||||
.baz {
|
||||
color: blue;
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('applied classes are always inserted before subsequent declarations in the same rule, even if it means moving those subsequent declarations to a new rule', () => {
|
||||
const input = `
|
||||
.foo {
|
||||
background: blue;
|
||||
@apply opacity-50 hover:opacity-100 text-right sm:align-middle;
|
||||
color: red;
|
||||
}
|
||||
`
|
||||
const expected = `
|
||||
.foo {
|
||||
background: blue;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.foo:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.foo {
|
||||
text-align: right;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.foo {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
.foo {
|
||||
color: red;
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('adjacent rules are collapsed after being applied', () => {
|
||||
const input = `
|
||||
.foo {
|
||||
@apply hover:bg-white hover:opacity-50 absolute text-right sm:align-middle sm:text-center;
|
||||
}
|
||||
`
|
||||
const expected = `
|
||||
.foo:hover {
|
||||
--bg-opacity: 1;
|
||||
background-color: #fff;
|
||||
background-color: rgba(255, 255, 255, var(--bg-opacity));
|
||||
opacity: 0.5;
|
||||
}
|
||||
.foo {
|
||||
position: absolute;
|
||||
text-align: right;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.foo {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('applying a class applies all instances of that class, even complex selectors', () => {
|
||||
const input = `
|
||||
h1 > p:hover .banana:first-child * {
|
||||
@apply bar;
|
||||
}
|
||||
.bar {
|
||||
color: blue;
|
||||
}
|
||||
@media (print) {
|
||||
@supports (display: grid) {
|
||||
.baz .bar:hover {
|
||||
text-align: right;
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const expected = `
|
||||
h1 > p:hover .banana:first-child * {
|
||||
color: blue;
|
||||
}
|
||||
@media (print) {
|
||||
@supports (display: grid) {
|
||||
.baz h1 > p:hover .banana:first-child *:hover {
|
||||
text-align: right;
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
.bar {
|
||||
color: blue;
|
||||
}
|
||||
@media (print) {
|
||||
@supports (display: grid) {
|
||||
.baz .bar:hover {
|
||||
text-align: right;
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('you can apply classes to rules within at-rules', () => {
|
||||
const input = `
|
||||
@supports (display: grid) {
|
||||
.baz .bar {
|
||||
@apply text-right float-left hover:opacity-50 md:float-right;
|
||||
}
|
||||
}
|
||||
`
|
||||
const expected = `
|
||||
@supports (display: grid) {
|
||||
.baz .bar {
|
||||
float: left;
|
||||
}
|
||||
.baz .bar:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.baz .bar {
|
||||
text-align: right;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.baz .bar {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('using apply with the prefix option', () => {
|
||||
test('applying a class including the prefix', () => {
|
||||
const input = `
|
||||
.foo { @apply tw-mt-4; }
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.foo { margin-top: 1rem; }
|
||||
`
|
||||
|
||||
const config = resolveConfig([
|
||||
{
|
||||
...defaultConfig,
|
||||
prefix: 'tw-',
|
||||
},
|
||||
])
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input, config).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('applying a class including the prefix when using a prefix function', () => {
|
||||
const input = `
|
||||
.foo { @apply tw-func-mt-4; }
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.foo { margin-top: 1rem; }
|
||||
`
|
||||
|
||||
const config = resolveConfig([
|
||||
{
|
||||
...defaultConfig,
|
||||
prefix: () => {
|
||||
return 'tw-func-'
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input, config).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('applying a class without the prefix fails', () => {
|
||||
const input = `
|
||||
.foo { @apply mt-4; }
|
||||
`
|
||||
|
||||
const config = resolveConfig([
|
||||
{
|
||||
...defaultConfig,
|
||||
prefix: 'tw-',
|
||||
},
|
||||
])
|
||||
|
||||
expect.assertions(1)
|
||||
|
||||
return run(input, config).catch(e => {
|
||||
expect(e).toMatchObject({ name: 'CssSyntaxError' })
|
||||
})
|
||||
})
|
||||
|
||||
test('custom classes with no prefix can be applied', () => {
|
||||
const input = `
|
||||
.foo { @apply mt-4; }
|
||||
.mt-4 { color: red; }
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.foo { color: red; }
|
||||
.mt-4 { color: red; }
|
||||
`
|
||||
|
||||
const config = resolveConfig([
|
||||
{
|
||||
...defaultConfig,
|
||||
prefix: 'tw-',
|
||||
},
|
||||
])
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input, config).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('built-in prefixed utilities can be extended and applied', () => {
|
||||
const input = `
|
||||
.foo { @apply tw-mt-4; }
|
||||
.tw-mt-4 { color: red; }
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.foo { margin-top: 1rem; color: red; }
|
||||
.tw-mt-4 { color: red; }
|
||||
`
|
||||
|
||||
const config = resolveConfig([
|
||||
{
|
||||
...defaultConfig,
|
||||
prefix: 'tw-',
|
||||
},
|
||||
])
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input, config).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('a helpful error message is provided if it appears the user forgot to include their prefix', () => {
|
||||
const input = `
|
||||
.foo { @apply mt-4; }
|
||||
`
|
||||
|
||||
const config = resolveConfig([
|
||||
{
|
||||
...defaultConfig,
|
||||
prefix: 'tw-',
|
||||
},
|
||||
])
|
||||
|
||||
expect.assertions(1)
|
||||
|
||||
return run(input, config).catch(e => {
|
||||
expect(e).toMatchObject({
|
||||
name: 'CssSyntaxError',
|
||||
reason: 'The `mt-4` class does not exist, but `tw-mt-4` does. Did you forget the prefix?',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('you can apply classes with important and a prefix enabled', () => {
|
||||
const input = `
|
||||
.foo { @apply tw-mt-4; }
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.foo { margin-top: 1rem; }
|
||||
`
|
||||
|
||||
const config = resolveConfig([
|
||||
{
|
||||
...defaultConfig,
|
||||
prefix: 'tw-',
|
||||
important: true,
|
||||
},
|
||||
])
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input, config).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('you can apply classes with an important selector and a prefix enabled', () => {
|
||||
const input = `
|
||||
.foo { @apply tw-mt-4; }
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.foo { margin-top: 1rem; }
|
||||
`
|
||||
|
||||
const config = resolveConfig([
|
||||
{
|
||||
...defaultConfig,
|
||||
prefix: 'tw-',
|
||||
important: '#app',
|
||||
},
|
||||
])
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input, config).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('you can apply utility classes when a selector is used for the important option', () => {
|
||||
const input = `
|
||||
.foo {
|
||||
@apply mt-8 mb-8;
|
||||
}
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.foo {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
`
|
||||
|
||||
const config = resolveConfig([
|
||||
{
|
||||
...defaultConfig,
|
||||
important: '#app',
|
||||
},
|
||||
])
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input, config).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('you can apply classes to a rule with multiple selectors', () => {
|
||||
const input = `
|
||||
@supports (display: grid) {
|
||||
.foo, h1 > .bar * {
|
||||
@apply float-left opacity-50 hover:opacity-100 md:float-right;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const expected = `
|
||||
@supports (display: grid) {
|
||||
.foo, h1 > .bar * {
|
||||
float: left;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.foo:hover, h1 > .bar *:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.foo, h1 > .bar * {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
19
__tests__/cli.compile.test.js
Normal file
19
__tests__/cli.compile.test.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import path from 'path'
|
||||
|
||||
import autoprefixer from 'autoprefixer'
|
||||
|
||||
import tailwind from '../src'
|
||||
import compile from '../src/cli/compile'
|
||||
|
||||
describe('cli compile', () => {
|
||||
const inputFile = path.resolve(__dirname, 'fixtures/tailwind-input.css')
|
||||
const outputFile = 'output.css'
|
||||
const plugins = [tailwind(), autoprefixer]
|
||||
|
||||
it('compiles CSS file', () => {
|
||||
return compile({ inputFile, outputFile, plugins }).then(result => {
|
||||
expect(result.css).toContain('.example')
|
||||
expect(result.css).toContain('-ms-input-placeholder')
|
||||
})
|
||||
})
|
||||
})
|
||||
98
__tests__/cli.test.js
Normal file
98
__tests__/cli.test.js
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import path from 'path'
|
||||
|
||||
import cli from '../src/cli/main'
|
||||
import * as constants from '../src/constants'
|
||||
import * as utils from '../src/cli/utils'
|
||||
import runInTempDirectory from '../jest/runInTempDirectory'
|
||||
|
||||
describe('cli', () => {
|
||||
const inputCssPath = path.resolve(__dirname, 'fixtures/tailwind-input.css')
|
||||
const customConfigPath = path.resolve(__dirname, 'fixtures/custom-config.js')
|
||||
const defaultConfigFixture = utils.readFile(constants.defaultConfigStubFile)
|
||||
const simpleConfigFixture = utils.readFile(constants.simpleConfigStubFile)
|
||||
const defaultPostCssConfigFixture = utils.readFile(constants.defaultPostCssConfigStubFile)
|
||||
|
||||
beforeEach(() => {
|
||||
console.log = jest.fn()
|
||||
process.stdout.write = jest.fn()
|
||||
})
|
||||
|
||||
describe('init', () => {
|
||||
it('creates a Tailwind config file', () => {
|
||||
return runInTempDirectory(() => {
|
||||
return cli(['init']).then(() => {
|
||||
expect(utils.readFile(constants.defaultConfigFile)).toEqual(simpleConfigFixture)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('creates a Tailwind config file and a postcss.config.js file', () => {
|
||||
return runInTempDirectory(() => {
|
||||
return cli(['init', '-p']).then(() => {
|
||||
expect(utils.readFile(constants.defaultConfigFile)).toEqual(simpleConfigFixture)
|
||||
expect(utils.readFile(constants.defaultPostCssConfigFile)).toEqual(
|
||||
defaultPostCssConfigFixture
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('creates a full Tailwind config file', () => {
|
||||
return runInTempDirectory(() => {
|
||||
return cli(['init', '--full']).then(() => {
|
||||
expect(utils.readFile(constants.defaultConfigFile)).toEqual(defaultConfigFixture)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('creates a Tailwind config file in a custom location', () => {
|
||||
return runInTempDirectory(() => {
|
||||
return cli(['init', 'custom.js']).then(() => {
|
||||
expect(utils.exists('custom.js')).toEqual(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('build', () => {
|
||||
it('compiles CSS file using an input css file', () => {
|
||||
return cli(['build', inputCssPath]).then(() => {
|
||||
expect(process.stdout.write.mock.calls[0][0]).toContain('.example')
|
||||
})
|
||||
})
|
||||
|
||||
it('compiles CSS file without an input css file', () => {
|
||||
return cli(['build']).then(() => {
|
||||
expect(process.stdout.write.mock.calls[0][0]).toContain('normalize.css') // base
|
||||
expect(process.stdout.write.mock.calls[0][0]).toContain('.container') // components
|
||||
expect(process.stdout.write.mock.calls[0][0]).toContain('.mx-auto') // utilities
|
||||
})
|
||||
})
|
||||
|
||||
it('compiles CSS file using custom configuration', () => {
|
||||
return cli(['build', inputCssPath, '--config', customConfigPath]).then(() => {
|
||||
expect(process.stdout.write.mock.calls[0][0]).toContain('400px')
|
||||
})
|
||||
})
|
||||
|
||||
it('creates compiled CSS file', () => {
|
||||
return runInTempDirectory(() => {
|
||||
return cli(['build', inputCssPath, '--output', 'output.css']).then(() => {
|
||||
expect(utils.readFile('output.css')).toContain('.example')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('compiles CSS file with autoprefixer', () => {
|
||||
return cli(['build', inputCssPath]).then(() => {
|
||||
expect(process.stdout.write.mock.calls[0][0]).toContain('-ms-input-placeholder')
|
||||
})
|
||||
})
|
||||
|
||||
it('compiles CSS file without autoprefixer', () => {
|
||||
return cli(['build', inputCssPath, '--no-autoprefixer']).then(() => {
|
||||
expect(process.stdout.write.mock.calls[0][0]).not.toContain('-ms-input-placeholder')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
73
__tests__/cli.utils.test.js
Normal file
73
__tests__/cli.utils.test.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import * as utils from '../src/cli/utils'
|
||||
|
||||
describe('cli utils', () => {
|
||||
describe('parseCliParams', () => {
|
||||
it('parses CLI parameters', () => {
|
||||
const result = utils.parseCliParams(['a', 'b', '-c', 'd'])
|
||||
|
||||
expect(result).toEqual(['a', 'b'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseCliOptions', () => {
|
||||
it('parses CLI options', () => {
|
||||
const result = utils.parseCliOptions(['a', '-b', 'c'], { test: ['b'] })
|
||||
|
||||
expect(result).toEqual({ test: ['c'] })
|
||||
})
|
||||
|
||||
it('parses multiple types of options', () => {
|
||||
const result = utils.parseCliOptions(['a', '-b', 'c', '--test', 'd', '-test', 'e'], {
|
||||
test: ['test', 'b'],
|
||||
})
|
||||
|
||||
expect(result).toEqual({ test: ['c', 'd', 'e'] })
|
||||
})
|
||||
|
||||
it('ignores unknown options', () => {
|
||||
const result = utils.parseCliOptions(['a', '-b', 'c'], {})
|
||||
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
|
||||
it('maps options', () => {
|
||||
const result = utils.parseCliOptions(['a', '-b', 'c', '-d', 'e'], { test: ['b', 'd'] })
|
||||
|
||||
expect(result).toEqual({ test: ['c', 'e'] })
|
||||
})
|
||||
|
||||
it('parses undefined options', () => {
|
||||
const result = utils.parseCliOptions(['a'], { test: ['b'] })
|
||||
|
||||
expect(result).toEqual({ test: undefined })
|
||||
})
|
||||
|
||||
it('parses flags', () => {
|
||||
const result = utils.parseCliOptions(['a', '-b'], { test: ['b'] })
|
||||
|
||||
expect(result).toEqual({ test: [] })
|
||||
})
|
||||
|
||||
it('accepts multiple values per option', () => {
|
||||
const result = utils.parseCliOptions(['a', '-b', 'c', 'd', '-e', 'f', '-g', 'h'], {
|
||||
test: ['b', 'g'],
|
||||
})
|
||||
|
||||
expect(result).toEqual({ test: ['c', 'd', 'h'] })
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSimplePath', () => {
|
||||
it('strips leading ./', () => {
|
||||
const result = utils.getSimplePath('./test')
|
||||
|
||||
expect(result).toEqual('test')
|
||||
})
|
||||
|
||||
it('returns unchanged path if it does not begin with ./', () => {
|
||||
const result = utils.getSimplePath('../test')
|
||||
|
||||
expect(result).toEqual('../test')
|
||||
})
|
||||
})
|
||||
})
|
||||
42
__tests__/configurePlugins.test.js
Normal file
42
__tests__/configurePlugins.test.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import configurePlugins from '../src/util/configurePlugins'
|
||||
|
||||
test('setting a plugin to false removes it', () => {
|
||||
const plugins = {
|
||||
fontSize: () => 'fontSize',
|
||||
display: () => 'display',
|
||||
backgroundPosition: () => 'backgroundPosition',
|
||||
}
|
||||
|
||||
const configuredPlugins = configurePlugins(
|
||||
{
|
||||
display: false,
|
||||
},
|
||||
plugins
|
||||
)
|
||||
|
||||
expect(configuredPlugins).toEqual(['fontSize', 'backgroundPosition'])
|
||||
})
|
||||
|
||||
test('passing only false removes all plugins', () => {
|
||||
const plugins = {
|
||||
fontSize: () => 'fontSize',
|
||||
display: () => 'display',
|
||||
backgroundPosition: () => 'backgroundPosition',
|
||||
}
|
||||
|
||||
const configuredPlugins = configurePlugins(false, plugins)
|
||||
|
||||
expect(configuredPlugins).toEqual([])
|
||||
})
|
||||
|
||||
test('passing an array whitelists plugins', () => {
|
||||
const plugins = {
|
||||
fontSize: () => 'fontSize',
|
||||
display: () => 'display',
|
||||
backgroundPosition: () => 'backgroundPosition',
|
||||
}
|
||||
|
||||
const configuredPlugins = configurePlugins(['display'], plugins)
|
||||
|
||||
expect(configuredPlugins).toEqual(['display'])
|
||||
})
|
||||
330
__tests__/containerPlugin.test.js
Normal file
330
__tests__/containerPlugin.test.js
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
import _ from 'lodash'
|
||||
import postcss from 'postcss'
|
||||
import processPlugins from '../src/util/processPlugins'
|
||||
import container from '../src/plugins/container'
|
||||
|
||||
function css(nodes) {
|
||||
return postcss.root({ nodes }).toString()
|
||||
}
|
||||
|
||||
function config(overrides) {
|
||||
return _.defaultsDeep(overrides, {
|
||||
theme: {
|
||||
screens: {
|
||||
sm: '576px',
|
||||
md: '768px',
|
||||
lg: '992px',
|
||||
xl: '1200px',
|
||||
},
|
||||
},
|
||||
prefix: '',
|
||||
})
|
||||
}
|
||||
|
||||
test('options are not required', () => {
|
||||
const { components } = processPlugins([container()], config())
|
||||
|
||||
expect(css(components)).toMatchCss(`
|
||||
@layer components {
|
||||
@variants {
|
||||
.container { width: 100% }
|
||||
@media (min-width: 576px) {
|
||||
.container { max-width: 576px }
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.container { max-width: 768px }
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.container { max-width: 992px }
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.container { max-width: 1200px }
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test('screens can be passed explicitly', () => {
|
||||
const { components } = processPlugins(
|
||||
[container()],
|
||||
config({
|
||||
theme: {
|
||||
container: {
|
||||
screens: ['400px', '500px'],
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
expect(css(components)).toMatchCss(`
|
||||
@layer components {
|
||||
@variants {
|
||||
.container { width: 100% }
|
||||
@media (min-width: 400px) {
|
||||
.container { max-width: 400px }
|
||||
}
|
||||
@media (min-width: 500px) {
|
||||
.container { max-width: 500px }
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test('screens are ordered ascending by min-width', () => {
|
||||
const { components } = processPlugins(
|
||||
[container()],
|
||||
config({
|
||||
theme: {
|
||||
container: {
|
||||
screens: ['500px', '400px'],
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
expect(css(components)).toMatchCss(`
|
||||
@layer components {
|
||||
@variants {
|
||||
.container { width: 100% }
|
||||
@media (min-width: 400px) {
|
||||
.container { max-width: 400px }
|
||||
}
|
||||
@media (min-width: 500px) {
|
||||
.container { max-width: 500px }
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test('screens are deduplicated by min-width', () => {
|
||||
const { components } = processPlugins(
|
||||
[container()],
|
||||
config({
|
||||
theme: {
|
||||
container: {
|
||||
screens: {
|
||||
sm: '576px',
|
||||
md: '768px',
|
||||
'sm-only': { min: '576px', max: '767px' },
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
expect(css(components)).toMatchCss(`
|
||||
@layer components {
|
||||
@variants {
|
||||
.container { width: 100% }
|
||||
@media (min-width: 576px) {
|
||||
.container { max-width: 576px }
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.container { max-width: 768px }
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test('the container can be centered by default', () => {
|
||||
const { components } = processPlugins(
|
||||
[container()],
|
||||
config({
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
expect(css(components)).toMatchCss(`
|
||||
@layer components {
|
||||
@variants {
|
||||
.container {
|
||||
width: 100%;
|
||||
margin-right: auto;
|
||||
margin-left: auto
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.container { max-width: 576px }
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.container { max-width: 768px }
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.container { max-width: 992px }
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.container { max-width: 1200px }
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test('horizontal padding can be included by default', () => {
|
||||
const { components } = processPlugins(
|
||||
[container()],
|
||||
config({
|
||||
theme: {
|
||||
container: {
|
||||
padding: '2rem',
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
expect(css(components)).toMatchCss(`
|
||||
@layer components {
|
||||
@variants {
|
||||
.container {
|
||||
width: 100%;
|
||||
padding-right: 2rem;
|
||||
padding-left: 2rem
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.container { max-width: 576px }
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.container { max-width: 768px }
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.container { max-width: 992px }
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.container { max-width: 1200px }
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test('responsive horizontal padding can be included by default', () => {
|
||||
const { components } = processPlugins(
|
||||
[container()],
|
||||
config({
|
||||
theme: {
|
||||
container: {
|
||||
padding: {
|
||||
default: '1rem',
|
||||
sm: '2rem',
|
||||
lg: '4rem',
|
||||
xl: '5rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
expect(css(components)).toMatchCss(`
|
||||
@layer components {
|
||||
@variants {
|
||||
.container {
|
||||
width: 100%;
|
||||
padding-right: 1rem;
|
||||
padding-left: 1rem
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.container {
|
||||
max-width: 576px;
|
||||
padding-right: 2rem;
|
||||
padding-left: 2rem
|
||||
}
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.container { max-width: 768px }
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.container {
|
||||
max-width: 992px;
|
||||
padding-right: 4rem;
|
||||
padding-left: 4rem
|
||||
}
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
padding-right: 5rem;
|
||||
padding-left: 5rem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test('setting all options at once', () => {
|
||||
const { components } = processPlugins(
|
||||
[container()],
|
||||
config({
|
||||
theme: {
|
||||
container: {
|
||||
screens: ['400px', '500px'],
|
||||
center: true,
|
||||
padding: '2rem',
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
expect(css(components)).toMatchCss(`
|
||||
@layer components {
|
||||
@variants {
|
||||
.container {
|
||||
width: 100%;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
padding-right: 2rem;
|
||||
padding-left: 2rem
|
||||
}
|
||||
@media (min-width: 400px) {
|
||||
.container { max-width: 400px }
|
||||
}
|
||||
@media (min-width: 500px) {
|
||||
.container { max-width: 500px }
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test('container can use variants', () => {
|
||||
const { components } = processPlugins(
|
||||
[container()],
|
||||
config({
|
||||
theme: {
|
||||
container: {
|
||||
screens: ['400px', '500px'],
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
container: ['responsive', 'hover'],
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
expect(css(components)).toMatchCss(`
|
||||
@layer components {
|
||||
@variants responsive, hover {
|
||||
.container {
|
||||
width: 100%
|
||||
}
|
||||
@media (min-width: 400px) {
|
||||
.container {
|
||||
max-width: 400px
|
||||
}
|
||||
}
|
||||
@media (min-width: 500px) {
|
||||
.container {
|
||||
max-width: 500px
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
212
__tests__/customConfig.test.js
Normal file
212
__tests__/customConfig.test.js
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import postcss from 'postcss'
|
||||
import tailwind from '../src/index'
|
||||
import { defaultConfigFile } from '../src/constants'
|
||||
import inTempDirectory from '../jest/runInTempDirectory'
|
||||
|
||||
test('it uses the values from the custom config file', () => {
|
||||
return postcss([tailwind(path.resolve(`${__dirname}/fixtures/custom-config.js`))])
|
||||
.process(
|
||||
`
|
||||
@responsive {
|
||||
.foo {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ from: undefined }
|
||||
)
|
||||
.then(result => {
|
||||
const expected = `
|
||||
.foo {
|
||||
color: blue;
|
||||
}
|
||||
@media (min-width: 400px) {
|
||||
.mobile\\:foo {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
`
|
||||
expect(result.css).toMatchCss(expected)
|
||||
})
|
||||
})
|
||||
|
||||
test('custom config can be passed as an object', () => {
|
||||
return postcss([
|
||||
tailwind({
|
||||
theme: {
|
||||
screens: {
|
||||
mobile: '400px',
|
||||
},
|
||||
},
|
||||
}),
|
||||
])
|
||||
.process(
|
||||
`
|
||||
@responsive {
|
||||
.foo {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ from: undefined }
|
||||
)
|
||||
.then(result => {
|
||||
const expected = `
|
||||
.foo {
|
||||
color: blue;
|
||||
}
|
||||
@media (min-width: 400px) {
|
||||
.mobile\\:foo {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
expect(result.css).toMatchCss(expected)
|
||||
})
|
||||
})
|
||||
|
||||
test('custom config path can be passed using `config` property in an object', () => {
|
||||
return postcss([tailwind({ config: path.resolve(`${__dirname}/fixtures/custom-config.js`) })])
|
||||
.process(
|
||||
`
|
||||
@responsive {
|
||||
.foo {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ from: undefined }
|
||||
)
|
||||
.then(result => {
|
||||
const expected = `
|
||||
.foo {
|
||||
color: blue;
|
||||
}
|
||||
@media (min-width: 400px) {
|
||||
.mobile\\:foo {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
`
|
||||
expect(result.css).toMatchCss(expected)
|
||||
})
|
||||
})
|
||||
|
||||
test('custom config can be passed under the `config` property', () => {
|
||||
return postcss([
|
||||
tailwind({
|
||||
config: {
|
||||
theme: {
|
||||
screens: {
|
||||
mobile: '400px',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
])
|
||||
.process(
|
||||
`
|
||||
@responsive {
|
||||
.foo {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ from: undefined }
|
||||
)
|
||||
.then(result => {
|
||||
const expected = `
|
||||
.foo {
|
||||
color: blue;
|
||||
}
|
||||
@media (min-width: 400px) {
|
||||
.mobile\\:foo {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
expect(result.css).toMatchCss(expected)
|
||||
})
|
||||
})
|
||||
|
||||
test('tailwind.config.js is picked up by default', () => {
|
||||
return inTempDirectory(() => {
|
||||
fs.writeFileSync(
|
||||
path.resolve(defaultConfigFile),
|
||||
`module.exports = {
|
||||
theme: {
|
||||
screens: {
|
||||
mobile: '400px',
|
||||
},
|
||||
},
|
||||
}`
|
||||
)
|
||||
|
||||
return postcss([tailwind])
|
||||
.process(
|
||||
`
|
||||
@responsive {
|
||||
.foo {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ from: undefined }
|
||||
)
|
||||
.then(result => {
|
||||
expect(result.css).toMatchCss(`
|
||||
.foo {
|
||||
color: blue;
|
||||
}
|
||||
@media (min-width: 400px) {
|
||||
.mobile\\:foo {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('tailwind.config.js is picked up by default when passing an empty object', () => {
|
||||
return inTempDirectory(() => {
|
||||
fs.writeFileSync(
|
||||
path.resolve(defaultConfigFile),
|
||||
`module.exports = {
|
||||
theme: {
|
||||
screens: {
|
||||
mobile: '400px',
|
||||
},
|
||||
},
|
||||
}`
|
||||
)
|
||||
|
||||
return postcss([tailwind({})])
|
||||
.process(
|
||||
`
|
||||
@responsive {
|
||||
.foo {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ from: undefined }
|
||||
)
|
||||
.then(result => {
|
||||
expect(result.css).toMatchCss(`
|
||||
.foo {
|
||||
color: blue;
|
||||
}
|
||||
@media (min-width: 400px) {
|
||||
.mobile\\:foo {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
})
|
||||
240
__tests__/darkMode.test.js
Normal file
240
__tests__/darkMode.test.js
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
import postcss from 'postcss'
|
||||
import tailwind from '../src/index'
|
||||
|
||||
function run(input, config = {}) {
|
||||
return postcss([tailwind({ experimental: { darkModeVariant: true }, ...config })]).process(
|
||||
input,
|
||||
{ from: undefined }
|
||||
)
|
||||
}
|
||||
|
||||
test('dark mode variants cannot be generated without enabling the dark mode experiment', () => {
|
||||
const input = `
|
||||
@variants dark {
|
||||
.text-red {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(1)
|
||||
return expect(run(input, { experimental: {} })).rejects.toThrow()
|
||||
})
|
||||
|
||||
test('generating dark mode variants uses the media strategy by default', () => {
|
||||
const input = `
|
||||
@variants dark {
|
||||
.text-red {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.text-red {
|
||||
color: red;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\\:text-red {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('dark mode variants can be generated using the class strategy', () => {
|
||||
const input = `
|
||||
@variants dark {
|
||||
.text-red {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.text-red {
|
||||
color: red;
|
||||
}
|
||||
.dark .dark\\:text-red {
|
||||
color: red;
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input, { dark: 'class' }).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('dark mode variants stack with other variants', () => {
|
||||
const input = `
|
||||
@variants responsive, dark, hover, focus {
|
||||
.text-red {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.text-red {
|
||||
color: red;
|
||||
}
|
||||
.hover\\:text-red:hover {
|
||||
color: red;
|
||||
}
|
||||
.focus\\:text-red:focus {
|
||||
color: red;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\\:text-red {
|
||||
color: red;
|
||||
}
|
||||
.dark\\:hover\\:text-red:hover {
|
||||
color: red;
|
||||
}
|
||||
.dark\\:focus\\:text-red:focus {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
@media (min-width: 500px) {
|
||||
.sm\\:text-red {
|
||||
color: red;
|
||||
}
|
||||
.sm\\:hover\\:text-red:hover {
|
||||
color: red;
|
||||
}
|
||||
.sm\\:focus\\:text-red:focus {
|
||||
color: red;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.sm\\:dark\\:text-red {
|
||||
color: red;
|
||||
}
|
||||
.sm\\:dark\\:hover\\:text-red:hover {
|
||||
color: red;
|
||||
}
|
||||
.sm\\:dark\\:focus\\:text-red:focus {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (min-width: 800px) {
|
||||
.lg\\:text-red {
|
||||
color: red;
|
||||
}
|
||||
.lg\\:hover\\:text-red:hover {
|
||||
color: red;
|
||||
}
|
||||
.lg\\:focus\\:text-red:focus {
|
||||
color: red;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.lg\\:dark\\:text-red {
|
||||
color: red;
|
||||
}
|
||||
.lg\\:dark\\:hover\\:text-red:hover {
|
||||
color: red;
|
||||
}
|
||||
.lg\\:dark\\:focus\\:text-red:focus {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input, { theme: { screens: { sm: '500px', lg: '800px' } } }).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('dark mode variants stack with other variants when using the class strategy', () => {
|
||||
const input = `
|
||||
@variants responsive, dark, hover, focus {
|
||||
.text-red {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const expected = `
|
||||
.text-red {
|
||||
color: red;
|
||||
}
|
||||
.hover\\:text-red:hover {
|
||||
color: red;
|
||||
}
|
||||
.focus\\:text-red:focus {
|
||||
color: red;
|
||||
}
|
||||
.dark .dark\\:text-red {
|
||||
color: red;
|
||||
}
|
||||
.dark .dark\\:hover\\:text-red:hover {
|
||||
color: red;
|
||||
}
|
||||
.dark .dark\\:focus\\:text-red:focus {
|
||||
color: red;
|
||||
}
|
||||
@media (min-width: 500px) {
|
||||
.sm\\:text-red {
|
||||
color: red;
|
||||
}
|
||||
.sm\\:hover\\:text-red:hover {
|
||||
color: red;
|
||||
}
|
||||
.sm\\:focus\\:text-red:focus {
|
||||
color: red;
|
||||
}
|
||||
.dark .sm\\:dark\\:text-red {
|
||||
color: red;
|
||||
}
|
||||
.dark .sm\\:dark\\:hover\\:text-red:hover {
|
||||
color: red;
|
||||
}
|
||||
.dark .sm\\:dark\\:focus\\:text-red:focus {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
@media (min-width: 800px) {
|
||||
.lg\\:text-red {
|
||||
color: red;
|
||||
}
|
||||
.lg\\:hover\\:text-red:hover {
|
||||
color: red;
|
||||
}
|
||||
.lg\\:focus\\:text-red:focus {
|
||||
color: red;
|
||||
}
|
||||
.dark .lg\\:dark\\:text-red {
|
||||
color: red;
|
||||
}
|
||||
.dark .lg\\:dark\\:hover\\:text-red:hover {
|
||||
color: red;
|
||||
}
|
||||
.dark .lg\\:dark\\:focus\\:text-red:focus {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input, { dark: 'class', theme: { screens: { sm: '500px', lg: '800px' } } }).then(
|
||||
result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
}
|
||||
)
|
||||
})
|
||||
11
__tests__/defaultConfig.test.js
Normal file
11
__tests__/defaultConfig.test.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import config from '../defaultConfig.js'
|
||||
import configStub from '../stubs/defaultConfig.stub.js'
|
||||
|
||||
test('the default config matches the stub', () => {
|
||||
expect(config).toEqual(configStub)
|
||||
})
|
||||
|
||||
test('modifying the default config does not affect the stub', () => {
|
||||
config.theme = {}
|
||||
expect(config).not.toEqual(configStub)
|
||||
})
|
||||
11
__tests__/defaultTheme.test.js
Normal file
11
__tests__/defaultTheme.test.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import theme from '../defaultTheme.js'
|
||||
import configStub from '../stubs/defaultConfig.stub.js'
|
||||
|
||||
test('the default theme matches the stub', () => {
|
||||
expect(theme).toEqual(configStub.theme)
|
||||
})
|
||||
|
||||
test('modifying the default theme does not affect the stub', () => {
|
||||
theme.colors = {}
|
||||
expect(theme).not.toEqual(configStub.theme)
|
||||
})
|
||||
5
__tests__/escapeClassName.test.js
Normal file
5
__tests__/escapeClassName.test.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import escapeClassName from '../src/util/escapeClassName'
|
||||
|
||||
test('invalid characters are escaped', () => {
|
||||
expect(escapeClassName('w:_$-1/2')).toEqual('w\\:_\\$-1\\/2')
|
||||
})
|
||||
7
__tests__/fixtures/custom-config.js
Normal file
7
__tests__/fixtures/custom-config.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
theme: {
|
||||
screens: {
|
||||
mobile: '400px',
|
||||
},
|
||||
},
|
||||
}
|
||||
39
__tests__/fixtures/purge-example.html
Normal file
39
__tests__/fixtures/purge-example.html
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<!-- Basic HTML -->
|
||||
<html>
|
||||
<head>
|
||||
<title>My Page</title>
|
||||
</head>
|
||||
<body id="tailwind">
|
||||
<main>
|
||||
<div class="bg-red-500 md:bg-blue-300 w-1/2"></div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<!-- Vue dynamic classes -->
|
||||
<span :class="{ block: enabled, 'md:flow-root': !enabled }"></span>
|
||||
|
||||
<!-- JSX with template strings -->
|
||||
<script>
|
||||
function Component() {
|
||||
return <div class={`h-screen`}></div>
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Custom classes with really weird characters -->
|
||||
<div class="min-h-(screen-4) bg-black! font-%#$@ w-(1/2+8)"></div>
|
||||
|
||||
<!-- Pug -->
|
||||
span.inline-grid.grid-cols-3(class="px-1.5")
|
||||
.col-span-2
|
||||
Hello
|
||||
.col-span-1.text-center
|
||||
World!
|
||||
|
||||
<!-- Haml -->
|
||||
.flow-root
|
||||
.text-green-700.bg-green-100
|
||||
.text-left= content
|
||||
%samp.font-mono{:data-foo => "bar"}= output
|
||||
.col-span-4[aria-hidden=true]
|
||||
.tracking-tight#headline
|
||||
12
__tests__/fixtures/tailwind-input-import.css
vendored
Normal file
12
__tests__/fixtures/tailwind-input-import.css
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
@import "tailwindcss/base";
|
||||
|
||||
@import 'tailwindcss/components';
|
||||
|
||||
@import "tailwindcss/utilities";
|
||||
|
||||
@responsive {
|
||||
.example {
|
||||
@apply .font-bold;
|
||||
color: theme('colors.red.500');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
@responsive {
|
||||
.example {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
@tailwind screens;
|
||||
|
||||
.john {
|
||||
content: "wick";
|
||||
}
|
||||
12
__tests__/fixtures/tailwind-input.css
Normal file
12
__tests__/fixtures/tailwind-input.css
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
@tailwind base;
|
||||
|
||||
@tailwind components;
|
||||
|
||||
@tailwind utilities;
|
||||
|
||||
@responsive {
|
||||
.example {
|
||||
@apply font-bold;
|
||||
color: theme('colors.red.500');
|
||||
}
|
||||
}
|
||||
207947
__tests__/fixtures/tailwind-output-flagged.css
Normal file
207947
__tests__/fixtures/tailwind-output-flagged.css
Normal file
File diff suppressed because it is too large
Load diff
52807
__tests__/fixtures/tailwind-output-ie11.css
Normal file
52807
__tests__/fixtures/tailwind-output-ie11.css
Normal file
File diff suppressed because it is too large
Load diff
91572
__tests__/fixtures/tailwind-output-important.css
Normal file
91572
__tests__/fixtures/tailwind-output-important.css
Normal file
File diff suppressed because it is too large
Load diff
79332
__tests__/fixtures/tailwind-output-no-color-opacity.css
Normal file
79332
__tests__/fixtures/tailwind-output-no-color-opacity.css
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,31 @@
|
|||
.example {
|
||||
color: red;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.sm\:example {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.md\:example {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.lg\:example {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.xl\:example {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
.john {
|
||||
content: "wick";
|
||||
}
|
||||
91572
__tests__/fixtures/tailwind-output.css
Normal file
91572
__tests__/fixtures/tailwind-output.css
Normal file
File diff suppressed because it is too large
Load diff
45
__tests__/flattenColorPalette.test.js
Normal file
45
__tests__/flattenColorPalette.test.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import flattenColorPalette from '../src/util/flattenColorPalette'
|
||||
|
||||
test('it flattens nested color objects', () => {
|
||||
expect(
|
||||
flattenColorPalette({
|
||||
purple: 'purple',
|
||||
white: {
|
||||
25: 'rgba(255,255,255,.25)',
|
||||
50: 'rgba(255,255,255,.5)',
|
||||
75: 'rgba(255,255,255,.75)',
|
||||
default: '#fff',
|
||||
},
|
||||
red: {
|
||||
1: 'rgb(33,0,0)',
|
||||
2: 'rgb(67,0,0)',
|
||||
3: 'rgb(100,0,0)',
|
||||
},
|
||||
green: {
|
||||
1: 'rgb(0,33,0)',
|
||||
2: 'rgb(0,67,0)',
|
||||
3: 'rgb(0,100,0)',
|
||||
},
|
||||
blue: {
|
||||
1: 'rgb(0,0,33)',
|
||||
2: 'rgb(0,0,67)',
|
||||
3: 'rgb(0,0,100)',
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
purple: 'purple',
|
||||
'white-25': 'rgba(255,255,255,.25)',
|
||||
'white-50': 'rgba(255,255,255,.5)',
|
||||
'white-75': 'rgba(255,255,255,.75)',
|
||||
white: '#fff',
|
||||
'red-1': 'rgb(33,0,0)',
|
||||
'red-2': 'rgb(67,0,0)',
|
||||
'red-3': 'rgb(100,0,0)',
|
||||
'green-1': 'rgb(0,33,0)',
|
||||
'green-2': 'rgb(0,67,0)',
|
||||
'green-3': 'rgb(0,100,0)',
|
||||
'blue-1': 'rgb(0,0,33)',
|
||||
'blue-2': 'rgb(0,0,67)',
|
||||
'blue-3': 'rgb(0,0,100)',
|
||||
})
|
||||
})
|
||||
93
__tests__/layerAtRule.test.js
Normal file
93
__tests__/layerAtRule.test.js
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import postcss from 'postcss'
|
||||
import tailwind from '../src/index'
|
||||
|
||||
function run(input, config = {}) {
|
||||
return postcss([tailwind({ corePlugins: [], ...config })]).process(input, { from: undefined })
|
||||
}
|
||||
|
||||
test('layers are grouped and inserted at the matching @tailwind rule', () => {
|
||||
const input = `
|
||||
@layer vanilla {
|
||||
strong { font-weight: medium }
|
||||
}
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer components {
|
||||
.btn { background: blue }
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.align-banana { text-align: banana }
|
||||
}
|
||||
|
||||
@layer base {
|
||||
h1 { font-weight: bold }
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.card { border-radius: 12px }
|
||||
}
|
||||
|
||||
@layer base {
|
||||
p { font-weight: normal }
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.align-sandwich { text-align: sandwich }
|
||||
}
|
||||
|
||||
@layer chocolate {
|
||||
a { text-decoration: underline }
|
||||
}
|
||||
`
|
||||
|
||||
const expected = `
|
||||
@layer vanilla {
|
||||
strong { font-weight: medium }
|
||||
}
|
||||
|
||||
body { margin: 0 }
|
||||
h1 { font-weight: bold }
|
||||
p { font-weight: normal }
|
||||
|
||||
.input { background: white }
|
||||
.btn { background: blue }
|
||||
.card { border-radius: 12px }
|
||||
|
||||
.float-squirrel { float: squirrel }
|
||||
.align-banana { text-align: banana }
|
||||
.align-sandwich { text-align: sandwich }
|
||||
|
||||
@layer chocolate {
|
||||
a { text-decoration: underline }
|
||||
}
|
||||
`
|
||||
|
||||
expect.assertions(2)
|
||||
|
||||
return run(input, {
|
||||
plugins: [
|
||||
function({ addBase, addComponents, addUtilities }) {
|
||||
addBase({
|
||||
body: {
|
||||
margin: 0,
|
||||
},
|
||||
})
|
||||
|
||||
addComponents({
|
||||
'.input': { background: 'white' },
|
||||
})
|
||||
|
||||
addUtilities({
|
||||
'.float-squirrel': { float: 'squirrel' },
|
||||
})
|
||||
},
|
||||
],
|
||||
}).then(result => {
|
||||
expect(result.css).toMatchCss(expected)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
14
__tests__/negateValue.test.js
Normal file
14
__tests__/negateValue.test.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import negateValue from '../src/util/negateValue'
|
||||
|
||||
test('it negates numeric CSS values', () => {
|
||||
expect(negateValue('5')).toEqual('-5')
|
||||
expect(negateValue('10px')).toEqual('-10px')
|
||||
expect(negateValue('18rem')).toEqual('-18rem')
|
||||
expect(negateValue('-10')).toEqual('10')
|
||||
expect(negateValue('-7ch')).toEqual('7ch')
|
||||
})
|
||||
|
||||
test('it leaves keywords untouched', () => {
|
||||
expect(negateValue('auto')).toEqual('auto')
|
||||
expect(negateValue('cover')).toEqual('cover')
|
||||
})
|
||||
302
__tests__/parseObjectStyles.test.js
Normal file
302
__tests__/parseObjectStyles.test.js
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
import parseObjectStyles from '../src/util/parseObjectStyles'
|
||||
import postcss from 'postcss'
|
||||
|
||||
function css(nodes) {
|
||||
return postcss.root({ nodes }).toString()
|
||||
}
|
||||
|
||||
test('it parses simple single class definitions', () => {
|
||||
const result = parseObjectStyles({
|
||||
'.foobar': {
|
||||
backgroundColor: 'red',
|
||||
color: 'white',
|
||||
padding: '1rem',
|
||||
},
|
||||
})
|
||||
|
||||
expect(css(result)).toMatchCss(`
|
||||
.foobar {
|
||||
background-color: red;
|
||||
color: white;
|
||||
padding: 1rem
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test('it parses multiple class definitions', () => {
|
||||
const result = parseObjectStyles({
|
||||
'.foo': {
|
||||
backgroundColor: 'red',
|
||||
color: 'white',
|
||||
padding: '1rem',
|
||||
},
|
||||
'.bar': {
|
||||
width: '200px',
|
||||
height: '100px',
|
||||
},
|
||||
})
|
||||
|
||||
expect(css(result)).toMatchCss(`
|
||||
.foo {
|
||||
background-color: red;
|
||||
color: white;
|
||||
padding: 1rem
|
||||
}
|
||||
.bar {
|
||||
width: 200px;
|
||||
height: 100px
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test('it parses nested pseudo-selectors', () => {
|
||||
const result = parseObjectStyles({
|
||||
'.foo': {
|
||||
backgroundColor: 'red',
|
||||
color: 'white',
|
||||
padding: '1rem',
|
||||
':hover': {
|
||||
backgroundColor: 'orange',
|
||||
},
|
||||
':focus': {
|
||||
backgroundColor: 'blue',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(css(result)).toMatchCss(`
|
||||
.foo {
|
||||
background-color: red;
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
}
|
||||
.foo:hover {
|
||||
background-color: orange;
|
||||
}
|
||||
.foo:focus {
|
||||
background-color: blue;
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test('it parses top-level media queries', () => {
|
||||
const result = parseObjectStyles({
|
||||
'@media (min-width: 200px)': {
|
||||
'.foo': {
|
||||
backgroundColor: 'orange',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(css(result)).toMatchCss(`
|
||||
@media (min-width: 200px) {
|
||||
.foo {
|
||||
background-color: orange
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test('it parses nested media queries', () => {
|
||||
const result = parseObjectStyles({
|
||||
'.foo': {
|
||||
backgroundColor: 'red',
|
||||
color: 'white',
|
||||
padding: '1rem',
|
||||
'@media (min-width: 200px)': {
|
||||
backgroundColor: 'orange',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(css(result)).toMatchCss(`
|
||||
.foo {
|
||||
background-color: red;
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
}
|
||||
@media (min-width: 200px) {
|
||||
.foo {
|
||||
background-color: orange;
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test('it bubbles nested screen rules', () => {
|
||||
const result = parseObjectStyles({
|
||||
'.foo': {
|
||||
backgroundColor: 'red',
|
||||
color: 'white',
|
||||
padding: '1rem',
|
||||
'@screen sm': {
|
||||
backgroundColor: 'orange',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(css(result)).toMatchCss(`
|
||||
.foo {
|
||||
background-color: red;
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
}
|
||||
@screen sm {
|
||||
.foo {
|
||||
background-color: orange;
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test('it parses pseudo-selectors in nested media queries', () => {
|
||||
const result = parseObjectStyles({
|
||||
'.foo': {
|
||||
backgroundColor: 'red',
|
||||
color: 'white',
|
||||
padding: '1rem',
|
||||
':hover': {
|
||||
'@media (min-width: 200px)': {
|
||||
backgroundColor: 'orange',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(css(result)).toMatchCss(`
|
||||
.foo {
|
||||
background-color: red;
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
}
|
||||
@media (min-width: 200px) {
|
||||
.foo:hover {
|
||||
background-color: orange;
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test('it parses descendant selectors', () => {
|
||||
const result = parseObjectStyles({
|
||||
'.foo': {
|
||||
backgroundColor: 'red',
|
||||
color: 'white',
|
||||
padding: '1rem',
|
||||
'.bar': {
|
||||
backgroundColor: 'orange',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(css(result)).toMatchCss(`
|
||||
.foo {
|
||||
background-color: red;
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
}
|
||||
.foo .bar {
|
||||
background-color: orange;
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test('it parses nested multi-class selectors', () => {
|
||||
const result = parseObjectStyles({
|
||||
'.foo': {
|
||||
backgroundColor: 'red',
|
||||
color: 'white',
|
||||
padding: '1rem',
|
||||
'&.bar': {
|
||||
backgroundColor: 'orange',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(css(result)).toMatchCss(`
|
||||
.foo {
|
||||
background-color: red;
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
}
|
||||
.foo.bar {
|
||||
background-color: orange;
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test('it parses nested multi-class selectors in media queries', () => {
|
||||
const result = parseObjectStyles({
|
||||
'.foo': {
|
||||
backgroundColor: 'red',
|
||||
color: 'white',
|
||||
padding: '1rem',
|
||||
'@media (min-width: 200px)': {
|
||||
'&.bar': {
|
||||
backgroundColor: 'orange',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(css(result)).toMatchCss(`
|
||||
.foo {
|
||||
background-color: red;
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
}
|
||||
@media (min-width: 200px) {
|
||||
.foo.bar {
|
||||
background-color: orange;
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test('it strips empty selectors when nesting', () => {
|
||||
const result = parseObjectStyles({
|
||||
'.foo': {
|
||||
'.bar': {
|
||||
backgroundColor: 'orange',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(css(result)).toMatchCss(`
|
||||
.foo .bar {
|
||||
background-color: orange
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test('it can parse an array of styles', () => {
|
||||
const result = parseObjectStyles([
|
||||
{
|
||||
'.foo': {
|
||||
backgroundColor: 'orange',
|
||||
},
|
||||
},
|
||||
{
|
||||
'.bar': {
|
||||
backgroundColor: 'red',
|
||||
},
|
||||
},
|
||||
{
|
||||
'.foo': {
|
||||
backgroundColor: 'blue',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
expect(css(result)).toMatchCss(`
|
||||
.foo {
|
||||
background-color: orange
|
||||
}
|
||||
.bar {
|
||||
background-color: red
|
||||
}
|
||||
.foo {
|
||||
background-color: blue
|
||||
}
|
||||
`)
|
||||
})
|
||||
63
__tests__/plugins/boxShadow.test.js
Normal file
63
__tests__/plugins/boxShadow.test.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import _ from 'lodash'
|
||||
import escapeClassName from '../../src/util/escapeClassName'
|
||||
import plugin from '../../src/plugins/boxShadow'
|
||||
|
||||
test('box shadow can use default keyword and negative prefix syntax', () => {
|
||||
const addedUtilities = []
|
||||
|
||||
const config = {
|
||||
theme: {
|
||||
boxShadow: {
|
||||
default: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
|
||||
md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
|
||||
'-': 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)',
|
||||
'-md': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
boxShadow: ['responsive'],
|
||||
},
|
||||
}
|
||||
|
||||
const getConfigValue = (path, defaultValue) => _.get(config, path, defaultValue)
|
||||
const pluginApi = {
|
||||
config: getConfigValue,
|
||||
e: escapeClassName,
|
||||
theme: (path, defaultValue) => getConfigValue(`theme.${path}`, defaultValue),
|
||||
variants: (path, defaultValue) => {
|
||||
if (_.isArray(config.variants)) {
|
||||
return config.variants
|
||||
}
|
||||
|
||||
return getConfigValue(`variants.${path}`, defaultValue)
|
||||
},
|
||||
addUtilities(utilities, variants) {
|
||||
addedUtilities.push({
|
||||
utilities,
|
||||
variants,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
plugin()(pluginApi)
|
||||
|
||||
expect(addedUtilities).toEqual([
|
||||
{
|
||||
utilities: {
|
||||
'.shadow': {
|
||||
'box-shadow': '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
|
||||
},
|
||||
'.shadow-md': {
|
||||
'box-shadow': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
|
||||
},
|
||||
'.-shadow': {
|
||||
'box-shadow': 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)',
|
||||
},
|
||||
'.-shadow-md': {
|
||||
'box-shadow': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
|
||||
},
|
||||
},
|
||||
variants: ['responsive'],
|
||||
},
|
||||
])
|
||||
})
|
||||
75
__tests__/plugins/divideWidth.test.js
Normal file
75
__tests__/plugins/divideWidth.test.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import invokePlugin from '../util/invokePlugin'
|
||||
import plugin from '../../src/plugins/divideWidth'
|
||||
|
||||
test('generating divide width utilities', () => {
|
||||
const config = {
|
||||
target: 'relaxed',
|
||||
theme: {
|
||||
divideWidth: {
|
||||
default: '1px',
|
||||
'0': '0',
|
||||
'2': '2px',
|
||||
'4': '4px',
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
divideWidth: ['responsive'],
|
||||
},
|
||||
}
|
||||
|
||||
const { utilities } = invokePlugin(plugin(), config)
|
||||
|
||||
expect(utilities).toEqual([
|
||||
[
|
||||
{
|
||||
'.divide-y > :not(template) ~ :not(template)': {
|
||||
'--divide-y-reverse': '0',
|
||||
'border-top-width': 'calc(1px * calc(1 - var(--divide-y-reverse)))',
|
||||
'border-bottom-width': 'calc(1px * var(--divide-y-reverse))',
|
||||
},
|
||||
'.divide-x > :not(template) ~ :not(template)': {
|
||||
'--divide-x-reverse': '0',
|
||||
'border-right-width': 'calc(1px * var(--divide-x-reverse))',
|
||||
'border-left-width': 'calc(1px * calc(1 - var(--divide-x-reverse)))',
|
||||
},
|
||||
'.divide-y-0 > :not(template) ~ :not(template)': {
|
||||
'--divide-y-reverse': '0',
|
||||
'border-top-width': 'calc(0px * calc(1 - var(--divide-y-reverse)))',
|
||||
'border-bottom-width': 'calc(0px * var(--divide-y-reverse))',
|
||||
},
|
||||
'.divide-x-0 > :not(template) ~ :not(template)': {
|
||||
'--divide-x-reverse': '0',
|
||||
'border-right-width': 'calc(0px * var(--divide-x-reverse))',
|
||||
'border-left-width': 'calc(0px * calc(1 - var(--divide-x-reverse)))',
|
||||
},
|
||||
'.divide-y-2 > :not(template) ~ :not(template)': {
|
||||
'--divide-y-reverse': '0',
|
||||
'border-top-width': 'calc(2px * calc(1 - var(--divide-y-reverse)))',
|
||||
'border-bottom-width': 'calc(2px * var(--divide-y-reverse))',
|
||||
},
|
||||
'.divide-x-2 > :not(template) ~ :not(template)': {
|
||||
'--divide-x-reverse': '0',
|
||||
'border-right-width': 'calc(2px * var(--divide-x-reverse))',
|
||||
'border-left-width': 'calc(2px * calc(1 - var(--divide-x-reverse)))',
|
||||
},
|
||||
'.divide-y-4 > :not(template) ~ :not(template)': {
|
||||
'--divide-y-reverse': '0',
|
||||
'border-top-width': 'calc(4px * calc(1 - var(--divide-y-reverse)))',
|
||||
'border-bottom-width': 'calc(4px * var(--divide-y-reverse))',
|
||||
},
|
||||
'.divide-x-4 > :not(template) ~ :not(template)': {
|
||||
'--divide-x-reverse': '0',
|
||||
'border-right-width': 'calc(4px * var(--divide-x-reverse))',
|
||||
'border-left-width': 'calc(4px * calc(1 - var(--divide-x-reverse)))',
|
||||
},
|
||||
'.divide-y-reverse > :not(template) ~ :not(template)': {
|
||||
'--divide-y-reverse': '1',
|
||||
},
|
||||
'.divide-x-reverse > :not(template) ~ :not(template)': {
|
||||
'--divide-x-reverse': '1',
|
||||
},
|
||||
},
|
||||
['responsive'],
|
||||
],
|
||||
])
|
||||
})
|
||||
86
__tests__/plugins/fontSize.test.js
Normal file
86
__tests__/plugins/fontSize.test.js
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import invokePlugin from '../util/invokePlugin'
|
||||
import plugin from '../../src/plugins/fontSize'
|
||||
|
||||
test('font-size utilities can include a default line-height', () => {
|
||||
const config = {
|
||||
theme: {
|
||||
fontSize: {
|
||||
sm: '12px',
|
||||
md: ['16px', '24px'],
|
||||
lg: ['20px', '28px'],
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
fontSize: ['responsive'],
|
||||
},
|
||||
}
|
||||
|
||||
const { utilities } = invokePlugin(plugin(), config)
|
||||
|
||||
expect(utilities).toEqual([
|
||||
[
|
||||
{
|
||||
'.text-sm': { 'font-size': '12px' },
|
||||
'.text-md': { 'font-size': '16px', 'line-height': '24px' },
|
||||
'.text-lg': { 'font-size': '20px', 'line-height': '28px' },
|
||||
},
|
||||
['responsive'],
|
||||
],
|
||||
])
|
||||
})
|
||||
|
||||
test('font-size utilities can include a default letter-spacing', () => {
|
||||
const config = {
|
||||
theme: {
|
||||
fontSize: {
|
||||
sm: '12px',
|
||||
md: ['16px', { letterSpacing: '-0.01em' }],
|
||||
lg: ['20px', { letterSpacing: '-0.02em' }],
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
fontSize: ['responsive'],
|
||||
},
|
||||
}
|
||||
|
||||
const { utilities } = invokePlugin(plugin(), config)
|
||||
|
||||
expect(utilities).toEqual([
|
||||
[
|
||||
{
|
||||
'.text-sm': { 'font-size': '12px' },
|
||||
'.text-md': { 'font-size': '16px', 'letter-spacing': '-0.01em' },
|
||||
'.text-lg': { 'font-size': '20px', 'letter-spacing': '-0.02em' },
|
||||
},
|
||||
['responsive'],
|
||||
],
|
||||
])
|
||||
})
|
||||
|
||||
test('font-size utilities can include a default line-height and letter-spacing', () => {
|
||||
const config = {
|
||||
theme: {
|
||||
fontSize: {
|
||||
sm: '12px',
|
||||
md: ['16px', { lineHeight: '24px', letterSpacing: '-0.01em' }],
|
||||
lg: ['20px', { lineHeight: '28px', letterSpacing: '-0.02em' }],
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
fontSize: ['responsive'],
|
||||
},
|
||||
}
|
||||
|
||||
const { utilities } = invokePlugin(plugin(), config)
|
||||
|
||||
expect(utilities).toEqual([
|
||||
[
|
||||
{
|
||||
'.text-sm': { 'font-size': '12px' },
|
||||
'.text-md': { 'font-size': '16px', 'line-height': '24px', 'letter-spacing': '-0.01em' },
|
||||
'.text-lg': { 'font-size': '20px', 'line-height': '28px', 'letter-spacing': '-0.02em' },
|
||||
},
|
||||
['responsive'],
|
||||
],
|
||||
])
|
||||
})
|
||||
57
__tests__/plugins/gradientColorStops.test.js
Normal file
57
__tests__/plugins/gradientColorStops.test.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import postcss from 'postcss'
|
||||
import tailwind from '../../src/index'
|
||||
|
||||
test('opacity variables are given to colors defined as closures', () => {
|
||||
return postcss([
|
||||
tailwind({
|
||||
theme: {
|
||||
colors: {
|
||||
primary: ({ opacityVariable, opacityValue }) => {
|
||||
if (opacityValue !== undefined) {
|
||||
return `rgba(31,31,31,${opacityValue})`
|
||||
}
|
||||
|
||||
if (opacityVariable !== undefined) {
|
||||
return `rgba(31,31,31,var(${opacityVariable},1))`
|
||||
}
|
||||
|
||||
return `rgb(31,31,31)`
|
||||
},
|
||||
},
|
||||
opacity: {
|
||||
'50': '0.5',
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
textColor: [],
|
||||
textOpacity: [],
|
||||
gradientColorStops: [],
|
||||
},
|
||||
corePlugins: ['textColor', 'textOpacity', 'gradientColorStops'],
|
||||
}),
|
||||
])
|
||||
.process('@tailwind utilities', { from: undefined })
|
||||
.then(result => {
|
||||
const expected = `
|
||||
.text-primary {
|
||||
color: rgba(31,31,31,var(--text-opacity,1))
|
||||
}
|
||||
.text-opacity-50 {
|
||||
--text-opacity: 0.5
|
||||
}
|
||||
.from-primary {
|
||||
--gradient-from-color: rgb(31,31,31);
|
||||
--gradient-color-stops: var(--gradient-from-color), var(--gradient-to-color, rgba(31, 31, 31, 0))
|
||||
}
|
||||
.via-primary {
|
||||
--gradient-via-color: rgb(31,31,31);
|
||||
--gradient-color-stops: var(--gradient-from-color), var(--gradient-via-color), var(--gradient-to-color, rgba(31, 31, 31, 0))
|
||||
}
|
||||
.to-primary {
|
||||
--gradient-to-color: rgb(31,31,31)
|
||||
}
|
||||
`
|
||||
|
||||
expect(result.css).toMatchCss(expected)
|
||||
})
|
||||
})
|
||||
51
__tests__/plugins/letterSpacing.test.js
Normal file
51
__tests__/plugins/letterSpacing.test.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import _ from 'lodash'
|
||||
import escapeClassName from '../../src/util/escapeClassName'
|
||||
import plugin from '../../src/plugins/letterSpacing'
|
||||
|
||||
test('letter spacing can use negative prefix syntax', () => {
|
||||
const addedUtilities = []
|
||||
|
||||
const config = {
|
||||
theme: {
|
||||
letterSpacing: {
|
||||
'-1': '-0.025em',
|
||||
'1': '0.025em',
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
letterSpacing: ['responsive'],
|
||||
},
|
||||
}
|
||||
|
||||
const getConfigValue = (path, defaultValue) => _.get(config, path, defaultValue)
|
||||
const pluginApi = {
|
||||
config: getConfigValue,
|
||||
e: escapeClassName,
|
||||
theme: (path, defaultValue) => getConfigValue(`theme.${path}`, defaultValue),
|
||||
variants: (path, defaultValue) => {
|
||||
if (_.isArray(config.variants)) {
|
||||
return config.variants
|
||||
}
|
||||
|
||||
return getConfigValue(`variants.${path}`, defaultValue)
|
||||
},
|
||||
addUtilities(utilities, variants) {
|
||||
addedUtilities.push({
|
||||
utilities,
|
||||
variants,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
plugin()(pluginApi)
|
||||
|
||||
expect(addedUtilities).toEqual([
|
||||
{
|
||||
utilities: {
|
||||
'.-tracking-1': { 'letter-spacing': '-0.025em' },
|
||||
'.tracking-1': { 'letter-spacing': '0.025em' },
|
||||
},
|
||||
variants: ['responsive'],
|
||||
},
|
||||
])
|
||||
})
|
||||
97
__tests__/plugins/space.test.js
Normal file
97
__tests__/plugins/space.test.js
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import invokePlugin from '../util/invokePlugin'
|
||||
import plugin from '../../src/plugins/space'
|
||||
|
||||
test('generating space utilities', () => {
|
||||
const config = {
|
||||
target: 'relaxed',
|
||||
theme: {
|
||||
space: {
|
||||
'0': '0',
|
||||
'1': '1px',
|
||||
'2': '2px',
|
||||
'4': '4px',
|
||||
'-2': '-2px',
|
||||
'-1': '-1px',
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
space: ['responsive'],
|
||||
},
|
||||
}
|
||||
|
||||
const { utilities } = invokePlugin(plugin(), config)
|
||||
|
||||
expect(utilities).toEqual([
|
||||
[
|
||||
{
|
||||
'.space-y-0 > :not(template) ~ :not(template)': {
|
||||
'--space-y-reverse': '0',
|
||||
'margin-top': 'calc(0px * calc(1 - var(--space-y-reverse)))',
|
||||
'margin-bottom': 'calc(0px * var(--space-y-reverse))',
|
||||
},
|
||||
'.space-x-0 > :not(template) ~ :not(template)': {
|
||||
'--space-x-reverse': '0',
|
||||
'margin-right': 'calc(0px * var(--space-x-reverse))',
|
||||
'margin-left': 'calc(0px * calc(1 - var(--space-x-reverse)))',
|
||||
},
|
||||
'.space-y-1 > :not(template) ~ :not(template)': {
|
||||
'--space-y-reverse': '0',
|
||||
'margin-top': 'calc(1px * calc(1 - var(--space-y-reverse)))',
|
||||
'margin-bottom': 'calc(1px * var(--space-y-reverse))',
|
||||
},
|
||||
'.space-x-1 > :not(template) ~ :not(template)': {
|
||||
'--space-x-reverse': '0',
|
||||
'margin-right': 'calc(1px * var(--space-x-reverse))',
|
||||
'margin-left': 'calc(1px * calc(1 - var(--space-x-reverse)))',
|
||||
},
|
||||
'.space-y-2 > :not(template) ~ :not(template)': {
|
||||
'--space-y-reverse': '0',
|
||||
'margin-top': 'calc(2px * calc(1 - var(--space-y-reverse)))',
|
||||
'margin-bottom': 'calc(2px * var(--space-y-reverse))',
|
||||
},
|
||||
'.space-x-2 > :not(template) ~ :not(template)': {
|
||||
'--space-x-reverse': '0',
|
||||
'margin-right': 'calc(2px * var(--space-x-reverse))',
|
||||
'margin-left': 'calc(2px * calc(1 - var(--space-x-reverse)))',
|
||||
},
|
||||
'.space-y-4 > :not(template) ~ :not(template)': {
|
||||
'--space-y-reverse': '0',
|
||||
'margin-top': 'calc(4px * calc(1 - var(--space-y-reverse)))',
|
||||
'margin-bottom': 'calc(4px * var(--space-y-reverse))',
|
||||
},
|
||||
'.space-x-4 > :not(template) ~ :not(template)': {
|
||||
'--space-x-reverse': '0',
|
||||
'margin-right': 'calc(4px * var(--space-x-reverse))',
|
||||
'margin-left': 'calc(4px * calc(1 - var(--space-x-reverse)))',
|
||||
},
|
||||
'.-space-y-2 > :not(template) ~ :not(template)': {
|
||||
'--space-y-reverse': '0',
|
||||
'margin-top': 'calc(-2px * calc(1 - var(--space-y-reverse)))',
|
||||
'margin-bottom': 'calc(-2px * var(--space-y-reverse))',
|
||||
},
|
||||
'.-space-x-2 > :not(template) ~ :not(template)': {
|
||||
'--space-x-reverse': '0',
|
||||
'margin-right': 'calc(-2px * var(--space-x-reverse))',
|
||||
'margin-left': 'calc(-2px * calc(1 - var(--space-x-reverse)))',
|
||||
},
|
||||
'.-space-y-1 > :not(template) ~ :not(template)': {
|
||||
'--space-y-reverse': '0',
|
||||
'margin-top': 'calc(-1px * calc(1 - var(--space-y-reverse)))',
|
||||
'margin-bottom': 'calc(-1px * var(--space-y-reverse))',
|
||||
},
|
||||
'.-space-x-1 > :not(template) ~ :not(template)': {
|
||||
'--space-x-reverse': '0',
|
||||
'margin-right': 'calc(-1px * var(--space-x-reverse))',
|
||||
'margin-left': 'calc(-1px * calc(1 - var(--space-x-reverse)))',
|
||||
},
|
||||
'.space-y-reverse > :not(template) ~ :not(template)': {
|
||||
'--space-y-reverse': '1',
|
||||
},
|
||||
'.space-x-reverse > :not(template) ~ :not(template)': {
|
||||
'--space-x-reverse': '1',
|
||||
},
|
||||
},
|
||||
['responsive'],
|
||||
],
|
||||
])
|
||||
})
|
||||
55
__tests__/plugins/zIndex.test.js
Normal file
55
__tests__/plugins/zIndex.test.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import _ from 'lodash'
|
||||
import escapeClassName from '../../src/util/escapeClassName'
|
||||
import plugin from '../../src/plugins/zIndex'
|
||||
|
||||
test('z index can use negative prefix syntax', () => {
|
||||
const addedUtilities = []
|
||||
|
||||
const config = {
|
||||
theme: {
|
||||
zIndex: {
|
||||
'-20': '-20',
|
||||
'-10': '-10',
|
||||
'10': '10',
|
||||
'20': '20',
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
zIndex: ['responsive'],
|
||||
},
|
||||
}
|
||||
|
||||
const getConfigValue = (path, defaultValue) => _.get(config, path, defaultValue)
|
||||
const pluginApi = {
|
||||
config: getConfigValue,
|
||||
e: escapeClassName,
|
||||
theme: (path, defaultValue) => getConfigValue(`theme.${path}`, defaultValue),
|
||||
variants: (path, defaultValue) => {
|
||||
if (_.isArray(config.variants)) {
|
||||
return config.variants
|
||||
}
|
||||
|
||||
return getConfigValue(`variants.${path}`, defaultValue)
|
||||
},
|
||||
addUtilities(utilities, variants) {
|
||||
addedUtilities.push({
|
||||
utilities,
|
||||
variants,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
plugin()(pluginApi)
|
||||
|
||||
expect(addedUtilities).toEqual([
|
||||
{
|
||||
utilities: {
|
||||
'.-z-20': { 'z-index': '-20' },
|
||||
'.-z-10': { 'z-index': '-10' },
|
||||
'.z-10': { 'z-index': '10' },
|
||||
'.z-20': { 'z-index': '20' },
|
||||
},
|
||||
variants: ['responsive'],
|
||||
},
|
||||
])
|
||||
})
|
||||
13
__tests__/prefixNegativeModifiers.test.js
Normal file
13
__tests__/prefixNegativeModifiers.test.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import prefixNegativeModifiers from '../src/util/prefixNegativeModifiers'
|
||||
|
||||
test('it does not prefix classes using standard syntax', () => {
|
||||
expect(prefixNegativeModifiers('base', 'modifier')).toEqual('base-modifier')
|
||||
})
|
||||
|
||||
test('it prefixes classes using negative syntax', () => {
|
||||
expect(prefixNegativeModifiers('base', '-modifier')).toEqual('-base-modifier')
|
||||
})
|
||||
|
||||
test('it prefixes classes and omits suffix using default negative syntax', () => {
|
||||
expect(prefixNegativeModifiers('base', '-')).toEqual('-base')
|
||||
})
|
||||
26
__tests__/prefixSelector.test.js
Normal file
26
__tests__/prefixSelector.test.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import prefix from '../src/util/prefixSelector'
|
||||
|
||||
test('it prefixes classes with the provided prefix', () => {
|
||||
expect(prefix('tw-', '.foo')).toEqual('.tw-foo')
|
||||
})
|
||||
|
||||
test('it handles a function as the prefix', () => {
|
||||
const prefixFunc = selector => {
|
||||
return selector === '.foo' ? 'tw-' : ''
|
||||
}
|
||||
|
||||
expect(prefix(prefixFunc, '.foo')).toEqual('.tw-foo')
|
||||
expect(prefix(prefixFunc, '.bar')).toEqual('.bar')
|
||||
})
|
||||
|
||||
test('it properly prefixes selectors with non-standard characters', () => {
|
||||
expect(prefix('tw-', '.hello\\:world')).toEqual('.tw-hello\\:world')
|
||||
expect(prefix('tw-', '.foo\\/bar')).toEqual('.tw-foo\\/bar')
|
||||
expect(prefix('tw-', '.wew\\.lad')).toEqual('.tw-wew\\.lad')
|
||||
})
|
||||
|
||||
test('it prefixes all classes in a selector', () => {
|
||||
expect(prefix('tw-', '.btn-blue .w-1\\/4 > h1.text-xl + a .bar')).toEqual(
|
||||
'.tw-btn-blue .tw-w-1\\/4 > h1.tw-text-xl + a .tw-bar'
|
||||
)
|
||||
})
|
||||
1948
__tests__/processPlugins.test.js
Normal file
1948
__tests__/processPlugins.test.js
Normal file
File diff suppressed because it is too large
Load diff
740
__tests__/purgeUnusedStyles.test.js
Normal file
740
__tests__/purgeUnusedStyles.test.js
Normal file
|
|
@ -0,0 +1,740 @@
|
|||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import postcss from 'postcss'
|
||||
import tailwind from '../src/index'
|
||||
import defaultConfig from '../stubs/defaultConfig.stub.js'
|
||||
|
||||
function suppressConsoleLogs(cb, type = 'warn') {
|
||||
return () => {
|
||||
const spy = jest.spyOn(global.console, type).mockImplementation(jest.fn())
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
Promise.resolve(cb()).then(resolve, reject)
|
||||
})
|
||||
|
||||
promise.then(spy.mockRestore, spy.mockRestore)
|
||||
|
||||
return promise
|
||||
}
|
||||
}
|
||||
|
||||
function extractRules(root) {
|
||||
let rules = []
|
||||
|
||||
root.walkRules(r => {
|
||||
rules = rules.concat(r.selectors)
|
||||
})
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
async function inProduction(callback) {
|
||||
const OLD_NODE_ENV = process.env.NODE_ENV
|
||||
process.env.NODE_ENV = 'production'
|
||||
|
||||
const result = await callback()
|
||||
process.env.NODE_ENV = OLD_NODE_ENV
|
||||
return result
|
||||
}
|
||||
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'black!': '#000',
|
||||
},
|
||||
spacing: {
|
||||
'1.5': '0.375rem',
|
||||
'(1/2+8)': 'calc(50% + 2rem)',
|
||||
},
|
||||
minHeight: {
|
||||
'(screen-4)': 'calc(100vh - 1rem)',
|
||||
},
|
||||
fontFamily: {
|
||||
'%#$@': 'Comic Sans',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
function assertPurged(result) {
|
||||
expect(result.css).not.toContain('.bg-red-600')
|
||||
expect(result.css).not.toContain('.w-1\\/3')
|
||||
expect(result.css).not.toContain('.flex')
|
||||
expect(result.css).not.toContain('.font-sans')
|
||||
expect(result.css).not.toContain('.text-right')
|
||||
expect(result.css).not.toContain('.px-4')
|
||||
expect(result.css).not.toContain('.h-full')
|
||||
|
||||
expect(result.css).toContain('.bg-red-500')
|
||||
expect(result.css).toContain('.md\\:bg-blue-300')
|
||||
expect(result.css).toContain('.w-1\\/2')
|
||||
expect(result.css).toContain('.block')
|
||||
expect(result.css).toContain('.md\\:flow-root')
|
||||
expect(result.css).toContain('.h-screen')
|
||||
expect(result.css).toContain('.min-h-\\(screen-4\\)')
|
||||
expect(result.css).toContain('.bg-black\\!')
|
||||
expect(result.css).toContain('.font-\\%\\#\\$\\@')
|
||||
expect(result.css).toContain('.w-\\(1\\/2\\+8\\)')
|
||||
expect(result.css).toContain('.inline-grid')
|
||||
expect(result.css).toContain('.grid-cols-3')
|
||||
expect(result.css).toContain('.px-1\\.5')
|
||||
expect(result.css).toContain('.col-span-2')
|
||||
expect(result.css).toContain('.col-span-1')
|
||||
expect(result.css).toContain('.text-center')
|
||||
expect(result.css).toContain('.flow-root')
|
||||
expect(result.css).toContain('.text-green-700')
|
||||
expect(result.css).toContain('.bg-green-100')
|
||||
expect(result.css).toContain('.text-left')
|
||||
expect(result.css).toContain('.font-mono')
|
||||
expect(result.css).toContain('.col-span-4')
|
||||
expect(result.css).toContain('.tracking-tight')
|
||||
expect(result.css).toContain('.tracking-tight')
|
||||
}
|
||||
|
||||
test('purges unused classes', () => {
|
||||
return inProduction(
|
||||
suppressConsoleLogs(() => {
|
||||
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
|
||||
const input = fs.readFileSync(inputPath, 'utf8')
|
||||
|
||||
return postcss([
|
||||
tailwind({
|
||||
...config,
|
||||
purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
|
||||
}),
|
||||
])
|
||||
.process(input, { from: inputPath })
|
||||
.then(result => {
|
||||
assertPurged(result)
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('custom css is not purged by default', () => {
|
||||
return inProduction(
|
||||
suppressConsoleLogs(() => {
|
||||
return postcss([
|
||||
tailwind({
|
||||
...config,
|
||||
purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
|
||||
}),
|
||||
])
|
||||
.process(
|
||||
`
|
||||
@tailwind base;
|
||||
|
||||
@tailwind components;
|
||||
|
||||
@tailwind utilities;
|
||||
|
||||
.example {
|
||||
@apply font-bold;
|
||||
color: theme('colors.red.500');
|
||||
}
|
||||
`,
|
||||
{ from: null }
|
||||
)
|
||||
.then(result => {
|
||||
const rules = extractRules(result.root)
|
||||
assertPurged(result)
|
||||
expect(rules).toContain('.example')
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('custom css that uses @responsive is not purged by default', () => {
|
||||
return inProduction(
|
||||
suppressConsoleLogs(() => {
|
||||
return postcss([
|
||||
tailwind({
|
||||
...config,
|
||||
purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
|
||||
}),
|
||||
])
|
||||
.process(
|
||||
`
|
||||
@tailwind base;
|
||||
|
||||
@tailwind components;
|
||||
|
||||
@tailwind utilities;
|
||||
|
||||
@responsive {
|
||||
.example {
|
||||
@apply font-bold;
|
||||
color: theme('colors.red.500');
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ from: null }
|
||||
)
|
||||
.then(result => {
|
||||
const rules = extractRules(result.root)
|
||||
assertPurged(result)
|
||||
expect(rules).toContain('.example')
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('custom css in a layer is purged by default when using layers mode', () => {
|
||||
return inProduction(
|
||||
suppressConsoleLogs(() => {
|
||||
return postcss([
|
||||
tailwind({
|
||||
...config,
|
||||
future: {
|
||||
purgeLayersByDefault: true,
|
||||
},
|
||||
purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
|
||||
}),
|
||||
])
|
||||
.process(
|
||||
`
|
||||
@tailwind base;
|
||||
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.example {
|
||||
@apply font-bold;
|
||||
color: theme('colors.red.500');
|
||||
}
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`,
|
||||
{ from: null }
|
||||
)
|
||||
.then(result => {
|
||||
const rules = extractRules(result.root)
|
||||
assertPurged(result)
|
||||
expect(rules).not.toContain('.example')
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('custom css in a layer in a @responsive at-rule is purged by default', () => {
|
||||
return inProduction(
|
||||
suppressConsoleLogs(() => {
|
||||
return postcss([
|
||||
tailwind({
|
||||
...config,
|
||||
future: {
|
||||
purgeLayersByDefault: true,
|
||||
},
|
||||
purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
|
||||
}),
|
||||
])
|
||||
.process(
|
||||
`
|
||||
@tailwind base;
|
||||
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
@responsive {
|
||||
.example {
|
||||
@apply font-bold;
|
||||
color: theme('colors.red.500');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`,
|
||||
{ from: null }
|
||||
)
|
||||
.then(result => {
|
||||
const rules = extractRules(result.root)
|
||||
assertPurged(result)
|
||||
expect(rules).not.toContain('.example')
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('purges unused classes with important string', () => {
|
||||
return inProduction(
|
||||
suppressConsoleLogs(() => {
|
||||
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
|
||||
const input = fs.readFileSync(inputPath, 'utf8')
|
||||
|
||||
return postcss([
|
||||
tailwind({
|
||||
...config,
|
||||
important: '#tailwind',
|
||||
purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
|
||||
}),
|
||||
])
|
||||
.process(input, { from: inputPath })
|
||||
.then(result => {
|
||||
assertPurged(result)
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('mode must be a valid value', () => {
|
||||
return inProduction(
|
||||
suppressConsoleLogs(() => {
|
||||
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
|
||||
const input = fs.readFileSync(inputPath, 'utf8')
|
||||
|
||||
return expect(
|
||||
postcss([
|
||||
tailwind({
|
||||
...config,
|
||||
purge: {
|
||||
mode: 'poop',
|
||||
content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
|
||||
},
|
||||
}),
|
||||
]).process(input, { from: inputPath })
|
||||
).rejects.toThrow()
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('components are purged by default in layers mode', () => {
|
||||
return inProduction(
|
||||
suppressConsoleLogs(() => {
|
||||
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
|
||||
const input = fs.readFileSync(inputPath, 'utf8')
|
||||
|
||||
return postcss([
|
||||
tailwind({
|
||||
...config,
|
||||
future: {
|
||||
purgeLayersByDefault: true,
|
||||
},
|
||||
purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
|
||||
}),
|
||||
])
|
||||
.process(input, { from: inputPath })
|
||||
.then(result => {
|
||||
expect(result.css).not.toContain('.container')
|
||||
assertPurged(result)
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('you can specify which layers to purge', () => {
|
||||
return inProduction(
|
||||
suppressConsoleLogs(() => {
|
||||
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
|
||||
const input = fs.readFileSync(inputPath, 'utf8')
|
||||
|
||||
return postcss([
|
||||
tailwind({
|
||||
...config,
|
||||
future: {
|
||||
purgeLayersByDefault: true,
|
||||
},
|
||||
purge: {
|
||||
mode: 'layers',
|
||||
layers: ['utilities'],
|
||||
content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
|
||||
},
|
||||
}),
|
||||
])
|
||||
.process(input, { from: inputPath })
|
||||
.then(result => {
|
||||
const rules = extractRules(result.root)
|
||||
expect(rules).toContain('optgroup')
|
||||
expect(rules).toContain('.container')
|
||||
assertPurged(result)
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('you can purge just base and component layers (but why)', () => {
|
||||
return inProduction(
|
||||
suppressConsoleLogs(() => {
|
||||
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
|
||||
const input = fs.readFileSync(inputPath, 'utf8')
|
||||
|
||||
return postcss([
|
||||
tailwind({
|
||||
...config,
|
||||
future: {
|
||||
purgeLayersByDefault: true,
|
||||
},
|
||||
purge: {
|
||||
mode: 'layers',
|
||||
layers: ['base', 'components'],
|
||||
content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
|
||||
},
|
||||
}),
|
||||
])
|
||||
.process(input, { from: inputPath })
|
||||
.then(result => {
|
||||
const rules = extractRules(result.root)
|
||||
expect(rules).not.toContain('[type="checkbox"]')
|
||||
expect(rules).not.toContain('.container')
|
||||
expect(rules).toContain('.float-left')
|
||||
expect(rules).toContain('.md\\:bg-red-500')
|
||||
expect(rules).toContain('.lg\\:appearance-none')
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('does not purge components when mode is conservative', () => {
|
||||
return inProduction(
|
||||
suppressConsoleLogs(() => {
|
||||
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
|
||||
const input = fs.readFileSync(inputPath, 'utf8')
|
||||
|
||||
return postcss([
|
||||
tailwind({
|
||||
...config,
|
||||
purge: {
|
||||
mode: 'conservative',
|
||||
content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
|
||||
},
|
||||
}),
|
||||
])
|
||||
.process(input, { from: inputPath })
|
||||
.then(result => {
|
||||
expect(result.css).toContain('.container')
|
||||
assertPurged(result)
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test(
|
||||
'does not purge except in production',
|
||||
suppressConsoleLogs(() => {
|
||||
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
|
||||
const input = fs.readFileSync(inputPath, 'utf8')
|
||||
|
||||
return postcss([
|
||||
tailwind({
|
||||
...defaultConfig,
|
||||
purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
|
||||
}),
|
||||
])
|
||||
.process(input, { from: inputPath })
|
||||
.then(result => {
|
||||
const expected = fs.readFileSync(
|
||||
path.resolve(`${__dirname}/fixtures/tailwind-output.css`),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
expect(result.css).toBe(expected)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
test('does not purge if the array is empty', () => {
|
||||
return inProduction(
|
||||
suppressConsoleLogs(() => {
|
||||
const OLD_NODE_ENV = process.env.NODE_ENV
|
||||
process.env.NODE_ENV = 'production'
|
||||
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
|
||||
const input = fs.readFileSync(inputPath, 'utf8')
|
||||
|
||||
return postcss([
|
||||
tailwind({
|
||||
...defaultConfig,
|
||||
purge: [],
|
||||
}),
|
||||
])
|
||||
.process(input, { from: inputPath })
|
||||
.then(result => {
|
||||
process.env.NODE_ENV = OLD_NODE_ENV
|
||||
const expected = fs.readFileSync(
|
||||
path.resolve(`${__dirname}/fixtures/tailwind-output.css`),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
expect(result.css).toBe(expected)
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('does not purge if explicitly disabled', () => {
|
||||
return inProduction(
|
||||
suppressConsoleLogs(() => {
|
||||
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
|
||||
const input = fs.readFileSync(inputPath, 'utf8')
|
||||
|
||||
return postcss([
|
||||
tailwind({
|
||||
...defaultConfig,
|
||||
purge: { enabled: false },
|
||||
}),
|
||||
])
|
||||
.process(input, { from: inputPath })
|
||||
.then(result => {
|
||||
const expected = fs.readFileSync(
|
||||
path.resolve(`${__dirname}/fixtures/tailwind-output.css`),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
expect(result.css).toBe(expected)
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('does not purge if purge is simply false', () => {
|
||||
return inProduction(
|
||||
suppressConsoleLogs(() => {
|
||||
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
|
||||
const input = fs.readFileSync(inputPath, 'utf8')
|
||||
|
||||
return postcss([
|
||||
tailwind({
|
||||
...defaultConfig,
|
||||
purge: false,
|
||||
}),
|
||||
])
|
||||
.process(input, { from: inputPath })
|
||||
.then(result => {
|
||||
const expected = fs.readFileSync(
|
||||
path.resolve(`${__dirname}/fixtures/tailwind-output.css`),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
expect(result.css).toBe(expected)
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('purges outside of production if explicitly enabled', () => {
|
||||
return inProduction(
|
||||
suppressConsoleLogs(() => {
|
||||
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
|
||||
const input = fs.readFileSync(inputPath, 'utf8')
|
||||
|
||||
return postcss([
|
||||
tailwind({
|
||||
...config,
|
||||
purge: { enabled: true, content: [path.resolve(`${__dirname}/fixtures/**/*.html`)] },
|
||||
}),
|
||||
])
|
||||
.process(input, { from: inputPath })
|
||||
.then(result => {
|
||||
assertPurged(result)
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test(
|
||||
'purgecss options can be provided',
|
||||
suppressConsoleLogs(() => {
|
||||
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
|
||||
const input = fs.readFileSync(inputPath, 'utf8')
|
||||
|
||||
return postcss([
|
||||
tailwind({
|
||||
...config,
|
||||
purge: {
|
||||
enabled: true,
|
||||
options: {
|
||||
content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
|
||||
whitelist: ['md:bg-green-500'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
])
|
||||
.process(input, { from: inputPath })
|
||||
.then(result => {
|
||||
expect(result.css).toContain('.md\\:bg-green-500')
|
||||
assertPurged(result)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
test(
|
||||
'can purge all CSS, not just Tailwind classes',
|
||||
suppressConsoleLogs(() => {
|
||||
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
|
||||
const input = fs.readFileSync(inputPath, 'utf8')
|
||||
|
||||
return postcss([
|
||||
tailwind({
|
||||
...config,
|
||||
purge: {
|
||||
enabled: true,
|
||||
mode: 'all',
|
||||
content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
|
||||
},
|
||||
}),
|
||||
function(css) {
|
||||
// Remove any comments to avoid accidentally asserting against them
|
||||
// instead of against real CSS rules.
|
||||
css.walkComments(c => c.remove())
|
||||
},
|
||||
])
|
||||
.process(input, { from: inputPath })
|
||||
.then(result => {
|
||||
expect(result.css).toContain('html')
|
||||
expect(result.css).toContain('body')
|
||||
expect(result.css).toContain('samp')
|
||||
expect(result.css).not.toContain('.example')
|
||||
expect(result.css).not.toContain('.sm\\:example')
|
||||
|
||||
assertPurged(result)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
test('the `conservative` mode can be set explicitly', () => {
|
||||
return inProduction(
|
||||
suppressConsoleLogs(() => {
|
||||
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
|
||||
const input = fs.readFileSync(inputPath, 'utf8')
|
||||
|
||||
return postcss([
|
||||
tailwind({
|
||||
...config,
|
||||
purge: {
|
||||
mode: 'conservative',
|
||||
content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
|
||||
},
|
||||
}),
|
||||
])
|
||||
.process(input, { from: inputPath })
|
||||
.then(result => {
|
||||
expect(result.css).not.toContain('.bg-red-600')
|
||||
expect(result.css).not.toContain('.w-1\\/3')
|
||||
expect(result.css).not.toContain('.flex')
|
||||
expect(result.css).not.toContain('.font-sans')
|
||||
expect(result.css).not.toContain('.text-right')
|
||||
expect(result.css).not.toContain('.px-4')
|
||||
expect(result.css).not.toContain('.h-full')
|
||||
|
||||
expect(result.css).toContain('.bg-red-500')
|
||||
expect(result.css).toContain('.md\\:bg-blue-300')
|
||||
expect(result.css).toContain('.w-1\\/2')
|
||||
expect(result.css).toContain('.block')
|
||||
expect(result.css).toContain('.md\\:flow-root')
|
||||
expect(result.css).toContain('.h-screen')
|
||||
expect(result.css).toContain('.min-h-\\(screen-4\\)')
|
||||
expect(result.css).toContain('.bg-black\\!')
|
||||
expect(result.css).toContain('.font-\\%\\#\\$\\@')
|
||||
expect(result.css).toContain('.w-\\(1\\/2\\+8\\)')
|
||||
expect(result.css).toContain('.inline-grid')
|
||||
expect(result.css).toContain('.grid-cols-3')
|
||||
expect(result.css).toContain('.px-1\\.5')
|
||||
expect(result.css).toContain('.col-span-2')
|
||||
expect(result.css).toContain('.col-span-1')
|
||||
expect(result.css).toContain('.text-center')
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('element selectors are preserved by default', () => {
|
||||
return inProduction(
|
||||
suppressConsoleLogs(() => {
|
||||
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
|
||||
const input = fs.readFileSync(inputPath, 'utf8')
|
||||
|
||||
return postcss([
|
||||
tailwind({
|
||||
...config,
|
||||
purge: {
|
||||
content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
|
||||
mode: 'all',
|
||||
},
|
||||
}),
|
||||
])
|
||||
.process(input, { from: inputPath })
|
||||
.then(result => {
|
||||
const rules = extractRules(result.root)
|
||||
;[
|
||||
'a',
|
||||
'blockquote',
|
||||
'body',
|
||||
'code',
|
||||
'fieldset',
|
||||
'figure',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'hr',
|
||||
'html',
|
||||
'img',
|
||||
'kbd',
|
||||
'ol',
|
||||
'p',
|
||||
'pre',
|
||||
'strong',
|
||||
'sup',
|
||||
'table',
|
||||
'ul',
|
||||
].forEach(e => expect(rules).toContain(e))
|
||||
|
||||
assertPurged(result)
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('preserving element selectors can be disabled', () => {
|
||||
return inProduction(
|
||||
suppressConsoleLogs(() => {
|
||||
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
|
||||
const input = fs.readFileSync(inputPath, 'utf8')
|
||||
|
||||
return postcss([
|
||||
tailwind({
|
||||
...config,
|
||||
purge: {
|
||||
content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
|
||||
mode: 'all',
|
||||
preserveHtmlElements: false,
|
||||
},
|
||||
}),
|
||||
])
|
||||
.process(input, { from: inputPath })
|
||||
.then(result => {
|
||||
const rules = extractRules(result.root)
|
||||
|
||||
;[
|
||||
'blockquote',
|
||||
'code',
|
||||
'em',
|
||||
'fieldset',
|
||||
'figure',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'hr',
|
||||
'img',
|
||||
'kbd',
|
||||
'li',
|
||||
'ol',
|
||||
'pre',
|
||||
'strong',
|
||||
'sup',
|
||||
'table',
|
||||
'ul',
|
||||
].forEach(e => expect(rules).not.toContain(e))
|
||||
|
||||
assertPurged(result)
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
1800
__tests__/resolveConfig.test.js
Normal file
1800
__tests__/resolveConfig.test.js
Normal file
File diff suppressed because it is too large
Load diff
440
__tests__/responsiveAtRule.test.js
Normal file
440
__tests__/responsiveAtRule.test.js
Normal file
|
|
@ -0,0 +1,440 @@
|
|||
import postcss from 'postcss'
|
||||
import plugin from '../src/lib/substituteResponsiveAtRules'
|
||||
import config from '../stubs/defaultConfig.stub.js'
|
||||
|
||||
function run(input, opts = config) {
|
||||
return postcss([plugin(opts)]).process(input, { from: undefined })
|
||||
}
|
||||
|
||||
test('it can generate responsive variants', () => {
|
||||
const input = `
|
||||
@responsive {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
|
||||
@tailwind screens;
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
@media (min-width: 500px) {
|
||||
.sm\\:banana { color: yellow; }
|
||||
.sm\\:chocolate { color: brown; }
|
||||
}
|
||||
@media (min-width: 750px) {
|
||||
.md\\:banana { color: yellow; }
|
||||
.md\\:chocolate { color: brown; }
|
||||
}
|
||||
@media (min-width: 1000px) {
|
||||
.lg\\:banana { color: yellow; }
|
||||
.lg\\:chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
theme: {
|
||||
screens: {
|
||||
sm: '500px',
|
||||
md: '750px',
|
||||
lg: '1000px',
|
||||
},
|
||||
},
|
||||
separator: ':',
|
||||
}).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it can generate responsive variants with a custom separator', () => {
|
||||
const input = `
|
||||
@responsive {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
|
||||
@tailwind screens;
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
@media (min-width: 500px) {
|
||||
.sm__banana { color: yellow; }
|
||||
.sm__chocolate { color: brown; }
|
||||
}
|
||||
@media (min-width: 750px) {
|
||||
.md__banana { color: yellow; }
|
||||
.md__chocolate { color: brown; }
|
||||
}
|
||||
@media (min-width: 1000px) {
|
||||
.lg__banana { color: yellow; }
|
||||
.lg__chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
theme: {
|
||||
screens: {
|
||||
sm: '500px',
|
||||
md: '750px',
|
||||
lg: '1000px',
|
||||
},
|
||||
},
|
||||
separator: '__',
|
||||
}).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it can generate responsive variants when classes have non-standard characters', () => {
|
||||
const input = `
|
||||
@responsive {
|
||||
.hover\\:banana { color: yellow; }
|
||||
.chocolate-2\\.5 { color: brown; }
|
||||
.group:hover .group-hover\\:toast { color: black; }
|
||||
}
|
||||
|
||||
@tailwind screens;
|
||||
`
|
||||
|
||||
const output = `
|
||||
.hover\\:banana { color: yellow; }
|
||||
.chocolate-2\\.5 { color: brown; }
|
||||
.group:hover .group-hover\\:toast { color: black; }
|
||||
@media (min-width: 500px) {
|
||||
.sm\\:hover\\:banana { color: yellow; }
|
||||
.sm\\:chocolate-2\\.5 { color: brown; }
|
||||
.group:hover .sm\\:group-hover\\:toast { color: black; }
|
||||
}
|
||||
@media (min-width: 750px) {
|
||||
.md\\:hover\\:banana { color: yellow; }
|
||||
.md\\:chocolate-2\\.5 { color: brown; }
|
||||
.group:hover .md\\:group-hover\\:toast { color: black; }
|
||||
}
|
||||
@media (min-width: 1000px) {
|
||||
.lg\\:hover\\:banana { color: yellow; }
|
||||
.lg\\:chocolate-2\\.5 { color: brown; }
|
||||
.group:hover .lg\\:group-hover\\:toast { color: black; }
|
||||
}
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
theme: {
|
||||
screens: {
|
||||
sm: '500px',
|
||||
md: '750px',
|
||||
lg: '1000px',
|
||||
},
|
||||
},
|
||||
separator: ':',
|
||||
}).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('responsive variants are grouped', () => {
|
||||
const input = `
|
||||
@responsive {
|
||||
.banana { color: yellow; }
|
||||
}
|
||||
|
||||
.apple { color: red; }
|
||||
|
||||
@responsive {
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
|
||||
@tailwind screens;
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.apple { color: red; }
|
||||
.chocolate { color: brown; }
|
||||
@media (min-width: 500px) {
|
||||
.sm\\:banana { color: yellow; }
|
||||
.sm\\:chocolate { color: brown; }
|
||||
}
|
||||
@media (min-width: 750px) {
|
||||
.md\\:banana { color: yellow; }
|
||||
.md\\:chocolate { color: brown; }
|
||||
}
|
||||
@media (min-width: 1000px) {
|
||||
.lg\\:banana { color: yellow; }
|
||||
.lg\\:chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
theme: {
|
||||
screens: {
|
||||
sm: '500px',
|
||||
md: '750px',
|
||||
lg: '1000px',
|
||||
},
|
||||
},
|
||||
separator: ':',
|
||||
}).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it can generate responsive variants for nested at-rules', () => {
|
||||
const input = `
|
||||
@responsive {
|
||||
.banana { color: yellow; }
|
||||
|
||||
@supports(display: grid) {
|
||||
.grid\\:banana { color: blue; }
|
||||
}
|
||||
}
|
||||
|
||||
@tailwind screens;
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
@supports(display: grid) {
|
||||
.grid\\:banana {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 500px) {
|
||||
.sm\\:banana {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
@supports(display: grid) {
|
||||
.sm\\:grid\\:banana {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
.lg\\:banana {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
@supports(display: grid) {
|
||||
.lg\\:grid\\:banana {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
theme: {
|
||||
screens: {
|
||||
sm: '500px',
|
||||
lg: '1000px',
|
||||
},
|
||||
},
|
||||
separator: ':',
|
||||
}).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it can generate responsive variants for deeply nested at-rules', () => {
|
||||
const input = `
|
||||
@responsive {
|
||||
.banana { color: yellow; }
|
||||
|
||||
@supports(display: grid) {
|
||||
@supports(display: flex) {
|
||||
.flex-grid\\:banana { color: blue; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@tailwind screens;
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
@supports(display: grid) {
|
||||
@supports(display: flex) {
|
||||
.flex-grid\\:banana {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 500px) {
|
||||
.sm\\:banana {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
@supports(display: grid) {
|
||||
@supports(display: flex) {
|
||||
.sm\\:flex-grid\\:banana {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
.lg\\:banana {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
@supports(display: grid) {
|
||||
@supports(display: flex) {
|
||||
.lg\\:flex-grid\\:banana {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
theme: {
|
||||
screens: {
|
||||
sm: '500px',
|
||||
lg: '1000px',
|
||||
},
|
||||
},
|
||||
separator: ':',
|
||||
}).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('screen prefix is only applied to the last class in a selector', () => {
|
||||
const input = `
|
||||
@responsive {
|
||||
.banana li * .sandwich #foo > div { color: yellow; }
|
||||
}
|
||||
|
||||
@tailwind screens;
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana li * .sandwich #foo > div { color: yellow; }
|
||||
@media (min-width: 500px) {
|
||||
.banana li * .sm\\:sandwich #foo > div { color: yellow; }
|
||||
}
|
||||
@media (min-width: 750px) {
|
||||
.banana li * .md\\:sandwich #foo > div { color: yellow; }
|
||||
}
|
||||
@media (min-width: 1000px) {
|
||||
.banana li * .lg\\:sandwich #foo > div { color: yellow; }
|
||||
}
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
theme: {
|
||||
screens: {
|
||||
sm: '500px',
|
||||
md: '750px',
|
||||
lg: '1000px',
|
||||
},
|
||||
},
|
||||
separator: ':',
|
||||
}).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('responsive variants are generated for all selectors in a rule', () => {
|
||||
const input = `
|
||||
@responsive {
|
||||
.foo, .bar { color: yellow; }
|
||||
}
|
||||
|
||||
@tailwind screens;
|
||||
`
|
||||
|
||||
const output = `
|
||||
.foo, .bar { color: yellow; }
|
||||
@media (min-width: 500px) {
|
||||
.sm\\:foo, .sm\\:bar { color: yellow; }
|
||||
}
|
||||
@media (min-width: 750px) {
|
||||
.md\\:foo, .md\\:bar { color: yellow; }
|
||||
}
|
||||
@media (min-width: 1000px) {
|
||||
.lg\\:foo, .lg\\:bar { color: yellow; }
|
||||
}
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
theme: {
|
||||
screens: {
|
||||
sm: '500px',
|
||||
md: '750px',
|
||||
lg: '1000px',
|
||||
},
|
||||
},
|
||||
separator: ':',
|
||||
}).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('selectors with no classes cannot be made responsive', () => {
|
||||
const input = `
|
||||
@responsive {
|
||||
div { color: yellow; }
|
||||
}
|
||||
|
||||
@tailwind screens;
|
||||
`
|
||||
expect.assertions(1)
|
||||
return run(input, {
|
||||
theme: {
|
||||
screens: {
|
||||
sm: '500px',
|
||||
md: '750px',
|
||||
lg: '1000px',
|
||||
},
|
||||
},
|
||||
separator: ':',
|
||||
}).catch(e => {
|
||||
expect(e).toMatchObject({ name: 'CssSyntaxError' })
|
||||
})
|
||||
})
|
||||
|
||||
test('all selectors in a rule must contain classes', () => {
|
||||
const input = `
|
||||
@responsive {
|
||||
.foo, div { color: yellow; }
|
||||
}
|
||||
|
||||
@tailwind screens;
|
||||
`
|
||||
expect.assertions(1)
|
||||
return run(input, {
|
||||
theme: {
|
||||
screens: {
|
||||
sm: '500px',
|
||||
md: '750px',
|
||||
lg: '1000px',
|
||||
},
|
||||
},
|
||||
separator: ':',
|
||||
}).catch(e => {
|
||||
expect(e).toMatchObject({ name: 'CssSyntaxError' })
|
||||
})
|
||||
})
|
||||
144
__tests__/sanity.test.js
Normal file
144
__tests__/sanity.test.js
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import postcss from 'postcss'
|
||||
import tailwind from '../src/index'
|
||||
import config from '../stubs/defaultConfig.stub.js'
|
||||
|
||||
it('generates the right CSS', () => {
|
||||
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
|
||||
const input = fs.readFileSync(inputPath, 'utf8')
|
||||
|
||||
return postcss([tailwind()])
|
||||
.process(input, { from: inputPath })
|
||||
.then(result => {
|
||||
const expected = fs.readFileSync(
|
||||
path.resolve(`${__dirname}/fixtures/tailwind-output.css`),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
expect(result.css).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
it('generates the right CSS when "important" is enabled', () => {
|
||||
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
|
||||
const input = fs.readFileSync(inputPath, 'utf8')
|
||||
|
||||
return postcss([tailwind({ ...config, important: true })])
|
||||
.process(input, { from: inputPath })
|
||||
.then(result => {
|
||||
const expected = fs.readFileSync(
|
||||
path.resolve(`${__dirname}/fixtures/tailwind-output-important.css`),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
expect(result.css).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
it('generates the right CSS when using @import instead of @tailwind', () => {
|
||||
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input-import.css`)
|
||||
const input = fs.readFileSync(inputPath, 'utf8')
|
||||
|
||||
return postcss([tailwind()])
|
||||
.process(input, { from: inputPath })
|
||||
.then(result => {
|
||||
const expected = fs.readFileSync(
|
||||
path.resolve(`${__dirname}/fixtures/tailwind-output.css`),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
expect(result.css).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
it('generates the right CSS when enabling flagged features', () => {
|
||||
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
|
||||
const input = fs.readFileSync(inputPath, 'utf8')
|
||||
|
||||
return postcss([
|
||||
tailwind({
|
||||
future: 'all',
|
||||
experimental: 'all',
|
||||
}),
|
||||
])
|
||||
.process(input, { from: inputPath })
|
||||
.then(result => {
|
||||
const expected = fs.readFileSync(
|
||||
path.resolve(`${__dirname}/fixtures/tailwind-output-flagged.css`),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
expect(result.css).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: Move to per plugin unit tests for this sort of thing
|
||||
it('generates the right CSS when color opacity plugins are disabled', () => {
|
||||
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
|
||||
const input = fs.readFileSync(inputPath, 'utf8')
|
||||
|
||||
return postcss([
|
||||
tailwind({
|
||||
...config,
|
||||
corePlugins: {
|
||||
textOpacity: false,
|
||||
backgroundOpacity: false,
|
||||
borderOpacity: false,
|
||||
placeholderOpacity: false,
|
||||
divideOpacity: false,
|
||||
},
|
||||
}),
|
||||
])
|
||||
.process(input, { from: inputPath })
|
||||
.then(result => {
|
||||
const expected = fs.readFileSync(
|
||||
path.resolve(`${__dirname}/fixtures/tailwind-output-no-color-opacity.css`),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
expect(result.css).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
it('does not add any CSS if no Tailwind features are used', () => {
|
||||
return postcss([tailwind()])
|
||||
.process('.foo { color: blue; }', { from: undefined })
|
||||
.then(result => {
|
||||
expect(result.css).toMatchCss('.foo { color: blue; }')
|
||||
})
|
||||
})
|
||||
|
||||
it('generates the right CSS with implicit screen utilities', () => {
|
||||
const inputPath = path.resolve(
|
||||
`${__dirname}/fixtures/tailwind-input-with-explicit-screen-utilities.css`
|
||||
)
|
||||
const input = fs.readFileSync(inputPath, 'utf8')
|
||||
|
||||
return postcss([tailwind()])
|
||||
.process(input, { from: inputPath })
|
||||
.then(result => {
|
||||
const expected = fs.readFileSync(
|
||||
path.resolve(`${__dirname}/fixtures/tailwind-output-with-explicit-screen-utilities.css`),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
expect(result.css).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
it('generates the right CSS when "important" is enabled', () => {
|
||||
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
|
||||
const input = fs.readFileSync(inputPath, 'utf8')
|
||||
|
||||
return postcss([tailwind({ ...config, important: true })])
|
||||
.process(input, { from: inputPath })
|
||||
.then(result => {
|
||||
const expected = fs.readFileSync(
|
||||
path.resolve(`${__dirname}/fixtures/tailwind-output-important.css`),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
expect(result.css).toBe(expected)
|
||||
})
|
||||
})
|
||||
47
__tests__/screenAtRule.test.js
Normal file
47
__tests__/screenAtRule.test.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import postcss from 'postcss'
|
||||
import plugin from '../src/lib/substituteScreenAtRules'
|
||||
import config from '../stubs/defaultConfig.stub.js'
|
||||
|
||||
function run(input, opts = config) {
|
||||
return postcss([plugin(opts)]).process(input, { from: undefined })
|
||||
}
|
||||
|
||||
test('it can generate media queries from configured screen sizes', () => {
|
||||
const input = `
|
||||
@screen sm {
|
||||
.banana { color: yellow; }
|
||||
}
|
||||
@screen md {
|
||||
.banana { color: red; }
|
||||
}
|
||||
@screen lg {
|
||||
.banana { color: green; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
@media (min-width: 500px) {
|
||||
.banana { color: yellow; }
|
||||
}
|
||||
@media (min-width: 750px) {
|
||||
.banana { color: red; }
|
||||
}
|
||||
@media (min-width: 1000px) {
|
||||
.banana { color: green; }
|
||||
}
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
theme: {
|
||||
screens: {
|
||||
sm: '500px',
|
||||
md: '750px',
|
||||
lg: '1000px',
|
||||
},
|
||||
},
|
||||
separator: ':',
|
||||
}).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
201
__tests__/targetMode.test.js
Normal file
201
__tests__/targetMode.test.js
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import postcss from 'postcss'
|
||||
import tailwind from '../src/index'
|
||||
import config from '../stubs/defaultConfig.stub.js'
|
||||
import processPlugins from '../src/util/processPlugins'
|
||||
import runInTempDirectory from '../jest/runInTempDirectory'
|
||||
|
||||
function css(nodes) {
|
||||
return postcss.root({ nodes }).toString()
|
||||
}
|
||||
|
||||
it('generates the right CSS in IE11 mode', () => {
|
||||
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
|
||||
const input = fs.readFileSync(inputPath, 'utf8')
|
||||
|
||||
return postcss([tailwind({ ...config, target: 'ie11' })])
|
||||
.process(input, { from: inputPath })
|
||||
.then(result => {
|
||||
const expected = fs.readFileSync(
|
||||
path.resolve(`${__dirname}/fixtures/tailwind-output-ie11.css`),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
expect(result.css).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
test('plugins can request the target for a specific plugin key', () => {
|
||||
const { utilities } = processPlugins(
|
||||
[
|
||||
function({ addUtilities, target }) {
|
||||
addUtilities({
|
||||
'.testA': {
|
||||
target: target('testPluginA'),
|
||||
},
|
||||
})
|
||||
},
|
||||
function({ addUtilities, target }) {
|
||||
addUtilities({
|
||||
'.testB': {
|
||||
target: target('testPluginB'),
|
||||
},
|
||||
})
|
||||
},
|
||||
function({ addUtilities, target }) {
|
||||
addUtilities({
|
||||
'.testC': {
|
||||
target: target('testPluginC'),
|
||||
},
|
||||
})
|
||||
},
|
||||
],
|
||||
{
|
||||
...config,
|
||||
target: [
|
||||
'ie11',
|
||||
{
|
||||
testPluginA: 'modern',
|
||||
testPluginB: 'relaxed',
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
expect(css(utilities)).toMatchCss(`
|
||||
@layer utilities {
|
||||
@variants {
|
||||
.testA {
|
||||
target: modern
|
||||
}
|
||||
}
|
||||
}
|
||||
@layer utilities {
|
||||
@variants {
|
||||
.testB {
|
||||
target: relaxed
|
||||
}
|
||||
}
|
||||
}
|
||||
@layer utilities {
|
||||
@variants {
|
||||
.testC {
|
||||
target: ie11
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test('browserslist target is translated to a target preset', () => {
|
||||
return runInTempDirectory(() => {
|
||||
fs.writeFileSync(
|
||||
path.resolve('./.browserslistrc'),
|
||||
`
|
||||
last 2 versions
|
||||
IE 11
|
||||
`
|
||||
)
|
||||
const { utilities } = processPlugins(
|
||||
[
|
||||
function({ addUtilities, target }) {
|
||||
addUtilities({
|
||||
'.test': {
|
||||
target: target('testPlugin'),
|
||||
},
|
||||
})
|
||||
},
|
||||
],
|
||||
{
|
||||
...config,
|
||||
target: 'browserslist',
|
||||
}
|
||||
)
|
||||
|
||||
expect(css(utilities)).toMatchCss(`
|
||||
@layer utilities {
|
||||
@variants {
|
||||
.test {
|
||||
target: ie11
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
return Promise.resolve()
|
||||
})
|
||||
})
|
||||
|
||||
test('browserslist target is translated to a target preset with overrides', () => {
|
||||
return runInTempDirectory(() => {
|
||||
fs.writeFileSync(
|
||||
path.resolve('./.browserslistrc'),
|
||||
`
|
||||
last 2 versions
|
||||
IE 11
|
||||
`
|
||||
)
|
||||
const { utilities } = processPlugins(
|
||||
[
|
||||
function({ addUtilities, target }) {
|
||||
addUtilities({
|
||||
'.testA': {
|
||||
target: target('testPluginA'),
|
||||
},
|
||||
})
|
||||
},
|
||||
function({ addUtilities, target }) {
|
||||
addUtilities({
|
||||
'.testB': {
|
||||
target: target('testPluginB'),
|
||||
},
|
||||
})
|
||||
},
|
||||
function({ addUtilities, target }) {
|
||||
addUtilities({
|
||||
'.testC': {
|
||||
target: target('testPluginC'),
|
||||
},
|
||||
})
|
||||
},
|
||||
],
|
||||
{
|
||||
...config,
|
||||
target: [
|
||||
'browserslist',
|
||||
{
|
||||
testPluginA: 'modern',
|
||||
testPluginB: 'relaxed',
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
expect(css(utilities)).toMatchCss(`
|
||||
@layer utilities {
|
||||
@variants {
|
||||
.testA {
|
||||
target: modern
|
||||
}
|
||||
}
|
||||
}
|
||||
@layer utilities {
|
||||
@variants {
|
||||
.testB {
|
||||
target: relaxed
|
||||
}
|
||||
}
|
||||
}
|
||||
@layer utilities {
|
||||
@variants {
|
||||
.testC {
|
||||
target: ie11
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
return Promise.resolve()
|
||||
})
|
||||
})
|
||||
156
__tests__/themeFunction.test.js
Normal file
156
__tests__/themeFunction.test.js
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
import postcss from 'postcss'
|
||||
import plugin from '../src/lib/evaluateTailwindFunctions'
|
||||
|
||||
function run(input, opts = {}) {
|
||||
return postcss([plugin(opts)]).process(input, { from: undefined })
|
||||
}
|
||||
|
||||
test('it looks up values in the theme using dot notation', () => {
|
||||
const input = `
|
||||
.banana { color: theme('colors.yellow'); }
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: #f7cc50; }
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
theme: {
|
||||
colors: {
|
||||
yellow: '#f7cc50',
|
||||
},
|
||||
},
|
||||
}).then(result => {
|
||||
expect(result.css).toEqual(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('quotes are optional around the lookup path', () => {
|
||||
const input = `
|
||||
.banana { color: theme(colors.yellow); }
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: #f7cc50; }
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
theme: {
|
||||
colors: {
|
||||
yellow: '#f7cc50',
|
||||
},
|
||||
},
|
||||
}).then(result => {
|
||||
expect(result.css).toEqual(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('a default value can be provided', () => {
|
||||
const input = `
|
||||
.cookieMonster { color: theme('colors.blue', #0000ff); }
|
||||
`
|
||||
|
||||
const output = `
|
||||
.cookieMonster { color: #0000ff; }
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
theme: {
|
||||
colors: {
|
||||
yellow: '#f7cc50',
|
||||
},
|
||||
},
|
||||
}).then(result => {
|
||||
expect(result.css).toEqual(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('quotes are preserved around default values', () => {
|
||||
const input = `
|
||||
.heading { font-family: theme('fonts.sans', "Helvetica Neue"); }
|
||||
`
|
||||
|
||||
const output = `
|
||||
.heading { font-family: "Helvetica Neue"; }
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
theme: {
|
||||
fonts: {
|
||||
serif: 'Constantia',
|
||||
},
|
||||
},
|
||||
}).then(result => {
|
||||
expect(result.css).toEqual(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('an unquoted list is valid as a default value', () => {
|
||||
const input = `
|
||||
.heading { font-family: theme('fonts.sans', Helvetica, Arial, sans-serif); }
|
||||
`
|
||||
|
||||
const output = `
|
||||
.heading { font-family: Helvetica, Arial, sans-serif; }
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
theme: {
|
||||
fonts: {
|
||||
serif: 'Constantia',
|
||||
},
|
||||
},
|
||||
}).then(result => {
|
||||
expect(result.css).toEqual(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('array values are joined by default', () => {
|
||||
const input = `
|
||||
.heading { font-family: theme('fontFamily.sans'); }
|
||||
`
|
||||
|
||||
const output = `
|
||||
.heading { font-family: Inter, Helvetica, sans-serif; }
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
theme: {
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'Helvetica', 'sans-serif'],
|
||||
},
|
||||
},
|
||||
}).then(result => {
|
||||
expect(result.css).toEqual(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('font sizes are retrieved without default line-heights or letter-spacing', () => {
|
||||
const input = `
|
||||
.heading-1 { font-size: theme('fontSize.lg'); }
|
||||
.heading-2 { font-size: theme('fontSize.xl'); }
|
||||
`
|
||||
|
||||
const output = `
|
||||
.heading-1 { font-size: 20px; }
|
||||
.heading-2 { font-size: 24px; }
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
theme: {
|
||||
fontSize: {
|
||||
lg: ['20px', '28px'],
|
||||
xl: ['24px', { lineHeight: '32px', letterSpacing: '-0.01em' }],
|
||||
},
|
||||
},
|
||||
}).then(result => {
|
||||
expect(result.css).toEqual(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
41
__tests__/util/invokePlugin.js
Normal file
41
__tests__/util/invokePlugin.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import _ from 'lodash'
|
||||
import escapeClassName from '../../src/util/escapeClassName'
|
||||
|
||||
export default function(plugin, config) {
|
||||
const addedUtilities = []
|
||||
|
||||
const getConfigValue = (path, defaultValue) => _.get(config, path, defaultValue)
|
||||
const pluginApi = {
|
||||
config: getConfigValue,
|
||||
e: escapeClassName,
|
||||
theme: (path, defaultValue) => getConfigValue(`theme.${path}`, defaultValue),
|
||||
variants: (path, defaultValue) => {
|
||||
if (_.isArray(config.variants)) {
|
||||
return config.variants
|
||||
}
|
||||
|
||||
return getConfigValue(`variants.${path}`, defaultValue)
|
||||
},
|
||||
target: path => {
|
||||
if (_.isString(config.target)) {
|
||||
return config.target
|
||||
}
|
||||
|
||||
const [defaultTarget, targetOverrides] = getConfigValue('target')
|
||||
|
||||
return _.get(targetOverrides, path, defaultTarget)
|
||||
},
|
||||
addUtilities(utilities, variants) {
|
||||
addedUtilities.push([utilities, variants])
|
||||
},
|
||||
}
|
||||
|
||||
plugin(pluginApi)
|
||||
|
||||
return {
|
||||
utilities: addedUtilities.map(([utilities, variants]) => [
|
||||
_.merge({}, ..._.castArray(utilities)),
|
||||
variants,
|
||||
]),
|
||||
}
|
||||
}
|
||||
854
__tests__/variantsAtRule.test.js
Normal file
854
__tests__/variantsAtRule.test.js
Normal file
|
|
@ -0,0 +1,854 @@
|
|||
import postcss from 'postcss'
|
||||
import plugin from '../src/lib/substituteVariantsAtRules'
|
||||
import processPlugins from '../src/util/processPlugins'
|
||||
import config from '../stubs/defaultConfig.stub.js'
|
||||
|
||||
function run(input, opts = config) {
|
||||
return postcss([plugin(opts, processPlugins(opts.plugins, opts))]).process(input, {
|
||||
from: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
test('it can generate hover variants', () => {
|
||||
const input = `
|
||||
@variants hover {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
.hover\\:banana:hover { color: yellow; }
|
||||
.hover\\:chocolate:hover { color: brown; }
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it can generate disabled variants', () => {
|
||||
const input = `
|
||||
@variants disabled {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
.disabled\\:banana:disabled { color: yellow; }
|
||||
.disabled\\:chocolate:disabled { color: brown; }
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it can generate checked variants', () => {
|
||||
const input = `
|
||||
@variants checked {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
.checked\\:banana:checked { color: yellow; }
|
||||
.checked\\:chocolate:checked { color: brown; }
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it can generate active variants', () => {
|
||||
const input = `
|
||||
@variants active {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
.active\\:banana:active { color: yellow; }
|
||||
.active\\:chocolate:active { color: brown; }
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it can generate visited variants', () => {
|
||||
const input = `
|
||||
@variants visited {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
.visited\\:banana:visited { color: yellow; }
|
||||
.visited\\:chocolate:visited { color: brown; }
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it can generate focus variants', () => {
|
||||
const input = `
|
||||
@variants focus {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
.focus\\:banana:focus { color: yellow; }
|
||||
.focus\\:chocolate:focus { color: brown; }
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it can generate focus-within variants', () => {
|
||||
const input = `
|
||||
@variants focus-within {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
.focus-within\\:banana:focus-within { color: yellow; }
|
||||
.focus-within\\:chocolate:focus-within { color: brown; }
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it can generate focus-visible variants', () => {
|
||||
const input = `
|
||||
@variants focus-visible {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
.focus-visible\\:banana:focus-visible { color: yellow; }
|
||||
.focus-visible\\:chocolate:focus-visible { color: brown; }
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it can generate motion-reduce variants', () => {
|
||||
const input = `
|
||||
@variants motion-reduce {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.motion-reduce\\:banana { color: yellow; }
|
||||
.motion-reduce\\:chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it can generate motion-safe variants', () => {
|
||||
const input = `
|
||||
@variants motion-safe {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.motion-safe\\:banana { color: yellow; }
|
||||
.motion-safe\\:chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it can generate motion-safe and motion-reduce variants', () => {
|
||||
const input = `
|
||||
@variants motion-safe, motion-reduce {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.motion-safe\\:banana { color: yellow; }
|
||||
.motion-safe\\:chocolate { color: brown; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.motion-reduce\\:banana { color: yellow; }
|
||||
.motion-reduce\\:chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('motion-reduce variants stack with basic variants', () => {
|
||||
const input = `
|
||||
@variants motion-reduce, hover, group-hover {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
.hover\\:banana:hover { color: yellow; }
|
||||
.hover\\:chocolate:hover { color: brown; }
|
||||
.group:hover .group-hover\\:banana { color: yellow; }
|
||||
.group:hover .group-hover\\:chocolate { color: brown; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.motion-reduce\\:banana { color: yellow; }
|
||||
.motion-reduce\\:chocolate { color: brown; }
|
||||
.motion-reduce\\:hover\\:banana:hover { color: yellow; }
|
||||
.motion-reduce\\:hover\\:chocolate:hover { color: brown; }
|
||||
.group:hover .motion-reduce\\:group-hover\\:banana { color: yellow; }
|
||||
.group:hover .motion-reduce\\:group-hover\\:chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('motion-safe variants stack with basic variants', () => {
|
||||
const input = `
|
||||
@variants motion-safe, hover, group-hover {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
.hover\\:banana:hover { color: yellow; }
|
||||
.hover\\:chocolate:hover { color: brown; }
|
||||
.group:hover .group-hover\\:banana { color: yellow; }
|
||||
.group:hover .group-hover\\:chocolate { color: brown; }
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.motion-safe\\:banana { color: yellow; }
|
||||
.motion-safe\\:chocolate { color: brown; }
|
||||
.motion-safe\\:hover\\:banana:hover { color: yellow; }
|
||||
.motion-safe\\:hover\\:chocolate:hover { color: brown; }
|
||||
.group:hover .motion-safe\\:group-hover\\:banana { color: yellow; }
|
||||
.group:hover .motion-safe\\:group-hover\\:chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('motion-safe and motion-reduce variants stack with basic variants', () => {
|
||||
const input = `
|
||||
@variants motion-reduce, motion-safe, hover {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
.hover\\:banana:hover { color: yellow; }
|
||||
.hover\\:chocolate:hover { color: brown; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.motion-reduce\\:banana { color: yellow; }
|
||||
.motion-reduce\\:chocolate { color: brown; }
|
||||
.motion-reduce\\:hover\\:banana:hover { color: yellow; }
|
||||
.motion-reduce\\:hover\\:chocolate:hover { color: brown; }
|
||||
}
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.motion-safe\\:banana { color: yellow; }
|
||||
.motion-safe\\:chocolate { color: brown; }
|
||||
.motion-safe\\:hover\\:banana:hover { color: yellow; }
|
||||
.motion-safe\\:hover\\:chocolate:hover { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it can generate first-child variants', () => {
|
||||
const input = `
|
||||
@variants first {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
.first\\:banana:first-child { color: yellow; }
|
||||
.first\\:chocolate:first-child { color: brown; }
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it can generate odd variants', () => {
|
||||
const input = `
|
||||
@variants odd {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
.odd\\:banana:nth-child(odd) { color: yellow; }
|
||||
.odd\\:chocolate:nth-child(odd) { color: brown; }
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it can generate last-child variants', () => {
|
||||
const input = `
|
||||
@variants last {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
.last\\:banana:last-child { color: yellow; }
|
||||
.last\\:chocolate:last-child { color: brown; }
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it can generate even variants', () => {
|
||||
const input = `
|
||||
@variants even {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
.even\\:banana:nth-child(even) { color: yellow; }
|
||||
.even\\:chocolate:nth-child(even) { color: brown; }
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it can generate group-hover variants', () => {
|
||||
const input = `
|
||||
@variants group-hover {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
.group:hover .group-hover\\:banana { color: yellow; }
|
||||
.group:hover .group-hover\\:chocolate { color: brown; }
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('group-hover variants respect any configured prefix', () => {
|
||||
const input = `
|
||||
@variants group-hover {
|
||||
.tw-banana { color: yellow; }
|
||||
.tw-chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.tw-banana { color: yellow; }
|
||||
.tw-chocolate { color: brown; }
|
||||
.tw-group:hover .group-hover\\:tw-banana { color: yellow; }
|
||||
.tw-group:hover .group-hover\\:tw-chocolate { color: brown; }
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
...config,
|
||||
prefix: 'tw-',
|
||||
}).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it can generate group-focus variants', () => {
|
||||
const input = `
|
||||
@variants group-focus {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
.group:focus .group-focus\\:banana { color: yellow; }
|
||||
.group:focus .group-focus\\:chocolate { color: brown; }
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('group-focus variants respect any configured prefix', () => {
|
||||
const input = `
|
||||
@variants group-focus {
|
||||
.tw-banana { color: yellow; }
|
||||
.tw-chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.tw-banana { color: yellow; }
|
||||
.tw-chocolate { color: brown; }
|
||||
.tw-group:focus .group-focus\\:tw-banana { color: yellow; }
|
||||
.tw-group:focus .group-focus\\:tw-chocolate { color: brown; }
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
...config,
|
||||
prefix: 'tw-',
|
||||
}).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it can generate hover, active and focus variants', () => {
|
||||
const input = `
|
||||
@variants group-hover, hover, focus, active {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
.group:hover .group-hover\\:banana { color: yellow; }
|
||||
.group:hover .group-hover\\:chocolate { color: brown; }
|
||||
.hover\\:banana:hover { color: yellow; }
|
||||
.hover\\:chocolate:hover { color: brown; }
|
||||
.focus\\:banana:focus { color: yellow; }
|
||||
.focus\\:chocolate:focus { color: brown; }
|
||||
.active\\:banana:active { color: yellow; }
|
||||
.active\\:chocolate:active { color: brown; }
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it can generate hover, active and focus variants for multiple classes in one rule', () => {
|
||||
const input = `
|
||||
@variants hover, focus, active {
|
||||
.banana, .lemon { color: yellow; }
|
||||
.chocolate, .coconut { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana, .lemon { color: yellow; }
|
||||
.chocolate, .coconut { color: brown; }
|
||||
.hover\\:banana:hover, .hover\\:lemon:hover { color: yellow; }
|
||||
.hover\\:chocolate:hover, .hover\\:coconut:hover { color: brown; }
|
||||
.focus\\:banana:focus, .focus\\:lemon:focus { color: yellow; }
|
||||
.focus\\:chocolate:focus, .focus\\:coconut:focus { color: brown; }
|
||||
.active\\:banana:active, .active\\:lemon:active { color: yellow; }
|
||||
.active\\:chocolate:active, .active\\:coconut:active { color: brown; }
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('it wraps the output in a responsive at-rule if responsive is included as a variant', () => {
|
||||
const input = `
|
||||
@variants responsive, hover, focus {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
@responsive {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
.hover\\:banana:hover { color: yellow; }
|
||||
.hover\\:chocolate:hover { color: brown; }
|
||||
.focus\\:banana:focus { color: yellow; }
|
||||
.focus\\:chocolate:focus { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('variants are generated in the order specified', () => {
|
||||
const input = `
|
||||
@variants focus, active, hover {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
.focus\\:banana:focus { color: yellow; }
|
||||
.focus\\:chocolate:focus { color: brown; }
|
||||
.active\\:banana:active { color: yellow; }
|
||||
.active\\:chocolate:active { color: brown; }
|
||||
.hover\\:banana:hover { color: yellow; }
|
||||
.hover\\:chocolate:hover { color: brown; }
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
...config,
|
||||
}).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('the built-in variant pseudo-selectors are appended before any pseudo-elements', () => {
|
||||
const input = `
|
||||
@variants hover, focus-within, focus-visible, focus, active, group-hover {
|
||||
.placeholder-yellow::placeholder { color: yellow; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.placeholder-yellow::placeholder { color: yellow; }
|
||||
.hover\\:placeholder-yellow:hover::placeholder { color: yellow; }
|
||||
.focus-within\\:placeholder-yellow:focus-within::placeholder { color: yellow; }
|
||||
.focus-visible\\:placeholder-yellow:focus-visible::placeholder { color: yellow; }
|
||||
.focus\\:placeholder-yellow:focus::placeholder { color: yellow; }
|
||||
.active\\:placeholder-yellow:active::placeholder { color: yellow; }
|
||||
.group:hover .group-hover\\:placeholder-yellow::placeholder { color: yellow; }
|
||||
`
|
||||
|
||||
return run(input).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('the default variant can be generated in a specified position', () => {
|
||||
const input = `
|
||||
@variants focus, active, default, hover {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.focus\\:banana:focus { color: yellow; }
|
||||
.focus\\:chocolate:focus { color: brown; }
|
||||
.active\\:banana:active { color: yellow; }
|
||||
.active\\:chocolate:active { color: brown; }
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
.hover\\:banana:hover { color: yellow; }
|
||||
.hover\\:chocolate:hover { color: brown; }
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
...config,
|
||||
}).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('nested rules are not modified', () => {
|
||||
const input = `
|
||||
@variants focus, active, hover {
|
||||
.banana {
|
||||
color: yellow;
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana {
|
||||
color: yellow;
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
.focus\\:banana:focus {
|
||||
color: yellow;
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
.active\\:banana:active {
|
||||
color: yellow;
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
.hover\\:banana:hover {
|
||||
color: yellow;
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
...config,
|
||||
}).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('plugin variants can modify rules using the raw PostCSS API', () => {
|
||||
const input = `
|
||||
@variants important {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
.\\!banana { color: yellow !important; }
|
||||
.\\!chocolate { color: brown !important; }
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
...config,
|
||||
plugins: [
|
||||
...config.plugins,
|
||||
function({ addVariant }) {
|
||||
addVariant('important', ({ container }) => {
|
||||
container.walkRules(rule => {
|
||||
rule.selector = `.\\!${rule.selector.slice(1)}`
|
||||
rule.walkDecls(decl => {
|
||||
decl.important = true
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
],
|
||||
}).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('plugin variants can modify selectors with a simplified API', () => {
|
||||
const input = `
|
||||
@variants first-child {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
.first-child\\:banana:first-child { color: yellow; }
|
||||
.first-child\\:chocolate:first-child { color: brown; }
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
...config,
|
||||
plugins: [
|
||||
...config.plugins,
|
||||
function({ addVariant, e }) {
|
||||
addVariant('first-child', ({ modifySelectors, separator }) => {
|
||||
modifySelectors(({ className }) => {
|
||||
return `.${e(`first-child${separator}${className}`)}:first-child`
|
||||
})
|
||||
})
|
||||
},
|
||||
],
|
||||
}).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('plugin variants that use modify selectors need to manually escape the class name they are modifying', () => {
|
||||
const input = `
|
||||
@variants first-child {
|
||||
.banana-1\\/2 { color: yellow; }
|
||||
.chocolate-1\\.5 { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana-1\\/2 { color: yellow; }
|
||||
.chocolate-1\\.5 { color: brown; }
|
||||
.first-child\\:banana-1\\/2:first-child { color: yellow; }
|
||||
.first-child\\:chocolate-1\\.5:first-child { color: brown; }
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
...config,
|
||||
plugins: [
|
||||
...config.plugins,
|
||||
function({ addVariant, e }) {
|
||||
addVariant('first-child', ({ modifySelectors, separator }) => {
|
||||
modifySelectors(({ className }) => {
|
||||
return `.${e(`first-child${separator}${className}`)}:first-child`
|
||||
})
|
||||
})
|
||||
},
|
||||
],
|
||||
}).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('plugin variants can wrap rules in another at-rule using the raw PostCSS API', () => {
|
||||
const input = `
|
||||
@variants supports-grid {
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
const output = `
|
||||
.banana { color: yellow; }
|
||||
.chocolate { color: brown; }
|
||||
@supports (display: grid) {
|
||||
.supports-grid\\:banana { color: yellow; }
|
||||
.supports-grid\\:chocolate { color: brown; }
|
||||
}
|
||||
`
|
||||
|
||||
return run(input, {
|
||||
...config,
|
||||
plugins: [
|
||||
...config.plugins,
|
||||
function({ addVariant, e }) {
|
||||
addVariant('supports-grid', ({ container, separator }) => {
|
||||
const supportsRule = postcss.atRule({
|
||||
name: 'supports',
|
||||
params: '(display: grid)',
|
||||
})
|
||||
supportsRule.nodes = container.nodes
|
||||
container.nodes = [supportsRule]
|
||||
supportsRule.walkRules(rule => {
|
||||
rule.selector = `.${e(`supports-grid${separator}${rule.selector.slice(1)}`)}`
|
||||
})
|
||||
})
|
||||
},
|
||||
],
|
||||
}).then(result => {
|
||||
expect(result.css).toMatchCss(output)
|
||||
expect(result.warnings().length).toBe(0)
|
||||
})
|
||||
})
|
||||
109
__tests__/withAlphaVariable.test.js
Normal file
109
__tests__/withAlphaVariable.test.js
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import withAlphaVariable from '../src/util/withAlphaVariable'
|
||||
|
||||
test('it adds the right custom property', () => {
|
||||
expect(
|
||||
withAlphaVariable({ color: '#ff0000', property: 'color', variable: '--text-opacity' })
|
||||
).toEqual({
|
||||
'--text-opacity': '1',
|
||||
color: ['#ff0000', 'rgba(255, 0, 0, var(--text-opacity))'],
|
||||
})
|
||||
})
|
||||
|
||||
test('it ignores colors that cannot be parsed', () => {
|
||||
expect(
|
||||
withAlphaVariable({
|
||||
color: 'currentColor',
|
||||
property: 'background-color',
|
||||
variable: '--bg-opacity',
|
||||
})
|
||||
).toEqual({
|
||||
'background-color': 'currentColor',
|
||||
})
|
||||
})
|
||||
|
||||
test('it ignores colors that already have an alpha channel', () => {
|
||||
expect(
|
||||
withAlphaVariable({
|
||||
color: '#ff0000ff',
|
||||
property: 'background-color',
|
||||
variable: '--bg-opacity',
|
||||
})
|
||||
).toEqual({
|
||||
'background-color': '#ff0000ff',
|
||||
})
|
||||
expect(
|
||||
withAlphaVariable({
|
||||
color: '#ff000080',
|
||||
property: 'background-color',
|
||||
variable: '--bg-opacity',
|
||||
})
|
||||
).toEqual({
|
||||
'background-color': '#ff000080',
|
||||
})
|
||||
expect(
|
||||
withAlphaVariable({
|
||||
color: '#f00a',
|
||||
property: 'background-color',
|
||||
variable: '--bg-opacity',
|
||||
})
|
||||
).toEqual({
|
||||
'background-color': '#f00a',
|
||||
})
|
||||
expect(
|
||||
withAlphaVariable({
|
||||
color: '#f00f',
|
||||
property: 'background-color',
|
||||
variable: '--bg-opacity',
|
||||
})
|
||||
).toEqual({
|
||||
'background-color': '#f00f',
|
||||
})
|
||||
expect(
|
||||
withAlphaVariable({
|
||||
color: 'rgba(255, 255, 255, 1)',
|
||||
property: 'background-color',
|
||||
variable: '--bg-opacity',
|
||||
})
|
||||
).toEqual({
|
||||
'background-color': 'rgba(255, 255, 255, 1)',
|
||||
})
|
||||
expect(
|
||||
withAlphaVariable({
|
||||
color: 'rgba(255, 255, 255, 0.5)',
|
||||
property: 'background-color',
|
||||
variable: '--bg-opacity',
|
||||
})
|
||||
).toEqual({
|
||||
'background-color': 'rgba(255, 255, 255, 0.5)',
|
||||
})
|
||||
expect(
|
||||
withAlphaVariable({
|
||||
color: 'hsla(240, 100%, 50%, 1)',
|
||||
property: 'background-color',
|
||||
variable: '--bg-opacity',
|
||||
})
|
||||
).toEqual({
|
||||
'background-color': 'hsla(240, 100%, 50%, 1)',
|
||||
})
|
||||
expect(
|
||||
withAlphaVariable({
|
||||
color: 'hsla(240, 100%, 50%, 0.5)',
|
||||
property: 'background-color',
|
||||
variable: '--bg-opacity',
|
||||
})
|
||||
).toEqual({
|
||||
'background-color': 'hsla(240, 100%, 50%, 0.5)',
|
||||
})
|
||||
})
|
||||
|
||||
test('it allows a closure to be passed', () => {
|
||||
expect(
|
||||
withAlphaVariable({
|
||||
color: ({ opacityVariable }) => `rgba(0, 0, 0, var(${opacityVariable}))`,
|
||||
property: 'background-color',
|
||||
variable: '--bg-opacity',
|
||||
})
|
||||
).toEqual({
|
||||
'background-color': 'rgba(0, 0, 0, var(--bg-opacity))',
|
||||
})
|
||||
})
|
||||
1
base.css
Normal file
1
base.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
@tailwind base;
|
||||
1
components.css
Normal file
1
components.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
@tailwind components;
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
[package]
|
||||
name = "classification-macros"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = "2"
|
||||
quote = "1"
|
||||
proc-macro2 = "1"
|
||||
|
|
@ -1,253 +0,0 @@
|
|||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{
|
||||
parse_macro_input, punctuated::Punctuated, token::Comma, Attribute, Data, DataEnum,
|
||||
DeriveInput, Expr, ExprLit, ExprRange, Ident, Lit, RangeLimits, Result, Variant,
|
||||
};
|
||||
|
||||
/// A custom derive that supports:
|
||||
///
|
||||
/// - `#[bytes(…)]` for single byte literals
|
||||
/// - `#[bytes_range(…)]` for inclusive byte ranges (b'a'..=b'z')
|
||||
/// - `#[fallback]` for a variant that covers everything else
|
||||
///
|
||||
/// Example usage:
|
||||
///
|
||||
/// ```rust
|
||||
/// use classification_macros::ClassifyBytes;
|
||||
///
|
||||
/// #[derive(Clone, Copy, ClassifyBytes)]
|
||||
/// enum Class {
|
||||
/// #[bytes(b'a', b'b', b'c')]
|
||||
/// Letters,
|
||||
///
|
||||
/// #[bytes_range(b'0'..=b'9')]
|
||||
/// Digits,
|
||||
///
|
||||
/// #[fallback]
|
||||
/// Other,
|
||||
/// }
|
||||
/// ```
|
||||
/// Then call `b'a'.into()` to get `Example::SomeLetters`.
|
||||
#[proc_macro_derive(ClassifyBytes, attributes(bytes, bytes_range, fallback))]
|
||||
pub fn classify_bytes_derive(input: TokenStream) -> TokenStream {
|
||||
let ast = parse_macro_input!(input as DeriveInput);
|
||||
|
||||
// This derive only works on an enum
|
||||
let Data::Enum(DataEnum { variants, .. }) = &ast.data else {
|
||||
return syn::Error::new_spanned(
|
||||
&ast.ident,
|
||||
"ClassifyBytes can only be derived on an enum.",
|
||||
)
|
||||
.to_compile_error()
|
||||
.into();
|
||||
};
|
||||
|
||||
let enum_name = &ast.ident;
|
||||
|
||||
let mut byte_map: [Option<Ident>; 256] = [const { None }; 256];
|
||||
let mut fallback_variant: Option<Ident> = None;
|
||||
|
||||
// Start parsing the variants
|
||||
for variant in variants {
|
||||
let variant_ident = &variant.ident;
|
||||
|
||||
// If this variant has #[fallback], record it
|
||||
if has_fallback_attr(variant) {
|
||||
if fallback_variant.is_some() {
|
||||
let err = syn::Error::new_spanned(
|
||||
variant_ident,
|
||||
"Multiple variants have #[fallback]. Only one allowed.",
|
||||
);
|
||||
return err.to_compile_error().into();
|
||||
}
|
||||
fallback_variant = Some(variant_ident.clone());
|
||||
}
|
||||
|
||||
// Get #[bytes(…)]
|
||||
let single_bytes = get_bytes_attrs(&variant.attrs);
|
||||
|
||||
// Get #[bytes_range(…)]
|
||||
let range_bytes = get_bytes_range_attrs(&variant.attrs);
|
||||
|
||||
// Combine them
|
||||
let all_bytes = single_bytes
|
||||
.into_iter()
|
||||
.chain(range_bytes)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Mark them in the table
|
||||
for b in all_bytes {
|
||||
byte_map[b as usize] = Some(variant_ident.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// If no fallback variant is found, default to "Other"
|
||||
let fallback_ident = fallback_variant.expect("A variant marked with #[fallback] is missing");
|
||||
|
||||
// For each of the 256 byte values, fill the table
|
||||
let fill = byte_map
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|variant_opt| match variant_opt {
|
||||
Some(ident) => quote!(#enum_name::#ident),
|
||||
None => quote!(#enum_name::#fallback_ident),
|
||||
});
|
||||
|
||||
// Generate the final expanded code
|
||||
let expanded = quote! {
|
||||
impl #enum_name {
|
||||
pub const TABLE: [#enum_name; 256] = [
|
||||
#(#fill),*
|
||||
];
|
||||
}
|
||||
|
||||
impl From<u8> for #enum_name {
|
||||
fn from(byte: u8) -> Self {
|
||||
#enum_name::TABLE[byte as usize]
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&u8> for #enum_name {
|
||||
fn from(byte: &u8) -> Self {
|
||||
#enum_name::TABLE[*byte as usize]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
|
||||
/// Checks if a variant has `#[fallback]`
|
||||
fn has_fallback_attr(variant: &Variant) -> bool {
|
||||
variant
|
||||
.attrs
|
||||
.iter()
|
||||
.any(|attr| attr.path().is_ident("fallback"))
|
||||
}
|
||||
|
||||
/// Get all single byte literals from `#[bytes(…)]`
|
||||
fn get_bytes_attrs(attrs: &[Attribute]) -> Vec<u8> {
|
||||
let mut assigned = Vec::new();
|
||||
for attr in attrs {
|
||||
if attr.path().is_ident("bytes") {
|
||||
match parse_bytes_attr(attr) {
|
||||
Ok(list) => assigned.extend(list),
|
||||
Err(e) => panic!("Error parsing #[bytes(...)]: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
assigned
|
||||
}
|
||||
|
||||
/// Parse `#[bytes(...)]` as a comma-separated list of **byte literals**, e.g. `b'a'`, `b'\n'`.
|
||||
fn parse_bytes_attr(attr: &Attribute) -> Result<Vec<u8>> {
|
||||
// We'll parse it as a list of syn::Lit separated by commas: e.g. (b'a', b'b')
|
||||
let items: Punctuated<Lit, Comma> = attr.parse_args_with(Punctuated::parse_terminated)?;
|
||||
let mut out = Vec::new();
|
||||
for lit in items {
|
||||
match lit {
|
||||
Lit::Byte(lb) => out.push(lb.value()),
|
||||
_ => {
|
||||
return Err(syn::Error::new_spanned(
|
||||
lit,
|
||||
"Expected a byte literal like b'a'",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Get all byte ranges from `#[bytes_range(...)]`
|
||||
fn get_bytes_range_attrs(attrs: &[Attribute]) -> Vec<u8> {
|
||||
let mut assigned = Vec::new();
|
||||
for attr in attrs {
|
||||
if attr.path().is_ident("bytes_range") {
|
||||
match parse_bytes_range_attr(attr) {
|
||||
Ok(list) => assigned.extend(list),
|
||||
Err(e) => panic!("Error parsing #[bytes_range(...)]: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
assigned
|
||||
}
|
||||
|
||||
/// Parse `#[bytes_range(...)]` as a comma-separated list of range expressions, e.g.:
|
||||
/// `b'a'..=b'z', b'0'..=b'9'`
|
||||
fn parse_bytes_range_attr(attr: &Attribute) -> Result<Vec<u8>> {
|
||||
// We'll parse each element as a syn::Expr, then see if it's an Expr::Range
|
||||
let exprs: Punctuated<Expr, Comma> = attr.parse_args_with(Punctuated::parse_terminated)?;
|
||||
let mut out = Vec::new();
|
||||
|
||||
for expr in exprs {
|
||||
if let Expr::Range(ExprRange {
|
||||
start: Some(start),
|
||||
end: Some(end),
|
||||
limits,
|
||||
..
|
||||
}) = expr
|
||||
{
|
||||
let from = extract_byte_literal(&start)?;
|
||||
let to = extract_byte_literal(&end)?;
|
||||
|
||||
match limits {
|
||||
RangeLimits::Closed(_) => {
|
||||
// b'a'..=b'z'
|
||||
if from <= to {
|
||||
out.extend(from..=to);
|
||||
}
|
||||
}
|
||||
RangeLimits::HalfOpen(_) => {
|
||||
// b'a'..b'z' => from..(to-1)
|
||||
if from < to {
|
||||
out.extend(from..to);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(syn::Error::new_spanned(
|
||||
expr,
|
||||
"Expected a byte range like b'a'..=b'z'",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Extract a u8 from an expression that can be:
|
||||
///
|
||||
/// - `Expr::Lit(Lit::Byte(...))`, e.g. b'a'
|
||||
/// - `Expr::Lit(Lit::Int(...))`, e.g. 0x80 or 255
|
||||
fn extract_byte_literal(expr: &Expr) -> Result<u8> {
|
||||
if let Expr::Lit(ExprLit { lit, .. }) = expr {
|
||||
match lit {
|
||||
// Existing case: b'a'
|
||||
Lit::Byte(lb) => Ok(lb.value()),
|
||||
|
||||
// New case: 0x80, 255, etc.
|
||||
Lit::Int(li) => {
|
||||
let value = li.base10_parse::<u64>()?;
|
||||
if value <= 255 {
|
||||
Ok(value as u8)
|
||||
} else {
|
||||
Err(syn::Error::new_spanned(
|
||||
li,
|
||||
format!("Integer literal {} out of range for a byte (0..255)", value),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
_ => Err(syn::Error::new_spanned(
|
||||
lit,
|
||||
"Expected b'...' or an integer literal in range 0..=255",
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
Err(syn::Error::new_spanned(
|
||||
expr,
|
||||
"Expected a literal expression like b'a' or 0x80",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
This project is dual-licensed under the Unlicense and MIT licenses.
|
||||
|
||||
You may use this code under the terms of either license.
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
[package]
|
||||
name = "ignore"
|
||||
version = "0.4.33" #:version
|
||||
authors = ["Andrew Gallant <jamslam@gmail.com>"]
|
||||
description = """
|
||||
A fast library for efficiently matching ignore files such as `.gitignore`
|
||||
against file paths.
|
||||
"""
|
||||
documentation = "https://docs.rs/ignore"
|
||||
homepage = "https://github.com/BurntSushi/ripgrep/tree/master/crates/ignore"
|
||||
repository = "https://github.com/BurntSushi/ripgrep/tree/master/crates/ignore"
|
||||
readme = "README.md"
|
||||
keywords = ["glob", "ignore", "gitignore", "pattern", "file"]
|
||||
license = "Unlicense OR MIT"
|
||||
# CHANGED: Use an explicit edition instead of `edition.workspace = true` since this crate is
|
||||
# vendored into the Tailwind CSS workspace.
|
||||
edition = "2024"
|
||||
rust-version = "1.88"
|
||||
|
||||
[lib]
|
||||
name = "ignore"
|
||||
bench = false
|
||||
|
||||
[dependencies]
|
||||
crossbeam-deque = "0.8.3"
|
||||
# CHANGED: Use the published globset crate instead of a path dependency.
|
||||
globset = "0.4.20"
|
||||
log = "0.4.20"
|
||||
memchr = "2.6.3"
|
||||
same-file = "1.0.6"
|
||||
walkdir = "2.4.0"
|
||||
# CHANGED: Added `dunce` to canonicalize paths without UNC prefixes on Windows.
|
||||
dunce = "1.0.5"
|
||||
|
||||
[dependencies.regex-automata]
|
||||
version = "0.4.18"
|
||||
default-features = false
|
||||
features = ["std", "perf", "syntax", "meta", "nfa", "hybrid", "dfa-onepass"]
|
||||
|
||||
[target.'cfg(windows)'.dependencies.winapi-util]
|
||||
version = "0.1.2"
|
||||
|
||||
[dev-dependencies]
|
||||
bstr = { version = "1.6.2", default-features = false, features = ["std"] }
|
||||
crossbeam-channel = "0.5.15"
|
||||
|
||||
[features]
|
||||
# DEPRECATED. It is a no-op. SIMD is done automatically through runtime
|
||||
# dispatch.
|
||||
simd-accel = []
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Andrew Gallant
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
ignore
|
||||
======
|
||||
The ignore crate provides a fast recursive directory iterator that respects
|
||||
various filters such as globs, file types and `.gitignore` files. This crate
|
||||
also provides lower level direct access to gitignore and file type matchers.
|
||||
|
||||
[](https://github.com/BurntSushi/ripgrep/actions)
|
||||
[](https://crates.io/crates/ignore)
|
||||
|
||||
Dual-licensed under MIT or the [UNLICENSE](https://unlicense.org/).
|
||||
|
||||
### Documentation
|
||||
|
||||
[https://docs.rs/ignore](https://docs.rs/ignore)
|
||||
|
||||
### Usage
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
ignore = "0.4"
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
This example shows the most basic usage of this crate. This code will
|
||||
recursively traverse the current directory while automatically filtering out
|
||||
files and directories according to ignore globs found in files like
|
||||
`.ignore` and `.gitignore`:
|
||||
|
||||
|
||||
```rust,no_run
|
||||
use ignore::Walk;
|
||||
|
||||
for result in Walk::new("./") {
|
||||
// Each item yielded by the iterator is either a directory entry or an
|
||||
// error, so either print the path or the error.
|
||||
match result {
|
||||
Ok(entry) => println!("{}", entry.path().display()),
|
||||
Err(err) => println!("ERROR: {}", err),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example: advanced
|
||||
|
||||
By default, the recursive directory iterator will ignore hidden files and
|
||||
directories. This can be disabled by building the iterator with `WalkBuilder`:
|
||||
|
||||
```rust,no_run
|
||||
use ignore::WalkBuilder;
|
||||
|
||||
for result in WalkBuilder::new("./").hidden(false).build() {
|
||||
println!("{:?}", result);
|
||||
}
|
||||
```
|
||||
|
||||
See the documentation for `WalkBuilder` for many other options.
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <http://unlicense.org/>
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
use std::{env, io::Write, path::Path};
|
||||
|
||||
use {bstr::ByteVec, ignore::WalkBuilder, walkdir::WalkDir};
|
||||
|
||||
fn main() {
|
||||
let mut path = env::args().nth(1).unwrap();
|
||||
let mut parallel = false;
|
||||
let mut simple = false;
|
||||
let (tx, rx) = crossbeam_channel::bounded::<DirEntry>(100);
|
||||
if path == "parallel" {
|
||||
path = env::args().nth(2).unwrap();
|
||||
parallel = true;
|
||||
} else if path == "walkdir" {
|
||||
path = env::args().nth(2).unwrap();
|
||||
simple = true;
|
||||
}
|
||||
|
||||
let stdout_thread = std::thread::spawn(move || {
|
||||
let mut stdout = std::io::BufWriter::new(std::io::stdout());
|
||||
for dent in rx {
|
||||
stdout.write_all(&Vec::from_path_lossy(dent.path())).unwrap();
|
||||
stdout.write_all(b"\n").unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
if parallel {
|
||||
let walker = WalkBuilder::new(path).threads(6).build_parallel();
|
||||
walker.run(|| {
|
||||
let tx = tx.clone();
|
||||
Box::new(move |result| {
|
||||
use ignore::WalkState::*;
|
||||
|
||||
tx.send(DirEntry::Y(result.unwrap())).unwrap();
|
||||
Continue
|
||||
})
|
||||
});
|
||||
} else if simple {
|
||||
let walker = WalkDir::new(path);
|
||||
for result in walker {
|
||||
tx.send(DirEntry::X(result.unwrap())).unwrap();
|
||||
}
|
||||
} else {
|
||||
let walker = WalkBuilder::new(path).build();
|
||||
for result in walker {
|
||||
tx.send(DirEntry::Y(result.unwrap())).unwrap();
|
||||
}
|
||||
}
|
||||
drop(tx);
|
||||
stdout_thread.join().unwrap();
|
||||
}
|
||||
|
||||
enum DirEntry {
|
||||
X(walkdir::DirEntry),
|
||||
Y(ignore::DirEntry),
|
||||
}
|
||||
|
||||
impl DirEntry {
|
||||
fn path(&self) -> &Path {
|
||||
match *self {
|
||||
DirEntry::X(ref x) => x.path(),
|
||||
DirEntry::Y(ref y) => y.path(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,378 +0,0 @@
|
|||
/// This list represents the default file types that ripgrep ships with. In
|
||||
/// general, any file format is fair game, although it should generally be
|
||||
/// limited to reasonably popular open formats. For other cases, you can add
|
||||
/// types to each invocation of ripgrep with the '--type-add' flag.
|
||||
///
|
||||
/// If you would like to add or improve this list, please file a PR:
|
||||
/// <https://github.com/BurntSushi/ripgrep>.
|
||||
///
|
||||
/// Please try to keep this list sorted lexicographically and wrapped to 79
|
||||
/// columns (inclusive).
|
||||
#[rustfmt::skip]
|
||||
pub(crate) const DEFAULT_TYPES: &[(&[&str], &[&str])] = &[
|
||||
(&["ada"], &["*.adb", "*.ads"]),
|
||||
(&["agda"], &["*.agda", "*.lagda"]),
|
||||
(&["aidl"], &["*.aidl"]),
|
||||
(&["alire"], &["alire.toml"]),
|
||||
(&["amake"], &["*.mk", "*.bp"]),
|
||||
(&["asciidoc"], &["*.adoc", "*.asc", "*.asciidoc"]),
|
||||
(&["asm"], &["*.asm", "*.s", "*.S"]),
|
||||
(&["asp"], &[
|
||||
"*.aspx", "*.aspx.cs", "*.aspx.vb", "*.ascx", "*.ascx.cs",
|
||||
"*.ascx.vb", "*.asp"
|
||||
]),
|
||||
(&["ats"], &["*.ats", "*.dats", "*.sats", "*.hats"]),
|
||||
(&["avro"], &["*.avdl", "*.avpr", "*.avsc"]),
|
||||
(&["awk"], &["*.awk"]),
|
||||
(&["bat", "batch"], &["*.bat"]),
|
||||
(&["bazel"], &[
|
||||
"*.bazel", "*.bzl", "*.BUILD", "*.bazelrc", "BUILD", "MODULE.bazel",
|
||||
"WORKSPACE", "WORKSPACE.bazel", "WORKSPACE.bzlmod",
|
||||
]),
|
||||
(&["bitbake"], &["*.bb", "*.bbappend", "*.bbclass", "*.conf", "*.inc"]),
|
||||
(&["boxlang"], &["*.bx", "*.bxm", "*.bxs"]),
|
||||
(&["brotli"], &["*.br"]),
|
||||
(&["buildstream"], &["*.bst"]),
|
||||
(&["bzip2"], &["*.bz2", "*.tbz2"]),
|
||||
(&["c"], &["*.[chH]", "*.[chH].in", "*.cats"]),
|
||||
(&["cabal"], &["*.cabal"]),
|
||||
(&["candid"], &["*.did"]),
|
||||
(&["carp"], &["*.carp"]),
|
||||
(&["cbor"], &["*.cbor"]),
|
||||
(&["ceylon"], &["*.ceylon"]),
|
||||
(&["cfml"], &["*.cfc", "*.cfm"]),
|
||||
(&["clojure"], &["*.clj", "*.cljc", "*.cljs", "*.cljx"]),
|
||||
(&["cmake"], &["*.cmake", "CMakeLists.txt"]),
|
||||
(&["cmd"], &["*.bat", "*.cmd"]),
|
||||
(&["cml"], &["*.cml"]),
|
||||
(&["coffeescript"], &["*.coffee"]),
|
||||
(&["config"], &["*.cfg", "*.conf", "*.config", "*.ini"]),
|
||||
(&["container"], &["*Containerfile*", "*Dockerfile*"]),
|
||||
(&["coq"], &["*.v"]),
|
||||
(&["cpp"], &[
|
||||
"*.[ChH]", "*.cc", "*.[ch]pp", "*.[ch]xx", "*.hh", "*.inl",
|
||||
"*.[ChH].in", "*.cc.in", "*.[ch]pp.in", "*.[ch]xx.in", "*.hh.in",
|
||||
]),
|
||||
(&["creole"], &["*.creole"]),
|
||||
(&["crystal"], &["Projectfile", "*.cr", "*.ecr", "shard.yml"]),
|
||||
(&["cs"], &["*.cs"]),
|
||||
(&["csharp"], &["*.cs"]),
|
||||
(&["cshtml"], &["*.cshtml"]),
|
||||
(&["csproj"], &["*.csproj"]),
|
||||
(&["css"], &["*.css", "*.scss"]),
|
||||
(&["csv"], &["*.csv"]),
|
||||
(&["cuda"], &["*.cu", "*.cuh"]),
|
||||
(&["cython"], &["*.pyx", "*.pxi", "*.pxd"]),
|
||||
(&["d"], &["*.d"]),
|
||||
(&["dart"], &["*.dart"]),
|
||||
(&["devicetree"], &["*.dts", "*.dtsi", "*.dtso"]),
|
||||
(&["dhall"], &["*.dhall"]),
|
||||
(&["diff"], &["*.patch", "*.diff"]),
|
||||
(&["dita"], &["*.dita", "*.ditamap", "*.ditaval"]),
|
||||
(&["docker"], &["*Dockerfile*"]),
|
||||
(&["dockercompose"], &["docker-compose.yml", "docker-compose.*.yml"]),
|
||||
(&["dts"], &["*.dts", "*.dtsi"]),
|
||||
(&["dvc"], &["Dvcfile", "*.dvc"]),
|
||||
(&["ebuild"], &["*.ebuild", "*.eclass"]),
|
||||
(&["edn"], &["*.edn"]),
|
||||
(&["elisp"], &["*.el"]),
|
||||
(&["elixir"], &["*.ex", "*.eex", "*.exs", "*.heex", "*.leex", "*.livemd"]),
|
||||
(&["elm"], &["*.elm"]),
|
||||
(&["erb"], &["*.erb"]),
|
||||
(&["erlang"], &["*.erl", "*.hrl"]),
|
||||
(&["fennel"], &["*.fnl"]),
|
||||
(&["fidl"], &["*.fidl"]),
|
||||
(&["fish"], &["*.fish"]),
|
||||
(&["flatbuffers"], &["*.fbs"]),
|
||||
(&["fortran"], &[
|
||||
"*.f", "*.F", "*.f77", "*.F77", "*.pfo",
|
||||
"*.f90", "*.F90", "*.f95", "*.F95",
|
||||
]),
|
||||
(&["fsharp"], &["*.fs", "*.fsx", "*.fsi"]),
|
||||
(&["fut"], &["*.fut"]),
|
||||
(&["gap"], &["*.g", "*.gap", "*.gi", "*.gd", "*.tst"]),
|
||||
(&["gdscript"], &["*.gd"]),
|
||||
(&["gleam"], &["*.gleam"]),
|
||||
(&["gn"], &["*.gn", "*.gni"]),
|
||||
(&["go"], &["*.go"]),
|
||||
(&["gprbuild"], &["*.gpr"]),
|
||||
(&["gradle"], &[
|
||||
"*.gradle", "*.gradle.kts", "gradle.properties", "gradle-wrapper.*",
|
||||
"gradlew", "gradlew.bat",
|
||||
]),
|
||||
(&["graphql"], &["*.graphql", "*.graphqls"]),
|
||||
(&["groovy"], &["*.groovy", "*.gradle"]),
|
||||
(&["gzip"], &["*.gz", "*.tgz"]),
|
||||
(&["h"], &["*.h", "*.hh", "*.hpp"]),
|
||||
(&["haml"], &["*.haml"]),
|
||||
(&["hare"], &["*.ha"]),
|
||||
(&["haskell"], &["*.hs", "*.lhs", "*.cpphs", "*.c2hs", "*.hsc"]),
|
||||
(&["hbs"], &["*.hbs"]),
|
||||
(&["hs"], &["*.hs", "*.lhs"]),
|
||||
(&["html"], &["*.htm", "*.html", "*.ejs"]),
|
||||
(&["hurl"], &["*.hurl"]),
|
||||
(&["hy"], &["*.hy"]),
|
||||
(&["idris"], &["*.idr", "*.lidr"]),
|
||||
(&["janet"], &["*.janet"]),
|
||||
(&["java"], &["*.java", "*.jsp", "*.jspx", "*.properties"]),
|
||||
(&["jinja"], &["*.j2", "*.jinja", "*.jinja2"]),
|
||||
(&["jl"], &["*.jl"]),
|
||||
(&["js"], &["*.js", "*.jsx", "*.vue", "*.cjs", "*.mjs"]),
|
||||
(&["json"], &["*.json", "composer.lock", "*.sarif"]),
|
||||
(&["jsonl"], &["*.jsonl"]),
|
||||
(&["julia"], &["*.jl"]),
|
||||
(&["jupyter"], &["*.ipynb", "*.jpynb"]),
|
||||
(&["k"], &["*.k"]),
|
||||
(&["kconfig"], &["Kconfig", "Kconfig.*"]),
|
||||
(&["kotlin"], &["*.kt", "*.kts"]),
|
||||
(&["lean"], &["*.lean"]),
|
||||
(&["less"], &["*.less"]),
|
||||
(&["license"], &[
|
||||
// General
|
||||
"COPYING", "COPYING[.-]*",
|
||||
"COPYRIGHT", "COPYRIGHT[.-]*",
|
||||
"EULA", "EULA[.-]*",
|
||||
"licen[cs]e", "licen[cs]e.*",
|
||||
"LICEN[CS]E", "LICEN[CS]E[.-]*", "*[.-]LICEN[CS]E*",
|
||||
"NOTICE", "NOTICE[.-]*",
|
||||
"PATENTS", "PATENTS[.-]*",
|
||||
"UNLICEN[CS]E", "UNLICEN[CS]E[.-]*",
|
||||
// GPL (gpl.txt, etc.)
|
||||
"agpl[.-]*",
|
||||
"gpl[.-]*",
|
||||
"lgpl[.-]*",
|
||||
// Other license-specific (APACHE-2.0.txt, etc.)
|
||||
"AGPL-*[0-9]*",
|
||||
"APACHE-*[0-9]*",
|
||||
"BSD-*[0-9]*",
|
||||
"CC-BY-*",
|
||||
"GFDL-*[0-9]*",
|
||||
"GNU-*[0-9]*",
|
||||
"GPL-*[0-9]*",
|
||||
"LGPL-*[0-9]*",
|
||||
"MIT-*[0-9]*",
|
||||
"MPL-*[0-9]*",
|
||||
"OFL-*[0-9]*",
|
||||
]),
|
||||
(&["lilypond"], &["*.ly", "*.ily"]),
|
||||
(&["lisp"], &["*.el", "*.jl", "*.lisp", "*.lsp", "*.sc", "*.scm"]),
|
||||
(&["llvm"], &["*.ll"]),
|
||||
(&["lock"], &["*.lock", "package-lock.json"]),
|
||||
(&["log"], &["*.log"]),
|
||||
(&["lua"], &["*.lua"]),
|
||||
(&["lz4"], &["*.lz4"]),
|
||||
(&["lzma"], &["*.lzma"]),
|
||||
(&["m4"], &["*.ac", "*.m4"]),
|
||||
(&["make"], &[
|
||||
"[Gg][Nn][Uu]makefile", "[Mm]akefile",
|
||||
"[Gg][Nn][Uu]makefile.am", "[Mm]akefile.am",
|
||||
"[Gg][Nn][Uu]makefile.in", "[Mm]akefile.in",
|
||||
"Makefile.*",
|
||||
"*.mk", "*.mak"
|
||||
]),
|
||||
(&["mako"], &["*.mako", "*.mao"]),
|
||||
(&["man"], &["*.[0-9lnpx]", "*.[0-9][cEFMmpSx]"]),
|
||||
(&["markdown", "md"], &[
|
||||
"*.markdown",
|
||||
"*.md",
|
||||
"*.mdown",
|
||||
"*.mdwn",
|
||||
"*.mkd",
|
||||
"*.mkdn",
|
||||
"*.mdx",
|
||||
]),
|
||||
(&["matlab"], &["*.m"]),
|
||||
(&["meson"], &["meson.build", "meson_options.txt", "meson.options"]),
|
||||
(&["minified"], &["*.min.html", "*.min.css", "*.min.js"]),
|
||||
(&["mint"], &["*.mint"]),
|
||||
(&["mk"], &["mkfile"]),
|
||||
(&["ml"], &["*.ml"]),
|
||||
(&["mojo"], &["*.mojo"]),
|
||||
(&["motoko"], &["*.mo"]),
|
||||
(&["msbuild"], &[
|
||||
"*.csproj", "*.fsproj", "*.vcxproj", "*.proj", "*.props", "*.targets",
|
||||
"*.sln", "*.slnf"
|
||||
]),
|
||||
(&["nim"], &["*.nim", "*.nimf", "*.nimble", "*.nims"]),
|
||||
(&["nix"], &["*.nix"]),
|
||||
(&["objc"], &["*.h", "*.m"]),
|
||||
(&["objcpp"], &["*.h", "*.mm"]),
|
||||
(&["ocaml"], &["*.ml", "*.mli", "*.mll", "*.mly"]),
|
||||
(&["org"], &["*.org", "*.org_archive"]),
|
||||
(&["pants"], &["BUILD"]),
|
||||
(&["pascal"], &["*.pas", "*.dpr", "*.lpr", "*.pp", "*.inc"]),
|
||||
(&["pdf"], &["*.pdf"]),
|
||||
(&["perl"], &["*.perl", "*.pl", "*.PL", "*.plh", "*.plx", "*.pm", "*.t"]),
|
||||
(&["php"], &[
|
||||
// note that PHP 6 doesn't exist
|
||||
// See: https://wiki.php.net/rfc/php6
|
||||
"*.php", "*.php3", "*.php4", "*.php5", "*.php7", "*.php8",
|
||||
"*.pht", "*.phtml"
|
||||
]),
|
||||
(&["pkgbuild"], &["PKGBUILD"]),
|
||||
(&["po"], &["*.po"]),
|
||||
(&["pod"], &["*.pod"]),
|
||||
(&["postscript"], &["*.eps", "*.ps"]),
|
||||
(&["prolog"], &["*.pl", "*.pro", "*.prolog", "*.P"]),
|
||||
(&["proto", "protobuf"], &["*.proto"]),
|
||||
(&["ps"], &["*.cdxml", "*.ps1", "*.ps1xml", "*.psd1", "*.psm1"]),
|
||||
(&["puppet"], &["*.epp", "*.erb", "*.pp", "*.rb"]),
|
||||
(&["purs"], &["*.purs"]),
|
||||
(&["py", "python"], &["*.py", "*.pyi"]),
|
||||
(&["qmake"], &["*.pro", "*.pri", "*.prf"]),
|
||||
(&["qml"], &["*.qml"]),
|
||||
(&["qrc"], &["*.qrc"]),
|
||||
(&["qui"], &["*.ui"]),
|
||||
(&["r"], &["*.R", "*.r", "*.Rmd", "*.rmd", "*.Rnw", "*.rnw"]),
|
||||
(&["racket"], &["*.rkt"]),
|
||||
(&["raku"], &[
|
||||
"*.raku", "*.rakumod", "*.rakudoc", "*.rakutest",
|
||||
"*.p6", "*.pl6", "*.pm6"
|
||||
]),
|
||||
(&["rdoc"], &["*.rdoc"]),
|
||||
(&["readme"], &["README*", "*README"]),
|
||||
(&["reasonml"], &["*.re", "*.rei"]),
|
||||
(&["red"], &["*.r", "*.red", "*.reds"]),
|
||||
(&["rescript"], &["*.res", "*.resi"]),
|
||||
(&["robot"], &["*.robot"]),
|
||||
(&["rocq"], &["*.v"]),
|
||||
(&["rst"], &["*.rst"]),
|
||||
(&["ruby"], &[
|
||||
// Idiomatic files
|
||||
"config.ru", "Gemfile", ".irbrc", "Rakefile",
|
||||
// Extensions
|
||||
"*.gemspec", "*.rb", "*.rbw", "*.rake"
|
||||
]),
|
||||
(&["rust"], &["*.rs"]),
|
||||
(&["sass"], &["*.sass", "*.scss"]),
|
||||
(&["scala"], &["*.scala", "*.sbt"]),
|
||||
(&["scdoc"], &["*.scd", "*.scdoc"]),
|
||||
(&["seed7"], &["*.sd7", "*.s7i"]),
|
||||
(&["sh"], &[
|
||||
// Portable/misc. init files
|
||||
".env", ".login", ".logout", ".profile", "profile",
|
||||
// bash-specific init files
|
||||
".bash_login", "bash_login",
|
||||
".bash_logout", "bash_logout",
|
||||
".bash_profile", "bash_profile",
|
||||
".bashrc", "bashrc", "*.bashrc",
|
||||
// csh-specific init files
|
||||
".cshrc", "*.cshrc",
|
||||
// ksh-specific init files
|
||||
".kshrc", "*.kshrc",
|
||||
// tcsh-specific init files
|
||||
".tcshrc",
|
||||
// zsh-specific init files
|
||||
".zshenv", "zshenv",
|
||||
".zlogin", "zlogin",
|
||||
".zlogout", "zlogout",
|
||||
".zprofile", "zprofile",
|
||||
".zshrc", "zshrc",
|
||||
// Extensions
|
||||
"*.bash", "*.csh", "*.env", "*.ksh", "*.sh", "*.tcsh", "*.zsh",
|
||||
]),
|
||||
(&["slim"], &["*.skim", "*.slim", "*.slime"]),
|
||||
(&["smarty"], &["*.tpl"]),
|
||||
(&["sml"], &["*.sml", "*.sig"]),
|
||||
(&["solidity"], &["*.sol"]),
|
||||
(&["soy"], &["*.soy"]),
|
||||
(&["spark"], &["*.spark"]),
|
||||
(&["spec"], &["*.spec"]),
|
||||
(&["sql"], &["*.sql", "*.psql"]),
|
||||
(&["ssa"], &["*.ssa"]),
|
||||
(&["stylus"], &["*.styl"]),
|
||||
(&["sv"], &["*.v", "*.vg", "*.sv", "*.svh", "*.h"]),
|
||||
(&["svelte"], &["*.svelte", "*.svelte.ts"]),
|
||||
(&["svg"], &["*.svg"]),
|
||||
(&["swift"], &["*.swift"]),
|
||||
(&["swig"], &["*.def", "*.i"]),
|
||||
(&["systemd"], &[
|
||||
"*.automount", "*.conf", "*.device", "*.link", "*.mount", "*.path",
|
||||
"*.scope", "*.service", "*.slice", "*.socket", "*.swap", "*.target",
|
||||
"*.timer",
|
||||
]),
|
||||
(&["taskpaper"], &["*.taskpaper"]),
|
||||
(&["tcl"], &["*.tcl"]),
|
||||
(&["tex"], &["*.tex", "*.ltx", "*.cls", "*.sty", "*.bib", "*.dtx", "*.ins"]),
|
||||
(&["texinfo"], &["*.texi"]),
|
||||
(&["textile"], &["*.textile"]),
|
||||
(&["tf"], &[
|
||||
"*.tf", "*.tf.json", "*.tfvars", "*.tfvars.json",
|
||||
"*.terraformrc", "terraform.rc", "*.tfrc", "*.terraform.lock.hcl",
|
||||
]),
|
||||
(&["thrift"], &["*.thrift"]),
|
||||
(&["toml"], &["*.toml", "Cargo.lock"]),
|
||||
(&["ts", "typescript"], &["*.ts", "*.tsx", "*.cts", "*.mts"]),
|
||||
(&["twig"], &["*.twig"]),
|
||||
(&["txt"], &["*.txt"]),
|
||||
(&["typoscript"], &["*.typoscript", "*.ts"]),
|
||||
(&["typst"], &["*.typ"]),
|
||||
(&["usd"], &["*.usd", "*.usda", "*.usdc"]),
|
||||
(&["v"], &["*.v", "*.vsh"]),
|
||||
(&["vala"], &["*.vala"]),
|
||||
(&["vb"], &["*.vb"]),
|
||||
(&["vcl"], &["*.vcl"]),
|
||||
(&["verilog"], &["*.v", "*.vh", "*.sv", "*.svh"]),
|
||||
(&["vhdl"], &["*.vhd", "*.vhdl"]),
|
||||
(&["vim"], &[
|
||||
"*.vim", ".vimrc", ".gvimrc", "vimrc", "gvimrc", "_vimrc", "_gvimrc",
|
||||
]),
|
||||
(&["vimscript"], &[
|
||||
"*.vim", ".vimrc", ".gvimrc", "vimrc", "gvimrc", "_vimrc", "_gvimrc",
|
||||
]),
|
||||
(&["vue"], &["*.vue"]),
|
||||
(&["webidl"], &["*.idl", "*.webidl", "*.widl"]),
|
||||
(&["wgsl"], &["*.wgsl"]),
|
||||
(&["wiki"], &["*.mediawiki", "*.wiki"]),
|
||||
(&["xml"], &[
|
||||
"*.xml", "*.xml.dist", "*.dtd", "*.xsl", "*.xslt", "*.xsd", "*.xjb",
|
||||
"*.rng", "*.sch", "*.xhtml",
|
||||
]),
|
||||
(&["xz"], &["*.xz", "*.txz"]),
|
||||
(&["yacc"], &["*.y"]),
|
||||
(&["yaml"], &["*.yaml", "*.yml"]),
|
||||
(&["yang"], &["*.yang"]),
|
||||
(&["z"], &["*.Z"]),
|
||||
(&["zig"], &["*.zig"]),
|
||||
(&["zsh"], &[
|
||||
".zshenv", "zshenv",
|
||||
".zlogin", "zlogin",
|
||||
".zlogout", "zlogout",
|
||||
".zprofile", "zprofile",
|
||||
".zshrc", "zshrc",
|
||||
"*.zsh",
|
||||
]),
|
||||
(&["zstd"], &["*.zst", "*.zstd"]),
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::DEFAULT_TYPES;
|
||||
|
||||
#[test]
|
||||
fn default_types_are_sorted() {
|
||||
let mut names = DEFAULT_TYPES.iter().map(|(aliases, _)| aliases[0]);
|
||||
let Some(mut previous_name) = names.next() else {
|
||||
return;
|
||||
};
|
||||
for name in names {
|
||||
assert!(
|
||||
name > previous_name,
|
||||
r#""{}" should be sorted before "{}" in `DEFAULT_TYPES`"#,
|
||||
name,
|
||||
previous_name
|
||||
);
|
||||
previous_name = name;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_types_aliases_are_sorted() {
|
||||
for (aliases, _) in DEFAULT_TYPES.iter() {
|
||||
assert!(
|
||||
aliases.is_sorted(),
|
||||
"this alias list is not sorted: {aliases:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,913 +0,0 @@
|
|||
/*!
|
||||
The gitignore module provides a way to match globs from a gitignore file
|
||||
against file paths.
|
||||
|
||||
Note that this module implements the specification as described in the
|
||||
`gitignore` man page from scratch. That is, this module does *not* shell out to
|
||||
the `git` command line tool.
|
||||
*/
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Read},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use {
|
||||
globset::{Candidate, GlobBuilder, GlobSet, GlobSetBuilder},
|
||||
regex_automata::util::pool::Pool,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
Error, Match, PartialErrorBuilder,
|
||||
pathutil::{is_file_name, strip_prefix},
|
||||
};
|
||||
|
||||
/// Glob represents a single glob in a gitignore file.
|
||||
///
|
||||
/// This is used to report information about the highest precedent glob that
|
||||
/// matched in one or more gitignore files.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Glob {
|
||||
/// The file path that this glob was extracted from.
|
||||
from: Option<PathBuf>,
|
||||
/// The original glob string.
|
||||
original: String,
|
||||
/// The actual glob string used to convert to a regex.
|
||||
actual: String,
|
||||
/// Whether this is a whitelisted glob or not.
|
||||
is_whitelist: bool,
|
||||
/// Whether this glob should only match directories or not.
|
||||
is_only_dir: bool,
|
||||
}
|
||||
|
||||
impl Glob {
|
||||
/// Returns the file path that defined this glob.
|
||||
pub fn from(&self) -> Option<&Path> {
|
||||
self.from.as_ref().map(|p| &**p)
|
||||
}
|
||||
|
||||
/// The original glob as it was defined in a gitignore file.
|
||||
pub fn original(&self) -> &str {
|
||||
&self.original
|
||||
}
|
||||
|
||||
/// The actual glob that was compiled to respect gitignore
|
||||
/// semantics.
|
||||
pub fn actual(&self) -> &str {
|
||||
&self.actual
|
||||
}
|
||||
|
||||
/// Whether this was a whitelisted glob or not.
|
||||
pub fn is_whitelist(&self) -> bool {
|
||||
self.is_whitelist
|
||||
}
|
||||
|
||||
/// Whether this glob must match a directory or not.
|
||||
pub fn is_only_dir(&self) -> bool {
|
||||
self.is_only_dir
|
||||
}
|
||||
|
||||
/// Returns true if and only if this glob has a `**/` prefix.
|
||||
fn has_doublestar_prefix(&self) -> bool {
|
||||
self.actual.starts_with("**/") || self.actual == "**"
|
||||
}
|
||||
}
|
||||
|
||||
/// Gitignore is a matcher for the globs in one or more gitignore files
|
||||
/// in the same directory.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Gitignore {
|
||||
set: GlobSet,
|
||||
root: PathBuf,
|
||||
globs: Vec<Glob>,
|
||||
num_ignores: u64,
|
||||
num_whitelists: u64,
|
||||
matches: Option<Arc<Pool<Vec<usize>>>>,
|
||||
// CHANGED: Add a flag to have Gitignore rules that apply only to files.
|
||||
only_on_files: bool,
|
||||
}
|
||||
|
||||
impl Gitignore {
|
||||
/// Creates a new gitignore matcher from the gitignore file path given.
|
||||
///
|
||||
/// If it's desirable to include multiple gitignore files in a single
|
||||
/// matcher, or read gitignore globs from a different source, then
|
||||
/// use `GitignoreBuilder`.
|
||||
///
|
||||
/// This always returns a valid matcher, even if it's empty. In particular,
|
||||
/// a Gitignore file can be partially valid, e.g., when one glob is invalid
|
||||
/// but the rest aren't.
|
||||
///
|
||||
/// Note that I/O errors are ignored. For more granular control over
|
||||
/// errors, use `GitignoreBuilder`.
|
||||
pub fn new<P: AsRef<Path>>(
|
||||
gitignore_path: P,
|
||||
) -> (Gitignore, Option<Error>) {
|
||||
let path = gitignore_path.as_ref();
|
||||
let parent = path.parent().unwrap_or(Path::new("/"));
|
||||
let mut builder = GitignoreBuilder::new(parent);
|
||||
let mut errs = PartialErrorBuilder::default();
|
||||
errs.maybe_push_ignore_io(builder.add(path));
|
||||
match builder.build() {
|
||||
Ok(gi) => (gi, errs.into_error_option()),
|
||||
Err(err) => {
|
||||
errs.push(err);
|
||||
(Gitignore::empty(), errs.into_error_option())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new gitignore matcher from the global ignore file, if one
|
||||
/// exists.
|
||||
///
|
||||
/// The global config file path is specified by git's `core.excludesFile`
|
||||
/// config option.
|
||||
///
|
||||
/// # Behavior
|
||||
///
|
||||
/// This routine does its best to discover any global git exclude files.
|
||||
/// This will try to parse out the `excludesFile` config option in your
|
||||
/// global git configuration, if necessary.
|
||||
///
|
||||
/// The specific things this routine tries (which are subject to change
|
||||
/// based on how git behaves) are:
|
||||
///
|
||||
///
|
||||
///
|
||||
/// Git's config file location is `$HOME/.gitconfig`. If `$HOME/.gitconfig`
|
||||
/// does not exist or does not specify `core.excludesFile`, then
|
||||
/// `$XDG_CONFIG_HOME/git/ignore` is read. If `$XDG_CONFIG_HOME` is not
|
||||
/// set or is empty, then `$HOME/.config/git/ignore` is used instead.
|
||||
pub fn global() -> (Gitignore, Option<Error>) {
|
||||
match std::env::current_dir() {
|
||||
Ok(cwd) => GitignoreBuilder::new(cwd).build_global(),
|
||||
Err(err) => (Gitignore::empty(), Some(err.into())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new empty gitignore matcher that never matches anything.
|
||||
///
|
||||
/// Its path is empty.
|
||||
pub fn empty() -> Gitignore {
|
||||
Gitignore {
|
||||
set: GlobSet::empty(),
|
||||
root: PathBuf::from(""),
|
||||
globs: vec![],
|
||||
num_ignores: 0,
|
||||
num_whitelists: 0,
|
||||
matches: None,
|
||||
// CHANGED: Add a flag to have Gitignore rules that apply only to
|
||||
// files.
|
||||
only_on_files: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the directory containing this gitignore matcher.
|
||||
///
|
||||
/// All matches are done relative to this path.
|
||||
pub fn path(&self) -> &Path {
|
||||
&*self.root
|
||||
}
|
||||
|
||||
/// Returns true if and only if this gitignore has zero globs, and
|
||||
/// therefore never matches any file path.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.set.is_empty()
|
||||
}
|
||||
|
||||
/// Returns the total number of globs, which should be equivalent to
|
||||
/// `num_ignores + num_whitelists`.
|
||||
pub fn len(&self) -> usize {
|
||||
self.set.len()
|
||||
}
|
||||
|
||||
/// Returns the total number of ignore globs.
|
||||
pub fn num_ignores(&self) -> u64 {
|
||||
self.num_ignores
|
||||
}
|
||||
|
||||
/// Returns the total number of whitelisted globs.
|
||||
pub fn num_whitelists(&self) -> u64 {
|
||||
self.num_whitelists
|
||||
}
|
||||
|
||||
/// Returns whether the given path (file or directory) matched a pattern in
|
||||
/// this gitignore matcher.
|
||||
///
|
||||
/// `is_dir` should be true if the path refers to a directory and false
|
||||
/// otherwise.
|
||||
///
|
||||
/// The given path is matched relative to the path given when building
|
||||
/// the matcher. Specifically, before matching `path`, its prefix (as
|
||||
/// determined by a common suffix of the directory containing this
|
||||
/// gitignore) is stripped. If there is no common suffix/prefix overlap,
|
||||
/// then `path` is assumed to be relative to this matcher.
|
||||
pub fn matched<P: AsRef<Path>>(
|
||||
&self,
|
||||
path: P,
|
||||
is_dir: bool,
|
||||
) -> Match<&Glob> {
|
||||
if self.is_empty() {
|
||||
return Match::None;
|
||||
}
|
||||
self.matched_stripped(self.strip(path.as_ref()), is_dir)
|
||||
}
|
||||
|
||||
/// Returns whether the given path (file or directory, and expected to be
|
||||
/// under the root) or any of its parent directories (up to the root)
|
||||
/// matched a pattern in this gitignore matcher.
|
||||
///
|
||||
/// NOTE: This method is more expensive than walking the directory hierarchy
|
||||
/// top-to-bottom and matching the entries. But, is easier to use in cases
|
||||
/// when a list of paths are available without a hierarchy.
|
||||
///
|
||||
/// `is_dir` should be true if the path refers to a directory and false
|
||||
/// otherwise.
|
||||
///
|
||||
/// The given path is matched relative to the path given when building
|
||||
/// the matcher. Specifically, before matching `path`, its prefix (as
|
||||
/// determined by a common suffix of the directory containing this
|
||||
/// gitignore) is stripped. If there is no common suffix/prefix overlap,
|
||||
/// then `path` is assumed to be relative to this matcher.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This method panics if the given file path is not under the root path
|
||||
/// of this matcher.
|
||||
pub fn matched_path_or_any_parents<P: AsRef<Path>>(
|
||||
&self,
|
||||
path: P,
|
||||
is_dir: bool,
|
||||
) -> Match<&Glob> {
|
||||
if self.is_empty() {
|
||||
return Match::None;
|
||||
}
|
||||
let mut path = self.strip(path.as_ref());
|
||||
assert!(!path.has_root(), "path is expected to be under the root");
|
||||
|
||||
match self.matched_stripped(path, is_dir) {
|
||||
Match::None => (), // walk up
|
||||
a_match => return a_match,
|
||||
}
|
||||
while let Some(parent) = path.parent() {
|
||||
match self.matched_stripped(parent, /* is_dir */ true) {
|
||||
Match::None => path = parent, // walk up
|
||||
a_match => return a_match,
|
||||
}
|
||||
}
|
||||
Match::None
|
||||
}
|
||||
|
||||
/// Like matched, but takes a path that has already been stripped.
|
||||
fn matched_stripped<P: AsRef<Path>>(
|
||||
&self,
|
||||
path: P,
|
||||
is_dir: bool,
|
||||
) -> Match<&Glob> {
|
||||
if self.is_empty() {
|
||||
return Match::None;
|
||||
}
|
||||
// CHANGED: Rules marked as only_on_files can not match against
|
||||
// directories.
|
||||
if self.only_on_files && is_dir {
|
||||
return Match::None;
|
||||
}
|
||||
let path = path.as_ref();
|
||||
let mut matches = self.matches.as_ref().unwrap().get();
|
||||
let candidate = Candidate::new(path);
|
||||
self.set.matches_candidate_into(&candidate, &mut *matches);
|
||||
for &i in matches.iter().rev() {
|
||||
let glob = &self.globs[i];
|
||||
if !glob.is_only_dir() || is_dir {
|
||||
return if glob.is_whitelist() {
|
||||
Match::Whitelist(glob)
|
||||
} else {
|
||||
Match::Ignore(glob)
|
||||
};
|
||||
}
|
||||
}
|
||||
Match::None
|
||||
}
|
||||
|
||||
/// Strips the given path such that it's suitable for matching with this
|
||||
/// gitignore matcher.
|
||||
fn strip<'a, P: 'a + AsRef<Path> + ?Sized>(
|
||||
&'a self,
|
||||
path: &'a P,
|
||||
) -> &'a Path {
|
||||
let mut path = path.as_ref();
|
||||
// A leading ./ is completely superfluous. We also strip it from
|
||||
// our gitignore root path, so we need to strip it from our candidate
|
||||
// path too.
|
||||
if let Some(p) = strip_prefix("./", path) {
|
||||
path = p;
|
||||
}
|
||||
// Strip any common prefix between the candidate path and the root
|
||||
// of the gitignore, to make sure we get relative matching right.
|
||||
// BUT, a file name might not have any directory components to it,
|
||||
// in which case, we don't want to accidentally strip any part of the
|
||||
// file name.
|
||||
//
|
||||
// As an additional special case, if the root is just `.`, then we
|
||||
// shouldn't try to strip anything, e.g., when path begins with a `.`.
|
||||
if self.root != Path::new(".") && !is_file_name(path) {
|
||||
if let Some(p) = strip_prefix(&self.root, path) {
|
||||
path = p;
|
||||
// If we're left with a leading slash, get rid of it.
|
||||
if let Some(p) = strip_prefix("/", path) {
|
||||
path = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a matcher for a single set of globs from a .gitignore file.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GitignoreBuilder {
|
||||
builder: GlobSetBuilder,
|
||||
root: PathBuf,
|
||||
globs: Vec<Glob>,
|
||||
case_insensitive: bool,
|
||||
allow_unclosed_class: bool,
|
||||
// CHANGED: Add a flag to have Gitignore rules that apply only to files.
|
||||
only_on_files: bool,
|
||||
}
|
||||
|
||||
impl GitignoreBuilder {
|
||||
/// Create a new builder for a gitignore file.
|
||||
///
|
||||
/// The path given should be the path at which the globs for this gitignore
|
||||
/// file should be matched. Note that paths are always matched relative
|
||||
/// to the root path given here. Generally, the root path should correspond
|
||||
/// to the *directory* containing a `.gitignore` file.
|
||||
pub fn new<P: AsRef<Path>>(root: P) -> GitignoreBuilder {
|
||||
let root = root.as_ref();
|
||||
GitignoreBuilder {
|
||||
builder: GlobSetBuilder::new(),
|
||||
root: strip_prefix("./", root).unwrap_or(root).to_path_buf(),
|
||||
globs: vec![],
|
||||
case_insensitive: false,
|
||||
allow_unclosed_class: true,
|
||||
// CHANGED: Add a flag to have Gitignore rules that apply only to
|
||||
// files.
|
||||
only_on_files: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a new matcher from the globs added so far.
|
||||
///
|
||||
/// Once a matcher is built, no new globs can be added to it.
|
||||
pub fn build(&self) -> Result<Gitignore, Error> {
|
||||
let nignore = self.globs.iter().filter(|g| !g.is_whitelist()).count();
|
||||
let nwhite = self.globs.iter().filter(|g| g.is_whitelist()).count();
|
||||
let set = self
|
||||
.builder
|
||||
.build()
|
||||
.map_err(|err| Error::Glob { glob: None, err: err.to_string() })?;
|
||||
Ok(Gitignore {
|
||||
set,
|
||||
root: self.root.clone(),
|
||||
globs: self.globs.clone(),
|
||||
num_ignores: nignore as u64,
|
||||
num_whitelists: nwhite as u64,
|
||||
matches: Some(Arc::new(
|
||||
Pool::with_available_parallelism_capacity(|| vec![]),
|
||||
)),
|
||||
// CHANGED: Add a flag to have Gitignore rules that apply only to
|
||||
// files.
|
||||
only_on_files: self.only_on_files,
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a global gitignore matcher using the configuration in this
|
||||
/// builder.
|
||||
///
|
||||
/// This consumes ownership of the builder unlike `build` because it
|
||||
/// must mutate the builder to add the global gitignore globs.
|
||||
///
|
||||
/// Note that this ignores the path given to this builder's constructor
|
||||
/// and instead derives the path automatically from git's global
|
||||
/// configuration.
|
||||
pub fn build_global(mut self) -> (Gitignore, Option<Error>) {
|
||||
match gitconfig_excludes_path() {
|
||||
None => (Gitignore::empty(), None),
|
||||
Some(path) => {
|
||||
if !path.is_file() {
|
||||
(Gitignore::empty(), None)
|
||||
} else {
|
||||
let mut errs = PartialErrorBuilder::default();
|
||||
errs.maybe_push_ignore_io(self.add(path));
|
||||
match self.build() {
|
||||
Ok(gi) => (gi, errs.into_error_option()),
|
||||
Err(err) => {
|
||||
errs.push(err);
|
||||
(Gitignore::empty(), errs.into_error_option())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add each glob from the file path given.
|
||||
///
|
||||
/// The file given should be formatted as a `gitignore` file.
|
||||
///
|
||||
/// Note that partial errors can be returned. For example, if there was
|
||||
/// a problem adding one glob, an error for that will be returned, but
|
||||
/// all other valid globs will still be added.
|
||||
pub fn add<P: AsRef<Path>>(&mut self, path: P) -> Option<Error> {
|
||||
let path = path.as_ref();
|
||||
let file = match File::open(path) {
|
||||
Err(err) => return Some(Error::Io(err).with_path(path)),
|
||||
Ok(file) => file,
|
||||
};
|
||||
log::debug!("opened gitignore file: {}", path.display());
|
||||
let rdr = BufReader::new(file);
|
||||
let mut errs = PartialErrorBuilder::default();
|
||||
for (i, line) in rdr.lines().enumerate() {
|
||||
let lineno = (i + 1) as u64;
|
||||
let line = match line {
|
||||
Ok(line) => line,
|
||||
Err(err) => {
|
||||
errs.push(Error::Io(err).tagged(path, lineno));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Match Git's handling of .gitignore files that begin with the Unicode BOM
|
||||
const UTF8_BOM: &str = "\u{feff}";
|
||||
let line =
|
||||
if i == 0 { line.trim_start_matches(UTF8_BOM) } else { &line };
|
||||
|
||||
if let Err(err) = self.add_line(Some(path.to_path_buf()), &line) {
|
||||
errs.push(err.tagged(path, lineno));
|
||||
}
|
||||
}
|
||||
errs.into_error_option()
|
||||
}
|
||||
|
||||
/// Add each glob line from the string given.
|
||||
///
|
||||
/// If this string came from a particular `gitignore` file, then its path
|
||||
/// should be provided here.
|
||||
///
|
||||
/// The string given should be formatted as a `gitignore` file.
|
||||
#[cfg(test)]
|
||||
fn add_str(
|
||||
&mut self,
|
||||
from: Option<PathBuf>,
|
||||
gitignore: &str,
|
||||
) -> Result<&mut GitignoreBuilder, Error> {
|
||||
for line in gitignore.lines() {
|
||||
self.add_line(from.clone(), line)?;
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Add a line from a gitignore file to this builder.
|
||||
///
|
||||
/// If this line came from a particular `gitignore` file, then its path
|
||||
/// should be provided here.
|
||||
///
|
||||
/// If the line could not be parsed as a glob, then an error is returned.
|
||||
pub fn add_line(
|
||||
&mut self,
|
||||
from: Option<PathBuf>,
|
||||
mut line: &str,
|
||||
) -> Result<&mut GitignoreBuilder, Error> {
|
||||
#![allow(deprecated)]
|
||||
|
||||
if line.starts_with("#") {
|
||||
return Ok(self);
|
||||
}
|
||||
if !line.ends_with("\\ ") {
|
||||
line = line.trim_right();
|
||||
}
|
||||
if line.is_empty() {
|
||||
return Ok(self);
|
||||
}
|
||||
let mut glob = Glob {
|
||||
from,
|
||||
original: line.to_string(),
|
||||
actual: String::new(),
|
||||
is_whitelist: false,
|
||||
is_only_dir: false,
|
||||
};
|
||||
let mut is_absolute = false;
|
||||
if line.starts_with("\\!") || line.starts_with("\\#") {
|
||||
line = &line[1..];
|
||||
is_absolute = line.chars().nth(0) == Some('/');
|
||||
} else {
|
||||
if line.starts_with("!") {
|
||||
glob.is_whitelist = true;
|
||||
line = &line[1..];
|
||||
}
|
||||
if line.starts_with("/") {
|
||||
// `man gitignore` says that if a glob starts with a slash,
|
||||
// then the glob can only match the beginning of a path
|
||||
// (relative to the location of gitignore). We achieve this by
|
||||
// simply banning wildcards from matching /.
|
||||
line = &line[1..];
|
||||
is_absolute = true;
|
||||
}
|
||||
}
|
||||
// If it ends with a slash, then this should only match directories,
|
||||
// but the slash should otherwise not be used while globbing.
|
||||
if line.as_bytes().last() == Some(&b'/') {
|
||||
glob.is_only_dir = true;
|
||||
line = &line[..line.len() - 1];
|
||||
// If the slash was escaped, then remove the escape.
|
||||
// See: https://github.com/BurntSushi/ripgrep/issues/2236
|
||||
if line.as_bytes().last() == Some(&b'\\') {
|
||||
line = &line[..line.len() - 1];
|
||||
}
|
||||
}
|
||||
glob.actual = line.to_string();
|
||||
// If there is a literal slash, then this is a glob that must match the
|
||||
// entire path name. Otherwise, we should let it match anywhere, so use
|
||||
// a **/ prefix.
|
||||
if !is_absolute && !line.chars().any(|c| c == '/') {
|
||||
// ... but only if we don't already have a **/ prefix.
|
||||
if !glob.has_doublestar_prefix() {
|
||||
glob.actual = format!("**/{}", glob.actual);
|
||||
}
|
||||
}
|
||||
// If the glob ends with `/**`, then we should only match everything
|
||||
// inside a directory, but not the directory itself. Standard globs
|
||||
// will match the directory. So we add `/*` to force the issue.
|
||||
if glob.actual.ends_with("/**") {
|
||||
glob.actual = format!("{}/*", glob.actual);
|
||||
}
|
||||
let parsed = GlobBuilder::new(&glob.actual)
|
||||
.literal_separator(true)
|
||||
.case_insensitive(self.case_insensitive)
|
||||
.backslash_escape(true)
|
||||
.allow_unclosed_class(self.allow_unclosed_class)
|
||||
.build()
|
||||
.map_err(|err| Error::Glob {
|
||||
glob: Some(glob.original.clone()),
|
||||
err: err.kind().to_string(),
|
||||
})?;
|
||||
self.builder.add(parsed);
|
||||
self.globs.push(glob);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Toggle whether the globs should be matched case insensitively or not.
|
||||
///
|
||||
/// When this option is changed, only globs added after the change will be
|
||||
/// affected.
|
||||
///
|
||||
/// This is disabled by default.
|
||||
pub fn case_insensitive(
|
||||
&mut self,
|
||||
yes: bool,
|
||||
) -> Result<&mut GitignoreBuilder, Error> {
|
||||
// TODO: This should not return a `Result`. Fix this in the next semver
|
||||
// release.
|
||||
self.case_insensitive = yes;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Toggle whether unclosed character classes are allowed. When allowed,
|
||||
/// a `[` without a matching `]` is treated literally instead of resulting
|
||||
/// in a parse error.
|
||||
///
|
||||
/// For example, if this is set then the glob `[abc` will be treated as the
|
||||
/// literal string `[abc` instead of returning an error.
|
||||
///
|
||||
/// By default, this is true in order to match established `gitignore`
|
||||
/// semantics. Generally speaking, enabling this leads to worse failure
|
||||
/// modes since the glob parser becomes more permissive. You might want to
|
||||
/// enable this when compatibility (e.g., with POSIX glob implementations)
|
||||
/// is more important than good error messages.
|
||||
pub fn allow_unclosed_class(
|
||||
&mut self,
|
||||
yes: bool,
|
||||
) -> &mut GitignoreBuilder {
|
||||
self.allow_unclosed_class = yes;
|
||||
self
|
||||
}
|
||||
|
||||
/// CHANGED: Add a flag to have Gitignore rules that apply only to files.
|
||||
///
|
||||
/// If this is set, then the globs will only be matched against file paths.
|
||||
/// This will ensure that ignore rules like `*.pages` will _only_ ignore
|
||||
/// files ending in `.pages` and not folders ending in `.pages`.
|
||||
pub fn only_on_files(&mut self, yes: bool) -> &mut GitignoreBuilder {
|
||||
self.only_on_files = yes;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the file path of the current environment's global gitignore file.
|
||||
///
|
||||
/// Note that the file path returned may not exist.
|
||||
pub fn gitconfig_excludes_path() -> Option<PathBuf> {
|
||||
// When GIT_CONFIG_GLOBAL is set, it replaces both $HOME/.gitconfig and
|
||||
// $XDG_CONFIG_HOME/git/config (per git 2.32+). Otherwise, git supports
|
||||
// $HOME/.gitconfig and $XDG_CONFIG_HOME/git/config simultaneously, where
|
||||
// $HOME/.gitconfig takes precedent.
|
||||
gitconfig_global_env_contents()
|
||||
.and_then(|x| parse_excludes_file(&x))
|
||||
.or_else(|| {
|
||||
gitconfig_home_contents().and_then(|x| parse_excludes_file(&x))
|
||||
})
|
||||
.or_else(|| {
|
||||
gitconfig_xdg_contents().and_then(|x| parse_excludes_file(&x))
|
||||
})
|
||||
// System-level config has the lowest priority for core.excludesFile.
|
||||
// GIT_CONFIG_SYSTEM overrides the default /etc/gitconfig path.
|
||||
.or_else(|| {
|
||||
gitconfig_system_contents().and_then(|x| parse_excludes_file(&x))
|
||||
})
|
||||
.or_else(excludes_file_default)
|
||||
}
|
||||
|
||||
/// Returns the file contents of git's global config file from the path
|
||||
/// specified by the `GIT_CONFIG_GLOBAL` environment variable.
|
||||
fn gitconfig_global_env_contents() -> Option<Vec<u8>> {
|
||||
let path = std::env::var_os("GIT_CONFIG_GLOBAL").map(PathBuf::from)?;
|
||||
if path.as_os_str().is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut file = BufReader::new(File::open(path).ok()?);
|
||||
let mut contents = vec![];
|
||||
file.read_to_end(&mut contents).ok().map(|_| contents)
|
||||
}
|
||||
|
||||
/// Returns the file contents of git's system-level config file.
|
||||
///
|
||||
/// Checks `GIT_CONFIG_SYSTEM` first, then falls back to `/etc/gitconfig`.
|
||||
fn gitconfig_system_contents() -> Option<Vec<u8>> {
|
||||
let path = std::env::var_os("GIT_CONFIG_SYSTEM")
|
||||
.map(PathBuf::from)
|
||||
.filter(|x| !x.as_os_str().is_empty())
|
||||
.unwrap_or_else(|| PathBuf::from("/etc/gitconfig"));
|
||||
let mut file = BufReader::new(File::open(path).ok()?);
|
||||
let mut contents = vec![];
|
||||
file.read_to_end(&mut contents).ok().map(|_| contents)
|
||||
}
|
||||
|
||||
/// Returns the file contents of git's global config file, if one exists, in
|
||||
/// the user's home directory.
|
||||
fn gitconfig_home_contents() -> Option<Vec<u8>> {
|
||||
let home = home_dir()?;
|
||||
let mut file = BufReader::new(File::open(home.join(".gitconfig")).ok()?);
|
||||
let mut contents = vec![];
|
||||
file.read_to_end(&mut contents).ok().map(|_| contents)
|
||||
}
|
||||
|
||||
/// Returns the file contents of git's global config file, if one exists, in
|
||||
/// the user's XDG_CONFIG_HOME directory.
|
||||
fn gitconfig_xdg_contents() -> Option<Vec<u8>> {
|
||||
let path = std::env::var_os("XDG_CONFIG_HOME")
|
||||
.map(PathBuf::from)
|
||||
.filter(|x| !x.as_os_str().is_empty())
|
||||
.or_else(|| home_dir().map(|p| p.join(".config")))
|
||||
.map(|x| x.join("git/config"))?;
|
||||
let mut file = BufReader::new(File::open(path).ok()?);
|
||||
let mut contents = vec![];
|
||||
file.read_to_end(&mut contents).ok().map(|_| contents)
|
||||
}
|
||||
|
||||
/// Returns the default file path for a global .gitignore file.
|
||||
///
|
||||
/// Specifically, this respects XDG_CONFIG_HOME.
|
||||
fn excludes_file_default() -> Option<PathBuf> {
|
||||
std::env::var_os("XDG_CONFIG_HOME")
|
||||
.map(PathBuf::from)
|
||||
.filter(|x| !x.as_os_str().is_empty())
|
||||
.or_else(|| home_dir().map(|p| p.join(".config")))
|
||||
.map(|x| x.join("git/ignore"))
|
||||
}
|
||||
|
||||
/// Extract git's `core.excludesfile` config setting from the raw file contents
|
||||
/// given.
|
||||
fn parse_excludes_file(data: &[u8]) -> Option<PathBuf> {
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use regex_automata::{meta::Regex, util::syntax};
|
||||
|
||||
// N.B. This is the lazy approach, and isn't technically correct, but
|
||||
// probably works in more circumstances. I guess we would ideally have
|
||||
// a full INI parser. Yuck.
|
||||
static RE: OnceLock<Regex> = OnceLock::new();
|
||||
let re = RE.get_or_init(|| {
|
||||
Regex::builder()
|
||||
.configure(Regex::config().utf8_empty(false))
|
||||
.syntax(syntax::Config::new().utf8(false))
|
||||
.build(r#"(?im-u)^\s*excludesfile\s*=\s*"?\s*(\S+?)\s*"?\s*$"#)
|
||||
.unwrap()
|
||||
});
|
||||
// We don't care about amortizing allocs here I think. This should only
|
||||
// be called ~once per traversal or so? (Although it's not guaranteed...)
|
||||
let mut caps = re.create_captures();
|
||||
re.captures(data, &mut caps);
|
||||
let span = caps.get_group(1)?;
|
||||
let candidate = &data[span];
|
||||
std::str::from_utf8(candidate).ok().map(|s| PathBuf::from(expand_tilde(s)))
|
||||
}
|
||||
|
||||
/// Expands ~ in file paths to the value of $HOME.
|
||||
fn expand_tilde(path: &str) -> String {
|
||||
let home = match home_dir() {
|
||||
None => return path.to_string(),
|
||||
Some(home) => home.to_string_lossy().into_owned(),
|
||||
};
|
||||
path.replace("~", &home)
|
||||
}
|
||||
|
||||
/// Returns the location of the user's home directory.
|
||||
fn home_dir() -> Option<PathBuf> {
|
||||
// We're fine with using std::env::home_dir for now. Its bugs are, IMO,
|
||||
// pretty minor corner cases.
|
||||
#![allow(deprecated)]
|
||||
std::env::home_dir()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use super::{Gitignore, GitignoreBuilder};
|
||||
|
||||
fn gi_from_str<P: AsRef<Path>>(root: P, s: &str) -> Gitignore {
|
||||
let mut builder = GitignoreBuilder::new(root);
|
||||
builder.add_str(None, s).unwrap();
|
||||
builder.build().unwrap()
|
||||
}
|
||||
|
||||
macro_rules! ignored {
|
||||
($name:ident, $root:expr, $gi:expr, $path:expr) => {
|
||||
ignored!($name, $root, $gi, $path, false);
|
||||
};
|
||||
($name:ident, $root:expr, $gi:expr, $path:expr, $is_dir:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let gi = gi_from_str($root, $gi);
|
||||
assert!(gi.matched($path, $is_dir).is_ignore());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! not_ignored {
|
||||
($name:ident, $root:expr, $gi:expr, $path:expr) => {
|
||||
not_ignored!($name, $root, $gi, $path, false);
|
||||
};
|
||||
($name:ident, $root:expr, $gi:expr, $path:expr, $is_dir:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let gi = gi_from_str($root, $gi);
|
||||
assert!(!gi.matched($path, $is_dir).is_ignore());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const ROOT: &'static str = "/home/foobar/rust/rg";
|
||||
|
||||
ignored!(ig1, ROOT, "months", "months");
|
||||
ignored!(ig2, ROOT, "*.lock", "Cargo.lock");
|
||||
ignored!(ig3, ROOT, "*.rs", "src/main.rs");
|
||||
ignored!(ig4, ROOT, "src/*.rs", "src/main.rs");
|
||||
ignored!(ig5, ROOT, "/*.c", "cat-file.c");
|
||||
ignored!(ig6, ROOT, "/src/*.rs", "src/main.rs");
|
||||
ignored!(ig7, ROOT, "!src/main.rs\n*.rs", "src/main.rs");
|
||||
ignored!(ig8, ROOT, "foo/", "foo", true);
|
||||
ignored!(ig9, ROOT, "**/foo", "foo");
|
||||
ignored!(ig10, ROOT, "**/foo", "src/foo");
|
||||
ignored!(ig11, ROOT, "**/foo/**", "src/foo/bar");
|
||||
ignored!(ig12, ROOT, "**/foo/**", "wat/src/foo/bar/baz");
|
||||
ignored!(ig13, ROOT, "**/foo/bar", "foo/bar");
|
||||
ignored!(ig14, ROOT, "**/foo/bar", "src/foo/bar");
|
||||
ignored!(ig15, ROOT, "abc/**", "abc/x");
|
||||
ignored!(ig16, ROOT, "abc/**", "abc/x/y");
|
||||
ignored!(ig17, ROOT, "abc/**", "abc/x/y/z");
|
||||
ignored!(ig18, ROOT, "a/**/b", "a/b");
|
||||
ignored!(ig19, ROOT, "a/**/b", "a/x/b");
|
||||
ignored!(ig20, ROOT, "a/**/b", "a/x/y/b");
|
||||
ignored!(ig21, ROOT, r"\!xy", "!xy");
|
||||
ignored!(ig22, ROOT, r"\#foo", "#foo");
|
||||
ignored!(ig23, ROOT, "foo", "./foo");
|
||||
ignored!(ig24, ROOT, "target", "grep/target");
|
||||
ignored!(ig25, ROOT, "Cargo.lock", "./tabwriter-bin/Cargo.lock");
|
||||
ignored!(ig26, ROOT, "/foo/bar/baz", "./foo/bar/baz");
|
||||
ignored!(ig27, ROOT, "foo/", "xyz/foo", true);
|
||||
ignored!(ig28, "./src", "/llvm/", "./src/llvm", true);
|
||||
ignored!(ig29, ROOT, "node_modules/ ", "node_modules", true);
|
||||
ignored!(ig30, ROOT, "**/", "foo/bar", true);
|
||||
ignored!(ig31, ROOT, "path1/*", "path1/foo");
|
||||
ignored!(ig32, ROOT, ".a/b", ".a/b");
|
||||
ignored!(ig33, "./", ".a/b", ".a/b");
|
||||
ignored!(ig34, ".", ".a/b", ".a/b");
|
||||
ignored!(ig35, "./.", ".a/b", ".a/b");
|
||||
ignored!(ig36, "././", ".a/b", ".a/b");
|
||||
ignored!(ig37, "././.", ".a/b", ".a/b");
|
||||
ignored!(ig38, ROOT, "\\[", "[");
|
||||
ignored!(ig39, ROOT, "\\?", "?");
|
||||
ignored!(ig40, ROOT, "\\*", "*");
|
||||
ignored!(ig41, ROOT, "\\a", "a");
|
||||
ignored!(ig42, ROOT, "s*.rs", "sfoo.rs");
|
||||
ignored!(ig43, ROOT, "**", "foo.rs");
|
||||
ignored!(ig44, ROOT, "**/**/*", "a/foo.rs");
|
||||
|
||||
not_ignored!(ignot1, ROOT, "amonths", "months");
|
||||
not_ignored!(ignot2, ROOT, "monthsa", "months");
|
||||
not_ignored!(ignot3, ROOT, "/src/*.rs", "src/grep/src/main.rs");
|
||||
not_ignored!(ignot4, ROOT, "/*.c", "mozilla-sha1/sha1.c");
|
||||
not_ignored!(ignot5, ROOT, "/src/*.rs", "src/grep/src/main.rs");
|
||||
not_ignored!(ignot6, ROOT, "*.rs\n!src/main.rs", "src/main.rs");
|
||||
not_ignored!(ignot7, ROOT, "foo/", "foo", false);
|
||||
not_ignored!(ignot8, ROOT, "**/foo/**", "wat/src/afoo/bar/baz");
|
||||
not_ignored!(ignot9, ROOT, "**/foo/**", "wat/src/fooa/bar/baz");
|
||||
not_ignored!(ignot10, ROOT, "**/foo/bar", "foo/src/bar");
|
||||
not_ignored!(ignot11, ROOT, "#foo", "#foo");
|
||||
not_ignored!(ignot12, ROOT, "\n\n\n", "foo");
|
||||
not_ignored!(ignot13, ROOT, "foo/**", "foo", true);
|
||||
not_ignored!(
|
||||
ignot14,
|
||||
"./third_party/protobuf",
|
||||
"m4/ltoptions.m4",
|
||||
"./third_party/protobuf/csharp/src/packages/repositories.config"
|
||||
);
|
||||
not_ignored!(ignot15, ROOT, "!/bar", "foo/bar");
|
||||
not_ignored!(ignot16, ROOT, "*\n!**/", "foo", true);
|
||||
not_ignored!(ignot17, ROOT, "src/*.rs", "src/grep/src/main.rs");
|
||||
not_ignored!(ignot18, ROOT, "path1/*", "path2/path1/foo");
|
||||
not_ignored!(ignot19, ROOT, "s*.rs", "src/foo.rs");
|
||||
|
||||
fn bytes(s: &str) -> Vec<u8> {
|
||||
s.to_string().into_bytes()
|
||||
}
|
||||
|
||||
fn path_string<P: AsRef<Path>>(path: P) -> String {
|
||||
path.as_ref().to_str().unwrap().to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_excludes_file1() {
|
||||
let data = bytes("[core]\nexcludesFile = /foo/bar");
|
||||
let got = super::parse_excludes_file(&data).unwrap();
|
||||
assert_eq!(path_string(got), "/foo/bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_excludes_file2() {
|
||||
let data = bytes("[core]\nexcludesFile = ~/foo/bar");
|
||||
let got = super::parse_excludes_file(&data).unwrap();
|
||||
assert_eq!(path_string(got), super::expand_tilde("~/foo/bar"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_excludes_file3() {
|
||||
let data = bytes("[core]\nexcludeFile = /foo/bar");
|
||||
assert!(super::parse_excludes_file(&data).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_excludes_file4() {
|
||||
let data = bytes("[core]\nexcludesFile = \"~/foo/bar\"");
|
||||
let got = super::parse_excludes_file(&data);
|
||||
assert_eq!(
|
||||
path_string(got.unwrap()),
|
||||
super::expand_tilde("~/foo/bar")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_excludes_file5() {
|
||||
let data = bytes("[core]\nexcludesFile = \" \"~/foo/bar \" \"");
|
||||
assert!(super::parse_excludes_file(&data).is_none());
|
||||
}
|
||||
|
||||
// See: https://github.com/BurntSushi/ripgrep/issues/106
|
||||
#[test]
|
||||
fn regression_106() {
|
||||
gi_from_str("/", " ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn case_insensitive() {
|
||||
let gi = GitignoreBuilder::new(ROOT)
|
||||
.case_insensitive(true)
|
||||
.unwrap()
|
||||
.add_str(None, "*.html")
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
assert!(gi.matched("foo.html", false).is_ignore());
|
||||
assert!(gi.matched("foo.HTML", false).is_ignore());
|
||||
assert!(!gi.matched("foo.htm", false).is_ignore());
|
||||
assert!(!gi.matched("foo.HTM", false).is_ignore());
|
||||
}
|
||||
|
||||
ignored!(cs1, ROOT, "*.html", "foo.html");
|
||||
not_ignored!(cs2, ROOT, "*.html", "foo.HTML");
|
||||
not_ignored!(cs3, ROOT, "*.html", "foo.htm");
|
||||
not_ignored!(cs4, ROOT, "*.html", "foo.HTM");
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,549 +0,0 @@
|
|||
/*!
|
||||
The ignore crate provides a fast recursive directory iterator that respects
|
||||
various filters such as globs, file types and `.gitignore` files. The precise
|
||||
matching rules and precedence is explained in the documentation for
|
||||
`WalkBuilder`.
|
||||
|
||||
Secondarily, this crate exposes gitignore and file type matchers for use cases
|
||||
that demand more fine-grained control.
|
||||
|
||||
# Example
|
||||
|
||||
This example shows the most basic usage of this crate. This code will
|
||||
recursively traverse the current directory while automatically filtering out
|
||||
files and directories according to ignore globs found in files like
|
||||
`.ignore` and `.gitignore`:
|
||||
|
||||
|
||||
```rust,no_run
|
||||
use ignore::Walk;
|
||||
|
||||
for result in Walk::new("./") {
|
||||
// Each item yielded by the iterator is either a directory entry or an
|
||||
// error, so either print the path or the error.
|
||||
match result {
|
||||
Ok(entry) => println!("{}", entry.path().display()),
|
||||
Err(err) => println!("ERROR: {}", err),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Example: advanced
|
||||
|
||||
By default, the recursive directory iterator will ignore hidden files and
|
||||
directories. This can be disabled by building the iterator with `WalkBuilder`:
|
||||
|
||||
```rust,no_run
|
||||
use ignore::WalkBuilder;
|
||||
|
||||
for result in WalkBuilder::new("./").hidden(false).build() {
|
||||
println!("{:?}", result);
|
||||
}
|
||||
```
|
||||
|
||||
See the documentation for `WalkBuilder` for many other options.
|
||||
*/
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub use crate::incremental::{IncrementalIgnore, IncrementalMatch};
|
||||
pub use crate::walk::{
|
||||
DirEntry, ParallelVisitor, ParallelVisitorBuilder, Walk, WalkBuilder,
|
||||
WalkParallel, WalkState,
|
||||
};
|
||||
|
||||
mod default_types;
|
||||
mod dir;
|
||||
pub mod gitignore;
|
||||
mod incremental;
|
||||
pub mod overrides;
|
||||
mod pathutil;
|
||||
pub mod types;
|
||||
mod walk;
|
||||
|
||||
/// Represents an error that can occur when parsing a gitignore file.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// A collection of "soft" errors. These occur when adding an ignore
|
||||
/// file partially succeeded.
|
||||
Partial(Vec<Error>),
|
||||
/// An error associated with a specific line number.
|
||||
WithLineNumber {
|
||||
/// The line number.
|
||||
line: u64,
|
||||
/// The underlying error.
|
||||
err: Box<Error>,
|
||||
},
|
||||
/// An error associated with a particular file path.
|
||||
WithPath {
|
||||
/// The file path.
|
||||
path: PathBuf,
|
||||
/// The underlying error.
|
||||
err: Box<Error>,
|
||||
},
|
||||
/// An error associated with a particular directory depth when recursively
|
||||
/// walking a directory.
|
||||
WithDepth {
|
||||
/// The directory depth.
|
||||
depth: usize,
|
||||
/// The underlying error.
|
||||
err: Box<Error>,
|
||||
},
|
||||
/// An error that occurs when a file loop is detected when traversing
|
||||
/// symbolic links.
|
||||
Loop {
|
||||
/// The ancestor file path in the loop.
|
||||
ancestor: PathBuf,
|
||||
/// The child file path in the loop.
|
||||
child: PathBuf,
|
||||
},
|
||||
/// An error that occurs when doing I/O, such as reading an ignore file.
|
||||
Io(std::io::Error),
|
||||
/// An error that occurs when trying to parse a glob.
|
||||
Glob {
|
||||
/// The original glob that caused this error. This glob, when
|
||||
/// available, always corresponds to the glob provided by an end user.
|
||||
/// e.g., It is the glob as written in a `.gitignore` file.
|
||||
///
|
||||
/// (This glob may be distinct from the glob that is actually
|
||||
/// compiled, after accounting for `gitignore` semantics.)
|
||||
glob: Option<String>,
|
||||
/// The underlying glob error as a string.
|
||||
err: String,
|
||||
},
|
||||
/// A type selection for a file type that is not defined.
|
||||
UnrecognizedFileType(String),
|
||||
/// A user specified file type definition could not be parsed.
|
||||
InvalidDefinition,
|
||||
}
|
||||
|
||||
impl Clone for Error {
|
||||
fn clone(&self) -> Error {
|
||||
match *self {
|
||||
Error::Partial(ref errs) => Error::Partial(errs.clone()),
|
||||
Error::WithLineNumber { line, ref err } => {
|
||||
Error::WithLineNumber { line, err: err.clone() }
|
||||
}
|
||||
Error::WithPath { ref path, ref err } => {
|
||||
Error::WithPath { path: path.clone(), err: err.clone() }
|
||||
}
|
||||
Error::WithDepth { depth, ref err } => {
|
||||
Error::WithDepth { depth, err: err.clone() }
|
||||
}
|
||||
Error::Loop { ref ancestor, ref child } => Error::Loop {
|
||||
ancestor: ancestor.clone(),
|
||||
child: child.clone(),
|
||||
},
|
||||
Error::Io(ref err) => match err.raw_os_error() {
|
||||
Some(e) => Error::Io(std::io::Error::from_raw_os_error(e)),
|
||||
None => {
|
||||
Error::Io(std::io::Error::new(err.kind(), err.to_string()))
|
||||
}
|
||||
},
|
||||
Error::Glob { ref glob, ref err } => {
|
||||
Error::Glob { glob: glob.clone(), err: err.clone() }
|
||||
}
|
||||
Error::UnrecognizedFileType(ref err) => {
|
||||
Error::UnrecognizedFileType(err.clone())
|
||||
}
|
||||
Error::InvalidDefinition => Error::InvalidDefinition,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Returns true if this is a partial error.
|
||||
///
|
||||
/// A partial error occurs when only some operations failed while others
|
||||
/// may have succeeded. For example, an ignore file may contain an invalid
|
||||
/// glob among otherwise valid globs.
|
||||
pub fn is_partial(&self) -> bool {
|
||||
match *self {
|
||||
Error::Partial(_) => true,
|
||||
Error::WithLineNumber { ref err, .. } => err.is_partial(),
|
||||
Error::WithPath { ref err, .. } => err.is_partial(),
|
||||
Error::WithDepth { ref err, .. } => err.is_partial(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this error is exclusively an I/O error.
|
||||
pub fn is_io(&self) -> bool {
|
||||
match *self {
|
||||
Error::Partial(ref errs) => errs.len() == 1 && errs[0].is_io(),
|
||||
Error::WithLineNumber { ref err, .. } => err.is_io(),
|
||||
Error::WithPath { ref err, .. } => err.is_io(),
|
||||
Error::WithDepth { ref err, .. } => err.is_io(),
|
||||
Error::Loop { .. } => false,
|
||||
Error::Io(_) => true,
|
||||
Error::Glob { .. } => false,
|
||||
Error::UnrecognizedFileType(_) => false,
|
||||
Error::InvalidDefinition => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Inspect the original [`std::io::Error`] if there is one.
|
||||
///
|
||||
/// [`None`] is returned if the [`Error`] doesn't correspond to an
|
||||
/// [`std::io::Error`]. This might happen, for example, when the error was
|
||||
/// produced because a cycle was found in the directory tree while
|
||||
/// following symbolic links.
|
||||
///
|
||||
/// This method returns a borrowed value that is bound to the lifetime of the [`Error`]. To
|
||||
/// obtain an owned value, the [`into_io_error`] can be used instead.
|
||||
///
|
||||
/// > This is the original [`std::io::Error`] and is _not_ the same as
|
||||
/// > [`impl From<Error> for std::io::Error`][impl] which contains
|
||||
/// > additional context about the error.
|
||||
///
|
||||
/// [`None`]: https://doc.rust-lang.org/stable/std/option/enum.Option.html#variant.None
|
||||
/// [`std::io::Error`]: https://doc.rust-lang.org/stable/std/io/struct.Error.html
|
||||
/// [`From`]: https://doc.rust-lang.org/stable/std/convert/trait.From.html
|
||||
/// [`Error`]: struct.Error.html
|
||||
/// [`into_io_error`]: struct.Error.html#method.into_io_error
|
||||
/// [impl]: struct.Error.html#impl-From%3CError%3E
|
||||
pub fn io_error(&self) -> Option<&std::io::Error> {
|
||||
match *self {
|
||||
Error::Partial(ref errs) => {
|
||||
if errs.len() == 1 {
|
||||
errs[0].io_error()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Error::WithLineNumber { ref err, .. } => err.io_error(),
|
||||
Error::WithPath { ref err, .. } => err.io_error(),
|
||||
Error::WithDepth { ref err, .. } => err.io_error(),
|
||||
Error::Loop { .. } => None,
|
||||
Error::Io(ref err) => Some(err),
|
||||
Error::Glob { .. } => None,
|
||||
Error::UnrecognizedFileType(_) => None,
|
||||
Error::InvalidDefinition => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Similar to [`io_error`] except consumes self to convert to the original
|
||||
/// [`std::io::Error`] if one exists.
|
||||
///
|
||||
/// [`io_error`]: struct.Error.html#method.io_error
|
||||
/// [`std::io::Error`]: https://doc.rust-lang.org/stable/std/io/struct.Error.html
|
||||
pub fn into_io_error(self) -> Option<std::io::Error> {
|
||||
match self {
|
||||
Error::Partial(mut errs) => {
|
||||
if errs.len() == 1 {
|
||||
errs.remove(0).into_io_error()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Error::WithLineNumber { err, .. } => err.into_io_error(),
|
||||
Error::WithPath { err, .. } => err.into_io_error(),
|
||||
Error::WithDepth { err, .. } => err.into_io_error(),
|
||||
Error::Loop { .. } => None,
|
||||
Error::Io(err) => Some(err),
|
||||
Error::Glob { .. } => None,
|
||||
Error::UnrecognizedFileType(_) => None,
|
||||
Error::InvalidDefinition => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a depth associated with recursively walking a directory (if
|
||||
/// this error was generated from a recursive directory iterator).
|
||||
pub fn depth(&self) -> Option<usize> {
|
||||
match *self {
|
||||
Error::WithPath { ref err, .. } => err.depth(),
|
||||
Error::WithDepth { depth, .. } => Some(depth),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Turn an error into a tagged error with the given file path.
|
||||
fn with_path<P: AsRef<Path>>(self, path: P) -> Error {
|
||||
Error::WithPath {
|
||||
path: path.as_ref().to_path_buf(),
|
||||
err: Box::new(self),
|
||||
}
|
||||
}
|
||||
|
||||
/// Turn an error into a tagged error with the given depth.
|
||||
fn with_depth(self, depth: usize) -> Error {
|
||||
Error::WithDepth { depth, err: Box::new(self) }
|
||||
}
|
||||
|
||||
/// Turn an error into a tagged error with the given file path and line
|
||||
/// number. If path is empty, then it is omitted from the error.
|
||||
fn tagged<P: AsRef<Path>>(self, path: P, lineno: u64) -> Error {
|
||||
let errline =
|
||||
Error::WithLineNumber { line: lineno, err: Box::new(self) };
|
||||
if path.as_ref().as_os_str().is_empty() {
|
||||
return errline;
|
||||
}
|
||||
errline.with_path(path)
|
||||
}
|
||||
|
||||
/// Build an error from a walkdir error.
|
||||
fn from_walkdir(err: walkdir::Error) -> Error {
|
||||
let depth = err.depth();
|
||||
if let (Some(anc), Some(child)) = (err.loop_ancestor(), err.path()) {
|
||||
return Error::WithDepth {
|
||||
depth,
|
||||
err: Box::new(Error::Loop {
|
||||
ancestor: anc.to_path_buf(),
|
||||
child: child.to_path_buf(),
|
||||
}),
|
||||
};
|
||||
}
|
||||
let path = err.path().map(|p| p.to_path_buf());
|
||||
let mut ig_err = Error::WithDepth {
|
||||
depth,
|
||||
err: Box::new(Error::Io(std::io::Error::from(err))),
|
||||
};
|
||||
if let Some(path) = path {
|
||||
ig_err = Error::WithPath { path, err: Box::new(ig_err) };
|
||||
}
|
||||
ig_err
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
#[allow(deprecated)]
|
||||
fn description(&self) -> &str {
|
||||
match *self {
|
||||
Error::Partial(_) => "partial error",
|
||||
Error::WithLineNumber { ref err, .. } => err.description(),
|
||||
Error::WithPath { ref err, .. } => err.description(),
|
||||
Error::WithDepth { ref err, .. } => err.description(),
|
||||
Error::Loop { .. } => "file system loop found",
|
||||
Error::Io(ref err) => err.description(),
|
||||
Error::Glob { ref err, .. } => err,
|
||||
Error::UnrecognizedFileType(_) => "unrecognized file type",
|
||||
Error::InvalidDefinition => "invalid definition",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match *self {
|
||||
Error::Partial(ref errs) => {
|
||||
let msgs: Vec<String> =
|
||||
errs.iter().map(|err| err.to_string()).collect();
|
||||
write!(f, "{}", msgs.join("\n"))
|
||||
}
|
||||
Error::WithLineNumber { line, ref err } => {
|
||||
write!(f, "line {}: {}", line, err)
|
||||
}
|
||||
Error::WithPath { ref path, ref err } => {
|
||||
write!(f, "{}: {}", path.display(), err)
|
||||
}
|
||||
Error::WithDepth { ref err, .. } => err.fmt(f),
|
||||
Error::Loop { ref ancestor, ref child } => write!(
|
||||
f,
|
||||
"File system loop found: \
|
||||
{} points to an ancestor {}",
|
||||
child.display(),
|
||||
ancestor.display()
|
||||
),
|
||||
Error::Io(ref err) => err.fmt(f),
|
||||
Error::Glob { glob: None, ref err } => write!(f, "{}", err),
|
||||
Error::Glob { glob: Some(ref glob), ref err } => {
|
||||
write!(f, "error parsing glob '{}': {}", glob, err)
|
||||
}
|
||||
Error::UnrecognizedFileType(ref ty) => {
|
||||
write!(f, "unrecognized file type: {}", ty)
|
||||
}
|
||||
Error::InvalidDefinition => write!(
|
||||
f,
|
||||
"invalid definition (format is type:glob, e.g., \
|
||||
html:*.html)"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(err: std::io::Error) -> Error {
|
||||
Error::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct PartialErrorBuilder(Vec<Error>);
|
||||
|
||||
impl PartialErrorBuilder {
|
||||
fn push(&mut self, err: Error) {
|
||||
self.0.push(err);
|
||||
}
|
||||
|
||||
fn push_ignore_io(&mut self, err: Error) {
|
||||
if !err.is_io() {
|
||||
self.push(err);
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_push(&mut self, err: Option<Error>) {
|
||||
if let Some(err) = err {
|
||||
self.push(err);
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_push_ignore_io(&mut self, err: Option<Error>) {
|
||||
if let Some(err) = err {
|
||||
self.push_ignore_io(err);
|
||||
}
|
||||
}
|
||||
|
||||
fn into_error_option(mut self) -> Option<Error> {
|
||||
if self.0.is_empty() {
|
||||
None
|
||||
} else if self.0.len() == 1 {
|
||||
Some(self.0.pop().unwrap())
|
||||
} else {
|
||||
Some(Error::Partial(self.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of a glob match.
|
||||
///
|
||||
/// The type parameter `T` typically refers to a type that provides more
|
||||
/// information about a particular match. For example, it might identify
|
||||
/// the specific gitignore file and the specific glob pattern that caused
|
||||
/// the match.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Match<T> {
|
||||
/// The path didn't match any glob.
|
||||
None,
|
||||
/// The highest precedent glob matched indicates the path should be
|
||||
/// ignored.
|
||||
Ignore(T),
|
||||
/// The highest precedent glob matched indicates the path should be
|
||||
/// whitelisted.
|
||||
Whitelist(T),
|
||||
}
|
||||
|
||||
impl<T> Match<T> {
|
||||
/// Returns true if the match result didn't match any globs.
|
||||
pub fn is_none(&self) -> bool {
|
||||
match *self {
|
||||
Match::None => true,
|
||||
Match::Ignore(_) | Match::Whitelist(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the match result implies the path should be ignored.
|
||||
pub fn is_ignore(&self) -> bool {
|
||||
match *self {
|
||||
Match::Ignore(_) => true,
|
||||
Match::None | Match::Whitelist(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the match result implies the path should be
|
||||
/// whitelisted.
|
||||
pub fn is_whitelist(&self) -> bool {
|
||||
match *self {
|
||||
Match::Whitelist(_) => true,
|
||||
Match::None | Match::Ignore(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Inverts the match so that `Ignore` becomes `Whitelist` and
|
||||
/// `Whitelist` becomes `Ignore`. A non-match remains the same.
|
||||
pub fn invert(self) -> Match<T> {
|
||||
match self {
|
||||
Match::None => Match::None,
|
||||
Match::Ignore(t) => Match::Whitelist(t),
|
||||
Match::Whitelist(t) => Match::Ignore(t),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the value inside this match if it exists.
|
||||
pub fn inner(&self) -> Option<&T> {
|
||||
match *self {
|
||||
Match::None => None,
|
||||
Match::Ignore(ref t) => Some(t),
|
||||
Match::Whitelist(ref t) => Some(t),
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the given function to the value inside this match.
|
||||
///
|
||||
/// If the match has no value, then return the match unchanged.
|
||||
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Match<U> {
|
||||
match self {
|
||||
Match::None => Match::None,
|
||||
Match::Ignore(t) => Match::Ignore(f(t)),
|
||||
Match::Whitelist(t) => Match::Whitelist(f(t)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the match if it is not none. Otherwise, return other.
|
||||
pub fn or(self, other: Self) -> Self {
|
||||
if self.is_none() { other } else { self }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
/// A convenient result type alias.
|
||||
pub(crate) type Result<T> =
|
||||
std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
||||
|
||||
macro_rules! err {
|
||||
($($tt:tt)*) => {
|
||||
Box::<dyn std::error::Error + Send + Sync>::from(format!($($tt)*))
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple wrapper for creating a temporary directory that is
|
||||
/// automatically deleted when it's dropped.
|
||||
///
|
||||
/// We use this in lieu of tempfile because tempfile brings in too many
|
||||
/// dependencies.
|
||||
#[derive(Debug)]
|
||||
pub struct TempDir(PathBuf);
|
||||
|
||||
impl Drop for TempDir {
|
||||
fn drop(&mut self) {
|
||||
fs::remove_dir_all(&self.0).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl TempDir {
|
||||
/// Create a new empty temporary directory under the system's configured
|
||||
/// temporary directory.
|
||||
pub fn new() -> Result<TempDir> {
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
static TRIES: usize = 100;
|
||||
static COUNTER: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
let tmpdir = env::temp_dir();
|
||||
for _ in 0..TRIES {
|
||||
let count = COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let path = tmpdir.join("rust-ignore").join(count.to_string());
|
||||
if path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
fs::create_dir_all(&path).map_err(|e| {
|
||||
err!("failed to create {}: {}", path.display(), e)
|
||||
})?;
|
||||
return Ok(TempDir(path));
|
||||
}
|
||||
Err(err!("failed to create temp dir after {} tries", TRIES))
|
||||
}
|
||||
|
||||
/// Return the underlying path to this temporary directory.
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,293 +0,0 @@
|
|||
/*!
|
||||
The overrides module provides a way to specify a set of override globs.
|
||||
|
||||
This provides functionality similar to `--include` or `--exclude` in command
|
||||
line tools.
|
||||
*/
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{
|
||||
Error, Match,
|
||||
gitignore::{self, Gitignore, GitignoreBuilder},
|
||||
};
|
||||
|
||||
/// Glob represents a single glob in an override matcher.
|
||||
///
|
||||
/// This is used to report information about the highest precedent glob
|
||||
/// that matched.
|
||||
///
|
||||
/// Note that not all matches necessarily correspond to a specific glob. For
|
||||
/// example, if there are one or more whitelist globs and a file path doesn't
|
||||
/// match any glob in the set, then the file path is considered to be ignored.
|
||||
///
|
||||
/// The lifetime `'a` refers to the lifetime of the matcher that produced
|
||||
/// this glob.
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Glob<'a>(GlobInner<'a>);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(dead_code)]
|
||||
enum GlobInner<'a> {
|
||||
/// No glob matched, but the file path should still be ignored.
|
||||
UnmatchedIgnore,
|
||||
/// A glob matched.
|
||||
Matched(&'a gitignore::Glob),
|
||||
}
|
||||
|
||||
impl<'a> Glob<'a> {
|
||||
fn unmatched() -> Glob<'a> {
|
||||
Glob(GlobInner::UnmatchedIgnore)
|
||||
}
|
||||
}
|
||||
|
||||
/// Manages a set of overrides provided explicitly by the end user.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Override(Gitignore);
|
||||
|
||||
impl Override {
|
||||
/// Returns an empty matcher that never matches any file path.
|
||||
pub fn empty() -> Override {
|
||||
Override(Gitignore::empty())
|
||||
}
|
||||
|
||||
/// Returns the directory of this override set.
|
||||
///
|
||||
/// All matches are done relative to this path.
|
||||
pub fn path(&self) -> &Path {
|
||||
self.0.path()
|
||||
}
|
||||
|
||||
/// Returns true if and only if this matcher is empty.
|
||||
///
|
||||
/// When a matcher is empty, it will never match any file path.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
/// Returns the total number of ignore globs.
|
||||
pub fn num_ignores(&self) -> u64 {
|
||||
self.0.num_whitelists()
|
||||
}
|
||||
|
||||
/// Returns the total number of whitelisted globs.
|
||||
pub fn num_whitelists(&self) -> u64 {
|
||||
self.0.num_ignores()
|
||||
}
|
||||
|
||||
/// Returns whether the given file path matched a pattern in this override
|
||||
/// matcher.
|
||||
///
|
||||
/// `is_dir` should be true if the path refers to a directory and false
|
||||
/// otherwise.
|
||||
///
|
||||
/// If there are no overrides, then this always returns `Match::None`.
|
||||
///
|
||||
/// If there is at least one whitelist override and `is_dir` is false, then
|
||||
/// this never returns `Match::None`, since non-matches are interpreted as
|
||||
/// ignored.
|
||||
///
|
||||
/// The given path is matched to the globs relative to the path given
|
||||
/// when building the override matcher. Specifically, before matching
|
||||
/// `path`, its prefix (as determined by a common suffix of the directory
|
||||
/// given) is stripped. If there is no common suffix/prefix overlap, then
|
||||
/// `path` is assumed to reside in the same directory as the root path for
|
||||
/// this set of overrides.
|
||||
pub fn matched<'a, P: AsRef<Path>>(
|
||||
&'a self,
|
||||
path: P,
|
||||
is_dir: bool,
|
||||
) -> Match<Glob<'a>> {
|
||||
if self.is_empty() {
|
||||
return Match::None;
|
||||
}
|
||||
let mat = self.0.matched(path, is_dir).invert();
|
||||
if mat.is_none() && self.num_whitelists() > 0 && !is_dir {
|
||||
return Match::Ignore(Glob::unmatched());
|
||||
}
|
||||
mat.map(move |giglob| Glob(GlobInner::Matched(giglob)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a matcher for a set of glob overrides.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OverrideBuilder {
|
||||
builder: GitignoreBuilder,
|
||||
}
|
||||
|
||||
impl OverrideBuilder {
|
||||
/// Create a new override builder.
|
||||
///
|
||||
/// Matching is done relative to the directory path provided.
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> OverrideBuilder {
|
||||
let mut builder = GitignoreBuilder::new(path);
|
||||
builder.allow_unclosed_class(false);
|
||||
OverrideBuilder { builder }
|
||||
}
|
||||
|
||||
/// Builds a new override matcher from the globs added so far.
|
||||
///
|
||||
/// Once a matcher is built, no new globs can be added to it.
|
||||
pub fn build(&self) -> Result<Override, Error> {
|
||||
Ok(Override(self.builder.build()?))
|
||||
}
|
||||
|
||||
/// Add a glob to the set of overrides.
|
||||
///
|
||||
/// Globs provided here have precisely the same semantics as a single
|
||||
/// line in a `gitignore` file, where the meaning of `!` is inverted:
|
||||
/// namely, `!` at the beginning of a glob will ignore a file. Without `!`,
|
||||
/// all matches of the glob provided are treated as whitelist matches.
|
||||
pub fn add(&mut self, glob: &str) -> Result<&mut OverrideBuilder, Error> {
|
||||
self.builder.add_line(None, glob)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Toggle whether the globs should be matched case insensitively or not.
|
||||
///
|
||||
/// When this option is changed, only globs added after the change will be
|
||||
/// affected.
|
||||
///
|
||||
/// This is disabled by default.
|
||||
pub fn case_insensitive(
|
||||
&mut self,
|
||||
yes: bool,
|
||||
) -> Result<&mut OverrideBuilder, Error> {
|
||||
// TODO: This should not return a `Result`. Fix this in the next semver
|
||||
// release.
|
||||
self.builder.case_insensitive(yes)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Toggle whether unclosed character classes are allowed. When allowed,
|
||||
/// a `[` without a matching `]` is treated literally instead of resulting
|
||||
/// in a parse error.
|
||||
///
|
||||
/// For example, if this is set then the glob `[abc` will be treated as the
|
||||
/// literal string `[abc` instead of returning an error.
|
||||
///
|
||||
/// By default, this is false. Generally speaking, enabling this leads to
|
||||
/// worse failure modes since the glob parser becomes more permissive. You
|
||||
/// might want to enable this when compatibility (e.g., with POSIX glob
|
||||
/// implementations) is more important than good error messages.
|
||||
///
|
||||
/// This default is different from the default for [`Gitignore`]. Namely,
|
||||
/// [`Gitignore`] is intended to match git's behavior as-is. But this
|
||||
/// abstraction for "override" globs does not necessarily conform to any
|
||||
/// other known specification and instead prioritizes better error
|
||||
/// messages.
|
||||
pub fn allow_unclosed_class(&mut self, yes: bool) -> &mut OverrideBuilder {
|
||||
self.builder.allow_unclosed_class(yes);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Override, OverrideBuilder};
|
||||
|
||||
const ROOT: &'static str = "/home/andrew/foo";
|
||||
|
||||
fn ov(globs: &[&str]) -> Override {
|
||||
let mut builder = OverrideBuilder::new(ROOT);
|
||||
for glob in globs {
|
||||
builder.add(glob).unwrap();
|
||||
}
|
||||
builder.build().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let ov = ov(&[]);
|
||||
assert!(ov.matched("a.foo", false).is_none());
|
||||
assert!(ov.matched("a", false).is_none());
|
||||
assert!(ov.matched("", false).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple() {
|
||||
let ov = ov(&["*.foo", "!*.bar"]);
|
||||
assert!(ov.matched("a.foo", false).is_whitelist());
|
||||
assert!(ov.matched("a.foo", true).is_whitelist());
|
||||
assert!(ov.matched("a.rs", false).is_ignore());
|
||||
assert!(ov.matched("a.rs", true).is_none());
|
||||
assert!(ov.matched("a.bar", false).is_ignore());
|
||||
assert!(ov.matched("a.bar", true).is_ignore());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_ignores() {
|
||||
let ov = ov(&["!*.bar"]);
|
||||
assert!(ov.matched("a.rs", false).is_none());
|
||||
assert!(ov.matched("a.rs", true).is_none());
|
||||
assert!(ov.matched("a.bar", false).is_ignore());
|
||||
assert!(ov.matched("a.bar", true).is_ignore());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn precedence() {
|
||||
let ov = ov(&["*.foo", "!*.bar.foo"]);
|
||||
assert!(ov.matched("a.foo", false).is_whitelist());
|
||||
assert!(ov.matched("a.baz", false).is_ignore());
|
||||
assert!(ov.matched("a.bar.foo", false).is_ignore());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gitignore() {
|
||||
let ov = ov(&["/foo", "bar/*.rs", "baz/**"]);
|
||||
assert!(ov.matched("bar/lib.rs", false).is_whitelist());
|
||||
assert!(ov.matched("bar/wat/lib.rs", false).is_ignore());
|
||||
assert!(ov.matched("wat/bar/lib.rs", false).is_ignore());
|
||||
assert!(ov.matched("foo", false).is_whitelist());
|
||||
assert!(ov.matched("wat/foo", false).is_ignore());
|
||||
assert!(ov.matched("baz", false).is_ignore());
|
||||
assert!(ov.matched("baz/a", false).is_whitelist());
|
||||
assert!(ov.matched("baz/a/b", false).is_whitelist());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allow_directories() {
|
||||
// This tests that directories are NOT ignored when they are unmatched.
|
||||
let ov = ov(&["*.rs"]);
|
||||
assert!(ov.matched("foo.rs", false).is_whitelist());
|
||||
assert!(ov.matched("foo.c", false).is_ignore());
|
||||
assert!(ov.matched("foo", false).is_ignore());
|
||||
assert!(ov.matched("foo", true).is_none());
|
||||
assert!(ov.matched("src/foo.rs", false).is_whitelist());
|
||||
assert!(ov.matched("src/foo.c", false).is_ignore());
|
||||
assert!(ov.matched("src/foo", false).is_ignore());
|
||||
assert!(ov.matched("src/foo", true).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absolute_path() {
|
||||
let ov = ov(&["!/bar"]);
|
||||
assert!(ov.matched("./foo/bar", false).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn case_insensitive() {
|
||||
let ov = OverrideBuilder::new(ROOT)
|
||||
.case_insensitive(true)
|
||||
.unwrap()
|
||||
.add("*.html")
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
assert!(ov.matched("foo.html", false).is_whitelist());
|
||||
assert!(ov.matched("foo.HTML", false).is_whitelist());
|
||||
assert!(ov.matched("foo.htm", false).is_ignore());
|
||||
assert!(ov.matched("foo.HTM", false).is_ignore());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_case_sensitive() {
|
||||
let ov =
|
||||
OverrideBuilder::new(ROOT).add("*.html").unwrap().build().unwrap();
|
||||
assert!(ov.matched("foo.html", false).is_whitelist());
|
||||
assert!(ov.matched("foo.HTML", false).is_ignore());
|
||||
assert!(ov.matched("foo.htm", false).is_ignore());
|
||||
assert!(ov.matched("foo.HTM", false).is_ignore());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
use std::{ffi::OsStr, path::Path};
|
||||
|
||||
use crate::walk::DirEntry;
|
||||
|
||||
/// Returns true if and only if this path is considered to be hidden.
|
||||
///
|
||||
/// # Platform behavior
|
||||
///
|
||||
/// ## Windows
|
||||
///
|
||||
/// This returns true if one of the following is true:
|
||||
///
|
||||
/// * The base name of the path starts with a `.`.
|
||||
/// * The file attributes have the `HIDDEN` property set.
|
||||
///
|
||||
/// ## All other platforms
|
||||
///
|
||||
/// This only returns true if the base name of the path starts with a `.`.
|
||||
pub(crate) fn is_hidden_path(dent: &Path) -> bool {
|
||||
#[cfg(not(windows))]
|
||||
fn imp(path: &Path) -> bool {
|
||||
is_hidden_path_only(path)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn imp(path: &Path) -> bool {
|
||||
use std::os::windows::fs::MetadataExt;
|
||||
use winapi_util::file;
|
||||
|
||||
if let Ok(md) = path.metadata() {
|
||||
if file::is_hidden(md.file_attributes() as u64) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
is_hidden_path_only(path)
|
||||
}
|
||||
|
||||
imp(dent)
|
||||
}
|
||||
|
||||
/// Returns true if and only if this directory entry is considered to be
|
||||
/// hidden.
|
||||
///
|
||||
/// # Platform behavior
|
||||
///
|
||||
/// ## Windows
|
||||
///
|
||||
/// This returns true if one of the following is true:
|
||||
///
|
||||
/// * The base name of the path starts with a `.`.
|
||||
/// * The file attributes have the `HIDDEN` property set.
|
||||
///
|
||||
/// ## All other platforms
|
||||
///
|
||||
/// This only returns true if the base name of the path starts with a `.`.
|
||||
pub(crate) fn is_hidden_entry(dent: &DirEntry) -> bool {
|
||||
#[cfg(not(windows))]
|
||||
fn imp(dent: &DirEntry) -> bool {
|
||||
is_hidden_path_only(dent.path())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn imp(dent: &DirEntry) -> bool {
|
||||
use std::os::windows::fs::MetadataExt;
|
||||
use winapi_util::file;
|
||||
|
||||
// This looks like we're doing an extra stat call, but on Windows, the
|
||||
// directory traverser reuses the metadata retrieved from each directory
|
||||
// entry and stores it on the DirEntry itself. So this is "free."
|
||||
if let Ok(md) = dent.metadata() {
|
||||
if file::is_hidden(md.file_attributes() as u64) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
is_hidden_path_only(dent.path())
|
||||
}
|
||||
|
||||
imp(dent)
|
||||
}
|
||||
|
||||
/// Returns true if and only if this path is considered to be hidden from only
|
||||
/// the path itself.
|
||||
///
|
||||
/// This has the same behavior on all platforms.
|
||||
fn is_hidden_path_only(path: &Path) -> bool {
|
||||
if let Some(name) = file_name(path) {
|
||||
name.as_encoded_bytes().starts_with(b".")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Strip `prefix` from the `path` and return the remainder.
|
||||
///
|
||||
/// If `path` doesn't have a prefix `prefix`, then return `None`.
|
||||
pub(crate) fn strip_prefix<'a, P: AsRef<Path> + ?Sized>(
|
||||
prefix: &'a P,
|
||||
path: &'a Path,
|
||||
) -> Option<&'a Path> {
|
||||
#[cfg(unix)]
|
||||
fn imp<'a>(prefix: &'a Path, path: &'a Path) -> Option<&'a Path> {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
let prefix = prefix.as_os_str().as_bytes();
|
||||
let path = path.as_os_str().as_bytes();
|
||||
if prefix.len() > path.len() || prefix != &path[0..prefix.len()] {
|
||||
None
|
||||
} else {
|
||||
Some(&Path::new(OsStr::from_bytes(&path[prefix.len()..])))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn imp<'a>(prefix: &'a Path, path: &'a Path) -> Option<&'a Path> {
|
||||
path.strip_prefix(prefix).ok()
|
||||
}
|
||||
|
||||
imp(prefix.as_ref(), path)
|
||||
}
|
||||
|
||||
/// Returns true if this file path is just a file name. i.e., Its parent is
|
||||
/// the empty string.
|
||||
pub(crate) fn is_file_name<P: AsRef<Path>>(path: P) -> bool {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
memchr::memchr(b'/', path.as_ref().as_os_str().as_encoded_bytes())
|
||||
.is_none()
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
path.as_ref()
|
||||
.parent()
|
||||
.map(|p| p.as_os_str().is_empty())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// The final component of the path, if it is a normal file.
|
||||
///
|
||||
/// If the path terminates in `.`, `..`, or consists solely of a root of
|
||||
/// prefix, this will return `None`.
|
||||
pub(crate) fn file_name<'a, P: AsRef<Path> + ?Sized>(
|
||||
path: &'a P,
|
||||
) -> Option<&'a OsStr> {
|
||||
#[cfg(unix)]
|
||||
fn imp(path: &Path) -> Option<&OsStr> {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
use memchr::memrchr;
|
||||
|
||||
let path = path.as_os_str().as_bytes();
|
||||
if path.is_empty() {
|
||||
return None;
|
||||
} else if path.len() == 1 && path[0] == b'.' {
|
||||
return None;
|
||||
} else if path.last() == Some(&b'.') {
|
||||
return None;
|
||||
} else if path.len() >= 2 && &path[path.len() - 2..] == &b".."[..] {
|
||||
return None;
|
||||
}
|
||||
let last_slash = memrchr(b'/', path).map(|i| i + 1).unwrap_or(0);
|
||||
Some(OsStr::from_bytes(&path[last_slash..]))
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn imp(path: &Path) -> Option<&OsStr> {
|
||||
path.file_name()
|
||||
}
|
||||
|
||||
imp(path.as_ref())
|
||||
}
|
||||
|
|
@ -1,588 +0,0 @@
|
|||
/*!
|
||||
The types module provides a way of associating globs on file names to file
|
||||
types.
|
||||
|
||||
This can be used to match specific types of files. For example, among
|
||||
the default file types provided, the Rust file type is defined to be `*.rs`
|
||||
with name `rust`. Similarly, the C file type is defined to be `*.{c,h}` with
|
||||
name `c`.
|
||||
|
||||
Note that the set of default types may change over time.
|
||||
|
||||
# Example
|
||||
|
||||
This shows how to create and use a simple file type matcher using the default
|
||||
file types defined in this crate.
|
||||
|
||||
```
|
||||
use ignore::types::TypesBuilder;
|
||||
|
||||
let mut builder = TypesBuilder::new();
|
||||
builder.add_defaults();
|
||||
builder.select("rust");
|
||||
let matcher = builder.build().unwrap();
|
||||
|
||||
assert!(matcher.matched("foo.rs", false).is_whitelist());
|
||||
assert!(matcher.matched("foo.c", false).is_ignore());
|
||||
```
|
||||
|
||||
# Example: negation
|
||||
|
||||
This is like the previous example, but shows how negating a file type works.
|
||||
That is, this will let us match file paths that *don't* correspond to a
|
||||
particular file type.
|
||||
|
||||
```
|
||||
use ignore::types::TypesBuilder;
|
||||
|
||||
let mut builder = TypesBuilder::new();
|
||||
builder.add_defaults();
|
||||
builder.negate("c");
|
||||
let matcher = builder.build().unwrap();
|
||||
|
||||
assert!(matcher.matched("foo.rs", false).is_none());
|
||||
assert!(matcher.matched("foo.c", false).is_ignore());
|
||||
```
|
||||
|
||||
# Example: custom file type definitions
|
||||
|
||||
This shows how to extend this library default file type definitions with
|
||||
your own.
|
||||
|
||||
```
|
||||
use ignore::types::TypesBuilder;
|
||||
|
||||
let mut builder = TypesBuilder::new();
|
||||
builder.add_defaults();
|
||||
builder.add("foo", "*.foo");
|
||||
// Another way of adding a file type definition.
|
||||
// This is useful when accepting input from an end user.
|
||||
builder.add_def("bar:*.bar");
|
||||
// Note: we only select `foo`, not `bar`.
|
||||
builder.select("foo");
|
||||
let matcher = builder.build().unwrap();
|
||||
|
||||
assert!(matcher.matched("x.foo", false).is_whitelist());
|
||||
// This is ignored because we only selected the `foo` file type.
|
||||
assert!(matcher.matched("x.bar", false).is_ignore());
|
||||
```
|
||||
|
||||
We can also add file type definitions based on other definitions.
|
||||
|
||||
```
|
||||
use ignore::types::TypesBuilder;
|
||||
|
||||
let mut builder = TypesBuilder::new();
|
||||
builder.add_defaults();
|
||||
builder.add("foo", "*.foo");
|
||||
builder.add_def("bar:include:foo,cpp");
|
||||
builder.select("bar");
|
||||
let matcher = builder.build().unwrap();
|
||||
|
||||
assert!(matcher.matched("x.foo", false).is_whitelist());
|
||||
assert!(matcher.matched("y.cpp", false).is_whitelist());
|
||||
```
|
||||
*/
|
||||
|
||||
use std::{collections::HashMap, path::Path, sync::Arc};
|
||||
|
||||
use {
|
||||
globset::{GlobBuilder, GlobSet, GlobSetBuilder},
|
||||
regex_automata::util::pool::Pool,
|
||||
};
|
||||
|
||||
use crate::{Error, Match, default_types::DEFAULT_TYPES, pathutil::file_name};
|
||||
|
||||
/// Glob represents a single glob in a set of file type definitions.
|
||||
///
|
||||
/// There may be more than one glob for a particular file type.
|
||||
///
|
||||
/// This is used to report information about the highest precedent glob
|
||||
/// that matched.
|
||||
///
|
||||
/// Note that not all matches necessarily correspond to a specific glob.
|
||||
/// For example, if there are one or more selections and a file path doesn't
|
||||
/// match any of those selections, then the file path is considered to be
|
||||
/// ignored.
|
||||
///
|
||||
/// The lifetime `'a` refers to the lifetime of the underlying file type
|
||||
/// definition, which corresponds to the lifetime of the file type matcher.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Glob<'a>(GlobInner<'a>);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum GlobInner<'a> {
|
||||
/// No glob matched, but the file path should still be ignored.
|
||||
UnmatchedIgnore,
|
||||
/// A glob matched.
|
||||
Matched {
|
||||
/// The file type definition which provided the glob.
|
||||
def: &'a FileTypeDef,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> Glob<'a> {
|
||||
fn unmatched() -> Glob<'a> {
|
||||
Glob(GlobInner::UnmatchedIgnore)
|
||||
}
|
||||
|
||||
/// Return the file type definition that matched, if one exists. A file type
|
||||
/// definition always exists when a specific definition matches a file
|
||||
/// path.
|
||||
pub fn file_type_def(&self) -> Option<&FileTypeDef> {
|
||||
match self {
|
||||
Glob(GlobInner::UnmatchedIgnore) => None,
|
||||
Glob(GlobInner::Matched { def, .. }) => Some(def),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A single file type definition.
|
||||
///
|
||||
/// File type definitions can be retrieved in aggregate from a file type
|
||||
/// matcher. File type definitions are also reported when its responsible
|
||||
/// for a match.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct FileTypeDef {
|
||||
name: String,
|
||||
globs: Vec<String>,
|
||||
}
|
||||
|
||||
impl FileTypeDef {
|
||||
/// Return the name of this file type.
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// Return the globs used to recognize this file type.
|
||||
pub fn globs(&self) -> &[String] {
|
||||
&self.globs
|
||||
}
|
||||
}
|
||||
|
||||
/// Types is a file type matcher.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Types {
|
||||
/// All of the file type definitions, sorted lexicographically by name.
|
||||
defs: Vec<FileTypeDef>,
|
||||
/// All of the selections made by the user.
|
||||
selections: Vec<Selection<FileTypeDef>>,
|
||||
/// Whether there is at least one Selection::Select in our selections.
|
||||
/// When this is true, a Match::None is converted to Match::Ignore.
|
||||
has_selected: bool,
|
||||
/// A mapping from glob index in the set to two indices. The first is an
|
||||
/// index into `selections` and the second is an index into the
|
||||
/// corresponding file type definition's list of globs.
|
||||
glob_to_selection: Vec<(usize, usize)>,
|
||||
/// The set of all glob selections, used for actual matching.
|
||||
set: GlobSet,
|
||||
/// Temporary storage for globs that match.
|
||||
matches: Arc<Pool<Vec<usize>>>,
|
||||
}
|
||||
|
||||
/// Indicates the type of a selection for a particular file type.
|
||||
#[derive(Clone, Debug)]
|
||||
enum Selection<T> {
|
||||
Select(String, T),
|
||||
Negate(String, T),
|
||||
}
|
||||
|
||||
impl<T> Selection<T> {
|
||||
fn is_negated(&self) -> bool {
|
||||
match *self {
|
||||
Selection::Select(..) => false,
|
||||
Selection::Negate(..) => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
match *self {
|
||||
Selection::Select(ref name, _) => name,
|
||||
Selection::Negate(ref name, _) => name,
|
||||
}
|
||||
}
|
||||
|
||||
fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Selection<U> {
|
||||
match self {
|
||||
Selection::Select(name, inner) => {
|
||||
Selection::Select(name, f(inner))
|
||||
}
|
||||
Selection::Negate(name, inner) => {
|
||||
Selection::Negate(name, f(inner))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn inner(&self) -> &T {
|
||||
match *self {
|
||||
Selection::Select(_, ref inner) => inner,
|
||||
Selection::Negate(_, ref inner) => inner,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Types {
|
||||
/// Creates a new file type matcher that never matches any path and
|
||||
/// contains no file type definitions.
|
||||
pub fn empty() -> Types {
|
||||
Types {
|
||||
defs: vec![],
|
||||
selections: vec![],
|
||||
has_selected: false,
|
||||
glob_to_selection: vec![],
|
||||
set: GlobSetBuilder::new().build().unwrap(),
|
||||
matches: Arc::new(Pool::with_available_parallelism_capacity(
|
||||
|| vec![],
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if and only if this matcher has zero selections.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.selections.is_empty()
|
||||
}
|
||||
|
||||
/// Returns the number of selections used in this matcher.
|
||||
pub fn len(&self) -> usize {
|
||||
self.selections.len()
|
||||
}
|
||||
|
||||
/// Return the set of current file type definitions.
|
||||
///
|
||||
/// Definitions and globs are sorted.
|
||||
pub fn definitions(&self) -> &[FileTypeDef] {
|
||||
&self.defs
|
||||
}
|
||||
|
||||
/// Returns a match for the given path against this file type matcher.
|
||||
///
|
||||
/// The path is considered whitelisted if it matches a selected file type.
|
||||
/// The path is considered ignored if it matches a negated file type.
|
||||
/// If at least one file type is selected and `path` doesn't match, then
|
||||
/// the path is also considered ignored.
|
||||
pub fn matched<'a, P: AsRef<Path>>(
|
||||
&'a self,
|
||||
path: P,
|
||||
is_dir: bool,
|
||||
) -> Match<Glob<'a>> {
|
||||
// File types don't apply to directories, and we can't do anything
|
||||
// if our glob set is empty.
|
||||
if is_dir || self.set.is_empty() {
|
||||
return Match::None;
|
||||
}
|
||||
// We only want to match against the file name, so extract it.
|
||||
// If one doesn't exist, then we can't match it.
|
||||
let name = match file_name(path.as_ref()) {
|
||||
Some(name) => name,
|
||||
None if self.has_selected => {
|
||||
return Match::Ignore(Glob::unmatched());
|
||||
}
|
||||
None => {
|
||||
return Match::None;
|
||||
}
|
||||
};
|
||||
let mut matches = self.matches.get();
|
||||
self.set.matches_into(name, &mut *matches);
|
||||
// The highest precedent match is the last one.
|
||||
if let Some(&i) = matches.last() {
|
||||
let (isel, _) = self.glob_to_selection[i];
|
||||
let sel = &self.selections[isel];
|
||||
let glob = Glob(GlobInner::Matched { def: sel.inner() });
|
||||
return if sel.is_negated() {
|
||||
Match::Ignore(glob)
|
||||
} else {
|
||||
Match::Whitelist(glob)
|
||||
};
|
||||
}
|
||||
if self.has_selected {
|
||||
Match::Ignore(Glob::unmatched())
|
||||
} else {
|
||||
Match::None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// TypesBuilder builds a type matcher from a set of file type definitions and
|
||||
/// a set of file type selections.
|
||||
pub struct TypesBuilder {
|
||||
types: HashMap<String, FileTypeDef>,
|
||||
selections: Vec<Selection<()>>,
|
||||
}
|
||||
|
||||
impl TypesBuilder {
|
||||
/// Create a new builder for a file type matcher.
|
||||
///
|
||||
/// The builder contains *no* type definitions to start with. A set
|
||||
/// of default type definitions can be added with `add_defaults`, and
|
||||
/// additional type definitions can be added with `select` and `negate`.
|
||||
pub fn new() -> TypesBuilder {
|
||||
TypesBuilder { types: HashMap::new(), selections: vec![] }
|
||||
}
|
||||
|
||||
/// Build the current set of file type definitions *and* selections into
|
||||
/// a file type matcher.
|
||||
pub fn build(&self) -> Result<Types, Error> {
|
||||
let defs = self.definitions();
|
||||
let has_selected = self.selections.iter().any(|s| !s.is_negated());
|
||||
|
||||
let mut selections = vec![];
|
||||
let mut glob_to_selection = vec![];
|
||||
let mut build_set = GlobSetBuilder::new();
|
||||
for (isel, selection) in self.selections.iter().enumerate() {
|
||||
let def = match self.types.get(selection.name()) {
|
||||
Some(def) => def.clone(),
|
||||
None => {
|
||||
let name = selection.name().to_string();
|
||||
return Err(Error::UnrecognizedFileType(name));
|
||||
}
|
||||
};
|
||||
for (iglob, glob) in def.globs.iter().enumerate() {
|
||||
build_set.add(
|
||||
GlobBuilder::new(glob)
|
||||
.literal_separator(true)
|
||||
.build()
|
||||
.map_err(|err| Error::Glob {
|
||||
glob: Some(glob.to_string()),
|
||||
err: err.kind().to_string(),
|
||||
})?,
|
||||
);
|
||||
glob_to_selection.push((isel, iglob));
|
||||
}
|
||||
selections.push(selection.clone().map(move |_| def));
|
||||
}
|
||||
let set = build_set
|
||||
.build()
|
||||
.map_err(|err| Error::Glob { glob: None, err: err.to_string() })?;
|
||||
Ok(Types {
|
||||
defs,
|
||||
selections,
|
||||
has_selected,
|
||||
glob_to_selection,
|
||||
set,
|
||||
matches: Arc::new(Pool::with_available_parallelism_capacity(
|
||||
|| vec![],
|
||||
)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the set of current file type definitions.
|
||||
///
|
||||
/// Definitions and globs are sorted.
|
||||
pub fn definitions(&self) -> Vec<FileTypeDef> {
|
||||
let mut defs = vec![];
|
||||
for def in self.types.values() {
|
||||
let mut def = def.clone();
|
||||
def.globs.sort();
|
||||
defs.push(def);
|
||||
}
|
||||
defs.sort_by(|def1, def2| def1.name().cmp(def2.name()));
|
||||
defs
|
||||
}
|
||||
|
||||
/// Select the file type given by `name`.
|
||||
///
|
||||
/// If `name` is `all`, then all file types currently defined are selected.
|
||||
pub fn select(&mut self, name: &str) -> &mut TypesBuilder {
|
||||
if name == "all" {
|
||||
for name in self.types.keys() {
|
||||
self.selections.push(Selection::Select(name.to_string(), ()));
|
||||
}
|
||||
} else {
|
||||
self.selections.push(Selection::Select(name.to_string(), ()));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Ignore the file type given by `name`.
|
||||
///
|
||||
/// If `name` is `all`, then all file types currently defined are negated.
|
||||
pub fn negate(&mut self, name: &str) -> &mut TypesBuilder {
|
||||
if name == "all" {
|
||||
for name in self.types.keys() {
|
||||
self.selections.push(Selection::Negate(name.to_string(), ()));
|
||||
}
|
||||
} else {
|
||||
self.selections.push(Selection::Negate(name.to_string(), ()));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Clear any file type definitions for the type name given.
|
||||
pub fn clear(&mut self, name: &str) -> &mut TypesBuilder {
|
||||
self.types.remove(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a new file type definition. `name` can be arbitrary and `pat`
|
||||
/// should be a glob recognizing file paths belonging to the `name` type.
|
||||
///
|
||||
/// If `name` is `all` or otherwise contains any character that is not a
|
||||
/// Unicode letter or number, then an error is returned.
|
||||
pub fn add(&mut self, name: &str, glob: &str) -> Result<(), Error> {
|
||||
if name == "all" || !name.chars().all(|c| c.is_alphanumeric()) {
|
||||
return Err(Error::InvalidDefinition);
|
||||
}
|
||||
let (key, glob) = (name.to_string(), glob.to_string());
|
||||
self.types
|
||||
.entry(key)
|
||||
.or_insert_with(|| FileTypeDef {
|
||||
name: name.to_string(),
|
||||
globs: vec![],
|
||||
})
|
||||
.globs
|
||||
.push(glob);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a new file type definition specified in string form. There are two
|
||||
/// valid formats:
|
||||
/// 1. `{name}:{glob}`. This defines a 'root' definition that associates the
|
||||
/// given name with the given glob.
|
||||
/// 2. `{name}:include:{comma-separated list of already defined names}.
|
||||
/// This defines an 'include' definition that associates the given name
|
||||
/// with the definitions of the given existing types.
|
||||
/// Names may not include any characters that are not
|
||||
/// Unicode letters or numbers.
|
||||
pub fn add_def(&mut self, def: &str) -> Result<(), Error> {
|
||||
let parts: Vec<&str> = def.split(':').collect();
|
||||
match parts.len() {
|
||||
2 => {
|
||||
let name = parts[0];
|
||||
let glob = parts[1];
|
||||
if name.is_empty() || glob.is_empty() {
|
||||
return Err(Error::InvalidDefinition);
|
||||
}
|
||||
self.add(name, glob)
|
||||
}
|
||||
3 => {
|
||||
let name = parts[0];
|
||||
let types_string = parts[2];
|
||||
if name.is_empty()
|
||||
|| parts[1] != "include"
|
||||
|| types_string.is_empty()
|
||||
{
|
||||
return Err(Error::InvalidDefinition);
|
||||
}
|
||||
let types = types_string.split(',');
|
||||
// Check ahead of time to ensure that all types specified are
|
||||
// present and fail fast if not.
|
||||
if types.clone().any(|t| !self.types.contains_key(t)) {
|
||||
return Err(Error::InvalidDefinition);
|
||||
}
|
||||
for type_name in types {
|
||||
let globs =
|
||||
self.types.get(type_name).unwrap().globs.clone();
|
||||
for glob in globs {
|
||||
self.add(name, &glob)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(Error::InvalidDefinition),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a set of default file type definitions.
|
||||
pub fn add_defaults(&mut self) -> &mut TypesBuilder {
|
||||
static MSG: &'static str = "adding a default type should never fail";
|
||||
for &(names, exts) in DEFAULT_TYPES {
|
||||
for name in names {
|
||||
for ext in exts {
|
||||
self.add(name, ext).expect(MSG);
|
||||
}
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::TypesBuilder;
|
||||
|
||||
macro_rules! matched {
|
||||
($name:ident, $types:expr, $sel:expr, $selnot:expr,
|
||||
$path:expr) => {
|
||||
matched!($name, $types, $sel, $selnot, $path, true);
|
||||
};
|
||||
(not, $name:ident, $types:expr, $sel:expr, $selnot:expr,
|
||||
$path:expr) => {
|
||||
matched!($name, $types, $sel, $selnot, $path, false);
|
||||
};
|
||||
($name:ident, $types:expr, $sel:expr, $selnot:expr,
|
||||
$path:expr, $matched:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let mut btypes = TypesBuilder::new();
|
||||
for tydef in $types {
|
||||
btypes.add_def(tydef).unwrap();
|
||||
}
|
||||
for sel in $sel {
|
||||
btypes.select(sel);
|
||||
}
|
||||
for selnot in $selnot {
|
||||
btypes.negate(selnot);
|
||||
}
|
||||
let types = btypes.build().unwrap();
|
||||
let mat = types.matched($path, false);
|
||||
assert_eq!($matched, !mat.is_ignore());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn types() -> Vec<&'static str> {
|
||||
vec![
|
||||
"html:*.html",
|
||||
"html:*.htm",
|
||||
"rust:*.rs",
|
||||
"js:*.js",
|
||||
"py:*.py",
|
||||
"python:*.py",
|
||||
"foo:*.{rs,foo}",
|
||||
"combo:include:html,rust",
|
||||
]
|
||||
}
|
||||
|
||||
matched!(match1, types(), vec!["rust"], vec![], "lib.rs");
|
||||
matched!(match2, types(), vec!["html"], vec![], "index.html");
|
||||
matched!(match3, types(), vec!["html"], vec![], "index.htm");
|
||||
matched!(match4, types(), vec!["html", "rust"], vec![], "main.rs");
|
||||
matched!(match5, types(), vec![], vec![], "index.html");
|
||||
matched!(match6, types(), vec![], vec!["rust"], "index.html");
|
||||
matched!(match7, types(), vec!["foo"], vec!["rust"], "main.foo");
|
||||
matched!(match8, types(), vec!["combo"], vec![], "index.html");
|
||||
matched!(match9, types(), vec!["combo"], vec![], "lib.rs");
|
||||
matched!(match10, types(), vec!["py"], vec![], "main.py");
|
||||
matched!(match11, types(), vec!["python"], vec![], "main.py");
|
||||
|
||||
matched!(not, matchnot1, types(), vec!["rust"], vec![], "index.html");
|
||||
matched!(not, matchnot2, types(), vec![], vec!["rust"], "main.rs");
|
||||
matched!(not, matchnot3, types(), vec!["foo"], vec!["rust"], "main.rs");
|
||||
matched!(not, matchnot4, types(), vec!["rust"], vec!["foo"], "main.rs");
|
||||
matched!(not, matchnot5, types(), vec!["rust"], vec!["foo"], "main.foo");
|
||||
matched!(not, matchnot6, types(), vec!["combo"], vec![], "leftpad.js");
|
||||
matched!(not, matchnot7, types(), vec!["py"], vec![], "index.html");
|
||||
matched!(not, matchnot8, types(), vec!["python"], vec![], "doc.md");
|
||||
|
||||
#[test]
|
||||
fn test_invalid_defs() {
|
||||
let mut btypes = TypesBuilder::new();
|
||||
for tydef in types() {
|
||||
btypes.add_def(tydef).unwrap();
|
||||
}
|
||||
// Preserve the original definitions for later comparison.
|
||||
let original_defs = btypes.definitions();
|
||||
let bad_defs = vec![
|
||||
// Reference to type that does not exist
|
||||
"combo:include:html,qwerty",
|
||||
// Bad format
|
||||
"combo:foobar:html,rust",
|
||||
"",
|
||||
];
|
||||
for def in bad_defs {
|
||||
assert!(btypes.add_def(def).is_err());
|
||||
// Ensure that nothing changed, even if some of the includes were valid.
|
||||
assert_eq!(btypes.definitions(), original_defs);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,216 +0,0 @@
|
|||
# Based on https://github.com/behnam/gitignore-test/blob/master/.gitignore
|
||||
|
||||
### file in root
|
||||
|
||||
# MATCH /file_root_1
|
||||
file_root_00
|
||||
|
||||
# NO_MATCH
|
||||
file_root_01/
|
||||
|
||||
# NO_MATCH
|
||||
file_root_02/*
|
||||
|
||||
# NO_MATCH
|
||||
file_root_03/**
|
||||
|
||||
|
||||
# MATCH /file_root_10
|
||||
/file_root_10
|
||||
|
||||
# NO_MATCH
|
||||
/file_root_11/
|
||||
|
||||
# NO_MATCH
|
||||
/file_root_12/*
|
||||
|
||||
# NO_MATCH
|
||||
/file_root_13/**
|
||||
|
||||
|
||||
# NO_MATCH
|
||||
*/file_root_20
|
||||
|
||||
# NO_MATCH
|
||||
*/file_root_21/
|
||||
|
||||
# NO_MATCH
|
||||
*/file_root_22/*
|
||||
|
||||
# NO_MATCH
|
||||
*/file_root_23/**
|
||||
|
||||
|
||||
# MATCH /file_root_30
|
||||
**/file_root_30
|
||||
|
||||
# NO_MATCH
|
||||
**/file_root_31/
|
||||
|
||||
# NO_MATCH
|
||||
**/file_root_32/*
|
||||
|
||||
# NO_MATCH
|
||||
**/file_root_33/**
|
||||
|
||||
|
||||
### file in sub-dir
|
||||
|
||||
# MATCH /parent_dir/file_deep_1
|
||||
file_deep_00
|
||||
|
||||
# NO_MATCH
|
||||
file_deep_01/
|
||||
|
||||
# NO_MATCH
|
||||
file_deep_02/*
|
||||
|
||||
# NO_MATCH
|
||||
file_deep_03/**
|
||||
|
||||
|
||||
# NO_MATCH
|
||||
/file_deep_10
|
||||
|
||||
# NO_MATCH
|
||||
/file_deep_11/
|
||||
|
||||
# NO_MATCH
|
||||
/file_deep_12/*
|
||||
|
||||
# NO_MATCH
|
||||
/file_deep_13/**
|
||||
|
||||
|
||||
# MATCH /parent_dir/file_deep_20
|
||||
*/file_deep_20
|
||||
|
||||
# NO_MATCH
|
||||
*/file_deep_21/
|
||||
|
||||
# NO_MATCH
|
||||
*/file_deep_22/*
|
||||
|
||||
# NO_MATCH
|
||||
*/file_deep_23/**
|
||||
|
||||
|
||||
# MATCH /parent_dir/file_deep_30
|
||||
**/file_deep_30
|
||||
|
||||
# NO_MATCH
|
||||
**/file_deep_31/
|
||||
|
||||
# NO_MATCH
|
||||
**/file_deep_32/*
|
||||
|
||||
# NO_MATCH
|
||||
**/file_deep_33/**
|
||||
|
||||
|
||||
### dir in root
|
||||
|
||||
# MATCH /dir_root_00
|
||||
dir_root_00
|
||||
|
||||
# MATCH /dir_root_01
|
||||
dir_root_01/
|
||||
|
||||
# MATCH /dir_root_02
|
||||
dir_root_02/*
|
||||
|
||||
# MATCH /dir_root_03
|
||||
dir_root_03/**
|
||||
|
||||
|
||||
# MATCH /dir_root_10
|
||||
/dir_root_10
|
||||
|
||||
# MATCH /dir_root_11
|
||||
/dir_root_11/
|
||||
|
||||
# MATCH /dir_root_12
|
||||
/dir_root_12/*
|
||||
|
||||
# MATCH /dir_root_13
|
||||
/dir_root_13/**
|
||||
|
||||
|
||||
# NO_MATCH
|
||||
*/dir_root_20
|
||||
|
||||
# NO_MATCH
|
||||
*/dir_root_21/
|
||||
|
||||
# NO_MATCH
|
||||
*/dir_root_22/*
|
||||
|
||||
# NO_MATCH
|
||||
*/dir_root_23/**
|
||||
|
||||
|
||||
# MATCH /dir_root_30
|
||||
**/dir_root_30
|
||||
|
||||
# MATCH /dir_root_31
|
||||
**/dir_root_31/
|
||||
|
||||
# MATCH /dir_root_32
|
||||
**/dir_root_32/*
|
||||
|
||||
# MATCH /dir_root_33
|
||||
**/dir_root_33/**
|
||||
|
||||
|
||||
### dir in sub-dir
|
||||
|
||||
# MATCH /parent_dir/dir_deep_00
|
||||
dir_deep_00
|
||||
|
||||
# MATCH /parent_dir/dir_deep_01
|
||||
dir_deep_01/
|
||||
|
||||
# NO_MATCH
|
||||
dir_deep_02/*
|
||||
|
||||
# NO_MATCH
|
||||
dir_deep_03/**
|
||||
|
||||
|
||||
# NO_MATCH
|
||||
/dir_deep_10
|
||||
|
||||
# NO_MATCH
|
||||
/dir_deep_11/
|
||||
|
||||
# NO_MATCH
|
||||
/dir_deep_12/*
|
||||
|
||||
# NO_MATCH
|
||||
/dir_deep_13/**
|
||||
|
||||
|
||||
# MATCH /parent_dir/dir_deep_20
|
||||
*/dir_deep_20
|
||||
|
||||
# MATCH /parent_dir/dir_deep_21
|
||||
*/dir_deep_21/
|
||||
|
||||
# MATCH /parent_dir/dir_deep_22
|
||||
*/dir_deep_22/*
|
||||
|
||||
# MATCH /parent_dir/dir_deep_23
|
||||
*/dir_deep_23/**
|
||||
|
||||
|
||||
# MATCH /parent_dir/dir_deep_30
|
||||
**/dir_deep_30
|
||||
|
||||
# MATCH /parent_dir/dir_deep_31
|
||||
**/dir_deep_31/
|
||||
|
||||
# MATCH /parent_dir/dir_deep_32
|
||||
**/dir_deep_32/*
|
||||
|
||||
# MATCH /parent_dir/dir_deep_33
|
||||
**/dir_deep_33/**
|
||||
|
|
@ -1,318 +0,0 @@
|
|||
use std::path::Path;
|
||||
|
||||
use ignore::gitignore::{Gitignore, GitignoreBuilder};
|
||||
|
||||
const IGNORE_FILE: &'static str =
|
||||
"tests/gitignore_matched_path_or_any_parents_tests.gitignore";
|
||||
|
||||
fn get_gitignore() -> Gitignore {
|
||||
let mut builder = GitignoreBuilder::new("ROOT");
|
||||
let error = builder.add(IGNORE_FILE);
|
||||
assert!(error.is_none(), "failed to open gitignore file");
|
||||
builder.build().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "path is expected to be under the root")]
|
||||
fn test_path_should_be_under_root() {
|
||||
let gitignore = get_gitignore();
|
||||
let path = "/tmp/some_file";
|
||||
gitignore.matched_path_or_any_parents(Path::new(path), false);
|
||||
assert!(false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_files_in_root() {
|
||||
let gitignore = get_gitignore();
|
||||
let m = |path: &str| {
|
||||
gitignore.matched_path_or_any_parents(Path::new(path), false)
|
||||
};
|
||||
|
||||
// 0x
|
||||
assert!(m("ROOT/file_root_00").is_ignore());
|
||||
assert!(m("ROOT/file_root_01").is_none());
|
||||
assert!(m("ROOT/file_root_02").is_none());
|
||||
assert!(m("ROOT/file_root_03").is_none());
|
||||
|
||||
// 1x
|
||||
assert!(m("ROOT/file_root_10").is_ignore());
|
||||
assert!(m("ROOT/file_root_11").is_none());
|
||||
assert!(m("ROOT/file_root_12").is_none());
|
||||
assert!(m("ROOT/file_root_13").is_none());
|
||||
|
||||
// 2x
|
||||
assert!(m("ROOT/file_root_20").is_none());
|
||||
assert!(m("ROOT/file_root_21").is_none());
|
||||
assert!(m("ROOT/file_root_22").is_none());
|
||||
assert!(m("ROOT/file_root_23").is_none());
|
||||
|
||||
// 3x
|
||||
assert!(m("ROOT/file_root_30").is_ignore());
|
||||
assert!(m("ROOT/file_root_31").is_none());
|
||||
assert!(m("ROOT/file_root_32").is_none());
|
||||
assert!(m("ROOT/file_root_33").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_files_in_deep() {
|
||||
let gitignore = get_gitignore();
|
||||
let m = |path: &str| {
|
||||
gitignore.matched_path_or_any_parents(Path::new(path), false)
|
||||
};
|
||||
|
||||
// 0x
|
||||
assert!(m("ROOT/parent_dir/file_deep_00").is_ignore());
|
||||
assert!(m("ROOT/parent_dir/file_deep_01").is_none());
|
||||
assert!(m("ROOT/parent_dir/file_deep_02").is_none());
|
||||
assert!(m("ROOT/parent_dir/file_deep_03").is_none());
|
||||
|
||||
// 1x
|
||||
assert!(m("ROOT/parent_dir/file_deep_10").is_none());
|
||||
assert!(m("ROOT/parent_dir/file_deep_11").is_none());
|
||||
assert!(m("ROOT/parent_dir/file_deep_12").is_none());
|
||||
assert!(m("ROOT/parent_dir/file_deep_13").is_none());
|
||||
|
||||
// 2x
|
||||
assert!(m("ROOT/parent_dir/file_deep_20").is_ignore());
|
||||
assert!(m("ROOT/parent_dir/file_deep_21").is_none());
|
||||
assert!(m("ROOT/parent_dir/file_deep_22").is_none());
|
||||
assert!(m("ROOT/parent_dir/file_deep_23").is_none());
|
||||
|
||||
// 3x
|
||||
assert!(m("ROOT/parent_dir/file_deep_30").is_ignore());
|
||||
assert!(m("ROOT/parent_dir/file_deep_31").is_none());
|
||||
assert!(m("ROOT/parent_dir/file_deep_32").is_none());
|
||||
assert!(m("ROOT/parent_dir/file_deep_33").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dirs_in_root() {
|
||||
let gitignore = get_gitignore();
|
||||
let m = |path: &str, is_dir: bool| {
|
||||
gitignore.matched_path_or_any_parents(Path::new(path), is_dir)
|
||||
};
|
||||
|
||||
// 00
|
||||
assert!(m("ROOT/dir_root_00", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_00/file", false).is_ignore());
|
||||
assert!(m("ROOT/dir_root_00/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_00/child_dir/file", false).is_ignore());
|
||||
|
||||
// 01
|
||||
assert!(m("ROOT/dir_root_01", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_01/file", false).is_ignore());
|
||||
assert!(m("ROOT/dir_root_01/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_01/child_dir/file", false).is_ignore());
|
||||
|
||||
// 02
|
||||
assert!(m("ROOT/dir_root_02", true).is_none()); // dir itself doesn't match
|
||||
assert!(m("ROOT/dir_root_02/file", false).is_ignore());
|
||||
assert!(m("ROOT/dir_root_02/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_02/child_dir/file", false).is_ignore());
|
||||
|
||||
// 03
|
||||
assert!(m("ROOT/dir_root_03", true).is_none()); // dir itself doesn't match
|
||||
assert!(m("ROOT/dir_root_03/file", false).is_ignore());
|
||||
assert!(m("ROOT/dir_root_03/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_03/child_dir/file", false).is_ignore());
|
||||
|
||||
// 10
|
||||
assert!(m("ROOT/dir_root_10", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_10/file", false).is_ignore());
|
||||
assert!(m("ROOT/dir_root_10/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_10/child_dir/file", false).is_ignore());
|
||||
|
||||
// 11
|
||||
assert!(m("ROOT/dir_root_11", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_11/file", false).is_ignore());
|
||||
assert!(m("ROOT/dir_root_11/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_11/child_dir/file", false).is_ignore());
|
||||
|
||||
// 12
|
||||
assert!(m("ROOT/dir_root_12", true).is_none()); // dir itself doesn't match
|
||||
assert!(m("ROOT/dir_root_12/file", false).is_ignore());
|
||||
assert!(m("ROOT/dir_root_12/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_12/child_dir/file", false).is_ignore());
|
||||
|
||||
// 13
|
||||
assert!(m("ROOT/dir_root_13", true).is_none());
|
||||
assert!(m("ROOT/dir_root_13/file", false).is_ignore());
|
||||
assert!(m("ROOT/dir_root_13/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_13/child_dir/file", false).is_ignore());
|
||||
|
||||
// 20
|
||||
assert!(m("ROOT/dir_root_20", true).is_none());
|
||||
assert!(m("ROOT/dir_root_20/file", false).is_none());
|
||||
assert!(m("ROOT/dir_root_20/child_dir", true).is_none());
|
||||
assert!(m("ROOT/dir_root_20/child_dir/file", false).is_none());
|
||||
|
||||
// 21
|
||||
assert!(m("ROOT/dir_root_21", true).is_none());
|
||||
assert!(m("ROOT/dir_root_21/file", false).is_none());
|
||||
assert!(m("ROOT/dir_root_21/child_dir", true).is_none());
|
||||
assert!(m("ROOT/dir_root_21/child_dir/file", false).is_none());
|
||||
|
||||
// 22
|
||||
assert!(m("ROOT/dir_root_22", true).is_none());
|
||||
assert!(m("ROOT/dir_root_22/file", false).is_none());
|
||||
assert!(m("ROOT/dir_root_22/child_dir", true).is_none());
|
||||
assert!(m("ROOT/dir_root_22/child_dir/file", false).is_none());
|
||||
|
||||
// 23
|
||||
assert!(m("ROOT/dir_root_23", true).is_none());
|
||||
assert!(m("ROOT/dir_root_23/file", false).is_none());
|
||||
assert!(m("ROOT/dir_root_23/child_dir", true).is_none());
|
||||
assert!(m("ROOT/dir_root_23/child_dir/file", false).is_none());
|
||||
|
||||
// 30
|
||||
assert!(m("ROOT/dir_root_30", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_30/file", false).is_ignore());
|
||||
assert!(m("ROOT/dir_root_30/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_30/child_dir/file", false).is_ignore());
|
||||
|
||||
// 31
|
||||
assert!(m("ROOT/dir_root_31", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_31/file", false).is_ignore());
|
||||
assert!(m("ROOT/dir_root_31/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_31/child_dir/file", false).is_ignore());
|
||||
|
||||
// 32
|
||||
assert!(m("ROOT/dir_root_32", true).is_none()); // dir itself doesn't match
|
||||
assert!(m("ROOT/dir_root_32/file", false).is_ignore());
|
||||
assert!(m("ROOT/dir_root_32/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_32/child_dir/file", false).is_ignore());
|
||||
|
||||
// 33
|
||||
assert!(m("ROOT/dir_root_33", true).is_none()); // dir itself doesn't match
|
||||
assert!(m("ROOT/dir_root_33/file", false).is_ignore());
|
||||
assert!(m("ROOT/dir_root_33/child_dir", true).is_ignore());
|
||||
assert!(m("ROOT/dir_root_33/child_dir/file", false).is_ignore());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dirs_in_deep() {
|
||||
let gitignore = get_gitignore();
|
||||
let m = |path: &str, is_dir: bool| {
|
||||
gitignore.matched_path_or_any_parents(Path::new(path), is_dir)
|
||||
};
|
||||
|
||||
// 00
|
||||
assert!(m("ROOT/parent_dir/dir_deep_00", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_00/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_00/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_00/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 01
|
||||
assert!(m("ROOT/parent_dir/dir_deep_01", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_01/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_01/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_01/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 02
|
||||
assert!(m("ROOT/parent_dir/dir_deep_02", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_02/file", false).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_02/child_dir", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_02/child_dir/file", false).is_none());
|
||||
|
||||
// 03
|
||||
assert!(m("ROOT/parent_dir/dir_deep_03", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_03/file", false).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_03/child_dir", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_03/child_dir/file", false).is_none());
|
||||
|
||||
// 10
|
||||
assert!(m("ROOT/parent_dir/dir_deep_10", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_10/file", false).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_10/child_dir", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_10/child_dir/file", false).is_none());
|
||||
|
||||
// 11
|
||||
assert!(m("ROOT/parent_dir/dir_deep_11", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_11/file", false).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_11/child_dir", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_11/child_dir/file", false).is_none());
|
||||
|
||||
// 12
|
||||
assert!(m("ROOT/parent_dir/dir_deep_12", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_12/file", false).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_12/child_dir", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_12/child_dir/file", false).is_none());
|
||||
|
||||
// 13
|
||||
assert!(m("ROOT/parent_dir/dir_deep_13", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_13/file", false).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_13/child_dir", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_13/child_dir/file", false).is_none());
|
||||
|
||||
// 20
|
||||
assert!(m("ROOT/parent_dir/dir_deep_20", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_20/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_20/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_20/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 21
|
||||
assert!(m("ROOT/parent_dir/dir_deep_21", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_21/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_21/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_21/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 22
|
||||
// dir itself doesn't match
|
||||
assert!(m("ROOT/parent_dir/dir_deep_22", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_22/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_22/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_22/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 23
|
||||
// dir itself doesn't match
|
||||
assert!(m("ROOT/parent_dir/dir_deep_23", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_23/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_23/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_23/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 30
|
||||
assert!(m("ROOT/parent_dir/dir_deep_30", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_30/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_30/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_30/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 31
|
||||
assert!(m("ROOT/parent_dir/dir_deep_31", true).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_31/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_31/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_31/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 32
|
||||
// dir itself doesn't match
|
||||
assert!(m("ROOT/parent_dir/dir_deep_32", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_32/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_32/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_32/child_dir/file", false).is_ignore()
|
||||
);
|
||||
|
||||
// 33
|
||||
// dir itself doesn't match
|
||||
assert!(m("ROOT/parent_dir/dir_deep_33", true).is_none());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_33/file", false).is_ignore());
|
||||
assert!(m("ROOT/parent_dir/dir_deep_33/child_dir", true).is_ignore());
|
||||
assert!(
|
||||
m("ROOT/parent_dir/dir_deep_33/child_dir/file", false).is_ignore()
|
||||
);
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
ignore/this/path
|
||||
# This file begins with a BOM (U+FEFF)
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
use ignore::gitignore::GitignoreBuilder;
|
||||
|
||||
const IGNORE_FILE: &'static str = "tests/gitignore_skip_bom.gitignore";
|
||||
|
||||
/// Skip a Byte-Order Mark (BOM) at the beginning of the file, matching Git's
|
||||
/// behavior.
|
||||
///
|
||||
/// Ref: <https://github.com/BurntSushi/ripgrep/issues/2177>
|
||||
#[test]
|
||||
fn gitignore_skip_bom() {
|
||||
let mut builder = GitignoreBuilder::new("ROOT");
|
||||
let error = builder.add(IGNORE_FILE);
|
||||
assert!(error.is_none(), "failed to open gitignore file");
|
||||
let g = builder.build().unwrap();
|
||||
|
||||
assert!(g.matched("ignore/this/path", false).is_ignore());
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
[target.aarch64-unknown-linux-musl]
|
||||
linker = "aarch64-linux-musl-gcc"
|
||||
rustflags = ["-C", "target-feature=-crt-static"]
|
||||
[target.armv7-unknown-linux-gnueabihf]
|
||||
linker = "arm-linux-gnueabihf-gcc"
|
||||
# Statically link Visual Studio redistributables on Windows builds
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
[target.aarch64-pc-windows-msvc]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
[target.'cfg(target_env = "gnu")']
|
||||
rustflags = ["-C", "link-args=-Wl,-z,nodelete"]
|
||||
207
crates/node/.gitignore
vendored
207
crates/node/.gitignore
vendored
|
|
@ -1,207 +0,0 @@
|
|||
# Created by https://www.toptal.com/developers/gitignore/api/node
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=node
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/macos
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=macos
|
||||
|
||||
### macOS ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### macOS Patch ###
|
||||
# iCloud generated files
|
||||
*.icloud
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/macos
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/windows
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=windows
|
||||
|
||||
### Windows ###
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/windows
|
||||
|
||||
#Added by cargo
|
||||
|
||||
/target
|
||||
Cargo.lock
|
||||
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
# Generated
|
||||
*.node
|
||||
*.wasm
|
||||
index.d.ts
|
||||
index.js
|
||||
browser.js
|
||||
tailwindcss-oxide.wasi-browser.js
|
||||
tailwindcss-oxide.wasi.cjs
|
||||
tailwindcss-oxide.wasi.d.cts
|
||||
wasi-worker-browser.mjs
|
||||
wasi-worker.mjs
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
target
|
||||
Cargo.lock
|
||||
.cargo
|
||||
.github
|
||||
npm
|
||||
.eslintrc
|
||||
.prettierignore
|
||||
rustfmt.toml
|
||||
yarn.lock
|
||||
*.node
|
||||
.yarn
|
||||
__test__
|
||||
renovate.json
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
[package]
|
||||
edition = "2021"
|
||||
name = "tailwind-oxide"
|
||||
version = "0.0.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
|
||||
napi = { version = "3.11.0", default-features = false, features = ["napi4"] }
|
||||
napi-derive = "3.6.0"
|
||||
tailwindcss-oxide = { path = "../oxide" }
|
||||
rayon = "1.12.0"
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = "2.3.2"
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
extern crate napi_build;
|
||||
|
||||
fn main() {
|
||||
napi_build::setup();
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue