Created
April 5, 2025 10:51
-
-
Save adriandmitroca/2443eebb3b703bfb8328ddf3f8685d1e to your computer and use it in GitHub Desktop.
Bootstrap 5 to Tailwind migration script
This file contains hidden or 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
| 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