Skip to content

Commit f6c6f45

Browse files
Introduce suspenders:cleanup:organize_gemfile task (#1181)
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]: rails/rails#49512
1 parent f76fb25 commit f6c6f45

File tree

9 files changed

+353
-4
lines changed

9 files changed

+353
-4
lines changed

NEWS.md

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Unreleased
1717
* Introduce `suspenders:testing` generator
1818
* Introduce `suspenders:prerequisites` generator
1919
* Introduce `suspenders:ci` generator
20+
* Introduce `suspenders:cleanup:organize_gemfile` task
2021

2122
20230113.0 (January, 13, 2023)
2223

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ Custom Suspenders tasks
157157
```
158158
bin/rails suspenders:rake
159159
bin/rails suspenders:db:migrate
160+
bin/rails suspenders:cleanup:organize_gemfile
160161
```
161162

162163
### Email

lib/suspenders.rb

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
require "suspenders/engine"
33
require "suspenders/railtie"
44
require "suspenders/generators"
5+
require "suspenders/cleanup/organize_gemfile"
56

67
module Suspenders
78
# Your code goes here...
+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
module Suspenders
2+
module Cleanup
3+
class OrganizeGemfile
4+
def self.perform(gemfile)
5+
new(gemfile).perform
6+
end
7+
8+
attr_reader :gemfile, :current_lines, :new_lines, :new_line_markers,
9+
:current_group, :gem_groups
10+
11+
def initialize(gemfile)
12+
@gemfile = gemfile
13+
14+
@current_lines = File.read(gemfile).lines
15+
@new_lines = []
16+
@new_line_markers = []
17+
18+
@current_group = nil
19+
@gem_groups = {}
20+
end
21+
22+
def perform
23+
remove_line_breaks
24+
sort_gems_and_groups
25+
add_gem_groups_to_gemfile
26+
add_line_breaks
27+
cleanup
28+
29+
File.open(gemfile, "w+") { _1.write new_lines.join }
30+
end
31+
32+
private
33+
34+
def remove_line_breaks
35+
current_lines.delete("\n")
36+
end
37+
38+
def sort_gems_and_groups
39+
current_lines.each do |line|
40+
if line.starts_with?(/group/)
41+
@current_group = line
42+
end
43+
44+
# Consolidate gem groups
45+
if current_group
46+
if line.starts_with?(/end/)
47+
@current_group = nil
48+
elsif !line.starts_with?(/group/)
49+
gem_groups[current_group] ||= []
50+
gem_groups[current_group] << line
51+
end
52+
# Add non-grouped gems
53+
elsif !line.starts_with?(/\n/)
54+
new_lines << line
55+
@current_group = nil
56+
end
57+
end
58+
end
59+
60+
def add_gem_groups_to_gemfile
61+
gem_groups.keys.each do |group|
62+
gems = gem_groups[group]
63+
64+
gems.each_with_index do |gem, index|
65+
if index == 0
66+
new_lines << group
67+
end
68+
69+
new_lines << gem
70+
71+
if gems.size == (index + 1)
72+
new_lines << "end\n"
73+
end
74+
end
75+
end
76+
end
77+
78+
def add_line_breaks
79+
new_lines.each_with_index do |line, index|
80+
previous_line = new_lines[index - 1] if index > 0
81+
next_line = new_lines[index + 1]
82+
marker = index + 1
83+
84+
# Add line break if it's a gem and the next line is commented out
85+
if (line.starts_with?(/\s*gem/) || line.starts_with?(/\s*\#\s*gem/)) && next_line&.starts_with?(/\s*\#/)
86+
new_line_markers << marker
87+
end
88+
89+
# Add line break if it's a commented out gem and the next line is a gem
90+
if line.starts_with?(/\s*\#\s*gem/) && next_line&.starts_with?(/\s*gem/)
91+
new_line_markers << marker
92+
end
93+
94+
# Add line break if it's a gem with a comment and the next line is a gem
95+
if previous_line&.starts_with?(/\s*\#/) \
96+
&& line.starts_with?(/\s*gem/) \
97+
&& next_line&.starts_with?(/\s*gem/) \
98+
&& !previous_line.starts_with?(/\s*\#\s*gem/)
99+
new_line_markers << marker
100+
end
101+
102+
# Add a line break if it's /end/
103+
if line.starts_with?(/end/)
104+
new_line_markers << marker
105+
end
106+
107+
# Add a line break if it's a gem and the next line is a group
108+
if line.starts_with?(/gem/) && next_line&.starts_with?(/group/)
109+
new_line_markers << marker
110+
end
111+
112+
# Add line break if it's /source/ or /ruby/
113+
if line.starts_with?(/\w/) && !line.starts_with?(/\s*(gem|group|end)/)
114+
new_line_markers << marker
115+
end
116+
end
117+
118+
new_line_markers.each_with_index do |marker, index|
119+
# Each time we insert, the original marker if off by 1
120+
marker_offset = marker + index
121+
122+
new_lines.insert(marker_offset, "\n")
123+
end
124+
end
125+
126+
def cleanup
127+
# Remove last line
128+
if /\n/.match?(new_lines.last)
129+
new_lines.pop
130+
end
131+
end
132+
end
133+
end
134+
end

lib/tasks/suspenders.rake

+7
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,11 @@ namespace :suspenders do
2222
Rake::Task["db:test:prepare"].invoke
2323
end
2424
end
25+
26+
namespace :cleanup do
27+
desc "Organizes Gemfile"
28+
task :organize_gemfile do
29+
Suspenders::Cleanup::OrganizeGemfile.perform(Rails.root.join("Gemfile"))
30+
end
31+
end
2532
end

lib/tasks/suspenders_tasks.rake

-4
This file was deleted.

test/fixtures/files/gemfile_clean

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
source "https://rubygems.org"
2+
3+
ruby "3.3.0"
4+
5+
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
6+
gem "rails", "~> 7.1.3", ">= 7.1.3.2"
7+
8+
# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
9+
gem "sprockets-rails"
10+
11+
# Use postgresql as the database for Active Record
12+
gem "pg", "~> 1.1"
13+
14+
# Use the Puma web server [https://github.com/puma/puma]
15+
gem "puma", ">= 5.0"
16+
17+
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
18+
gem "importmap-rails"
19+
20+
# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
21+
gem "turbo-rails"
22+
23+
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
24+
gem "stimulus-rails"
25+
26+
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
27+
gem "jbuilder"
28+
29+
# Use Redis adapter to run Action Cable in production
30+
gem "redis", ">= 4.0.1"
31+
32+
# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]
33+
# gem "kredis"
34+
35+
# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
36+
# gem "bcrypt", "~> 3.1.7"
37+
38+
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
39+
gem "tzinfo-data", platforms: %i[windows jruby]
40+
41+
# Reduces boot times through caching; required in config/boot.rb
42+
gem "bootsnap", require: false
43+
44+
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
45+
# gem "image_processing", "~> 1.2"
46+
47+
gem "cssbundling-rails"
48+
gem "inline_svg"
49+
gem "sidekiq"
50+
gem "title"
51+
52+
group :development, :test do
53+
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
54+
gem "debug", platforms: %i[mri windows]
55+
56+
gem "suspenders", github: "thoughtbot/suspenders", branch: "suspenders-3-0-0-web-generator"
57+
gem "bundler-audit", ">= 0.7.0", require: false
58+
gem "factory_bot_rails"
59+
gem "rspec-rails", "~> 6.1.0"
60+
gem "better_html", require: false
61+
gem "erb_lint", require: false
62+
gem "erblint-github", require: false
63+
gem "standard"
64+
end
65+
66+
group :development do
67+
# Use console on exceptions pages [https://github.com/rails/web-console]
68+
gem "web-console"
69+
70+
# Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]
71+
# gem "rack-mini-profiler"
72+
73+
# Speed up commands on slow machines / big apps [https://github.com/rails/spring]
74+
# gem "spring"
75+
end
76+
77+
group :test do
78+
gem "capybara_accessibility_audit"
79+
gem "capybara_accessible_selectors", github: "citizensadvice/capybara_accessible_selectors"
80+
gem "capybara"
81+
gem "action_dispatch-testing-integration-capybara", github: "thoughtbot/action_dispatch-testing-integration-capybara", tag: "v0.1.1", require: "action_dispatch/testing/integration/capybara/rspec"
82+
gem "selenium-webdriver"
83+
gem "shoulda-matchers", "~> 6.0"
84+
gem "webmock"
85+
end

test/fixtures/files/gemfile_messy

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
source "https://rubygems.org"
2+
3+
ruby "3.3.0"
4+
5+
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
6+
gem "rails", "~> 7.1.3", ">= 7.1.3.2"
7+
8+
# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
9+
gem "sprockets-rails"
10+
11+
# Use postgresql as the database for Active Record
12+
gem "pg", "~> 1.1"
13+
14+
# Use the Puma web server [https://github.com/puma/puma]
15+
gem "puma", ">= 5.0"
16+
17+
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
18+
gem "importmap-rails"
19+
20+
# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
21+
gem "turbo-rails"
22+
23+
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
24+
gem "stimulus-rails"
25+
26+
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
27+
gem "jbuilder"
28+
29+
# Use Redis adapter to run Action Cable in production
30+
gem "redis", ">= 4.0.1"
31+
32+
# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]
33+
# gem "kredis"
34+
35+
# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
36+
# gem "bcrypt", "~> 3.1.7"
37+
38+
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
39+
gem "tzinfo-data", platforms: %i[windows jruby]
40+
41+
# Reduces boot times through caching; required in config/boot.rb
42+
gem "bootsnap", require: false
43+
44+
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
45+
# gem "image_processing", "~> 1.2"
46+
47+
group :development, :test do
48+
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
49+
gem "debug", platforms: %i[mri windows]
50+
end
51+
52+
group :development do
53+
# Use console on exceptions pages [https://github.com/rails/web-console]
54+
gem "web-console"
55+
56+
# Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]
57+
# gem "rack-mini-profiler"
58+
59+
# Speed up commands on slow machines / big apps [https://github.com/rails/spring]
60+
# gem "spring"
61+
end
62+
63+
group :development, :test do
64+
gem "suspenders", github: "thoughtbot/suspenders", branch: "suspenders-3-0-0-web-generator"
65+
end
66+
67+
group :test do
68+
gem "capybara_accessibility_audit"
69+
gem "capybara_accessible_selectors", github: "citizensadvice/capybara_accessible_selectors"
70+
end
71+
gem "cssbundling-rails"
72+
73+
group :development, :test do
74+
gem "bundler-audit", ">= 0.7.0", require: false
75+
end
76+
gem "inline_svg"
77+
78+
group :development, :test do
79+
gem "factory_bot_rails"
80+
end
81+
gem "sidekiq"
82+
gem "title"
83+
84+
group :development, :test do
85+
gem "rspec-rails", "~> 6.1.0"
86+
end
87+
88+
group :test do
89+
gem "capybara"
90+
gem "action_dispatch-testing-integration-capybara", github: "thoughtbot/action_dispatch-testing-integration-capybara", tag: "v0.1.1", require: "action_dispatch/testing/integration/capybara/rspec"
91+
gem "selenium-webdriver"
92+
gem "shoulda-matchers", "~> 6.0"
93+
gem "webmock"
94+
end
95+
96+
group :development, :test do
97+
gem "better_html", require: false
98+
gem "erb_lint", require: false
99+
gem "erblint-github", require: false
100+
gem "standard"
101+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
require "test_helper"
2+
require "tempfile"
3+
require_relative "../../../lib/suspenders/cleanup/organize_gemfile"
4+
5+
module Suspenders
6+
module Cleanup
7+
class OrganizeGemfileTest < ActiveSupport::TestCase
8+
test "organizes Gemfile by group" do
9+
original = file_fixture("gemfile_messy").read
10+
modified = file_fixture("gemfile_clean").read
11+
12+
Tempfile.create "Gemfile" do |gemfile|
13+
gemfile.write original
14+
gemfile.rewind
15+
16+
Suspenders::Cleanup::OrganizeGemfile.perform(gemfile.path)
17+
18+
assert_equal modified, gemfile.read
19+
end
20+
end
21+
end
22+
end
23+
end

0 commit comments

Comments
 (0)