Skip to content

Commit eb54dcd

Browse files
Install @tailwindcss/postcss next to tailwindcss (#14830)
This PR improves the PostCSS migrations to make sure that we install `@tailwindcss/postcss` in the same bucket as `tailwindcss`. If `tailwindcss` exists in the `dependencies` bucket, we install `@tailwindcss/postcss` in the same bucket. If `tailwindcss` exists in the `devDependencies` bucket, we install `@tailwindcss/postcss` in the same bucket. This also contains an internal refactor that normalizes the package manager to make sure we can install a package to the correct bucket depending on the package manager. --------- Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
1 parent 840c9e6 commit eb54dcd

File tree

6 files changed

+149
-26
lines changed

6 files changed

+149
-26
lines changed

CHANGELOG.md

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

1212
- Detect classes in new files when using `@tailwindcss/postcss` ([#14829](https://github.com/tailwindlabs/tailwindcss/pull/14829))
13+
- _Upgrade (experimental)_: Install `@tailwindcss/postcss` next to `tailwindcss` ([#14830](https://github.com/tailwindlabs/tailwindcss/pull/14830))
1314

1415
## [4.0.0-alpha.31] - 2024-10-29
1516

integrations/upgrade/index.test.ts

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,100 @@ test(
544544
})
545545
expect(packageJson.dependencies).not.toHaveProperty('autoprefixer')
546546
expect(packageJson.dependencies).not.toHaveProperty('postcss-import')
547+
expect(packageJson.dependencies).toMatchObject({
548+
'@tailwindcss/postcss': expect.stringContaining('4.0.0'),
549+
})
550+
},
551+
)
552+
553+
test(
554+
'`@tailwindcss/postcss` should be installed in dependencies when `tailwindcss` exists in dependencies',
555+
{
556+
fs: {
557+
'package.json': json`
558+
{
559+
"dependencies": {
560+
"postcss": "^8",
561+
"tailwindcss": "^3",
562+
"@tailwindcss/upgrade": "workspace:^"
563+
}
564+
}
565+
`,
566+
'tailwind.config.js': js`
567+
/** @type {import('tailwindcss').Config} */
568+
module.exports = {
569+
content: ['./src/**/*.{html,js}'],
570+
}
571+
`,
572+
'postcss.config.js': js`
573+
module.exports = {
574+
plugins: {
575+
tailwindcss: {},
576+
},
577+
}
578+
`,
579+
'src/index.html': html`
580+
<div class="bg-[--my-red]"></div>
581+
`,
582+
'src/index.css': css`
583+
@tailwind base;
584+
@tailwind components;
585+
@tailwind utilities;
586+
`,
587+
},
588+
},
589+
async ({ fs, exec }) => {
590+
await exec('npx @tailwindcss/upgrade')
591+
592+
let packageJsonContent = await fs.read('package.json')
593+
let packageJson = JSON.parse(packageJsonContent)
594+
expect(packageJson.dependencies).toMatchObject({
595+
'@tailwindcss/postcss': expect.stringContaining('4.0.0'),
596+
})
597+
},
598+
)
599+
600+
test(
601+
'`@tailwindcss/postcss` should be installed in devDependencies when `tailwindcss` exists in dev dependencies',
602+
{
603+
fs: {
604+
'package.json': json`
605+
{
606+
"devDependencies": {
607+
"postcss": "^8",
608+
"tailwindcss": "^3",
609+
"@tailwindcss/upgrade": "workspace:^"
610+
}
611+
}
612+
`,
613+
'tailwind.config.js': js`
614+
/** @type {import('tailwindcss').Config} */
615+
module.exports = {
616+
content: ['./src/**/*.{html,js}'],
617+
}
618+
`,
619+
'postcss.config.js': js`
620+
module.exports = {
621+
plugins: {
622+
tailwindcss: {},
623+
},
624+
}
625+
`,
626+
'src/index.html': html`
627+
<div class="bg-[--my-red]"></div>
628+
`,
629+
'src/index.css': css`
630+
@tailwind base;
631+
@tailwind components;
632+
@tailwind utilities;
633+
`,
634+
},
635+
},
636+
async ({ fs, exec }) => {
637+
await exec('npx @tailwindcss/upgrade')
638+
639+
let packageJsonContent = await fs.read('package.json')
640+
let packageJson = JSON.parse(packageJsonContent)
547641
expect(packageJson.devDependencies).toMatchObject({
548642
'@tailwindcss/postcss': expect.stringContaining('4.0.0'),
549643
})
@@ -617,7 +711,7 @@ test(
617711
})
618712
expect(packageJson.dependencies).not.toHaveProperty('autoprefixer')
619713
expect(packageJson.dependencies).not.toHaveProperty('postcss-import')
620-
expect(packageJson.devDependencies).toMatchObject({
714+
expect(packageJson.dependencies).toMatchObject({
621715
'@tailwindcss/postcss': expect.stringContaining('4.0.0'),
622716
})
623717
},
@@ -694,7 +788,7 @@ test(
694788
})
695789
expect(packageJson.dependencies).not.toHaveProperty('autoprefixer')
696790
expect(packageJson.dependencies).not.toHaveProperty('postcss-import')
697-
expect(packageJson.devDependencies).toMatchObject({
791+
expect(packageJson.dependencies).toMatchObject({
698792
'@tailwindcss/postcss': expect.stringContaining('4.0.0'),
699793
})
700794
},

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ async function run() {
199199

200200
try {
201201
// Upgrade Tailwind CSS
202-
await pkg('add tailwindcss@next', base)
202+
await pkg(base).add(['tailwindcss@next'])
203203
} catch {}
204204

205205
// Remove the JS config if it was fully migrated

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

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,19 +83,25 @@ export async function migratePostCSSConfig(base: string) {
8383
}
8484

8585
if (didAddPostcssClient) {
86-
try {
87-
await pkg('add -D @tailwindcss/postcss@next', base)
88-
} catch {}
86+
let location = Object.hasOwn(packageJson?.dependencies ?? {}, 'tailwindcss')
87+
? ('dependencies' as const)
88+
: Object.hasOwn(packageJson?.devDependencies ?? {}, 'tailwindcss')
89+
? ('devDependencies' as const)
90+
: null
91+
92+
if (location !== null) {
93+
try {
94+
await pkg(base).add(['@tailwindcss/postcss@next'], location)
95+
} catch {}
96+
}
8997
}
9098
if (didRemoveAutoprefixer || didRemovePostCSSImport) {
9199
try {
92100
let packagesToRemove = [
93101
didRemoveAutoprefixer ? 'autoprefixer' : null,
94102
didRemovePostCSSImport ? 'postcss-import' : null,
95-
]
96-
.filter(Boolean)
97-
.join(' ')
98-
await pkg(`remove ${packagesToRemove}`, base)
103+
].filter(Boolean) as string[]
104+
await pkg(base).remove(packagesToRemove)
99105
} catch {}
100106
}
101107

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export async function migratePrettierPlugin(base: string) {
88
try {
99
let packageJson = await fs.readFile(packageJsonPath, 'utf-8')
1010
if (packageJson.includes('prettier-plugin-tailwindcss')) {
11-
await pkg('add prettier-plugin-tailwindcss@latest', base)
11+
await pkg(base).add(['prettier-plugin-tailwindcss@latest'])
1212
success(`Prettier plugin migrated to latest version.`)
1313
}
1414
} catch {}

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

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,36 @@
1-
import { execSync } from 'node:child_process'
1+
import { exec as execCb } from 'node:child_process'
22
import fs from 'node:fs/promises'
33
import { dirname, resolve } from 'node:path'
4+
import { promisify } from 'node:util'
5+
import { DefaultMap } from '../../../tailwindcss/src/utils/default-map'
46
import { warn } from './renderer'
57

6-
let didWarnAboutPackageManager = false
8+
const exec = promisify(execCb)
79

8-
export async function pkg(command: string, base: string): Promise<Buffer | void> {
9-
let packageManager = await detectPackageManager(base)
10-
if (!packageManager) {
11-
if (!didWarnAboutPackageManager) {
12-
didWarnAboutPackageManager = true
13-
warn('Could not detect a package manager. Please manually update `tailwindcss` to v4.')
14-
}
15-
return
10+
const SAVE_DEV: Record<string, string> = {
11+
default: '-D',
12+
bun: '-d',
13+
}
14+
15+
export function pkg(base: string) {
16+
return {
17+
async add(packages: string[], location: 'dependencies' | 'devDependencies' = 'dependencies') {
18+
let packageManager = await packageManagerForBase.get(base)
19+
let args = packages.slice()
20+
if (location === 'devDependencies') {
21+
args.push(SAVE_DEV[packageManager] || SAVE_DEV.default)
22+
}
23+
return exec(`${packageManager} add ${args.join(' ')}`, { cwd: base })
24+
},
25+
async remove(packages: string[]) {
26+
let packageManager = await packageManagerForBase.get(base)
27+
return exec(`${packageManager} remove ${packages.join(' ')}`, { cwd: base })
28+
},
1629
}
17-
return execSync(`${packageManager} ${command}`, {
18-
cwd: base,
19-
})
2030
}
2131

22-
async function detectPackageManager(base: string): Promise<null | string> {
32+
let didWarnAboutPackageManager = false
33+
let packageManagerForBase = new DefaultMap(async (base) => {
2334
do {
2435
// 1. Check package.json for a `packageManager` field
2536
let packageJsonPath = resolve(base, 'package.json')
@@ -67,6 +78,17 @@ async function detectPackageManager(base: string): Promise<null | string> {
6778
} catch {}
6879

6980
// 3. If no lockfile is found, we might be in a monorepo
81+
let previousBase = base
7082
base = dirname(base)
83+
84+
// Already at the root
85+
if (previousBase === base) {
86+
if (!didWarnAboutPackageManager) {
87+
didWarnAboutPackageManager = true
88+
warn('Could not detect a package manager. Please manually update `tailwindcss` to v4.')
89+
}
90+
91+
return Promise.reject('No package manager detected')
92+
}
7193
} while (true)
72-
}
94+
})

0 commit comments

Comments
 (0)