tailwindcss/integrations/upgrade/js-config.test.ts
Robin Malfait e4856c9720
Make upgrade tooling more stable (#19846)
This PR is an attempt to make the upgrade tooling more stable.


### TL;DR

1. When migrating from Tailwind CSS v3 → Tailwind CSS v4, only migrate
files listed in the `config.content` instead of relying on v4's auto
content detection feature
2. Skip writing files that have not been changed
3. Write changed files in a safe way: first write to a temporary file,
then rename the file atomically
4. Never migrate files that are git ignored, even if they are listed in
the `config.content` file
5. Always ignore `.env` and `.env.*` files when scanning for files. Most
people will have this in their `.gitignore` file, but if not, then this
is a fallback mechanism.

---

Looking at the #18972 issue, it looks like some people are running into
weird situations where some of the contents is just gone.

I have never been able to reproduce this on my own devices and in my own
projects unfortunately. But there is definitely _something_ happening
that's not right that people are running into.

Therefore, this PR is an attempt to fix what I think _might_ be wrong,
but I'm not 100% sure if these fixes are enough, or if something else is
still happening here.

This builds on top of the #19779 PR which has some small fixes, but is
incomplete to make this work.

### What's happening

Looking at some of the comments, it looks like a few things are
happening such as:

1. The upgrade tool is emptying out my files — it looks like these are
only happening if you ctrl+c while the process is taking a while. It
could be that a lot of files are being checked and therefore the tooling
looks like its stuck.
6. The upgrade tool is looking at files it shouldn't look at — in
Tailwind CSS v4 we have this concept of the auto-content detection. This
means that we will look at any plain text file that is not git ignored.

### Fixes


#### Emptying out files

The files being emptied looks like it's because how `fs.writeFile`
behaves by default. It opens the file handle with the `w` flag, which
will first truncate the file before writing the new contents. This is
not a single atomic operation, so a killed process in the middle will
cause invalid state.

When we migrate your template files, everything is happening in promises
to migrate things at the same time. When a lot of files are being
scanned, truncating might have happened already before we write the new
content. Since we migrate a bunch of files in parallel, a ctrl+c could
cause data loss in multiple files.

To mitigate this, I switched to an alternative way of writing files. 

1. First, we do some quick checks where if the contents didn't change we
just bail out immediately. Files that don't include Tailwind CSS classes
won't change, and therefore we don't need to override these files with
the same contents.
2. When the migrated contents is empty, we bail out as well. I'm 100%
sure that this is not the spot where the "emptying out" happens, I still
believe it happens in the `writeFile` itself, but added it just in case.
7. Next, I introduced a safe write, where we first write to a temporary
file in the same folder. We could write it to `/tmp`, but then we can't
guarantee that we are on the same file system.
   
If we ctrl+c at this stage, then the worst case scenario is that you
have additional temporary files in your project, but your original files
are still there.
   
Once that file was written, we will use the atomic `fs.rename`. This
should be atomic as long as we are on the same file system, so either
the rename didn't happen yet, or it completed.

I added an integration test for this, but I had to change the
`writeFile` implementation slightly. In the test, we will truncate the
file first, after that we will write the new contents. This is so that
we have enough time to kill the current process and allows us to verify
that we didn't clear out the file. Again, this is a hacky way of testing
this, just because I can't reproduce this issue myself, let alone
reproduce it reliable in a CI environment.

Note: we are also using `realpath` to make sure that we are updating the
real file. Otherwise, if we were dealing with a symlinked file, we would
override the symlink with a "hard" copy instead.

#### Touching files that should not be touched

During the migration, we rely on the Tailwind CSS v4 auto detection
logic which means that it will scan any plain text file that is not git
ignored. Therefore changes to php files could happen because in theory
they could contain Tailwind CSS classes.

To solve this, when migrating from Tailwind CSS v3 to Tailwind CSS v4,
we will _only_ take the sources into account that were listed in the
`config.content` array. Since this was a requirement in Tailwind CSS v3,
it should be safe to rely on this array.

Additionally, this will make sure that we are dealing with way fewer
files to migrate as well.

On top of that, files that match the patterns in the content array that
are git ignored will also be skipped. This is to prevent that we mutate
files in `node_modules` for example.

In one of the comments I read that `.env` files were emptied out. In
most cases people will have these files gitignored but I explicitly
added `.env` and `.env.*` as files to never ever touch by default when
scanning.

Last but not least, this also updates the output a little bit of the
upgrade tool in case we skip content files (because of git ignore) and
if we changed a file.

<img width="1122" height="1376" alt="image"
src="https://github.com/user-attachments/assets/318fdbbf-e319-4c7e-9648-ee9283842624"
/>

- "Git ignored folder, skipping: `./node_modules`": this is because the
content array looks like this while the `node_modules` are being
ignored:
<img width="1090" height="398" alt="image"
src="https://github.com/user-attachments/assets/7d694720-5671-47ec-bb2e-f24c5f2c4248"
/>

- "Migrated
`./resources/views/vendor/filament-panels/components/logo.blade.php`":
this is because **I** made a change to showcase this feature.

Fixes: #18972
Closes: #19779

### Test plan

1. Existing tests still pass
1. Added a dedicated integration test to ensure that we only take
`config.content` into account when migrating from Tailwind CSS v3 to
Tailwind CSS v4 projects.
1. Added a dedicated integration test to make sure that files listed in
`config.content` that are also git ignored, will still be skipped.
1. Added a dedicated integration test to ensure that when `writeFile` is
cancelled mid-write that our old files are still present.
1. Added a dedicated integration test to ensure that we ignore `.env`
and `.env.*` files even if you didn't git ignore them.

[ci-all] To verify on Windows

---------

Co-authored-by: Sami <sychocouldy@gmail.com>
2026-03-24 17:24:12 +01:00

2187 lines
57 KiB
TypeScript

import path from 'node:path'
import { describe } from 'vitest'
import { css, html, json, test, ts, txt } from '../utils'
test(
`upgrade JS config files with flat theme values, darkMode, and content fields`,
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'
module.exports = {
darkMode: 'selector',
content: ['./src/**/*.{html,js}', './node_modules/my-external-lib/**/*.{html}'],
theme: {
boxShadow: {
sm: '0 2px 6px rgb(15 23 42 / 0.08)',
},
colors: {
red: {
400: '#f87171',
500: 'red',
},
superRed: '#ff0000',
steel: 'rgb(70 130 180 / <alpha-value>)',
smoke: 'rgba(245, 245, 245, var(--smoke-alpha, <alpha-value>))',
},
ringColor: {
DEFAULT: '#c0ffee',
},
opacity: {
superOpaque: '0.95',
},
fontSize: {
xs: ['0.75rem', { lineHeight: '1rem' }],
sm: ['0.875rem', { lineHeight: '1.5rem' }],
base: ['1rem', { lineHeight: '2rem' }],
lg: ['1.125rem', '2.5rem'],
xl: ['1.5rem', '3rem', 'invalid'],
'2xl': ['2rem'],
},
width: {
px: '1px',
auto: 'auto',
1: '0.25rem',
1.5: '0.375rem',
2: '0.5rem',
2.5: '0.625rem',
3: '0.75rem',
3.5: '0.875rem',
4: '1rem',
5: '1.25rem',
6: '1.5rem',
8: '2rem',
10: '2.5rem',
11: '2.75rem',
12: '3rem',
16: '4rem',
24: '6rem',
32: '8rem',
40: '10rem',
48: '12rem',
64: '16rem',
80: '20rem',
96: '24rem',
128: '32rem',
full: '100%',
0: '0%',
'1/2': '50%',
'1/3': 'calc(100% / 3)',
'2/3': 'calc(100% / 3 * 2)',
'1/4': '25%',
'3/4': '75%',
'1/5': '20%',
'2/5': '40%',
'3/5': '60%',
'4/5': '80%',
'1/6': 'calc(100% / 6)',
'5/6': 'calc(100% / 6 * 5)',
'1/7': 'calc(100% / 7)',
'1/10': 'calc(100% / 10)',
'3/10': 'calc(100% / 10 * 3)',
'7/10': 'calc(100% / 10 * 7)',
'9/10': 'calc(100% / 10 * 9)',
screen: '100vw',
'full-minus-80': 'calc(100% - 20rem)',
'full-minus-96': 'calc(100% - 24rem)',
'225px': '225px',
},
extend: {
colors: {
red: {
500: '#ef4444',
600: '#dc2626',
},
},
fontFamily: {
sans: 'Inter, system-ui, sans-serif',
display: ['Cabinet Grotesk', ...defaultTheme.fontFamily.sans],
},
borderRadius: {
'4xl': '2rem',
},
keyframes: {
'spin-clockwise': {
'0%': { transform: 'rotate(0deg)' },
'100%': { transform: 'rotate(360deg)' },
},
'spin-counterclockwise': {
'0%': { transform: 'rotate(0deg)' },
'100%': { transform: 'rotate(-360deg)' },
},
},
animation: {
'spin-clockwise': 'spin-clockwise 1s linear infinite',
'spin-counterclockwise': 'spin-counterclockwise 1s linear infinite',
},
letterSpacing: {
superWide: '0.25em',
},
lineHeight: {
superLoose: '3',
},
},
},
plugins: [],
} satisfies Config
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
// prettier-ignore
'src/test.js': ts`
export default {
'shouldNotMigrate': !border.test + '',
'filter': 'drop-shadow(0 0 0.5rem #000)',
}
`,
'src/index.html': html`
<div
class="[letter-spacing:theme(letterSpacing.superWide)] [line-height:theme(lineHeight.superLoose)]"
></div>
<div class="text-superRed/superOpaque leading-superLoose"></div>
`,
'node_modules/my-external-lib/src/template.html': html`
<div class="text-red-500">
Hello world!
</div>
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.{css,js,html}')).toMatchInlineSnapshot(`
"
--- src/index.html ---
<div
class="tracking-superWide leading-superLoose"
></div>
<div class="text-superRed/superOpaque leading-superLoose"></div>
--- src/input.css ---
@import 'tailwindcss';
@source '../node_modules/my-external-lib/**/*.{html}';
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--shadow-*: initial;
--shadow-sm: 0 2px 6px rgb(15 23 42 / 0.08);
--color-*: initial;
--color-red-400: #f87171;
--color-red-500: #ef4444;
--color-red-600: #dc2626;
--color-superRed: #ff0000;
--color-steel: rgb(70 130 180);
--color-smoke: rgba(245, 245, 245, var(--smoke-alpha, 1));
--default-ring-color: #c0ffee;
--opacity-*: initial;
--opacity-superOpaque: 95%;
--text-*: initial;
--text-xs: 0.75rem;
--text-xs--line-height: 1rem;
--text-sm: 0.875rem;
--text-sm--line-height: 1.5rem;
--text-base: 1rem;
--text-base--line-height: 2rem;
--text-lg: 1.125rem;
--text-lg--line-height: 2.5rem;
--text-xl: 1.5rem;
--text-xl--line-height: 3rem;
--text-2xl: 2rem;
--width-*: initial;
--width-0: 0%;
--width-1: 0.25rem;
--width-2: 0.5rem;
--width-3: 0.75rem;
--width-4: 1rem;
--width-5: 1.25rem;
--width-6: 1.5rem;
--width-8: 2rem;
--width-10: 2.5rem;
--width-11: 2.75rem;
--width-12: 3rem;
--width-16: 4rem;
--width-24: 6rem;
--width-32: 8rem;
--width-40: 10rem;
--width-48: 12rem;
--width-64: 16rem;
--width-80: 20rem;
--width-96: 24rem;
--width-128: 32rem;
--width-px: 1px;
--width-auto: auto;
--width-1_5: 0.375rem;
--width-2_5: 0.625rem;
--width-3_5: 0.875rem;
--width-full: 100%;
--width-1\\/2: 50%;
--width-1\\/3: calc(100% / 3);
--width-2\\/3: calc(100% / 3 * 2);
--width-1\\/4: 25%;
--width-3\\/4: 75%;
--width-1\\/5: 20%;
--width-2\\/5: 40%;
--width-3\\/5: 60%;
--width-4\\/5: 80%;
--width-1\\/6: calc(100% / 6);
--width-5\\/6: calc(100% / 6 * 5);
--width-1\\/7: calc(100% / 7);
--width-1\\/10: calc(100% / 10);
--width-3\\/10: calc(100% / 10 * 3);
--width-7\\/10: calc(100% / 10 * 7);
--width-9\\/10: calc(100% / 10 * 9);
--width-screen: 100vw;
--width-full-minus-80: calc(100% - 20rem);
--width-full-minus-96: calc(100% - 24rem);
--width-225px: 225px;
--font-sans: Inter, system-ui, sans-serif;
--font-display:
Cabinet Grotesk, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--radius-4xl: 2rem;
--animate-spin-clockwise: spin-clockwise 1s linear infinite;
--animate-spin-counterclockwise: spin-counterclockwise 1s linear infinite;
--tracking-superWide: 0.25em;
--leading-superLoose: 3;
@keyframes spin-clockwise {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes spin-counterclockwise {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(-360deg);
}
}
}
/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
--- src/test.js ---
export default {
'shouldNotMigrate': !border.test + '',
'filter': 'drop-shadow(0 0 0.5rem #000)',
}
"
`)
expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toBe('')
},
)
test(
'skips gitignored template files even when they are explicitly referenced in `content`',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'.gitignore': txt`
node_modules
src/ignored
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
module.exports = {
content: [
'./src/migrate-me.html',
'./src/ignored/do-not-migrate-me.html',
'./node_modules/my-external-lib/template.html',
],
theme: {},
plugins: [],
} satisfies Config
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
'src/migrate-me.html': html`
<div class="order-[0]"></div>
`,
'src/ignored/do-not-migrate-me.html': html`
<div class="order-[0]"></div>
`,
'node_modules/my-external-lib/template.html': html`
<div
class="order-[0]"
></div>
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(
await fs.dumpFiles(
'{src/**/*.{css,html},node_modules/my-external-lib/template.html,.gitignore}',
),
).toMatchInlineSnapshot(`
"
--- .gitignore ---
node_modules
src/ignored
--- src/input.css ---
@import 'tailwindcss';
@source './ignored/do-not-migrate-me.html';
@source '../node_modules/my-external-lib/template.html';
/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
--- src/migrate-me.html ---
<div class="order-0"></div>
--- node_modules/my-external-lib/template.html ---
<div
class="order-[0]"
></div>
--- src/ignored/do-not-migrate-me.html ---
<div class="order-[0]"></div>
"
`)
},
)
test(
'upgrades JS config files with plugins',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
import typography from '@tailwindcss/typography'
import customPlugin from './custom-plugin'
export default {
darkMode: [
'variant',
[
'@media not print { .dark & }',
'@media not eink { .dark & }',
'&:where(.dark, .dark *)',
],
],
plugins: [
typography,
customPlugin({
'is-null': null,
'is-true': true,
'is-false': false,
'is-int': 1234567,
'is-float': 1.35,
'is-sci': 1.35e-5,
'is-str-null': 'null',
'is-str-true': 'true',
'is-str-false': 'false',
'is-str-int': '1234567',
'is-str-float': '1.35',
'is-str-sci': '1.35e-5',
'is-arr': ['foo', 'bar'],
'is-arr-mixed': [null, true, false, 1234567, 1.35, 'foo', 'bar', 'true'],
}),
],
} satisfies Config
`,
'custom-plugin.js': ts`
import plugin from 'tailwindcss/plugin'
export default plugin.withOptions((_options) => ({ addVariant }) => {
addVariant('inverted', '@media (inverted-colors: inverted)')
addVariant('hocus', ['&:focus', '&:hover'])
})
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
"
--- src/input.css ---
@import 'tailwindcss';
@plugin '@tailwindcss/typography';
@plugin '../custom-plugin' {
is-null: null;
is-true: true;
is-false: false;
is-int: 1234567;
is-float: 1.35;
is-sci: 0.0000135;
is-str-null: 'null';
is-str-true: 'true';
is-str-false: 'false';
is-str-int: '1234567';
is-str-float: '1.35';
is-str-sci: '1.35e-5';
is-arr: 'foo', 'bar';
is-arr-mixed: null, true, false, 1234567, 1.35, 'foo', 'bar', 'true';
}
@custom-variant dark {
@media not print {
.dark & {
@slot;
}
}
@media not eink {
.dark & {
@slot;
}
}
&:where(.dark, .dark *) {
@slot;
}
}
/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
"
`)
expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(`
"
"
`)
},
)
test(
'upgrades JS config files with functions in the theme config',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
export default {
theme: {
extend: {
colors: ({ colors }) => ({
gray: colors.neutral,
}),
},
},
} satisfies Config
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.{css,ts}')).toMatchInlineSnapshot(`
"
--- src/input.css ---
@import 'tailwindcss';
@theme {
--color-gray-50: oklch(98.5% 0 0);
--color-gray-100: oklch(97% 0 0);
--color-gray-200: oklch(92.2% 0 0);
--color-gray-300: oklch(87% 0 0);
--color-gray-400: oklch(70.8% 0 0);
--color-gray-500: oklch(55.6% 0 0);
--color-gray-600: oklch(43.9% 0 0);
--color-gray-700: oklch(37.1% 0 0);
--color-gray-800: oklch(26.9% 0 0);
--color-gray-900: oklch(20.5% 0 0);
--color-gray-950: oklch(14.5% 0 0);
}
/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
"
`)
expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(`
"
"
`)
},
)
test(
'does not upgrade JS config files with theme keys contributed to by plugins in the theme config',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
export default {
theme: {
typography: {
DEFAULT: {
css: {
'--tw-prose-body': 'red',
color: 'var(--tw-prose-body)',
},
},
},
},
} satisfies Config
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
@config '../tailwind.config.ts';
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
"
--- src/input.css ---
@import 'tailwindcss';
@config '../tailwind.config.ts';
/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
"
`)
expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(`
"
--- tailwind.config.ts ---
import { type Config } from 'tailwindcss'
export default {
theme: {
typography: {
DEFAULT: {
css: {
'--tw-prose-body': 'red',
color: 'var(--tw-prose-body)',
},
},
},
},
} satisfies Config
"
`)
},
)
test(
`does not upgrade JS config files with inline plugins`,
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
export default {
plugins: [function complexConfig() {}],
} satisfies Config
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
"
--- src/input.css ---
@import 'tailwindcss';
@config '../tailwind.config.ts';
/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
"
`)
expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(`
"
--- tailwind.config.ts ---
import { type Config } from 'tailwindcss'
export default {
plugins: [function complexConfig() {}],
} satisfies Config
"
`)
},
)
test(
`does not upgrade JS config files with non-simple screens object`,
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
export default {
theme: {
screens: {
xl: { min: '1024px', max: '1279px' },
tall: { raw: '(min-height: 800px)' },
},
},
} satisfies Config
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
"
--- src/input.css ---
@import 'tailwindcss';
@config '../tailwind.config.ts';
/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
"
`)
expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(`
"
--- tailwind.config.ts ---
import { type Config } from 'tailwindcss'
export default {
theme: {
screens: {
xl: { min: '1024px', max: '1279px' },
tall: { raw: '(min-height: 800px)' },
},
},
} satisfies Config
"
`)
},
)
test(
'multi-root project',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
// Project A
'project-a/tailwind.config.ts': ts`
export default {
content: {
relative: true,
files: ['./src/**/*.html'],
},
theme: {
extend: {
colors: {
primary: 'red',
},
},
},
}
`,
'project-a/src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
@config "../tailwind.config.ts";
`,
'project-a/src/index.html': html`<div class="!text-primary"></div>`,
// Project B
'project-b/tailwind.config.ts': ts`
export default {
content: {
relative: true,
files: ['./src/**/*.html'],
},
theme: {
extend: {
colors: {
primary: 'blue',
},
},
},
}
`,
'project-b/src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
@config "../tailwind.config.ts";
`,
'project-b/src/index.html': html`<div class="!text-primary"></div>`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('project-{a,b}/**/*.{css,ts}')).toMatchInlineSnapshot(`
"
--- project-a/src/input.css ---
@import 'tailwindcss';
@theme {
--color-primary: red;
}
/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
--- project-b/src/input.css ---
@import 'tailwindcss';
@theme {
--color-primary: blue;
}
/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
"
`)
},
)
test(
'migrate sources when pointing to folders outside the project root',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'frontend/tailwind.config.ts': ts`
export default {
content: {
relative: true,
files: ['./src/**/*.html', '../backend/mails/**/*.blade.php'],
},
theme: {
extend: {
colors: {
primary: 'red',
},
},
},
}
`,
'frontend/src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
@config "../tailwind.config.ts";
`,
'frontend/src/index.html': html`<div class="!text-primary"></div>`,
'backend/mails/welcome.blade.php': html`<div class="!text-primary"></div>`,
},
},
async ({ root, exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade', {
cwd: path.join(root, 'frontend'),
})
expect(await fs.dumpFiles('frontend/**/*.css')).toMatchInlineSnapshot(`
"
--- frontend/src/input.css ---
@import 'tailwindcss';
@source '../../backend/mails/**/*.blade.php';
@theme {
--color-primary: red;
}
/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
"
`)
},
)
test(
'migrate aria theme keys to custom variants',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
export default {
content: {
relative: true,
files: ['./src/**/*.html'],
},
theme: {
extend: {
aria: {
// Built-in (not really, but visible because of intellisense)
busy: 'busy="true"',
// Automatically handled by bare values
foo: 'foo="true"',
// Quotes are optional in CSS for these kinds of attribute
// selectors
bar: 'bar=true',
// Not automatically handled by bare values because names differ
baz: 'qux="true"',
// Completely custom
asc: 'sort="ascending"',
desc: 'sort="descending"',
},
},
},
}
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/*.css')).toMatchInlineSnapshot(`
"
--- src/input.css ---
@import 'tailwindcss';
@custom-variant aria-baz (&[aria-qux="true"]);
@custom-variant aria-asc (&[aria-sort="ascending"]);
@custom-variant aria-desc (&[aria-sort="descending"]);
/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
"
`)
},
)
test(
'migrate data theme keys to custom variants',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
export default {
content: {
relative: true,
files: ['./src/**/*.html'],
},
theme: {
extend: {
data: {
// Automatically handled by bare values
foo: 'foo',
// Not automatically handled by bare values because names differ
bar: 'baz',
// Custom
checked: 'ui~="checked"',
},
},
},
}
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/*.css')).toMatchInlineSnapshot(`
"
--- src/input.css ---
@import 'tailwindcss';
@custom-variant data-bar (&[data-baz]);
@custom-variant data-checked (&[data-ui~="checked"]);
/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
"
`)
},
)
test(
'migrate supports theme keys to custom variants',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
export default {
content: {
relative: true,
files: ['./src/**/*.html'],
},
theme: {
extend: {
supports: {
// Automatically handled by bare values (using CSS variable as the value)
foo: 'foo: var(--foo)', // parentheses are optional
bar: '(bar: var(--bar))',
// Not automatically handled by bare values because names differ
foo: 'bar: var(--foo)', // parentheses are optional
bar: '(qux: var(--bar))',
// Custom
grid: 'display: grid',
},
},
},
}
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/*.css')).toMatchInlineSnapshot(`
"
--- src/input.css ---
@import 'tailwindcss';
@custom-variant supports-foo {
@supports (bar: var(--foo)) {
@slot;
}
}
@custom-variant supports-bar {
@supports ((qux: var(--bar))) {
@slot;
}
}
@custom-variant supports-grid {
@supports (display: grid) {
@slot;
}
}
/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
"
`)
},
)
describe('border compatibility', () => {
test(
'migrate border compatibility',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
// Empty / default config
export default {
theme: {
extend: {},
},
} satisfies Config
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
"
--- src/input.css ---
@import 'tailwindcss';
/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
"
`)
},
)
test(
'migrate border compatibility if a custom border color is used',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
export default {
theme: {
extend: {
borderColor: ({ colors }) => ({
DEFAULT: colors.blue[500],
}),
},
},
} satisfies Config
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
"
--- src/input.css ---
@import 'tailwindcss';
/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: oklch(62.3% 0.214 259.815);
}
}
"
`)
},
)
test(
'migrate border compatibility if a custom border color is used, that matches the default v4 border color',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
export default {
theme: {
extend: {
borderColor: ({ colors }) => ({
DEFAULT: 'currentcolor',
}),
},
},
} satisfies Config
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
"
--- src/input.css ---
@import 'tailwindcss';
"
`)
},
)
test(
'migrate border compatibility in the file that uses the `@import "tailwindcss"` import',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
export default {
theme: {},
} satisfies Config
`,
'src/input.css': css`@import './tailwind.css';`,
'src/tailwind.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
"
--- src/input.css ---
@import './tailwind.css';
--- src/tailwind.css ---
@import 'tailwindcss';
/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
"
`)
},
)
test(
'migrate border compatibility in the file that uses the `@import "tailwindcss/preflight"` import',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
export default {
theme: {},
} satisfies Config
`,
'src/input.css': css`
@import './base.css';
@import './my-base.css';
@import './utilities.css';
`,
'src/base.css': css`@tailwind base;`,
'src/utilities.css': css`
@tailwind components;
@tailwind utilities;
`,
'src/my-base.css': css`
@layer base {
html {
color: black;
}
}
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
"
--- src/base.css ---
@import 'tailwindcss/theme' layer(theme);
@import 'tailwindcss/preflight' layer(base);
/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
--- src/input.css ---
@import './base.css';
@import './my-base.css';
@import './utilities.css';
--- src/my-base.css ---
@layer base {
html {
color: black;
}
}
--- src/utilities.css ---
@import 'tailwindcss/utilities' layer(utilities);
"
`)
},
)
test(
'migrates extended spacing keys',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
export default {
content: ['./src/**/*.html'],
theme: {
extend: {
spacing: {
2: '0.5rem',
4.5: '1.125rem',
5.5: '1.375em', // Units are different from --spacing scale
13: '3.25rem',
100: '100px',
miami: '1337px',
},
},
},
} satisfies Config
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
.container {
width: theme(spacing.2);
width: theme(spacing[4.5]);
width: theme(spacing[5.5]);
width: theme(spacing[13]);
width: theme(spacing[100]);
width: theme(spacing.miami);
}
`,
'src/index.html': html`
<div
class="[width:theme(spacing.2)]
[width:theme(spacing[4.5])]
[width:theme(spacing[5.5])]
[width:theme(spacing[13])]
[width:theme(spacing[100])]
[width:theme(spacing.miami)]"
></div>
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(`
"
--- src/index.html ---
<div
class="w-2
w-4.5
w-5.5
w-13
w-100
w-miami"
></div>
--- src/input.css ---
@import 'tailwindcss';
@theme {
--spacing-100: 100px;
--spacing-5_5: 1.375em;
--spacing-miami: 1337px;
}
/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
.container {
width: --spacing(2);
width: --spacing(4.5);
width: var(--spacing-5_5);
width: --spacing(13);
width: var(--spacing-100);
width: var(--spacing-miami);
}
"
`)
},
)
test(
'retains overwriting spacing scale',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
export default {
content: ['./src/**/*.html'],
theme: {
spacing: {
2: '0.5rem',
4.5: '1.125rem',
5.5: '1.375em',
13: '3.25rem',
100: '100px',
miami: '1337px',
},
},
} satisfies Config
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
.container {
width: theme(spacing.2);
width: theme(spacing[4.5]);
width: theme(spacing[5.5]);
width: theme(spacing[13]);
width: theme(spacing[100]);
width: theme(spacing.miami);
}
`,
'src/index.html': html`
<div
class="[width:theme(spacing.2)]
[width:theme(spacing[4.5])]
[width:theme(spacing[5.5])]
[width:theme(spacing[13])]
[width:theme(spacing[100])]
[width:theme(spacing.miami)]"
></div>
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(`
"
--- src/index.html ---
<div
class="w-2
w-4.5
w-5.5
w-13
w-100
w-miami"
></div>
--- src/input.css ---
@import 'tailwindcss';
@theme {
--spacing-*: initial;
--spacing-2: 0.5rem;
--spacing-13: 3.25rem;
--spacing-100: 100px;
--spacing-4_5: 1.125rem;
--spacing-5_5: 1.375em;
--spacing-miami: 1337px;
}
/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
.container {
width: var(--spacing-2);
width: var(--spacing-4_5);
width: var(--spacing-5_5);
width: var(--spacing-13);
width: var(--spacing-100);
width: var(--spacing-miami);
}
"
`)
},
)
test(
'migrates `container` component configurations',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
export default {
content: ['./src/**/*.html'],
theme: {
container: {
center: true,
padding: {
DEFAULT: '2rem',
'2xl': '4rem',
},
screens: {
md: '48rem', // Matches a default --breakpoint
xl: '1280px',
'2xl': '1536px',
},
},
},
} satisfies Config
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
'src/index.html': html`
<div class="container"></div>
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(`
"
--- src/index.html ---
<div class="container"></div>
--- src/input.css ---
@import 'tailwindcss';
@utility container {
margin-inline: auto;
padding-inline: 2rem;
@media (width >= --theme(--breakpoint-sm)) {
max-width: none;
}
@media (width >= 48rem) {
max-width: 48rem;
}
@media (width >= 1280px) {
max-width: 1280px;
}
@media (width >= 1536px) {
max-width: 1536px;
padding-inline: 4rem;
}
}
/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
"
`)
},
)
})
test(
`future and experimental keys are supported`,
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'
module.exports = {
darkMode: 'selector',
content: ['./src/**/*.{html,js}'],
future: {
hoverOnlyWhenSupported: true,
respectDefaultRingColorOpacity: true,
disableColorOpacityUtilitiesByDefault: true,
relativeContentPathsByDefault: true,
},
experimental: {
generalizedModifiers: true,
},
theme: {
colors: {
red: {
400: '#f87171',
500: 'red',
},
},
},
plugins: [],
} satisfies Config
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
"
--- src/input.css ---
@import 'tailwindcss';
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-*: initial;
--color-red-400: #f87171;
--color-red-500: red;
}
/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
"
`)
expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toBe('')
},
)
test(
`unknown future keys dont migrate the config`,
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'
module.exports = {
darkMode: 'selector',
content: ['./src/**/*.{html,js}'],
future: {
something: true,
},
} satisfies Config
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
"
--- src/input.css ---
@import 'tailwindcss';
@config '../tailwind.config.ts';
/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
"
`)
expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toMatchInlineSnapshot(`
"--- tailwind.config.ts ---
import { type Config } from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'
module.exports = {
darkMode: 'selector',
content: ['./src/**/*.{html,js}'],
future: {
something: true,
},
} satisfies Config"
`)
},
)
test(
`unknown experimental keys dont migrate the config`,
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.ts': ts`
import { type Config } from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'
module.exports = {
darkMode: 'selector',
content: ['./src/**/*.{html,js}'],
experimental: {
something: true,
},
} satisfies Config
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
"
--- src/input.css ---
@import 'tailwindcss';
@config '../tailwind.config.ts';
/*
The default border color has changed to \`currentcolor\` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
"
`)
expect((await fs.dumpFiles('tailwind.config.ts')).trim()).toMatchInlineSnapshot(`
"--- tailwind.config.ts ---
import { type Config } from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'
module.exports = {
darkMode: 'selector',
content: ['./src/**/*.{html,js}'],
experimental: {
something: true,
},
} satisfies Config"
`)
},
)