Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
40 changes: 40 additions & 0 deletions packages/@tailwindcss-upgrade/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<p align="center">
<a href="https://tailwindcss.com" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/tailwindlabs/tailwindcss/HEAD/.github/logo-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/tailwindlabs/tailwindcss/HEAD/.github/logo-light.svg">
<img alt="Tailwind CSS" src="https://raw.githubusercontent.com/tailwindlabs/tailwindcss/HEAD/.github/logo-light.svg" width="350" height="70" style="max-width: 100%;">
</picture>
</a>
</p>

<p align="center">
A utility-first CSS framework for rapidly building custom user interfaces.
</p>

<p align="center">
<a href="https://github.com/tailwindlabs/tailwindcss/actions"><img src="https://img.shields.io/github/actions/workflow/status/tailwindlabs/tailwindcss/ci.yml?branch=next" alt="Build Status"></a>
<a href="https://www.npmjs.com/package/tailwindcss"><img src="https://img.shields.io/npm/dt/tailwindcss.svg" alt="Total Downloads"></a>
<a href="https://github.com/tailwindcss/tailwindcss/releases"><img src="https://img.shields.io/npm/v/tailwindcss.svg" alt="Latest Release"></a>
<a href="https://github.com/tailwindcss/tailwindcss/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/tailwindcss.svg" alt="License"></a>
</p>

---

## Documentation

