Class: Seams::Generators::RemoveGenerator

Inherits:
Rails::Generators::NamedBase
  • Object
show all
Includes:
HostInjector
Defined in:
lib/generators/seams/remove/remove_generator.rb

Overview

Removes an engine generated by ‘seams:engine`. Prompts for confirmation unless –force is passed. Cleans up the surviving engines’ .rubocop.yml so OtherEngines no longer references the engine that was just removed. Reverses host file edits made by the canonical generators (mount line, includes) — leaves the Gemfile alone since other engines may share gem deps.

Run with: bin/rails generate seams:remove billing [–force]

Constant Summary collapse

NAME_PATTERN =

Same constraints as EngineGenerator — keeps ‘seams:remove ../../etc` from being interpreted as a relative path the destructive FileUtils.rm_rf at line 80 would happily follow with –force.

/\A[a-z][a-z0-9_]*\z/
CANONICAL_HOST_EDITS =

Maps engine name -> { mount: <Class>, includes: { user: […], application_controller: […] } } Lets remove know what to undo for the canonical engines. Generic engines aren’t in this table and just get the directory deleted.

{
  "auth" => {
    mount: "Auth::Engine",
    user_includes: %w[Auth::Authenticatable],
    application_controller_includes: %w[Auth::Authentication]
  },
  "notifications" => {
    mount: "Notifications::Engine",
    user_includes: %w[Notifications::Notifiable]
  },
  # Post-Wave-9: billing no longer injects Billing::Billable
  # into the host User — the engine includes it into the
  # configured tenant class (default Accounts::Account) at
  # boot via Billing.configuration.billable_class. The only
  # host-side edit billing makes is the mount line, so unmount
  # is the only reverse-edit we need.
  "billing" => {
    mount: "Billing::Engine",
    user_includes: %w[]
  },
  "teams" => {
    # Wave 9 removed Teams::Teamable — there's no canonical host
    # User concern to remove. The `mount Teams::Engine` line is
    # the only host edit the teams generator makes, so unmount
    # is the only reverse-edit we need here.
    mount: "Teams::Engine",
    user_includes: %w[]
  },
  # Wave 9 — accounts engine. Like billing/teams post-Wave-9, the
  # accounts generator does NOT inject anything into the host User
  # (the canonical demo doesn't have one). Mount is the only
  # host-side edit, so unmount is the only reverse-edit we need.
  "accounts" => {
    mount: "Accounts::Engine",
    user_includes: %w[]
  },
  # Wave 11A — admin engine. Mounts under Seams::Admin::Engine.
  # The generator injects `gem "administrate"` and `gem "pundit"`
  # into the host Gemfile but the remover does NOT prune them
  # (the host may have other dashboards depending on either).
  # Unmounting + dropping the engine dir is enough.
  "admin" => {
    mount: "Seams::Admin::Engine",
    user_includes: %w[]
  }
}.freeze

Instance Method Summary collapse

Methods included from HostInjector

#host_inject_gem, #host_inject_include_in_application_controller, #host_inject_include_in_user, #host_inject_mount, #host_uninject_gem, #host_uninject_include, #host_uninject_mount, #routes_draw_anchor

Instance Method Details

#capture_engine_tablesObject

Read the engine’s create_table calls before its directory is deleted. Used by write_drop_table_migration to generate the reversal. Pattern-matches the literal ActiveRecord::Migration call so we drop exactly what the engine created — never host-side tables that happen to share the prefix.



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/generators/seams/remove/remove_generator.rb', line 93

