2024-03-05 14:23:26 +01:00
|
|
|
import { unlink, writeFile } from 'node:fs/promises'
|
|
|
|
|
import postcss from 'postcss'
|
|
|
|
|
import { afterEach, beforeEach, describe, expect, test } from 'vitest'
|
2024-08-08 17:49:06 +02:00
|
|
|
// @ts-ignore
|
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.
|
|
|
|
|
const INPUT_CSS_PATH = `${__dirname}/fixtures/example-project/input.css`
|
|
|
|
|
|
|
|
|
|
const css = String.raw
|
|
|
|
|
|
|
|
|
|
test("`@import 'tailwindcss'` is replaced with the generated CSS", async () => {
|
2024-03-08 18:36:07 +01:00
|
|
|
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: INPUT_CSS_PATH })
|
|
|
|
|
|
|
|
|
|
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',
|
2024-07-29 16:50:06 +02:00
|
|
|
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 () => {
|
2024-03-08 18:36:07 +01:00
|
|
|
let processor = postcss([
|
|
|
|
|
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
|
|
|
|
|
])
|
2024-03-05 14:23:26 +01:00
|
|
|
|
|
|
|
|
// `@apply` is used because Lightning is skipped if neither `@tailwind` nor
|
|
|
|
|
// `@apply` is used.
|
|
|
|
|
let result = await processor.process(
|
|
|
|
|
css`
|
|
|
|
|
@layer utilities {
|
|
|
|
|
.foo {
|
|
|
|
|
@apply text-[black];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@layer utilities {
|
|
|
|
|
.bar {
|
|
|
|
|
color: red;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
{ from: INPUT_CSS_PATH },
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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 () => {
|
2024-03-08 18:36:07 +01:00
|
|
|
let processor = postcss([
|
|
|
|
|
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
|
|
|
|
|
])
|
2024-03-05 14:23:26 +01:00
|
|
|
|
|
|
|
|
// `@apply` is used because Lightning is skipped if neither `@tailwind` nor
|
|
|
|
|
// `@apply` is used.
|
|
|
|
|
let result = await processor.process(
|
|
|
|
|
css`
|
Add support for `inline` option when defining `@theme` values (#14095)
This PR adds support for a new `inline` option when defining a `@theme`
block that tells Tailwind to use raw theme values for utilities instead
of referencing the corresponding generated CSS variable.
```css
/* Input */
@theme inline {
--color-red-500: #ef4444;
/* ... */
}
/* Example output */
:root {
--color-red-500: #ef4444;
}
.text-red-500 {
color: #ef4444;
}
```
This can be composed with the existing `reference` option in case you
want to define a `@theme` block as both `reference` (so the variables
aren't generated) and `inline`:
```css
/* Input */
@theme inline reference {
--color-red-500: #ef4444;
/* ... */
}
/* Example output */
.text-red-500 {
color: #ef4444;
}
```
Since you can have multiple `@theme` blocks, you can even define some
values normally and some as inline based on how you're using them. For
example you might want to use `inline` for defining literal tokens like
`--color-red-500`, but include the variable for tokens that you want to
be able to theme like `--color-primary`:
```css
/* Input */
@theme inline {
--color-red-500: #ef4444;
/* ... */
}
@theme {
--color-primary: var(--color-red-500);
}
/* Example output */
:root {
--color-red-500: #ef4444;
--color-primary: var(--color-red-500);
}
.text-red-500 {
color: #ef4444;
}
.text-primary {
color: var(--color-primary, var(--color-red-500));
}
```
## Breaking changes
Prior to this PR, you could `@import` a stylesheet that contained
`@theme` blocks as reference by adding the `reference` keyword to your
import:
```css
@import "./my-theme.css" reference;
```
Now that `reference` isn't the only possible option when declaring your
`@theme`, this syntax has changed to a new `theme(…)` function that
accepts `reference` and `inline` as potential space-separated values:
```css
@import "./my-theme.css";
@import "./my-theme.css" theme(reference);
@import "./my-theme.css" theme(inline);
@import "./my-theme.css" theme(reference inline);
```
If you are using the `@import … reference` option with an earlier alpha
release, you'll need to update your code to `@import … theme(reference)`
once this PR lands in a release.
## Motivation
This PR is designed to solve an issue pointed out in #14091.
Prior to this PR, generated utilities would always reference variables
directly, with the raw value as a fallback:
```css
/* Input */
@theme {
--color-red-500: #ef4444;
/* ... */
}
/* Example output */
:root {
--color-red-500: #ef4444;
}
.text-red-500 {
color: var(--color-red-500, #ef4444);
}
```
But this can create issues with variables resolving to an unexpected
value when a theme value is referencing another variable defined on
`:root`.
For example, say you have a CSS file like this:
```css
:root, .light {
--text-fg: #000;
}
.dark {
--text-fg: #fff;
}
@theme {
--color-fg: var(--text-fg);
}
```
Without `@theme inline`, we'd generate this output if you used the
`text-fg` utility:
```css
:root, .light {
--text-fg: #000;
}
.dark {
--text-fg: #fff;
}
:root {
--color-fg: var(--text-fg);
}
.text-fg {
color: var(--color-fg, var(--text-fg));
}
```
Now if you wrote this HTML, you're probably expecting your text to be
the dark mode color:
```html
<div class="dark">
<h1 class="text-fg">Hello world</h1>
</div>
```
But you'd actually get the light mode color because of this rule:
```css
:root {
--color-fg: var(--text-fg);
}
.text-fg {
color: var(--color-fg, var(--text-fg));
}
```
The browser will try to resolve the `--color-fg` variable, which is
defined on `:root`. When it tries to resolve the value, _it uses the
value of `var(--text-fg)` as it would resolve at `:root`_, not what it
would resolve to based on the element that has the `text-fg` class.
So `var(--color-fg)` resolves to `#000` because `var(--text-fg)`
resolved to `#000` at the point in the tree where the browser resolved
the value of `var(--color-fg)`.
By using `@theme inline`, the `.text-fg` class looks like this:
```css
.text-fg {
color: var(--text-fg);
}
```
With this definition, the browser doesn't try to resolve `--color-fg` at
all and instead resolves `--text-fg` directly which correctly resolves
to `#fff` as expected.
---------
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2024-08-02 09:37:30 -04:00
|
|
|
@import 'tailwindcss/theme.css' theme(reference);
|
2024-03-05 14:23:26 +01:00
|
|
|
.foo {
|
|
|
|
|
@apply text-red-500;
|
|
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
{ from: INPUT_CSS_PATH },
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
expect(result.css.trim()).toMatchInlineSnapshot(`
|
|
|
|
|
".foo {
|
2024-03-21 13:25:43 -04:00
|
|
|
color: var(--color-red-500, #ef4444);
|
2024-03-05 14:23:26 +01:00
|
|
|
}"
|
|
|
|
|
`)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('processing without specifying a base path', () => {
|
|
|
|
|
let filepath = `${process.cwd()}/my-test-file.html`
|
|
|
|
|
|
|
|
|
|
beforeEach(() =>
|
|
|
|
|
writeFile(filepath, `<div class="md:[&:hover]:content-['testing_default_base_path']">`),
|
|
|
|
|
)
|
|
|
|
|
afterEach(() => unlink(filepath))
|
|
|
|
|
|
|
|
|
|
test('the current working directory is used by default', async () => {
|
2024-03-08 18:36:07 +01:00
|
|
|
let processor = postcss([tailwindcss({ optimize: { minify: false } })])
|
2024-03-05 14:23:26 +01:00
|
|
|
|
|
|
|
|
let result = await processor.process(`@import "tailwindcss"`, { from: INPUT_CSS_PATH })
|
|
|
|
|
|
|
|
|
|
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),
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
})
|
2024-07-11 09:47:26 -04:00
|
|
|
|
|
|
|
|
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';
|
2024-08-07 16:38:44 +02:00
|
|
|
@plugin './plugin.js';
|
2024-07-11 09:47:26 -04:00
|
|
|
`,
|
|
|
|
|
{ from: INPUT_CSS_PATH },
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
expect(result.css.trim()).toMatchInlineSnapshot(`
|
|
|
|
|
".underline {
|
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 {
|
2024-07-11 09:47:26 -04:00
|
|
|
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';
|
2024-07-29 16:58:07 +02:00
|
|
|
@plugin 'internal-example-plugin';
|
2024-07-11 09:47:26 -04:00
|
|
|
`,
|
|
|
|
|
{ from: INPUT_CSS_PATH },
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}"
|
|
|
|
|
`)
|
|
|
|
|
})
|
|
|
|
|
})
|