Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
98 changes: 96 additions & 2 deletions integrations/upgrade/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
})
Expand Down Expand Up @@ -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'),
})
},
Expand Down Expand Up @@ -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'),
})
},
Expand Down
2 changes: 1 addition & 1 deletion packages/@tailwindcss-upgrade/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 13 additions & 7 deletions packages/@tailwindcss-upgrade/src/migrate-postcss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/@tailwindcss-upgrade/src/migrate-prettier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand Down
52 changes: 37 additions & 15 deletions packages/@tailwindcss-upgrade/src/utils/packages.ts
Original file line number Diff line number Diff line change
@@ -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
const SAVE_DEV: Record<string, string> = {
default: '-D',
bun: '-d',
}

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 })
},
}
return execSync(`${packageManager} ${command}`, {
cwd: base,
})
}

async function detectPackageManager(base: string): Promise<null | string> {
let didWarnAboutPackageManager = false
let packageManagerForBase = new DefaultMap(async (base) => {
do {
// 1. Check package.json for a `packageManager` field
let packageJsonPath = resolve(base, 'package.json')
Expand Down Expand Up @@ -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)
}
})