Skip to content

Instantly share code, notes, and snippets.

@loicknuchel
Created February 4, 2022 23:39
Show Gist options
  • Save loicknuchel/4275a06736f834709a2dbc43a98b65ab to your computer and use it in GitHub Desktop.
Save loicknuchel/4275a06736f834709a2dbc43a98b65ab to your computer and use it in GitHub Desktop.
Check if tailwind classes are correctly generated
/*
Using tailwind 3 CLI when generating dynamic classes can sometimes be quite painful as you never know which classes may be missing.
Not anymore with this script!
It gets your stylesheets, then all your classes in HTML and print classes that are present in HTML but not in stylesheets.
*/
function identifyMissingClasses() {
getStyles().then(styles => {
const classes = getClassCounts(document.getElementsByTagName('html')[0])
const matches = matchClassesInStyles(styles, classes)
const misses = filterMissingClasses(matches)
console.log(`${Object.keys(misses).length} missing classes in styles`, misses)
})
}
function getStyles() {
const urls = Array.from(document.querySelectorAll('link[rel=stylesheet]')).map(l => l.href)
return Promise.all(urls.map(url => fetch(url).then(r => r.text()))).then(styles => styles.join('\n'))
}
function getClassCounts(node, classCounts = {}) {
node.classList.forEach(clazz => classCounts[clazz] ? classCounts[clazz]++ : classCounts[clazz] = 1)
Array.from(node.children).forEach(child => getClassCounts(child, classCounts))
return classCounts
}
function matchClassesInStyles(styles, classCounts) {
const res = {}
Object.keys(classCounts).forEach(clazz => {
const selector = classToCssSelector(clazz)
const matches = [...(styles.matchAll(new RegExp(`${escapeRegex(selector)} \{[^}]+}`, 'g')))].map(r => r[0])
res[clazz] = { count: classCounts[clazz], matches }
})
return res
}
function classToCssSelector(clazz) {
const [value, state] = clazz.split(':').reverse()
let className = value
.replaceAll('.', '\\.') // px-2.5 => .px-2\.5
.replaceAll('/', '\\/') // h-1/2 => .h-1\/2
let extension = className.startsWith('placeholder-') ? '::placeholder' : '' // placeholder-gray-500 => .placeholder-gray-500::placeholder
className = className.startsWith('space-') ? className + ' > :not([hidden]) ~ :not([hidden])' : className // space-x-3 => .space-x-3 > :not([hidden]) ~ :not([hidden])
className = className.startsWith('divide-') ? className + ' > :not([hidden]) ~ :not([hidden])' : className // divide-gray-200 => .divide-gray-200 > :not([hidden]) ~ :not([hidden])
switch (state) {
case 'hover': return `.hover\\:${className}:hover${extension}` // hover:to-indigo-700 => .hover\:to-indigo-700:hover
case 'focus': return `.focus\\:${className}:focus${extension}` // focus:ring-1 => .focus\:ring-1:focus
case 'focus-within': return `.focus-within\\:${className}:focus-within${extension}` // focus-within:outline-none => .focus-within\:outline-none:focus-within
case 'disabled': return `.disabled\\:${className}:disabled${extension}` // disabled:border-gray-200 => .disabled\:border-gray-200:disabled
case undefined: return `.${className}${extension}` // mt-6 => .mt-6
default: return `.${state}\\:${className}${extension}` // md:absolute => .md\:absolute
}
}
function escapeRegex(source) {
return source.replace(/[-[/\]{}()*+?.,\\^$|#\s]/g, '\\$&')
}
function filterMissingClasses(matches) {
const res = {}
Object.keys(matches).forEach(key => {
if (matches[key].matches.length === 0) {
res[key] = matches[key]
}
})
return res
}
identifyMissingClasses()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment