Skip to content

Commit

Permalink
Migrate simple PostCSS setup
Browse files Browse the repository at this point in the history
  • Loading branch information
philipp-spiess committed Oct 7, 2024
1 parent 14abf96 commit d2662d8
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 22 deletions.
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 @@ -262,8 +269,8 @@ test(
},
)

test.only(
'migrate a simple postcss setup',
test(
'fully migrate a simple postcss setup',
{
fs: {
'package.json': json`
Expand Down Expand Up @@ -302,27 +309,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 @@ -6,10 +6,12 @@ import type { Config } from 'tailwindcss'
import type { DesignSystem } from '../../tailwindcss/src/design-system'
import { help } from './commands/help'
import { migrate as migrateStylesheet } from './migrate'
import { migratePostCSSConfig } from './migrate-postcss'
import { migrate as migrateTemplate } from './template/migrate'
import { parseConfig } from './template/parseConfig'
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 @@ -123,6 +125,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)
}

0 comments on commit d2662d8

Please sign in to comment.