Skip to content

Instantly share code, notes, and snippets.

@yutamago
Last active November 25, 2024 19:51
Show Gist options
  • Select an option

  • Save yutamago/3ba4e14bb156ffe3a29c218156934e8d to your computer and use it in GitHub Desktop.

Select an option

Save yutamago/3ba4e14bb156ffe3a29c218156934e8d to your computer and use it in GitHub Desktop.
Deno script to migrate most bootstrap classes to tailwind classes. Angular-compatible
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