This PR adds some initial tooling for codemods. We are currently only interested in migrating CSS files, so we will be using PostCSS under the hood to do this. This PR also implements the "migrate `@apply`" codemod from #14412. The usage will look like this: ```sh npx @tailwindcss/upgrade ``` You can pass in CSS files to transform as arguments: ```sh npx @tailwindcss/upgrade src/**/*.css ``` But, if none are provided, it will search for CSS files in the current directory and its subdirectories. ``` ≈ tailwindcss v4.0.0-alpha.24 │ No files provided. Searching for CSS files in the current │ directory and its subdirectories… │ Migration complete. Verify the changes and commit them to │ your repository. ``` The tooling also requires the Git repository to be in a clean state. This is a common convention to ensure that everything is undo-able. If we detect that the git repository is dirty, we will abort the migration. ``` ≈ tailwindcss v4.0.0-alpha.24 │ Git directory is not clean. Please stash or commit your │ changes before migrating. │ You may use the `--force` flag to override this safety │ check. ``` --- This PR alsoo adds CSS codemods for migrating existing `@apply` directives to the new version. This PR has the ability to migrate the following cases: --- In v4, the convention is to put the important modifier `!` at the end of the utility class instead of right before it. This makes it easier to reason about, especially when you are variants. Input: ```css .foo { @apply !flex flex-col! hover:!items-start items-center; } ``` Output: ```css .foo { @apply flex! flex-col! hover:items-start! items-center; } ``` --- In v4 we don't support `!important` as a marker at the end of `@apply` directives. Instead, you can append the `!` to each utility class to make it `!important`. Input: ```css .foo { @apply flex flex-col !important; } ``` Output: ```css .foo { @apply flex! flex-col!; } ```
130 lines
3.1 KiB
TypeScript
130 lines
3.1 KiB
TypeScript
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import { stripVTControlCharacters } from 'node:util'
|
|
import pc from 'picocolors'
|
|
import { resolve } from '../utils/resolve'
|
|
import { formatNanoseconds } from './format-ns'
|
|
|
|
export const UI = {
|
|
indent: 2,
|
|
}
|
|
export function header() {
|
|
return `${pc.italic(pc.bold(pc.blue('\u2248')))} tailwindcss ${pc.blue(`v${getVersion()}`)}`
|
|
}
|
|
|
|
export function highlight(file: string) {
|
|
return `${pc.dim(pc.blue('`'))}${pc.blue(file)}${pc.dim(pc.blue('`'))}`
|
|
}
|
|
|
|
/**
|
|
* Convert an `absolute` path to a `relative` path from the current working
|
|
* directory.
|
|
*/
|
|
export function relative(
|
|
to: string,
|
|
from = process.cwd(),
|
|
{ preferAbsoluteIfShorter = true } = {},
|
|
) {
|
|
let result = path.relative(from, to)
|
|
if (!result.startsWith('..')) {
|
|
result = `.${path.sep}${result}`
|
|
}
|
|
|
|
if (preferAbsoluteIfShorter && result.length > to.length) {
|
|
return to
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Wrap `text` into multiple lines based on the `width`.
|
|
*/
|
|
export function wordWrap(text: string, width: number) {
|
|
let words = text.split(' ')
|
|
let lines = []
|
|
|
|
let line = ''
|
|
let lineLength = 0
|
|
for (let word of words) {
|
|
let wordLength = stripVTControlCharacters(word).length
|
|
|
|
if (lineLength + wordLength + 1 > width) {
|
|
lines.push(line)
|
|
line = ''
|
|
lineLength = 0
|
|
}
|
|
|
|
line += (lineLength ? ' ' : '') + word
|
|
lineLength += wordLength + (lineLength ? 1 : 0)
|
|
}
|
|
|
|
if (lineLength) {
|
|
lines.push(line)
|
|
}
|
|
|
|
return lines
|
|
}
|
|
|
|
/**
|
|
* Format a duration in nanoseconds to a more human readable format.
|
|
*/
|
|
export function formatDuration(ns: bigint) {
|
|
let formatted = formatNanoseconds(ns)
|
|
|
|
if (ns <= 50 * 1e6) return pc.green(formatted)
|
|
if (ns <= 300 * 1e6) return pc.blue(formatted)
|
|
if (ns <= 1000 * 1e6) return pc.yellow(formatted)
|
|
|
|
return pc.red(formatted)
|
|
}
|
|
|
|
export function indent(value: string, offset = 0) {
|
|
return `${' '.repeat(offset + UI.indent)}${value}`
|
|
}
|
|
|
|
export function success(message: string, print = eprintln) {
|
|
wordWrap(message, process.stderr.columns - 3).map((line) => {
|
|
return print(`${pc.green('\u2502')} ${line}`)
|
|
})
|
|
print()
|
|
}
|
|
|
|
export function info(message: string, print = eprintln) {
|
|
wordWrap(message, process.stderr.columns - 3).map((line) => {
|
|
return print(`${pc.blue('\u2502')} ${line}`)
|
|
})
|
|
print()
|
|
}
|
|
|
|
export function error(message: string, print = eprintln) {
|
|
wordWrap(message, process.stderr.columns - 3).map((line) => {
|
|
return print(`${pc.red('\u2502')} ${line}`)
|
|
})
|
|
print()
|
|
}
|
|
|
|
export function warn(message: string, print = eprintln) {
|
|
wordWrap(message, process.stderr.columns - 3).map((line) => {
|
|
return print(`${pc.yellow('\u2502')} ${line}`)
|
|
})
|
|
print()
|
|
}
|
|
|
|
// Rust inspired functions to print to the console:
|
|
|
|
export function eprintln(value = '') {
|
|
process.stderr.write(`${value}\n`)
|
|
}
|
|
|
|
export function println(value = '') {
|
|
process.stdout.write(`${value}\n`)
|
|
}
|
|
|
|
function getVersion(): string {
|
|
if (typeof globalThis.__tw_version === 'string') {
|
|
return globalThis.__tw_version
|
|
}
|
|
let { version } = JSON.parse(fs.readFileSync(resolve('tailwindcss/package.json'), 'utf-8'))
|
|
return version
|
|
}
|