Last active
November 25, 2024 19:51
-
-
Save yutamago/3ba4e14bb156ffe3a29c218156934e8d to your computer and use it in GitHub Desktop.
Deno script to migrate most bootstrap classes to tailwind classes. Angular-compatible
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 * as fs from 'node:fs'; | |
| import * as path from 'node:path'; | |
| // TODO: Insert your own breakpoints -> make sure to set tailwind to use the same breakpoints. | |
| // this script will not convert breakpoints to equivalent breakpoints in tailwind! | |
| // ex. d-xxl-flex becomes xxl:tw-flex, not 2xl:tw-flex | |
| const bootstrapScreenSizes = [ | |
| 'sm', | |
| 'md', | |
| 'lg', | |
| 'xl', | |
| 'xxl' | |
| ]; | |
| interface Replacement { | |
| bootstrap: string; | |
| tailwind: string | string[]; | |
| } | |
| interface RegexReplacement { | |
| pattern: RegExp; | |
| replacement: string; | |
| } | |
| /** | |
| * Recursively processes files in a given directory, replacing patterns in files. | |
| * @param dir - The directory to process. | |
| * @param replacements - List of regex patterns and their replacements. | |
| */ | |
| async function replaceInFiles(dir: string, replacements: RegexReplacement[]): Promise<void> { | |
| const entries = await fs.promises.readdir(dir, { withFileTypes: true }); | |
| for (const entry of entries) { | |
| const fullPath = path.join(dir, entry.name); | |
| if (entry.isDirectory()) { | |
| // If it's a directory, recurse | |
| await replaceInFiles(fullPath, replacements); | |
| } else if (entry.isFile()) { | |
| // If it's a file, perform replacement | |
| await processFile(fullPath, replacements); | |
| } | |
| } | |
| } | |
| /** | |
| * Reads a file, replaces all patterns, and writes back the modified content. | |
| * @param filePath - Path to the file. | |
| * @param replacements - List of regex patterns and their replacements. | |
| */ | |
| async function processFile(filePath: string, replacements: RegexReplacement[]): Promise<void> { | |
| if(!filePath.endsWith('.ts') && !filePath.endsWith('.html')) | |
| return; | |
| const content = await fs.promises.readFile(filePath, 'utf8'); | |
| let modifiedContent = content; | |
| replacements.forEach(({ pattern, replacement }) => { | |
| modifiedContent = modifiedContent.replace(pattern, replacement); | |
| }); | |
| if (modifiedContent !== content) { | |
| await fs.promises.writeFile(filePath, modifiedContent, 'utf8'); | |
| console.log(`Updated file: ${filePath}`); | |
| } | |
| } | |
| /** | |
| * d-flex => d-md-flex | |
| * => d-lg-flex | |
| * => d-xl-flex ... | |
| */ | |
| function genTailwindWithBreakpoints(prefix: string, replacements: Replacement[]): Replacement[] { | |
| const offsetBs = prefix.length; | |
| return [ | |
| ...replacements, | |
| ...replacements.flatMap(r => | |
| bootstrapScreenSizes.map(x => (<Replacement>{ | |
| bootstrap: `${r.bootstrap.substring(0, offsetBs)}-${x}${r.bootstrap.substring(offsetBs)}`, | |
| tailwind: Array.isArray(r.tailwind) ? r.tailwind.map(tw => `${x}:${tw}`) : `${x}:${r.tailwind}` | |
| }))) | |
| ]; | |
| } | |
| function genTailwindCols(): Replacement[] { | |
| const offsetBs = 'col'.length; | |
| const baseCols: Replacement[] = [ | |
| {bootstrap: 'col-1', tailwind: ['tw-w-1/12', 'tw-px-2']}, | |
| {bootstrap: 'col-2', tailwind: ['tw-w-2/12', 'tw-px-2']}, | |
| {bootstrap: 'col-3', tailwind: ['tw-w-3/12', 'tw-px-2']}, | |
| {bootstrap: 'col-4', tailwind: ['tw-w-4/12', 'tw-px-2']}, | |
| {bootstrap: 'col-5', tailwind: ['tw-w-5/12', 'tw-px-2']}, | |
| {bootstrap: 'col-6', tailwind: ['tw-w-6/12', 'tw-px-2']}, | |
| {bootstrap: 'col-7', tailwind: ['tw-w-7/12', 'tw-px-2']}, | |
| {bootstrap: 'col-8', tailwind: ['tw-w-8/12', 'tw-px-2']}, | |
| {bootstrap: 'col-9', tailwind: ['tw-w-9/12', 'tw-px-2']}, | |
| {bootstrap: 'col-10', tailwind: ['tw-w-10/12', 'tw-px-2']}, | |
| {bootstrap: 'col-11', tailwind: ['tw-w-11/12', 'tw-px-2']}, | |
| {bootstrap: 'col-12', tailwind: ['tw-w-full', 'tw-px-2']}, | |
| ]; | |
| return [ | |
| ...baseCols, | |
| ...baseCols.flatMap(r => | |
| bootstrapScreenSizes.map(x => (<Replacement>{ | |
| bootstrap: `${r.bootstrap.substring(0, offsetBs)}-${x}${r.bootstrap.substring(offsetBs)}`, | |
| tailwind: [`${x}:${r.tailwind[0]}`, r.tailwind[1]], | |
| }))) | |
| ]; | |
| } | |
| /** | |
| * m => m-0 | |
| * => m-1 | |
| * => m-2 ... | |
| * @param prefix | |
| * @param twPrefix | |
| */ | |
| function genTailwindSizes(prefix: string, twPrefix?: string): Replacement[] { | |
| return genTailwindWithBreakpoints(prefix, [ | |
| { bootstrap: `${prefix}-0`, tailwind: `tw-${twPrefix || prefix}-0` }, | |
| { bootstrap: `${prefix}-1`, tailwind: `tw-${twPrefix || prefix}-1` }, | |
| { bootstrap: `${prefix}-2`, tailwind: `tw-${twPrefix || prefix}-2` }, | |
| { bootstrap: `${prefix}-3`, tailwind: `tw-${twPrefix || prefix}-4` }, | |
| { bootstrap: `${prefix}-4`, tailwind: `tw-${twPrefix || prefix}-6` }, | |
| { bootstrap: `${prefix}-5`, tailwind: `tw-${twPrefix || prefix}-12` }, | |
| { bootstrap: `${prefix}-n1`, tailwind: `tw-${twPrefix || prefix}--1` }, | |
| { bootstrap: `${prefix}-n2`, tailwind: `tw-${twPrefix || prefix}--2` }, | |
| { bootstrap: `${prefix}-n3`, tailwind: `tw-${twPrefix || prefix}--4` }, | |
| { bootstrap: `${prefix}-n4`, tailwind: `tw-${twPrefix || prefix}--6` }, | |
| { bootstrap: `${prefix}-n5`, tailwind: `tw-${twPrefix || prefix}--12` }, | |
| ]); | |
| } | |
| (async () => { | |
| const targetFolder = './../src/'; // Change this to your folder path | |
| const classesToReplace: RegexReplacement[] = | |
| (<Replacement[]>[ | |
| // margin | |
| ...genTailwindSizes('m'), | |
| ...genTailwindSizes('mx'), | |
| ...genTailwindSizes('my'), | |
| ...genTailwindSizes('mt'), | |
| ...genTailwindSizes('mb'), | |
| ...genTailwindSizes('mr'), | |
| ...genTailwindSizes('ml'), | |
| ...genTailwindSizes('ms'), | |
| ...genTailwindSizes('me'), | |
| // padding | |
| ...genTailwindSizes('p'), | |
| ...genTailwindSizes('px'), | |
| ...genTailwindSizes('py'), | |
| ...genTailwindSizes('pt'), | |
| ...genTailwindSizes('pb'), | |
| ...genTailwindSizes('pr'), | |
| ...genTailwindSizes('pl'), | |
| ...genTailwindSizes('ps'), | |
| ...genTailwindSizes('pe'), | |
| // gap | |
| ...genTailwindSizes('gap'), | |
| ...genTailwindSizes('row-gap', 'gap-y'), | |
| ...genTailwindSizes('col-gap', 'gap-x'), | |
| // display | |
| ...genTailwindWithBreakpoints('d', [ | |
| {bootstrap: 'd-flex', tailwind: 'tw-flex'}, | |
| {bootstrap: 'd-none', tailwind: 'tw-hidden'}, | |
| ]), | |
| // flex | |
| ...genTailwindWithBreakpoints('flex', [ | |
| {bootstrap: 'flex-grow-1', tailwind: 'tw-grow'}, | |
| {bootstrap: 'flex-shrink-1', tailwind: 'tw-shrink'}, | |
| {bootstrap: 'flex-grow-0', tailwind: 'tw-grow-0'}, | |
| {bootstrap: 'flex-shrink-0', tailwind: 'tw-shrink-0'}, | |
| {bootstrap: 'flex-column', tailwind: 'tw-flex-col'}, | |
| {bootstrap: 'flex-column-reverse', tailwind: 'tw-flex-col-reverse'}, | |
| {bootstrap: 'flex-row', tailwind: 'tw-flex-row'}, | |
| {bootstrap: 'flex-row-reverse', tailwind: 'tw-flex-row-reverse'}, | |
| {bootstrap: 'flex-wrap', tailwind: 'tw-flex-wrap'}, | |
| {bootstrap: 'flex-wrap-reverse', tailwind: 'tw-flex-wrap-reverse'}, | |
| {bootstrap: 'flex-nowrap', tailwind: 'tw-flex-nowrap'}, | |
| ]), | |
| // position | |
| ...genTailwindWithBreakpoints('position', [ | |
| {bootstrap: 'position-relative', tailwind: 'tw-relative'}, | |
| {bootstrap: 'position-absolute', tailwind: 'tw-absolute'}, | |
| ]), | |
| // justify-content | |
| ...genTailwindWithBreakpoints('justify-content', [ | |
| {bootstrap: 'justify-content-normal', tailwind: 'tw-justify-normal'}, | |
| {bootstrap: 'justify-content-center', tailwind: 'tw-justify-center'}, | |
| {bootstrap: 'justify-content-start', tailwind: 'tw-justify-start'}, | |
| {bootstrap: 'justify-content-end', tailwind: 'tw-justify-end'}, | |
| {bootstrap: 'justify-content-between', tailwind: 'tw-justify-between'}, | |
| {bootstrap: 'justify-content-around', tailwind: 'tw-justify-around'}, | |
| {bootstrap: 'justify-content-evenly', tailwind: 'tw-justify-evenly'}, | |
| {bootstrap: 'justify-content-baseline', tailwind: 'tw-justify-baseline'}, | |
| {bootstrap: 'justify-content-stretch', tailwind: 'tw-justify-stretch'}, | |
| ]), | |
| // justify-items | |
| ...genTailwindWithBreakpoints('justify-items', [ | |
| {bootstrap: 'justify-items-center', tailwind: 'tw-justify-items-center'}, | |
| {bootstrap: 'justify-items-start', tailwind: 'tw-justify-items-start'}, | |
| {bootstrap: 'justify-items-end', tailwind: 'tw-justify-items-end'}, | |
| {bootstrap: 'justify-items-stretch', tailwind: 'tw-justify-items-stretch'}, | |
| {bootstrap: 'justify-items-baseline', tailwind: 'tw-justify-items-baseline'}, | |
| ]), | |
| // justify-self | |
| ...genTailwindWithBreakpoints('justify-self', [ | |
| {bootstrap: 'justify-self-center', tailwind: 'tw-justify-self-center'}, | |
| {bootstrap: 'justify-self-start', tailwind: 'tw-justify-self-start'}, | |
| {bootstrap: 'justify-self-end', tailwind: 'tw-justify-self-end'}, | |
| {bootstrap: 'justify-self-stretch', tailwind: 'tw-justify-self-stretch'}, | |
| {bootstrap: 'justify-self-baseline', tailwind: 'tw-justify-self-baseline'}, | |
| ]), | |
| // align | |
| ...genTailwindWithBreakpoints('align-items', [ | |
| {bootstrap: 'align-items-center', tailwind: 'tw-items-center'}, | |
| {bootstrap: 'align-items-start', tailwind: 'tw-items-start'}, | |
| {bootstrap: 'align-items-end', tailwind: 'tw-items-end'}, | |
| {bootstrap: 'align-items-stretch', tailwind: 'tw-items-stretch'}, | |
| {bootstrap: 'align-items-baseline', tailwind: 'tw-items-baseline'}, | |
| ]), | |
| // align-self | |
| ...genTailwindWithBreakpoints('align-self', [ | |
| {bootstrap: 'align-self-auto', tailwind: 'tw-self-auto'}, | |
| {bootstrap: 'align-self-center', tailwind: 'tw-self-center'}, | |
| {bootstrap: 'align-self-start', tailwind: 'tw-self-start'}, | |
| {bootstrap: 'align-self-end', tailwind: 'tw-self-end'}, | |
| {bootstrap: 'align-self-stretch', tailwind: 'tw-self-stretch'}, | |
| {bootstrap: 'align-self-baseline', tailwind: 'tw-self-baseline'}, | |
| ]), | |
| // align-content | |
| ...genTailwindWithBreakpoints('align-content', [ | |
| {bootstrap: 'align-content-normal', tailwind: 'tw-content-normal'}, | |
| {bootstrap: 'align-content-center', tailwind: 'tw-content-center'}, | |
| {bootstrap: 'align-content-start', tailwind: 'tw-content-start'}, | |
| {bootstrap: 'align-content-end', tailwind: 'tw-content-end'}, | |
| {bootstrap: 'align-content-between', tailwind: 'tw-content-between'}, | |
| {bootstrap: 'align-content-around', tailwind: 'tw-content-around'}, | |
| {bootstrap: 'align-content-evenly', tailwind: 'tw-content-evenly'}, | |
| {bootstrap: 'align-content-baseline', tailwind: 'tw-content-baseline'}, | |
| {bootstrap: 'align-content-stretch', tailwind: 'tw-content-stretch'}, | |
| ]), | |
| // height + width | |
| ...genTailwindWithBreakpoints('h', [ | |
| {bootstrap: 'h-0', tailwind: 'tw-h-0'}, | |
| {bootstrap: 'w-0', tailwind: 'tw-w-0'}, | |
| {bootstrap: 'h-100', tailwind: 'tw-h-full'}, | |
| {bootstrap: 'w-100', tailwind: 'tw-w-full'}, | |
| ]), | |
| // container-row-col | |
| {bootstrap: 'container', tailwind: ['tw-container', 'tw-mx-auto']}, | |
| {bootstrap: 'row', tailwind: ['tw-flex', 'tw-flex-wrap']}, | |
| {bootstrap: 'col', tailwind: ['tw-flex']}, | |
| ...genTailwindCols(), | |
| // text | |
| {bootstrap: 'text-center', tailwind: 'tw-text-center'}, | |
| {bootstrap: 'text-left', tailwind: 'tw-text-left'}, | |
| {bootstrap: 'text-right', tailwind: 'tw-text-right'}, | |
| {bootstrap: 'text-justify', tailwind: 'tw-text-justify'}, | |
| {bootstrap: 'text-start', tailwind: 'tw-text-start'}, | |
| {bootstrap: 'text-end', tailwind: 'tw-text-end'}, | |
| ]) | |
| // convert to Regex | |
| .map<RegexReplacement>(x => ({ | |
| pattern: new RegExp('(class="(?:[^"]* )?|\\[class\\.)' + x.bootstrap + '( |"|\\])', 'g'), | |
| replacement: '$1' + (Array.isArray(x.tailwind) ? x.tailwind.join(' ') : x.tailwind) + '$2' | |
| } )); | |
| try { | |
| await replaceInFiles(targetFolder, classesToReplace); | |
| console.log('Pattern replacement completed successfully.'); | |
| } catch (error) { | |
| console.error('Error processing files:', error); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment