Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a0a3933
move `src/codemods/` to `/src/codemods/css/`
RobinMalfait Apr 1, 2025
2eb981c
move `src/template/` to `src/codemods/template/`
RobinMalfait Apr 1, 2025
8830db3
bail when `params` is `null`
RobinMalfait Apr 1, 2025
5a4cc1f
add missing `reference` flag
RobinMalfait Apr 1, 2025
604aaf5
move `spliceChangesIntoString` into `utils/` folder
RobinMalfait Apr 1, 2025
42c4ce7
move `migrateStylesheet` into `codemods/css/migrate.ts`
RobinMalfait Apr 1, 2025
eb27a68
move `analyzeStylesheets` into `codemods/css/analyze.ts`
RobinMalfait Apr 1, 2025
ee0e944
move `linkConfigsToStylesheets` into `codemods/css/link`
RobinMalfait Apr 1, 2025
1800781
move `splitStylesheets` into `codemods/css/split`
RobinMalfait Apr 1, 2025
1345345
move `migrate-js-config` to `codemods/config/migrate-js-config`
RobinMalfait Apr 1, 2025
00c67cd
move `migrate-postcss` to `codemods/config/migrate-postcss`
RobinMalfait Apr 1, 2025
18ace7d
move `migrate-prettier` to `codemods/config/migrate-prettier`
RobinMalfait Apr 1, 2025
5976df1
prefix files with `migrate-`
RobinMalfait Apr 1, 2025
c77b41e
hoist template migrations
RobinMalfait Apr 1, 2025
7792829
add `migrate` prefix to `arbitraryValueToBareValue`
RobinMalfait Apr 1, 2025
ab16e0e
add `migrate` prefix to `automaticVarInjection`
RobinMalfait Apr 1, 2025
5a8c0c0
add `migrate` prefix to `bgGradient`
RobinMalfait Apr 1, 2025
7610ad0
rename `handleEmptyArbitraryValues` to `migrateEmptyArbitraryValues`
RobinMalfait Apr 1, 2025
8827d60
add `migrate` prefix to `important`
RobinMalfait Apr 1, 2025
13378c7
add `migrate` prefix to `legacyArbitraryValues`
RobinMalfait Apr 1, 2025
91172b9
add `migrate` prefix to `legacyClasses`
RobinMalfait Apr 1, 2025
33143ec
add `migrate` prefix to `maxWidthScreen`
RobinMalfait Apr 1, 2025
c67c6c1
add `migrate` prefix to `modernizeArbitraryValues`
RobinMalfait Apr 1, 2025
90f73d4
add `migrate` prefix to `simpleLegacyClasses`
RobinMalfait Apr 1, 2025
9fc9818
add `migrate` prefix to `themeToVar`
RobinMalfait Apr 1, 2025
65cb1f7
add `migrate` prefix to `variantOrder`
RobinMalfait Apr 1, 2025
ef35399
rename `migratePrefix` to `migratePrefixValue`
RobinMalfait Apr 1, 2025
ec92fd4
add `migrate` prefix to `prefix`
RobinMalfait Apr 1, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,30 @@ import { Scanner } from '@tailwindcss/oxide'
import fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { loadModule } from '../../@tailwindcss-node/src/compile'
import defaultTheme from '../../tailwindcss/dist/default-theme'
import { atRule, toCss, type AstNode } from '../../tailwindcss/src/ast'
import { loadModule } from '../../../../@tailwindcss-node/src/compile'
import defaultTheme from '../../../../tailwindcss/dist/default-theme'
import { atRule, toCss, type AstNode } from '../../../../tailwindcss/src/ast'
import {
keyPathToCssProperty,
themeableValues,
} from '../../tailwindcss/src/compat/apply-config-to-theme'
import { keyframesToRules } from '../../tailwindcss/src/compat/apply-keyframes-to-theme'
import { resolveConfig, type ConfigFile } from '../../tailwindcss/src/compat/config/resolve-config'
import type { ResolvedConfig, ThemeConfig } from '../../tailwindcss/src/compat/config/types'
import { buildCustomContainerUtilityRules } from '../../tailwindcss/src/compat/container'
import { darkModePlugin } from '../../tailwindcss/src/compat/dark-mode'
import type { Config } from '../../tailwindcss/src/compat/plugin-api'
import type { DesignSystem } from '../../tailwindcss/src/design-system'
import { escape } from '../../tailwindcss/src/utils/escape'
} from '../../../../tailwindcss/src/compat/apply-config-to-theme'
import { keyframesToRules } from '../../../../tailwindcss/src/compat/apply-keyframes-to-theme'
import {
resolveConfig,
type ConfigFile,
} from '../../../../tailwindcss/src/compat/config/resolve-config'
import type { ResolvedConfig, ThemeConfig } from '../../../../tailwindcss/src/compat/config/types'
import { buildCustomContainerUtilityRules } from '../../../../tailwindcss/src/compat/container'
import { darkModePlugin } from '../../../../tailwindcss/src/compat/dark-mode'
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
import { escape } from '../../../../tailwindcss/src/utils/escape'
import {
isValidOpacityValue,
isValidSpacingMultiplier,
} from '../../tailwindcss/src/utils/infer-data-type'
import { findStaticPlugins, type StaticPluginOptions } from './utils/extract-static-plugins'
import { highlight, info, relative } from './utils/renderer'
} from '../../../../tailwindcss/src/utils/infer-data-type'
import { findStaticPlugins, type StaticPluginOptions } from '../../utils/extract-static-plugins'
import { highlight, info, relative } from '../../utils/renderer'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import { pkg } from './utils/packages'
import { highlight, info, relative, success, warn } from './utils/renderer'
import { pkg } from '../../utils/packages'
import { highlight, info, relative, 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:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import { pkg } from './utils/packages'
import { highlight, success } from './utils/renderer'
import { pkg } from '../../utils/packages'
import { highlight, success } from '../../utils/renderer'

export async function migratePrettierPlugin(base: string) {
let packageJsonPath = path.resolve(base, 'package.json')
Expand Down
293 changes: 293 additions & 0 deletions packages/@tailwindcss-upgrade/src/codemods/css/analyze.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
import { isGitIgnored } from 'globby'
import path from 'node:path'
import postcss, { type Result } from 'postcss'
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
import { segment } from '../../../../tailwindcss/src/utils/segment'
import { Stylesheet, type StylesheetConnection } from '../../stylesheet'
import { error, highlight, relative } from '../../utils/renderer'
import { resolveCssId } from '../../utils/resolve'

export async function analyze(stylesheets: Stylesheet[]) {
let isIgnored = await isGitIgnored()
let processingQueue: (() => Promise<Result>)[] = []
let stylesheetsByFile = new DefaultMap<string, Stylesheet | null>((file) => {
// We don't want to process ignored files (like node_modules)
if (isIgnored(file)) {
return null
}

try {
let sheet = Stylesheet.loadSync(file)

// Mutate incoming stylesheets to include the newly discovered sheet
stylesheets.push(sheet)

// Queue up the processing of this stylesheet
processingQueue.push(() => processor.process(sheet.root, { from: sheet.file! }))

return sheet
} catch {
return null
}
})

// Step 1: Record which `@import` rules point to which stylesheets
// and which stylesheets are parents/children of each other
let processor = postcss([
{
postcssPlugin: 'mark-import-nodes',
AtRule: {
import(node) {
// Find what the import points to
let id = node.params.match(/['"](.*)['"]/)?.[1]
if (!id) return

let basePath = node.source?.input.file
? path.dirname(node.source.input.file)
: process.cwd()

// Resolve the import to a file path
let resolvedPath: string | false = false
try {
// We first try to resolve the file as relative to the current file
// to mimic the behavior of `postcss-import` since that's what was
// used to resolve imports in Tailwind CSS v3.
if (id[0] !== '.') {
try {
resolvedPath = resolveCssId(`./${id}`, basePath)
} catch {}
}

if (!resolvedPath) {
resolvedPath = resolveCssId(id, basePath)
}
} catch (err) {
// Import is a URL, we don't want to process these, but also don't
// want to show an error message for them.
if (id.startsWith('http://') || id.startsWith('https://') || id.startsWith('//')) {
return
}

// Something went wrong, we can't resolve the import.
error(
`Failed to resolve import: ${highlight(id)} in ${highlight(relative(node.source?.input.file!, basePath))}. Skipping.`,
{ prefix: '↳ ' },
)
return
}

if (!resolvedPath) return

// Find the stylesheet pointing to the resolved path
let stylesheet = stylesheetsByFile.get(resolvedPath)

// If it _does not_ exist in stylesheets we don't care and skip it
// this is likely because its in node_modules or a workspace package
// that we don't want to modify
if (!stylesheet) return

// Mark the import node with the ID of the stylesheet it points to
// We will use these later to build lookup tables and modify the AST
node.raws.tailwind_destination_sheet_id = stylesheet.id

let parent = node.source?.input.file
? stylesheetsByFile.get(node.source.input.file)
: undefined

let layers: string[] = []

for (let part of segment(node.params, ' ')) {
if (!part.startsWith('layer(')) continue
if (!part.endsWith(')')) continue

layers.push(part.slice(6, -1).trim())
}

// Connect sheets together in a dependency graph
if (parent) {
let meta = { layers }
stylesheet.parents.add({ item: parent, meta })
parent.children.add({ item: stylesheet, meta })
}
},
},
},
])

// Seed the map with all the known stylesheets, and queue up the processing of
// each incoming stylesheet.
for (let sheet of stylesheets) {
if (sheet.file) {
stylesheetsByFile.set(sheet.file, sheet)
processingQueue.push(() => processor.process(sheet.root, { from: sheet.file ?? undefined }))
}
}

// Process all the stylesheets from step 1
while (processingQueue.length > 0) {
let task = processingQueue.shift()!
await task()
}

// ---

let commonPath = process.cwd()

function pathToString(path: StylesheetConnection[]) {
let parts: string[] = []

for (let connection of path) {
if (!connection.item.file) continue

let filePath = connection.item.file.replace(commonPath, '')
let layers = connection.meta.layers.join(', ')

if (layers.length > 0) {
parts.push(`${filePath} (layers: ${layers})`)
} else {
parts.push(filePath)
}
}

return parts.join(' <- ')
}

let lines: string[] = []

for (let sheet of stylesheets) {
if (!sheet.file) continue

let { convertiblePaths, nonConvertiblePaths } = sheet.analyzeImportPaths()
let isAmbiguous = convertiblePaths.length > 0 && nonConvertiblePaths.length > 0

if (!isAmbiguous) continue

sheet.canMigrate = false

let filePath = sheet.file.replace(commonPath, '')

for (let path of convertiblePaths) {
lines.push(`- ${filePath} <- ${pathToString(path)}`)
}

for (let path of nonConvertiblePaths) {
lines.push(`- ${filePath} <- ${pathToString(path)}`)
}
}

if (lines.length === 0) {
let tailwindRootLeafs = new Set<Stylesheet>()

for (let sheet of stylesheets) {
// If the current file already contains `@config`, then we can assume it's
// a Tailwind CSS root file.
sheet.root.walkAtRules('config', () => {
sheet.isTailwindRoot = true
return false
})
if (sheet.isTailwindRoot) continue

// If an `@tailwind` at-rule, or `@import "tailwindcss"` is present,
// then we can assume it's a file where Tailwind CSS might be configured.
//
// However, if 2 or more stylesheets exist with these rules that share a
// common parent, then we want to mark the common parent as the root
// stylesheet instead.
sheet.root.walkAtRules((node) => {
if (
node.name === 'tailwind' ||
(node.name === 'import' && node.params.match(/^["']tailwindcss["']/)) ||
(node.name === 'import' && node.params.match(/^["']tailwindcss\/.*?["']$/))
) {
sheet.isTailwindRoot = true
tailwindRootLeafs.add(sheet)
}
})
}

// Only a single Tailwind CSS root file exists, no need to do anything else.
if (tailwindRootLeafs.size <= 1) {
return
}

// Mark the common parent as the root file
{
// Group each sheet from tailwindRootLeafs by their common parent
let commonParents = new DefaultMap<Stylesheet, Set<Stylesheet>>(() => new Set<Stylesheet>())

// Seed common parents with leafs
for (let sheet of tailwindRootLeafs) {
commonParents.get(sheet).add(sheet)
}

// If any 2 common parents come from the same tree, then all children of
// parent A and parent B will be moved to the parent of parent A and
// parent B. Parent A and parent B will be removed.
let repeat = true
repeat: while (repeat) {
repeat = false

for (let [sheetA, childrenA] of commonParents) {
for (let [sheetB, childrenB] of commonParents) {
if (sheetA === sheetB) continue

// Ancestors from self to root. Reversed order so we find the
// nearest common parent first
//
// Including self because if you compare a sheet with its parent,
// then the parent is still the common sheet between the two. In
// this case, the parent is the root file.
let ancestorsA = [sheetA].concat(Array.from(sheetA.ancestors()).reverse())
let ancestorsB = [sheetB].concat(Array.from(sheetB.ancestors()).reverse())

for (let parentA of ancestorsA) {
for (let parentB of ancestorsB) {
if (parentA !== parentB) continue

// Found the parent
let parent = parentA

commonParents.delete(sheetA)
commonParents.delete(sheetB)

for (let child of childrenA) {
commonParents.get(parent).add(child)
}

for (let child of childrenB) {
commonParents.get(parent).add(child)
}

// Found a common parent between sheet A and sheet B. We can
// stop looking for more common parents between A and B, and
// continue with the next sheet.
repeat = true
continue repeat
}
}
}
}
}

// Mark the common parent as the Tailwind CSS root file, and remove the
// flag from each leaf.
for (let [parent, children] of commonParents) {
parent.isTailwindRoot = true

for (let child of children) {
if (parent === child) continue

child.isTailwindRoot = false
}
}
return
}
}

{
let error = `You have one or more stylesheets that are imported into a utility layer and non-utility layer.\n`
error += `We cannot convert stylesheets under these conditions. Please look at the following stylesheets:\n`

throw new Error(error + lines.join('\n'))
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import postcss, { type ChildNode, type Plugin, type Root } from 'postcss'
import { format, type Options } from 'prettier'
import { walk } from '../utils/walk'
import { walk } from '../../utils/walk'

const FORMAT_OPTIONS: Options = {
parser: 'css',
Expand Down
Loading