From e2f6a0b2be860c60dc6eb8d2ef55385af8d43ed9 Mon Sep 17 00:00:00 2001 From: Steve Polito Date: Tue, 26 Mar 2024 13:11:43 -0400 Subject: [PATCH] Introduce `suspenders:cleanup:organize_gemfile` task Introduce `Suspenders::Cleanup::OrganizeGemfile` class and corresponding task in an effort to reduce duplicate groups in the modified Gemfile. This is because the [gem_group][] method does not modify existing groups. I've opened [#49512][] in an effort to fix this, but until then, this task will suffice. This class is designed to be run after `suspenders:install:web`, and does not account for all edge cases. For example, it assumes gems are grouped by symbols (i.e. :test and not "test"), and does not account for inline syntax: ```ruby gem 'my-gem', group: [:cucumber, :test] ``` We could consider extracting this into a Gem (with a fun name, of course. Maybe "Polish"), but for now, I think this simple procedural code if just fine. Additionally, this commit removes duplicate Rake task that was generated with the plugin. [gem_group]: https://api.rubyonrails.org/classes/Rails/Generators/Actions.html#method-i-gem_group [#49512]: https://github.com/rails/rails/pull/49512 --- NEWS.md | 1 + README.md | 1 + lib/suspenders.rb | 1 + lib/suspenders/cleanup/organize_gemfile.rb | 134 ++++++++++++++++++ lib/tasks/suspenders.rake | 7 + lib/tasks/suspenders_tasks.rake | 4 - test/fixtures/files/gemfile_clean | 85 +++++++++++ test/fixtures/files/gemfile_messy | 101 +++++++++++++ .../cleanup/organize_gemfile_test.rb | 23 +++ 9 files changed, 353 insertions(+), 4 deletions(-) create mode 100644 lib/suspenders/cleanup/organize_gemfile.rb delete mode 100644 lib/tasks/suspenders_tasks.rake create mode 100644 test/fixtures/files/gemfile_clean create mode 100644 test/fixtures/files/gemfile_messy create mode 100644 test/suspenders/cleanup/organize_gemfile_test.rb diff --git a/NEWS.md b/NEWS.md index 1323a5fe5..a61f8fcdc 100644 --- a/NEWS.md +++ b/NEWS.md @@ -17,6 +17,7 @@ Unreleased * Introduce `suspenders:testing` generator * Introduce `suspenders:prerequisites` generator * Introduce `suspenders:ci` generator +* Introduce `suspenders:cleanup:organize_gemfile` task 20230113.0 (January, 13, 2023) diff --git a/README.md b/README.md index f64559cdb..717ae40c1 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,7 @@ Custom Suspenders tasks ``` bin/rails suspenders:rake bin/rails suspenders:db:migrate +bin/rails suspenders:cleanup:organize_gemfile ``` ### Email diff --git a/lib/suspenders.rb b/lib/suspenders.rb index 0537d4b9c..406303a32 100644 --- a/lib/suspenders.rb +++ b/lib/suspenders.rb @@ -2,6 +2,7 @@ require "suspenders/engine" require "suspenders/railtie" require "suspenders/generators" +require "suspenders/cleanup/organize_gemfile" module Suspenders # Your code goes here... diff --git a/lib/suspenders/cleanup/organize_gemfile.rb b/lib/suspenders/cleanup/organize_gemfile.rb new file mode 100644 index 000000000..bc5b59e43 --- /dev/null +++ b/lib/suspenders/cleanup/organize_gemfile.rb @@ -0,0 +1,134 @@ +module Suspenders + module Cleanup + class OrganizeGemfile + def self.perform(gemfile) + new(gemfile).perform + end + + attr_reader :gemfile, :current_lines, :new_lines, :new_line_markers, + :current_group, :gem_groups + + def initialize(gemfile) + @gemfile = gemfile + + @current_lines = File.read(gemfile).lines + @new_lines = [] + @new_line_markers = [] + + @current_group = nil + @gem_groups = {} + end + + def perform + remove_line_breaks + sort_gems_and_groups + add_gem_groups_to_gemfile + add_line_breaks + cleanup + + File.open(gemfile, "w+") { _1.write new_lines.join } + end + + private + + def remove_line_breaks + current_lines.delete("\n") + end + + def sort_gems_and_groups + current_lines.each do |line| + if line.starts_with?(/group/) + @current_group = line + end + + # Consolidate gem groups + if current_group + if line.starts_with?(/end/) + @current_group = nil + elsif !line.starts_with?(/group/) + gem_groups[current_group] ||= [] + gem_groups[current_group] << line + end + # Add non-grouped gems + elsif !line.starts_with?(/\n/) + new_lines << line + @current_group = nil + end + end + end + + def add_gem_groups_to_gemfile + gem_groups.keys.each do |group| + gems = gem_groups[group] + + gems.each_with_index do |gem, index| + if index == 0 + new_lines << group + end + + new_lines << gem + + if gems.size == (index + 1) + new_lines << "end\n" + end + end + end + end + + def add_line_breaks + new_lines.each_with_index do |line, index| + previous_line = new_lines[index - 1] if index > 0 + next_line = new_lines[index + 1] + marker = index + 1 + + # Add line break if it's a gem and the next line is commented out + if (line.starts_with?(/\s*gem/) || line.starts_with?(/\s*\#\s*gem/)) && next_line&.starts_with?(/\s*\#/) + new_line_markers << marker + end + + # Add line break if it's a commented out gem and the next line is a gem + if line.starts_with?(/\s*\#\s*gem/) && next_line&.starts_with?(/\s*gem/) + new_line_markers << marker + end + + # Add line break if it's a gem with a comment and the next line is a gem + if previous_line&.starts_with?(/\s*\#/) \ + && line.starts_with?(/\s*gem/) \ + && next_line&.starts_with?(/\s*gem/) \ + && !previous_line.starts_with?(/\s*\#\s*gem/) + new_line_markers << marker + end + + # Add a line break if it's /end/ + if line.starts_with?(/end/) + new_line_markers << marker + end + + # Add a line break if it's a gem and the next line is a group + if line.starts_with?(/gem/) && next_line&.starts_with?(/group/) + new_line_markers << marker + end + + # Add line break if it's /source/ or /ruby/ + if line.starts_with?(/\w/) && !line.starts_with?(/\s*(gem|group|end)/) + new_line_markers << marker + end + end + + new_line_markers.each_with_index do |marker, index| + # Each time we insert, the original marker if off by 1 + marker_offset = marker + index + + new_lines.insert(marker_offset, "\n") + end + end + + def cleanup + # Remove last line + if /\n/.match?(new_lines.last) + new_lines.pop + end + end + end + end +end diff --git a/lib/tasks/suspenders.rake b/lib/tasks/suspenders.rake index 9b62842ad..da9a42ae2 100644 --- a/lib/tasks/suspenders.rake +++ b/lib/tasks/suspenders.rake @@ -22,4 +22,11 @@ namespace :suspenders do Rake::Task["db:test:prepare"].invoke end end + + namespace :cleanup do + desc "Organizes Gemfile" + task :organize_gemfile do + Suspenders::Cleanup::OrganizeGemfile.perform(Rails.root.join("Gemfile")) + end + end end diff --git a/lib/tasks/suspenders_tasks.rake b/lib/tasks/suspenders_tasks.rake deleted file mode 100644 index bad21522c..000000000 --- a/lib/tasks/suspenders_tasks.rake +++ /dev/null @@ -1,4 +0,0 @@ -# desc "Explaining what the task does" -# task :suspenders do -# # Task goes here -# end diff --git a/test/fixtures/files/gemfile_clean b/test/fixtures/files/gemfile_clean new file mode 100644 index 000000000..30c5d0c3a --- /dev/null +++ b/test/fixtures/files/gemfile_clean @@ -0,0 +1,85 @@ +source "https://rubygems.org" + +ruby "3.3.0" + +# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" +gem "rails", "~> 7.1.3", ">= 7.1.3.2" + +# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] +gem "sprockets-rails" + +# Use postgresql as the database for Active Record +gem "pg", "~> 1.1" + +# Use the Puma web server [https://github.com/puma/puma] +gem "puma", ">= 5.0" + +# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] +gem "importmap-rails" + +# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] +gem "turbo-rails" + +# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] +gem "stimulus-rails" + +# Build JSON APIs with ease [https://github.com/rails/jbuilder] +gem "jbuilder" + +# Use Redis adapter to run Action Cable in production +gem "redis", ">= 4.0.1" + +# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] +# gem "kredis" + +# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] +# gem "bcrypt", "~> 3.1.7" + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem "tzinfo-data", platforms: %i[windows jruby] + +# Reduces boot times through caching; required in config/boot.rb +gem "bootsnap", require: false + +# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] +# gem "image_processing", "~> 1.2" + +gem "cssbundling-rails" +gem "inline_svg" +gem "sidekiq" +gem "title" + +group :development, :test do + # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem + gem "debug", platforms: %i[mri windows] + + gem "suspenders", github: "thoughtbot/suspenders", branch: "suspenders-3-0-0-web-generator" + gem "bundler-audit", ">= 0.7.0", require: false + gem "factory_bot_rails" + gem "rspec-rails", "~> 6.1.0" + gem "better_html", require: false + gem "erb_lint", require: false + gem "erblint-github", require: false + gem "standard" +end + +group :development do + # Use console on exceptions pages [https://github.com/rails/web-console] + gem "web-console" + + # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] + # gem "rack-mini-profiler" + + # Speed up commands on slow machines / big apps [https://github.com/rails/spring] + # gem "spring" +end + +group :test do + gem "capybara_accessibility_audit" + gem "capybara_accessible_selectors", github: "citizensadvice/capybara_accessible_selectors" + gem "capybara" + gem "action_dispatch-testing-integration-capybara", github: "thoughtbot/action_dispatch-testing-integration-capybara", tag: "v0.1.1", require: "action_dispatch/testing/integration/capybara/rspec" + gem "selenium-webdriver" + gem "shoulda-matchers", "~> 6.0" + gem "webmock" +end diff --git a/test/fixtures/files/gemfile_messy b/test/fixtures/files/gemfile_messy new file mode 100644 index 000000000..7e19e3c1b --- /dev/null +++ b/test/fixtures/files/gemfile_messy @@ -0,0 +1,101 @@ +source "https://rubygems.org" + +ruby "3.3.0" + +# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" +gem "rails", "~> 7.1.3", ">= 7.1.3.2" + +# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] +gem "sprockets-rails" + +# Use postgresql as the database for Active Record +gem "pg", "~> 1.1" + +# Use the Puma web server [https://github.com/puma/puma] +gem "puma", ">= 5.0" + +# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] +gem "importmap-rails" + +# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] +gem "turbo-rails" + +# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] +gem "stimulus-rails" + +# Build JSON APIs with ease [https://github.com/rails/jbuilder] +gem "jbuilder" + +# Use Redis adapter to run Action Cable in production +gem "redis", ">= 4.0.1" + +# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] +# gem "kredis" + +# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] +# gem "bcrypt", "~> 3.1.7" + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem "tzinfo-data", platforms: %i[windows jruby] + +# Reduces boot times through caching; required in config/boot.rb +gem "bootsnap", require: false + +# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] +# gem "image_processing", "~> 1.2" + +group :development, :test do + # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem + gem "debug", platforms: %i[mri windows] +end + +group :development do + # Use console on exceptions pages [https://github.com/rails/web-console] + gem "web-console" + + # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] + # gem "rack-mini-profiler" + + # Speed up commands on slow machines / big apps [https://github.com/rails/spring] + # gem "spring" +end + +group :development, :test do + gem "suspenders", github: "thoughtbot/suspenders", branch: "suspenders-3-0-0-web-generator" +end + +group :test do + gem "capybara_accessibility_audit" + gem "capybara_accessible_selectors", github: "citizensadvice/capybara_accessible_selectors" +end +gem "cssbundling-rails" + +group :development, :test do + gem "bundler-audit", ">= 0.7.0", require: false +end +gem "inline_svg" + +group :development, :test do + gem "factory_bot_rails" +end +gem "sidekiq" +gem "title" + +group :development, :test do + gem "rspec-rails", "~> 6.1.0" +end + +group :test do + gem "capybara" + gem "action_dispatch-testing-integration-capybara", github: "thoughtbot/action_dispatch-testing-integration-capybara", tag: "v0.1.1", require: "action_dispatch/testing/integration/capybara/rspec" + gem "selenium-webdriver" + gem "shoulda-matchers", "~> 6.0" + gem "webmock" +end + +group :development, :test do + gem "better_html", require: false + gem "erb_lint", require: false + gem "erblint-github", require: false + gem "standard" +end diff --git a/test/suspenders/cleanup/organize_gemfile_test.rb b/test/suspenders/cleanup/organize_gemfile_test.rb new file mode 100644 index 000000000..b405ea375 --- /dev/null +++ b/test/suspenders/cleanup/organize_gemfile_test.rb @@ -0,0 +1,23 @@ +require "test_helper" +require "tempfile" +require_relative "../../../lib/suspenders/cleanup/organize_gemfile" + +module Suspenders + module Cleanup + class OrganizeGemfileTest < ActiveSupport::TestCase + test "organizes Gemfile by group" do + original = file_fixture("gemfile_messy").read + modified = file_fixture("gemfile_clean").read + + Tempfile.create "Gemfile" do |gemfile| + gemfile.write original + gemfile.rewind + + Suspenders::Cleanup::OrganizeGemfile.perform(gemfile.path) + + assert_equal modified, gemfile.read + end + end + end + end +end