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>
This commit is contained in:
Robin Malfait 2024-10-30 20:32:24 +01:00 • committed by GitHub
parent 840c9e65b9
commit eb54dcdbfc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 150 additions and 27 deletions

View file

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Detect classes in new files when using `@tailwindcss/postcss` ([#14829](https://github.com/tailwindlabs/tailwindcss/pull/14829))
- _Upgrade (experimental)_: Install `@tailwindcss/postcss` next to `tailwindcss` ([#14830](https://github.com/tailwindlabs/tailwindcss/pull/14830))
## [4.0.0-alpha.31] - 2024-10-29

View file

@ -544,6 +544,100 @@ test(
})
expect(packageJson.dependencies).not.toHaveProperty('autoprefixer')
expect(packageJson.dependencies).not.toHaveProperty('postcss-import')
expect(packageJson.dependencies).toMatchObject({
'@tailwindcss/postcss': expect.stringContaining('4.0.0'),
})
},
)
test(
'`@tailwindcss/postcss` should be installed in dependencies when `tailwindcss` exists in dependencies',
{
fs: {
'package.json': json`
{
"dependencies": {
"postcss": "^8",
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.js': js`
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{html,js}'],
}
`,
'postcss.config.js': js`
module.exports = {
plugins: {
tailwindcss: {},
},
}
`,
'src/index.html': html`
<div class="bg-[--my-red]"></div>
`,
'src/index.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
},
},
async ({ fs, exec }) => {
await exec('npx @tailwindcss/upgrade')
let packageJsonContent = await fs.read('package.json')
let packageJson = JSON.parse(packageJsonContent)
expect(packageJson.dependencies).toMatchObject({
'@tailwindcss/postcss': expect.stringContaining('4.0.0'),
})
},
)
test(
'`@tailwindcss/postcss` should be installed in devDependencies when `tailwindcss` exists in dev dependencies',
{
fs: {
'package.json': json`
{
"devDependencies": {
"postcss": "^8",
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.js': js`
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{html,js}'],
}
`,
'postcss.config.js': js`
module.exports = {
plugins: {
tailwindcss: {},
},
}
`,
'src/index.html': html`
<div class="bg-[--my-red]"></div>
`,
'src/index.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
},
},
async ({ fs, exec }) => {
await exec('npx @tailwindcss/upgrade')
let packageJsonContent = await fs.read('package.json')
let packageJson = JSON.parse(packageJsonContent)
expect(packageJson.devDependencies).toMatchObject({
'@tailwindcss/postcss': expect.stringContaining('4.0.0'),
})
@ -617,7 +711,7 @@ test(
})
expect(packageJson.dependencies).not.toHaveProperty('autoprefixer')
expect(packageJson.dependencies).not.toHaveProperty('postcss-import')
expect(packageJson.devDependencies).toMatchObject({
expect(packageJson.dependencies).toMatchObject({
'@tailwindcss/postcss': expect.stringContaining('4.0.0'),
})
},
@ -694,7 +788,7 @@ test(
})
expect(packageJson.dependencies).not.toHaveProperty('autoprefixer')
expect(packageJson.dependencies).not.toHaveProperty('postcss-import')
expect(packageJson.devDependencies).toMatchObject({
expect(packageJson.dependencies).toMatchObject({
'@tailwindcss/postcss': expect.stringContaining('4.0.0'),
})
},

View file

@ -199,7 +199,7 @@ async function run() {
try {
// Upgrade Tailwind CSS
await pkg('add tailwindcss@next', base)
await pkg(base).add(['tailwindcss@next'])
} catch {}
// Remove the JS config if it was fully migrated

View file

@ -83,19 +83,25 @@ export async function migratePostCSSConfig(base: string) {
}
if (didAddPostcssClient) {
try {
await pkg('add -D @tailwindcss/postcss@next', base)
} catch {}
let location = Object.hasOwn(packageJson?.dependencies ?? {}, 'tailwindcss')
? ('dependencies' as const)
: Object.hasOwn(packageJson?.devDependencies ?? {}, 'tailwindcss')
? ('devDependencies' as const)
: null
if (location !== null) {
try {
await pkg(base).add(['@tailwindcss/postcss@next'], location)
} catch {}
}
}
if (didRemoveAutoprefixer || didRemovePostCSSImport) {
try {
let packagesToRemove = [
didRemoveAutoprefixer ? 'autoprefixer' : null,
didRemovePostCSSImport ? 'postcss-import' : null,
]
.filter(Boolean)
.join(' ')
await pkg(`remove ${packagesToRemove}`, base)
].filter(Boolean) as string[]
await pkg(base).remove(packagesToRemove)
} catch {}
}

View file

@ -8,7 +8,7 @@ export async function migratePrettierPlugin(base: string) {
try {
let packageJson = await fs.readFile(packageJsonPath, 'utf-8')
if (packageJson.includes('prettier-plugin-tailwindcss')) {
await pkg('add prettier-plugin-tailwindcss@latest', base)
await pkg(base).add(['prettier-plugin-tailwindcss@latest'])
success(`Prettier plugin migrated to latest version.`)
}
} catch {}

View file

@ -1,25 +1,36 @@
import { execSync } from 'node:child_process'
import { exec as execCb } from 'node:child_process'
import fs from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
import { promisify } from 'node:util'
import { DefaultMap } from '../../../tailwindcss/src/utils/default-map'
import { warn } from './renderer'
let didWarnAboutPackageManager = false
const exec = promisify(execCb)
export async function pkg(command: string, base: string): Promise<Buffer | void> {
let packageManager = await detectPackageManager(base)
if (!packageManager) {
if (!didWarnAboutPackageManager) {
didWarnAboutPackageManager = true
warn('Could not detect a package manager. Please manually update `tailwindcss` to v4.')
}
return
}
return execSync(`${packageManager} ${command}`, {
cwd: base,
})
const SAVE_DEV: Record<string, string> = {
default: '-D',
bun: '-d',
}
async function detectPackageManager(base: string): Promise<null | string> {
export function pkg(base: string) {
return {
async add(packages: string[], location: 'dependencies' | 'devDependencies' = 'dependencies') {
let packageManager = await packageManagerForBase.get(base)
let args = packages.slice()
if (location === 'devDependencies') {
args.push(SAVE_DEV[packageManager] || SAVE_DEV.default)
}
return exec(`${packageManager} add ${args.join(' ')}`, { cwd: base })
},
async remove(packages: string[]) {
let packageManager = await packageManagerForBase.get(base)
return exec(`${packageManager} remove ${packages.join(' ')}`, { cwd: base })
},
}
}
let didWarnAboutPackageManager = false
let packageManagerForBase = new DefaultMap(async (base) => {
do {
// 1. Check package.json for a `packageManager` field
let packageJsonPath = resolve(base, 'package.json')
@ -67,6 +78,17 @@ async function detectPackageManager(base: string): Promise<null | string> {
} catch {}
// 3. If no lockfile is found, we might be in a monorepo
let previousBase = base
base = dirname(base)
// Already at the root
if (previousBase === base) {
if (!didWarnAboutPackageManager) {
didWarnAboutPackageManager = true
warn('Could not detect a package manager. Please manually update `tailwindcss` to v4.')
}
return Promise.reject('No package manager detected')
}
} while (true)
}
})