import fs from 'node:fs/promises'
import path from 'node:path'
import { describe, expect, test } from 'vitest'
import { compile } from '.'
import plugin from './plugin'
import { compileCss, optimizeCss } from './test-utils/run'
const css = String.raw
describe('--alpha(…)', () => {
test('--alpha(…)', async () => {
expect(
await compileCss(css`
.foo {
margin: --alpha(red / 50%);
}
`),
).toMatchInlineSnapshot(`
".foo {
margin: oklab(62.7955% .224 .125 / .5);
}"
`)
})
test('--alpha(…) errors when no arguments are used', async () => {
await expect(() =>
compileCss(css`
.foo {
margin: --alpha();
}
`),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: The --alpha(…) function requires a color and an alpha value, e.g.: \`--alpha(var(--my-color) / 50%)\`]`,
)
})
test('--alpha(…) errors when alpha value is missing', async () => {
await expect(() =>
compileCss(css`
.foo {
margin: --alpha(red);
}
`),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: The --alpha(…) function requires a color and an alpha value, e.g.: \`--alpha(red / 50%)\`]`,
)
})
test('--alpha(…) errors multiple arguments are used', async () => {
await expect(() =>
compileCss(css`
.foo {
margin: --alpha(red / 50%, blue);
}
`),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: The --alpha(…) function only accepts one argument, e.g.: \`--alpha(red / 50%)\`]`,
)
})
})
describe('--spacing(…)', () => {
test('--spacing(…)', async () => {
expect(
await compileCss(css`
@theme {
--spacing: 0.25rem;
}
.foo {
margin: --spacing(4);
}
`),
).toMatchInlineSnapshot(`
":root, :host {
--spacing: .25rem;
}
.foo {
margin: calc(var(--spacing) * 4);
}"
`)
})
test('--spacing(…) with inline `@theme` value', async () => {
expect(
await compileCss(css`
@theme inline {
--spacing: 0.25rem;
}
.foo {
margin: --spacing(4);
}
`),
).toMatchInlineSnapshot(`
".foo {
margin: 1rem;
}"
`)
})
test('--spacing(…) relies on `--spacing` to be defined', async () => {
await expect(() =>
compileCss(css`
.foo {
margin: --spacing(4);
}
`),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: The --spacing(…) function requires that the \`--spacing\` theme variable exists, but it was not found.]`,
)
})
test('--spacing(…) requires a single value', async () => {
await expect(() =>
compileCss(css`
@theme {
--spacing: 0.25rem;
}
.foo {
margin: --spacing();
}
`),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: The --spacing(…) function requires an argument, but received none.]`,
)
})
test('--spacing(…) does not have multiple arguments', async () => {
await expect(() =>
compileCss(css`
.foo {
margin: --spacing(4, 5, 6);
}
`),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: The --spacing(…) function only accepts a single argument, but received 3.]`,
)
})
})
describe('--theme(…)', () => {
test('--theme(--color-red-500)', async () => {
expect(
await compileCss(css`
@theme {
--color-red-500: #f00;
}
.red {
color: --theme(--color-red-500);
}
`),
).toMatchInlineSnapshot(`
":root, :host {
--color-red-500: red;
}
.red {
color: var(--color-red-500);
}"
`)
})
test('--theme(--color-red-500 inline)', async () => {
expect(
await compileCss(css`
@theme {
--color-red-500: #f00;
}
.red {
color: --theme(--color-red-500 inline);
}
`),
).toMatchInlineSnapshot(`
".red {
color: red;
}"
`)
})
test('--theme(--color-red-500/50)', async () => {
expect(
await compileCss(css`
@theme {
--color-red-500: #f00;
}
.red {
color: --theme(--color-red-500/0.5);
}
`),
).toMatchInlineSnapshot(`
":root, :host {
--color-red-500: red;
}
.red {
color: #ff000080;
}
@supports (color: color-mix(in lab, red, red)) {
.red {
color: color-mix(in oklab, var(--color-red-500) 50%, transparent);
}
}"
`)
})
test('--theme(--color-red-500/50 inline)', async () => {
expect(
await compileCss(css`
@theme {
--color-red-500: #f00;
}
.red {
color: --theme(--color-red-500/50 inline);
}
`),
).toMatchInlineSnapshot(`
".red {
color: oklab(62.7955% .224863 .125846);
}"
`)
})
test('--theme(…) injects the fallback when the value it refers is set to a `--theme(…)` function with the fallback `initial`', async () => {
expect(
await compileCss(css`
@theme prefix(tw) {
--default-font-family: --theme(--font-family, initial);
}
.red {
font-family: --theme(--default-font-family, Potato Sans, sans-serif);
}
`),
).toMatchInlineSnapshot(`
".red {
font-family: var(--tw-default-font-family, Potato Sans, sans-serif);
}"
`)
})
test('--theme(…) injects the fallback when the value it refers is set to a `--theme(…)` function with the fallback `initial` in @reference mode', async () => {
expect(
await compileCss(css`
@theme reference prefix(tw) {
--default-font-family: --theme(--font-family, initial);
}
.red {
font-family: --theme(--default-font-family, Potato Sans, sans-serif);
}
`),
).toMatchInlineSnapshot(`
".red {
font-family: var(--tw-default-font-family, Potato Sans, sans-serif);
}"
`)
})
test('--theme(…) resolves with the fallback when the value it refers is set to a `--theme(… inline)` function with the fallback `initial`', async () => {
expect(
await compileCss(css`
@theme prefix(tw) {
--default-font-family: --theme(--font-family inline, initial);
}
.red {
font-family: --theme(--default-font-family inline, Potato Sans, sans-serif);
}
`),
).toMatchInlineSnapshot(`
".red {
font-family: Potato Sans, sans-serif;
}"
`)
})
test('--theme(…) resolves with the fallback when the value it refers is set to a `--theme(… inline)` function with the fallback `initial` in @reference mode', async () => {
expect(
await compileCss(css`
@theme reference prefix(tw) {
--default-font-family: --theme(--font-family inline, initial);
}
.red {
font-family: --theme(--default-font-family inline, Potato Sans, sans-serif);
}
`),
).toMatchInlineSnapshot(`
".red {
font-family: Potato Sans, sans-serif;
}"
`)
})
test('--theme(…) does not inject the fallback if the fallback is `initial`', async () => {
expect(
await compileCss(
css`
@theme prefix(tw) {
--font-sans:
ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
--default-font-family: --theme(--font-sans, initial);
}
@layer base {
html {
font-family: --theme(--default-font-family, sans-serif);
}
}
@tailwind utilities;
`,
['tw:font-sans'],
),
).toMatchInlineSnapshot(`
":root, :host {
--tw-font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--tw-default-font-family: var(--tw-font-sans);
}
@layer base {
html {
font-family: var(--tw-default-font-family, sans-serif);
}
}
.tw\\:font-sans {
font-family: var(--tw-font-sans);
}"
`)
})
test('--theme(…) forces the value to be retrieved as inline when used inside an at-rule', async () => {
expect(
await compileCss(css`
@theme {
--breakpoint-md: 48rem;
--breakpoint-lg: 64rem;
}
@custom-media --md (width >= --theme(--breakpoint-md));
@media (--md) {
.blue {
color: blue;
}
}
@media (width >= --theme(--breakpoint-lg)) {
.red {
color: red;
}
}
`),
).toMatchInlineSnapshot(`
"@media (min-width: 48rem) {
.blue {
color: #00f;
}
}
@media (min-width: 64rem) {
.red {
color: red;
}
}"
`)
})
test('--theme(…) can only be used with CSS variables from your @theme', async () => {
await expect(() =>
compileCss(css`
@theme {
--color-red-500: #f00;
}
.red {
color: --theme(colors.red.500);
}
`),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: The --theme(…) function can only be used with CSS variables from your theme.]`,
)
await expect(() =>
compileCss(css`
@theme {
--color-red-500: #f00;
}
.red {
color: --theme(--color-green-500);
}
`),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Could not resolve value for theme function: \`theme(--color-green-500)\`. Consider checking if the variable name is correct or provide a fallback value to silence this error.]`,
)
})
test('--theme(…) function still works as expected, even when a plugin is imported', async () => {
expect(
await compileCss(
css`
@layer base {
html,
:host {
font-family: --theme(--default-font-family, system-ui);
}
}
@layer theme {
@theme {
--font-sans: sans-serif;
--default-font-family: --theme(--font-sans, initial);
}
}
@plugin "my-plugin.js";
`,
[],
{
loadModule: async () => ({
path: '',
base: '/root',
module: () => {},
}),
},
),
).toMatchInlineSnapshot(`
"@layer base {
html, :host {
font-family: var(--default-font-family, system-ui);
}
}
@layer theme {
:root, :host {
--font-sans: sans-serif;
--default-font-family: var(--font-sans);
}
}"
`)
})
})
describe('theme(…)', () => {
describe('in declaration values', () => {
describe('without fallback values', () => {
test('theme(colors.red.500)', async () => {
expect(
await compileCss(css`
@theme {
--color-red-500: #f00;
}
.red {
color: theme(colors.red.500);
}
`),
).toMatchInlineSnapshot(`
".red {
color: red;
}"
`)
})
test("theme('colors.red.500')", async () => {
expect(
await compileCss(css`
@theme {
--color-red-500: #f00;
}
.red {
color: theme('colors.red.500');
}
`),
).toMatchInlineSnapshot(`
".red {
color: red;
}"
`)
})
test('theme(colors[red]500)', async () => {
expect(
await compileCss(css`
@theme {
--color-red-500: #f00;
}
.red {
color: theme(colors[red]500);
}
`),
).toMatchInlineSnapshot(`
".red {
color: red;
}"
`)
})
test('theme(colors[red].500)', async () => {
expect(
await compileCss(css`
@theme {
--color-red-500: #f00;
}
.red {
color: theme(colors[red].500);
}
`),
).toMatchInlineSnapshot(`
".red {
color: red;
}"
`)
})
test('theme(colors[red][500])', async () => {
expect(
await compileCss(css`
@theme {
--color-red-500: #f00;
}
.red {
color: theme(colors[red][500]);
}
`),
).toMatchInlineSnapshot(`
".red {
color: red;
}"
`)
})
test('theme(colors[red].[500])', async () => {
expect(
await compileCss(css`
@theme {
--color-red-500: #f00;
}
.red {
color: theme(colors[red].[500]);
}
`),
).toMatchInlineSnapshot(`
".red {
color: red;
}"
`)
})
test('theme(colors.red.500/75%)', async () => {
expect(
await compileCss(css`
@theme {
--color-red-500: #f00;
}
.red {
color: theme(colors.red.500/75%);
}
`),
).toMatchInlineSnapshot(`
".red {
color: oklab(62.7955% .224 .125 / .75);
}"
`)
})
test('theme(colors.red.500 / 75%)', async () => {
expect(
await compileCss(css`
@theme {
--color-red-500: #f00;
}
.red {
color: theme(colors.red.500 / 75%);
}
`),
).toMatchInlineSnapshot(`
".red {
color: oklab(62.7955% .224 .125 / .75);
}"
`)
})
test("theme('colors.red.500 / 75%')", async () => {
expect(
await compileCss(css`
@theme {
--color-red-500: #f00;
}
.red {
color: theme('colors.red.500 / 75%');
}
`),
).toMatchInlineSnapshot(`
".red {
color: oklab(62.7955% .224 .125 / .75);
}"
`)
})
test('theme(colors.red.500/var(--opacity))', async () => {
expect(
await compileCss(css`
@theme {
--color-red-500: #f00;
}
.red {
color: theme(colors.red.500/var(--opacity));
}
`),
).toMatchInlineSnapshot(`
".red {
color: red;
}
@supports (color: color-mix(in lab, red, red)) {
.red {
color: color-mix(in oklab, red var(--opacity), transparent);
}
}"
`)
})
test('theme(colors.red.500/var(--opacity,50%))', async () => {
expect(
await compileCss(css`
@theme {
--color-red-500: #f00;
}
.red {
color: theme(colors.red.500/var(--opacity,50%));
}
`),
).toMatchInlineSnapshot(`
".red {
color: red;
}
@supports (color: color-mix(in lab, red, red)) {
.red {
color: color-mix(in oklab, red var(--opacity, 50%), transparent);
}
}"
`)
})
test('theme(spacing.12)', async () => {
expect(
await compileCss(css`
@theme {
--spacing-12: 3rem;
}
.space-on-the-left {
margin-left: theme(spacing.12);
}
`),
).toMatchInlineSnapshot(`
".space-on-the-left {
margin-left: 3rem;
}"
`)
})
test('theme(spacing[2.5])', async () => {
expect(
await compileCss(css`
@theme {
--spacing-2_5: 0.625rem;
}
.space-on-the-left {
margin-left: theme(spacing[2.5]);
}
`),
).toMatchInlineSnapshot(`
".space-on-the-left {
margin-left: .625rem;
}"
`)
})
test('calc(100vh - theme(spacing[2.5]))', async () => {
expect(
await compileCss(css`
@theme {
--spacing-2_5: 0.625rem;
}
.space-on-the-left {
margin-left: calc(100vh - theme(spacing[2.5]));
}
`),
).toMatchInlineSnapshot(`
".space-on-the-left {
margin-left: calc(100vh - .625rem);
}"
`)
})
test('theme(borderRadius.lg)', async () => {
expect(
await compileCss(css`
@theme {
--radius-lg: 0.5rem;
}
.radius {
border-radius: theme(borderRadius.lg);
}
`),
).toMatchInlineSnapshot(`
".radius {
border-radius: .5rem;
}"
`)
})
describe('for v3 compatibility', () => {
test('theme(blur.DEFAULT)', async () => {
expect(
await compileCss(css`
@theme {
--blur: 8px;
}
.default-blur {
filter: blur(theme(blur.DEFAULT));
}
`),
).toMatchInlineSnapshot(`
".default-blur {
filter: blur(8px);
}"
`)
})
test('theme(fontSize.xs[1].lineHeight)', async () => {
expect(
await compileCss(css`
@theme {
--text-xs: 1337.75rem;
--text-xs--line-height: 1337rem;
}
.text {
font-size: theme(fontSize.xs);
line-height: theme(fontSize.xs[1].lineHeight);
}
`),
).toMatchInlineSnapshot(`
".text {
font-size: 1337.75rem;
line-height: 1337rem;
}"
`)
})
test('theme(fontFamily.sans) (css)', async () => {
expect(
await compileCss(css`
@theme default reference {
--font-family-sans:
ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji,
Segoe UI Symbol, Noto Color Emoji;
}
.fam {
font-family: theme(fontFamily.sans);
}
`),
).toMatchInlineSnapshot(`
".fam {
font-family: ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
}"
`)
})
test('theme(fontFamily.sans) (config)', async () => {
let compiled = await compile(
css`
@config "./my-config.js";
.fam {
font-family: theme(fontFamily.sans);
}
`,
{
loadModule: async () => ({
path: '',
base: '/root',
module: {},
}),
},
)
expect(optimizeCss(compiled.build([])).trim()).toMatchInlineSnapshot(`
".fam {
font-family: ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
}"
`)
})
})
test('theme(colors.unknown.500)', async () =>
await expect(() =>
compileCss(css`
.red {
color: theme(colors.unknown.500);
}
`),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Could not resolve value for theme function: \`theme(colors.unknown.500)\`. Consider checking if the path is correct or provide a fallback value to silence this error.]`,
))
})
describe('with default values', () => {
test('theme(colors.red.unknown / 50%, #f00)', async () => {
expect(
await compileCss(css`
.red {
color: theme(colors.red.unknown / 50%, #f00);
}
`),
).toMatchInlineSnapshot(`
".red {
color: red;
}"
`)
})
test('theme(colors.red.unknown / 75%, theme(colors.red.500 / 25%))', async () => {
expect(
await compileCss(css`
@theme {
--color-red-500: #f00;
}
.red {
color: theme(colors.red.unknown / 75%, theme(colors.red.500 / 25%));
}
`),
).toMatchInlineSnapshot(`
".red {
color: oklab(62.7955% .224 .125 / .25);
}"
`)
})
test('theme(fontFamily.unknown, Helvetica Neue, Helvetica, sans-serif)', async () => {
expect(
await compileCss(css`
.fam {
font-family: theme(fontFamily.unknown, Helvetica Neue, Helvetica, sans-serif);
}
`),
).toMatchInlineSnapshot(`
".fam {
font-family: Helvetica Neue, Helvetica, sans-serif;
}"
`)
})
test('theme(\n\tfontFamily.unknown,\n\tHelvetica Neue,\n\tHelvetica,\n\tsans-serif\n)', async () => {
expect(
await compileCss(css`
.fam {
font-family: theme(
fontFamily.unknown,
Helvetica Neue,
Helvetica,
sans-serif
);
}
`),
).toMatchInlineSnapshot(`
".fam {
font-family: Helvetica Neue, Helvetica, sans-serif;
}"
`)
})
})
describe('recursive theme()', () => {
test('can references theme inside @theme', async () => {
expect(
await compileCss(css`
@theme {
--color-red-500: #f00;
--color-foo: theme(colors.red.500);
}
.red {
color: theme(colors.foo);
}
`),
).toMatchInlineSnapshot(`
".red {
color: red;
}"
`)
})
test('can references theme inside @theme and stacking opacity', async () => {
expect(
await compileCss(css`
@theme {
--color-red-500: #f00;
--color-foo: theme(colors.red.500 / 50%);
}
.red {
color: theme(colors.foo / 50%);
}
`),
).toMatchInlineSnapshot(`
".red {
color: oklab(62.7955% .224 .125 / .25);
}"
`)
})
})
describe('with CSS variable syntax', () => {
test('theme(--color-red-500)', async () => {
expect(
await compileCss(css`
@theme {
--color-red-500: #f00;
}
.red {
color: theme(--color-red-500);
}
`),
).toMatchInlineSnapshot(`
".red {
color: red;
}"
`)
})
test('theme(--color-red-500 / 50%)', async () => {
expect(
await compileCss(css`
@theme {
--color-red-500: #f00;
}
.red {
color: theme(--color-red-500 / 50%);
}
`),
).toMatchInlineSnapshot(`
".red {
color: oklab(62.7955% .224 .125 / .5);
}"
`)
})
test('theme("--color-red-500")', async () => {
expect(
await compileCss(css`
@theme {
--color-red-500: #f00;
}
.red {
color: theme(--color-red-500);
}
`),
).toMatchInlineSnapshot(`
".red {
color: red;
}"
`)
})
})
describe('resolving --default lookups', () => {
test('theme(blur.DEFAULT)', async () => {
expect(
await compileCss(css`
@theme {
--blur: 8px;
}
.blur {
filter: blur(theme(blur));
}
`),
).toMatchInlineSnapshot(`
".blur {
filter: blur(8px);
}"
`)
})
})
describe('with default theme', () => {
test.each([
[
'fontFamily.sans',
'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
],
['maxWidth.xs', '20rem'],
['transitionTimingFunction.in-out', 'cubic-bezier(.4, 0, .2, 1)'],
['letterSpacing.wide', '.025em'],
['lineHeight.tight', '1.25'],
['backgroundColor.red.500', 'oklch(63.7% .237 25.331)'],
])('theme(%s) → %s', async (value, result) => {
let defaultTheme = await fs.readFile(path.join(__dirname, '..', 'theme.css'), 'utf8')
let compiled = await compileCss(css`
${defaultTheme}
.custom {
--custom-value: theme(${value});
}
`)
let startOfCustomClass = compiled.indexOf('.custom {\n')
let endOfCustomClass = compiled.indexOf('}', startOfCustomClass)
let customClassRule = compiled
.slice(startOfCustomClass + '.custom {\n'.length, endOfCustomClass)
.replace('--custom-value:', '')
.trim()
.slice(0, -1)
expect(customClassRule).toBe(result)
})
})
})
describe('in candidates', () => {
test('sm:[--color:theme(colors.red[500])]', async () => {
expect(
await compileCss(
css`
@tailwind utilities;
@theme {
--breakpoint-sm: 40rem;
--color-red-500: #f00;
}
`,
['sm:[--color:theme(colors.red[500])]'],
),
).toMatchInlineSnapshot(`
"@media (min-width: 40rem) {
.sm\\:\\[--color\\:theme\\(colors\\.red\\[500\\]\\)\\] {
--color: red;
}
}"
`)
})
test("values that don't exist don't produce candidates", async () => {
expect(
await compileCss(
css`
@tailwind utilities;
@theme reference {
--radius-sm: 2rem;
}
`,
[
'rounded-[theme(--radius-sm)]',
'rounded-[theme(i.do.not.exist)]',
'rounded-[theme(--i-do-not-exist)]',
],
),
).toMatchInlineSnapshot(`
".rounded-\\[theme\\(--radius-sm\\)\\] {
border-radius: 2rem;
}"
`)
expect(
await compileCss(
css`
@tailwind utilities;
@theme reference {
--radius-sm: 2rem;
}
`,
['rounded-[theme(i.do.not.exist)]', 'rounded-[theme(--i-do-not-exist)]'],
),
).toEqual('')
})
})
describe('in @media queries', () => {
test('@media (min-width:theme(breakpoint.md)) and (max-width: theme(--breakpoint-lg))', async () => {
expect(
await compileCss(css`
@theme {
--breakpoint-md: 48rem;
--breakpoint-lg: 64rem;
}
@media (min-width:theme(breakpoint.md)) and (max-width: theme(--breakpoint-lg)) {
.red {
color: red;
}
}
`),
).toMatchInlineSnapshot(`
"@media (min-width: 48rem) and (max-width: 64rem) {
.red {
color: red;
}
}"
`)
})
test('@media (width >= theme(breakpoint.md)) and (width<theme(--breakpoint-lg))', async () => {
expect(
await compileCss(css`
@theme {
--breakpoint-md: 48rem;
--breakpoint-lg: 64rem;
}
@media (width >= theme(breakpoint.md)) and (width<theme(--breakpoint-lg)) {
.red {
color: red;
}
}
`),
).toMatchInlineSnapshot(`
"@media (min-width: 48rem) and (not (min-width: 64rem)) {
.red {
color: red;
}
}"
`)
})
})
test('@custom-media --my-media (min-width: theme(breakpoint.md))', async () => {
expect(
await compileCss(css`
@theme {
--breakpoint-md: 48rem;
}
@custom-media --my-media (min-width: theme(breakpoint.md));
@media (--my-media) {
.red {
color: red;
}
}
`),
).toMatchInlineSnapshot(`
"@media (min-width: 48rem) {
.red {
color: red;
}
}"
`)
})
test('@container (width > theme(breakpoint.md))', async () => {
expect(
await compileCss(css`
@theme {
--breakpoint-md: 48rem;
}
@container (width > theme(breakpoint.md)) {
.red {
color: red;
}
}
`),
).toMatchInlineSnapshot(`
"@container not (max-width: 48rem) {
.red {
color: red;
}
}"
`)
})
test('@supports (text-stroke: theme(--font-size-xs))', async () => {
expect(
await compileCss(css`
@theme {
--font-size-xs: 0.75rem;
}
@supports (text-stroke: theme(--font-size-xs)) {
.red {
color: red;
}
}
`),
).toMatchInlineSnapshot(`
"@supports (text-stroke: 0.75rem) {
.red {
color: red;
}
}"
`)
})
})
describe('in plugins', () => {
test('CSS theme functions in plugins are properly evaluated', async () => {
let compiled = await compile(
css`
@layer base, utilities;
@plugin "my-plugin";
@theme reference {
--color-red: oklch(62% 0.25 30);
--color-orange: oklch(79% 0.17 70);
--color-blue: oklch(45% 0.31 264);
--color-pink: oklch(87% 0.07 7);
}
@layer utilities {
@tailwind utilities;
}
`,
{
async loadModule() {
return {
path: '',
base: '/root',
module: plugin(({ addBase, addUtilities }) => {
addBase({
'.my-base-rule': {
color: 'theme(colors.red)',
'outline-color': 'theme(colors.orange / 15%)',
'background-color': 'theme(--color-blue)',
'border-color': 'theme(--color-pink / 10%)',
},
})
addUtilities({
'.my-utility': {
color: 'theme(colors.red)',
},
})
}),
}
},
},
)
expect(optimizeCss(compiled.build(['my-utility'])).trim()).toMatchInlineSnapshot(`
"@layer base {
.my-base-rule {
color: oklch(62% .25 30);
background-color: oklch(45% .31 264);
border-color: oklab(87% .069 .008 / .1);
outline-color: oklab(79% .058 .159 / .15);
}
}
@layer utilities {
.my-utility {
color: oklch(62% .25 30);
}
}"
`)
})
})
describe('in JS config files', () => {
test('CSS theme functions in config files are properly evaluated', async () => {
let compiled = await compile(
css`
@layer base, utilities;
@config "./my-config.js";
@theme reference {
--color-red: red;
--color-orange: orange;
}
@layer utilities {
@tailwind utilities;
}
`,
{
loadModule: async () => ({
path: '',
base: '/root',
module: {
theme: {
extend: {
colors: {
primary: 'theme(colors.red)',
secondary: 'theme(--color-orange)',
},
},
},
plugins: [
plugin(({ addBase, addUtilities }) => {
addBase({
'.my-base-rule': {
background: 'theme(colors.primary)',
color: 'theme(colors.secondary)',
},
})
addUtilities({
'.my-utility': {
color: 'theme(colors.red)',
},
})
}),
],
},
}),
},
)
expect(optimizeCss(compiled.build(['my-utility'])).trim()).toMatchInlineSnapshot(`
"@layer base {
.my-base-rule {
color: orange;
background: red;
}
}
@layer utilities {
.my-utility {
color: red;
}
}"
`)
})
})
test('replaces CSS theme() function with values inside imported stylesheets', async () => {
expect(
await compileCss(
css`
@theme {
--color-red-500: #f00;
}
@import './bar.css';
`,
[],
{
async loadStylesheet() {
return {
path: '',
base: '/bar.css',
content: css`
.red {
color: theme(colors.red.500);
}
`,
}
},
},
),
).toMatchInlineSnapshot(`
".red {
color: red;
}"
`)
})
test('resolves paths ending with a 1', async () => {
expect(
await compileCss(
css`
@theme {
--spacing-1: 0.25rem;
}
.foo {
margin: theme(spacing.1);
}
`,
[],
),
).toMatchInlineSnapshot(`
".foo {
margin: .25rem;
}"
`)
})
test('upgrades to a full JS compat theme lookup if a value can not be mapped to a CSS variable', async () => {
expect(
await compileCss(
css`
.semi {
font-weight: theme(fontWeight.semibold);
}
`,
[],
),
).toMatchInlineSnapshot(`
".semi {
font-weight: 600;
}"
`)
})