def capture_engine_tables
  engine_path = File.join(destination_root, "engines", name)
  return @engine_tables = [] unless File.directory?(engine_path)

  migrate_dir = File.join(engine_path, "db/migrate")
  return @engine_tables = [] unless File.directory?(migrate_dir)

  @engine_tables = Dir.glob(File.join(migrate_dir, "*.rb")).flat_map do |file|
    # Strip line comments before scanning so a stray
    # `# create_table :backup_table do |t|` in a migration
    # doesn't end up in the drop-table list.
    source = File.read(file).gsub(/^\s*#.*$/, "")
    source.scan(/^\s*create_table\s+:(\w+)/).flatten
  end.uniq
end

#remove_engine_directoryObject



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/generators/seams/remove/remove_generator.rb', line 109

def remove_engine_directory
  engine_path = File.join(destination_root, "engines", name)

  unless File.directory?(engine_path)
    @engine_was_present = false
    say "  skip    engines/#{name}/ (not found)", :yellow
    return
  end

  return unless options[:force] || confirm_removal?

  FileUtils.rm_rf(engine_path)
  @engine_was_present = true
  say "  remove  engines/#{name}/", :red
end

#run_bundle_installObject

Phase 1.7 — re-run ‘bundle install` so the host’s lockfile no longer references the removed engine’s gem deps (the canonical generators each inject their own — bcrypt, faraday, etc; the remover doesn’t touch the Gemfile because deps are usually shared, but a fresh ‘bundle install` keeps lockfile + Gemfile in sync if the host did prune anything by hand). Skipped when the engine was never present (no-op remove) or when there’s no Gemfile to bundle against.



152
153
154
155
156
157
158
159
160
# File 'lib/generators/seams/remove/remove_generator.rb', line 152

def run_bundle_install
  return unless @engine_was_present
  return unless File.exist?(File.join(destination_root, "Gemfile"))

  say "  run     bundle install (post-remove sync)", :green
  Dir.chdir(destination_root) do
    system("bundle", "install", "--quiet") || say("          → bundle install failed; resolve manually.", :red)
  end
end

#unwire_from_hostObject



179
180
181
182
183
184
# File 'lib/generators/seams/remove/remove_generator.rb', line 179

def unwire_from_host
  return unless @engine_was_present

  unwire_generic_host_edits
  unwire_canonical_host_edits(CANONICAL_HOST_EDITS[name])
end

#update_sibling_enginesObject



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/generators/seams/remove/remove_generator.rb', line 162

def update_sibling_engines
  return unless @engine_was_present

  engines_root = File.join(destination_root, "engines")
  return unless Dir.exist?(engines_root)

  survivors = Dir.children(engines_root)
                 .select { |c| File.directory?(File.join(engines_root, c)) }
                 .reject { |c| c.start_with?(".") }
                 .sort

  return if survivors.empty?

  Seams::Generators::SiblingRubocopWriter.rewrite!(engines_root: engines_root, dirs: survivors)
  say "  update  .rubocop.yml of #{survivors.size} sibling engine(s)", :green
end

#validate_nameObject



30
31
32
33
34
35
36
# File 'lib/generators/seams/remove/remove_generator.rb', line 30

def validate_name
  return if NAME_PATTERN.match?(name)

  raise Seams::GeneratorError,
        "Engine name #{name.inspect} must be lowercase letters, digits, " \
        "and underscores, starting with a letter."
end

#write_drop_table_migrationObject

Phase 1.7 — generate a drop-table migration in the host’s db/migrate so the host can run ‘bin/rails db:migrate` to reclaim the engine’s tables. Idempotent if-table-exists check so re-running doesn’t blow up on already-dropped tables.



129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/generators/seams/remove/remove_generator.rb', line 129

def write_drop_table_migration
  return unless @engine_was_present
  return if @engine_tables.nil? || @engine_tables.empty?

  migrate_dir = File.join(destination_root, "db/migrate")
  FileUtils.mkdir_p(migrate_dir)

  filename       = "#{drop_migration_timestamp}_drop_#{name}_tables.rb"
  migration_path = File.join(migrate_dir, filename)
  File.write(migration_path, drop_table_migration_body)

  say "  create  db/migrate/#{filename}", :green
  say "          → run `bin/rails db:migrate` to drop #{@engine_tables.size} table(s).", :yellow
end