import type { ObjectExpression, ObjectMethod, ObjectProperty } from '@babel/types' import { parse } from '@babel/parser' import * as t from '@babel/types' import { generate } from '../babel' const ROOT_LEGACY_KEYS = ['cwd', 'overwrite', 'tailwind', 'features', 'output', 'applyPatches'] as const type OptionObjectScope = 'root' | 'registry' | 'patch' export interface ConfigSourceMigrationResult { changed: boolean code: string changes: string[] } function getPropertyKeyName(property: ObjectProperty | ObjectMethod): string | undefined { if (!property.computed && t.isIdentifier(property.key)) { return property.key.name } if (t.isStringLiteral(property.key)) { return property.key.value } return undefined } function findObjectProperty(objectExpression: ObjectExpression, name: string): ObjectProperty | undefined { for (const property of objectExpression.properties) { if (!t.isObjectProperty(property)) { continue } if (getPropertyKeyName(property) === name) { return property } } return undefined } function findObjectExpressionProperty(objectExpression: ObjectExpression, name: string): ObjectExpression | undefined { const property = findObjectProperty(objectExpression, name) if (!property) { return undefined } if (t.isObjectExpression(property.value)) { return property.value } return undefined } function removeObjectProperty(objectExpression: ObjectExpression, property: ObjectProperty) { const index = objectExpression.properties.indexOf(property) if (index >= 0) { objectExpression.properties.splice(index, 1) } } function hasObjectProperty(objectExpression: ObjectExpression, name: string) { return findObjectProperty(objectExpression, name) !== undefined } function keyAsIdentifier(name: string) { return t.identifier(name) } function mergeObjectProperties(target: ObjectExpression, source: ObjectExpression) { let changed = false for (const sourceProperty of source.properties) { if (t.isSpreadElement(sourceProperty)) { target.properties.push(sourceProperty) changed = true continue } const sourceKey = getPropertyKeyName(sourceProperty) if (!sourceKey) { target.properties.push(sourceProperty) changed = true continue } if (hasObjectProperty(target, sourceKey)) { continue } target.properties.push(sourceProperty) changed = true } return changed } function moveProperty( objectExpression: ObjectExpression, from: string, to: string, changes: Set, scope: OptionObjectScope, ) { const source = findObjectProperty(objectExpression, from) if (!source) { return false } const target = findObjectProperty(objectExpression, to) if (!target) { source.key = keyAsIdentifier(to) source.computed = false source.shorthand = false changes.add(`${scope}.${from} -> ${scope}.${to}`) return true } if (t.isObjectExpression(source.value) && t.isObjectExpression(target.value)) { const merged = mergeObjectProperties(target.value, source.value) if (merged) { changes.add(`${scope}.${from} merged into ${scope}.${to}`) } } removeObjectProperty(objectExpression, source) changes.add(`${scope}.${from} removed (preferred ${scope}.${to})`) return true } function migrateExtractOptions(extract: ObjectExpression, changes: Set, scope: OptionObjectScope) { let changed = false changed = moveProperty(extract, 'enabled', 'write', changes, scope) || changed changed = moveProperty(extract, 'stripUniversalSelector', 'removeUniversalSelector', changes, scope) || changed return changed } function migrateTailwindOptions(tailwindcss: ObjectExpression, changes: Set, scope: OptionObjectScope) { let changed = false changed = moveProperty(tailwindcss, 'package', 'packageName', changes, scope) || changed changed = moveProperty(tailwindcss, 'legacy', 'v2', changes, scope) || changed changed = moveProperty(tailwindcss, 'classic', 'v3', changes, scope) || changed changed = moveProperty(tailwindcss, 'next', 'v4', changes, scope) || changed return changed } function migrateApplyOptions(apply: ObjectExpression, changes: Set, scope: OptionObjectScope) { return moveProperty(apply, 'exportContext', 'exposeContext', changes, scope) } function ensureObjectExpressionProperty( objectExpression: ObjectExpression, name: string, changes: Set, scope: OptionObjectScope, ) { const existing = findObjectProperty(objectExpression, name) if (existing) { return t.isObjectExpression(existing.value) ? existing.value : undefined } const value = t.objectExpression([]) objectExpression.properties.push(t.objectProperty(keyAsIdentifier(name), value)) changes.add(`${scope}.${name} created`) return value } function moveOverwriteToApply(objectExpression: ObjectExpression, changes: Set, scope: OptionObjectScope) { const overwrite = findObjectProperty(objectExpression, 'overwrite') if (!overwrite) { return false } const apply = ensureObjectExpressionProperty(objectExpression, 'apply', changes, scope) if (!apply) { return false } const hasApplyOverwrite = hasObjectProperty(apply, 'overwrite') if (!hasApplyOverwrite) { apply.properties.push( t.objectProperty(keyAsIdentifier('overwrite'), overwrite.value), ) changes.add(`${scope}.overwrite -> ${scope}.apply.overwrite`) } removeObjectProperty(objectExpression, overwrite) return true } function hasAnyRootLegacyKeys(objectExpression: ObjectExpression) { return ROOT_LEGACY_KEYS.some(key => hasObjectProperty(objectExpression, key)) } function migrateOptionObject(objectExpression: ObjectExpression, scope: OptionObjectScope, changes: Set) { let changed = false changed = moveProperty(objectExpression, 'cwd', 'projectRoot', changes, scope) || changed changed = moveProperty(objectExpression, 'tailwind', 'tailwindcss', changes, scope) || changed changed = moveProperty(objectExpression, 'features', 'apply', changes, scope) || changed changed = moveProperty(objectExpression, 'applyPatches', 'apply', changes, scope) || changed changed = moveProperty(objectExpression, 'output', 'extract', changes, scope) || changed changed = moveOverwriteToApply(objectExpression, changes, scope) || changed const extract = findObjectExpressionProperty(objectExpression, 'extract') if (extract) { changed = migrateExtractOptions(extract, changes, scope) || changed } const tailwindcss = findObjectExpressionProperty(objectExpression, 'tailwindcss') if (tailwindcss) { changed = migrateTailwindOptions(tailwindcss, changes, scope) || changed } const apply = findObjectExpressionProperty(objectExpression, 'apply') if (apply) { changed = migrateApplyOptions(apply, changes, scope) || changed } return changed } function unwrapExpression(node: t.Node): t.Node { let current = node while ( t.isTSAsExpression(current) || t.isTSSatisfiesExpression(current) || t.isTSTypeAssertion(current) || t.isParenthesizedExpression(current) ) { current = current.expression } return current } function resolveObjectExpressionFromExpression(expression: t.Node): ObjectExpression | undefined { const unwrapped = unwrapExpression(expression) if (t.isObjectExpression(unwrapped)) { return unwrapped } if (t.isCallExpression(unwrapped)) { const [firstArg] = unwrapped.arguments if (!firstArg || !t.isExpression(firstArg)) { return undefined } const firstArgUnwrapped = unwrapExpression(firstArg) if (t.isObjectExpression(firstArgUnwrapped)) { return firstArgUnwrapped } } return undefined } function resolveObjectExpressionFromProgram(program: t.Program, name: string): ObjectExpression | undefined { for (const statement of program.body) { if (!t.isVariableDeclaration(statement)) { continue } for (const declaration of statement.declarations) { if (!t.isIdentifier(declaration.id) || declaration.id.name !== name || !declaration.init) { continue } const objectExpression = resolveObjectExpressionFromExpression(declaration.init) if (objectExpression) { return objectExpression } } } return undefined } function resolveRootConfigObjectExpression(program: t.Program): ObjectExpression | undefined { for (const statement of program.body) { if (!t.isExportDefaultDeclaration(statement)) { continue } const declaration = statement.declaration if (t.isIdentifier(declaration)) { return resolveObjectExpressionFromProgram(program, declaration.name) } const objectExpression = resolveObjectExpressionFromExpression(declaration) if (objectExpression) { return objectExpression } } return undefined } export function migrateConfigSource(source: string): ConfigSourceMigrationResult { const ast = parse(source, { sourceType: 'module', plugins: ['typescript', 'jsx'], }) const root = resolveRootConfigObjectExpression(ast.program) if (!root) { return { changed: false, code: source, changes: [], } } const changes = new Set() let changed = false const registry = findObjectExpressionProperty(root, 'registry') if (registry) { changed = migrateOptionObject(registry, 'registry', changes) || changed } const patch = findObjectExpressionProperty(root, 'patch') if (patch) { changed = migrateOptionObject(patch, 'patch', changes) || changed } if (hasAnyRootLegacyKeys(root)) { changed = migrateOptionObject(root, 'root', changes) || changed } if (!changed) { return { changed: false, code: source, changes: [], } } const generated = generate(ast, { comments: true, }).code const code = source.endsWith('\n') ? `${generated}\n` : generated return { changed: true, code, changes: [...changes], } }