Last active
August 17, 2023 13:51
-
-
Save knoopx/240cbe3b16a7a2f9e4101238533f87c8 to your computer and use it in GitHub Desktop.
Transform tachyons classes to tailwind css (using babel-codemod)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Installation: | |
// yarn add -D @codemod/cli @babel/plugin-syntax-jsx @babel/generator | |
import jsx from "@babel/plugin-syntax-jsx" | |
import generate from "@babel/generator" | |
const COLOR_MAP = { | |
"white": "white", | |
"near-white": "gray-100", | |
"light-gray": "gray-200", | |
"moon-gray": "gray-400", | |
"silver": "gray-500", | |
"gray": "gray-600", | |
"dark-gray": "gray-800", | |
"black": "black", | |
"blue": "blue-600", | |
// my tailwind.config.js extensions | |
"gray-90": "gray-90", | |
} | |
const COLOR_PREFIX_MAP = { | |
"": "text-", | |
"bg-": "bg-", | |
"b--": "border-", | |
} | |
// Custom classes that should not be converted because they're included in my index.css | |
const CUSTOM_CLASSES = [] | |
const MAP = { | |
// Remove (junk classes in my specific source) | |
// Color mappings for text, background, and border | |
...Object.keys(COLOR_PREFIX_MAP).reduce( | |
(result, prefix) => ({ | |
...result, | |
...Object.keys(COLOR_MAP).reduce( | |
(rest, key) => ({ | |
...rest, | |
[prefix + key]: COLOR_PREFIX_MAP[prefix] + COLOR_MAP[key], | |
}), | |
{}, | |
), | |
}), | |
{}, | |
), | |
"p([tblr])(\\d+)": "p$1-SPACING$2", | |
"pa(\\d+)": "p-SPACING$1", | |
"ph(\\d+)": "px-SPACING$1", | |
"pv(\\d+)": "py-SPACING$1", | |
"m([tblr])(\\d+)": "m$1-SPACING$2", | |
"n([tblr])(\\d+)": "-m$1-SPACING$2", | |
"ma(\\d+)": "m-SPACING$1", | |
"mh(\\d+)": "mx-SPACING$1", | |
"mv(\\d+)": "my-SPACING$1", | |
// "m([tblr])(\\d+)": "m$1-$2", | |
// "ma(\\d+)": "m-$1", | |
// "mh(\\d+)": "mx-$1", | |
// "mv(\\d+)": "my-$1", | |
"(.*)-SPACING0": "$1-0", | |
"(.*)-SPACING1": "$1-1", | |
"(.*)-SPACING2": "$1-2", | |
"(.*)-SPACING3": "$1-4", | |
"(.*)-SPACING4": "$1-8", | |
"(.*)-SPACING5": "$1-16", | |
"(.*)-SPACING6": "$1-32", | |
"(.*)-SPACING7": "$1-64", | |
"b([tblr])": "border-$1", | |
"bw1": "border-2", | |
"bw2": "border-4", | |
"ba": "border border-solid", | |
"bt": "border-t border-solid", | |
"bb": "border-b border-solid", | |
"bl": "border-l border-solid", | |
"br": "border-r border-solid", | |
"bn": "border-0 border-none", | |
"br0": "rounded-none", | |
"br1": "rounded-sm", | |
"br2": "rounded", | |
"br3": "rounded-lg", | |
"br-pill": "rounded-full", | |
"br-100": "rounded-full", | |
"sans-serif": "font-sans", | |
"serif": "font-serif", | |
"code": "font-mono", | |
"cf": "clearfix", | |
"fl": "float-left", | |
"fr": "float-right", | |
"b": "font-bold", | |
"tc": "text-center", | |
"tr": "text-right", | |
"tl": "text-left", | |
"tj": "text-justify", | |
"absolute--fill": "inset-0", | |
"outline-0": "outline-none", | |
"ttu": "uppercase", | |
"ttl": "lowercase", | |
"ttc": "capitalize", | |
"ttn": "normal-case", | |
"(pointer)": "cursor-$1", | |
"flex-column": "flex-col", | |
"flex-column-reverse": "flex-col-reverse", | |
"f7": "text-xs", | |
"f6": "text-sm", | |
"f5": "text-base", | |
"f4": "text-xl", | |
"f3": "text-2xl", | |
"f2": "text-4xl", | |
"f1": "text-5xl", | |
"fw9": "font-black", | |
"fw8": "font-extrabold", | |
"fw7": "font-bold", | |
"fw6": "font-semibold", | |
"fw5": "font-medium", | |
"fw4": "font-normal", | |
"fw3": "font-light", | |
"fw2": "font-thin", | |
"fw1": "font-hairline", | |
"mw6": "max-w-lg", | |
"mw7": "max-w-3lx", | |
"mw8": "max-w-5xl", | |
"mw9": "max-w-6xl", | |
"w1": "w-4", | |
"w2": "w-8", | |
"w3": "w-16", | |
"w4": "w-32", | |
"w5": "w-64", | |
"w-third": "w-1/3", | |
"w-two-thirds": "w-2/3", | |
"w-60": "w-3/5", | |
"w-100": "w-full", | |
"measure-narrow": "max-w-xs", | |
"measure": "max-w-md", | |
"measure-wide": "max-w-lg", | |
"h1": "h-4", | |
"h2": "h-8", | |
"h3": "h-16", | |
"h4": "h-32", | |
"h5": "h-64", | |
"h-100": "h-full", | |
"vh-100": "h-screen", | |
"input-reset": "appearance-none", | |
"list": "list-reset", | |
"lh-solid": "leading-none", | |
"lh-copy": "leading-normal", | |
"lh-title": "leading-tight", | |
"db": "block", | |
"di": "inline", | |
"dib": "inline-block", | |
"dn": "hidden", | |
"center": "mx-auto", | |
"nowrap": "whitespace-no-wrap", | |
"z-0": "z-0", | |
"z-1": "z-10", | |
"z-2": "z-20", | |
"z-3": "z-30", | |
"z-4": "z-40", | |
"z-5": "z-50", | |
"cover": "bg-cover", | |
"contain": "bg-contain", | |
// No mapping necessary: matches Tailwind. | |
// We still need to list them because everything else will be flagged as UNSUPPORTED so that | |
// we can quickly tell which mappings need to be added to this MAP | |
"flex": "flex", | |
"flex-row": "flex-row", | |
"flex-row-reverse": "flex-row-reverse", | |
"inline-flex": "inline-flex", | |
"items-center": "items-center", | |
"justify-start": "justify-start", | |
"justify-center": "justify-center", | |
"justify-end": "justify-end", | |
"justify-between": "justify-between", | |
"justify-around": "justify-around", | |
"absolute": "absolute", | |
"top-0": "top-0", | |
"bottom-0": "bottom-0", | |
"left-0": "left-0", | |
"right-0": "right-0", | |
"inset-0": "inset-0", | |
"bg-transparent": "bg-transparent", | |
"truncate": "truncate", | |
// No mapping necessary: manually added to my index.css to match Tachyons | |
"aspect-ratio--object": "aspect-ratio--object", | |
// my tailwind.config.js extensions | |
"vh-25": "h-25vh", | |
"vh-50": "h-50vh", | |
"vh-75": "h-75vh", | |
"mw5": "max-w-2xs", | |
"z-999": "z-999", | |
"z-max": "z-max", | |
// tailwindcss-aspect-ratio plugin | |
"aspect-ratio--1x1": "aspect-ratio-1x1", | |
"aspect-ratio--16x9": "aspect-ratio-16x9", | |
// Unsupported | |
// '(mw\\d+)': 'UNSUPPORTED-$1', // mw2 | |
// dim: 'UNSUPPORTED-dim', | |
"(.*\\$\\{.*)": "$1", | |
"(UNSUPPORTED-.+)": "$1", | |
"(APPROXNEXT-.+)": "$1", | |
} | |
const NS_REGEXP = /-ns$/ | |
const M_OR_L_REGEXP = /-[ml]$/ | |
function replace(className) { | |
return className | |
.split(/\s/g) | |
.map((klass) => { | |
if (klass === "") { | |
return klass | |
} | |
if ( | |
CUSTOM_CLASSES.some((pattern) => new RegExp(`^${pattern}$`).test(klass)) | |
) { | |
return klass | |
} | |
// Strip tachyon responsive modifiers | |
let responsivePrefix = "" | |
let result = klass.replace(NS_REGEXP, "") | |
if (result !== klass) { | |
responsivePrefix = "sm:" | |
} else { | |
result = klass.replace(M_OR_L_REGEXP, "") | |
if (result !== klass) { | |
responsivePrefix = "lg:" | |
} | |
} | |
let matchFound = false | |
result = Object.keys(MAP).reduce((acc, key) => { | |
// console.log(result); | |
const regexp = new RegExp(`^${key}$`) | |
if (regexp.test(acc)) { | |
matchFound = true | |
return acc.replace(regexp, MAP[key]) | |
} | |
return acc | |
}, result) | |
// return matchFound ? `${responsivePrefix}${result}` : `UNSUPPORTED-${klass}`; | |
return `${responsivePrefix}${result}` | |
}) | |
.join(" ") | |
} | |
export default function({ types: t }) { | |
function isClassNameJSXAttribute(path) { | |
// console.log(path.node) | |
// return ( | |
// path.node.type === 'JSXAttribute' && path.node.name.name === 'className' | |
// ) | |
return t.isJSXIdentifier(path.node.name, { name: "className" }) | |
} | |
function isContainedInClassNameJSXAttribute(path) { | |
return path.findParent((p) => p.node && isClassNameJSXAttribute(p)) | |
} | |
const visitor = { | |
ObjectProperty(path) { | |
if (isContainedInClassNameJSXAttribute(path)) { | |
// className={classNames('white', { bb: 'value' })} | |
console.log( | |
"ObjectProperty", | |
path.findParent(isClassNameJSXAttribute).toString(), | |
) | |
if (t.isIdentifier(path.node.key)) { | |
path.node.key = t.stringLiteral(replace(path.node.key.name)) | |
} else if (t.isStringLiteral(path.node.key)) { | |
path.node.key.value = replace(path.node.key.value) | |
} | |
} | |
}, | |
TemplateElement(path) { | |
if (isContainedInClassNameJSXAttribute(path)) { | |
// className={`${a} white`} | |
console.log( | |
"TemplateElement", | |
path.findParent(isClassNameJSXAttribute).toString(), | |
) | |
const replacement = replace(path.node.value.raw) | |
path.node.value.raw = replacement | |
path.node.value.cooked = replacement | |
} | |
}, | |
StringLiteral(path) { | |
if (isContainedInClassNameJSXAttribute(path)) { | |
console.log( | |
"StringLiteral", | |
path.findParent(isClassNameJSXAttribute).toString(), | |
) | |
path.node.value = replace(path.node.value) | |
} | |
}, | |
JSXAttribute(path) { | |
if (isClassNameJSXAttribute(path) && t.isStringLiteral(path.node.value)) { | |
// className="pointer" | |
console.log("JSXAttribute", path.toString()) | |
path.node.value.value = replace(path.node.value.value).trim() | |
} | |
}, | |
} | |
return { | |
inherits: jsx, | |
visitor, | |
} | |
} |
@minthemiddle You might want to check out my fork at https://gist.github.com/huylift/f40e321ed0d0d0281ac823a039f5ac1a , which I've updated and customized to my codebase.
But this script is good really just to get started on conversion. Then you'll have to do a lot of manual conversion.
would this be useful to transform all files within a complex application?
To anyone else who stumbled upon this script right here, i would recommend using jscodeshift, and you can use AST explorer to test out your code. Also to note here, the map is by no means complete, you'll need to add some more keys value pairs to the map in order to completely replace tachyon with tailwind.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@minthemiddle this thing just maps names within
className
JSX attributes. And this is also quite outdated.