Migrate simple PostCSS setup (#14612)
This PR attempts to detect simple postcss setups: These are setups that do not load dynamic modules and are based off the examples we are [recommending in our docs](https://tailwindcss.com/docs/installation/using-postcss). We detect wether a config is appropriate by having it use the object plugin config and by not requiring any other modules: ```js module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, } ``` When we find such a config file, we will go over it line-by-line and attempt to: - Upgrade `tailwindcss:` to `'@tailwindcss/postcss':` - Remove `autoprefixer` if used We then attempt to install and remove the respective npm packages based on the package manger we detect. And since we now have logic to upgrade packages, this also makes sure to install `tailwindcss@next` at the end of a sucessful migration. --------- Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
parent
3ae22f1f9d
commit
75b906643c
6 changed files with 648 additions and 3 deletions
|
|
@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
- Add support for `tailwindcss/colors.js`, `tailwindcss/defaultTheme.js`, and `tailwindcss/plugin.js` exports ([#14595](https://github.com/tailwindlabs/tailwindcss/pull/14595))
|
||||
- Support `keyframes` in JS config file themes ([#14594](https://github.com/tailwindlabs/tailwindcss/pull/14594))
|
||||
- _Upgrade (experimental)_: Migrate v3 PostCSS setups to v4 in some cases ([#14612](https://github.com/tailwindlabs/tailwindcss/pull/14612))
|
||||
- _Upgrade (experimental)_: The upgrade tool now automatically discovers your JavaScript config ([#14597](https://github.com/tailwindlabs/tailwindcss/pull/14597))
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { expect } from 'vitest'
|
||||
import { css, html, js, json, test } from '../utils'
|
||||
|
||||
test(
|
||||
|
|
@ -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'),
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -265,6 +272,233 @@ test(
|
|||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'migrates a simple postcss setup',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"postcss": "^8",
|
||||
"postcss-cli": "^10",
|
||||
"postcss-import": "^16",
|
||||
"autoprefixer": "^10",
|
||||
"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: {
|
||||
'postcss-import': {},
|
||||
'tailwindcss/nesting': 'postcss-nesting',
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
`,
|
||||
'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')
|
||||
|
||||
await fs.expectFileToContain(
|
||||
'postcss.config.js',
|
||||
js`
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
}
|
||||
`,
|
||||
)
|
||||
await fs.expectFileToContain('src/index.css', css`@import 'tailwindcss';`)
|
||||
await fs.expectFileToContain(
|
||||
'src/index.html',
|
||||
// prettier-ignore
|
||||
js`
|
||||
<div class="bg-[var(--my-red)]"></div>
|
||||
`,
|
||||
)
|
||||
|
||||
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.dependencies).not.toHaveProperty('postcss-import')
|
||||
expect(packageJson.devDependencies).toMatchObject({
|
||||
'@tailwindcss/postcss': expect.stringContaining('4.0.0'),
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'migrates a postcss setup using package.json config',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"postcss": "^8",
|
||||
"postcss-cli": "^10",
|
||||
"postcss-import": "^16",
|
||||
"autoprefixer": "^10",
|
||||
"tailwindcss": "^3",
|
||||
"@tailwindcss/upgrade": "workspace:^"
|
||||
},
|
||||
"postcss": {
|
||||
"plugins": {
|
||||
"postcss-import": {},
|
||||
"tailwindcss/nesting": "postcss-nesting",
|
||||
"tailwindcss": {},
|
||||
"autoprefixer": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
'tailwind.config.js': js`
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{html,js}'],
|
||||
}
|
||||
`,
|
||||
'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')
|
||||
|
||||
await fs.expectFileToContain('src/index.css', css`@import 'tailwindcss';`)
|
||||
await fs.expectFileToContain(
|
||||
'src/index.html',
|
||||
// prettier-ignore
|
||||
js`
|
||||
<div class="bg-[var(--my-red)]"></div>
|
||||
`,
|
||||
)
|
||||
|
||||
let packageJsonContent = await fs.read('package.json')
|
||||
let packageJson = JSON.parse(packageJsonContent)
|
||||
expect(packageJson.postcss).toMatchInlineSnapshot(`
|
||||
{
|
||||
"plugins": {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
}
|
||||
`)
|
||||
|
||||
expect(packageJson.dependencies).toMatchObject({
|
||||
tailwindcss: expect.stringContaining('4.0.0'),
|
||||
})
|
||||
expect(packageJson.dependencies).not.toHaveProperty('autoprefixer')
|
||||
expect(packageJson.dependencies).not.toHaveProperty('postcss-import')
|
||||
expect(packageJson.devDependencies).toMatchObject({
|
||||
'@tailwindcss/postcss': expect.stringContaining('4.0.0'),
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'migrates a postcss setup using a json based config file',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"postcss": "^8",
|
||||
"postcss-cli": "^10",
|
||||
"postcss-import": "^16",
|
||||
"autoprefixer": "^10",
|
||||
"tailwindcss": "^3",
|
||||
"@tailwindcss/upgrade": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'.postcssrc.json': json`
|
||||
{
|
||||
"plugins": {
|
||||
"postcss-import": {},
|
||||
"tailwindcss/nesting": "postcss-nesting",
|
||||
"tailwindcss": {},
|
||||
"autoprefixer": {}
|
||||
}
|
||||
}
|
||||
`,
|
||||
'tailwind.config.js': js`
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{html,js}'],
|
||||
}
|
||||
`,
|
||||
'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')
|
||||
|
||||
await fs.expectFileToContain('src/index.css', css`@import 'tailwindcss';`)
|
||||
await fs.expectFileToContain(
|
||||
'src/index.html',
|
||||
// prettier-ignore
|
||||
js`
|
||||
<div class="bg-[var(--my-red)]"></div>
|
||||
`,
|
||||
)
|
||||
|
||||
let jsonConfigContent = await fs.read('.postcssrc.json')
|
||||
let jsonConfig = JSON.parse(jsonConfigContent)
|
||||
expect(jsonConfig).toMatchInlineSnapshot(`
|
||||
{
|
||||
"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.dependencies).not.toHaveProperty('postcss-import')
|
||||
expect(packageJson.devDependencies).toMatchObject({
|
||||
'@tailwindcss/postcss': expect.stringContaining('4.0.0'),
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
`migrates prefixes even if other files have unprefixed versions of the candidate`,
|
||||
{
|
||||
|
|
@ -297,7 +531,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs }) => {
|
||||
await exec('npx @tailwindcss/upgrade -c tailwind.config.js')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
await fs.expectFileToContain('src/index.html', html`
|
||||
<div class="flex"></div>
|
||||
|
|
@ -335,7 +569,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs }) => {
|
||||
await exec('npx @tailwindcss/upgrade -c tailwind.config.js')
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
|
||||
await fs.expectFileToContain(
|
||||
'src/index.html',
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ export function test(
|
|||
) {
|
||||
return (only || (!process.env.CI && debug) ? defaultTest.only : defaultTest)(
|
||||
name,
|
||||
{ timeout: TEST_TIMEOUT, retry: debug ? 0 : 3 },
|
||||
{ timeout: TEST_TIMEOUT, retry: debug || only ? 0 : 3 },
|
||||
async (options) => {
|
||||
let rootDir = debug ? path.join(REPO_ROOT, '.debug') : TMP_ROOT
|
||||
await fs.mkdir(rootDir, { recursive: true })
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
@ -98,6 +100,16 @@ async function run() {
|
|||
success('Stylesheet migration complete.')
|
||||
}
|
||||
|
||||
{
|
||||
// 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.')
|
||||
|
|
|
|||
326
packages/@tailwindcss-upgrade/src/migrate-postcss.ts
Normal file
326
packages/@tailwindcss-upgrade/src/migrate-postcss.ts
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
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) {
|
||||
try {
|
||||
await pkg('add -D @tailwindcss/postcss@next', base)
|
||||
} catch {}
|
||||
}
|
||||
if (didRemoveAutoprefixer || didRemovePostCSSImport) {
|
||||
try {
|
||||
let packagesToRemove = [
|
||||
didRemoveAutoprefixer ? 'autoprefixer' : null,
|
||||
didRemovePostCSSImport ? 'postcss-import' : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
await pkg(`remove ${packagesToRemove}`, base)
|
||||
} 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
|
||||
}
|
||||
72
packages/@tailwindcss-upgrade/src/utils/packages.ts
Normal file
72
packages/@tailwindcss-upgrade/src/utils/packages.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
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('bun')) {
|
||||
return 'bun'
|
||||
}
|
||||
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, 'bun.lockb'))
|
||||
return 'bun'
|
||||
} catch {}
|
||||
try {
|
||||
await fs.access(resolve(base, 'bun.lock'))
|
||||
return 'bun'
|
||||
} catch {}
|
||||
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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue