Class: Seams::Generators::InstallGenerator

Inherits:
Rails::Generators::Base
  • Object
show all
Includes:
HostInjector
Defined in:
lib/generators/seams/install/install_generator.rb

Overview

Adds the Seams framework to a host Rails application:

- config/initializers/seams.rb        (configure adapters)
- config/initializers/seams_engines.rb (load engines/* into autoload)
- engines/.keep                       (where future engines live)
- lib/tasks/seams.rake                (rake namespace)

Run with: bin/rails generate seams:install

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

#append_engines_to_eager_load ⇒ Object



51
52
53
# File 'lib/generators/seams/install/install_generator.rb', line 51

def append_engines_to_eager_load
  template "seams_engines.rb.tt", "config/seams_engines.rb"
end

#create_architecture_doc ⇒ Object



193
194
195
# File 'lib/generators/seams/install/install_generator.rb', line 193

def create_architecture_doc
  template_if_missing "doc/ARCHITECTURE.md.tt", "doc/ARCHITECTURE.md"
end

#create_bin_seams ⇒ Object



143
144
145
146
147
# File 'lib/generators/seams/install/install_generator.rb', line 143

def create_bin_seams
  template "bin_seams.tt", "bin/seams"
  full_path = File.join(destination_root, "bin/seams")
  File.chmod(0o755, full_path) if File.exist?(full_path)
end

#create_ci_workflow ⇒ Object



127
128
129
# File 'lib/generators/seams/install/install_generator.rb', line 127

def create_ci_workflow
  template "ci.yml.tt", ".github/workflows/ci.yml"
end

#create_deployment_templates ⇒ Object



131
132
133
134
135
136
137
138
139
140
141
# File 'lib/generators/seams/install/install_generator.rb', line 131

def create_deployment_templates
  # Skip any file the host already has — Rails 8 ships its own
  # Dockerfile and bin/docker-entrypoint.
  template_if_missing "Dockerfile.tt",         "Dockerfile"
  template_if_missing "docker-entrypoint.tt",  "bin/docker-entrypoint"
  template_if_missing "Procfile.tt",           "Procfile"
  template_if_missing "deploy.yml.tt",         "config/deploy.yml"

  full = File.join(destination_root, "bin/docker-entrypoint")
  File.chmod(0o755, full) if File.exist?(full)
end

#create_engines_directory ⇒ Object



42
43
44
45
# File 'lib/generators/seams/install/install_generator.rb', line 42

def create_engines_directory
  empty_directory "engines"
  create_file "engines/.keep"
end

#create_helper_scripts ⇒ Object

Phase 1.5 — per-host helper scripts and architecture doc.



185
186
187
188
189
190
191
# File 'lib/generators/seams/install/install_generator.rb', line 185

def create_helper_scripts
  template_if_missing "script/collate_coverage.rb.tt",   "script/collate_coverage.rb"
  template_if_missing "script/run_affected_tests.sh.tt", "script/run_affected_tests.sh"

  runner = File.join(destination_root, "script/run_affected_tests.sh")
  File.chmod(0o755, runner) if File.exist?(runner)
end

#create_herb_config ⇒ Object

--- Quality toolchain (default on; opt out per tool) -----------------



151
152
153
154
155
# File 'lib/generators/seams/install/install_generator.rb', line 151

def create_herb_config
  return unless options[:herb]

  template_if_missing "herb.yml.tt", ".herb.yml"
end

#create_host_rubocop ⇒ Object



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/generators/seams/install/install_generator.rb', line 87

def create_host_rubocop
  # Three cases:
  #   1. Host has no .rubocop.yml → write the seams baseline.
  #   2. Host already has one → don't overwrite, but inject an
  #      `engines/**/*` Exclude so host RuboCop (which may use
  #      rubocop-rails-omakase or another flavor) doesn't lint
  #      engine code under rules written for application code.
  #      Engines have their own self-contained .rubocop.yml.
  host_path = File.join(destination_root, ".rubocop.yml")
  unless File.exist?(host_path)
    template "rubocop.yml.tt", ".rubocop.yml"
    return
  end

  return if File.read(host_path).include?("engines/**/*")

  say "  inject  .rubocop.yml (Exclude engines + seams.rake)", :green
  append_to_file(host_path, <<~YML)

    # Engines have their own self-contained .rubocop.yml. Linting them
    # from the host runs gem-style code under whatever flavor of rules
    # the host uses (omakase / etc.) and produces noisy false positives.
    # The gem-generated lib/tasks/seams.rake is excluded for the same
    # reason.
    AllCops:
      Exclude:
        - "engines/**/*"
        - "lib/tasks/seams.rake"
  YML
end

#create_initializer ⇒ Object



38
39
40
# File 'lib/generators/seams/install/install_generator.rb', line 38

def create_initializer
  template "seams.rb.tt", "config/initializers/seams.rb"
end

#create_lefthook_config ⇒ Object



163
164
165
166
167
# File 'lib/generators/seams/install/install_generator.rb', line 163

def create_lefthook_config
  return unless options[:lefthook]

  template_if_missing "lefthook.yml.tt", "lefthook.yml"
end

#create_rake_tasks ⇒ Object



47
48
49
# File 'lib/generators/seams/install/install_generator.rb', line 47

def create_rake_tasks
  template "seams.rake.tt", "lib/tasks/seams.rake"
end

#create_ruby_version ⇒ Object



118
119
120
121
122
123
124
125
# File 'lib/generators/seams/install/install_generator.rb', line 118

def create_ruby_version
  # The host CI workflow does `ruby-version: ".ruby-version"`, so the
  # host needs a `.ruby-version` file. Rails 8's `rails new` doesn't
  # ship one. Don't overwrite if the host has pinned their own.
  return if File.exist?(File.join(destination_root, ".ruby-version"))

  template "ruby-version.tt", ".ruby-version"
end

#create_strong_migrations_initializer ⇒ Object



157
158
159
160
161
# File 'lib/generators/seams/install/install_generator.rb', line 157

def create_strong_migrations_initializer
  return unless options[:strong_migrations]

  template_if_missing "strong_migrations.rb.tt", "config/initializers/strong_migrations.rb"
end

#install_quality_gems ⇒ Object



169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/generators/seams/install/install_generator.rb', line 169

def install_quality_gems
  # The CI workflow (and lefthook) shell out to these, so make sure the
  # gems are in the host Gemfile. All idempotent — skipped if present.
  host_inject_gem("rubocop", group: :development)
  host_inject_gem("brakeman", group: :development)
  host_inject_gem("bundler-audit", group: :development)
  host_inject_gem("herb", group: :development) if options[:herb]
  host_inject_gem("lefthook", group: :development) if options[:lefthook]
  # strong_migrations is a dev/CI guard — it vets migrations where they're
  # written and in CI, and is kept OUT of the production path. The
  # initializer is guarded with `if defined?(StrongMigrations)` so a
  # production boot (where the gem isn't loaded) is unaffected.
  host_inject_gem("strong_migrations", group: %i[development test]) if options[:strong_migrations]
end

#post_install_message ⇒ Object

rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength



223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/generators/seams/install/install_generator.rb', line 223

def post_install_message
  say ""
  say "  Seams is installed. Generate your first engine with:", :green
  say "    bin/seams core          (or bin/rails generate seams:core)"
  say ""
  say "  Quality toolchain (installed by default — opt out with --no-<tool>):", :yellow
  say "    rubocop + brakeman + bundle-audit (in CI)" \
      "#{", herb (HTML+ERB lint)" if options[:herb]}" \
      "#{", strong_migrations" if options[:strong_migrations]}"
  say "    HTML+ERB linting (herb) is opt-in — add --herb (arm64-darwin only).", :yellow unless options[:herb]
  say "    bundle install   then   bin/rails db:prepare"
  if options[:lefthook]
    say "    Activate the git hooks (pre-commit lint, pre-push specs):", :yellow
    say "    bundle exec lefthook install"
  end
  say ""
  say "  Canonical generators (run in this order):", :yellow
  say "    bin/seams core          - Core engine (concerns, audit log)"
  say "    bin/seams auth          - Auth engine (Identity, sessions, OAuth)"
  say "    bin/seams accounts      - Accounts engine (tenant + Membership + system actor)"
  say "    bin/seams notifications - Notifications engine"
  say "    bin/seams billing       - Billing engine"
  say "    bin/seams teams         - Teams engine"
  say ""
  say "  Optional engines (generate after the canonical six are in place):", :yellow
  say "    bin/seams admin         - Admin engine (Administrate-backed dashboards"
  say "                              for the canonical models, Pundit-gated,"
  say "                              audit-log auto-write). Requires auth + accounts."
  say "    bin/seams design        - Design engine (ui_* component partials,"
  say "                              Tailwind v4 tokens, Design::FormBuilder)."
  say "                              Non-isolated: ui_* helpers resolve host-wide."
  say ""
  say "  Follow-up generators (extend an already-installed engine):", :yellow
  say "    bin/rails generate seams:auth:add_oauth_provider <name>"
  say "                            - add a new OAuth provider adapter"
  say "                              (e.g. linkedin, apple, microsoft)"
  say ""
  say "  Other useful commands:", :yellow
  say "    bin/seams list                          - list engines + their events"
  say "    bin/seams resolve --eject <engine>/<file>"
  say "                                            - mark a host file as host-owned"
  say "                                              (skipped on regenerate)"
  say "    bin/seams resolve --list-markers <engine>"
  say "                                            - list insertion-point markers"
  say "    bin/seams resolve --list-ejected        - list every ejected file under engines/"
  say ""
  say "  Recommended order: core -> auth -> accounts -> notifications -> billing -> teams.", :yellow
  say "  Optional: append `admin` last for an Administrate-backed admin surface.", :yellow
  say "  See doc/reference/CURRENT_ATTRIBUTES.md (after install) for the per-request namespace cascade.", :yellow
  say "  See doc/how-to/WRITING_FOLLOW_UP_GENERATORS.md to write your own follow-up generator.", :yellow
  say ""
end

#wire_engines_into_application_rb ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/generators/seams/install/install_generator.rb', line 55

def wire_engines_into_application_rb
  # Each engine under engines/ must be required BEFORE
  # Rails.application.initialize! so its Railtie registers paths
  # (db/migrate, app/*) and initializers with the host. The
  # `require_relative` is injected directly after Bundler.require.
  application_rb = File.join(destination_root, "config/application.rb")
  return unless File.exist?(application_rb)

  snippet = %(require_relative "seams_engines")
  contents = File.read(application_rb)
  return if contents.include?(snippet)

  # The default Rails 8 application.rb contains
  # `Bundler.require(*Rails.groups)` verbatim. If a host has
  # customised it (Rails 4-style asset groups, multi-arg
  # Bundler.require, brace-form do-block, trailing comment, ...),
  # the regex misses and Thor silently warns "File unchanged!"
  # — leaving the host bootable but with engines never required.
  # That is the worst kind of failure: silent + production-bug.
  # Print a loud red warning so the user knows to wire it by hand.
  anchor = /Bundler\.require\(\*Rails\.groups\)\n/
  unless contents.match?(anchor)
    say "  WARNING config/application.rb has no `Bundler.require(*Rails.groups)` line — " \
        "add `#{snippet}` manually after Bundler.require so engines load before initialize!",
        :red
    return
  end

  say "  inject  config/application.rb (require_relative \"seams_engines\")", :green
  inject_into_file(application_rb, "\n#{snippet}\n", after: anchor)
end

#wire_into_host ⇒ Object



197
198
199
200
201
202
203
204
205
206
207
# File 'lib/generators/seams/install/install_generator.rb', line 197

def wire_into_host
  # Auto-add seams to the host Gemfile if not already present —
  # covers the `gem install seams` global-install path. Pinned to
  # a pessimistic 0.x to keep major-version bumps explicit.
  host_inject_gem("seams", "~> #{Seams::VERSION}")
  # Every Seams host needs rspec-rails so the per-engine
  # spec/dummy specs can actually run. Idempotent — skipped if
  # the host already has these gems. The range lets bundler pick
  # rspec-rails 8.x on Rails 7.2+ and fall back to 7.1.x on Rails 7.1.
  host_inject_gem("rspec-rails", ">= 7.1", "< 9", group: :test)
end