For full documentation, visit [tailwindcss.com](https://tailwindcss.com).

## Community

For help, discussion about best practices, or any other conversation that would benefit from being searchable:

[Discuss Tailwind CSS on GitHub](https://github.com/tailwindcss/tailwindcss/discussions)

For chatting with others using the framework:

[Join the Tailwind CSS Discord Server](https://discord.gg/7NF8GNe)

## Contributing

If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/next/.github/CONTRIBUTING.md) **before submitting a pull request**.
45 changes: 45 additions & 0 deletions packages/@tailwindcss-upgrade/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "@tailwindcss/upgrade",
"version": "4.0.0-alpha.24",
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/tailwindlabs/tailwindcss.git",
"directory": "packages/@tailwindcss-cli"
},
"bugs": "https://github.com/tailwindlabs/tailwindcss/issues",
"homepage": "https://tailwindcss.com",
"scripts": {
"lint": "tsc --noEmit",
"build": "tsup-node",
"dev": "pnpm run build -- --watch"
},
"bin": {
"tailwindcss-upgrade": "./dist/index.mjs"
},
"exports": {
"./package.json": "./package.json"
},
"files": [
"dist"
],
"publishConfig": {
"provenance": true,
"access": "public"
},
"dependencies": {
"enhanced-resolve": "^5.17.1",
"fast-glob": "^3.3.2",
"mri": "^1.2.0",
"picocolors": "^1.0.1",
"postcss": "^8.4.41",
"postcss-import": "^16.1.0",
"tailwindcss": "workspace:^"
},
"devDependencies": {
"@types/node": "catalog:",
"@types/postcss-import": "^14.0.3",
"dedent": "1.5.3"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import dedent from 'dedent'
import postcss from 'postcss'
import { expect, it } from 'vitest'
import { migrateAtApply } from './migrate-at-apply'

const css = dedent

function migrate(input: string) {
return postcss()
.use(migrateAtApply())
.process(input, { from: expect.getState().testPath })
.then((result) => result.css)
}

it('should not migrate `@apply`, when there are no issues', async () => {
expect(
await migrate(css`
.foo {
@apply flex flex-col items-center;
}
`),
).toMatchInlineSnapshot(`
".foo {
@apply flex flex-col items-center;
}"
`)
})

it('should append `!` to each utility, when using `!important`', async () => {
expect(
await migrate(css`
.foo {
@apply flex flex-col !important;
}
`),
).toMatchInlineSnapshot(`
".foo {
@apply flex! flex-col!;
}"
`)
})

// TODO: Handle SCSS syntax
it.skip('should append `!` to each utility, when using `#{!important}`', async () => {
expect(
await migrate(css`
.foo {
@apply flex flex-col #{!important};
}
`),
).toMatchInlineSnapshot(`
".foo {
@apply flex! flex-col!;
}"
`)
})

it('should move the legacy `!` prefix, to the new `!` postfix notation', async () => {
expect(
await migrate(css`
.foo {
@apply !flex flex-col! hover:!items-start items-center;
}
`),
).toMatchInlineSnapshot(`
".foo {
@apply flex! flex-col! hover:items-start! items-center;
}"
`)
})
43 changes: 43 additions & 0 deletions packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { AtRule, Plugin } from 'postcss'
import { segment } from '../../../tailwindcss/src/utils/segment'

export function migrateAtApply(): Plugin {
function migrate(atRule: AtRule) {
let utilities = atRule.params.split(/(\s+)/)
let important =
utilities[utilities.length - 1] === '!important' ||
utilities[utilities.length - 1] === '#{!important}' // Sass/SCSS

if (important) utilities.pop() // Remove `!important`

let params = utilities.map((part) => {
// Keep whitespace
if (part.trim() === '') return part

let variants = segment(part, ':')
let utility = variants.pop()!

// Apply the important modifier to all the rules if necessary
if (important && utility[0] !== '!' && utility[utility.length - 1] !== '!') {
utility += '!'
}

// Migrate the important modifier to the end of the utility
if (utility[0] === '!') {
utility = `${utility.slice(1)}!`
}

// Reconstruct the utility with the variants
return [...variants, utility].join(':')
})

atRule.params = params.join('').trim()
}

return {
postcssPlugin: '@tailwindcss/upgrade/migrate-at-apply',
AtRule: {
apply: migrate,
},
}
}
170 changes: 170 additions & 0 deletions packages/@tailwindcss-upgrade/src/commands/help/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import pc from 'picocolors'
import type { Arg } from '../../utils/args'
import { UI, header, highlight, indent, println, wordWrap } from '../../utils/renderer'

export function help({
invalid,
usage,
options,
}: {
invalid?: string
usage?: string[]
options?: Arg
}) {
// Available terminal width
let width = process.stdout.columns

// Render header
println(header())

// Render the invalid command
if (invalid) {
println()
println(`${pc.dim('Invalid command:')} ${invalid}`)
}

// Render usage
if (usage && usage.length > 0) {
println()
println(pc.dim('Usage:'))
for (let [idx, example] of usage.entries()) {
// Split the usage example into the command and its options. This allows
// us to wrap the options based on the available width of the terminal.
let command = example.slice(0, example.indexOf('['))
let options = example.slice(example.indexOf('['))

// Make the options dimmed, to make them stand out less than the command
// itself.
options = options.replace(/\[.*?\]/g, (option) => pc.dim(option))

// The space between the command and the options.
let space = 1

// Wrap the options based on the available width of the terminal.
let lines = wordWrap(options, width - UI.indent - command.length - space)

// Print an empty line between the usage examples if we need to split due
// to width constraints. This ensures that the usage examples are visually
// separated.
//
// E.g.: when enough space is available
//
// ```
// Usage:
// tailwindcss build [--input input.css] [--output output.css] [--watch] [options...]
// tailwindcss other [--watch] [options...]
// ```
//
// E.g.: when not enough space is available
//
// ```
// Usage:
// tailwindcss build [--input input.css] [--output output.css]
// [--watch] [options...]
//
// tailwindcss other [--watch] [options...]
// ```
if (lines.length > 1 && idx !== 0) {
println()
}

// Print the usage examples based on available width of the terminal.
//
// E.g.: when enough space is available
//
// ```
// Usage:
// tailwindcss [--input input.css] [--output output.css] [--watch] [options...]
// ```
//
// E.g.: when not enough space is available
//
// ```
// Usage:
// tailwindcss [--input input.css] [--output output.css]
// [--watch] [options...]
// ```
//
// > Note how the second line is indented to align with the first line.
println(indent(`${command}${lines.shift()}`))
for (let line of lines) {
println(indent(line, command.length))
}
}
}

// Render options
if (options) {
// Track the max alias length, this is used to indent the options that don't
// have an alias such that everything is aligned properly.
let maxAliasLength = 0
for (let { alias } of Object.values(options)) {
if (alias) {
maxAliasLength = Math.max(maxAliasLength, alias.length)
}
}

// The option strings, which are the combination of the `alias` and the
// `flag`, with the correct spacing.
let optionStrings: string[] = []

// Track the max option length, which is the longest combination of an
// `alias` followed by `, ` and followed by the `flag`.
let maxOptionLength = 0

for (let [flag, { alias }] of Object.entries(options)) {
// The option string, which is the combination of the alias and the flag
// but already properly indented based on the other aliases to ensure
// everything is aligned properly.
let option = [
alias ? `${alias.padStart(maxAliasLength)}` : alias,
alias ? flag : ' '.repeat(maxAliasLength + 2 /* `, `.length */) + flag,
]
.filter(Boolean)
.join(', ')

optionStrings.push(option)
maxOptionLength = Math.max(maxOptionLength, option.length)
}

println()
println(pc.dim('Options:'))

// The minimum amount of dots between the option and the description.
let minimumGap = 8

for (let { description, default: defaultValue = null } of Object.values(options)) {
// The option to render
let option = optionStrings.shift() as string

// The amount of dots to show between the option and the description.
let dotCount = minimumGap + (maxOptionLength - option.length)

// To account for the space before and after the dots.
let spaces = 2

// The available width remaining for the description.
let availableWidth = width - option.length - dotCount - spaces - UI.indent

// Wrap the description and the default value (if present), based on the
// available width.
let lines = wordWrap(
defaultValue !== null
? `${description} ${pc.dim(`[default:\u202F${highlight(`${defaultValue}`)}]`)}`
: description,
availableWidth,
)

// Print the option, the spacer dots and the start of the description.
println(
indent(`${pc.blue(option)} ${pc.dim(pc.gray('\u00B7')).repeat(dotCount)} ${lines.shift()}`),
)

// Print the remaining lines of the description, indenting them to align
// with the start of the description.
for (let line of lines) {
println(indent(`${' '.repeat(option.length + dotCount + spaces)}${line}`))
}
}
}
}
Loading