Skip to content

Instantly share code, notes, and snippets.

@adriandmitroca
Created April 5, 2025 10:51
Show Gist options
  • Select an option

  • Save adriandmitroca/2443eebb3b703bfb8328ddf3f8685d1e to your computer and use it in GitHub Desktop.

Select an option

Save adriandmitroca/2443eebb3b703bfb8328ddf3f8685d1e to your computer and use it in GitHub Desktop.
Bootstrap 5 to Tailwind migration script
import { glob } from "glob"
import { readFileSync, writeFileSync } from "node:fs"
// Bootstrap to Tailwind utility class mappings
const utilityMappings = {
// Spacing
"m-0": "m-0",
"m-1": "m-1",
"m-2": "m-2",
"m-3": "m-3",
"m-4": "m-4",
"m-5": "m-5",
"mt-0": "mt-0",
"mt-1": "mt-1",
"mt-2": "mt-2",
"mt-3": "mt-3",
"mt-4": "mt-4",
"mt-5": "mt-5",
"mb-0": "mb-0",
"mb-1": "mb-1",
"mb-2": "mb-2",
"mb-3": "mb-3",
"mb-4": "mb-4",
"mb-5": "mb-5",
"ml-0": "ml-0",
"ml-1": "ml-1",
"ml-2": "ml-2",
"ml-3": "ml-3",
"ml-4": "ml-4",
"ml-5": "ml-5",
"mr-0": "mr-0",
"mr-1": "mr-1",
"mr-2": "mr-2",
"mr-3": "mr-3",
"mr-4": "mr-4",
"mr-5": "mr-5",
"mx-0": "mx-0",
"mx-1": "mx-1",
"mx-2": "mx-2",
"mx-3": "mx-3",
"mx-4": "mx-4",
"mx-5": "mx-5",
"my-0": "my-0",
"my-1": "my-1",
"my-2": "my-2",
"my-3": "my-3",
"my-4": "my-4",
"my-5": "my-5",
"p-0": "p-0",
"p-1": "p-1",
"p-2": "p-2",
"p-3": "p-3",
"p-4": "p-4",
"p-5": "p-5",
"pt-0": "pt-0",
"pt-1": "pt-1",
"pt-2": "pt-2",
"pt-3": "pt-3",
"pt-4": "pt-4",
"pt-5": "pt-5",
"pb-0": "pb-0",
"pb-1": "pb-1",
"pb-2": "pb-2",
"pb-3": "pb-3",
"pb-4": "pb-4",
"pb-5": "pb-5",
"pl-0": "pl-0",
"pl-1": "pl-1",
"pl-2": "pl-2",
"pl-3": "pl-3",
"pl-4": "pl-4",
"pl-5": "pl-5",
"pr-0": "pr-0",
"pr-1": "pr-1",
"pr-2": "pr-2",
"pr-3": "pr-3",
"pr-4": "pr-4",
"pr-5": "pr-5",
"px-0": "px-0",
"px-1": "px-1",
"px-2": "px-2",
"px-3": "px-3",
"px-4": "px-4",
"px-5": "px-5",
"py-0": "py-0",
"py-1": "py-1",
"py-2": "py-2",
"py-3": "py-3",
"py-4": "py-4",
"py-5": "py-5",
// Display
"d-none": "hidden",
"d-block": "block",
"d-inline": "inline",
"d-inline-block": "inline-block",
"d-flex": "flex",
"d-inline-flex": "inline-flex",
// Flex
"flex-row": "flex-row",
"flex-column": "flex-col",
"justify-content-start": "justify-start",
"justify-content-end": "justify-end",
"justify-content-center": "justify-center",
"justify-content-between": "justify-between",
"justify-content-around": "justify-around",
"align-items-start": "items-start",
"align-items-end": "items-end",
"align-items-center": "items-center",
"align-items-baseline": "items-baseline",
"align-items-stretch": "items-stretch",
// Text
"text-left": "text-left",
"text-center": "text-center",
"text-right": "text-right",
"text-justify": "text-justify",
"text-nowrap": "whitespace-nowrap",
"text-truncate": "truncate",
"text-lowercase": "lowercase",
"text-uppercase": "uppercase",
"text-capitalize": "capitalize",
"font-weight-bold": "font-bold",
"font-weight-normal": "font-normal",
"font-weight-light": "font-light",
"font-italic": "italic",
"text-muted": "text-gray-500",
// Colors
"color-primary": "text-neutral-800",
"color-secondary": "text-brand-600",
"color-success": "text-green-600",
"color-danger": "text-red-600",
"color-warning": "text-yellow-600",
"color-info": "text-blue-600",
"color-light": "text-white",
"color-dark": "text-gray-900",
"bg-primary": "bg-neutral-800",
"bg-secondary": "bg-brand-600",
"bg-success": "bg-green-600",
"bg-danger": "bg-red-600",
"bg-warning": "bg-yellow-600",
"bg-info": "bg-blue-600",
"bg-light": "bg-white",
"bg-dark": "bg-gray-900",
// Width/Height
"w-25": "w-1/4",
"w-50": "w-1/2",
"w-75": "w-3/4",
"w-100": "w-full",
"h-25": "h-1/4",
"h-50": "h-1/2",
"h-75": "h-3/4",
"h-100": "h-full",
// Position
"position-static": "static",
"position-relative": "relative",
"position-absolute": "absolute",
"position-fixed": "fixed",
"position-sticky": "sticky",
// Border
border: "border",
"border-top": "border-t",
"border-right": "border-r",
"border-bottom": "border-b",
"border-left": "border-l",
"border-0": "border-0",
rounded: "rounded",
"rounded-top": "rounded-t",
"rounded-right": "rounded-r",
"rounded-bottom": "rounded-b",
"rounded-left": "rounded-l",
"rounded-circle": "rounded-full",
"rounded-0": "rounded-none",
}
// Breakpoint mappings
const breakpointMappings = {
sm: "sm",
md: "md",
lg: "lg",
xl: "xl",
xxl: "2xl",
}
function replaceClasses(classes) {
let hasChanges = false
const newClasses = classes
.split(/\s+/)
.map((cls) => {
// Handle responsive classes (e.g., sm:text-center)
const responsiveMatch = cls.match(/^(sm|md|lg|xl|xxl):(.+)$/)
if (responsiveMatch) {
const [, breakpoint, utility] = responsiveMatch
const tailwindBreakpoint = breakpointMappings[breakpoint]
if (tailwindBreakpoint && utilityMappings[utility]) {
const newClass = `${tailwindBreakpoint}:${utilityMappings[utility]}`
if (newClass !== cls) {
hasChanges = true
}
return newClass
}
}
// Handle regular classes
const newClass = utilityMappings[cls] || cls
if (newClass !== cls) {
hasChanges = true
}
return newClass
})
.join(" ")
return { newClasses, hasChanges }
}
function replaceBootstrapClasses(content) {
let hasChanges = false
let newContent = content
// Replace classes in className="..." or className='...'
newContent = newContent.replaceAll(
/className=["']([^"']*)["']/g,
(match, classes) => {
const { newClasses, hasChanges: hasClassChanges } =
replaceClasses(classes)
hasChanges = hasChanges || hasClassChanges
return `className="${newClasses}"`
}
)
// Replace classes in className={`...`}
newContent = newContent.replaceAll(
/className={`([^`]*)`}/g,
(match, classes) => {
const { newClasses, hasChanges: hasClassChanges } =
replaceClasses(classes)
hasChanges = hasChanges || hasClassChanges
return `className={\`${newClasses}\`}`
}
)
// Replace classes in clsx(...) with multiple arguments
newContent = newContent.replaceAll(/clsx\(([^)]*)\)/g, (match, args) => {
let hasClassChanges = false
let newArgs = args
// Handle template literals inside clsx
newArgs = newArgs.replaceAll(/`([^`]*)`/g, (tmplMatch, tmplContent) => {
const { newClasses, hasChanges: hasTmplChanges } =
replaceClasses(tmplContent)
hasClassChanges = hasClassChanges || hasTmplChanges
return `\`${newClasses}\``
})
// Handle string arguments
newArgs = newArgs.replaceAll(
/(?<!\`)(["'])([^"']*)\1/g,
(strMatch, quote, content) => {
const { newClasses, hasChanges: hasStrChanges } =
replaceClasses(content)
hasClassChanges = hasClassChanges || hasStrChanges
return `${quote}${newClasses}${quote}`
}
)
// Handle object syntax
newArgs = newArgs.replaceAll(/\{([^}]*)\}/g, (objMatch, objContent) => {
const { newClasses, hasChanges: hasObjChanges } =
replaceClasses(objContent)
hasClassChanges = hasClassChanges || hasObjChanges
return `{${newClasses}}`
})
hasChanges = hasChanges || hasClassChanges
return `clsx(${newArgs})`
})
return { newContent, hasChanges }
}
function processFile(filePath) {
console.log(`Processing ${filePath}...`)
const content = readFileSync(filePath, "utf8")
const { newContent, hasChanges } = replaceBootstrapClasses(content)
if (hasChanges) {
writeFileSync(filePath, newContent)
console.log(`Updated ${filePath}`)
return true
}
console.log(`No changes needed in ${filePath}`)
return false
}
// Find all .tsx files in src directory
try {
const files = await glob("src/**/*.tsx")
let modifiedCount = 0
for (const file of files) {
if (processFile(file)) {
modifiedCount++
}
}
console.log(`\nMigration complete!`)
console.log(`Total files processed: ${files.length}`)
console.log(`Files modified: ${modifiedCount}`)
} catch (error) {
console.error("Error finding files:", error)
throw error
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment