Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Canonicalization: collapse `overflow-{x,y}-*` into `overflow-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842))
- Canonicalization: collapse `overscroll-{x,y}-*` into `overscroll-*` ([#19842](https://github.com/tailwindlabs/tailwindcss/pull/19842))
- Read from `--placeholder-color` instead of `--background-color` for `placeholder-*` utilities ([#19843](https://github.com/tailwindlabs/tailwindcss/pull/19843))
- Upgrade: Ensure files are not emptied out when killing the upgrade process while it's running ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))
- Upgrade: Use `config.content` when migrating from Tailwind CSS v3 to Tailwind CSS v4 ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))
- Upgrade: Never migrate files that are ignored by git ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))
- Add `.env` and `.env.*` to default ignored content files ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))

## [4.2.2] - 2026-03-18

Expand Down
2 changes: 2 additions & 0 deletions crates/oxide/src/scanner/fixtures/ignored-files.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ package-lock.json
pnpm-lock.yaml
bun.lockb
.gitignore
.env
.env.*
244 changes: 243 additions & 1 deletion integrations/upgrade/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from 'node:path'
import { isRepoDirty } from '../../packages/@tailwindcss-upgrade/src/utils/git'
import { candidate, css, html, js, json, test, ts, yaml } from '../utils'
import { candidate, css, html, js, json, test, ts, txt, yaml } from '../utils'

test(
'error when no CSS file with @tailwind is used',
Expand Down Expand Up @@ -171,6 +171,56 @@ test(
},
)

test(
'only migrates files matched by `config.content` when upgrading from v3 to v4',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.js': js`
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.html'],
}
`,
'src/index.html': html`
<div class="order-[0] bg-[--my-red]"></div>
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
'templates/email.php': html`
<div class="order-[0] bg-[--my-red]"></div>
`,
'notes/unrelated.txt': `order-[0] bg-[--my-red]`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')

expect(await fs.dumpFiles('./**/*.{html,php,txt}')).toMatchInlineSnapshot(`
"
--- notes/unrelated.txt ---
order-[0] bg-[--my-red]

--- src/index.html ---
<div class="order-0 bg-(--my-red)"></div>

--- templates/email.php ---
<div class="order-[0] bg-[--my-red]"></div>
"
`)
},
)

test(
`upgrades a v3 project with prefixes to v4`,
{
Expand Down Expand Up @@ -2972,6 +3022,198 @@ test(
},
)

test(
'v4 ignores .env files during template migration',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^4",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'src/app.css': css`@import 'tailwindcss';`,
'src/index.html': html`
<div class="order-[0]"></div>
`,
'src/.env': `TW_TEST_CLASS=order-[0]`,
'src/.env.production': `TW_TEST_CLASS=order-[0]`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')

expect(await fs.dumpFiles('./src/**/{*,.env,.env.*}')).toMatchInlineSnapshot(`
"
--- ./src/index.html ---
<div class="order-0"></div>

--- ./src/.env ---
TW_TEST_CLASS=order-[0]

--- ./src/.env.production ---
TW_TEST_CLASS=order-[0]

--- ./src/app.css ---
@import 'tailwindcss';
"
`)
},
)

test(
'v4 linked configs respect `content` and still ignore gitignored files',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^4",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.js': js`
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/*.html', './src/*.less'],
}
`,
'src/app.css': css`
@import 'tailwindcss';
@config '../tailwind.config.js';
`,

// Ignore all .html files
'src/.gitignore': txt`
*.html
`,

// HTML files are in .gitignore, even though they are explicitly mentioned
// in the `content` array. Still ignore them
'src/do-not-migrate-me.html': html`
<div class="order-[0]"></div>
`,

// Should be picked up by auto-content detection
'templates/migrate-me.php': html`
<div class="order-[0]"></div>
`,

// Does not get picked up by auto content detection (because it's a less
// file), but was explicitly listed in the `content` array.
//
// A bit of a hacky way, I admit, but it allows us to differentiate
// between git ignored files, auto content detection and explicitly listed
// files.
'src/migrate-me.less': html`
<div class="order-[0]"></div>
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')

expect(await fs.dumpFiles('./{src,templates}/**/*')).toMatchInlineSnapshot(`
"
--- ./src/app.css ---
@import 'tailwindcss';
@config '../tailwind.config.js';

--- ./src/do-not-migrate-me.html ---
<div class="order-[0]"></div>

--- ./src/migrate-me.less ---
<div class="order-0"></div>

--- ./templates/migrate-me.php ---
<div class="order-0"></div>
"
`)
},
)

test(
'interrupting template migration does not truncate files',
{
timeout: 180_000,
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^4",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'src/app.css': css` @import 'tailwindcss'; `,
'src/index.html': html`
<div class="order-[0]"></div>
`,
'hook.cjs': js`
let fs = require('node:fs/promises')
let path = require('node:path')
let originalWriteFile = fs.writeFile.bind(fs)

fs.writeFile = async (file, contents, ...rest) => {
// Mimic a bad write
await originalWriteFile(file, '') // As-if we truncated first
console.error('__TRUNCATED_TARGET__')
await new Promise((r) => setTimeout(r, 50)) // Wait 50ms to allow us to kill the process
await originalWriteFile(file, contents, ...rest) // Write the actual contents
}
`,
'src/keep.php': `
<?php

return [
'keep' => 'this file should never be truncated',
];
`,
},
},
async ({ spawn, fs, expect }) => {
let repeatedCandidates = Array.from(
{ length: 250 },
() => '<div class="order-[0]"></div>',
).join('\n')

for (let i = 0; i < 100; i++) {
await fs.write(
`src/templates/template-${i}.php`,
`<?php\n\n${repeatedCandidates}\n\nreturn ['template' => ${i}];\n`,
)
}

let originalKeepFile = await fs.read('src/keep.php')
let originalTemplate = await fs.read('src/templates/template-0.php')

let process = await spawn('npx @tailwindcss/upgrade --force', {
env: {
NODE_OPTIONS: '--require=./hook.cjs',
},
})

// We're only interested once we start migrating the templates
await process.onStderr((message) => message.includes('Migrating templates'))

// Wait for the trigger that we are mid-write
await process.onStderr((message) => message === '__TRUNCATED_TARGET__')

// Kill the process
await process.dispose()

expect(await fs.read('src/keep.php')).toBe(originalKeepFile)
expect(await fs.read('src/templates/template-0.php')).toBe(originalTemplate)
Comment thread
RobinMalfait marked this conversation as resolved.

for (let [file, contents] of await fs.glob('src/**/*.{html,php,css}')) {
expect(contents.trim(), `${file} should not be empty after interruption`).not.toBe('')
}
},
)

test(
'upgrades can run in a pnpm workspace',
{
Expand Down
101 changes: 100 additions & 1 deletion integrations/upgrade/js-config.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from 'node:path'
import { describe } from 'vitest'
import { css, html, json, test, ts } from '../utils'
import { css, html, json, test, ts, txt } from '../utils'

test(
`upgrade JS config files with flat theme values, darkMode, and content fields`,
Expand Down Expand Up @@ -322,6 +322,105 @@ test(
},
)

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',
{
Expand Down
Loading