mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2026-01-18 16:17:36 +00:00
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>
333 lines
10 KiB
TypeScript
333 lines
10 KiB
TypeScript
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: {
|
|
// 'postcss-import': {},
|
|
// 'tailwindcss/nesting': 'postcss-nesting',
|
|
// tailwindcss: {},
|
|
// autoprefixer: {},
|
|
// }
|
|
// }
|
|
export async function migratePostCSSConfig(base: string) {
|
|
let didMigrate = false
|
|
let didAddPostcssClient = false
|
|
let didRemoveAutoprefixer = false
|
|
let didRemovePostCSSImport = false
|
|
|
|
// Priority 1: Handle JS config files
|
|
let jsConfigPath = await detectJSConfigPath(base)
|
|
if (jsConfigPath) {
|
|
let result = await migratePostCSSJSConfig(base, jsConfigPath)
|
|
|
|
if (result) {
|
|
didMigrate = true
|
|
didAddPostcssClient = result.didAddPostcssClient
|
|
didRemoveAutoprefixer = result.didRemoveAutoprefixer
|
|
didRemovePostCSSImport = result.didRemovePostCSSImport
|
|
}
|
|
}
|
|
|
|
// Priority 2: Handle package.json config
|
|
let packageJsonPath = path.resolve(base, 'package.json')
|
|
let packageJson
|
|
try {
|
|
packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'))
|
|
} catch {}
|
|
if (!didMigrate && packageJson && 'postcss' in packageJson) {
|
|
let result = await migratePostCSSJsonConfig(base, packageJson.postcss)
|
|
|
|
if (result) {
|
|
await fs.writeFile(
|
|
packageJsonPath,
|
|
JSON.stringify({ ...packageJson, postcss: result?.json }, null, 2),
|
|
)
|
|
|
|
didMigrate = true
|
|
didAddPostcssClient = result.didAddPostcssClient
|
|
didRemoveAutoprefixer = result.didRemoveAutoprefixer
|
|
didRemovePostCSSImport = result.didRemovePostCSSImport
|
|
}
|
|
}
|
|
|
|
// Priority 3: JSON based postcss config files
|
|
let jsonConfigPath = await detectJSONConfigPath(base)
|
|
let jsonConfig: null | any = null
|
|
if (!didMigrate && jsonConfigPath) {
|
|
try {
|
|
jsonConfig = JSON.parse(await fs.readFile(jsonConfigPath, 'utf-8'))
|
|
} catch {}
|
|
if (jsonConfig) {
|
|
let result = await migratePostCSSJsonConfig(base, jsonConfig)
|
|
|
|
if (result) {
|
|
await fs.writeFile(jsonConfigPath, JSON.stringify(result.json, null, 2))
|
|
|
|
didMigrate = true
|
|
didAddPostcssClient = result.didAddPostcssClient
|
|
didRemoveAutoprefixer = result.didRemoveAutoprefixer
|
|
didRemovePostCSSImport = result.didRemovePostCSSImport
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!didMigrate) {
|
|
info(`No PostCSS config found, skipping migration.`)
|
|
return
|
|
}
|
|
|
|
if (didAddPostcssClient) {
|
|
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) as string[]
|
|
await pkg(base).remove(packagesToRemove)
|
|
} catch {}
|
|
}
|
|
|
|
success(`PostCSS config has been upgraded.`)
|
|
}
|
|
|
|
async function migratePostCSSJSConfig(
|
|
base: string,
|
|
configPath: string,
|
|
): Promise<{
|
|
didAddPostcssClient: boolean
|
|
didRemoveAutoprefixer: boolean
|
|
didRemovePostCSSImport: boolean
|
|
} | null> {
|
|
function isTailwindCSSPlugin(line: string) {
|
|
return /['"]?tailwindcss['"]?\: ?\{\}/.test(line)
|
|
}
|
|
function isPostCSSImportPlugin(line: string) {
|
|
return /['"]?postcss-import['"]?\: ?\{\}/.test(line)
|
|
}
|
|
function isAutoprefixerPlugin(line: string) {
|
|
return /['"]?autoprefixer['"]?\: ?\{\}/.test(line)
|
|
}
|
|
function isTailwindCSSNestingPlugin(line: string) {
|
|
return /['"]tailwindcss\/nesting['"]\: ?(\{\}|['"]postcss-nesting['"])/.test(line)
|
|
}
|
|
|
|
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 null
|
|
}
|
|
|
|
let didAddPostcssClient = false
|
|
let didRemoveAutoprefixer = false
|
|
let didRemovePostCSSImport = 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 i = 0; i < lines.length; i++) {
|
|
let line = lines[i]
|
|
|
|
if (isTailwindCSSPlugin(line)) {
|
|
didAddPostcssClient = true
|
|
newLines.push(line.replace('tailwindcss:', `'@tailwindcss/postcss':`))
|
|
} else if (isAutoprefixerPlugin(line)) {
|
|
didRemoveAutoprefixer = true
|
|
} else if (isPostCSSImportPlugin(line)) {
|
|
// Check that there are no unknown plugins before the tailwindcss plugin
|
|
let hasUnknownPluginsBeforeTailwindCSS = false
|
|
for (let j = i + 1; j < lines.length; j++) {
|
|
let nextLine = lines[j]
|
|
if (isTailwindCSSPlugin(nextLine)) {
|
|
break
|
|
}
|
|
if (isTailwindCSSNestingPlugin(nextLine)) {
|
|
continue
|
|
}
|
|
hasUnknownPluginsBeforeTailwindCSS = true
|
|
break
|
|
}
|
|
|
|
if (!hasUnknownPluginsBeforeTailwindCSS) {
|
|
didRemovePostCSSImport = true
|
|
} else {
|
|
newLines.push(line)
|
|
}
|
|
} else if (isTailwindCSSNestingPlugin(line)) {
|
|
// Check if the following rule is the tailwindcss plugin
|
|
let nextLine = lines[i + 1]
|
|
if (isTailwindCSSPlugin(nextLine)) {
|
|
// Since this plugin is bundled with `tailwindcss`, we don't need to
|
|
// clean up a package when deleting this line.
|
|
} else {
|
|
newLines.push(line)
|
|
}
|
|
} else {
|
|
newLines.push(line)
|
|
}
|
|
}
|
|
await fs.writeFile(fullPath, newLines.join('\n'))
|
|
|
|
return { didAddPostcssClient, didRemoveAutoprefixer, didRemovePostCSSImport }
|
|
}
|
|
|
|
async function migratePostCSSJsonConfig(
|
|
base: string,
|
|
json: any,
|
|
): Promise<{
|
|
json: any
|
|
didAddPostcssClient: boolean
|
|
didRemoveAutoprefixer: boolean
|
|
didRemovePostCSSImport: boolean
|
|
} | null> {
|
|
function isTailwindCSSPlugin(plugin: string, options: any) {
|
|
return plugin === 'tailwindcss' && isEmptyObject(options)
|
|
}
|
|
function isPostCSSImportPlugin(plugin: string, options: any) {
|
|
return plugin === 'postcss-import' && isEmptyObject(options)
|
|
}
|
|
function isAutoprefixerPlugin(plugin: string, options: any) {
|
|
return plugin === 'autoprefixer' && isEmptyObject(options)
|
|
}
|
|
function isTailwindCSSNestingPlugin(plugin: string, options: any) {
|
|
return (
|
|
plugin === 'tailwindcss/nesting' && (options === 'postcss-nesting' || isEmptyObject(options))
|
|
)
|
|
}
|
|
|
|
let didAddPostcssClient = false
|
|
let didRemoveAutoprefixer = false
|
|
let didRemovePostCSSImport = false
|
|
|
|
let plugins = Object.entries(json.plugins || {})
|
|
|
|
let newPlugins: [string, any][] = []
|
|
for (let i = 0; i < plugins.length; i++) {
|
|
let [plugin, options] = plugins[i]
|
|
|
|
if (isTailwindCSSPlugin(plugin, options)) {
|
|
didAddPostcssClient = true
|
|
newPlugins.push(['@tailwindcss/postcss', options])
|
|
} else if (isAutoprefixerPlugin(plugin, options)) {
|
|
didRemoveAutoprefixer = true
|
|
} else if (isPostCSSImportPlugin(plugin, options)) {
|
|
// Check that there are no unknown plugins before the tailwindcss plugin
|
|
let hasUnknownPluginsBeforeTailwindCSS = false
|
|
for (let j = i + 1; j < plugins.length; j++) {
|
|
let [nextPlugin, nextOptions] = plugins[j]
|
|
if (isTailwindCSSPlugin(nextPlugin, nextOptions)) {
|
|
break
|
|
}
|
|
if (isTailwindCSSNestingPlugin(nextPlugin, nextOptions)) {
|
|
continue
|
|
}
|
|
hasUnknownPluginsBeforeTailwindCSS = true
|
|
break
|
|
}
|
|
|
|
if (!hasUnknownPluginsBeforeTailwindCSS) {
|
|
didRemovePostCSSImport = true
|
|
} else {
|
|
newPlugins.push([plugin, options])
|
|
}
|
|
} else if (isTailwindCSSNestingPlugin(plugin, options)) {
|
|
// Check if the following rule is the tailwindcss plugin
|
|
let [nextPlugin, nextOptions] = plugins[i + 1]
|
|
if (isTailwindCSSPlugin(nextPlugin, nextOptions)) {
|
|
// Since this plugin is bundled with `tailwindcss`, we don't need to
|
|
// clean up a package when deleting this line.
|
|
} else {
|
|
newPlugins.push([plugin, options])
|
|
}
|
|
} else {
|
|
newPlugins.push([plugin, options])
|
|
}
|
|
}
|
|
|
|
return {
|
|
json: { ...json, plugins: Object.fromEntries(newPlugins) },
|
|
didAddPostcssClient,
|
|
didRemoveAutoprefixer,
|
|
didRemovePostCSSImport,
|
|
}
|
|
}
|
|
|
|
const JS_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 detectJSConfigPath(base: string): Promise<null | string> {
|
|
for (let file of JS_CONFIG_FILE_LOCATIONS) {
|
|
let fullPath = path.resolve(base, file)
|
|
try {
|
|
await fs.access(fullPath)
|
|
return file
|
|
} catch {}
|
|
}
|
|
return null
|
|
}
|
|
|
|
const JSON_CONFIG_FILE_LOCATIONS = [
|
|
'.postcssrc',
|
|
'.postcssrc.json',
|
|
// yaml syntax is not supported
|
|
// '.postcssrc.yml'
|
|
]
|
|
async function detectJSONConfigPath(base: string): Promise<null | string> {
|
|
for (let file of JSON_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') ||
|
|
// Adding a space at the end to not match `'postcss-import'`
|
|
content.includes('import ')
|
|
)
|
|
)
|
|
}
|
|
|
|
function isEmptyObject(obj: any) {
|
|
return typeof obj === 'object' && obj !== null && Object.keys(obj).length === 0
|
|
}
|