Last active
August 25, 2022 23:17
-
-
Save borama/952edd2a0dc286a8b4591bb3ff19fd03 to your computer and use it in GitHub Desktop.
A script to compare the classes in two CSS files. Useful for migrating utility CSS systems, such as Tachyons to Tailwind. Please find all the context here: https://dev.to/nejremeslnici/migrating-tachyons-to-tailwind-css-part-i-ich.
This file contains 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
#!/bin/env ruby | |
# Compare classes between Tachyons & Tailwind | |
# | |
# Please read https://dev.to/nejremeslnici/migrating-tachyons-to-tailwind-css-part-i-ich for more context. | |
# | |
# To use this, do the following: | |
# Install css_parser gem (https://github.com/premailer/css_parser) | |
# - `gem install css_parser` | |
# | |
# Make this script executable: `chmod a+x compare_classes.rb`. | |
# | |
# Prepare the final CSS file from Tachyons: | |
# - add Tachyons to asset pipeline: add `//= link tachyons.css` to app/assets/config/manifest.js | |
# - run asset precompilation: `bundle exec rails assets:precompile` | |
# - the compiled css file is now in public/assets/tachyons*.css | |
# - purge unused classes (we ignore those in the migration): | |
# `npx purgecss -con "app/views/**/*" "app/helpers/**/*" "app/presenters/**/*" "app/javascript/**/*.js" -css public/assets/tachyons*.css -o /tmp/` | |
# - rename the final CSS: `mv /tmp/tachyons*.css /tmp/tachyons.css` | |
# - the final CSS is now in /tmp/tachyons.css | |
# | |
# Prepare the final CSS from Tailwind: | |
# - temporarily remove the `@import 'tailwindcss/base';` line from app/javascript/stylesheets/nere-tailwind.pcss | |
# (to remove base styles) | |
# - temporarily disable css purging in tailwind.config.js: `purge: { enable: false }` | |
# - show a page in dev environment (to recompile the CSS via Webpacker) or run the recompilation again manually | |
# - copy the compiled CSS to temp: `cp public/packs/css/application.css /tmp/tailwind.css` | |
# - the final CSS is now in /tmp/tailwind.css | |
# | |
# Now, call this script from the project root: | |
# `resources/tailwind/compare_classes.rb /tmp/tachyons.css /tmp/tailwind.css migrated_classes.txt | |
# | |
# It will output three sections: | |
# - identical classes = same name & declarations | |
# - renamed classes = different name but same declarations | |
# - colliding classes = same name but different declarations | |
# - missing classes = different name & declarations | |
# | |
# for each class, an approximate usage count in the project will be shown, too (uses "grep" command). | |
# | |
# Configuration: | |
# | |
# - TEMPLATE_DIRECTORIES - the directories of templates or other code to search utility classes for | |
# - TACHYONS_MQS_REGEX - all responsive variants (postfixes) that we can expect in the Tachyons CSS file | |
# - TACHYONS_MIGRATED_MQS_REGEX - the responsive variants that we actually are about to migrate | |
# (may be the same as TACHYONS_MQS_REGEX or a subset) | |
# - TAILWIND_MQS_REGEX - all responsive variants (prefixes) that we can expect in the Tailwind CSS file | |
require "css_parser" | |
include CssParser # rubocop:disable Style/MixinUsage | |
require "yaml" | |
if ARGV.length < 2 | |
puts "Compares CSS classes between Tachyons and Tailwind" | |
puts "Usage: compare_classes.rb path/to/tachyons.css path/to/tailwind.css [migrated_classes.txt]" | |
exit 1 | |
end | |
TEMPLATE_DIRECTORIES = %w[app/views app/helpers app/javascript/src app/presenters].freeze | |
TACHYONS_MQS_REGEX = /-(ns|s|m|l)$/.freeze | |
TACHYONS_MIGRATED_MQS_REGEX = /-(ns|l)$/.freeze | |
TAILWIND_MQS_REGEX = /^\.(sm|md|lg|xl|2xl|32xl):/.freeze | |
def usage_count(class_name) | |
command = "grep -RE '\\b#{class_name.delete('.')}\\b' #{TEMPLATE_DIRECTORIES.join(' ')} | wc -l" | |
`#{command}`.strip.to_i | |
end | |
def normalize_selector(selector) | |
selector.delete('\\') # remove escaping | |
end | |
def normalize_declarations(css_declarations) | |
declarations = css_declarations.gsub(/:\s+/, ": ").split(/\s*;\s*/).sort.map do |declaration| | |
declaration.gsub(/\b0px\b/, "0") # "normalize" 0px to 0 | |
.gsub(/^-[a-z-]+:.*$/, "") # remove prefixed properties and variables | |
.delete('\\') # remove escaping | |
.strip | |
end | |
declarations.select { |d| !d.nil? && d != "" }.join("; ") | |
end | |
tachyons_parser = CssParser::Parser.new | |
tachyons_parser.load_string!(File.read(ARGV[0])) | |
tailwind_parser = CssParser::Parser.new | |
tailwind_parser.load_string!(File.read(ARGV[1])) | |
tachyons = {} | |
tachyons_parser.each_selector do |selector, declarations, _specificity| | |
selector = normalize_selector(selector) | |
next if selector.match?(/^[^.]/) # only utility classes | |
next if selector.match?(/[ ,:]/) # no complex selector | |
next if selector.match?(/\..*\./) # no complex selector | |
next if selector.match?(/^\.(fg|hfg|fsfg|bg|hbg|b|hb|s|hs)-/) # no colors | |
next if selector.match?(/^\.(border-underline-.*):/) # special excludes | |
# extract media query | |
variant = selector.match?(TACHYONS_MQS_REGEX) | |
next if variant && !selector.match?(TACHYONS_MIGRATED_MQS_REGEX) | |
selector = selector.sub(TACHYONS_MIGRATED_MQS_REGEX, "") if variant | |
declarations = normalize_declarations(declarations) | |
next if declarations.nil? || declarations == "" | |
if tachyons[declarations] | |
# unless variant | |
# puts "Warning: not redefining declaration #{declarations} " \ | |
# "from #{tachyons[declarations]} to #{selector}! (Tachyons)" | |
# end | |
next | |
end | |
tachyons[declarations] = selector | |
end | |
tailwind = {} | |
tailwind_parser.each_selector do |selector, declarations, _specificity| | |
selector = normalize_selector(selector) | |
next if selector.match?(/^[^.]/) # only utility classes | |
next if selector.match?(/[ ,]/) # no complex selectors | |
next if selector.match?(/^\.(hover|focus|focus-within|:):/) # no complex selectors | |
next if selector.match?(TAILWIND_MQS_REGEX) # no media query variants | |
declarations = normalize_declarations(declarations) | |
next if declarations.nil? || declarations == "" | |
if tailwind[declarations] | |
# puts "Warning: not redefining declaration #{declarations} from #{tailwind[declarations]} to #{selector}! (Tailwind)" | |
next | |
end | |
tailwind[declarations] = selector | |
end | |
# identical classes | |
output = [] | |
(tachyons.keys & tailwind.keys).each do |declaration| | |
next if tachyons[declaration] != tailwind[declaration] | |
output << " #{tachyons[declaration]} ⟶ #{tailwind[declaration]} (#{usage_count(tachyons[declaration])})" | |
end | |
puts "Identical classes (#{output.size}):" | |
puts output.sort | |
to_migrate_count = 0 | |
# renamed classes | |
output = [] | |
(tachyons.keys & tailwind.keys).each do |declaration| | |
next if tachyons[declaration] == tailwind[declaration] | |
output << " #{tachyons[declaration]} ⟶ #{tailwind[declaration]} (#{usage_count(tachyons[declaration])})" | |
to_migrate_count += 1 | |
end | |
puts "Renamed classes (#{output.size}):" | |
puts output.sort | |
# collisions | |
tachyons_rev = tachyons.invert | |
tailwind_rev = tailwind.invert | |
output = [] | |
(tachyons_rev.keys & tailwind_rev.keys).each do |class_name| | |
next if tachyons_rev[class_name] == tailwind_rev[class_name] | |
output << " #{class_name} (#{usage_count(class_name)}, | |
tachyons: '#{tachyons_rev[class_name]}', | |
tailwind: '#{tailwind_rev[class_name]}')".gsub(/[[:space:]]+/, " ").strip | |
to_migrate_count += 1 | |
end | |
puts "Colliding classes (#{output.size}):" | |
puts output.sort | |
# missed classes | |
output = [] | |
(tachyons.keys - tailwind.keys).each do |declaration| | |
output << " #{tachyons[declaration]} (#{usage_count(tachyons[declaration])})" | |
to_migrate_count += 1 | |
end | |
puts "Missed classes (non-colours) (#{output.size}):" | |
puts output.sort | |
puts "Found #{to_migrate_count} classes still to migrate." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment