Skip to content

Commit 8538ad8

Browse files
authored
Ensure @config is injected in common ancestor sheet (#14989)
This PR fixes an issue where an `@config` was injected in a strange location if you have multiple CSS files with Tailwind directives. Let's say you have this setup: ```css /* ./src/index.css */ @import "./tailwind-setup.css"; /* ./src/tailwind-setup.css */ @import "./base.css"; @import "./components.css"; @import "./utilities.css"; /* ./src/base.css */ @tailwind base; /* ./src/components.css */ @tailwind components; /* ./src/utilities.css */ @tailwind utilities; ``` In this case, `base.css`, `components.css`, and `utilities.css` are all considered Tailwind roots because they contain Tailwind directives or imports. Since there are multiple roots, the nearest common ancestor should become the tailwind root (where `@config` is injected). In this case, the nearest common ancestor is `tailwind-setup.css` (not `index.css` because that's further away). Before this change, we find the common ancestor between `base.css` and `components.css` which would be `index.css` instead of `tailwind-setup.css`. In a next iteration, we compare `index.css` with `utilities.css` and find that there is no common ancestor (because the `index.css` file has no parents). This resulted in the `@config` being injected in `index.css` and in `utilities.css`. Continuing with the rest of the migrations, we migrate the `index.css`'s `@config` away, but we didn't migrate the `@config` from `utilities.css`. With this PR, we don't even have the `@config` in the `utilities.css` file anymore. Test plan --- 1. Added an integration test with a non-migrateable config file to ensure that the `@config` is injected in the correct file. 2. Added an integration test with a migrateable config file to ensure that the CSS config is injected in the correct file. h/t @philipp-spiess 3. Ran the upgrade on the https://commit.tailwindui.com project and ensured that 1. The `@config` does not exist in the `utilities.css` file (this was the first bug we solved) 2. The `@config` is replaced in the `tailwind.css` file correctly. <img width="592" alt="image" src="https://github.com/user-attachments/assets/02e3f6ea-a85d-46c2-ac93-09f34ac4a4b8"> <img width="573" alt="image" src="https://github.com/user-attachments/assets/e372eb5f-5732-4052-ab39-096ba7970ff6">
1 parent 49484f0 commit 8538ad8

File tree

5 files changed

+297
-44
lines changed

5 files changed

+297
-44
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
- Ensure that CSS inside Svelte `<style>` blocks always run the expected Svelte processors when using the Vite extension ([#14981](https://github.com/tailwindlabs/tailwindcss/pull/14981))
1919
- _Upgrade (experimental)_: Ensure it's safe to migrate `blur`, `rounded`, or `shadow` ([#14979](https://github.com/tailwindlabs/tailwindcss/pull/14979))
2020
- _Upgrade (experimental)_: Do not rename classes using custom defined theme values ([#14976](https://github.com/tailwindlabs/tailwindcss/pull/14976))
21+
- _Upgrade (experimental)_: Ensure `@config` is injected in nearest common ancestor stylesheet ([#14989](https://github.com/tailwindlabs/tailwindcss/pull/14989))
2122

2223
## [4.0.0-alpha.33] - 2024-11-11
2324

integrations/upgrade/index.test.ts

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1467,6 +1467,276 @@ test(
14671467
},
14681468
)
14691469

1470+
test(
1471+
'injecting `@config` in the shared root, when a tailwind.config.{js,ts,…} is detected',
1472+
{
1473+
fs: {
1474+
'package.json': json`
1475+
{
1476+
"dependencies": {
1477+
"@tailwindcss/upgrade": "workspace:^"
1478+
}
1479+
}
1480+
`,
1481+
'tailwind.config.ts': js`
1482+
export default {
1483+
content: ['./src/**/*.{html,js}'],
1484+
plugins: [
1485+
() => {
1486+
// custom stuff which is too complicated to migrate to CSS
1487+
},
1488+
],
1489+
}
1490+
`,
1491+
'src/index.html': html`
1492+
<div
1493+
class="!flex sm:!block bg-gradient-to-t bg-[--my-red]"
1494+
></div>
1495+
`,
1496+
'src/index.css': css`@import './tailwind-setup.css';`,
1497+
'src/tailwind-setup.css': css`
1498+
@import './base.css';
1499+
@import './components.css';
1500+
@import './utilities.css';
1501+
`,
1502+
'src/base.css': css`
1503+
html {
1504+
color: red;
1505+
}
1506+
@tailwind base;
1507+
`,
1508+
'src/components.css': css`
1509+
@import './typography.css';
1510+
@layer components {
1511+
.foo {
1512+
color: red;
1513+
}
1514+
}
1515+
@tailwind components;
1516+
`,
1517+
'src/typography.css': css`
1518+
.typography {
1519+
color: red;
1520+
}
1521+
`,
1522+
'src/utilities.css': css`
1523+
@layer utilities {
1524+
.bar {
1525+
color: red;
1526+
}
1527+
}
1528+
@tailwind utilities;
1529+
`,
1530+
},
1531+
},
1532+
async ({ exec, fs }) => {
1533+
await exec('npx @tailwindcss/upgrade --force')
1534+
1535+
expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(`
1536+
"
1537+
--- ./src/index.html ---
1538+
<div
1539+
class="flex! sm:block! bg-linear-to-t bg-[var(--my-red)]"
1540+
></div>
1541+
1542+
--- ./src/index.css ---
1543+
@import './tailwind-setup.css';
1544+
1545+
--- ./src/base.css ---
1546+
@import 'tailwindcss/theme' layer(theme);
1547+
@import 'tailwindcss/preflight' layer(base);
1548+
1549+
/*
1550+
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
1551+
so we've added these compatibility styles to make sure everything still
1552+
looks the same as it did with Tailwind CSS v3.
1553+
1554+
If we ever want to remove these styles, we need to add an explicit border
1555+
color utility to any element that depends on these defaults.
1556+
*/
1557+
@layer base {
1558+
*,
1559+
::after,
1560+
::before,
1561+
::backdrop,
1562+
::file-selector-button {
1563+
border-color: var(--color-gray-200, currentColor);
1564+
}
1565+
}
1566+
1567+
@layer base {
1568+
html {
1569+
color: red;
1570+
}
1571+
}
1572+
1573+
--- ./src/components.css ---
1574+
@import './typography.css';
1575+
1576+
@utility foo {
1577+
color: red;
1578+
}
1579+
1580+
--- ./src/tailwind-setup.css ---
1581+
@import './base.css';
1582+
@import './components.css';
1583+
@import './utilities.css';
1584+
1585+
@config '../tailwind.config.ts';
1586+
1587+
--- ./src/typography.css ---
1588+
.typography {
1589+
color: red;
1590+
}
1591+
1592+
--- ./src/utilities.css ---
1593+
@import 'tailwindcss/utilities' layer(utilities);
1594+
1595+
@utility bar {
1596+
color: red;
1597+
}
1598+
"
1599+
`)
1600+
},
1601+
)
1602+
1603+
test(
1604+
'injecting `@config` in the shared root (+ migrating), when a tailwind.config.{js,ts,…} is detected',
1605+
{
1606+
fs: {
1607+
'package.json': json`
1608+
{
1609+
"dependencies": {
1610+
"@tailwindcss/upgrade": "workspace:^"
1611+
}
1612+
}
1613+
`,
1614+
'tailwind.config.ts': js`
1615+
export default {
1616+
content: ['./src/**/*.{html,js}'],
1617+
theme: {
1618+
extend: {
1619+
colors: {
1620+
'my-red': 'red',
1621+
},
1622+
},
1623+
},
1624+
}
1625+
`,
1626+
'src/index.html': html`
1627+
<div
1628+
class="!flex sm:!block bg-gradient-to-t bg-[--my-red]"
1629+
></div>
1630+
`,
1631+
'src/index.css': css`@import './tailwind-setup.css';`,
1632+
'src/tailwind-setup.css': css`
1633+
@import './base.css';
1634+
@import './components.css';
1635+
@import './utilities.css';
1636+
`,
1637+
'src/base.css': css`
1638+
html {
1639+
color: red;
1640+
}
1641+
@tailwind base;
1642+
`,
1643+
'src/components.css': css`
1644+
@import './typography.css';
1645+
@layer components {
1646+
.foo {
1647+
color: red;
1648+
}
1649+
}
1650+
@tailwind components;
1651+
`,
1652+
'src/typography.css': css`
1653+
.typography {
1654+
color: red;
1655+
}
1656+
`,
1657+
'src/utilities.css': css`
1658+
@layer utilities {
1659+
.bar {
1660+
color: red;
1661+
}
1662+
}
1663+
@tailwind utilities;
1664+
`,
1665+
},
1666+
},
1667+
async ({ exec, fs }) => {
1668+
await exec('npx @tailwindcss/upgrade --force')
1669+
1670+
expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(`
1671+
"
1672+
--- ./src/index.html ---
1673+
<div
1674+
class="flex! sm:block! bg-linear-to-t bg-[var(--my-red)]"
1675+
></div>
1676+
1677+
--- ./src/index.css ---
1678+
@import './tailwind-setup.css';
1679+
1680+
--- ./src/base.css ---
1681+
@import 'tailwindcss/theme' layer(theme);
1682+
@import 'tailwindcss/preflight' layer(base);
1683+
1684+
/*
1685+
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
1686+
so we've added these compatibility styles to make sure everything still
1687+
looks the same as it did with Tailwind CSS v3.
1688+
1689+
If we ever want to remove these styles, we need to add an explicit border
1690+
color utility to any element that depends on these defaults.
1691+
*/
1692+
@layer base {
1693+
*,
1694+
::after,
1695+
::before,
1696+
::backdrop,
1697+
::file-selector-button {
1698+
border-color: var(--color-gray-200, currentColor);
1699+
}
1700+
}
1701+
1702+
@layer base {
1703+
html {
1704+
color: red;
1705+
}
1706+
}
1707+
1708+
--- ./src/components.css ---
1709+
@import './typography.css';
1710+
1711+
@utility foo {
1712+
color: red;
1713+
}
1714+
1715+
--- ./src/tailwind-setup.css ---
1716+
@import './base.css';
1717+
@import './components.css';
1718+
@import './utilities.css';
1719+
1720+
@theme {
1721+
--color-my-red: red;
1722+
}
1723+
1724+
--- ./src/typography.css ---
1725+
.typography {
1726+
color: red;
1727+
}
1728+
1729+
--- ./src/utilities.css ---
1730+
@import 'tailwindcss/utilities' layer(utilities);
1731+
1732+
@utility bar {
1733+
color: red;
1734+
}
1735+
"
1736+
`)
1737+
},
1738+
)
1739+
14701740
test(
14711741
'relative imports without a relative path prefix are migrated to include a relative path prefix',
14721742
{

packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts

Lines changed: 4 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import path from 'node:path'
2-
import postcss, { AtRule, type Plugin, Root } from 'postcss'
2+
import postcss, { AtRule, type Plugin } from 'postcss'
33
import { normalizePath } from '../../../@tailwindcss-node/src/normalize-path'
44
import type { JSConfigMigration } from '../migrate-js-config'
55
import type { Stylesheet } from '../stylesheet'
@@ -13,7 +13,9 @@ export function migrateConfig(
1313
jsConfigMigration,
1414
}: { configFilePath: string; jsConfigMigration: JSConfigMigration },
1515
): Plugin {
16-
function injectInto(sheet: Stylesheet) {
16+
function migrate() {
17+
if (!sheet.isTailwindRoot) return
18+
1719
let alreadyInjected = ALREADY_INJECTED.get(sheet)
1820
if (alreadyInjected && alreadyInjected.includes(configFilePath)) {
1921
return
@@ -82,41 +84,6 @@ export function migrateConfig(
8284
root.append(cssConfig.nodes)
8385
}
8486

85-
function migrate(root: Root) {
86-
// We can only migrate if there is an `@import "tailwindcss"` (or sub-import)
87-
let hasTailwindImport = false
88-
let hasFullTailwindImport = false
89-
root.walkAtRules('import', (node) => {
90-
if (node.params.match(/['"]tailwindcss['"]/)) {
91-
hasTailwindImport = true
92-
hasFullTailwindImport = true
93-
return false
94-
} else if (node.params.match(/['"]tailwindcss\/.*?['"]/)) {
95-
hasTailwindImport = true
96-
}
97-
})
98-
99-
if (!hasTailwindImport) return
100-
101-
// If a full `@import "tailwindcss"` is present or this is the root
102-
// stylesheet, we can inject the `@config` directive directly into this
103-
// file.
104-
if (hasFullTailwindImport || sheet.parents.size <= 0) {
105-
injectInto(sheet)
106-
return
107-
}
108-
109-
// Otherwise, if we are not the root file, we need to inject the `@config`
110-
// into the root file.
111-
if (sheet.parents.size > 0) {
112-
for (let parent of sheet.ancestors()) {
113-
if (parent.parents.size === 0) {
114-
injectInto(parent)
115-
}
116-
}
117-
}
118-
}
119-
12087
return {
12188
postcssPlugin: '@tailwindcss/upgrade/migrate-config',
12289
OnceExit: migrate,

packages/@tailwindcss-upgrade/src/migrate.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -253,22 +253,38 @@ export async function analyze(stylesheets: Stylesheet[]) {
253253
for (let [sheetB, childrenB] of commonParents) {
254254
if (sheetA === sheetB) continue
255255

256-
for (let parentA of sheetA.ancestors()) {
257-
for (let parentB of sheetB.ancestors()) {
256+
// Ancestors from self to root. Reversed order so we find the
257+
// nearest common parent first
258+
//
259+
// Including self because if you compare a sheet with its parent,
260+
// then the parent is still the common sheet between the two. In
261+
// this case, the parent is the root file.
262+
let ancestorsA = [sheetA].concat(Array.from(sheetA.ancestors()).reverse())
263+
let ancestorsB = [sheetB].concat(Array.from(sheetB.ancestors()).reverse())
264+
265+
for (let parentA of ancestorsA) {
266+
for (let parentB of ancestorsB) {
258267
if (parentA !== parentB) continue
259268

269+
// Found the parent
270+
let parent = parentA
271+
260272
commonParents.delete(sheetA)
261273
commonParents.delete(sheetB)
262274

263275
for (let child of childrenA) {
264-
commonParents.get(parentA).add(child)
276+
commonParents.get(parent).add(child)
265277
}
266278

267279
for (let child of childrenB) {
268-
commonParents.get(parentA).add(child)
280+
commonParents.get(parent).add(child)
269281
}
270282

271-
repeat = true
283+
repeat = parent !== sheetA && parent !== sheetB
284+
285+
// Found a common parent between sheet A and sheet B. We can
286+
// stop looking for more common parents between A and B, and
287+
// continue with the next sheet.
272288
break outer
273289
}
274290
}

packages/tailwindcss/src/compat/plugin-api.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2973,8 +2973,7 @@ describe('matchUtilities()', () => {
29732973
return compiled.build(candidates)
29742974
}
29752975

2976-
expect(optimizeCss(await run(['@w-1','hover:@w-1'])).trim())
2977-
.toMatchInlineSnapshot(`
2976+
expect(optimizeCss(await run(['@w-1', 'hover:@w-1'])).trim()).toMatchInlineSnapshot(`
29782977
".\\@w-1 {
29792978
width: 1px;
29802979
}

0 commit comments

Comments
 (0)