Skip to content

Commit 08c6c96

Browse files
Improve multi-root @config linking (#15001)
This PR improves the discoverability of Tailwind config files when we are trying to link them to your CSS files. When you have multiple "root" CSS files in your project, and if they don't include an `@config` directive, then we tried to find the Tailwind config file in your current working directory. This means that if you run the upgrade command from the root of your project, and you have a nested folder with a separate Tailwind setup, then the nested CSS file would link to the root Tailwind config file. Visually, you can think of it like this: ``` . ├── admin │   ├── src │   │   └── styles │   │   └── index.css <-- This will be linked to (1) │   └── tailwind.config.js (2) ├── src │   └── styles │   └── index.css <-- This will be linked to (1) └── tailwind.config.js (1) ``` If you run the upgrade command from the root of your project, then the `/src/styles/index.css` will be linked to `/tailwind.config.js` which is what we expect. But `/admin/src/styles/index.css` will _also_ be linked to `/tailwind.config.js` With this PR we improve this behavior by looking at the CSS file, and crawling up the parent tree. This mens that the new behavior looks like this: ``` . ├── admin │   ├── src │   │   └── styles │   │   └── index.css <-- This will be linked to (2) │   └── tailwind.config.js (2) ├── src │   └── styles │   └── index.css <-- This will be linked to (1) └── tailwind.config.js (1) ``` Now `/src/styles/index.css` will be linked to `/tailwind.config.js`, and `/admin/src/styles/index.css` will be linked to `/admin/tailwind.config.js`. When we discover the Tailwind config file, we will also print a message to the user to let them know which CSS file is linked to which Tailwind config file. This should be a safe improvement because if your Tailwind config file had a different name, or if it lived in a sibling folder then Tailwind wouldn't find it either and you already required a `@config "…";` directive in your CSS file to point to the correct file. In the unlikely event that it turns out that 2 (or more) CSS files resolve to the same to the same Tailwind config file, then an upgrade might not be safe and some manual intervention might be needed. In this case, we will show a warning about this. <img width="1552" alt="image" src="https://github.com/user-attachments/assets/7a1ad11d-18c5-4b7d-9a02-14f0116ae955"> Test plan: --- - Added an integration test that properly links the nearest Tailwind config file by looking up the tree - Added an integration test that resolves 2 or more CSS files to the same config file, resulting in an error where manual intervention is needed - Ran it on the Tailwind UI codebase Running this on Tailwind UI's codebase it looks like this: <img width="1552" alt="image" src="https://github.com/user-attachments/assets/21785428-5e0d-47f7-80ec-dab497f58784"> --------- Co-authored-by: Jordan Pittman <jordan@cryptica.me>
1 parent dd3441b commit 08c6c96

File tree

9 files changed

+287
-104
lines changed

9 files changed

+287
-104
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919

2020
- Ensure `flex` is suggested ([#15014](https://github.com/tailwindlabs/tailwindcss/pull/15014))
2121
- _Upgrade (experimental)_: Resolve imports when specifying a CSS entry point on the command-line ([#15010](https://github.com/tailwindlabs/tailwindcss/pull/15010))
22+
- _Upgrade (experimental)_: Resolve nearest Tailwind config file when CSS file does not contain `@config` ([#15001](https://github.com/tailwindlabs/tailwindcss/pull/15001))
2223

2324
### Changed
2425

integrations/upgrade/index.test.ts

Lines changed: 145 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1385,9 +1385,7 @@ test(
13851385
export default {
13861386
content: ['./src/**/*.{html,js}'],
13871387
plugins: [
1388-
() => {
1389-
// custom stuff which is too complicated to migrate to CSS
1390-
},
1388+
() => {}, // custom stuff which is too complicated to migrate to CSS
13911389
],
13921390
}
13931391
`,
@@ -1396,20 +1394,28 @@ test(
13961394
class="!flex sm:!block bg-gradient-to-t bg-[--my-red]"
13971395
></div>
13981396
`,
1399-
'src/root.1.css': css`
1397+
'src/root.1/index.css': css`
14001398
/* Inject missing @config */
14011399
@tailwind base;
14021400
@tailwind components;
14031401
@tailwind utilities;
14041402
`,
1405-
'src/root.2.css': css`
1403+
'src/root.1/tailwind.config.ts': js`
1404+
export default {
1405+
content: ['./src/**/*.{html,js}'],
1406+
plugins: [
1407+
() => {}, // custom stuff which is too complicated to migrate to CSS
1408+
],
1409+
}
1410+
`,
1411+
'src/root.2/index.css': css`
14061412
/* Already contains @config */
14071413
@tailwind base;
14081414
@tailwind components;
14091415
@tailwind utilities;
1410-
@config "../tailwind.config.ts";
1416+
@config "../../tailwind.config.ts";
14111417
`,
1412-
'src/root.3.css': css`
1418+
'src/root.3/index.css': css`
14131419
/* Inject missing @config above first @theme */
14141420
@tailwind base;
14151421
@tailwind components;
@@ -1425,18 +1431,35 @@ test(
14251431
--color-blue-500: #00f;
14261432
}
14271433
`,
1428-
'src/root.4.css': css`
1434+
'src/root.3/tailwind.config.ts': js`
1435+
export default {
1436+
content: ['./src/**/*.{html,js}'],
1437+
plugins: [
1438+
() => {}, // custom stuff which is too complicated to migrate to CSS
1439+
],
1440+
}
1441+
`,
1442+
'src/root.4/index.css': css`
14291443
/* Inject missing @config due to nested imports with tailwind imports */
1430-
@import './root.4/base.css';
1431-
@import './root.4/utilities.css';
1444+
@import './base.css';
1445+
@import './utilities.css';
1446+
`,
1447+
'src/root.4/tailwind.config.ts': js`
1448+
export default {
1449+
content: ['./src/**/*.{html,js}'],
1450+
plugins: [
1451+
() => {}, // custom stuff which is too complicated to migrate to CSS
1452+
],
1453+
}
14321454
`,
14331455
'src/root.4/base.css': css`@import 'tailwindcss/base';`,
14341456
'src/root.4/utilities.css': css`@import 'tailwindcss/utilities';`,
14351457

1436-
'src/root.5.css': css`@import './root.5/tailwind.css';`,
1458+
'src/root.5/index.css': css`@import './tailwind.css';`,
14371459
'src/root.5/tailwind.css': css`
14381460
/* Inject missing @config in this file, due to full import */
1439-
@import 'tailwindcss/tailwind.css';
1461+
/* Should be located in the root: ../../ */
1462+
@import 'tailwindcss';
14401463
`,
14411464
},
14421465
},
@@ -1450,11 +1473,11 @@ test(
14501473
class="flex! sm:block! bg-linear-to-t bg-(--my-red)"
14511474
></div>
14521475
1453-
--- ./src/root.1.css ---
1476+
--- ./src/root.1/index.css ---
14541477
/* Inject missing @config */
14551478
@import 'tailwindcss';
14561479
1457-
@config '../tailwind.config.ts';
1480+
@config './tailwind.config.ts';
14581481
14591482
/*
14601483
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
@@ -1474,11 +1497,11 @@ test(
14741497
}
14751498
}
14761499
1477-
--- ./src/root.2.css ---
1500+
--- ./src/root.2/index.css ---
14781501
/* Already contains @config */
14791502
@import 'tailwindcss';
14801503
1481-
@config "../tailwind.config.ts";
1504+
@config "../../tailwind.config.ts";
14821505
14831506
/*
14841507
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
@@ -1498,11 +1521,11 @@ test(
14981521
}
14991522
}
15001523
1501-
--- ./src/root.3.css ---
1524+
--- ./src/root.3/index.css ---
15021525
/* Inject missing @config above first @theme */
15031526
@import 'tailwindcss';
15041527
1505-
@config '../tailwind.config.ts';
1528+
@config './tailwind.config.ts';
15061529
15071530
@variant hocus (&:hover, &:focus);
15081531
@@ -1532,15 +1555,12 @@ test(
15321555
}
15331556
}
15341557
1535-
--- ./src/root.4.css ---
1558+
--- ./src/root.4/index.css ---
15361559
/* Inject missing @config due to nested imports with tailwind imports */
1537-
@import './root.4/base.css';
1538-
@import './root.4/utilities.css';
1539-
1540-
@config '../tailwind.config.ts';
1560+
@import './base.css';
1561+
@import './utilities.css';
15411562
1542-
--- ./src/root.5.css ---
1543-
@import './root.5/tailwind.css';
1563+
@config './tailwind.config.ts';
15441564
15451565
--- ./src/root.4/base.css ---
15461566
@import 'tailwindcss/theme' layer(theme);
@@ -1567,8 +1587,12 @@ test(
15671587
--- ./src/root.4/utilities.css ---
15681588
@import 'tailwindcss/utilities' layer(utilities);
15691589
1590+
--- ./src/root.5/index.css ---
1591+
@import './tailwind.css';
1592+
15701593
--- ./src/root.5/tailwind.css ---
15711594
/* Inject missing @config in this file, due to full import */
1595+
/* Should be located in the root: ../../ */
15721596
@import 'tailwindcss';
15731597
15741598
@config '../../tailwind.config.ts';
@@ -1595,13 +1619,92 @@ test(
15951619
},
15961620
)
15971621

1622+
test(
1623+
'multiple CSS roots that resolve to the same Tailwind config file requires manual intervention',
1624+
{
1625+
fs: {
1626+
'package.json': json`
1627+
{
1628+
"dependencies": {
1629+
"tailwindcss": "^3",
1630+
"@tailwindcss/upgrade": "workspace:^"
1631+
}
1632+
}
1633+
`,
1634+
'tailwind.config.ts': js`
1635+
export default {
1636+
content: ['./src/**/*.{html,js}'],
1637+
plugins: [
1638+
() => {}, // custom stuff which is too complicated to migrate to CSS
1639+
],
1640+
}
1641+
`,
1642+
'src/index.html': html`
1643+
<div
1644+
class="!flex sm:!block bg-gradient-to-t bg-[--my-red]"
1645+
></div>
1646+
`,
1647+
'src/root.1.css': css`
1648+
/* Inject missing @config */
1649+
@tailwind base;
1650+
@tailwind components;
1651+
@tailwind utilities;
1652+
`,
1653+
'src/root.2.css': css`
1654+
/* Already contains @config */
1655+
@tailwind base;
1656+
@tailwind components;
1657+
@tailwind utilities;
1658+
@config "../tailwind.config.ts";
1659+
`,
1660+
'src/root.3.css': css`
1661+
/* Inject missing @config above first @theme */
1662+
@tailwind base;
1663+
@tailwind components;
1664+
@tailwind utilities;
1665+
1666+
@variant hocus (&:hover, &:focus);
1667+
1668+
@theme {
1669+
--color-red-500: #f00;
1670+
}
1671+
1672+
@theme {
1673+
--color-blue-500: #00f;
1674+
}
1675+
`,
1676+
'src/root.4.css': css`
1677+
/* Inject missing @config due to nested imports with tailwind imports */
1678+
@import './root.4/base.css';
1679+
@import './root.4/utilities.css';
1680+
`,
1681+
'src/root.4/base.css': css`@import 'tailwindcss/base';`,
1682+
'src/root.4/utilities.css': css`@import 'tailwindcss/utilities';`,
1683+
1684+
'src/root.5.css': css`@import './root.5/tailwind.css';`,
1685+
'src/root.5/tailwind.css': css`
1686+
/* Inject missing @config in this file, due to full import */
1687+
@import 'tailwindcss/tailwind.css';
1688+
`,
1689+
},
1690+
},
1691+
async ({ exec }) => {
1692+
let output = await exec('npx @tailwindcss/upgrade --force', {}, { ignoreStdErr: true }).catch(
1693+
(e) => e.toString(),
1694+
)
1695+
1696+
expect(output).toMatch('Could not determine configuration file for:')
1697+
},
1698+
)
1699+
15981700
test(
15991701
'injecting `@config` in the shared root, when a tailwind.config.{js,ts,…} is detected',
16001702
{
16011703
fs: {
16021704
'package.json': json`
16031705
{
16041706
"dependencies": {
1707+
"tailwindcss": "^3",
16051708
"@tailwindcss/upgrade": "workspace:^"
16061709
}
16071710
}
@@ -1662,14 +1765,14 @@ test(
16621765

16631766
expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(`
16641767
"
1768+
--- ./src/index.css ---
1769+
@import './tailwind-setup.css';
1770+
16651771
--- ./src/index.html ---
16661772
<div
16671773
class="flex! sm:block! bg-linear-to-t bg-(--my-red)"
16681774
></div>
16691775
1670-
--- ./src/index.css ---
1671-
@import './tailwind-setup.css';
1672-
16731776
--- ./src/base.css ---
16741777
@import 'tailwindcss/theme' layer(theme);
16751778
@import 'tailwindcss/preflight' layer(base);
@@ -1735,6 +1838,7 @@ test(
17351838
'package.json': json`
17361839
{
17371840
"dependencies": {
1841+
"tailwindcss": "^3",
17381842
"@tailwindcss/upgrade": "workspace:^"
17391843
}
17401844
}
@@ -1797,14 +1901,14 @@ test(
17971901

17981902
expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(`
17991903
"
1904+
--- ./src/index.css ---
1905+
@import './tailwind-setup.css';
1906+
18001907
--- ./src/index.html ---
18011908
<div
18021909
class="flex! sm:block! bg-linear-to-t bg-(--my-red)"
18031910
></div>
18041911
1805-
--- ./src/index.css ---
1806-
@import './tailwind-setup.css';
1807-
18081912
--- ./src/base.css ---
18091913
@import 'tailwindcss/theme' layer(theme);
18101914
@import 'tailwindcss/preflight' layer(base);
@@ -2105,13 +2209,6 @@ test(
21052209
// Files should not be modified
21062210
expect(await fs.dumpFiles('./*.{js,css,html}')).toMatchInlineSnapshot(`
21072211
"
2108-
--- index.html ---
2109-
<div>
2110-
<div class="shadow shadow-sm shadow-xs"></div>
2111-
<div class="blur blur-xs"></div>
2112-
<div class="rounded rounded-sm"></div>
2113-
</div>
2114-
21152212
--- index.css ---
21162213
@import 'tailwindcss';
21172214
@@ -2141,6 +2238,13 @@ test(
21412238
border-color: var(--color-gray-200, currentColor);
21422239
}
21432240
}
2241+
2242+
--- index.html ---
2243+
<div>
2244+
<div class="shadow shadow-sm shadow-xs"></div>
2245+
<div class="blur blur-xs"></div>
2246+
<div class="rounded rounded-sm"></div>
2247+
</div>
21442248
"
21452249
`)
21462250
},
@@ -2196,9 +2300,6 @@ test(
21962300
// Files should not be modified
21972301
expect(await fs.dumpFiles('./*.{js,css,html,tsx}')).toMatchInlineSnapshot(`
21982302
"
2199-
--- index.html ---
2200-
<div class="rounded-sm blur-sm shadow-sm"></div>
2201-
22022303
--- index.css ---
22032304
@import 'tailwindcss';
22042305
@@ -2220,6 +2321,9 @@ test(
22202321
}
22212322
}
22222323
2324+
--- index.html ---
2325+
<div class="rounded-sm blur-sm shadow-sm"></div>
2326+
22232327
--- example-component.tsx ---
22242328
type Star = [
22252329
x: number,

integrations/utils.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -346,15 +346,21 @@ export function test(
346346
let zParts = z[0].split('/')
347347
348348
let aFile = aParts.at(-1)
349-
let zFile = aParts.at(-1)
349+
let zFile = zParts.at(-1)
350350
351351
// Sort by depth, shallow first
352352
if (aParts.length < zParts.length) return -1
353353
if (aParts.length > zParts.length) return 1
354354
355+
// Sort by folder names, alphabetically
356+
for (let i = 0; i < aParts.length - 1; i++) {
357+
let diff = aParts[i].localeCompare(zParts[i])
358+
if (diff !== 0) return diff
359+
}
360+
355361
// Sort by filename, sort files named `index` before others
356-
if (aFile?.startsWith('index')) return -1
357-
if (zFile?.startsWith('index')) return 1
362+
if (aFile?.startsWith('index') && !zFile?.startsWith('index')) return -1
363+
if (zFile?.startsWith('index') && !aFile?.startsWith('index')) return 1
358364
359365
// Sort by filename, alphabetically
360366
return a[0].localeCompare(z[0])

0 commit comments

Comments
 (0)