Module: Seams::Generators::EjectAware

Included in:
AccountsGenerator, AdminGenerator, AuthGenerator, BillingGenerator, CoreGenerator, NotificationsGenerator, TeamsGenerator
Defined in:
lib/seams/generators/eject_aware.rb

Overview

Mixin that lets a canonical engine generator (auth, accounts, billing, core, notifications, teams) cooperate with the bin/seams resolve --eject CLI introduced in Wave 10 Phase 2B.

The contract is one-way: ejection happens in the host's working tree (the CLI prepends a # seams:ejected from <engine>.<path> header to a file the host wants to own). This module gives the generator the matching skip behaviour — when the destination file already starts with that header, the generator's template_unless_ejected helper logs a yellow "skip" line and leaves the file untouched.

Eject-eligibility rule

The CLI and this helper agree on a single eligibility rule:

  • Eligible: every templated file under app/ and lib/ and config/ (controllers, models, services, jobs, mailers, views, concerns, lib classes, configuration.rb, routes.rb). These are pieces a host might reasonably want to own outright — to override behaviour, change copy, swap a layout, etc.
  • Eligible: spec files (spec/) and factories. The host owns the regenerated specs the same way the host owns the engine code; ejecting a spec is rare but legal.
  • INELIGIBLE: migrations under db/migrate/. Migrations run exactly once on a host's timetable; ejecting a migration is meaningless (it's already been run) and re-running the generator never overwrites an existing migration anyway — each run produces a new timestamp.
  • INELIGIBLE: framework-managed engine boot files — lib/<engine>/engine.rb, lib/<engine>/version.rb, the engine's Gemfile and gemspec. These are the contract between the engine and the host's Rails boot; if the host wants different behaviour they extend via insertion points or a follow-up generator.

The CLI enforces this rule at eject time. The template_unless_ejected helper is intentionally permissive — if a host has somehow stamped the eject header onto a framework-managed file, we skip rather than overwrite. The error path is loud at eject, not at generation.

Usage

In an engine generator:

class AuthGenerator < Rails::Generators::Base
include Seams::Generators::EjectAware

def create_models
  template_unless_ejected "app/models/identity.rb.tt",
                          engine_path("app/models/auth/identity.rb")
end
end

template_unless_ejected accepts the same positional + keyword arguments as Thor's template, including force: true, so generators can adopt it incrementally without changing call sites.

Constant Summary collapse

EJECT_HEADER_PREFIX =
"# seams:ejected from"

Instance Method Summary collapse

Instance Method Details

#ejected?(path) ⇒ Boolean

True when the file at path carries the eject header on its first line. Reads only the first 200 bytes — the header sits at the very top and we don't want to slurp megabytes for files under app/views/.

Returns:

  • (Boolean)


84
85
86
87
88
# File 'lib/seams/generators/eject_aware.rb', line 84

def ejected?(path)
  return false unless File.exist?(path)

  File.read(path, 200).to_s.start_with?(EJECT_HEADER_PREFIX)
end

#template_unless_ejected(source, *args) ⇒ Object

Drop-in replacement for Thor's template. If the destination exists and starts with the eject header, log a skip line and return without writing. Otherwise delegate to template — Thor handles force: true and conflict resolution as usual.



70
71
72
73
74
75
76
77
78
# File 'lib/seams/generators/eject_aware.rb', line 70

def template_unless_ejected(source, *args, **)
  destination = args.first
  if destination && File.exist?(destination) && ejected?(destination)
    say_status :skip, ejected_say_message(destination), :yellow
    return
  end

  template(source, *args, **)
end