Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Migrate simple PostCSS setup
  • Loading branch information
philipp-spiess committed Oct 7, 2024
commit 16686c232bbe8395cca6bbe32966dea3827d6016
49 changes: 28 additions & 21 deletions integrations/upgrade/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { candidate, css, html, js, json, test } from '../utils'
import { expect } from 'vitest'
import { css, html, js, json, test } from '../utils'

test(
`upgrades a v3 project to v4`,
Expand Down Expand Up @@ -40,6 +41,12 @@ test(
)

await fs.expectFileToContain('src/input.css', css`@import 'tailwindcss';`)

let packageJsonContent = await fs.read('package.json')
let packageJson = JSON.parse(packageJsonContent)
expect(packageJson.dependencies).toMatchObject({
tailwindcss: expect.stringContaining('4.0.0'),
})
},
)

Expand Down Expand Up @@ -265,8 +272,8 @@ test(
},
)

test.only(
'migrate a simple postcss setup',
test(
'fully migrate a simple postcss setup',
{
fs: {
'package.json': json`
Expand Down Expand Up @@ -305,27 +312,27 @@ test.only(
},
},
async ({ fs, exec }) => {
// Assert that the v3 project works as expected
await exec('pnpm postcss src/index.css --output dist/out.css')
await fs.expectFileToContain('dist/out.css', [candidate`bg-[--my-red]`])

await exec('npx @tailwindcss/upgrade')

console.log(await exec('npx @tailwindcss/upgrade -c tailwind.config.js'))
await fs.expectFileToContain(
'src/index.css',
css`
@utility btn {
@apply rounded-md px-2 py-1 bg-blue-500 text-white;
}

@utility no-scrollbar {
&::-webkit-scrollbar {
display: none;
}
-ms-overflow-style: none;
scrollbar-width: none;
'postcss.config.js',
js`
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
},
}
`,
)

let packageJsonContent = await fs.read('package.json')
let packageJson = JSON.parse(packageJsonContent)

expect(packageJson.dependencies).toMatchObject({
tailwindcss: expect.stringContaining('4.0.0'),
})
expect(packageJson.dependencies).not.toHaveProperty('autoprefixer')
expect(packageJson.devDependencies).toMatchObject({
'@tailwindcss/postcss': expect.stringContaining('4.0.0'),
})
},
)
2 changes: 1 addition & 1 deletion integrations/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export function test(
) {
return (only || (!process.env.CI && debug) ? defaultTest.only : defaultTest)(
name,
{ timeout: TEST_TIMEOUT, retry: 3 },
{ timeout: TEST_TIMEOUT },
async (options) => {
let rootDir = debug ? path.join(REPO_ROOT, '.debug') : TMP_ROOT
await fs.mkdir(rootDir, { recursive: true })
Expand Down
12 changes: 12 additions & 0 deletions packages/@tailwindcss-upgrade/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { globby } from 'globby'
import path from 'node:path'
import { help } from './commands/help'
import { migrate as migrateStylesheet } from './migrate'
import { migratePostCSSConfig } from './migrate-postcss'
import { migrate as migrateTemplate } from './template/migrate'
import { prepareConfig } from './template/prepare-config'
import { args, type Arg } from './utils/args'
import { isRepoDirty } from './utils/git'
import { pkg } from './utils/packages'
import { eprintln, error, header, highlight, info, success } from './utils/renderer'

const options = {
Expand Down Expand Up @@ -98,6 +100,16 @@ async function run() {
success('Stylesheet migration complete.')
}

if (parsedConfig) {
// PostCSS config migration
await migratePostCSSConfig(process.cwd())
}

try {
// Upgrade Tailwind CSS
await pkg('add tailwindcss@next', process.cwd())
} catch {}

// Figure out if we made any changes
if (isRepoDirty()) {
success('Verify the changes and commit them to your repository.')
Expand Down
95 changes: 95 additions & 0 deletions packages/@tailwindcss-upgrade/src/migrate-postcss.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import { pkg } from './utils/packages'
import { info, success, warn } from './utils/renderer'

// Migrates simple PostCSS setups. This is to cover non-dynamic config files
// similar to the ones we have all over our docs:
//
// ```js
// module.exports = {
// plugins: {
// tailwindcss: {},
// autoprefixer: {},
// }
// }
export async function migratePostCSSConfig(base: string) {
let configPath = await detectConfigPath(base)
if (configPath === null) {
// TODO: We can look for an eventual config inside package.json
return
}

info(`Attempt to upgrade the PostCSS config in file: ${configPath}`)

let isSimpleConfig = await isSimplePostCSSConfig(base, configPath)
if (!isSimpleConfig) {
warn(`The PostCSS config contains dynamic JavaScript and can not be automatically migrated.`)
return
}

let didAddPostcssClient = false
let didRemoveAutoprefixer = false

let fullPath = path.resolve(base, configPath)
let content = await fs.readFile(fullPath, 'utf-8')
let lines = content.split('\n')
let newLines: string[] = []
for (let line of lines) {
if (line.includes('tailwindcss:')) {
didAddPostcssClient = true
newLines.push(line.replace('tailwindcss:', `'@tailwindcss/postcss':`))
} else if (line.includes('autoprefixer:')) {
didRemoveAutoprefixer = true
} else {
newLines.push(line)
}
}
await fs.writeFile(fullPath, newLines.join('\n'))

if (didAddPostcssClient) {
try {
await pkg('add -D @tailwindcss/postcss@next', base)
} catch {}
}
if (didRemoveAutoprefixer) {
try {
await pkg('remove autoprefixer', base)
} catch {}
}

success(`PostCSS config in file ${configPath} has been upgraded.`)
}

const CONFIG_FILE_LOCATIONS = [
'.postcssrc.js',
'.postcssrc.mjs',
'.postcssrc.cjs',
'.postcssrc.ts',
'.postcssrc.mts',
'.postcssrc.cts',
'postcss.config.js',
'postcss.config.mjs',
'postcss.config.cjs',
'postcss.config.ts',
'postcss.config.mts',
'postcss.config.cts',
]
async function detectConfigPath(base: string): Promise<null | string> {
for (let file of CONFIG_FILE_LOCATIONS) {
let fullPath = path.resolve(base, file)
try {
await fs.access(fullPath)
return file
} catch {}
}
return null
}

async function isSimplePostCSSConfig(base: string, configPath: string): Promise<boolean> {
let fullPath = path.resolve(base, configPath)
let content = await fs.readFile(fullPath, 'utf-8')
return (
content.includes('tailwindcss:') && !(content.includes('require') || content.includes('import'))
)
}
61 changes: 61 additions & 0 deletions packages/@tailwindcss-upgrade/src/utils/packages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { execSync } from 'node:child_process'
import fs from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
import { warn } from './renderer'

let didWarnAboutPackageManager = false

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,
})
}

async function detectPackageManager(base: string): Promise<null | string> {
do {
// 1. Check package.json for a `packageManager` field
let packageJsonPath = resolve(base, 'package.json')
try {
let packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8')
let packageJson = JSON.parse(packageJsonContent)
if (packageJson.packageManager) {
if (packageJson.packageManager.includes('yarn')) {
return 'yarn'
}
if (packageJson.packageManager.includes('pnpm')) {
return 'pnpm'
}
if (packageJson.packageManager.includes('npm')) {
return 'npm'
}
}
} catch {}

// 2. Check for common lockfiles
try {
await fs.access(resolve(base, 'pnpm-lock.yaml'))
return 'pnpm'
} catch {}

try {
await fs.access(resolve(base, 'yarn.lock'))
return 'yarn'
} catch {}

try {
await fs.access(resolve(base, 'package-lock.json'))
return 'npm'
} catch {}

// 3. If no lockfile is found, we might be in a monorepo
base = dirname(base)
} while (true)
}