Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
add codemod tooling
+ add codemod for migrating `@apply`
  • Loading branch information
RobinMalfait committed Sep 16, 2024
commit 3d508f2d1072671ceaffb195d2a81694e2089e9d
12 changes: 9 additions & 3 deletions packages/@tailwindcss-upgrade/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
"build": "tsup-node",
"dev": "pnpm run build -- --watch"
},
"bin": {
"tailwindcss-upgrade": "./dist/index.mjs"
},
"exports": {
"./package.json": "./package.json"
},
Expand All @@ -26,10 +29,13 @@
"access": "public"
},
"dependencies": {
"postcss-import": "^16.1.0",
"postcss": "^8.4.41"
"picocolors": "^1.0.1",
"postcss": "^8.4.41",
"postcss-import": "^16.1.0"
},
"devDependencies": {
"@types/postcss-import": "^14.0.3"
"@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,
},
}
}
28 changes: 28 additions & 0 deletions packages/@tailwindcss-upgrade/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import dedent from 'dedent'
import { expect, it } from 'vitest'
import { migrateContents } from './migrate'

const css = dedent

it('should print the input as-is', async () => {
expect(
await migrateContents(
css`
/* above */
.foo/* after */ {
/* above */
color: /* before */ red /* after */;
/* below */
}
`,
expect.getState().testPath,
),
).toMatchInlineSnapshot(`
"/* above */
.foo/* after */ {
/* above */
color: /* before */ red /* after */;
/* below */
}"
`)
})
54 changes: 50 additions & 4 deletions packages/@tailwindcss-upgrade/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,53 @@
#!/usr/bin/env node

console.log({
argv: process.argv,
})
import { execSync } from 'node:child_process'
import path from 'node:path'
import pc from 'picocolors'
import { help } from '../../@tailwindcss-cli/src/commands/help'
import { args, type Arg } from '../../@tailwindcss-cli/src/utils/args'
import { eprintln, header, highlight, wordWrap } from '../../@tailwindcss-cli/src/utils/renderer'
import { migrate } from './migrate'

process.exit(0)
const options = {
'--help': { type: 'boolean', description: 'Display usage information', alias: '-h' },
'--force': { type: 'boolean', description: 'Force the migration', alias: '-f' },
'--version': { type: 'boolean', description: 'Display the version number', alias: '-v' },
} satisfies Arg
const flags = args(options)

if (flags['--help']) {
help({
usage: ['npx @tailwindcss/upgrade'],
options,
})
process.exit(0)
}

const file = flags._[0]

async function run() {
eprintln(header())
eprintln()

if (!flags['--force']) {
let stdout = execSync('git status --porcelain', { encoding: 'utf-8' })
if (stdout.trim()) {
wordWrap(
'Git directory is not clean. Please stash or commit your changes before migrating.',
process.stderr.columns - 5 - 4,
).map((line) => eprintln(`${pc.red('\u2502')} ${line}`))
wordWrap(
`You may use the ${highlight('--force')} flag to override this safety check.`,
process.stderr.columns - 2 - 4,
).map((line) => eprintln(`${pc.red('\u2502')} ${line}`))
eprintln()
process.exit(1)
}
}

await migrate(path.resolve(process.cwd(), file))
}

run()
.then(() => process.exit(0))
.catch(() => process.exit(1))
36 changes: 36 additions & 0 deletions packages/@tailwindcss-upgrade/src/migrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { execSync } from 'node:child_process'
import fs from 'node:fs/promises'
import path from 'node:path'
import pc from 'picocolors'
import postcss from 'postcss'
import { eprintln, wordWrap } from '../../@tailwindcss-cli/src/utils/renderer'
import { migrateAtApply } from './codemods/migrate-at-apply'

export async function migrateContents(contents: string, file?: string) {
return postcss()
.use(migrateAtApply())
.process(contents, { from: file })
.then((result) => result.css)
}

export async function migrate(file: string) {
let fullPath = path.resolve(process.cwd(), file)
let contents = await fs.readFile(fullPath, 'utf-8')

await fs.writeFile(fullPath, await migrateContents(contents, fullPath))

let stdout = execSync('git status --porcelain', { encoding: 'utf-8' })
if (stdout.trim()) {
wordWrap(
'Migration complete. Verify the changes and commit them to your repository.',
process.stderr.columns - 5 - 4,
).map((line) => eprintln(`${pc.green('\u2502')} ${line}`))
eprintln()
} else {
wordWrap(
'Migration complete. No changes were made to your repository.',
process.stderr.columns - 5 - 4,
).map((line) => eprintln(`${pc.green('\u2502')} ${line}`))
eprintln()
}
}
Loading