diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 248fe125c..0cfc3eac6 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -18,6 +18,15 @@ nav_order: 5 *Joseph Carpenter* +* Add codemod to detect and migrate deprecated Slots setters to new `with_*` prefix introduced in v3.x. Note: This codemod is non-deterministic and works on a best-effort basis. + + ```bash + bin/rails view_component:detect_legacy_slots + bin/rails view_component:migrate_legacy_slots + ``` + + *Hans Lemuet, Kirill Platonov* + ### v3.0.0 1,000+ days and 100+ releases later, the 200+ contributors to ViewComponent are proud to ship v3.0.0! diff --git a/lib/tasks/view_component.rake b/lib/tasks/view_component.rake new file mode 100644 index 000000000..9b0bcd45e --- /dev/null +++ b/lib/tasks/view_component.rake @@ -0,0 +1,15 @@ +require "view_component/codemods/v3_slot_setters" + +namespace :view_component do + task detect_legacy_slots: :environment do + ARGV.each { |a| task a.to_sym {} } + custom_paths = ARGV.compact.map { |path| Rails.root.join(path) } + ViewComponent::Codemods::V3SlotSetters.new(view_path: custom_paths).call + end + + task migrate_legacy_slots: :environment do + ARGV.each { |a| task a.to_sym {} } + custom_paths = ARGV.compact.map { |path| Rails.root.join(path) } + ViewComponent::Codemods::V3SlotSetters.new(view_path: custom_paths, migrate: true).call + end +end diff --git a/lib/view_component/codemods/v3_slot_setters.rb b/lib/view_component/codemods/v3_slot_setters.rb index 7c3a900ad..05d7e1c84 100644 --- a/lib/view_component/codemods/v3_slot_setters.rb +++ b/lib/view_component/codemods/v3_slot_setters.rb @@ -1,16 +1,19 @@ -# Usage (in rails console): -# -# Run the codemod: -# -# ViewComponent::Codemods::V3SlotSetters.new.call -# -# If your app uses custom paths for views, you can pass them in: -# -# ViewComponent::Codemods::V3SlotSetters.new( -# view_path: "../app/views", -# ).call +# frozen_string_literal: true module ViewComponent + # Usage: + # + # Run via rake task: + # + # bin/rails view_component:detect_legacy_slots + # bin/rails view_component:migrate_legacy_slots + # bin/rails view_component:migrate_legacy_slots app/views + # + # Or run via rails console if you need to pass custom paths: + # + # ViewComponent::Codemods::V3SlotSetters.new( + # view_path: Rails.root.join("app/views"), + # ).call module Codemods class V3SlotSetters TEMPLATE_LANGUAGES = %w[erb slim haml].join(",").freeze @@ -18,11 +21,12 @@ class V3SlotSetters Suggestion = Struct.new(:file, :line, :message) - def initialize(view_component_path: [], view_path: []) - Zeitwerk::Loader.eager_load_all + def initialize(view_component_path: [], view_path: [], migrate: false) + Rails.application.eager_load! @view_component_path = view_component_path @view_path = view_path + @migrate = migrate end def call @@ -72,7 +76,8 @@ def scan_exact_matches(file) end end - File.open(file) do |f| + File.open(file, "r+") do |f| + lines = [] f.each_line do |line| rendered_components.each do |rendered_component| arg = rendered_component[:arg] @@ -80,10 +85,25 @@ def scan_exact_matches(file) if (matches = line.scan(/#{arg}\.#{Regexp.union(slots)}/)) matches.each do |match| - suggestions << Suggestion.new(file, f.lineno, "probably replace `#{match}` with `#{match.gsub("#{arg}.", "#{arg}.with_")}`") + new_value = match.gsub("#{arg}.", "#{arg}.with_") + message = if @migrate + "replaced `#{match}` with `#{new_value}`" + else + "probably replace `#{match}` with `#{new_value}`" + end + suggestions << Suggestion.new(file, f.lineno, message) + if @migrate + line.gsub!("#{arg}.", "#{arg}.with_") + end end end end + lines << line + end + + if @migrate + f.rewind + f.write(lines.join) end end end @@ -91,17 +111,30 @@ def scan_exact_matches(file) def scan_uncertain_matches(file) [].tap do |suggestions| - File.open(file) do |f| + File.open(file, "r+") do |f| + lines = [] f.each_line do |line| - if (matches = line.scan(/(?#{Regexp.union(all_registered_slot_names)})/)) - next if matches.size == 0 - + if (matches = line.scan(/(?#{Regexp.union(all_registered_slot_names)})\b/)) matches.flatten.each do |match| next if @suggestions.find { |s| s.file == file && s.line == f.lineno } - suggestions << Suggestion.new(file, f.lineno, "maybe replace `.#{match}` with `.with_#{match}`") + message = if @migrate + "replaced `#{match}` with `with_#{match}`" + else + "maybe replace `#{match}` with `with_#{match}`" + end + suggestions << Suggestion.new(file, f.lineno, message) + if @migrate + line.gsub!(/(? + <% component.subtitle do %> + This is my subtitle! + <% end %> +<% end %> diff --git a/test/sandbox/app/views/codemods/_v2_slots_setters_exact.html.erb b/test/sandbox/app/views/codemods/_v2_slots_setters_exact.html.erb new file mode 100644 index 000000000..404869e8f --- /dev/null +++ b/test/sandbox/app/views/codemods/_v2_slots_setters_exact.html.erb @@ -0,0 +1,14 @@ +<%= render SlotsComponent.new do |component| %> + <% component.title do %> + This is my title! + <% end %> +<% end %> + +<%= render SlotsComponent.new do |component| %> + <% component.tab do %> +