Class: ReactOnRails::Engine

Inherits:
Rails::Engine
  • Object
show all
Defined in:
lib/react_on_rails/engine.rb

Constant Summary collapse

SHAKAPACKER_PACKAGE_MANAGER_CHECK =
:error_unless_package_manager_is_obvious!
SHAKAPACKER_MANAGER_GUARD_ISSUE_URL =
"https://github.com/shakacode/react_on_rails/issues/3145"

Class Method Summary collapse

Class Method Details

.fetch_shakapacker_package_manager_guard_method(manager) ⇒ Object



186
187
188
189
190
191
192
193
# File 'lib/react_on_rails/engine.rb', line 186

def self.fetch_shakapacker_package_manager_guard_method(manager)
  return manager.method(SHAKAPACKER_PACKAGE_MANAGER_CHECK) if manager.respond_to?(SHAKAPACKER_PACKAGE_MANAGER_CHECK)

  log_shakapacker_guard_warning(
    "Shakapacker::Utils::Manager does not define #{SHAKAPACKER_PACKAGE_MANAGER_CHECK}."
  )
  nil
end

.install_shakapacker_package_manager_check_wrapperObject



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/react_on_rails/engine.rb', line 135

def self.install_shakapacker_package_manager_check_wrapper
  # Idempotency flag: skip re-patching the singleton on repeated calls (test reloads,
  # multi-app processes). Lives on Engine's singleton, so a constant reload resets it,
  # which is fine — the spec around block resets it explicitly during isolated tests.
  return if @shakapacker_guard_suppressed

  manager = shakapacker_utils_manager
  return unless manager

  original_package_manager_check = fetch_shakapacker_package_manager_guard_method(manager)
  return unless original_package_manager_check

  Rails.logger&.info(
    "[React on Rails] No Shakapacker config found; skipping Shakapacker " \
    "packageManager check (Shakapacker is loaded as a transitive dependency but " \
    "is not the configured bundler)."
  )

  # Define the override on the singleton (not the class) so that any subsequent reopening
  # of Shakapacker::Utils::Manager that redefines the class method is still shadowed by
  # this singleton method, which Ruby resolves first.
  manager.define_singleton_method(SHAKAPACKER_PACKAGE_MANAGER_CHECK) do
    # Delegate back to the original guard once Shakapacker becomes the configured bundler.
    original_package_manager_check.call if ReactOnRails::Engine.shakapacker_configured_as_bundler?
  end
  @shakapacker_guard_suppressed = true
end

.log_shakapacker_guard_warning(message) ⇒ Object



195
196
197
198
199
200
# File 'lib/react_on_rails/engine.rb', line 195

def self.log_shakapacker_guard_warning(message)
  Rails.logger&.warn(
    "[React on Rails] #{message} The packageManager guard suppression could not be applied. " \
    "If boot fails, please report this at #{SHAKAPACKER_MANAGER_GUARD_ISSUE_URL}"
  )
end

.package_json_missing?Boolean

Check if package.json doesn’t exist yet

Returns:

  • (Boolean)

    true if package.json is missing



101
102
103
# File 'lib/react_on_rails/engine.rb', line 101

def self.package_json_missing?
  !File.exist?(VersionChecker::NodePackageVersion.package_json_path)
end

.running_generator?Boolean

Check if we’re running a Rails generator Heuristic: Rails::Generators is typically only defined during generator commands. It could be defined by test helpers or gems that require “rails/generators”, but this is a fallback behind the ENV check above.

Returns:

  • (Boolean)

    true if running a generator



95
96
97
# File 'lib/react_on_rails/engine.rb', line 95

def self.running_generator?
  defined?(Rails::Generators)
end

.shakapacker_config_pathObject

Resolves the Shakapacker config path, mirroring how Shakapacker itself locates the file. Relative SHAKAPACKER_CONFIG values are expanded against Rails.root so bundler detection stays consistent with Shakapacker when the process starts from a different working directory.



116
117
118
119
120
121
# File 'lib/react_on_rails/engine.rb', line 116

def self.shakapacker_config_path
  env_config_path = ENV.fetch("SHAKAPACKER_CONFIG", nil)
  return Pathname.new(env_config_path).expand_path(Rails.root) unless env_config_path.to_s.empty?

  Rails.root.join("config", "shakapacker.yml")
end

.shakapacker_configured_as_bundler?Boolean

