2024-10-03 16:21:54 +02:00
|
|
|
import QuickLRU from '@alloc/quick-lru'
|
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
|
|
|
import {
|
|
|
|
|
compileAst,
|
|
|
|
|
env,
|
|
|
|
|
Features,
|
|
|
|
|
Instrumentation,
|
|
|
|
|
optimize as optimizeCss,
|
|
|
|
|
Polyfills,
|
|
|
|
|
} from '@tailwindcss/node'
|
2024-09-02 12:03:16 -04:00
|
|
|
import { clearRequireCache } from '@tailwindcss/node/require-cache'
|
Improve Oxide scanner API (#14187)
This PR updates the API for interacting with the Oxide API. Until now,
we used the name `scanDir(…)` which is fine, but we do way more work
right now.
We now have features such as:
1. Auto source detection (can be turned off, e.g.: `@tailwindcss/vite`
doesn't need it)
2. Scan based on `@source`s found in CSS files
3. Do "incremental" rebuilds (which means that the `scanDir(…)` result
was stateful).
To solve these issues, this PR introduces a new `Scanner` class where
you can pass in the `detectSources` and `sources` options. E.g.:
```ts
let scanner = new Scanner({
// Optional, omitting `detectSources` field disables automatic source detection
detectSources: { base: __dirname },
// List of glob entries to scan. These come from `@source` directives in CSS.
sources: [
{ base: __dirname, pattern: "src/**/*.css" },
// …
],
});
```
The scanner object has the following API:
```ts
export interface ChangedContent {
/** File path to the changed file */
file?: string
/** Contents of the changed file */
content?: string
/** File extension */
extension: string
}
export interface DetectSources {
/** Base path to start scanning from */
base: string
}
export interface GlobEntry {
/** Base path of the glob */
base: string
/** Glob pattern */
pattern: string
}
export interface ScannerOptions {
/** Automatically detect sources in the base path */
detectSources?: DetectSources
/** Glob sources */
sources?: Array<GlobEntry>
}
export declare class Scanner {
constructor(opts: ScannerOptions)
scan(): Array<string>
scanFiles(input: Array<ChangedContent>): Array<string>
get files(): Array<string>
get globs(): Array<GlobEntry>
}
```
The `scanFiles(…)` method is used for incremental rebuilds. It takes the
`ChangedContent` array for all the new/changes files. It returns whether
we scanned any new candidates or not.
Note that the `scanner` object is stateful, this means that we don't
have to track candidates in a `Set` anymore. We can just call
`getCandidates()` when we need it.
This PR also removed some unused code that we had in the `scanDir(…)`
function to allow for sequential or parallel `IO`, and sequential or
parallel `Parsing`. We only used the same `IO` and `Parsing` strategies
for all files, so I just got rid of it.
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
2024-08-16 15:05:42 +02:00
|
|
|
import { Scanner } from '@tailwindcss/oxide'
|
2024-11-04 17:52:11 +01:00
|
|
|
import fs from 'node:fs'
|
2024-12-11 15:27:20 +01:00
|
|
|
import path, { relative } from 'node:path'
|
2025-09-18 12:03:19 -04:00
|
|
|
import type { AcceptedPlugin, PluginCreator, Postcss, Root } from 'postcss'
|
2024-12-04 15:43:59 +01:00
|
|
|
import { toCss, type AstNode } from '../../tailwindcss/src/ast'
|
|
|
|
|
import { cssAstToPostCssAst, postCssAstToCssAst } from './ast'
|
Resolve `@import` in core (#14446)
This PR brings `@import` resolution into Tailwind CSS core. This means
that our clients (PostCSS, Vite, and CLI) no longer need to depend on
`postcss` and `postcss-import` to resolve `@import`. Furthermore this
simplifies the handling of relative paths for `@source`, `@plugin`, or
`@config` in transitive CSS files (where the relative root should always
be relative to the CSS file that contains the directive). This PR also
fixes a plugin resolution bug where non-relative imports (e.g. directly
importing node modules like `@plugin '@tailwindcss/typography';`) would
not work in CSS files that are based in a different npm package.
### Resolving `@import`
The core of the `@import` resolution is inside
`packages/tailwindcss/src/at-import.ts`. There, to keep things
performant, we do a two-step process to resolve imports. Imagine the
following input CSS file:
```css
@import "tailwindcss/theme.css";
@import "tailwindcss/utilities.css";
```
Since our AST walks are synchronous, we will do a first traversal where
we start a loading request for each `@import` directive. Once all loads
are started, we will await the promise and do a second walk where we
actually replace the AST nodes with their resolved stylesheets. All of
this is recursive, so that `@import`-ed files can again `@import` other
files.
The core `@import` resolver also includes extensive test cases for
[various combinations of media query and supports conditionals as well
als layered
imports](https://developer.mozilla.org/en-US/docs/Web/CSS/@import).
When the same file is imported multiple times, the AST nodes are
duplicated but duplicate I/O is avoided on a per-file basis, so this
will only load one file, but include the `@theme` rules twice:
```css
@import "tailwindcss/theme.css";
@import "tailwindcss/theme.css";
```
### Adding a new `context` node to the AST
One limitation we had when working with the `postcss-import` plugin was
the need to do an additional traversal to rewrite relative `@source`,
`@plugin`, and `@config` directives. This was needed because we want
these paths to be relative to the CSS file that defines the directive
but when flattening a CSS file, this information is no longer part of
the stringifed CSS representation. We worked around this by rewriting
the content of these directives to be relative to the input CSS file,
which resulted in added complexity and caused a lot of issues with
Windows paths in the beginning.
Now that we are doing the `@import` resolution in core, we can use a
different data structure to persist this information. This PR adds a new
`context` node so that we can store arbitrary context like this inside
the Ast directly. This allows us to share information with the sub tree
_while doing the Ast walk_.
Here's an example of how the new `context` node can be used to share
information with subtrees:
```ts
const ast = [
rule('.foo', [decl('color', 'red')]),
context({ value: 'a' }, [
rule('.bar', [
decl('color', 'blue'),
context({ value: 'b' }, [
rule('.baz', [decl('color', 'green')]),
]),
]),
]),
]
walk(ast, (node, { context }) => {
if (node.kind !== 'declaration') return
switch (node.value) {
case 'red': assert(context.value === undefined)
case 'blue': assert(context.value === 'a')
case 'green': assert(context.value === 'b')
}
})
```
In core, we use this new Ast node specifically to persist the `base`
path of the current CSS file. We put the input CSS file `base` at the
root of the Ast and then overwrite the `base` on every `@import`
substitution.
### Removing the dependency on `postcss-import`
Now that we support `@import` resolution in core, our clients no longer
need a dependency on `postcss-import`. Furthermore, most dependencies
also don't need to know about `postcss` at all anymore (except the
PostCSS client, of course!).
This also means that our workaround for rewriting `@source`, the
`postcss-fix-relative-paths` plugin, can now go away as a shared
dependency between all of our clients. Note that we still have it for
the PostCSS plugin only, where it's possible that users already have
`postcss-import` running _before_ the `@tailwindcss/postcss` plugin.
Here's an example of the changes to the dependencies for our Vite client
✨ :
<img width="854" alt="Screenshot 2024-09-19 at 16 59 45"
src="https://github.com/user-attachments/assets/ae1f9d5f-d93a-4de9-9244-61af3aff1237">
### Performance
Since our Vite and CLI clients now no longer need to use `postcss` at
all, we have also measured a significant improvement to the initial
build times. For a small test setup that contains only a hand full of
files (nothing super-complex), we measured an improvement in the
**3.5x** range:
<img width="1334" alt="Screenshot 2024-09-19 at 14 52 49"
src="https://github.com/user-attachments/assets/06071fb0-7f2a-4de6-8ec8-f202d2cc78e5">
The code for this is in the commit history if you want to reproduce the
results. The test was based on the Vite client.
### Caveats
One thing to note is that we previously relied on finding specific
symbols in the input CSS to _bail out of Tailwind processing
completely_. E.g. if a file does not contain a `@tailwind` or `@apply`
directive, it can never be a Tailwind file.
Since we no longer have a string representation of the flattened CSS
file, we can no longer do this check. However, the current
implementation was already inconsistent with differences on the allowed
symbol list between our clients. Ideally, Tailwind CSS should figure out
wether a CSS file is a Tailwind CSS file. This, however, is left as an
improvement for a future API since it goes hand-in-hand with our planned
API changes for the core `tailwindcss` package.
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
2024-09-23 17:05:55 +02:00
|
|
|
import fixRelativePathsPlugin from './postcss-fix-relative-paths'
|
2024-03-05 14:23:26 +01:00
|
|
|
|
2024-12-11 15:27:20 +01:00
|
|
|
const DEBUG = env.DEBUG
|
|
|
|
|
|
2024-10-03 16:21:54 +02:00
|
|
|
interface CacheEntry {
|
|
|
|
|
mtimes: Map<string, number>
|
2025-04-03 17:07:38 +02:00
|
|
|
compiler: null | ReturnType<typeof compileAst>
|
2024-10-03 16:21:54 +02:00
|
|
|
scanner: null | Scanner
|
2024-12-04 15:43:59 +01:00
|
|
|
tailwindCssAst: AstNode[]
|
2025-09-18 12:03:19 -04:00
|
|
|
cachedPostCssAst: Root
|
|
|
|
|
optimizedPostCssAst: Root
|
2024-10-03 16:21:54 +02:00
|
|
|
fullRebuildPaths: string[]
|
|
|
|
|
}
|
2024-12-11 15:27:20 +01:00
|
|
|
const cache = new QuickLRU<string, CacheEntry>({ maxSize: 50 })
|
2024-10-03 16:21:54 +02:00
|
|
|
|
2025-09-18 12:03:19 -04:00
|
|
|
function getContextFromCache(postcss: Postcss, inputFile: string, opts: PluginOptions): CacheEntry {
|
2024-12-04 15:43:59 +01:00
|
|
|
let key = `${inputFile}:${opts.base ?? ''}:${JSON.stringify(opts.optimize)}`
|
2024-10-03 16:21:54 +02:00
|
|
|
if (cache.has(key)) return cache.get(key)!
|
|
|
|
|
let entry = {
|
|
|
|
|
mtimes: new Map<string, number>(),
|
|
|
|
|
compiler: null,
|
|
|
|
|
scanner: null,
|
2024-12-04 15:43:59 +01:00
|
|
|
|
|
|
|
|
tailwindCssAst: [],
|
|
|
|
|
cachedPostCssAst: postcss.root(),
|
|
|
|
|
optimizedPostCssAst: postcss.root(),
|
|
|
|
|
|
2024-10-03 16:21:54 +02:00
|
|
|
fullRebuildPaths: [] as string[],
|
2024-03-11 20:01:28 +01:00
|
|
|
}
|
2024-10-03 16:21:54 +02:00
|
|
|
cache.set(key, entry)
|
|
|
|
|
return entry
|
2024-03-11 20:01:28 +01:00
|
|
|
}
|
|
|
|
|
|
2024-08-26 15:54:07 +02:00
|
|
|
export type PluginOptions = {
|
2025-07-30 10:35:10 -04:00
|
|
|
/**
|
|
|
|
|
* The base directory to scan for class candidates.
|
|
|
|
|
*
|
|
|
|
|
* Defaults to the current working directory.
|
|
|
|
|
*/
|
2024-03-05 14:23:26 +01:00
|
|
|
base?: string
|
2024-03-08 18:36:07 +01:00
|
|
|
|
2025-07-30 10:35:10 -04:00
|
|
|
/**
|
|
|
|
|
* Optimize and minify the output CSS.
|
|
|
|
|
*/
|
2024-03-08 18:36:07 +01:00
|
|
|
optimize?: boolean | { minify?: boolean }
|
2025-07-30 10:35:10 -04:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Enable or disable asset URL rewriting.
|
|
|
|
|
*
|
|
|
|
|
* Defaults to `true`.
|
|
|
|
|
*/
|
|
|
|
|
transformAssetUrls?: boolean
|
2024-03-05 14:23:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
|
|
|
|
|
let base = opts.base ?? process.cwd()
|
2024-03-08 18:36:07 +01:00
|
|
|
let optimize = opts.optimize ?? process.env.NODE_ENV === 'production'
|
2025-07-30 10:35:10 -04:00
|
|
|
let shouldRewriteUrls = opts.transformAssetUrls ?? true
|
2024-03-05 14:23:26 +01:00
|
|
|
|
|
|
|
|
return {
|
2024-03-11 20:01:28 +01:00
|
|
|
postcssPlugin: '@tailwindcss/postcss',
|
2024-03-05 14:23:26 +01:00
|
|
|
plugins: [
|
2024-10-03 16:21:54 +02:00
|
|
|
// We need to handle the case where `postcss-import` might have run before
|
|
|
|
|
// the Tailwind CSS plugin is run. In this case, we need to manually fix
|
|
|
|
|
// relative paths before processing it in core.
|
2024-08-07 16:38:44 +02:00
|
|
|
fixRelativePathsPlugin(),
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
postcssPlugin: 'tailwindcss',
|
2025-09-18 12:03:19 -04:00
|
|
|
async Once(root, { result, postcss }) {
|
2024-12-11 15:27:20 +01:00
|
|
|
using I = new Instrumentation()
|
|
|
|
|
|
|
|
|
|
let inputFile = result.opts.from ?? ''
|
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
|
|
|
let isCSSModuleFile = inputFile.endsWith('.module.css')
|
2024-12-11 15:27:20 +01:00
|
|
|
|
|
|
|
|
DEBUG && I.start(`[@tailwindcss/postcss] ${relative(base, inputFile)}`)
|
2024-12-09 11:55:33 +01:00
|
|
|
|
|
|
|
|
// Bail out early if this is guaranteed to be a non-Tailwind CSS file.
|
|
|
|
|
{
|
2024-12-11 15:27:20 +01:00
|
|
|
DEBUG && I.start('Quick bail check')
|
2024-12-09 11:55:33 +01:00
|
|
|
let canBail = true
|
|
|
|
|
root.walkAtRules((node) => {
|
|
|
|
|
if (
|
|
|
|
|
node.name === 'import' ||
|
2025-01-30 16:29:08 +01:00
|
|
|
node.name === 'reference' ||
|
2024-12-09 11:55:33 +01:00
|
|
|
node.name === 'theme' ||
|
2025-01-30 16:29:08 +01:00
|
|
|
node.name === 'variant' ||
|
2024-12-09 11:55:33 +01:00
|
|
|
node.name === 'config' ||
|
|
|
|
|
node.name === 'plugin' ||
|
2025-04-03 17:07:38 +02:00
|
|
|
node.name === 'apply' ||
|
|
|
|
|
node.name === 'tailwind'
|
2024-12-09 11:55:33 +01:00
|
|
|
) {
|
|
|
|
|
canBail = false
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
})
|
2024-12-11 15:27:20 +01:00
|
|
|
if (canBail) return
|
|
|
|
|
DEBUG && I.end('Quick bail check')
|
2024-12-09 11:55:33 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-18 12:03:19 -04:00
|
|
|
let context = getContextFromCache(postcss, inputFile, opts)
|
2024-08-07 16:38:44 +02:00
|
|
|
let inputBasePath = path.dirname(path.resolve(inputFile))
|
|
|
|
|
|
2025-04-25 12:02:10 +02:00
|
|
|
// Whether this is the first build or not, if it is, then we can
|
|
|
|
|
// optimize the build by not creating the compiler until we need it.
|
|
|
|
|
let isInitialBuild = context.compiler === null
|
|
|
|
|
|
2024-09-02 12:03:16 -04:00
|
|
|
async function createCompiler() {
|
2024-12-11 15:27:20 +01:00
|
|
|
DEBUG && I.start('Setup compiler')
|
Improve performance of `@tailwindcss/postcss` and `@tailwindcss/vite` (#15226)
This PR improves the performance of the `@tailwindcss/postcss` and
`@tailwindcss/vite` implementations.
The issue is that in some scenarios, if you have multiple `.css` files,
then all of the CSS files are ran through the Tailwind CSS compiler. The
issue with this is that in a lot of cases, the CSS files aren't even
related to Tailwind CSS at all.
E.g.: in a Next.js project, if you use the `next/font/local` tool, then
every font you used will be in a separate CSS file. This means that we
run Tailwind CSS in all these files as well.
That said, running Tailwind CSS on these files isn't the end of the
world because we still need to handle `@import` in case `@tailwind
utilities` is being used. However, we also run the auto source detection
logic for every CSS file in the system. This part is bad.
To solve this, this PR introduces an internal `features` to collect what
CSS features are used throughout the system (`@import`, `@plugin`,
`@apply`, `@tailwind utilities`, etc…)
The `@tailwindcss/postcss` and `@tailwindcss/vite` plugin can use that
information to decide if they can take some shortcuts or not.
---
Overall, this means that we don't run the slow parts of Tailwind CSS if
we don't need to.
---------
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
2024-11-29 16:59:29 +01:00
|
|
|
if (context.fullRebuildPaths.length > 0 && !isInitialBuild) {
|
|
|
|
|
clearRequireCache(context.fullRebuildPaths)
|
|
|
|
|
}
|
2024-09-02 12:03:16 -04:00
|
|
|
|
|
|
|
|
context.fullRebuildPaths = []
|
|
|
|
|
|
2024-12-11 15:27:20 +01:00
|
|
|
DEBUG && I.start('PostCSS AST -> Tailwind CSS AST')
|
|
|
|
|
let ast = postCssAstToCssAst(root)
|
|
|
|
|
DEBUG && I.end('PostCSS AST -> Tailwind CSS AST')
|
|
|
|
|
|
|
|
|
|
DEBUG && I.start('Create compiler')
|
|
|
|
|
let compiler = await compileAst(ast, {
|
Add support for source maps (#17775)
Closes #13694
Closes #13591
# Source Maps Support for Tailwind CSS
This PR adds support for source maps to Tailwind CSS v4 allowing us to
track where styles come from whether that be user CSS, imported
stylesheets, or generated utilities. This will improve debuggability in
browser dev tools and gives us a good foundation for producing better
error messages. I'll go over the details on how end users can enable
source maps, any limitations in our implementation, changes to the
internal `compile(…)` API, and some details and reasoning around the
implementation we chose.
## Usage
### CLI
Source maps can be enabled in the CLI by using the command line argument
`--map` which will generate an inline source map comment at the bottom
of your CSS. A separate file may be generated by passing a file name to
`--map`:
```bash
# Generates an inline source map
npx tailwindcss -i input.css -o output.css --map
# Generates a separate source map file
npx tailwindcss -i input.css -o output.css --map output.css.map
```
### PostCSS
Source maps are supported when using Tailwind as a PostCSS plugin *in
development mode only*. They may or may not be enabled by default
depending on your build tool. If they are not you may be able to
configure them within your PostCSS config:
```jsonc
// package.json
{
// …
"postcss": {
"map": { "inline": true },
"plugins": {
"@tailwindcss/postcss": {},
},
}
}
```
### Vite
Source maps are supported when using the Tailwind CSS Vite plugin in
*development mode only* by enabling the `css.devSourcemap` setting:
```js
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [tailwindcss()],
css: {
devSourcemap: true,
},
})
```
Now when a CSS file is requested by the browser it'll have an inline
source map comment that the browser can use.
## Limitations
- Production build source maps are currently disabled due to a bug in
Lightning CSS. See
https://github.com/parcel-bundler/lightningcss/pull/971 for more
details.
- In Vite, minified CSS build source maps are not supported at all. See
https://github.com/vitejs/vite/issues/2830 for more details.
- In PostCSS, minified CSS source maps are not supported. This is due to
the complexity required around re-associating every AST node with a
location in the generated, optimized CSS. This complexity would also
have a non-trivial performance impact.
## Testing
Here's how to test the source map functionality in different
environments:
### Testing the CLI
1. Setup typical project that the CLI can use and with sources to scan.
```css
@import "tailwindcss";
@utilty my-custom-utility {
color: red;
}
/* to test `@apply` */
.card {
@apply bg-white text-center shadow-md;
}
```
2. Build with source maps:
```bash
bun /path/to/tailwindcss/packages/@tailwindcss-cli/src/index.ts --input input.css -o output.css --map
```
3. Open Chrome DevTools, inspect an element with utility classes, and
you should see rules pointing to `input.css` or
`node_modules/tailwindcss/index.css`
### Testing with Vite
Testing in Vite will require building and installing necessary files
under `dist/*.tgz`.
1. Create a Vite project and enable source maps in `vite.config.js`:
```js
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [tailwindcss()],
css: {
// This line is required for them to work
devSourcemap: true,
},
})
```
2. Add a component that uses Tailwind classes and custom CSS:
```jsx
// ./src/app.jsx
export default function App() {
return (
<div className="bg-blue-500 my-custom-class">
Hello World
</div>
)
}
```
```css
/* ./src/styles.css */
@import "tailwindcss";
@utilty my-custom-utility {
color: red;
}
/* to test `@apply` */
.card {
@apply bg-white text-center shadow-md;
}
```
3. Run `npm run dev`, open DevTools, and inspect elements to verify
source mapping works for both utility classes and custom CSS.
### Testing with PostCSS CLI
1. Create a test file and update your PostCSS config:
```css
/* input.css */
@import "tailwindcss";
@layer components {
.card {
@apply p-6 rounded-lg shadow-lg;
}
}
```
```jsonc
// package.json
{
// …
"postcss": {
"map": {
"inline": true
},
"plugins": {
"/path/to/tailwindcss/packages/packages/@tailwindcss-postcss/src/index.ts": {}
}
}
}
```
2. Run PostCSS through Bun:
```bash
bunx --bun postcss ./src/index.css -o out.css
```
3. Inspect the output CSS - it should include an inline source map
comment at the bottom.
### Testing with PostCSS + Next.js
Testing in Next.js will require building and installing necessary files
under `dist/*.tgz`. However, I've not been able to get CSS source maps
to work in Next.js without this hack:
```js
const nextConfig: NextConfig = {
// next.js overwrites config.devtool so we prevent it from doing so
// please don't actually do this…
webpack: (config) =>
Object.defineProperty(config, "devtool", {
get: () => "inline-source-map",
set: () => {},
}),
};
```
This is definitely not supported and also doesn't work with turbopack.
This can be used to test them temporarily but I suspect that they just
don't work there.
### Manual source map analysis
You can analyze source maps using Evan Wallace's [Source Map
Visualization](https://evanw.github.io/source-map-visualization/) tool
which will help to verify the accuracy and quality of source maps. This
is what I used extensively while developing this implementation.
It'll help verify that custom, user CSS maps back to itself in the
input, that generated utilities all map back to `@tailwind utilities;`,
that source locations from imported files are also handled correctly,
etc… It also highlights the ranges of stuff so it's easy to see if there
are off-by-one errors.
It's easiest to use inline source maps with this tool because you can
take the CSS file and drop it on the page and it'll analyze it while
showing the file content.
If you're using Vite you'll want to access the CSS file with `?direct`
at the end so you don't get a JS module back.
## Implementation
The source map implementation follows the ECMA-426 specification and
includes several key components to aid in that goal:
### Source Location Tracking
Each emittable AST node in the compilation pipeline tracks two types of
source locations:
- `src`: Original source location - [source file, start offset, end
offset]
- `dst`: Generated source location - [output file, start offset, end
offset]
This dual tracking allows us to maintain mappings between the original
source and generated output for things like user CSS, generated
utilities, uses of `@apply`, and tracking theme variables.
It is important to note that source locations for nodes _never overlap_
within a file which helps simplify source map generation. As such each
type of node tracks a specific piece of itself rather than its entire
"block":
| Node | What a `SourceLocation` represents |
| ----------- |
---------------------------------------------------------------- |
| Style Rule | The selector |
| At Rule | Rule name and params, includes the `@` |
| Declaration | Property name and value, excludes the semicolon |
| Comment | The entire comment, includes the start `/*` and end `*/`
markers |
### Windows line endings when parsing CSS
Because our AST tracks nodes through offsets we must ensure that any
mutations to the file do *not* change the lenth of the string. We were
previously replacing `\r\n` with `\n` (see [filter code
points](https://drafts.csswg.org/css-syntax/#css-filter-code-points)
from the spec) — which changes the length of the string and all offsets
may end up incorrect. The CSS parser was updated to handle the CRLF
token directly by skipping over the `\r` and letting remaining code
handle `\n` as it did previously. Some additional tweaks were required
when "peeking" the input but those changes were fairly small.
### Tracking of imports
Source maps need paths to the actual imported stylesheets but the
resolve step for stylesheets happens inside the call to `loadStylesheet`
which make the file path unavailable to us. Because of this the
`loadStylesheet` API was augmented such that it has to return a `path`
property that we can then use to identify imported sources. I've also
made the same change to the `loadModule` API for consistency but nothing
currently uses this property.
The `path` property likely makes `base` redundant but elminating that
(if we even want to) is a future task.
### Optimizing the AST
Our optimization pass may intoduce some nodes, for example, fallbacks we
create for `@property`. These nodes are linked back to `@tailwind
utilities` as ultimately that is what is responsible for creating them.
### Line Offset Tables
A key component to our source map generation is the line offset table,
which was inspired by some ESBuild internals. It stores a sorted list of
offsets for the start of each line allowing us to translate offsets to
line/column `Position`s in `O(log N)` time and from `Position`s to
offsets in `O(1)` time. Creation of the table takes `O(N)` time.
This means that we can store code point offsets for source locations and
not have to worry about computing or tracking line/column numbers during
parsing and serialization. Only when a source map is generated do these
offsets need to be computed. This ensures the performance penalty when
not using source maps is minimal.
### Source Map Generation
The source map returned by `buildSourceMap()` is designed to follow the
[ECMA-426 spec](https://tc39.es/ecma426). Because that spec is not
completely finalized we consider the result of `buildSourceMap()` to be
internal API that may change as the spec chamges.
The produces source map is a "decoded" map such that all sources and
mappings are in an object graph. A library like `source-map-js` must be
used to convert this to an encoded source map of the right version where
mappings are encoded with base 64 VLQs.
Any specific integration (Vite, PostCSS, etc…) can then use
`toSourceMap()` from `@tailwindcss/node` to convert from the internal
source map to an spec-compliant encoded source map that can be
understood by other tools.
### Handling minification in Lightning
Since we use Lightning CSS for optimization, and it takes in an input
map, we generate an encoded source map that we then pass to lightning.
The output source map *from lighting itself* is then passed back in
during the second optimization pass. The final map is then passed from
lightning to the CLI (but not Vite or PostCSS — see the limitations
section for details).
In some cases we have to "fix up" the output CSS. When this happens we
use `magic-string` to do the replacement in a way that is trackable and
`@amppproject/remapping` to map that change back onto the original
source map. Once the need for these fix ups disappear these dependencies
can go away.
Notes:
- The accuracy of source maps run though lightning is reduced as it only
tracks on a per-rule level. This is sufficient enough for browser dev
tools so should be fine.
- Source maps during optimization do not function properly at this time
because of a bug in Lightning CSS regarding license comments. Once this
bug is fixed they will start working as expected.
### How source locations flow through the system
1. During initial CSS parsing, source locations are preserved.
2. During parsing these source locations are also mapped to the
destinations which supports an optimization for when no utilities are
generated.
3. Throughout the compilation process, transformations maintain source
location data
4. Generated utilities are explicitly pointed to `@tailwind utilities`
unless generated by `@apply`.
5. When optimization is enabled, source maps are remapped through
lightningcss
6. Final source maps are written in the requested format (inline or
separate file)
2025-05-08 16:29:49 -04:00
|
|
|
from: result.opts.from,
|
2024-09-02 12:03:16 -04:00
|
|
|
base: inputBasePath,
|
2025-07-30 10:35:10 -04:00
|
|
|
shouldRewriteUrls,
|
2025-04-25 12:02:10 +02:00
|
|
|
onDependency: (path) => context.fullRebuildPaths.push(path),
|
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
|
|
|
// In CSS Module files, we have to disable the `@property` polyfill since these will
|
|
|
|
|
// emit global `*` rules which are considered to be non-pure and will cause builds
|
|
|
|
|
// to fail.
|
|
|
|
|
polyfills: isCSSModuleFile ? Polyfills.All ^ Polyfills.AtProperty : Polyfills.All,
|
2024-08-07 16:38:44 +02:00
|
|
|
})
|
2024-12-11 15:27:20 +01:00
|
|
|
DEBUG && I.end('Create compiler')
|
2024-09-30 16:39:21 +02:00
|
|
|
|
2024-12-11 15:27:20 +01:00
|
|
|
DEBUG && I.end('Setup compiler')
|
2024-09-30 16:39:21 +02:00
|
|
|
return compiler
|
2024-08-07 16:38:44 +02:00
|
|
|
}
|
2024-03-05 14:23:26 +01:00
|
|
|
|
2025-04-25 12:02:10 +02:00
|
|
|
try {
|
|
|
|
|
// Setup the compiler if it doesn't exist yet. This way we can
|
|
|
|
|
// guarantee a `build()` function is available.
|
|
|
|
|
context.compiler ??= createCompiler()
|
2024-10-03 16:21:54 +02:00
|
|
|
|
2025-04-25 12:02:10 +02:00
|
|
|
if ((await context.compiler).features === Features.None) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2024-08-07 16:38:44 +02:00
|
|
|
|
2025-04-25 12:02:10 +02:00
|
|
|
let rebuildStrategy: 'full' | 'incremental' = 'incremental'
|
Improve performance of `@tailwindcss/postcss` and `@tailwindcss/vite` (#15226)
This PR improves the performance of the `@tailwindcss/postcss` and
`@tailwindcss/vite` implementations.
The issue is that in some scenarios, if you have multiple `.css` files,
then all of the CSS files are ran through the Tailwind CSS compiler. The
issue with this is that in a lot of cases, the CSS files aren't even
related to Tailwind CSS at all.
E.g.: in a Next.js project, if you use the `next/font/local` tool, then
every font you used will be in a separate CSS file. This means that we
run Tailwind CSS in all these files as well.
That said, running Tailwind CSS on these files isn't the end of the
world because we still need to handle `@import` in case `@tailwind
utilities` is being used. However, we also run the auto source detection
logic for every CSS file in the system. This part is bad.
To solve this, this PR introduces an internal `features` to collect what
CSS features are used throughout the system (`@import`, `@plugin`,
`@apply`, `@tailwind utilities`, etc…)
The `@tailwindcss/postcss` and `@tailwindcss/vite` plugin can use that
information to decide if they can take some shortcuts or not.
---
Overall, this means that we don't run the slow parts of Tailwind CSS if
we don't need to.
---------
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
2024-11-29 16:59:29 +01:00
|
|
|
|
2025-04-25 12:02:10 +02:00
|
|
|
// Track file modification times to CSS files
|
|
|
|
|
DEBUG && I.start('Register full rebuild paths')
|
|
|
|
|
{
|
|
|
|
|
for (let file of context.fullRebuildPaths) {
|
|
|
|
|
result.messages.push({
|
|
|
|
|
type: 'dependency',
|
|
|
|
|
plugin: '@tailwindcss/postcss',
|
|
|
|
|
file: path.resolve(file),
|
|
|
|
|
parent: result.opts.from,
|
|
|
|
|
})
|
|
|
|
|
}
|
2024-08-07 16:38:44 +02:00
|
|
|
|
2025-04-25 12:02:10 +02:00
|
|
|
let files = result.messages.flatMap((message) => {
|
|
|
|
|
if (message.type !== 'dependency') return []
|
|
|
|
|
return message.file
|
2024-09-02 12:03:16 -04:00
|
|
|
})
|
2025-04-25 12:02:10 +02:00
|
|
|
files.push(inputFile)
|
|
|
|
|
|
|
|
|
|
for (let file of files) {
|
|
|
|
|
let changedTime = fs.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? null
|
|
|
|
|
if (changedTime === null) {
|
|
|
|
|
if (file === inputFile) {
|
|
|
|
|
rebuildStrategy = 'full'
|
|
|
|
|
}
|
|
|
|
|
continue
|
2024-08-07 16:38:44 +02:00
|
|
|
}
|
2024-03-05 14:23:26 +01:00
|
|
|
|
2025-04-25 12:02:10 +02:00
|
|
|
let prevTime = context.mtimes.get(file)
|
|
|
|
|
if (prevTime === changedTime) continue
|
2024-03-05 14:23:26 +01:00
|
|
|
|
2025-04-25 12:02:10 +02:00
|
|
|
rebuildStrategy = 'full'
|
|
|
|
|
context.mtimes.set(file, changedTime)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
DEBUG && I.end('Register full rebuild paths')
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
rebuildStrategy === 'full' &&
|
|
|
|
|
// We can re-use the compiler if it was created during the
|
|
|
|
|
// initial build. If it wasn't, we need to create a new one.
|
|
|
|
|
!isInitialBuild
|
|
|
|
|
) {
|
|
|
|
|
context.compiler = createCompiler()
|
2024-08-07 16:38:44 +02:00
|
|
|
}
|
2024-10-03 16:21:54 +02:00
|
|
|
|
2025-04-25 12:02:10 +02:00
|
|
|
let compiler = await context.compiler
|
2025-04-03 17:07:38 +02:00
|
|
|
|
2025-04-25 12:02:10 +02:00
|
|
|
if (context.scanner === null || rebuildStrategy === 'full') {
|
|
|
|
|
DEBUG && I.start('Setup scanner')
|
|
|
|
|
let sources = (() => {
|
|
|
|
|
// Disable auto source detection
|
|
|
|
|
if (compiler.root === 'none') {
|
|
|
|
|
return []
|
|
|
|
|
}
|
Auto source detection improvements (#14820)
This PR introduces a new `source(…)` argument and improves on the
existing `@source`. The goal of this PR is to make the automatic source
detection configurable, let's dig in.
By default, we will perform automatic source detection starting at the
current working directory. Auto source detection will find plain text
files (no binaries, images, ...) and will ignore git-ignored files.
If you want to start from a different directory, you can use the new
`source(…)` next to the `@import "tailwindcss/utilities"
layer(utilities) source(…)`.
E.g.:
```css
/* ./src/styles/index.css */
@import 'tailwindcss/utilities' layer(utilities) source('../../');
```
Most people won't split their source files, and will just use the simple
`@import "tailwindcss";`, because of this reason, you can use
`source(…)` on the import as well:
E.g.:
```css
/* ./src/styles/index.css */
@import 'tailwindcss' source('../../');
```
Sometimes, you want to rely on auto source detection, but also want to
look in another directory for source files. In this case, yuo can use
the `@source` directive:
```css
/* ./src/index.css */
@import 'tailwindcss';
/* Look for `blade.php` files in `../resources/views` */
@source '../resources/views/**/*.blade.php';
```
However, you don't need to specify the extension, instead you can just
point the directory and all the same automatic source detection rules
will apply.
```css
/* ./src/index.css */
@import 'tailwindcss';
@source '../resources/views';
```
If, for whatever reason, you want to disable the default source
detection feature entirely, and only want to rely on very specific glob
patterns you define, then you can disable it via `source(none)`.
```css
/* Completely disable the default auto source detection */
@import 'tailwindcss' source(none);
/* Only look at .blade.php files, nothing else */
@source "../resources/views/**/*.blade.php";
```
Note: even with `source(none)`, if your `@source` points to a directory,
then auto source detection will still be performed in that directory. If
you don't want that, then you can simply add explicit files in the globs
as seen in the previous example.
```css
/* Completely disable the default auto source detection */
@import 'tailwindcss' source(none);
/* Run auto source detection in `../resources/views` */
@source "../resources/views";
```
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
2024-10-29 21:33:34 +01:00
|
|
|
|
2025-04-25 12:02:10 +02:00
|
|
|
// No root specified, use the base directory
|
|
|
|
|
if (compiler.root === null) {
|
|
|
|
|
return [{ base, pattern: '**/*', negated: false }]
|
|
|
|
|
}
|
Auto source detection improvements (#14820)
This PR introduces a new `source(…)` argument and improves on the
existing `@source`. The goal of this PR is to make the automatic source
detection configurable, let's dig in.
By default, we will perform automatic source detection starting at the
current working directory. Auto source detection will find plain text
files (no binaries, images, ...) and will ignore git-ignored files.
If you want to start from a different directory, you can use the new
`source(…)` next to the `@import "tailwindcss/utilities"
layer(utilities) source(…)`.
E.g.:
```css
/* ./src/styles/index.css */
@import 'tailwindcss/utilities' layer(utilities) source('../../');
```
Most people won't split their source files, and will just use the simple
`@import "tailwindcss";`, because of this reason, you can use
`source(…)` on the import as well:
E.g.:
```css
/* ./src/styles/index.css */
@import 'tailwindcss' source('../../');
```
Sometimes, you want to rely on auto source detection, but also want to
look in another directory for source files. In this case, yuo can use
the `@source` directive:
```css
/* ./src/index.css */
@import 'tailwindcss';
/* Look for `blade.php` files in `../resources/views` */
@source '../resources/views/**/*.blade.php';
```
However, you don't need to specify the extension, instead you can just
point the directory and all the same automatic source detection rules
will apply.
```css
/* ./src/index.css */
@import 'tailwindcss';
@source '../resources/views';
```
If, for whatever reason, you want to disable the default source
detection feature entirely, and only want to rely on very specific glob
patterns you define, then you can disable it via `source(none)`.
```css
/* Completely disable the default auto source detection */
@import 'tailwindcss' source(none);
/* Only look at .blade.php files, nothing else */
@source "../resources/views/**/*.blade.php";
```
Note: even with `source(none)`, if your `@source` points to a directory,
then auto source detection will still be performed in that directory. If
you don't want that, then you can simply add explicit files in the globs
as seen in the previous example.
```css
/* Completely disable the default auto source detection */
@import 'tailwindcss' source(none);
/* Run auto source detection in `../resources/views` */
@source "../resources/views";
```
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
2024-10-29 21:33:34 +01:00
|
|
|
|
2025-04-25 12:02:10 +02:00
|
|
|
// Use the specified root
|
|
|
|
|
return [{ ...compiler.root, negated: false }]
|
|
|
|
|
})().concat(compiler.sources)
|
2024-08-07 16:38:44 +02:00
|
|
|
|
2025-04-25 12:02:10 +02:00
|
|
|
// Look for candidates used to generate the CSS
|
|
|
|
|
context.scanner = new Scanner({ sources })
|
|
|
|
|
DEBUG && I.end('Setup scanner')
|
Auto source detection improvements (#14820)
This PR introduces a new `source(…)` argument and improves on the
existing `@source`. The goal of this PR is to make the automatic source
detection configurable, let's dig in.
By default, we will perform automatic source detection starting at the
current working directory. Auto source detection will find plain text
files (no binaries, images, ...) and will ignore git-ignored files.
If you want to start from a different directory, you can use the new
`source(…)` next to the `@import "tailwindcss/utilities"
layer(utilities) source(…)`.
E.g.:
```css
/* ./src/styles/index.css */
@import 'tailwindcss/utilities' layer(utilities) source('../../');
```
Most people won't split their source files, and will just use the simple
`@import "tailwindcss";`, because of this reason, you can use
`source(…)` on the import as well:
E.g.:
```css
/* ./src/styles/index.css */
@import 'tailwindcss' source('../../');
```
Sometimes, you want to rely on auto source detection, but also want to
look in another directory for source files. In this case, yuo can use
the `@source` directive:
```css
/* ./src/index.css */
@import 'tailwindcss';
/* Look for `blade.php` files in `../resources/views` */
@source '../resources/views/**/*.blade.php';
```
However, you don't need to specify the extension, instead you can just
point the directory and all the same automatic source detection rules
will apply.
```css
/* ./src/index.css */
@import 'tailwindcss';
@source '../resources/views';
```
If, for whatever reason, you want to disable the default source
detection feature entirely, and only want to rely on very specific glob
patterns you define, then you can disable it via `source(none)`.
```css
/* Completely disable the default auto source detection */
@import 'tailwindcss' source(none);
/* Only look at .blade.php files, nothing else */
@source "../resources/views/**/*.blade.php";
```
Note: even with `source(none)`, if your `@source` points to a directory,
then auto source detection will still be performed in that directory. If
you don't want that, then you can simply add explicit files in the globs
as seen in the previous example.
```css
/* Completely disable the default auto source detection */
@import 'tailwindcss' source(none);
/* Run auto source detection in `../resources/views` */
@source "../resources/views";
```
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
2024-10-29 21:33:34 +01:00
|
|
|
}
|
Improve performance of `@tailwindcss/postcss` and `@tailwindcss/vite` (#15226)
This PR improves the performance of the `@tailwindcss/postcss` and
`@tailwindcss/vite` implementations.
The issue is that in some scenarios, if you have multiple `.css` files,
then all of the CSS files are ran through the Tailwind CSS compiler. The
issue with this is that in a lot of cases, the CSS files aren't even
related to Tailwind CSS at all.
E.g.: in a Next.js project, if you use the `next/font/local` tool, then
every font you used will be in a separate CSS file. This means that we
run Tailwind CSS in all these files as well.
That said, running Tailwind CSS on these files isn't the end of the
world because we still need to handle `@import` in case `@tailwind
utilities` is being used. However, we also run the auto source detection
logic for every CSS file in the system. This part is bad.
To solve this, this PR introduces an internal `features` to collect what
CSS features are used throughout the system (`@import`, `@plugin`,
`@apply`, `@tailwind utilities`, etc…)
The `@tailwindcss/postcss` and `@tailwindcss/vite` plugin can use that
information to decide if they can take some shortcuts or not.
---
Overall, this means that we don't run the slow parts of Tailwind CSS if
we don't need to.
---------
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
2024-11-29 16:59:29 +01:00
|
|
|
|
2025-04-25 12:02:10 +02:00
|
|
|
DEBUG && I.start('Scan for candidates')
|
|
|
|
|
let candidates = compiler.features & Features.Utilities ? context.scanner.scan() : []
|
|
|
|
|
DEBUG && I.end('Scan for candidates')
|
|
|
|
|
|
|
|
|
|
if (compiler.features & Features.Utilities) {
|
|
|
|
|
DEBUG && I.start('Register dependency messages')
|
|
|
|
|
// Add all found files as direct dependencies
|
|
|
|
|
// Note: With Turbopack, the input file might not be a resolved path
|
|
|
|
|
let resolvedInputFile = path.resolve(base, inputFile)
|
|
|
|
|
for (let file of context.scanner.files) {
|
|
|
|
|
let absolutePath = path.resolve(file)
|
|
|
|
|
// The CSS file cannot be a dependency of itself
|
|
|
|
|
if (absolutePath === resolvedInputFile) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
Improve performance of `@tailwindcss/postcss` and `@tailwindcss/vite` (#15226)
This PR improves the performance of the `@tailwindcss/postcss` and
`@tailwindcss/vite` implementations.
The issue is that in some scenarios, if you have multiple `.css` files,
then all of the CSS files are ran through the Tailwind CSS compiler. The
issue with this is that in a lot of cases, the CSS files aren't even
related to Tailwind CSS at all.
E.g.: in a Next.js project, if you use the `next/font/local` tool, then
every font you used will be in a separate CSS file. This means that we
run Tailwind CSS in all these files as well.
That said, running Tailwind CSS on these files isn't the end of the
world because we still need to handle `@import` in case `@tailwind
utilities` is being used. However, we also run the auto source detection
logic for every CSS file in the system. This part is bad.
To solve this, this PR introduces an internal `features` to collect what
CSS features are used throughout the system (`@import`, `@plugin`,
`@apply`, `@tailwind utilities`, etc…)
The `@tailwindcss/postcss` and `@tailwindcss/vite` plugin can use that
information to decide if they can take some shortcuts or not.
---
Overall, this means that we don't run the slow parts of Tailwind CSS if
we don't need to.
---------
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
2024-11-29 16:59:29 +01:00
|
|
|
result.messages.push({
|
|
|
|
|
type: 'dependency',
|
|
|
|
|
plugin: '@tailwindcss/postcss',
|
2025-04-25 12:02:10 +02:00
|
|
|
file: absolutePath,
|
Improve performance of `@tailwindcss/postcss` and `@tailwindcss/vite` (#15226)
This PR improves the performance of the `@tailwindcss/postcss` and
`@tailwindcss/vite` implementations.
The issue is that in some scenarios, if you have multiple `.css` files,
then all of the CSS files are ran through the Tailwind CSS compiler. The
issue with this is that in a lot of cases, the CSS files aren't even
related to Tailwind CSS at all.
E.g.: in a Next.js project, if you use the `next/font/local` tool, then
every font you used will be in a separate CSS file. This means that we
run Tailwind CSS in all these files as well.
That said, running Tailwind CSS on these files isn't the end of the
world because we still need to handle `@import` in case `@tailwind
utilities` is being used. However, we also run the auto source detection
logic for every CSS file in the system. This part is bad.
To solve this, this PR introduces an internal `features` to collect what
CSS features are used throughout the system (`@import`, `@plugin`,
`@apply`, `@tailwind utilities`, etc…)
The `@tailwindcss/postcss` and `@tailwindcss/vite` plugin can use that
information to decide if they can take some shortcuts or not.
---
Overall, this means that we don't run the slow parts of Tailwind CSS if
we don't need to.
---------
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
2024-11-29 16:59:29 +01:00
|
|
|
parent: result.opts.from,
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-04-25 12:02:10 +02:00
|
|
|
|
|
|
|
|
// Register dependencies so changes in `base` cause a rebuild while
|
|
|
|
|
// giving tools like Vite or Parcel a glob that can be used to limit
|
|
|
|
|
// the files that cause a rebuild to only those that match it.
|
|
|
|
|
for (let { base: globBase, pattern } of context.scanner.globs) {
|
|
|
|
|
// Avoid adding a dependency on the base directory itself, since it
|
|
|
|
|
// causes Next.js to start an endless recursion if the `distDir` is
|
|
|
|
|
// configured to anything other than the default `.next` dir.
|
|
|
|
|
if (pattern === '*' && base === globBase) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (pattern === '') {
|
|
|
|
|
result.messages.push({
|
|
|
|
|
type: 'dependency',
|
|
|
|
|
plugin: '@tailwindcss/postcss',
|
|
|
|
|
file: path.resolve(globBase),
|
|
|
|
|
parent: result.opts.from,
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
result.messages.push({
|
|
|
|
|
type: 'dir-dependency',
|
|
|
|
|
plugin: '@tailwindcss/postcss',
|
|
|
|
|
dir: path.resolve(globBase),
|
|
|
|
|
glob: pattern,
|
|
|
|
|
parent: result.opts.from,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
DEBUG && I.end('Register dependency messages')
|
Improve performance of `@tailwindcss/postcss` and `@tailwindcss/vite` (#15226)
This PR improves the performance of the `@tailwindcss/postcss` and
`@tailwindcss/vite` implementations.
The issue is that in some scenarios, if you have multiple `.css` files,
then all of the CSS files are ran through the Tailwind CSS compiler. The
issue with this is that in a lot of cases, the CSS files aren't even
related to Tailwind CSS at all.
E.g.: in a Next.js project, if you use the `next/font/local` tool, then
every font you used will be in a separate CSS file. This means that we
run Tailwind CSS in all these files as well.
That said, running Tailwind CSS on these files isn't the end of the
world because we still need to handle `@import` in case `@tailwind
utilities` is being used. However, we also run the auto source detection
logic for every CSS file in the system. This part is bad.
To solve this, this PR introduces an internal `features` to collect what
CSS features are used throughout the system (`@import`, `@plugin`,
`@apply`, `@tailwind utilities`, etc…)
The `@tailwindcss/postcss` and `@tailwindcss/vite` plugin can use that
information to decide if they can take some shortcuts or not.
---
Overall, this means that we don't run the slow parts of Tailwind CSS if
we don't need to.
---------
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
2024-11-29 16:59:29 +01:00
|
|
|
}
|
2024-08-07 16:38:44 +02:00
|
|
|
|
2025-04-25 12:02:10 +02:00
|
|
|
DEBUG && I.start('Build utilities')
|
|
|
|
|
let tailwindCssAst = compiler.build(candidates)
|
|
|
|
|
DEBUG && I.end('Build utilities')
|
2024-12-04 15:43:59 +01:00
|
|
|
|
2025-04-25 12:02:10 +02:00
|
|
|
if (context.tailwindCssAst !== tailwindCssAst) {
|
|
|
|
|
if (optimize) {
|
|
|
|
|
DEBUG && I.start('Optimization')
|
2024-12-11 15:27:20 +01:00
|
|
|
|
2025-04-25 12:02:10 +02:00
|
|
|
DEBUG && I.start('AST -> CSS')
|
|
|
|
|
let css = toCss(tailwindCssAst)
|
|
|
|
|
DEBUG && I.end('AST -> CSS')
|
2024-12-11 15:27:20 +01:00
|
|
|
|
2025-04-25 12:02:10 +02:00
|
|
|
DEBUG && I.start('Lightning CSS')
|
Add support for source maps (#17775)
Closes #13694
Closes #13591
# Source Maps Support for Tailwind CSS
This PR adds support for source maps to Tailwind CSS v4 allowing us to
track where styles come from whether that be user CSS, imported
stylesheets, or generated utilities. This will improve debuggability in
browser dev tools and gives us a good foundation for producing better
error messages. I'll go over the details on how end users can enable
source maps, any limitations in our implementation, changes to the
internal `compile(…)` API, and some details and reasoning around the
implementation we chose.
## Usage
### CLI
Source maps can be enabled in the CLI by using the command line argument
`--map` which will generate an inline source map comment at the bottom
of your CSS. A separate file may be generated by passing a file name to
`--map`:
```bash
# Generates an inline source map
npx tailwindcss -i input.css -o output.css --map
# Generates a separate source map file
npx tailwindcss -i input.css -o output.css --map output.css.map
```
### PostCSS
Source maps are supported when using Tailwind as a PostCSS plugin *in
development mode only*. They may or may not be enabled by default
depending on your build tool. If they are not you may be able to
configure them within your PostCSS config:
```jsonc
// package.json
{
// …
"postcss": {
"map": { "inline": true },
"plugins": {
"@tailwindcss/postcss": {},
},
}
}
```
### Vite
Source maps are supported when using the Tailwind CSS Vite plugin in
*development mode only* by enabling the `css.devSourcemap` setting:
```js
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [tailwindcss()],
css: {
devSourcemap: true,
},
})
```
Now when a CSS file is requested by the browser it'll have an inline
source map comment that the browser can use.
## Limitations
- Production build source maps are currently disabled due to a bug in
Lightning CSS. See
https://github.com/parcel-bundler/lightningcss/pull/971 for more
details.
- In Vite, minified CSS build source maps are not supported at all. See
https://github.com/vitejs/vite/issues/2830 for more details.
- In PostCSS, minified CSS source maps are not supported. This is due to
the complexity required around re-associating every AST node with a
location in the generated, optimized CSS. This complexity would also
have a non-trivial performance impact.
## Testing
Here's how to test the source map functionality in different
environments:
### Testing the CLI
1. Setup typical project that the CLI can use and with sources to scan.
```css
@import "tailwindcss";
@utilty my-custom-utility {
color: red;
}
/* to test `@apply` */
.card {
@apply bg-white text-center shadow-md;
}
```
2. Build with source maps:
```bash
bun /path/to/tailwindcss/packages/@tailwindcss-cli/src/index.ts --input input.css -o output.css --map
```
3. Open Chrome DevTools, inspect an element with utility classes, and
you should see rules pointing to `input.css` or
`node_modules/tailwindcss/index.css`
### Testing with Vite
Testing in Vite will require building and installing necessary files
under `dist/*.tgz`.
1. Create a Vite project and enable source maps in `vite.config.js`:
```js
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [tailwindcss()],
css: {
// This line is required for them to work
devSourcemap: true,
},
})
```
2. Add a component that uses Tailwind classes and custom CSS:
```jsx
// ./src/app.jsx
export default function App() {
return (
<div className="bg-blue-500 my-custom-class">
Hello World
</div>
)
}
```
```css
/* ./src/styles.css */
@import "tailwindcss";
@utilty my-custom-utility {
color: red;
}
/* to test `@apply` */
.card {
@apply bg-white text-center shadow-md;
}
```
3. Run `npm run dev`, open DevTools, and inspect elements to verify
source mapping works for both utility classes and custom CSS.
### Testing with PostCSS CLI
1. Create a test file and update your PostCSS config:
```css
/* input.css */
@import "tailwindcss";
@layer components {
.card {
@apply p-6 rounded-lg shadow-lg;
}
}
```
```jsonc
// package.json
{
// …
"postcss": {
"map": {
"inline": true
},
"plugins": {
"/path/to/tailwindcss/packages/packages/@tailwindcss-postcss/src/index.ts": {}
}
}
}
```
2. Run PostCSS through Bun:
```bash
bunx --bun postcss ./src/index.css -o out.css
```
3. Inspect the output CSS - it should include an inline source map
comment at the bottom.
### Testing with PostCSS + Next.js
Testing in Next.js will require building and installing necessary files
under `dist/*.tgz`. However, I've not been able to get CSS source maps
to work in Next.js without this hack:
```js
const nextConfig: NextConfig = {
// next.js overwrites config.devtool so we prevent it from doing so
// please don't actually do this…
webpack: (config) =>
Object.defineProperty(config, "devtool", {
get: () => "inline-source-map",
set: () => {},
}),
};
```
This is definitely not supported and also doesn't work with turbopack.
This can be used to test them temporarily but I suspect that they just
don't work there.
### Manual source map analysis
You can analyze source maps using Evan Wallace's [Source Map
Visualization](https://evanw.github.io/source-map-visualization/) tool
which will help to verify the accuracy and quality of source maps. This
is what I used extensively while developing this implementation.
It'll help verify that custom, user CSS maps back to itself in the
input, that generated utilities all map back to `@tailwind utilities;`,
that source locations from imported files are also handled correctly,
etc… It also highlights the ranges of stuff so it's easy to see if there
are off-by-one errors.
It's easiest to use inline source maps with this tool because you can
take the CSS file and drop it on the page and it'll analyze it while
showing the file content.
If you're using Vite you'll want to access the CSS file with `?direct`
at the end so you don't get a JS module back.
## Implementation
The source map implementation follows the ECMA-426 specification and
includes several key components to aid in that goal:
### Source Location Tracking
Each emittable AST node in the compilation pipeline tracks two types of
source locations:
- `src`: Original source location - [source file, start offset, end
offset]
- `dst`: Generated source location - [output file, start offset, end
offset]
This dual tracking allows us to maintain mappings between the original
source and generated output for things like user CSS, generated
utilities, uses of `@apply`, and tracking theme variables.
It is important to note that source locations for nodes _never overlap_
within a file which helps simplify source map generation. As such each
type of node tracks a specific piece of itself rather than its entire
"block":
| Node | What a `SourceLocation` represents |
| ----------- |
---------------------------------------------------------------- |
| Style Rule | The selector |
| At Rule | Rule name and params, includes the `@` |
| Declaration | Property name and value, excludes the semicolon |
| Comment | The entire comment, includes the start `/*` and end `*/`
markers |
### Windows line endings when parsing CSS
Because our AST tracks nodes through offsets we must ensure that any
mutations to the file do *not* change the lenth of the string. We were
previously replacing `\r\n` with `\n` (see [filter code
points](https://drafts.csswg.org/css-syntax/#css-filter-code-points)
from the spec) — which changes the length of the string and all offsets
may end up incorrect. The CSS parser was updated to handle the CRLF
token directly by skipping over the `\r` and letting remaining code
handle `\n` as it did previously. Some additional tweaks were required
when "peeking" the input but those changes were fairly small.
### Tracking of imports
Source maps need paths to the actual imported stylesheets but the
resolve step for stylesheets happens inside the call to `loadStylesheet`
which make the file path unavailable to us. Because of this the
`loadStylesheet` API was augmented such that it has to return a `path`
property that we can then use to identify imported sources. I've also
made the same change to the `loadModule` API for consistency but nothing
currently uses this property.
The `path` property likely makes `base` redundant but elminating that
(if we even want to) is a future task.
### Optimizing the AST
Our optimization pass may intoduce some nodes, for example, fallbacks we
create for `@property`. These nodes are linked back to `@tailwind
utilities` as ultimately that is what is responsible for creating them.
### Line Offset Tables
A key component to our source map generation is the line offset table,
which was inspired by some ESBuild internals. It stores a sorted list of
offsets for the start of each line allowing us to translate offsets to
line/column `Position`s in `O(log N)` time and from `Position`s to
offsets in `O(1)` time. Creation of the table takes `O(N)` time.
This means that we can store code point offsets for source locations and
not have to worry about computing or tracking line/column numbers during
parsing and serialization. Only when a source map is generated do these
offsets need to be computed. This ensures the performance penalty when
not using source maps is minimal.
### Source Map Generation
The source map returned by `buildSourceMap()` is designed to follow the
[ECMA-426 spec](https://tc39.es/ecma426). Because that spec is not
completely finalized we consider the result of `buildSourceMap()` to be
internal API that may change as the spec chamges.
The produces source map is a "decoded" map such that all sources and
mappings are in an object graph. A library like `source-map-js` must be
used to convert this to an encoded source map of the right version where
mappings are encoded with base 64 VLQs.
Any specific integration (Vite, PostCSS, etc…) can then use
`toSourceMap()` from `@tailwindcss/node` to convert from the internal
source map to an spec-compliant encoded source map that can be
understood by other tools.
### Handling minification in Lightning
Since we use Lightning CSS for optimization, and it takes in an input
map, we generate an encoded source map that we then pass to lightning.
The output source map *from lighting itself* is then passed back in
during the second optimization pass. The final map is then passed from
lightning to the CLI (but not Vite or PostCSS — see the limitations
section for details).
In some cases we have to "fix up" the output CSS. When this happens we
use `magic-string` to do the replacement in a way that is trackable and
`@amppproject/remapping` to map that change back onto the original
source map. Once the need for these fix ups disappear these dependencies
can go away.
Notes:
- The accuracy of source maps run though lightning is reduced as it only
tracks on a per-rule level. This is sufficient enough for browser dev
tools so should be fine.
- Source maps during optimization do not function properly at this time
because of a bug in Lightning CSS regarding license comments. Once this
bug is fixed they will start working as expected.
### How source locations flow through the system
1. During initial CSS parsing, source locations are preserved.
2. During parsing these source locations are also mapped to the
destinations which supports an optimization for when no utilities are
generated.
3. Throughout the compilation process, transformations maintain source
location data
4. Generated utilities are explicitly pointed to `@tailwind utilities`
unless generated by `@apply`.
5. When optimization is enabled, source maps are remapped through
lightningcss
6. Final source maps are written in the requested format (inline or
separate file)
2025-05-08 16:29:49 -04:00
|
|
|
let optimized = optimizeCss(css, {
|
2025-04-25 12:02:10 +02:00
|
|
|
minify: typeof optimize === 'object' ? optimize.minify : true,
|
|
|
|
|
})
|
|
|
|
|
DEBUG && I.end('Lightning CSS')
|
|
|
|
|
|
|
|
|
|
DEBUG && I.start('CSS -> PostCSS AST')
|
Add support for source maps (#17775)
Closes #13694
Closes #13591
# Source Maps Support for Tailwind CSS
This PR adds support for source maps to Tailwind CSS v4 allowing us to
track where styles come from whether that be user CSS, imported
stylesheets, or generated utilities. This will improve debuggability in
browser dev tools and gives us a good foundation for producing better
error messages. I'll go over the details on how end users can enable
source maps, any limitations in our implementation, changes to the
internal `compile(…)` API, and some details and reasoning around the
implementation we chose.
## Usage
### CLI
Source maps can be enabled in the CLI by using the command line argument
`--map` which will generate an inline source map comment at the bottom
of your CSS. A separate file may be generated by passing a file name to
`--map`:
```bash
# Generates an inline source map
npx tailwindcss -i input.css -o output.css --map
# Generates a separate source map file
npx tailwindcss -i input.css -o output.css --map output.css.map
```
### PostCSS
Source maps are supported when using Tailwind as a PostCSS plugin *in
development mode only*. They may or may not be enabled by default
depending on your build tool. If they are not you may be able to
configure them within your PostCSS config:
```jsonc
// package.json
{
// …
"postcss": {
"map": { "inline": true },
"plugins": {
"@tailwindcss/postcss": {},
},
}
}
```
### Vite
Source maps are supported when using the Tailwind CSS Vite plugin in
*development mode only* by enabling the `css.devSourcemap` setting:
```js
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [tailwindcss()],
css: {
devSourcemap: true,
},
})
```
Now when a CSS file is requested by the browser it'll have an inline
source map comment that the browser can use.
## Limitations
- Production build source maps are currently disabled due to a bug in
Lightning CSS. See
https://github.com/parcel-bundler/lightningcss/pull/971 for more
details.
- In Vite, minified CSS build source maps are not supported at all. See
https://github.com/vitejs/vite/issues/2830 for more details.
- In PostCSS, minified CSS source maps are not supported. This is due to
the complexity required around re-associating every AST node with a
location in the generated, optimized CSS. This complexity would also
have a non-trivial performance impact.
## Testing
Here's how to test the source map functionality in different
environments:
### Testing the CLI
1. Setup typical project that the CLI can use and with sources to scan.
```css
@import "tailwindcss";
@utilty my-custom-utility {
color: red;
}
/* to test `@apply` */
.card {
@apply bg-white text-center shadow-md;
}
```
2. Build with source maps:
```bash
bun /path/to/tailwindcss/packages/@tailwindcss-cli/src/index.ts --input input.css -o output.css --map
```
3. Open Chrome DevTools, inspect an element with utility classes, and
you should see rules pointing to `input.css` or
`node_modules/tailwindcss/index.css`
### Testing with Vite
Testing in Vite will require building and installing necessary files
under `dist/*.tgz`.
1. Create a Vite project and enable source maps in `vite.config.js`:
```js
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [tailwindcss()],
css: {
// This line is required for them to work
devSourcemap: true,
},
})
```
2. Add a component that uses Tailwind classes and custom CSS:
```jsx
// ./src/app.jsx
export default function App() {
return (
<div className="bg-blue-500 my-custom-class">
Hello World
</div>
)
}
```
```css
/* ./src/styles.css */
@import "tailwindcss";
@utilty my-custom-utility {
color: red;
}
/* to test `@apply` */
.card {
@apply bg-white text-center shadow-md;
}
```
3. Run `npm run dev`, open DevTools, and inspect elements to verify
source mapping works for both utility classes and custom CSS.
### Testing with PostCSS CLI
1. Create a test file and update your PostCSS config:
```css
/* input.css */
@import "tailwindcss";
@layer components {
.card {
@apply p-6 rounded-lg shadow-lg;
}
}
```
```jsonc
// package.json
{
// …
"postcss": {
"map": {
"inline": true
},
"plugins": {
"/path/to/tailwindcss/packages/packages/@tailwindcss-postcss/src/index.ts": {}
}
}
}
```
2. Run PostCSS through Bun:
```bash
bunx --bun postcss ./src/index.css -o out.css
```
3. Inspect the output CSS - it should include an inline source map
comment at the bottom.
### Testing with PostCSS + Next.js
Testing in Next.js will require building and installing necessary files
under `dist/*.tgz`. However, I've not been able to get CSS source maps
to work in Next.js without this hack:
```js
const nextConfig: NextConfig = {
// next.js overwrites config.devtool so we prevent it from doing so
// please don't actually do this…
webpack: (config) =>
Object.defineProperty(config, "devtool", {
get: () => "inline-source-map",
set: () => {},
}),
};
```
This is definitely not supported and also doesn't work with turbopack.
This can be used to test them temporarily but I suspect that they just
don't work there.
### Manual source map analysis
You can analyze source maps using Evan Wallace's [Source Map
Visualization](https://evanw.github.io/source-map-visualization/) tool
which will help to verify the accuracy and quality of source maps. This
is what I used extensively while developing this implementation.
It'll help verify that custom, user CSS maps back to itself in the
input, that generated utilities all map back to `@tailwind utilities;`,
that source locations from imported files are also handled correctly,
etc… It also highlights the ranges of stuff so it's easy to see if there
are off-by-one errors.
It's easiest to use inline source maps with this tool because you can
take the CSS file and drop it on the page and it'll analyze it while
showing the file content.
If you're using Vite you'll want to access the CSS file with `?direct`
at the end so you don't get a JS module back.
## Implementation
The source map implementation follows the ECMA-426 specification and
includes several key components to aid in that goal:
### Source Location Tracking
Each emittable AST node in the compilation pipeline tracks two types of
source locations:
- `src`: Original source location - [source file, start offset, end
offset]
- `dst`: Generated source location - [output file, start offset, end
offset]
This dual tracking allows us to maintain mappings between the original
source and generated output for things like user CSS, generated
utilities, uses of `@apply`, and tracking theme variables.
It is important to note that source locations for nodes _never overlap_
within a file which helps simplify source map generation. As such each
type of node tracks a specific piece of itself rather than its entire
"block":
| Node | What a `SourceLocation` represents |
| ----------- |
---------------------------------------------------------------- |
| Style Rule | The selector |
| At Rule | Rule name and params, includes the `@` |
| Declaration | Property name and value, excludes the semicolon |
| Comment | The entire comment, includes the start `/*` and end `*/`
markers |
### Windows line endings when parsing CSS
Because our AST tracks nodes through offsets we must ensure that any
mutations to the file do *not* change the lenth of the string. We were
previously replacing `\r\n` with `\n` (see [filter code
points](https://drafts.csswg.org/css-syntax/#css-filter-code-points)
from the spec) — which changes the length of the string and all offsets
may end up incorrect. The CSS parser was updated to handle the CRLF
token directly by skipping over the `\r` and letting remaining code
handle `\n` as it did previously. Some additional tweaks were required
when "peeking" the input but those changes were fairly small.
### Tracking of imports
Source maps need paths to the actual imported stylesheets but the
resolve step for stylesheets happens inside the call to `loadStylesheet`
which make the file path unavailable to us. Because of this the
`loadStylesheet` API was augmented such that it has to return a `path`
property that we can then use to identify imported sources. I've also
made the same change to the `loadModule` API for consistency but nothing
currently uses this property.
The `path` property likely makes `base` redundant but elminating that
(if we even want to) is a future task.
### Optimizing the AST
Our optimization pass may intoduce some nodes, for example, fallbacks we
create for `@property`. These nodes are linked back to `@tailwind
utilities` as ultimately that is what is responsible for creating them.
### Line Offset Tables
A key component to our source map generation is the line offset table,
which was inspired by some ESBuild internals. It stores a sorted list of
offsets for the start of each line allowing us to translate offsets to
line/column `Position`s in `O(log N)` time and from `Position`s to
offsets in `O(1)` time. Creation of the table takes `O(N)` time.
This means that we can store code point offsets for source locations and
not have to worry about computing or tracking line/column numbers during
parsing and serialization. Only when a source map is generated do these
offsets need to be computed. This ensures the performance penalty when
not using source maps is minimal.
### Source Map Generation
The source map returned by `buildSourceMap()` is designed to follow the
[ECMA-426 spec](https://tc39.es/ecma426). Because that spec is not
completely finalized we consider the result of `buildSourceMap()` to be
internal API that may change as the spec chamges.
The produces source map is a "decoded" map such that all sources and
mappings are in an object graph. A library like `source-map-js` must be
used to convert this to an encoded source map of the right version where
mappings are encoded with base 64 VLQs.
Any specific integration (Vite, PostCSS, etc…) can then use
`toSourceMap()` from `@tailwindcss/node` to convert from the internal
source map to an spec-compliant encoded source map that can be
understood by other tools.
### Handling minification in Lightning
Since we use Lightning CSS for optimization, and it takes in an input
map, we generate an encoded source map that we then pass to lightning.
The output source map *from lighting itself* is then passed back in
during the second optimization pass. The final map is then passed from
lightning to the CLI (but not Vite or PostCSS — see the limitations
section for details).
In some cases we have to "fix up" the output CSS. When this happens we
use `magic-string` to do the replacement in a way that is trackable and
`@amppproject/remapping` to map that change back onto the original
source map. Once the need for these fix ups disappear these dependencies
can go away.
Notes:
- The accuracy of source maps run though lightning is reduced as it only
tracks on a per-rule level. This is sufficient enough for browser dev
tools so should be fine.
- Source maps during optimization do not function properly at this time
because of a bug in Lightning CSS regarding license comments. Once this
bug is fixed they will start working as expected.
### How source locations flow through the system
1. During initial CSS parsing, source locations are preserved.
2. During parsing these source locations are also mapped to the
destinations which supports an optimization for when no utilities are
generated.
3. Throughout the compilation process, transformations maintain source
location data
4. Generated utilities are explicitly pointed to `@tailwind utilities`
unless generated by `@apply`.
5. When optimization is enabled, source maps are remapped through
lightningcss
6. Final source maps are written in the requested format (inline or
separate file)
2025-05-08 16:29:49 -04:00
|
|
|
context.optimizedPostCssAst = postcss.parse(optimized.code, result.opts)
|
2025-04-25 12:02:10 +02:00
|
|
|
DEBUG && I.end('CSS -> PostCSS AST')
|
|
|
|
|
|
|
|
|
|
DEBUG && I.end('Optimization')
|
|
|
|
|
} else {
|
|
|
|
|
// Convert our AST to a PostCSS AST
|
|
|
|
|
DEBUG && I.start('Transform Tailwind CSS AST into PostCSS AST')
|
2025-09-18 12:03:19 -04:00
|
|
|
context.cachedPostCssAst = cssAstToPostCssAst(postcss, tailwindCssAst, root.source)
|
2025-04-25 12:02:10 +02:00
|
|
|
DEBUG && I.end('Transform Tailwind CSS AST into PostCSS AST')
|
|
|
|
|
}
|
2024-12-04 15:43:59 +01:00
|
|
|
}
|
|
|
|
|
|
2025-04-25 12:02:10 +02:00
|
|
|
context.tailwindCssAst = tailwindCssAst
|
|
|
|
|
|
|
|
|
|
DEBUG && I.start('Update PostCSS AST')
|
|
|
|
|
root.removeAll()
|
|
|
|
|
root.append(
|
|
|
|
|
optimize
|
|
|
|
|
? context.optimizedPostCssAst.clone().nodes
|
|
|
|
|
: context.cachedPostCssAst.clone().nodes,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Trick PostCSS into thinking the indent is 2 spaces, so it uses that
|
|
|
|
|
// as the default instead of 4.
|
|
|
|
|
root.raws.indent = ' '
|
|
|
|
|
DEBUG && I.end('Update PostCSS AST')
|
2024-09-30 16:39:21 +02:00
|
|
|
|
2025-04-25 12:02:10 +02:00
|
|
|
DEBUG && I.end(`[@tailwindcss/postcss] ${relative(base, inputFile)}`)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// An error requires a full rebuild to fix
|
|
|
|
|
context.compiler = null
|
2024-12-04 15:43:59 +01:00
|
|
|
|
2025-04-25 12:02:10 +02:00
|
|
|
// Ensure all dependencies we have collected thus far are included so that the rebuild
|
|
|
|
|
// is correctly triggered
|
|
|
|
|
for (let file of context.fullRebuildPaths) {
|
|
|
|
|
result.messages.push({
|
|
|
|
|
type: 'dependency',
|
|
|
|
|
plugin: '@tailwindcss/postcss',
|
|
|
|
|
file: path.resolve(file),
|
|
|
|
|
parent: result.opts.from,
|
|
|
|
|
})
|
|
|
|
|
}
|
2024-12-04 15:43:59 +01:00
|
|
|
|
2025-04-25 12:02:10 +02:00
|
|
|
// We found that throwing the error will cause PostCSS to no longer watch for changes
|
|
|
|
|
// in some situations so we instead log the error and continue with an empty stylesheet.
|
|
|
|
|
console.error(error)
|
2025-09-18 12:03:19 -04:00
|
|
|
|
|
|
|
|
if (error && typeof error === 'object' && 'message' in error) {
|
|
|
|
|
throw root.error(`${error.message}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw root.error(`${error}`)
|
2025-04-25 12:02:10 +02:00
|
|
|
}
|
2024-08-07 16:38:44 +02:00
|
|
|
},
|
2024-03-05 14:23:26 +01:00
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-26 15:54:07 +02:00
|
|
|
export default Object.assign(tailwindcss, { postcss: true }) as PluginCreator<PluginOptions>
|