Add `@source not` support (#17255)
This PR adds a new source detection feature: `@source not "…"`. It can
be used to exclude files specifically from your source configuration
without having to think about creating a rule that matches all but the
requested file:
```css
@import "tailwindcss";
@source not "../src/my-tailwind-js-plugin.js";
```
While working on this feature, we noticed that there are multiple places
with different heuristics we used to scan the file system. These are:
- Auto source detection (so the default configuration or an `@source
"./my-dir"`)
- Custom sources ( e.g. `@source "./**/*.bin"` — these contain file
extensions)
- The code to detect updates on the file system
Because of the different heuristics, we were able to construct failing
cases (e.g. when you create a new file into `my-dir` that would be
thrown out by auto-source detection, it'd would actually be scanned). We
were also leaving a lot of performance on the table as the file system
is traversed multiple times for certain problems.
To resolve these issues, we're now unifying all of these systems into
one `ignore` crate walker setup. We also implemented features like
auto-source-detection and the `not` flag as additional _gitignore_ rules
only, avoid the need for a lot of custom code needed to make decisions.
High level, this is what happens after the now:
- We collect all non-negative `@source` rules into a list of _roots_
(that is the source directory for this rule) and optional _globs_ (that
is the actual rules for files in this file). For custom sources (i.e
with a custom `glob`), we add an allowlist rule to the gitignore setup,
so that we can be sure these files are always included.
- For every negative `@source` rule, we create respective ignore rules.
- Furthermore we have a custom filter that ensures files are only read
if they have been changed since the last time they were read.
So, consider the following setup:
```css
/* packages/web/src/index.css */
@import "tailwindcss";
@source "../../lib/ui/**/*.bin";
@source not "../../lib/ui/expensive.bin";
```
This creates a git ignore file that (simplified) looks like this:
```gitignore
# Auto-source rules
*.{exe,node,bin,…}
*.{css,scss,sass,…}
{node_modules,git}/
# Custom sources can overwrite auto-source rules
!lib/ui/**/*.bin
# Negative rules
lib/ui/expensive.bin
```
We then use this information _on top of your existing `.gitignore`
setup_ to resolve files (i.e so if your `.gitignore` contains rules e.g.
`dist/` this line is going to be added _before_ any of the rules lined
out in the example above. This allows negative rules to allow-list your
`.gitignore` rules.
To implement this, we're rely on the `ignore` crate but we had to make
various changes, very specific, to it so we decided to fork the crate.
All changes are prefixed with a `// CHANGED:` block but here are the
most-important ones:
- We added a way to add custom ignore rules that _extend_ (rather than
overwrite) your existing `.gitignore` rules
- We updated the order in which files are resolved and made it so that
more-specific files can allow-list more generic ignore rules.
- We resolved various issues related to adding more than one base path
to the traversal and ensured it works consistent for Linux, macOS, and
Windows.
## Behavioral changes
1. Any custom glob defined via `@source` now wins over your `.gitignore`
file and the auto-content rules.
- Resolves #16920
3. The `node_modules` and `.git` folders as well as the `.gitignore`
file are now ignored by default (but can be overridden by an explicit
`@source` rule).
- Resolves #17318
- Resolves #15882
4. Source paths into ignored-by-default folders (like `node_modules`)
now also win over your `.gitignore` configuration and auto-content
rules.
- Resolves #16669
5. Introduced `@source not "…"` to negate any previous rules.
- Resolves #17058
6. Negative `content` rules in your legacy JavaScript configuration
(e.g. `content: ['!./src']`) now work with v4.
- Resolves #15943
7. The order of `@source` definitions matter now, because you can
technically include or negate previous rules. This is similar to your
`.gitingore` file.
9. Rebuilds in watch mode now take the `@source` configuration into
account
- Resolves #15684
## Combining with other features
Note that the `not` flag is also already compatible with [`@source
inline(…)`](https://github.com/tailwindlabs/tailwindcss/pull/17147)
added in an earlier commit:
```css
@import "tailwindcss";
@source not inline("container");
```
## Test plan
- We added a bunch of oxide unit tests to ensure that the right files
are scanned
- We updated the existing integration tests with new `@source not "…"`
specific examples and updated the existing tests to match the subtle
behavior changes
- We also added a new special tag `[ci-all]` that, when added to the
description of a PR, causes the PR to run unit and integration tests on
all operating systems.
[ci-all]
---------
Co-authored-by: Philipp Spiess <hello@philippspiess.com>
2025-03-25 15:54:41 +01:00
|
|
|
import dedent from 'dedent'
|
|
|
|
|
import path from 'node:path'
|
|
|
|
|
import { candidate, css, html, js, json, test, yaml } from '../utils'
|
|
|
|
|
|
|
|
|
|
test(
|
|
|
|
|
'auto source detection kitchen sink',
|
|
|
|
|
{
|
|
|
|
|
fs: {
|
|
|
|
|
'package.json': json`
|
|
|
|
|
{
|
|
|
|
|
"dependencies": {
|
|
|
|
|
"postcss": "^8",
|
|
|
|
|
"postcss-cli": "^10",
|
|
|
|
|
"tailwindcss": "workspace:^",
|
|
|
|
|
"@tailwindcss/postcss": "workspace:^"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
'postcss.config.js': js`
|
|
|
|
|
module.exports = {
|
|
|
|
|
plugins: {
|
|
|
|
|
'@tailwindcss/postcss': {},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
'index.css': css`
|
|
|
|
|
@reference 'tailwindcss/theme';
|
|
|
|
|
|
|
|
|
|
/* (1) */
|
|
|
|
|
/* - Only './src' should be auto-scanned, not the current working directory */
|
|
|
|
|
/* - .gitignore'd paths should be ignored (node_modules) */
|
|
|
|
|
/* - Binary extensions should be ignored (jpg, zip) */
|
|
|
|
|
@import 'tailwindcss/utilities' source('./src');
|
|
|
|
|
|
|
|
|
|
/* (2) */
|
|
|
|
|
/* - All HTML and JSX files in 'ignored/components' should be scanned */
|
|
|
|
|
/* - All other extensions should be ignored */
|
|
|
|
|
@source "./ignored/components/*.{html,jsx}";
|
|
|
|
|
|
|
|
|
|
/* (3) */
|
|
|
|
|
/* - './components' should be auto-scanned in addition to './src' */
|
|
|
|
|
/* - './components/ignored.html' should still be ignored */
|
|
|
|
|
/* - Binary extensions in './components' should be ignored */
|
|
|
|
|
@source "./components";
|
|
|
|
|
|
|
|
|
|
/* (4) */
|
|
|
|
|
/* - './pages' should be auto-scanned */
|
|
|
|
|
/* - Only '.html' files should be included */
|
|
|
|
|
/* - './page/ignored.html' will not be ignored because of the specific pattern */
|
|
|
|
|
@source "./pages/**/*.html";
|
|
|
|
|
`,
|
|
|
|
|
|
|
|
|
|
'.gitignore': dedent`
|
|
|
|
|
/src/ignored
|
|
|
|
|
/ignored
|
|
|
|
|
/components/ignored.html
|
|
|
|
|
/pages/ignored.html
|
|
|
|
|
`,
|
|
|
|
|
|
|
|
|
|
// (1)
|
|
|
|
|
'index.html': 'content-["index.html"] content-["BAD"]', // "Root" source is in `./src`
|
|
|
|
|
'src/index.html': 'content-["src/index.html"]',
|
|
|
|
|
'src/nested/index.html': 'content-["src/nested/index.html"]',
|
|
|
|
|
'src/index.jpg': 'content-["src/index.jpg"] content-["BAD"]',
|
|
|
|
|
'src/nested/index.tar': 'content-["src/nested/index.tar"] content-["BAD"]',
|
|
|
|
|
'src/ignored/index.html': 'content-["src/ignored/index.html"] content-["BAD"]',
|
|
|
|
|
|
|
|
|
|
// (2)
|
|
|
|
|
'ignored/components/my-component.html': 'content-["ignored/components/my-component.html"]',
|
|
|
|
|
'ignored/components/my-component.jsx': 'content-["ignored/components/my-component.jsx"]',
|
|
|
|
|
|
|
|
|
|
// Ignored and not explicitly listed by (2)
|
|
|
|
|
'ignored/components/my-component.tsx':
|
|
|
|
|
'content-["ignored/components/my-component.tsx"] content-["BAD"]',
|
|
|
|
|
'ignored/components/nested/my-component.html':
|
|
|
|
|
'content-["ignored/components/nested/my-component.html"] content-["BAD"]',
|
|
|
|
|
|
|
|
|
|
// (3)
|
|
|
|
|
'components/my-component.tsx': 'content-["components/my-component.tsx"]',
|
|
|
|
|
'components/nested/my-component.tsx': 'content-["components/nested/my-component.tsx"]',
|
|
|
|
|
'components/ignored.html': 'content-["components/ignored.html"] content-["BAD"]',
|
|
|
|
|
|
|
|
|
|
// (4)
|
|
|
|
|
'pages/foo.html': 'content-["pages/foo.html"]',
|
|
|
|
|
'pages/nested/foo.html': 'content-["pages/nested/foo.html"]',
|
|
|
|
|
'pages/ignored.html': 'content-["pages/ignored.html"]',
|
|
|
|
|
'pages/foo.jsx': 'content-["pages/foo.jsx"] content-["BAD"]',
|
|
|
|
|
'pages/nested/foo.jsx': 'content-["pages/nested/foo.jsx"] content-["BAD"]',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
async ({ fs, exec, expect }) => {
|
|
|
|
|
await exec('pnpm postcss index.css --output dist/out.css')
|
|
|
|
|
|
|
|
|
|
expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(`
|
|
|
|
|
"
|
|
|
|
|
--- ./dist/out.css ---
|
Improve compatibility with Safari 15 (#17435)
This PR improves the compatibility with Tailwind CSS v4 with unsupported
browsers with the goal to greatly improve compatibility with Safari 15.
To make this work, this PR makes the following changes to all code
- Change `oklab(…)` default theme values to use a percentage in the
first place (so instead of `--color-red-500: oklch(0.637 0.237 25.331);`
we now define it as `--color-red-500: oklch(63.7% 0.237 25.331);` since
this syntax has much broader support on Safari).
- Polyfill `@property` with a `@supports` query targeting older versions
of Safari and Firefox *
- Create fallbacks for the `color-mix(…)` function that use _inlined
color values from your theme_ so that they can be computed a compile
time by `lightningcss`. These fallbacks will convert to srgb to increase
compatibility.
- Create fallbacks for the _relative color_ feature used in the new
shadow utilities and using `color-mix(…)` in case _relative color_ is
applied on `currentcolor` (due to limited browser support)
- Create fallbacks for gradient interpolation methods (e.g. to support
`bg-linear-to-r/oklab`)
- Polyfill `@media` queries range syntax.
## A simplified example
Given this example CSS input:
```css
@import 'tailwindcss';
@source inline('from-cyan-500/50 bg-linear-45');
```
Here's the updated output CSS including the newly added polyfills and
updated `oklab` values:
```css
.bg-linear-45 {
--tw-gradient-position: 45deg;
background-image: linear-gradient(var(--tw-gradient-stops));
}
@supports (background-image: linear-gradient(in lab, red, red)) {
.bg-linear-45 {
--tw-gradient-position: 45deg in oklab;
}
}
.from-cyan-500\\/50 {
--tw-gradient-from: oklab(71.5% -.11682 -.08247 / .5);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
@supports (color: color-mix(in lab, red, red)) {
.from-cyan-500\\/50 {
--tw-gradient-from: color-mix(in oklab, var(--color-cyan-500) 50%, transparent);
}
}
:root, :host {
--color-cyan-500: oklch(71.5% .143 215.221);
}
@supports (((-webkit-hyphens: none)) and (not (margin-trim: 1lh))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) {
@layer base {
*, :before, :after, ::backdrop {
--tw-gradient-position: initial;
--tw-gradient-from: #0000;
--tw-gradient-via: #0000;
--tw-gradient-to: #0000;
--tw-gradient-stops: initial;
--tw-gradient-via-stops: initial;
--tw-gradient-from-position: 0%;
--tw-gradient-via-position: 50%;
--tw-gradient-to-position: 100%;
}
}
}
@property --tw-gradient-position {
syntax: "*";
inherits: false
}
@property --tw-gradient-from {
syntax: "<color>";
inherits: false;
initial-value: #0000;
}
@property --tw-gradient-via {
syntax: "<color>";
inherits: false;
initial-value: #0000;
}
@property --tw-gradient-to {
syntax: "<color>";
inherits: false;
initial-value: #0000;
}
@property --tw-gradient-stops {
syntax: "*";
inherits: false
}
@property --tw-gradient-via-stops {
syntax: "*";
inherits: false
}
@property --tw-gradient-from-position {
syntax: "<length-percentage>";
inherits: false;
initial-value: 0%;
}
@property --tw-gradient-via-position {
syntax: "<length-percentage>";
inherits: false;
initial-value: 50%;
}
@property --tw-gradient-to-position {
syntax: "<length-percentage>";
inherits: false;
initial-value: 100%;
}
```
## \* A note on `@property` polyfills and CSS modules
On Next.js, CSS module files are required to be _pure_, meaning that all
selectors must either be scoped to a class or an ID. Fortunatnyl for us,
this does not apply to `@property` rules which we've been using before
to initialize CSS variables.
However, since we're now bringing back the `@property` polyfills, that
would cause unexpected rules to be exported from the CSS file as this:
```css
@reference "tailwindcss";
.skew {
@apply skew-7;
}
```
Would turn to the following file:
```css
.skew {
/* … */
}
@supports (/*…*/) {
@layer base {
*, :before, :after, ::backdrop {
--tw-gradient-position: initial;
}
}
}
@property /* … */
```
Notice that this adds a `*` selector which is not considered pure.
Unfortunately there is no way for us to silence this warning or work
around it, as the dependency causing this errors
([`postcss-modules-local-by-default`](https://github.com/css-modules/postcss-modules-local-by-default))
is bundled into Next.js. To work around crashes, these polyfills will
not apply to CSS modules processed by the PostCSS extension for now.
## Testing on tailwindcss.com
To see the changes in effect, take a look at this screencast that
compares tailwindcss.com on iOS 15.5 with a version that has the patches
of this PR applied:
https://github.com/user-attachments/assets/1279d6f5-3c63-4f30-839c-198a789f4292
## Test plan
- Tested on tailwindcss.com via a preview build:
https://tailwindcss-com-git-legacy-browsers-tailwindlabs.vercel.app/
- Updated tests
- Ensure we also test on Chrome 111, Safari 16.4, Firefox 128 to
make sure we have no regressions. Also tested on Safari 16.4, 15.5, 18.0
2025-04-01 13:33:22 +02:00
|
|
|
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
|
|
|
|
@layer base {
|
|
|
|
|
*, ::before, ::after, ::backdrop {
|
|
|
|
|
--tw-content: "";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
Add `@source not` support (#17255)
This PR adds a new source detection feature: `@source not "…"`. It can
be used to exclude files specifically from your source configuration
without having to think about creating a rule that matches all but the
requested file:
```css
@import "tailwindcss";
@source not "../src/my-tailwind-js-plugin.js";
```
While working on this feature, we noticed that there are multiple places
with different heuristics we used to scan the file system. These are:
- Auto source detection (so the default configuration or an `@source
"./my-dir"`)
- Custom sources ( e.g. `@source "./**/*.bin"` — these contain file
extensions)
- The code to detect updates on the file system
Because of the different heuristics, we were able to construct failing
cases (e.g. when you create a new file into `my-dir` that would be
thrown out by auto-source detection, it'd would actually be scanned). We
were also leaving a lot of performance on the table as the file system
is traversed multiple times for certain problems.
To resolve these issues, we're now unifying all of these systems into
one `ignore` crate walker setup. We also implemented features like
auto-source-detection and the `not` flag as additional _gitignore_ rules
only, avoid the need for a lot of custom code needed to make decisions.
High level, this is what happens after the now:
- We collect all non-negative `@source` rules into a list of _roots_
(that is the source directory for this rule) and optional _globs_ (that
is the actual rules for files in this file). For custom sources (i.e
with a custom `glob`), we add an allowlist rule to the gitignore setup,
so that we can be sure these files are always included.
- For every negative `@source` rule, we create respective ignore rules.
- Furthermore we have a custom filter that ensures files are only read
if they have been changed since the last time they were read.
So, consider the following setup:
```css
/* packages/web/src/index.css */
@import "tailwindcss";
@source "../../lib/ui/**/*.bin";
@source not "../../lib/ui/expensive.bin";
```
This creates a git ignore file that (simplified) looks like this:
```gitignore
# Auto-source rules
*.{exe,node,bin,…}
*.{css,scss,sass,…}
{node_modules,git}/
# Custom sources can overwrite auto-source rules
!lib/ui/**/*.bin
# Negative rules
lib/ui/expensive.bin
```
We then use this information _on top of your existing `.gitignore`
setup_ to resolve files (i.e so if your `.gitignore` contains rules e.g.
`dist/` this line is going to be added _before_ any of the rules lined
out in the example above. This allows negative rules to allow-list your
`.gitignore` rules.
To implement this, we're rely on the `ignore` crate but we had to make
various changes, very specific, to it so we decided to fork the crate.
All changes are prefixed with a `// CHANGED:` block but here are the
most-important ones:
- We added a way to add custom ignore rules that _extend_ (rather than
overwrite) your existing `.gitignore` rules
- We updated the order in which files are resolved and made it so that
more-specific files can allow-list more generic ignore rules.
- We resolved various issues related to adding more than one base path
to the traversal and ensured it works consistent for Linux, macOS, and
Windows.
## Behavioral changes
1. Any custom glob defined via `@source` now wins over your `.gitignore`
file and the auto-content rules.
- Resolves #16920
3. The `node_modules` and `.git` folders as well as the `.gitignore`
file are now ignored by default (but can be overridden by an explicit
`@source` rule).
- Resolves #17318
- Resolves #15882
4. Source paths into ignored-by-default folders (like `node_modules`)
now also win over your `.gitignore` configuration and auto-content
rules.
- Resolves #16669
5. Introduced `@source not "…"` to negate any previous rules.
- Resolves #17058
6. Negative `content` rules in your legacy JavaScript configuration
(e.g. `content: ['!./src']`) now work with v4.
- Resolves #15943
7. The order of `@source` definitions matter now, because you can
technically include or negate previous rules. This is similar to your
`.gitingore` file.
9. Rebuilds in watch mode now take the `@source` configuration into
account
- Resolves #15684
## Combining with other features
Note that the `not` flag is also already compatible with [`@source
inline(…)`](https://github.com/tailwindlabs/tailwindcss/pull/17147)
added in an earlier commit:
```css
@import "tailwindcss";
@source not inline("container");
```
## Test plan
- We added a bunch of oxide unit tests to ensure that the right files
are scanned
- We updated the existing integration tests with new `@source not "…"`
specific examples and updated the existing tests to match the subtle
behavior changes
- We also added a new special tag `[ci-all]` that, when added to the
description of a PR, causes the PR to run unit and integration tests on
all operating systems.
[ci-all]
---------
Co-authored-by: Philipp Spiess <hello@philippspiess.com>
2025-03-25 15:54:41 +01:00
|
|
|
.content-\\[\\"components\\/my-component\\.tsx\\"\\] {
|
|
|
|
|
--tw-content: "components/my-component.tsx";
|
|
|
|
|
content: var(--tw-content);
|
|
|
|
|
}
|
|
|
|
|
.content-\\[\\"components\\/nested\\/my-component\\.tsx\\"\\] {
|
|
|
|
|
--tw-content: "components/nested/my-component.tsx";
|
|
|
|
|
content: var(--tw-content);
|
|
|
|
|
}
|
|
|
|
|
.content-\\[\\"ignored\\/components\\/my-component\\.html\\"\\] {
|
|
|
|
|
--tw-content: "ignored/components/my-component.html";
|
|
|
|
|
content: var(--tw-content);
|
|
|
|
|
}
|
|
|
|
|
.content-\\[\\"ignored\\/components\\/my-component\\.jsx\\"\\] {
|
|
|
|
|
--tw-content: "ignored/components/my-component.jsx";
|
|
|
|
|
content: var(--tw-content);
|
|
|
|
|
}
|
|
|
|
|
.content-\\[\\"pages\\/foo\\.html\\"\\] {
|
|
|
|
|
--tw-content: "pages/foo.html";
|
|
|
|
|
content: var(--tw-content);
|
|
|
|
|
}
|
|
|
|
|
.content-\\[\\"pages\\/ignored\\.html\\"\\] {
|
|
|
|
|
--tw-content: "pages/ignored.html";
|
|
|
|
|
content: var(--tw-content);
|
|
|
|
|
}
|
|
|
|
|
.content-\\[\\"pages\\/nested\\/foo\\.html\\"\\] {
|
|
|
|
|
--tw-content: "pages/nested/foo.html";
|
|
|
|
|
content: var(--tw-content);
|
|
|
|
|
}
|
|
|
|
|
.content-\\[\\"src\\/index\\.html\\"\\] {
|
|
|
|
|
--tw-content: "src/index.html";
|
|
|
|
|
content: var(--tw-content);
|
|
|
|
|
}
|
|
|
|
|
.content-\\[\\"src\\/nested\\/index\\.html\\"\\] {
|
|
|
|
|
--tw-content: "src/nested/index.html";
|
|
|
|
|
content: var(--tw-content);
|
|
|
|
|
}
|
|
|
|
|
@property --tw-content {
|
|
|
|
|
syntax: "*";
|
|
|
|
|
inherits: false;
|
|
|
|
|
initial-value: "";
|
|
|
|
|
}
|
|
|
|
|
"
|
|
|
|
|
`)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
test(
|
|
|
|
|
'auto source detection in depth, source(…) and `@source` can be configured to use auto source detection (build + watch mode)',
|
|
|
|
|
{
|
|
|
|
|
fs: {
|
|
|
|
|
'package.json': json`{}`,
|
|
|
|
|
'pnpm-workspace.yaml': yaml`
|
|
|
|
|
#
|
|
|
|
|
packages:
|
|
|
|
|
- project-a
|
|
|
|
|
`,
|
|
|
|
|
'project-a/package.json': json`
|
|
|
|
|
{
|
|
|
|
|
"dependencies": {
|
|
|
|
|
"postcss": "^8",
|
|
|
|
|
"postcss-cli": "^10",
|
|
|
|
|
"tailwindcss": "workspace:^",
|
|
|
|
|
"@tailwindcss/postcss": "workspace:^"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
'project-a/postcss.config.js': js`
|
|
|
|
|
module.exports = {
|
|
|
|
|
plugins: {
|
|
|
|
|
'@tailwindcss/postcss': {},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
'project-a/src/index.css': css`
|
|
|
|
|
@reference 'tailwindcss/theme';
|
|
|
|
|
|
|
|
|
|
/* Run auto-content detection in ../../project-b */
|
|
|
|
|
@import 'tailwindcss/utilities' source('../../project-b');
|
|
|
|
|
|
|
|
|
|
/* Explicitly using node_modules in the @source allows git ignored folders */
|
|
|
|
|
@source '../node_modules/{my-lib-1,my-lib-2}/src/**/*.html';
|
|
|
|
|
|
|
|
|
|
/* We typically ignore these extensions, but now include them explicitly */
|
|
|
|
|
@source './logo.{jpg,png}';
|
|
|
|
|
|
|
|
|
|
/* Project C should apply auto source detection */
|
|
|
|
|
@source '../../project-c';
|
|
|
|
|
|
|
|
|
|
/* Project D should apply auto source detection rules, such as ignoring node_modules */
|
|
|
|
|
@source '../../project-d/**/*.{html,js}';
|
|
|
|
|
@source '../../project-d/**/*.bin';
|
|
|
|
|
|
|
|
|
|
/* Same as above, but my-lib-2 _should_ be includes */
|
|
|
|
|
@source '../../project-d/node_modules/my-lib-2/src/*.{html,js}';
|
|
|
|
|
|
|
|
|
|
/* bar.html is git ignored, but explicitly listed here to scan */
|
|
|
|
|
@source '../../project-d/src/bar.html';
|
|
|
|
|
`,
|
|
|
|
|
|
|
|
|
|
// Project A is the current folder, but we explicitly configured
|
|
|
|
|
// `source(project-b)`, therefore project-a should not be included in
|
|
|
|
|
// the output.
|
|
|
|
|
'project-a/src/index.html': html`
|
|
|
|
|
<div
|
|
|
|
|
class="content-['SHOULD-NOT-EXIST-IN-OUTPUT'] content-['project-a/src/index.html']"
|
|
|
|
|
></div>
|
|
|
|
|
`,
|
|
|
|
|
|
|
|
|
|
// Project A explicitly includes an extension we usually ignore,
|
|
|
|
|
// therefore it should be included in the output.
|
|
|
|
|
'project-a/src/logo.jpg': html`
|
|
|
|
|
<div
|
|
|
|
|
class="content-['project-a/src/logo.jpg']"
|
|
|
|
|
></div>
|
|
|
|
|
`,
|
|
|
|
|
|
|
|
|
|
// Project A explicitly includes node_modules/{my-lib-1,my-lib-2},
|
|
|
|
|
// therefore these files should be included in the output.
|
|
|
|
|
'project-a/node_modules/my-lib-1/src/index.html': html`
|
|
|
|
|
<div
|
|
|
|
|
class="content-['project-a/node_modules/my-lib-1/src/index.html']"
|
|
|
|
|
></div>
|
|
|
|
|
`,
|
|
|
|
|
'project-a/node_modules/my-lib-2/src/index.html': html`
|
|
|
|
|
<div
|
|
|
|
|
class="content-['project-a/node_modules/my-lib-2/src/index.html']"
|
|
|
|
|
></div>
|
|
|
|
|
`,
|
|
|
|
|
|
|
|
|
|
// Project B is the configured `source(…)`, therefore auto source
|
|
|
|
|
// detection should include known extensions and folders in the output.
|
|
|
|
|
'project-b/src/index.html': html`
|
|
|
|
|
<div
|
|
|
|
|
class="content-['project-b/src/index.html']"
|
|
|
|
|
></div>
|
|
|
|
|
`,
|
|
|
|
|
|
|
|
|
|
// Project B is the configured `source(…)`, therefore auto source
|
|
|
|
|
// detection should apply and node_modules should not be included in the
|
|
|
|
|
// output.
|
|
|
|
|
'project-b/node_modules/my-lib-3/src/index.html': html`
|
|
|
|
|
<div
|
|
|
|
|
class="content-['SHOULD-NOT-EXIST-IN-OUTPUT'] content-['project-b/node_modules/my-lib-3/src/index.html']"
|
|
|
|
|
></div>
|
|
|
|
|
`,
|
|
|
|
|
|
|
|
|
|
// Project C should apply auto source detection, therefore known
|
|
|
|
|
// extensions and folders should be included in the output.
|
|
|
|
|
'project-c/src/index.html': html`
|
|
|
|
|
<div
|
|
|
|
|
class="content-['project-c/src/index.html']"
|
|
|
|
|
></div>
|
|
|
|
|
`,
|
|
|
|
|
|
|
|
|
|
// Project C should apply auto source detection, therefore known ignored
|
|
|
|
|
// extensions should not be included in the output.
|
|
|
|
|
'project-c/src/logo.jpg': html`
|
|
|
|
|
<div
|
|
|
|
|
class="content-['SHOULD-NOT-EXIST-IN-OUTPUT'] content-['project-c/src/logo.jpg']"
|
|
|
|
|
></div>
|
|
|
|
|
`,
|
|
|
|
|
|
|
|
|
|
// Project C should apply auto source detection, therefore node_modules
|
|
|
|
|
// should not be included in the output.
|
|
|
|
|
'project-c/node_modules/my-lib-1/src/index.html': html`
|
|
|
|
|
<div
|
|
|
|
|
class="content-['SHOULD-NOT-EXIST-IN-OUTPUT'] content-['project-c/node_modules/my-lib-1/src/index.html']"
|
|
|
|
|
></div>
|
|
|
|
|
`,
|
|
|
|
|
|
|
|
|
|
// Project D should apply auto source detection rules, such as ignoring
|
|
|
|
|
// node_modules.
|
|
|
|
|
'project-d/node_modules/my-lib-1/src/index.html': html`
|
|
|
|
|
<div
|
|
|
|
|
class="content-['SHOULD-NOT-EXIST-IN-OUTPUT'] content-['project-d/node_modules/my-lib-1/src/index.html']"
|
|
|
|
|
></div>
|
|
|
|
|
`,
|
|
|
|
|
|
|
|
|
|
// Project D has an explicit glob containing node_modules, thus should include the html file
|
|
|
|
|
'project-d/node_modules/my-lib-2/src/index.html': html`
|
|
|
|
|
<div
|
|
|
|
|
class="content-['project-d/node_modules/my-lib-2/src/index.html']"
|
|
|
|
|
></div>
|
|
|
|
|
`,
|
|
|
|
|
|
|
|
|
|
'project-d/src/.gitignore': dedent`
|
|
|
|
|
foo.html
|
|
|
|
|
bar.html
|
|
|
|
|
`,
|
|
|
|
|
|
|
|
|
|
// Project D, foo.html is ignored by the gitignore file but the source rule is explicit about
|
|
|
|
|
// adding all `.html` files.
|
|
|
|
|
'project-d/src/foo.html': html`
|
|
|
|
|
<div
|
|
|
|
|
class="content-['project-d/src/foo.html']"
|
|
|
|
|
></div>
|
|
|
|
|
`,
|
|
|
|
|
|
|
|
|
|
// Project D, bar.html is ignored by the gitignore file. But explicitly
|
|
|
|
|
// listed as a `@source` glob.
|
|
|
|
|
'project-d/src/bar.html': html`
|
|
|
|
|
<div
|
|
|
|
|
class="content-['project-d/src/bar.html']"
|
|
|
|
|
></div>
|
|
|
|
|
`,
|
|
|
|
|
|
|
|
|
|
// Project D should look for files with the extensions html and js.
|
|
|
|
|
'project-d/src/index.html': html`
|
|
|
|
|
<div
|
|
|
|
|
class="content-['project-d/src/index.html']"
|
|
|
|
|
></div>
|
|
|
|
|
`,
|
|
|
|
|
|
|
|
|
|
// Project D should have a binary file even though we ignore binary files
|
|
|
|
|
// by default, but it's explicitly listed.
|
|
|
|
|
'project-d/my-binary-file.bin': html`
|
|
|
|
|
<div
|
|
|
|
|
class="content-['project-d/my-binary-file.bin']"
|
|
|
|
|
></div>
|
|
|
|
|
`,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
async ({ fs, exec, spawn, root, expect }) => {
|
|
|
|
|
await exec('pnpm postcss src/index.css --output dist/out.css --verbose', {
|
|
|
|
|
cwd: path.join(root, 'project-a'),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(await fs.dumpFiles('./project-a/dist/*.css')).toMatchInlineSnapshot(`
|
|
|
|
|
"
|
|
|
|
|
--- ./project-a/dist/out.css ---
|
Improve compatibility with Safari 15 (#17435)
This PR improves the compatibility with Tailwind CSS v4 with unsupported
browsers with the goal to greatly improve compatibility with Safari 15.
To make this work, this PR makes the following changes to all code
- Change `oklab(…)` default theme values to use a percentage in the
first place (so instead of `--color-red-500: oklch(0.637 0.237 25.331);`
we now define it as `--color-red-500: oklch(63.7% 0.237 25.331);` since
this syntax has much broader support on Safari).
- Polyfill `@property` with a `@supports` query targeting older versions
of Safari and Firefox *
- Create fallbacks for the `color-mix(…)` function that use _inlined
color values from your theme_ so that they can be computed a compile
time by `lightningcss`. These fallbacks will convert to srgb to increase
compatibility.
- Create fallbacks for the _relative color_ feature used in the new
shadow utilities and using `color-mix(…)` in case _relative color_ is
applied on `currentcolor` (due to limited browser support)
- Create fallbacks for gradient interpolation methods (e.g. to support
`bg-linear-to-r/oklab`)
- Polyfill `@media` queries range syntax.
## A simplified example
Given this example CSS input:
```css
@import 'tailwindcss';
@source inline('from-cyan-500/50 bg-linear-45');
```
Here's the updated output CSS including the newly added polyfills and
updated `oklab` values:
```css
.bg-linear-45 {
--tw-gradient-position: 45deg;
background-image: linear-gradient(var(--tw-gradient-stops));
}
@supports (background-image: linear-gradient(in lab, red, red)) {
.bg-linear-45 {
--tw-gradient-position: 45deg in oklab;
}
}
.from-cyan-500\\/50 {
--tw-gradient-from: oklab(71.5% -.11682 -.08247 / .5);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
@supports (color: color-mix(in lab, red, red)) {
.from-cyan-500\\/50 {
--tw-gradient-from: color-mix(in oklab, var(--color-cyan-500) 50%, transparent);
}
}
:root, :host {
--color-cyan-500: oklch(71.5% .143 215.221);
}
@supports (((-webkit-hyphens: none)) and (not (margin-trim: 1lh))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) {
@layer base {
*, :before, :after, ::backdrop {
--tw-gradient-position: initial;
--tw-gradient-from: #0000;
--tw-gradient-via: #0000;
--tw-gradient-to: #0000;
--tw-gradient-stops: initial;
--tw-gradient-via-stops: initial;
--tw-gradient-from-position: 0%;
--tw-gradient-via-position: 50%;
--tw-gradient-to-position: 100%;
}
}
}
@property --tw-gradient-position {
syntax: "*";
inherits: false
}
@property --tw-gradient-from {
syntax: "<color>";
inherits: false;
initial-value: #0000;
}
@property --tw-gradient-via {
syntax: "<color>";
inherits: false;
initial-value: #0000;
}
@property --tw-gradient-to {
syntax: "<color>";
inherits: false;
initial-value: #0000;
}
@property --tw-gradient-stops {
syntax: "*";
inherits: false
}
@property --tw-gradient-via-stops {
syntax: "*";
inherits: false
}
@property --tw-gradient-from-position {
syntax: "<length-percentage>";
inherits: false;
initial-value: 0%;
}
@property --tw-gradient-via-position {
syntax: "<length-percentage>";
inherits: false;
initial-value: 50%;
}
@property --tw-gradient-to-position {
syntax: "<length-percentage>";
inherits: false;
initial-value: 100%;
}
```
## \* A note on `@property` polyfills and CSS modules
On Next.js, CSS module files are required to be _pure_, meaning that all
selectors must either be scoped to a class or an ID. Fortunatnyl for us,
this does not apply to `@property` rules which we've been using before
to initialize CSS variables.
However, since we're now bringing back the `@property` polyfills, that
would cause unexpected rules to be exported from the CSS file as this:
```css
@reference "tailwindcss";
.skew {
@apply skew-7;
}
```
Would turn to the following file:
```css
.skew {
/* … */
}
@supports (/*…*/) {
@layer base {
*, :before, :after, ::backdrop {
--tw-gradient-position: initial;
}
}
}
@property /* … */
```
Notice that this adds a `*` selector which is not considered pure.
Unfortunately there is no way for us to silence this warning or work
around it, as the dependency causing this errors
([`postcss-modules-local-by-default`](https://github.com/css-modules/postcss-modules-local-by-default))
is bundled into Next.js. To work around crashes, these polyfills will
not apply to CSS modules processed by the PostCSS extension for now.
## Testing on tailwindcss.com
To see the changes in effect, take a look at this screencast that
compares tailwindcss.com on iOS 15.5 with a version that has the patches
of this PR applied:
https://github.com/user-attachments/assets/1279d6f5-3c63-4f30-839c-198a789f4292
## Test plan
- Tested on tailwindcss.com via a preview build:
https://tailwindcss-com-git-legacy-browsers-tailwindlabs.vercel.app/
- Updated tests
- Ensure we also test on Chrome 111, Safari 16.4, Firefox 128 to
make sure we have no regressions. Also tested on Safari 16.4, 15.5, 18.0
2025-04-01 13:33:22 +02:00
|
|
|
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
|
|
|
|
@layer base {
|
|
|
|
|
*, ::before, ::after, ::backdrop {
|
|
|
|
|
--tw-content: "";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
Add `@source not` support (#17255)
This PR adds a new source detection feature: `@source not "…"`. It can
be used to exclude files specifically from your source configuration
without having to think about creating a rule that matches all but the
requested file:
```css
@import "tailwindcss";
@source not "../src/my-tailwind-js-plugin.js";
```
While working on this feature, we noticed that there are multiple places
with different heuristics we used to scan the file system. These are:
- Auto source detection (so the default configuration or an `@source
"./my-dir"`)
- Custom sources ( e.g. `@source "./**/*.bin"` — these contain file
extensions)
- The code to detect updates on the file system
Because of the different heuristics, we were able to construct failing
cases (e.g. when you create a new file into `my-dir` that would be
thrown out by auto-source detection, it'd would actually be scanned). We
were also leaving a lot of performance on the table as the file system
is traversed multiple times for certain problems.
To resolve these issues, we're now unifying all of these systems into
one `ignore` crate walker setup. We also implemented features like
auto-source-detection and the `not` flag as additional _gitignore_ rules
only, avoid the need for a lot of custom code needed to make decisions.
High level, this is what happens after the now:
- We collect all non-negative `@source` rules into a list of _roots_
(that is the source directory for this rule) and optional _globs_ (that
is the actual rules for files in this file). For custom sources (i.e
with a custom `glob`), we add an allowlist rule to the gitignore setup,
so that we can be sure these files are always included.
- For every negative `@source` rule, we create respective ignore rules.
- Furthermore we have a custom filter that ensures files are only read
if they have been changed since the last time they were read.
So, consider the following setup:
```css
/* packages/web/src/index.css */
@import "tailwindcss";
@source "../../lib/ui/**/*.bin";
@source not "../../lib/ui/expensive.bin";
```
This creates a git ignore file that (simplified) looks like this:
```gitignore
# Auto-source rules
*.{exe,node,bin,…}
*.{css,scss,sass,…}
{node_modules,git}/
# Custom sources can overwrite auto-source rules
!lib/ui/**/*.bin
# Negative rules
lib/ui/expensive.bin
```
We then use this information _on top of your existing `.gitignore`
setup_ to resolve files (i.e so if your `.gitignore` contains rules e.g.
`dist/` this line is going to be added _before_ any of the rules lined
out in the example above. This allows negative rules to allow-list your
`.gitignore` rules.
To implement this, we're rely on the `ignore` crate but we had to make
various changes, very specific, to it so we decided to fork the crate.
All changes are prefixed with a `// CHANGED:` block but here are the
most-important ones:
- We added a way to add custom ignore rules that _extend_ (rather than
overwrite) your existing `.gitignore` rules
- We updated the order in which files are resolved and made it so that
more-specific files can allow-list more generic ignore rules.
- We resolved various issues related to adding more than one base path
to the traversal and ensured it works consistent for Linux, macOS, and
Windows.
## Behavioral changes
1. Any custom glob defined via `@source` now wins over your `.gitignore`
file and the auto-content rules.
- Resolves #16920
3. The `node_modules` and `.git` folders as well as the `.gitignore`
file are now ignored by default (but can be overridden by an explicit
`@source` rule).
- Resolves #17318
- Resolves #15882
4. Source paths into ignored-by-default folders (like `node_modules`)
now also win over your `.gitignore` configuration and auto-content
rules.
- Resolves #16669
5. Introduced `@source not "…"` to negate any previous rules.
- Resolves #17058
6. Negative `content` rules in your legacy JavaScript configuration
(e.g. `content: ['!./src']`) now work with v4.
- Resolves #15943
7. The order of `@source` definitions matter now, because you can
technically include or negate previous rules. This is similar to your
`.gitingore` file.
9. Rebuilds in watch mode now take the `@source` configuration into
account
- Resolves #15684
## Combining with other features
Note that the `not` flag is also already compatible with [`@source
inline(…)`](https://github.com/tailwindlabs/tailwindcss/pull/17147)
added in an earlier commit:
```css
@import "tailwindcss";
@source not inline("container");
```
## Test plan
- We added a bunch of oxide unit tests to ensure that the right files
are scanned
- We updated the existing integration tests with new `@source not "…"`
specific examples and updated the existing tests to match the subtle
behavior changes
- We also added a new special tag `[ci-all]` that, when added to the
description of a PR, causes the PR to run unit and integration tests on
all operating systems.
[ci-all]
---------
Co-authored-by: Philipp Spiess <hello@philippspiess.com>
2025-03-25 15:54:41 +01:00
|
|
|
.content-\\[\\'project-a\\/node_modules\\/my-lib-1\\/src\\/index\\.html\\'\\] {
|
|
|
|
|
--tw-content: 'project-a/node modules/my-lib-1/src/index.html';
|
|
|
|
|
content: var(--tw-content);
|
|
|
|
|
}
|
|
|
|
|
.content-\\[\\'project-a\\/node_modules\\/my-lib-2\\/src\\/index\\.html\\'\\] {
|
|
|
|
|
--tw-content: 'project-a/node modules/my-lib-2/src/index.html';
|
|
|
|
|
content: var(--tw-content);
|
|
|
|
|
}
|
|
|
|
|
.content-\\[\\'project-a\\/src\\/logo\\.jpg\\'\\] {
|
|
|
|
|
--tw-content: 'project-a/src/logo.jpg';
|
|
|
|
|
content: var(--tw-content);
|
|
|
|
|
}
|
|
|
|
|
.content-\\[\\'project-b\\/src\\/index\\.html\\'\\] {
|
|
|
|
|
--tw-content: 'project-b/src/index.html';
|
|
|
|
|
content: var(--tw-content);
|
|
|
|
|
}
|
|
|
|
|
.content-\\[\\'project-c\\/src\\/index\\.html\\'\\] {
|
|
|
|
|
--tw-content: 'project-c/src/index.html';
|
|
|
|
|
content: var(--tw-content);
|
|
|
|
|
}
|
|
|
|
|
.content-\\[\\'project-d\\/my-binary-file\\.bin\\'\\] {
|
|
|
|
|
--tw-content: 'project-d/my-binary-file.bin';
|
|
|
|
|
content: var(--tw-content);
|
|
|
|
|
}
|
|
|
|
|
.content-\\[\\'project-d\\/node_modules\\/my-lib-2\\/src\\/index\\.html\\'\\] {
|
|
|
|
|
--tw-content: 'project-d/node modules/my-lib-2/src/index.html';
|
|
|
|
|
content: var(--tw-content);
|
|
|
|
|
}
|
|
|
|
|
.content-\\[\\'project-d\\/src\\/bar\\.html\\'\\] {
|
|
|
|
|
--tw-content: 'project-d/src/bar.html';
|
|
|
|
|
content: var(--tw-content);
|
|
|
|
|
}
|
|
|
|
|
.content-\\[\\'project-d\\/src\\/foo\\.html\\'\\] {
|
|
|
|
|
--tw-content: 'project-d/src/foo.html';
|
|
|
|
|
content: var(--tw-content);
|
|
|
|
|
}
|
|
|
|
|
.content-\\[\\'project-d\\/src\\/index\\.html\\'\\] {
|
|
|
|
|
--tw-content: 'project-d/src/index.html';
|
|
|
|
|
content: var(--tw-content);
|
|
|
|
|
}
|
|
|
|
|
@property --tw-content {
|
|
|
|
|
syntax: "*";
|
|
|
|
|
inherits: false;
|
|
|
|
|
initial-value: "";
|
|
|
|
|
}
|
|
|
|
|
"
|
|
|
|
|
`)
|
|
|
|
|
|
|
|
|
|
// Watch mode tests
|
|
|
|
|
let process = await spawn(
|
|
|
|
|
'pnpm postcss src/index.css --output dist/out.css --watch --verbose',
|
|
|
|
|
{
|
|
|
|
|
cwd: path.join(root, 'project-a'),
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
await process.onStderr((message) => message.includes('Waiting for file changes...'))
|
|
|
|
|
|
|
|
|
|
// Changes to project-a should not be included in the output, we changed the
|
|
|
|
|
// base folder to project-b.
|
|
|
|
|
await fs.write(
|
|
|
|
|
'project-a/src/index.html',
|
|
|
|
|
html`<div class="[.changed_&]:content-['project-a/src/index.html']"></div>`,
|
|
|
|
|
)
|
|
|
|
|
await fs.expectFileNotToContain('./project-a/dist/out.css', [
|
|
|
|
|
candidate`[.changed_&]:content-['project-a/src/index.html']`,
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
// Changes to this file should be included, because we explicitly listed
|
|
|
|
|
// them using `@source`.
|
|
|
|
|
await fs.write(
|
|
|
|
|
'project-a/src/logo.jpg',
|
|
|
|
|
html`<div class="[.changed_&]:content-['project-a/src/logo.jpg']"></div>`,
|
|
|
|
|
)
|
|
|
|
|
await fs.expectFileToContain('./project-a/dist/out.css', [
|
|
|
|
|
candidate`[.changed_&]:content-['project-a/src/logo.jpg']`,
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
// Changes to these files should be included, because we explicitly listed
|
|
|
|
|
// them using `@source`.
|
|
|
|
|
await fs.write(
|
|
|
|
|
'project-a/node_modules/my-lib-1/src/index.html',
|
|
|
|
|
html`<div
|
|
|
|
|
class="[.changed_&]:content-['project-a/node_modules/my-lib-1/src/index.html']"
|
|
|
|
|
></div>`,
|
|
|
|
|
)
|
|
|
|
|
await fs.expectFileToContain('./project-a/dist/out.css', [
|
|
|
|
|
candidate`[.changed_&]:content-['project-a/node_modules/my-lib-1/src/index.html']`,
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
await fs.write(
|
|
|
|
|
'project-a/node_modules/my-lib-2/src/index.html',
|
|
|
|
|
html`<div
|
|
|
|
|
class="[.changed_&]:content-['project-a/node_modules/my-lib-2/src/index.html']"
|
|
|
|
|
></div>`,
|
|
|
|
|
)
|
|
|
|
|
await fs.expectFileToContain('./project-a/dist/out.css', [
|
|
|
|
|
candidate`[.changed_&]:content-['project-a/node_modules/my-lib-2/src/index.html']`,
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
// Changes to this file should be included, because we changed the base to
|
|
|
|
|
// `project-b`.
|
|
|
|
|
await fs.write(
|
|
|
|
|
'project-b/src/index.html',
|
|
|
|
|
html`<div class="[.changed_&]:content-['project-b/src/index.html']"></div>`,
|
|
|
|
|
)
|
|
|
|
|
await fs.expectFileToContain('./project-a/dist/out.css', [
|
|
|
|
|
candidate`[.changed_&]:content-['project-b/src/index.html']`,
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
// Changes to this file should not be included. We did change the base to
|
|
|
|
|
// `project-b`, but we still apply the auto source detection rules which
|
|
|
|
|
// ignore `node_modules`.
|
|
|
|
|
await fs.write(
|
|
|
|
|
'project-b/node_modules/my-lib-3/src/index.html',
|
|
|
|
|
html`<div
|
|
|
|
|
class="[.changed_&]:content-['project-b/node_modules/my-lib-3/src/index.html']"
|
|
|
|
|
></div>`,
|
|
|
|
|
)
|
|
|
|
|
await fs.expectFileNotToContain('./project-a/dist/out.css', [
|
|
|
|
|
candidate`[.changed_&]:content-['project-b/node_modules/my-lib-3/src/index.html']`,
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
// Project C was added explicitly via `@source`, therefore changes to these
|
|
|
|
|
// files should be included.
|
|
|
|
|
await fs.write(
|
|
|
|
|
'project-c/src/index.html',
|
|
|
|
|
html`<div class="[.changed_&]:content-['project-c/src/index.html']"></div>`,
|
|
|
|
|
)
|
|
|
|
|
await fs.expectFileToContain('./project-a/dist/out.css', [
|
|
|
|
|
candidate`[.changed_&]:content-['project-c/src/index.html']`,
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
// Except for these files, since they are ignored by the default auto source
|
|
|
|
|
// detection rules.
|
|
|
|
|
await fs.write(
|
|
|
|
|
'project-c/src/logo.jpg',
|
|
|
|
|
html`<div class="[.changed_&]:content-['project-c/src/logo.jpg']"></div>`,
|
|
|
|
|
)
|
|
|
|
|
await fs.expectFileNotToContain('./project-a/dist/out.css', [
|
|
|
|
|
candidate`[.changed_&]:content-['project-c/src/logo.jpg']`,
|
|
|
|
|
])
|
|
|
|
|
await fs.write(
|
|
|
|
|
'project-c/node_modules/my-lib-1/src/index.html',
|
|
|
|
|
html`<div
|
|
|
|
|
class="[.changed_&]:content-['project-c/node_modules/my-lib-1/src/index.html']"
|
|
|
|
|
></div>`,
|
|
|
|
|
)
|
|
|
|
|
await fs.expectFileNotToContain('./project-a/dist/out.css', [
|
|
|
|
|
candidate`[.changed_&]:content-['project-c/node_modules/my-lib-1/src/index.html']`,
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
// Creating new files in the "root" of auto source detected folders
|
|
|
|
|
// We need to create the files and *then* update them because postcss-cli
|
|
|
|
|
// does not pick up new files — only changes to existing files.
|
|
|
|
|
await fs.create([
|
|
|
|
|
'project-b/new-file.html',
|
|
|
|
|
'project-b/new-folder/new-file.html',
|
|
|
|
|
'project-c/new-file.html',
|
|
|
|
|
'project-c/new-folder/new-file.html',
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
// If we don't wait writes will be coalesced into a "add" event which
|
|
|
|
|
// isn't picked up by postcss-cli.
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
|
|
|
|
|
|
|
|
await fs.write(
|
|
|
|
|
'project-b/new-file.html',
|
|
|
|
|
html`<div class="[.created_&]:content-['project-b/new-file.html']"></div>`,
|
|
|
|
|
)
|
|
|
|
|
await fs.write(
|
|
|
|
|
'project-b/new-folder/new-file.html',
|
|
|
|
|
html`<div class="[.created_&]:content-['project-b/new-folder/new-file.html']"></div>`,
|
|
|
|
|
)
|
|
|
|
|
await fs.write(
|
|
|
|
|
'project-c/new-file.html',
|
|
|
|
|
html`<div class="[.created_&]:content-['project-c/new-file.html']"></div>`,
|
|
|
|
|
)
|
|
|
|
|
await fs.write(
|
|
|
|
|
'project-c/new-folder/new-file.html',
|
|
|
|
|
html`<div class="[.created_&]:content-['project-c/new-folder/new-file.html']"></div>`,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await fs.expectFileToContain('./project-a/dist/out.css', [
|
|
|
|
|
candidate`[.created_&]:content-['project-b/new-file.html']`,
|
|
|
|
|
candidate`[.created_&]:content-['project-b/new-folder/new-file.html']`,
|
|
|
|
|
candidate`[.created_&]:content-['project-c/new-file.html']`,
|
|
|
|
|
candidate`[.created_&]:content-['project-c/new-folder/new-file.html']`,
|
|
|
|
|
])
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
test(
|
|
|
|
|
'auto source detection disabled',
|
|
|
|
|
{
|
|
|
|
|
fs: {
|
|
|
|
|
'package.json': json`
|
|
|
|
|
{
|
|
|
|
|
"dependencies": {
|
|
|
|
|
"postcss": "^8",
|
|
|
|
|
"postcss-cli": "^10",
|
|
|
|
|
"tailwindcss": "workspace:^",
|
|
|
|
|
"@tailwindcss/postcss": "workspace:^"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
'postcss.config.js': js`
|
|
|
|
|
module.exports = {
|
|
|
|
|
plugins: {
|
|
|
|
|
'@tailwindcss/postcss': {},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
'index.css': css`
|
|
|
|
|
@reference 'tailwindcss/theme';
|
|
|
|
|
|
|
|
|
|
/* (1) */
|
|
|
|
|
/* - Only './src' should be auto-scanned, not the current working directory */
|
|
|
|
|
/* - .gitignore'd paths should be ignored (node_modules) */
|
|
|
|
|
/* - Binary extensions should be ignored (jpg, zip) */
|
|
|
|
|
@import 'tailwindcss/utilities' source(none);
|
|
|
|
|
|
|
|
|
|
/* (2) */
|
|
|
|
|
/* - './pages' should be auto-scanned */
|
|
|
|
|
/* - Only '.html' files should be included */
|
|
|
|
|
/* - './page/ignored.html' will not be ignored because of the specific pattern */
|
|
|
|
|
@source "./pages/**/*.html";
|
|
|
|
|
`,
|
|
|
|
|
|
|
|
|
|
'.gitignore': dedent`
|
|
|
|
|
/src/ignored
|
|
|
|
|
/pages/ignored.html
|
|
|
|
|
`,
|
|
|
|
|
|
|
|
|
|
// (1)
|
|
|
|
|
'index.html': 'content-["index.html"] content-["BAD"]', // "Root" source is in `./src`
|
|
|
|
|
'src/index.html': 'content-["src/index.html"] content-["BAD"]',
|
|
|
|
|
'src/nested/index.html': 'content-["src/nested/index.html"] content-["BAD"]',
|
|
|
|
|
'src/index.jpg': 'content-["src/index.jpg"] content-["BAD"]',
|
|
|
|
|
'src/nested/index.tar': 'content-["src/nested/index.tar"] content-["BAD"]',
|
|
|
|
|
'src/ignored/index.html': 'content-["src/ignored/index.html"] content-["BAD"]',
|
|
|
|
|
|
|
|
|
|
// (4)
|
|
|
|
|
'pages/foo.html': 'content-["pages/foo.html"]',
|
|
|
|
|
'pages/nested/foo.html': 'content-["pages/nested/foo.html"]',
|
|
|
|
|
'pages/ignored.html': 'content-["pages/ignored.html"]',
|
|
|
|
|
'pages/foo.jsx': 'content-["pages/foo.jsx"] content-["BAD"]',
|
|
|
|
|
'pages/nested/foo.jsx': 'content-["pages/nested/foo.jsx"] content-["BAD"]',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
async ({ fs, exec, expect }) => {
|
|
|
|
|
await exec('pnpm postcss index.css --output dist/out.css')
|
|
|
|
|
|
|
|
|
|
expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(`
|
|
|
|
|
"
|
|
|
|
|
--- ./dist/out.css ---
|
Improve compatibility with Safari 15 (#17435)
This PR improves the compatibility with Tailwind CSS v4 with unsupported
browsers with the goal to greatly improve compatibility with Safari 15.
To make this work, this PR makes the following changes to all code
- Change `oklab(…)` default theme values to use a percentage in the
first place (so instead of `--color-red-500: oklch(0.637 0.237 25.331);`
we now define it as `--color-red-500: oklch(63.7% 0.237 25.331);` since
this syntax has much broader support on Safari).
- Polyfill `@property` with a `@supports` query targeting older versions
of Safari and Firefox *
- Create fallbacks for the `color-mix(…)` function that use _inlined
color values from your theme_ so that they can be computed a compile
time by `lightningcss`. These fallbacks will convert to srgb to increase
compatibility.
- Create fallbacks for the _relative color_ feature used in the new
shadow utilities and using `color-mix(…)` in case _relative color_ is
applied on `currentcolor` (due to limited browser support)
- Create fallbacks for gradient interpolation methods (e.g. to support
`bg-linear-to-r/oklab`)
- Polyfill `@media` queries range syntax.
## A simplified example
Given this example CSS input:
```css
@import 'tailwindcss';
@source inline('from-cyan-500/50 bg-linear-45');
```
Here's the updated output CSS including the newly added polyfills and
updated `oklab` values:
```css
.bg-linear-45 {
--tw-gradient-position: 45deg;
background-image: linear-gradient(var(--tw-gradient-stops));
}
@supports (background-image: linear-gradient(in lab, red, red)) {
.bg-linear-45 {
--tw-gradient-position: 45deg in oklab;
}
}
.from-cyan-500\\/50 {
--tw-gradient-from: oklab(71.5% -.11682 -.08247 / .5);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
@supports (color: color-mix(in lab, red, red)) {
.from-cyan-500\\/50 {
--tw-gradient-from: color-mix(in oklab, var(--color-cyan-500) 50%, transparent);
}
}
:root, :host {
--color-cyan-500: oklch(71.5% .143 215.221);
}
@supports (((-webkit-hyphens: none)) and (not (margin-trim: 1lh))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) {
@layer base {
*, :before, :after, ::backdrop {
--tw-gradient-position: initial;
--tw-gradient-from: #0000;
--tw-gradient-via: #0000;
--tw-gradient-to: #0000;
--tw-gradient-stops: initial;
--tw-gradient-via-stops: initial;
--tw-gradient-from-position: 0%;
--tw-gradient-via-position: 50%;
--tw-gradient-to-position: 100%;
}
}
}
@property --tw-gradient-position {
syntax: "*";
inherits: false
}
@property --tw-gradient-from {
syntax: "<color>";
inherits: false;
initial-value: #0000;
}
@property --tw-gradient-via {
syntax: "<color>";
inherits: false;
initial-value: #0000;
}
@property --tw-gradient-to {
syntax: "<color>";
inherits: false;
initial-value: #0000;
}
@property --tw-gradient-stops {
syntax: "*";
inherits: false
}
@property --tw-gradient-via-stops {
syntax: "*";
inherits: false
}
@property --tw-gradient-from-position {
syntax: "<length-percentage>";
inherits: false;
initial-value: 0%;
}
@property --tw-gradient-via-position {
syntax: "<length-percentage>";
inherits: false;
initial-value: 50%;
}
@property --tw-gradient-to-position {
syntax: "<length-percentage>";
inherits: false;
initial-value: 100%;
}
```
## \* A note on `@property` polyfills and CSS modules
On Next.js, CSS module files are required to be _pure_, meaning that all
selectors must either be scoped to a class or an ID. Fortunatnyl for us,
this does not apply to `@property` rules which we've been using before
to initialize CSS variables.
However, since we're now bringing back the `@property` polyfills, that
would cause unexpected rules to be exported from the CSS file as this:
```css
@reference "tailwindcss";
.skew {
@apply skew-7;
}
```
Would turn to the following file:
```css
.skew {
/* … */
}
@supports (/*…*/) {
@layer base {
*, :before, :after, ::backdrop {
--tw-gradient-position: initial;
}
}
}
@property /* … */
```
Notice that this adds a `*` selector which is not considered pure.
Unfortunately there is no way for us to silence this warning or work
around it, as the dependency causing this errors
([`postcss-modules-local-by-default`](https://github.com/css-modules/postcss-modules-local-by-default))
is bundled into Next.js. To work around crashes, these polyfills will
not apply to CSS modules processed by the PostCSS extension for now.
## Testing on tailwindcss.com
To see the changes in effect, take a look at this screencast that
compares tailwindcss.com on iOS 15.5 with a version that has the patches
of this PR applied:
https://github.com/user-attachments/assets/1279d6f5-3c63-4f30-839c-198a789f4292
## Test plan
- Tested on tailwindcss.com via a preview build:
https://tailwindcss-com-git-legacy-browsers-tailwindlabs.vercel.app/
- Updated tests
- Ensure we also test on Chrome 111, Safari 16.4, Firefox 128 to
make sure we have no regressions. Also tested on Safari 16.4, 15.5, 18.0
2025-04-01 13:33:22 +02:00
|
|
|
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
|
|
|
|
@layer base {
|
|
|
|
|
*, ::before, ::after, ::backdrop {
|
|
|
|
|
--tw-content: "";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
Add `@source not` support (#17255)
This PR adds a new source detection feature: `@source not "…"`. It can
be used to exclude files specifically from your source configuration
without having to think about creating a rule that matches all but the
requested file:
```css
@import "tailwindcss";
@source not "../src/my-tailwind-js-plugin.js";
```
While working on this feature, we noticed that there are multiple places
with different heuristics we used to scan the file system. These are:
- Auto source detection (so the default configuration or an `@source
"./my-dir"`)
- Custom sources ( e.g. `@source "./**/*.bin"` — these contain file
extensions)
- The code to detect updates on the file system
Because of the different heuristics, we were able to construct failing
cases (e.g. when you create a new file into `my-dir` that would be
thrown out by auto-source detection, it'd would actually be scanned). We
were also leaving a lot of performance on the table as the file system
is traversed multiple times for certain problems.
To resolve these issues, we're now unifying all of these systems into
one `ignore` crate walker setup. We also implemented features like
auto-source-detection and the `not` flag as additional _gitignore_ rules
only, avoid the need for a lot of custom code needed to make decisions.
High level, this is what happens after the now:
- We collect all non-negative `@source` rules into a list of _roots_
(that is the source directory for this rule) and optional _globs_ (that
is the actual rules for files in this file). For custom sources (i.e
with a custom `glob`), we add an allowlist rule to the gitignore setup,
so that we can be sure these files are always included.
- For every negative `@source` rule, we create respective ignore rules.
- Furthermore we have a custom filter that ensures files are only read
if they have been changed since the last time they were read.
So, consider the following setup:
```css
/* packages/web/src/index.css */
@import "tailwindcss";
@source "../../lib/ui/**/*.bin";
@source not "../../lib/ui/expensive.bin";
```
This creates a git ignore file that (simplified) looks like this:
```gitignore
# Auto-source rules
*.{exe,node,bin,…}
*.{css,scss,sass,…}
{node_modules,git}/
# Custom sources can overwrite auto-source rules
!lib/ui/**/*.bin
# Negative rules
lib/ui/expensive.bin
```
We then use this information _on top of your existing `.gitignore`
setup_ to resolve files (i.e so if your `.gitignore` contains rules e.g.
`dist/` this line is going to be added _before_ any of the rules lined
out in the example above. This allows negative rules to allow-list your
`.gitignore` rules.
To implement this, we're rely on the `ignore` crate but we had to make
various changes, very specific, to it so we decided to fork the crate.
All changes are prefixed with a `// CHANGED:` block but here are the
most-important ones:
- We added a way to add custom ignore rules that _extend_ (rather than
overwrite) your existing `.gitignore` rules
- We updated the order in which files are resolved and made it so that
more-specific files can allow-list more generic ignore rules.
- We resolved various issues related to adding more than one base path
to the traversal and ensured it works consistent for Linux, macOS, and
Windows.
## Behavioral changes
1. Any custom glob defined via `@source` now wins over your `.gitignore`
file and the auto-content rules.
- Resolves #16920
3. The `node_modules` and `.git` folders as well as the `.gitignore`
file are now ignored by default (but can be overridden by an explicit
`@source` rule).
- Resolves #17318
- Resolves #15882
4. Source paths into ignored-by-default folders (like `node_modules`)
now also win over your `.gitignore` configuration and auto-content
rules.
- Resolves #16669
5. Introduced `@source not "…"` to negate any previous rules.
- Resolves #17058
6. Negative `content` rules in your legacy JavaScript configuration
(e.g. `content: ['!./src']`) now work with v4.
- Resolves #15943
7. The order of `@source` definitions matter now, because you can
technically include or negate previous rules. This is similar to your
`.gitingore` file.
9. Rebuilds in watch mode now take the `@source` configuration into
account
- Resolves #15684
## Combining with other features
Note that the `not` flag is also already compatible with [`@source
inline(…)`](https://github.com/tailwindlabs/tailwindcss/pull/17147)
added in an earlier commit:
```css
@import "tailwindcss";
@source not inline("container");
```
## Test plan
- We added a bunch of oxide unit tests to ensure that the right files
are scanned
- We updated the existing integration tests with new `@source not "…"`
specific examples and updated the existing tests to match the subtle
behavior changes
- We also added a new special tag `[ci-all]` that, when added to the
description of a PR, causes the PR to run unit and integration tests on
all operating systems.
[ci-all]
---------
Co-authored-by: Philipp Spiess <hello@philippspiess.com>
2025-03-25 15:54:41 +01:00
|
|
|
.content-\\[\\"pages\\/foo\\.html\\"\\] {
|
|
|
|
|
--tw-content: "pages/foo.html";
|
|
|
|
|
content: var(--tw-content);
|
|
|
|
|
}
|
|
|
|
|
.content-\\[\\"pages\\/ignored\\.html\\"\\] {
|
|
|
|
|
--tw-content: "pages/ignored.html";
|
|
|
|
|
content: var(--tw-content);
|
|
|
|
|
}
|
|
|
|
|
.content-\\[\\"pages\\/nested\\/foo\\.html\\"\\] {
|
|
|
|
|
--tw-content: "pages/nested/foo.html";
|
|
|
|
|
content: var(--tw-content);
|
|
|
|
|
}
|
|
|
|
|
@property --tw-content {
|
|
|
|
|
syntax: "*";
|
|
|
|
|
inherits: false;
|
|
|
|
|
initial-value: "";
|
|
|
|
|
}
|
|
|
|
|
"
|
|
|
|
|
`)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
test(
|
|
|
|
|
'`@source not "…"`',
|
|
|
|
|
{
|
|
|
|
|
fs: {
|
|
|
|
|
'package.json': json`{}`,
|
|
|
|
|
'pnpm-workspace.yaml': yaml`
|
|
|
|
|
#
|
|
|
|
|
packages:
|
|
|
|
|
- project-a
|
|
|
|
|
`,
|
|
|
|
|
'project-a/package.json': json`
|
|
|
|
|
{
|
|
|
|
|
"dependencies": {
|
|
|
|
|
"postcss": "^8",
|
|
|
|
|
"postcss-cli": "^10",
|
|
|
|
|
"tailwindcss": "workspace:^",
|
|
|
|
|
"@tailwindcss/postcss": "workspace:^"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
'project-a/postcss.config.js': js`
|
|
|
|
|
module.exports = {
|
|
|
|
|
plugins: {
|
|
|
|
|
'@tailwindcss/postcss': {},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
'project-a/src/index.css': css`
|
|
|
|
|
@reference 'tailwindcss/theme';
|
|
|
|
|
@import 'tailwindcss/utilities';
|
|
|
|
|
|
|
|
|
|
/* Ignore a specific file */
|
|
|
|
|
@source not "./ignore-me-file.html";
|
|
|
|
|
|
|
|
|
|
/* Ignore a entire folder */
|
|
|
|
|
@source not "./ignore-me-folder";
|
|
|
|
|
|
|
|
|
|
/* Ignore an extension */
|
|
|
|
|
@source not "**/*.ts";
|
|
|
|
|
|
|
|
|
|
/* Explicit source detection for 'project-b' */
|
|
|
|
|
@source "../../project-b/**/*.html";
|
|
|
|
|
|
|
|
|
|
/* Explicitly ignoring a file in 'project-b' */
|
|
|
|
|
@source not "../../project-b/src/ignore-me.html";
|
|
|
|
|
`,
|
|
|
|
|
'project-a/src/ignore-me-file.html': html`
|
|
|
|
|
<div>
|
|
|
|
|
<div class="content-['project-a/src/ignore-me-file.html']"></div>
|
|
|
|
|
</div>
|
|
|
|
|
`,
|
|
|
|
|
'project-a/src/ignore-me-folder/index.html': html`
|
|
|
|
|
<div>
|
|
|
|
|
<div class="content-['project-a/src/ignore-me-folder/index.html']"></div>
|
|
|
|
|
</div>
|
|
|
|
|
`,
|
|
|
|
|
'project-a/src/keep-me.html': html`<div class="content-['keep-me.html']"></div>`,
|
|
|
|
|
'project-a/src/ignore-me-extension.ts': html`
|
|
|
|
|
<div>
|
|
|
|
|
<div class="content-['ignore-me-extension.ts']"></div>
|
|
|
|
|
</div>
|
|
|
|
|
`,
|
|
|
|
|
'project-b/src/ignore-me.html': html`
|
|
|
|
|
<div>
|
|
|
|
|
<div class="content-['project-b/src/ignore-me.html']"></div>
|
|
|
|
|
</div>
|
|
|
|
|
`,
|
|
|
|
|
'project-b/src/keep-me.html': html`
|
|
|
|
|
<div>
|
|
|
|
|
<div class="content-['project-b/src/keep-me.html']"></div>
|
|
|
|
|
</div>
|
|
|
|
|
`,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
async ({ fs, exec, spawn, root, expect }) => {
|
|
|
|
|
await exec('pnpm postcss src/index.css --output dist/out.css --verbose', {
|
|
|
|
|
cwd: path.join(root, 'project-a'),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(await fs.dumpFiles('./project-a/dist/*.css')).toMatchInlineSnapshot(`
|
|
|
|
|
"
|
|
|
|
|
--- ./project-a/dist/out.css ---
|
Improve compatibility with Safari 15 (#17435)
This PR improves the compatibility with Tailwind CSS v4 with unsupported
browsers with the goal to greatly improve compatibility with Safari 15.
To make this work, this PR makes the following changes to all code
- Change `oklab(…)` default theme values to use a percentage in the
first place (so instead of `--color-red-500: oklch(0.637 0.237 25.331);`
we now define it as `--color-red-500: oklch(63.7% 0.237 25.331);` since
this syntax has much broader support on Safari).
- Polyfill `@property` with a `@supports` query targeting older versions
of Safari and Firefox *
- Create fallbacks for the `color-mix(…)` function that use _inlined
color values from your theme_ so that they can be computed a compile
time by `lightningcss`. These fallbacks will convert to srgb to increase
compatibility.
- Create fallbacks for the _relative color_ feature used in the new
shadow utilities and using `color-mix(…)` in case _relative color_ is
applied on `currentcolor` (due to limited browser support)
- Create fallbacks for gradient interpolation methods (e.g. to support
`bg-linear-to-r/oklab`)
- Polyfill `@media` queries range syntax.
## A simplified example
Given this example CSS input:
```css
@import 'tailwindcss';
@source inline('from-cyan-500/50 bg-linear-45');
```
Here's the updated output CSS including the newly added polyfills and
updated `oklab` values:
```css
.bg-linear-45 {
--tw-gradient-position: 45deg;
background-image: linear-gradient(var(--tw-gradient-stops));
}
@supports (background-image: linear-gradient(in lab, red, red)) {
.bg-linear-45 {
--tw-gradient-position: 45deg in oklab;
}
}
.from-cyan-500\\/50 {
--tw-gradient-from: oklab(71.5% -.11682 -.08247 / .5);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
@supports (color: color-mix(in lab, red, red)) {
.from-cyan-500\\/50 {
--tw-gradient-from: color-mix(in oklab, var(--color-cyan-500) 50%, transparent);
}
}
:root, :host {
--color-cyan-500: oklch(71.5% .143 215.221);
}
@supports (((-webkit-hyphens: none)) and (not (margin-trim: 1lh))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) {
@layer base {
*, :before, :after, ::backdrop {
--tw-gradient-position: initial;
--tw-gradient-from: #0000;
--tw-gradient-via: #0000;
--tw-gradient-to: #0000;
--tw-gradient-stops: initial;
--tw-gradient-via-stops: initial;
--tw-gradient-from-position: 0%;
--tw-gradient-via-position: 50%;
--tw-gradient-to-position: 100%;
}
}
}
@property --tw-gradient-position {
syntax: "*";
inherits: false
}
@property --tw-gradient-from {
syntax: "<color>";
inherits: false;
initial-value: #0000;
}
@property --tw-gradient-via {
syntax: "<color>";
inherits: false;
initial-value: #0000;
}
@property --tw-gradient-to {
syntax: "<color>";
inherits: false;
initial-value: #0000;
}
@property --tw-gradient-stops {
syntax: "*";
inherits: false
}
@property --tw-gradient-via-stops {
syntax: "*";
inherits: false
}
@property --tw-gradient-from-position {
syntax: "<length-percentage>";
inherits: false;
initial-value: 0%;
}
@property --tw-gradient-via-position {
syntax: "<length-percentage>";
inherits: false;
initial-value: 50%;
}
@property --tw-gradient-to-position {
syntax: "<length-percentage>";
inherits: false;
initial-value: 100%;
}
```
## \* A note on `@property` polyfills and CSS modules
On Next.js, CSS module files are required to be _pure_, meaning that all
selectors must either be scoped to a class or an ID. Fortunatnyl for us,
this does not apply to `@property` rules which we've been using before
to initialize CSS variables.
However, since we're now bringing back the `@property` polyfills, that
would cause unexpected rules to be exported from the CSS file as this:
```css
@reference "tailwindcss";
.skew {
@apply skew-7;
}
```
Would turn to the following file:
```css
.skew {
/* … */
}
@supports (/*…*/) {
@layer base {
*, :before, :after, ::backdrop {
--tw-gradient-position: initial;
}
}
}
@property /* … */
```
Notice that this adds a `*` selector which is not considered pure.
Unfortunately there is no way for us to silence this warning or work
around it, as the dependency causing this errors
([`postcss-modules-local-by-default`](https://github.com/css-modules/postcss-modules-local-by-default))
is bundled into Next.js. To work around crashes, these polyfills will
not apply to CSS modules processed by the PostCSS extension for now.
## Testing on tailwindcss.com
To see the changes in effect, take a look at this screencast that
compares tailwindcss.com on iOS 15.5 with a version that has the patches
of this PR applied:
https://github.com/user-attachments/assets/1279d6f5-3c63-4f30-839c-198a789f4292
## Test plan
- Tested on tailwindcss.com via a preview build:
https://tailwindcss-com-git-legacy-browsers-tailwindlabs.vercel.app/
- Updated tests
- Ensure we also test on Chrome 111, Safari 16.4, Firefox 128 to
make sure we have no regressions. Also tested on Safari 16.4, 15.5, 18.0
2025-04-01 13:33:22 +02:00
|
|
|
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
|
|
|
|
@layer base {
|
|
|
|
|
*, ::before, ::after, ::backdrop {
|
|
|
|
|
--tw-content: "";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
Add `@source not` support (#17255)
This PR adds a new source detection feature: `@source not "…"`. It can
be used to exclude files specifically from your source configuration
without having to think about creating a rule that matches all but the
requested file:
```css
@import "tailwindcss";
@source not "../src/my-tailwind-js-plugin.js";
```
While working on this feature, we noticed that there are multiple places
with different heuristics we used to scan the file system. These are:
- Auto source detection (so the default configuration or an `@source
"./my-dir"`)
- Custom sources ( e.g. `@source "./**/*.bin"` — these contain file
extensions)
- The code to detect updates on the file system
Because of the different heuristics, we were able to construct failing
cases (e.g. when you create a new file into `my-dir` that would be
thrown out by auto-source detection, it'd would actually be scanned). We
were also leaving a lot of performance on the table as the file system
is traversed multiple times for certain problems.
To resolve these issues, we're now unifying all of these systems into
one `ignore` crate walker setup. We also implemented features like
auto-source-detection and the `not` flag as additional _gitignore_ rules
only, avoid the need for a lot of custom code needed to make decisions.
High level, this is what happens after the now:
- We collect all non-negative `@source` rules into a list of _roots_
(that is the source directory for this rule) and optional _globs_ (that
is the actual rules for files in this file). For custom sources (i.e
with a custom `glob`), we add an allowlist rule to the gitignore setup,
so that we can be sure these files are always included.
- For every negative `@source` rule, we create respective ignore rules.
- Furthermore we have a custom filter that ensures files are only read
if they have been changed since the last time they were read.
So, consider the following setup:
```css
/* packages/web/src/index.css */
@import "tailwindcss";
@source "../../lib/ui/**/*.bin";
@source not "../../lib/ui/expensive.bin";
```
This creates a git ignore file that (simplified) looks like this:
```gitignore
# Auto-source rules
*.{exe,node,bin,…}
*.{css,scss,sass,…}
{node_modules,git}/
# Custom sources can overwrite auto-source rules
!lib/ui/**/*.bin
# Negative rules
lib/ui/expensive.bin
```
We then use this information _on top of your existing `.gitignore`
setup_ to resolve files (i.e so if your `.gitignore` contains rules e.g.
`dist/` this line is going to be added _before_ any of the rules lined
out in the example above. This allows negative rules to allow-list your
`.gitignore` rules.
To implement this, we're rely on the `ignore` crate but we had to make
various changes, very specific, to it so we decided to fork the crate.
All changes are prefixed with a `// CHANGED:` block but here are the
most-important ones:
- We added a way to add custom ignore rules that _extend_ (rather than
overwrite) your existing `.gitignore` rules
- We updated the order in which files are resolved and made it so that
more-specific files can allow-list more generic ignore rules.
- We resolved various issues related to adding more than one base path
to the traversal and ensured it works consistent for Linux, macOS, and
Windows.
## Behavioral changes
1. Any custom glob defined via `@source` now wins over your `.gitignore`
file and the auto-content rules.
- Resolves #16920
3. The `node_modules` and `.git` folders as well as the `.gitignore`
file are now ignored by default (but can be overridden by an explicit
`@source` rule).
- Resolves #17318
- Resolves #15882
4. Source paths into ignored-by-default folders (like `node_modules`)
now also win over your `.gitignore` configuration and auto-content
rules.
- Resolves #16669
5. Introduced `@source not "…"` to negate any previous rules.
- Resolves #17058
6. Negative `content` rules in your legacy JavaScript configuration
(e.g. `content: ['!./src']`) now work with v4.
- Resolves #15943
7. The order of `@source` definitions matter now, because you can
technically include or negate previous rules. This is similar to your
`.gitingore` file.
9. Rebuilds in watch mode now take the `@source` configuration into
account
- Resolves #15684
## Combining with other features
Note that the `not` flag is also already compatible with [`@source
inline(…)`](https://github.com/tailwindlabs/tailwindcss/pull/17147)
added in an earlier commit:
```css
@import "tailwindcss";
@source not inline("container");
```
## Test plan
- We added a bunch of oxide unit tests to ensure that the right files
are scanned
- We updated the existing integration tests with new `@source not "…"`
specific examples and updated the existing tests to match the subtle
behavior changes
- We also added a new special tag `[ci-all]` that, when added to the
description of a PR, causes the PR to run unit and integration tests on
all operating systems.
[ci-all]
---------
Co-authored-by: Philipp Spiess <hello@philippspiess.com>
2025-03-25 15:54:41 +01:00
|
|
|
.content-\\[\\'keep-me\\.html\\'\\] {
|
|
|
|
|
--tw-content: 'keep-me.html';
|
|
|
|
|
content: var(--tw-content);
|
|
|
|
|
}
|
|
|
|
|
.content-\\[\\'project-b\\/src\\/keep-me\\.html\\'\\] {
|
|
|
|
|
--tw-content: 'project-b/src/keep-me.html';
|
|
|
|
|
content: var(--tw-content);
|
|
|
|
|
}
|
|
|
|
|
@property --tw-content {
|
|
|
|
|
syntax: "*";
|
|
|
|
|
inherits: false;
|
|
|
|
|
initial-value: "";
|
|
|
|
|
}
|
|
|
|
|
"
|
|
|
|
|
`)
|
|
|
|
|
|
|
|
|
|
// Watch mode tests
|
|
|
|
|
let process = await spawn(
|
|
|
|
|
'pnpm postcss src/index.css --output dist/out.css --watch --verbose',
|
|
|
|
|
{
|
|
|
|
|
cwd: path.join(root, 'project-a'),
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
await process.onStderr((message) => message.includes('Waiting for file changes...'))
|
|
|
|
|
|
|
|
|
|
fs.expectFileNotToContain('./project-a/dist/out.css', [
|
|
|
|
|
candidate`content-['project-a/src/ignore-me-file.html']`,
|
|
|
|
|
candidate`content-['project-a/src/ignore-me-folder/index.html']`,
|
|
|
|
|
candidate`content-['project-b/src/ignore-me.html']`,
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
// Changes to the keep-me files should be included
|
|
|
|
|
await fs.write(
|
|
|
|
|
'project-a/src/keep-me.html',
|
|
|
|
|
html`<div class="[.changed_&]:content-['project-a/src/keep-me.html']"></div>`,
|
|
|
|
|
)
|
|
|
|
|
await fs.expectFileToContain('./project-a/dist/out.css', [
|
|
|
|
|
candidate`[.changed_&]:content-['project-a/src/keep-me.html']`,
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
await fs.write(
|
|
|
|
|
'project-b/src/keep-me.html',
|
|
|
|
|
html`<div class="[.changed_&]:content-['project-b/src/keep-me.html']"></div>`,
|
|
|
|
|
)
|
|
|
|
|
await fs.expectFileToContain('./project-a/dist/out.css', [
|
|
|
|
|
candidate`[.changed_&]:content-['project-b/src/keep-me.html']`,
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
// Changes to the ignored files should not be included
|
|
|
|
|
await fs.write(
|
|
|
|
|
'project-a/src/ignore-me.html',
|
|
|
|
|
html`<div class="[.changed_&]:content-['project-a/src/ignore-me.html']"></div>`,
|
|
|
|
|
)
|
|
|
|
|
await fs.expectFileNotToContain('./project-a/dist/out.css', [
|
|
|
|
|
candidate`[.changed_&]:content-['project-a/src/ignore-me.html']`,
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
await fs.write(
|
|
|
|
|
'project-b/src/ignore-me.html',
|
|
|
|
|
html`<div class="[.changed_&]:content-['project-b/src/ignore-me.html']"></div>`,
|
|
|
|
|
)
|
|
|
|
|
await fs.expectFileNotToContain('./project-a/dist/out.css', [
|
|
|
|
|
candidate`[.changed_&]:content-['project-b/src/ignore-me.html']`,
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
fs.expectFileNotToContain('./project-a/dist/out.css', [
|
|
|
|
|
candidate`content-['project-a/src/ignore-me-file.html']`,
|
|
|
|
|
candidate`content-['project-a/src/ignore-me-folder/index.html']`,
|
|
|
|
|
candidate`content-['project-b/src/ignore-me.html']`,
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
// Creating new files that match the source patterns should be included.
|
|
|
|
|
await fs.create([
|
|
|
|
|
'project-a/src/new-file.html',
|
|
|
|
|
'project-a/src/new-folder/new-file.html',
|
|
|
|
|
'project-b/src/new-file.html',
|
|
|
|
|
'project-b/src/new-folder/new-file.html',
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
await fs.write(
|
|
|
|
|
'project-a/src/new-file.html',
|
|
|
|
|
html`<div class="[.created_&]:content-['project-a/src/new-file.html']"></div>`,
|
|
|
|
|
)
|
|
|
|
|
await fs.write(
|
|
|
|
|
'project-a/src/new-folder/new-file.html',
|
|
|
|
|
html`<div class="[.created_&]:content-['project-a/src/new-folder/new-file.html']"></div>`,
|
|
|
|
|
)
|
|
|
|
|
await fs.write(
|
|
|
|
|
'project-b/src/new-file.html',
|
|
|
|
|
html`<div class="[.created_&]:content-['project-b/src/new-file.html']"></div>`,
|
|
|
|
|
)
|
|
|
|
|
await fs.write(
|
|
|
|
|
'project-b/src/new-folder/new-file.html',
|
|
|
|
|
html`<div class="[.created_&]:content-['project-b/src/new-folder/new-file.html']"></div>`,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// If we don't wait writes will be coalesced into a "add" event which
|
|
|
|
|
// isn't picked up by postcss-cli.
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
|
|
|
|
|
|
|
|
await fs.expectFileToContain('./project-a/dist/out.css', [
|
|
|
|
|
candidate`[.created_&]:content-['project-a/src/new-file.html']`,
|
|
|
|
|
candidate`[.created_&]:content-['project-a/src/new-folder/new-file.html']`,
|
|
|
|
|
candidate`[.created_&]:content-['project-b/src/new-file.html']`,
|
|
|
|
|
candidate`[.created_&]:content-['project-b/src/new-folder/new-file.html']`,
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
fs.expectFileNotToContain('./project-a/dist/out.css', [
|
|
|
|
|
candidate`content-['project-a/src/ignore-me-file.html']`,
|
|
|
|
|
candidate`content-['project-a/src/ignore-me-folder/index.html']`,
|
|
|
|
|
candidate`content-['project-b/src/ignore-me.html']`,
|
|
|
|
|
])
|
|
|
|
|
},
|
|
|
|
|
)
|