Returns true when the host app has a Shakapacker config file, meaning Shakapacker is the configured bundler. Returns false when Shakapacker is only present as a transitive gem dependency (e.g., a Vite Rails app adopting React on Rails for client-only mounts).

Returns:

  • (Boolean)


109
110
111
# File 'lib/react_on_rails/engine.rb', line 109

def self.shakapacker_configured_as_bundler?
  shakapacker_config_path.exist?
end

.shakapacker_utils_managerObject



179
180
181
182
183
184
# File 'lib/react_on_rails/engine.rb', line 179

def self.shakapacker_utils_manager
  return ::Shakapacker::Utils::Manager if defined?(::Shakapacker::Utils::Manager)

  log_shakapacker_guard_warning("Shakapacker is loaded but ::Shakapacker::Utils::Manager is not defined.")
  nil
end

.skip_version_validation?Boolean

Note:

Thread Safety: ENV variables are process-global. In practice, Rails generators run in a single process, so concurrent execution is not a concern. If running generators concurrently (e.g., in parallel tests), ensure tests run in separate processes to avoid ENV variable conflicts.

Note:

Manual ENV Setting: While this ENV variable is designed to be set by generators, users can manually set it (e.g., ‘REACT_ON_RAILS_SKIP_VALIDATION=true rails server`) to bypass validation. This should only be done temporarily during debugging or setup scenarios. The validation helps catch version mismatches early, so bypassing it in production is not recommended.

Determine if version validation should be skipped

This method checks multiple conditions to determine if package version validation should be skipped. Validation is skipped during setup scenarios where the npm package isn’t installed yet (e.g., during generator execution).

Examples:

Testing with parallel processes

# In RSpec configuration:
config.before(:each) do |example|
  ENV.delete("REACT_ON_RAILS_SKIP_VALIDATION")
end

Returns:

  • (Boolean)

    true if validation should be skipped



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/react_on_rails/engine.rb', line 63

def self.skip_version_validation?
  # Skip if explicitly disabled via environment variable (set by generators)
  # Using ENV variable instead of ARGV because Rails can modify/clear ARGV during
  # initialization, making ARGV unreliable for detecting generator context. The ENV
  # variable persists through the entire Rails initialization process.
  if ENV["REACT_ON_RAILS_SKIP_VALIDATION"] == "true"
    Rails.logger.debug "[React on Rails] Skipping validation - disabled via environment variable"
    return true
  end

  # Check package.json first as it's cheaper and handles more cases
  if package_json_missing?
    Rails.logger.debug "[React on Rails] Skipping validation - package.json not found"
    return true
  end

  # Skip during generator runtime since packages are installed during execution
  # This is a fallback check in case ENV wasn't set, though ENV is the primary mechanism
  if running_generator?
    Rails.logger.debug "[React on Rails] Skipping validation during generator runtime"
    return true
  end

  false
end

.suppress_shakapacker_package_manager_check_if_not_bundler!Object

Wrap Shakapacker’s package-manager guard when Shakapacker is not the bundler. The wrapper checks Shakapacker config presence at call time so the original guard still runs if another app in the same process configures Shakapacker later.



126
127
128
129
130
131
132
133
# File 'lib/react_on_rails/engine.rb', line 126

def self.suppress_shakapacker_package_manager_check_if_not_bundler!
  # Nothing to suppress when Shakapacker is genuinely absent from the load path.
  return unless defined?(::Shakapacker)
  return if shakapacker_configured_as_bundler?

  warn_if_shakapacker_env_config_missing
  install_shakapacker_package_manager_check_wrapper
end

.warn_if_shakapacker_env_config_missingObject

Warn at boot when SHAKAPACKER_CONFIG points to a missing file so typos or not-yet-created paths surface clearly. The warning describes the config state (missing file at the user-specified path) rather than the suppression outcome, because the downstream install step may still bail out — keeping the message honest in that edge case.



168
169
170
171
172
173
174
175
176
177
# File 'lib/react_on_rails/engine.rb', line 168

def self.warn_if_shakapacker_env_config_missing
  env_config_path = ENV.fetch("SHAKAPACKER_CONFIG", nil)
  return if env_config_path.to_s.empty?

  Rails.logger&.warn(
    "[React on Rails] SHAKAPACKER_CONFIG is set to '#{env_config_path}' but the file " \
    "does not exist. Bundler detection treated Shakapacker as not configured for this app; " \
    "fix the path or unset the variable if this is unintended."
  )
end