tailwindcss/packages/@tailwindcss-postcss/src/index.test.ts

445 lines
11 KiB
TypeScript
Raw Normal View History

import dedent from 'dedent'
import { mkdir, mkdtemp, readFile, rm, unlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'path'
2024-03-05 14:23:26 +01:00
import postcss from 'postcss'
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
2024-03-05 14:23:26 +01:00
import tailwindcss from './index'
// We give this file path to PostCSS for processing.
// This file doesn't exist, but the path is used to resolve imports.
// We place it in packages/ because Vitest runs in the monorepo root,
// and packages/tailwindcss must be a sub-folder for
// @import 'tailwindcss' to work.
function inputCssFilePath() {
// Including the current test name to ensure that the cache is invalidated per
// test otherwise the cache will be used across tests.
return `${__dirname}/fixtures/example-project/input.css?test=${expect.getState().currentTestName}`
}
2024-03-05 14:23:26 +01:00
const css = dedent
2024-03-05 14:23:26 +01:00
test("`@import 'tailwindcss'` is replaced with the generated CSS", async () => {
let processor = postcss([
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
])
2024-03-05 14:23:26 +01:00
let result = await processor.process(`@import 'tailwindcss'`, { from: inputCssFilePath() })
2024-03-05 14:23:26 +01:00
expect(result.css.trim()).toMatchSnapshot()
// Check for dependency messages
expect(result.messages).toContainEqual({
type: 'dependency',
file: expect.stringMatching(/index.html$/g),
parent: expect.any(String),
plugin: expect.any(String),
})
expect(result.messages).toContainEqual({
type: 'dependency',
file: expect.stringMatching(/index.js$/g),
parent: expect.any(String),
plugin: expect.any(String),
})
expect(result.messages).toContainEqual({
type: 'dir-dependency',
dir: expect.stringMatching(/example-project[\/|\\]src$/g),
2024-03-05 14:23:26 +01:00
glob: expect.stringMatching(/^\*\*\/\*/g),
parent: expect.any(String),
plugin: expect.any(String),
})
})
test('output is optimized by Lightning CSS', async () => {
let processor = postcss([
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
])
2024-03-05 14:23:26 +01:00
let result = await processor.process(
css`
@layer utilities {
.foo {
@apply text-[black];
}
}
@layer utilities {
.bar {
color: red;
}
}
`,
{ from: inputCssFilePath() },
2024-03-05 14:23:26 +01:00
)
expect(result.css.trim()).toMatchInlineSnapshot(`
"@layer utilities {
.foo {
color: #000;
}
.bar {
color: red;
}
}"
`)
})
test('@apply can be used without emitting the theme in the CSS file', async () => {
let processor = postcss([
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
])
2024-03-05 14:23:26 +01:00
let result = await processor.process(
css`
@reference 'tailwindcss/theme.css';
2024-03-05 14:23:26 +01:00
.foo {
@apply text-red-500;
}
`,
{ from: inputCssFilePath() },
2024-03-05 14:23:26 +01:00
)
expect(result.css.trim()).toMatchInlineSnapshot(`
".foo {
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
color: var(--color-red-500, oklch(63.7% .237 25.331));
2024-03-05 14:23:26 +01:00
}"
`)
})
describe('processing without specifying a base path', () => {
let filepath: string
let dir: string
beforeEach(async () => {
dir = await mkdtemp(path.join(tmpdir(), 'tw-postcss'))
await mkdir(dir, { recursive: true })
filepath = path.join(dir, 'my-test-file.html')
await writeFile(filepath, `<div class="md:[&:hover]:content-['testing_default_base_path']">`)
})
2024-03-05 14:23:26 +01:00
afterEach(() => unlink(filepath))
test('the current working directory is used by default', async () => {
const spy = vi.spyOn(process, 'cwd')
spy.mockReturnValue(dir)
let processor = postcss([tailwindcss({ optimize: { minify: false } })])
2024-03-05 14:23:26 +01:00
let result = await processor.process(`@import "tailwindcss"`, { from: inputCssFilePath() })
2024-03-05 14:23:26 +01:00
expect(result.css).toContain(
".md\\:\\[\\&\\:hover\\]\\:content-\\[\\'testing_default_base_path\\'\\]",
)
expect(result.messages).toContainEqual({
type: 'dependency',
file: expect.stringMatching(/my-test-file.html$/g),
parent: expect.any(String),
plugin: expect.any(String),
})
})
})
describe('plugins', () => {
test('local CJS plugin', async () => {
let processor = postcss([
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
])
let result = await processor.process(
css`
@import 'tailwindcss/utilities';
Add `@source` support (#14078) This PR is an umbrella PR where we will add support for the new `@source` directive. This will allow you to add explicit content glob patterns if you want to look for Tailwind classes in other files that are not automatically detected yet. Right now this is an addition to the existing auto content detection that is automatically enabled in the `@tailwindcss/postcss` and `@tailwindcss/cli` packages. The `@tailwindcss/vite` package doesn't use the auto content detection, but uses the module graph instead. From an API perspective there is not a lot going on. There are only a few things that you have to know when using the `@source` directive, and you probably already know the rules: 1. You can use multiple `@source` directives if you want. 2. The `@source` accepts a glob pattern so that you can match multiple files at once 3. The pattern is relative to the current file you are in 4. The pattern includes all files it is matching, even git ignored files 1. The motivation for this is so that you can explicitly point to a `node_modules` folder if you want to look at `node_modules` for whatever reason. 6. Right now we don't support negative globs (starting with a `!`) yet, that will be available in the near future. Usage example: ```css /* ./src/input.css */ @import "tailwindcss"; @source "../laravel/resources/views/**/*.blade.php"; @source "../../packages/monorepo-package/**/*.js"; ``` It looks like the PR introduced a lot of changes, but this is a side effect of all the other plumbing work we had to do to make this work. For example: 1. We added dedicated integration tests that run on Linux and Windows in CI (just to make sure that all the `path` logic is correct) 2. We Have to make sure that the glob patterns are always correct even if you are using `@import` in your CSS and use `@source` in an imported file. This is because we receive the flattened CSS contents where all `@import`s are inlined. 3. We have to make sure that we also listen for changes in the files that match any of these patterns and trigger a rebuild. PRs: - [x] https://github.com/tailwindlabs/tailwindcss/pull/14063 - [x] https://github.com/tailwindlabs/tailwindcss/pull/14085 - [x] https://github.com/tailwindlabs/tailwindcss/pull/14079 - [x] https://github.com/tailwindlabs/tailwindcss/pull/14067 - [x] https://github.com/tailwindlabs/tailwindcss/pull/14076 - [x] https://github.com/tailwindlabs/tailwindcss/pull/14080 - [x] https://github.com/tailwindlabs/tailwindcss/pull/14127 - [x] https://github.com/tailwindlabs/tailwindcss/pull/14135 Once all the PRs are merged, then this umbrella PR can be merged. > [!IMPORTANT] > Make sure to merge this without rebasing such that each individual PR ends up on the main branch. --------- Co-authored-by: Philipp Spiess <hello@philippspiess.com> Co-authored-by: Jordan Pittman <jordan@cryptica.me> Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
2024-08-07 16:38:44 +02:00
@plugin './plugin.js';
`,
{ from: inputCssFilePath() },
)
expect(result.css.trim()).toMatchInlineSnapshot(`
".underline {
Add `@source` support (#14078) This PR is an umbrella PR where we will add support for the new `@source` directive. This will allow you to add explicit content glob patterns if you want to look for Tailwind classes in other files that are not automatically detected yet. Right now this is an addition to the existing auto content detection that is automatically enabled in the `@tailwindcss/postcss` and `@tailwindcss/cli` packages. The `@tailwindcss/vite` package doesn't use the auto content detection, but uses the module graph instead. From an API perspective there is not a lot going on. There are only a few things that you have to know when using the `@source` directive, and you probably already know the rules: 1. You can use multiple `@source` directives if you want. 2. The `@source` accepts a glob pattern so that you can match multiple files at once 3. The pattern is relative to the current file you are in 4. The pattern includes all files it is matching, even git ignored files 1. The motivation for this is so that you can explicitly point to a `node_modules` folder if you want to look at `node_modules` for whatever reason. 6. Right now we don't support negative globs (starting with a `!`) yet, that will be available in the near future. Usage example: ```css /* ./src/input.css */ @import "tailwindcss"; @source "../laravel/resources/views/**/*.blade.php"; @source "../../packages/monorepo-package/**/*.js"; ``` It looks like the PR introduced a lot of changes, but this is a side effect of all the other plumbing work we had to do to make this work. For example: 1. We added dedicated integration tests that run on Linux and Windows in CI (just to make sure that all the `path` logic is correct) 2. We Have to make sure that the glob patterns are always correct even if you are using `@import` in your CSS and use `@source` in an imported file. This is because we receive the flattened CSS contents where all `@import`s are inlined. 3. We have to make sure that we also listen for changes in the files that match any of these patterns and trigger a rebuild. PRs: - [x] https://github.com/tailwindlabs/tailwindcss/pull/14063 - [x] https://github.com/tailwindlabs/tailwindcss/pull/14085 - [x] https://github.com/tailwindlabs/tailwindcss/pull/14079 - [x] https://github.com/tailwindlabs/tailwindcss/pull/14067 - [x] https://github.com/tailwindlabs/tailwindcss/pull/14076 - [x] https://github.com/tailwindlabs/tailwindcss/pull/14080 - [x] https://github.com/tailwindlabs/tailwindcss/pull/14127 - [x] https://github.com/tailwindlabs/tailwindcss/pull/14135 Once all the PRs are merged, then this umbrella PR can be merged. > [!IMPORTANT] > Make sure to merge this without rebasing such that each individual PR ends up on the main branch. --------- Co-authored-by: Philipp Spiess <hello@philippspiess.com> Co-authored-by: Jordan Pittman <jordan@cryptica.me> Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
2024-08-07 16:38:44 +02:00
text-decoration-line: underline;
}
@media (inverted-colors: inverted) {
.inverted\\:flex {
display: flex;
}
}
.hocus\\:underline:focus, .hocus\\:underline:hover {
text-decoration-line: underline;
}"
`)
})
test('local CJS plugin from `@import`-ed file', async () => {
let processor = postcss([
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
])
let result = await processor.process(
css`
@import 'tailwindcss/utilities';
@import '../example-project/src/relative-import.css';
`,
{ from: `${__dirname}/fixtures/another-project/input.css` },
)
expect(result.css.trim()).toMatchInlineSnapshot(`
".underline {
text-decoration-line: underline;
}
@media (inverted-colors: inverted) {
.inverted\\:flex {
display: flex;
}
}
.hocus\\:underline:focus, .hocus\\:underline:hover {
text-decoration-line: underline;
}"
`)
})
test('published CJS plugin', async () => {
let processor = postcss([
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
])
let result = await processor.process(
css`
@import 'tailwindcss/utilities';
@plugin 'internal-example-plugin';
`,
{ from: inputCssFilePath() },
)
expect(result.css.trim()).toMatchInlineSnapshot(`
".underline {
text-decoration-line: underline;
}
@media (inverted-colors: inverted) {
.inverted\\:flex {
display: flex;
}
}
.hocus\\:underline:focus, .hocus\\:underline:hover {
text-decoration-line: underline;
}"
`)
})
})
test('bail early when Tailwind is not used', async () => {
let processor = postcss([
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
])
let result = await processor.process(
css`
.custom-css {
color: red;
}
`,
{ from: inputCssFilePath() },
)
// `fixtures/example-project` includes an `underline` candidate. But since we
// didn't use `@tailwind utilities` we didn't scan for utilities.
expect(result.css).not.toContain('.underline {')
expect(result.css.trim()).toMatchInlineSnapshot(`
".custom-css {
color: red;
}"
`)
})
test('handle CSS when only using a `@reference` (we should not bail early)', async () => {
let processor = postcss([
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
])
let result = await processor.process(
css`
@reference "tailwindcss/theme.css";
.foo {
@variant md {
bar: baz;
}
}
`,
{ from: inputCssFilePath() },
)
expect(result.css.trim()).toMatchInlineSnapshot(`
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
"@media (min-width: 48rem) {
.foo {
bar: baz;
}
}"
`)
})
test('handle CSS when using a `@variant` using variants that do not rely on the `@theme`', async () => {
let processor = postcss([
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
])
let result = await processor.process(
css`
.foo {
@variant data-is-hoverable {
bar: baz;
}
}
`,
{ from: inputCssFilePath() },
)
expect(result.css.trim()).toMatchInlineSnapshot(`
".foo[data-is-hoverable] {
bar: baz;
}"
`)
})
test('runs `Once` plugins in the right order', async () => {
let before = ''
let after = ''
let processor = postcss([
{
postcssPlugin: 'before',
Once(root) {
before = root.toString()
},
},
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
{
postcssPlugin: 'after',
Once(root) {
after = root.toString()
},
},
])
let result = await processor.process(
css`
@theme {
--color-red-500: red;
}
.custom-css {
color: theme(--color-red-500);
}
`,
{ from: inputCssFilePath() },
)
expect(result.css.trim()).toMatchInlineSnapshot(`
Re-enable: Only expose used CSS variables (#16676) This PR re-enables the changes necessary to remove unused theme variables and keyframes form your CSS. This change was initially landed as #16211 and then later reverted in #16403 because we found some unexpected interactions with using `@apply` and CSS variables in multi-root setups like CSS modules or Vue inline `<style>` blocks that were no longer seeing their required variables defined. This issue is fixed by now ensuring that theme variables that are defined within an `@reference "…"` boundary will still be emitted in the generated CSS when used (as this would otherwise not generate a valid stylesheet). So given the following input CSS: ```css @reference "tailwindcss"; .text-red { @apply text-red-500; } ``` We will now compile this to: ```css @layer theme { :root, :host { --text-red-500: oklch(0.637 0.237 25.331); } } .text-red { color: var(--text-red-500); } ``` This PR also improves the initial implementation to not mark theme variables as used if they are only used to define other theme variables. For example: ```css @theme { --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; --default-font-family: var(--font-sans); --default-mono-font-family: var(--font-mono); } .default-font-family { font-family: var(--default-font-family); } ``` This would be reduced to the following now as `--font-mono` is only used to define another variable and never used outside the theme block: ```css :root, :host { --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; --default-font-family: var(--font-sans); } .default-font-family { font-family: var(--default-font-family); } ``` ## Test plan - See updated unit and integration tests - Validated it works end-to-end by using a SvelteKit example
2025-02-21 10:45:22 +01:00
".custom-css {
color: red;
}"
`)
expect(before).toMatchInlineSnapshot(`
"@theme {
--color-red-500: red;
}
.custom-css {
color: theme(--color-red-500);
}"
`)
expect(after).toMatchInlineSnapshot(`
Re-enable: Only expose used CSS variables (#16676) This PR re-enables the changes necessary to remove unused theme variables and keyframes form your CSS. This change was initially landed as #16211 and then later reverted in #16403 because we found some unexpected interactions with using `@apply` and CSS variables in multi-root setups like CSS modules or Vue inline `<style>` blocks that were no longer seeing their required variables defined. This issue is fixed by now ensuring that theme variables that are defined within an `@reference "…"` boundary will still be emitted in the generated CSS when used (as this would otherwise not generate a valid stylesheet). So given the following input CSS: ```css @reference "tailwindcss"; .text-red { @apply text-red-500; } ``` We will now compile this to: ```css @layer theme { :root, :host { --text-red-500: oklch(0.637 0.237 25.331); } } .text-red { color: var(--text-red-500); } ``` This PR also improves the initial implementation to not mark theme variables as used if they are only used to define other theme variables. For example: ```css @theme { --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; --default-font-family: var(--font-sans); --default-mono-font-family: var(--font-mono); } .default-font-family { font-family: var(--default-font-family); } ``` This would be reduced to the following now as `--font-mono` is only used to define another variable and never used outside the theme block: ```css :root, :host { --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; --default-font-family: var(--font-sans); } .default-font-family { font-family: var(--default-font-family); } ``` ## Test plan - See updated unit and integration tests - Validated it works end-to-end by using a SvelteKit example
2025-02-21 10:45:22 +01:00
".custom-css {
color: red;
}"
`)
})
describe('concurrent builds', () => {
let dir: string
beforeEach(async () => {
dir = await mkdtemp(path.join(tmpdir(), 'tw-postcss'))
await writeFile(path.join(dir, 'index.html'), `<div class="underline"></div>`)
await writeFile(
path.join(dir, 'index.css'),
css`
@import './dependency.css';
`,
)
await writeFile(
path.join(dir, 'dependency.css'),
css`
@tailwind utilities;
`,
)
})
afterEach(() => rm(dir, { recursive: true, force: true }))
test('does experience a race-condition when calling the plugin two times for the same change', async () => {
const spy = vi.spyOn(process, 'cwd')
spy.mockReturnValue(dir)
let from = path.join(dir, 'index.css')
let input = (await readFile(path.join(dir, 'index.css'))).toString()
let plugin = tailwindcss({ optimize: { minify: false } })
async function run(input: string): Promise<string> {
let ast = postcss.parse(input)
for (let runner of (plugin as any).plugins) {
if (runner.Once) {
await runner.Once(ast, { result: { opts: { from }, messages: [] } })
}
}
return ast.toString()
}
let result = await run(input)
expect(result).toContain('.underline')
// Ensure that the mtime is updated
await new Promise((resolve) => setTimeout(resolve, 100))
await writeFile(
path.join(dir, 'dependency.css'),
css`
@tailwind utilities;
.red {
color: red;
}
`,
)
let promise1 = run(input)
let promise2 = run(input)
expect(await promise1).toContain('.red')
expect(await promise2).toContain('.red')
})
})
PostCSS: Fix Turbopack 'one-revision-behind' bug (#17554) Closes #17508 This PR fixes another issue we found that caused dev builds with Next.js and Turbopack to resolve the CSS file that was saved one revision before the latest update. When debugging this we noticed that the PostCSS entry is called twice for every one update when changing the input CSS file directly. That was caused by the input file itself being added as a _dependency_ so you would first get the callback that a _dependency_ has updated (at which point we look at the file system and figure out we need a full-rebuild because the input.css file has changed) and then another callback for when the _input file_ has updated. The problem with the second callback was that the file-system was already scanned for updates and since this includes the `mtimes` for the input file, we seemingly thought that the input file did not change. However, the issue is that the first callback actually came with an outdated PostCSS input AST... We found that this problem arises when you register the input CSS as a dependency of itself. This is not expected and we actually guard against this in the PostCSS client. However, we found that the input `from` argument is _a relative path when using Next.js with Turbopack_ so that check was not working as expected. ## Test plan Added the change to the repro from #17508 and it seems to work fine now. https://github.com/user-attachments/assets/2acb0078-f961-4498-be1a-b1c72d5ceda1 Also added a unit test to ensure we document that the input file path can be a relative path. Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2025-04-04 14:58:50 +02:00
test('does not register the input file as a dependency, even if it is passed in as relative path', async () => {
let processor = postcss([
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
])
let result = await processor.process(`@tailwind utilities`, { from: './input.css' })
expect(result.css.trim()).toMatchInlineSnapshot(`
".underline {
text-decoration-line: underline;
}"
`)
// Check for dependency messages
expect(result.messages).not.toContainEqual({
type: 'dependency',
file: expect.stringMatching(/input.css$/g),
parent: expect.any(String),
plugin: expect.any(String),
})
})