Skip to content

Instantly share code, notes, and snippets.

@iamgideonidoko
Last active February 5, 2025 10:36
Show Gist options
  • Save iamgideonidoko/4d27c3bc94d622cd0fb1ae9148835619 to your computer and use it in GitHub Desktop.
Save iamgideonidoko/4d27c3bc94d622cd0fb1ae9148835619 to your computer and use it in GitHub Desktop.
CONVERT TAILWIND CSS (V3) PREDEFINED & ARBITRARY CLASSES (WITH REM) FROM 10px BASE TO 16px BASE
// CONVERT TAILWIND CSS (V3) PREDEFINED & ARBITRARY CLASSES (WITH REM) FROM 10px BASE TO 16px BASE
/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-var-requires */
import { readdirSync, statSync, readFileSync, writeFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
/*
* Caveat:
* Predefined Classes like scroll-* are ingored
* Arbitrary Classes like .*-\[.*3rem.*\].* are not captured so handle or convert manually
*/
const scaleFactor = 10 / 16;
const allPredefinedValues = [
0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, 20, 24, 28, 32, 36, 40, 44,
48, 52, 56, 60, 64, 72, 80, 96,
];
const classesRegex =
/(text|leading|tracking|w|h|p|pt|pb|pl|pr|ps|pe|px|py|m|mt|mb|ml|mr|ms|me|mx|my|gap|gap-x|gap-y|top|right|bottom|left|start|end|inset|inset-x|inset-y|basis|space-x|space-y|size|indent)-(\d*\.?\d+|\[(\-?\d*\.?\d+)rem\])(?=\s|'|"|`)/g;
// Special case for rounded predefined classes
const roundedClassesRegex =
/(rounded|rounded-s|rounded-t|rounded-r|rounded-b)(?:-(sm|md|lg|xl|2xl|3xl|\[(\-?\d*\.?\d+)rem\]))?(?=\s|'|"|`)/g;
// Values in rem
const roundedPredefinedValues = {
sm: 0.125,
default: 0.25,
md: 0.375,
lg: 0.5,
xl: 0.75,
'2xl': 1,
'3xl': 1.5,
};
/**
* @param {string} match
* @param {string} prefix (capture group 1)
* @param {string|undefined} predefinedValue (capture group 2)
* @param {string|undefined} arbitraryValue (capture group 3)
* @return {string}
*/
const roundedClassesReplacer = (match, prefix, predefinedValue, arbitraryValue) => {
/** @type {number|undefined} */
const arbitraryValueToUse =
arbitraryValue !== undefined
? arbitraryValue
: predefinedValue
? roundedPredefinedValues[predefinedValue]
: roundedPredefinedValues.default;
if (arbitraryValueToUse === undefined) {
return match;
}
const newArbitraryValue = +(arbitraryValueToUse * scaleFactor).toFixed(2);
const possiblyPredefinedValue = Object.entries(roundedPredefinedValues).find(
([, v]) => v === arbitraryValueToUse,
)?.[0];
return (
prefix +
(possiblyPredefinedValue && possiblyPredefinedValue !== 'default'
? `-${possiblyPredefinedValue}`
: '') +
(!possiblyPredefinedValue ? `-[${newArbitraryValue}rem]` : '')
);
};
/**
* @param {string} prefix
* @param {number} arbitraryValue
* @param {boolean|undefined} predefined
* @return {[string, string]}
*/
const getReplaceInfoFromArbitraryValue = (prefix, arbitraryValue, predefined) => {
const newArbitraryValue = +(arbitraryValue * scaleFactor).toFixed(2);
const possiblyPredefinedValue = +(newArbitraryValue * 4).toFixed(2);
const standaloneArbitraryRegex = /\[(\-?\d*\.?\d+)rem\]/g;
const isPredefinedValue =
allPredefinedValues.includes(possiblyPredefinedValue) && prefix !== 'text';
/** @type {RegExp} */
let replaceRegex = predefined === true ? `${+(arbitraryValue * 4).toFixed(2)}` : arbitraryValue;
let replaceValue = newArbitraryValue;
if (isPredefinedValue) {
replaceRegex = standaloneArbitraryRegex;
replaceValue = possiblyPredefinedValue;
} else if (predefined === true) {
replaceValue = `[${replaceValue}rem]`;
}
return [replaceRegex, replaceValue];
};
/**
* @param {string} match
* @param {string} prefix
* @param {string} predefinedValue (capture group 1)
* @param {string|undefined} arbitraryValue (capture group 2)
* @return {string}
*/
const classesReplacer = (match, prefix, predefinedValue, arbitraryValue) => {
/** @type {number} */
let arbitraryValueToUse;
if (arbitraryValue !== undefined) {
arbitraryValueToUse = parseFloat(arbitraryValue);
} else {
// Predefined value is correct here
arbitraryValueToUse = parseFloat(predefinedValue) / 4;
}
return match.replace(
...getReplaceInfoFromArbitraryValue(prefix, arbitraryValueToUse, arbitraryValue === undefined),
);
};
const processFiles = (dir) => {
readdirSync(dir).forEach((file) => {
const fullPath = join(dir, file);
if (statSync(fullPath).isDirectory()) {
processFiles(fullPath);
} else if (
file.endsWith('.js') ||
file.endsWith('.jsx') ||
file.endsWith('.ts') ||
file.endsWith('.tsx')
) {
let content = readFileSync(fullPath, 'utf8');
const updatedContent = content
.replace(classesRegex, classesReplacer)
.replace(roundedClassesRegex, roundedClassesReplacer);
if (content !== updatedContent) {
writeFileSync(fullPath, updatedContent, 'utf8');
console.log(`Updated: ${fullPath}`);
}
}
});
};
processFiles(join(__dirname, './src'));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment