Module: ReactOnRails::Utils

Defined in:
lib/react_on_rails/utils.rb

Defined Under Namespace

Modules: Required

Constant Summary collapse

TRUNCATION_FILLER =
"\n... TRUNCATED #{
  Rainbow('To see the full output, set FULL_TEXT_ERRORS=true.').red
} ...\n".freeze

Class Method Summary collapse

Class Method Details

.bundle_js_file_path(bundle_name) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/react_on_rails/utils.rb', line 78

def self.bundle_js_file_path(bundle_name)
  # Priority order depends on bundle type:
  # SERVER BUNDLES (normal case): Try private non-public locations first, then manifest, then legacy
  # CLIENT BUNDLES (normal case): Try manifest first, then fallback locations
  if bundle_name == "manifest.json"
    # Default to the non-hashed name in the specified output directory, which, for legacy
    # React on Rails, this is the output directory picked up by the asset pipeline.
    # For Shakapacker, this is the public output path defined in the (shaka/web)packer.yml file.
    File.join(public_bundles_full_path, bundle_name)
  else
    bundle_js_file_path_with_packer(bundle_name)
  end
end

.command_available?(command) ⇒ Boolean

Returns:

  • (Boolean)


164
165
166
167
# File 'lib/react_on_rails/utils.rb', line 164

def self.command_available?(command)
  which_command = running_on_windows? ? "where" : "which"
  !!system(which_command, command, out: File::NULL, err: File::NULL)
end

.default_troubleshooting_sectionObject



461
462
463
464
465
466
467
468
469
# File 'lib/react_on_rails/utils.rb', line 461

def self.default_troubleshooting_section
  <<~DEFAULT
    📞 Get Help & Support:
       • 🚀 Professional Support: react_on_rails@shakacode.com (fastest resolution)
       • 💬 React + Rails Slack: https://invite.reactrails.com
       • 🆓 GitHub Issues: https://github.com/shakacode/react_on_rails/issues
       • 📖 Discussions: https://github.com/shakacode/react_on_rails/discussions
  DEFAULT
end

.detect_package_managerSymbol

Detects which package manager is being used. First checks the packageManager field in package.json (Node.js Corepack standard), then falls back to checking for lock files.

Returns:

  • (Symbol)

    The package manager symbol (:npm, :yarn, :pnpm, :bun)



295
296
297
298
# File 'lib/react_on_rails/utils.rb', line 295

def self.detect_package_manager
  manager = detect_package_manager_from_package_json || detect_package_manager_from_lock_files
  manager || :yarn # Default to yarn if no detection succeeds
end

.find_most_recent_mtime(files) ⇒ Object



268
269
270
271
272
273
# File 'lib/react_on_rails/utils.rb', line 268

def self.find_most_recent_mtime(files)
  files.reduce(1.year.ago) do |newest_time, file|
    mt = File.mtime(file)
    [mt, newest_time].max
  end
end

.full_text_errors_enabled?Boolean

Returns:

  • (Boolean)


248
249
250
# File 'lib/react_on_rails/utils.rb', line 248

def self.full_text_errors_enabled?
  ENV["FULL_TEXT_ERRORS"] == "true"
end

.gem_available?(name) ⇒ Boolean

Returns:

  • (Boolean)


207
208
209
210
211
212
213
214
215
216
217
# File 'lib/react_on_rails/utils.rb', line 207

def self.gem_available?(name)
  Gem.loaded_specs[name].present?
rescue Gem::LoadError
  false
rescue StandardError
  begin
    Gem.available?(name).present?
  rescue NoMethodError
    false
  end
end

.generated_assets_full_pathObject

DEPRECATED: Use public_bundles_full_path for clarity about public vs private bundle paths



203
204
205
# File 'lib/react_on_rails/utils.rb', line 203

def self.generated_assets_full_path
  public_bundles_full_path
end

.invoke_and_exit_if_failed(cmd, failure_message) ⇒ Object

Invokes command, exiting with a detailed message if there’s a failure.



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/react_on_rails/utils.rb', line 49

def self.invoke_and_exit_if_failed(cmd, failure_message)
  stdout, stderr, status = Open3.capture3(cmd)
  unless status.success?
    stdout_msg = stdout.present? ? "\nstdout:\n#{stdout.strip}\n" : ""
    stderr_msg = stderr.present? ? "\nstderr:\n#{stderr.strip}\n" : ""
    msg = <<~MSG
      React on Rails FATAL ERROR!
      #{failure_message}
      cmd: #{cmd}
      exitstatus: #{status.exitstatus}#{stdout_msg}#{stderr_msg}
    MSG

    # Use warn to ensure output is visible in CI logs (goes to stderr)
    # and flush immediately before calling exit!
    warn wrap_message(msg)
    warn ""
    warn default_troubleshooting_section
    $stderr.flush

    # Rspec catches exit without! in the exit callbacks
    exit!(1)
  end
  [stdout, stderr, status]
end

.normalize_to_relative_path(path) ⇒ String?

Converts an absolute path (String or Pathname) to a path relative to Rails.root. If the path is already relative or doesn’t contain Rails.root, returns it as-is.

This method is used to normalize paths from Shakapacker’s privateOutputPath (which is absolute) to relative paths suitable for React on Rails configuration.

Note: Absolute paths that don’t start with Rails.root are intentionally passed through unchanged. While there’s no known use case for server bundles outside Rails.root, this behavior preserves the original path for debugging and error messages.

Examples:

Converting absolute paths within Rails.root

# Assuming Rails.root is "/app"
normalize_to_relative_path("/app/ssr-generated") # => "ssr-generated"
normalize_to_relative_path("/app/foo/bar")       # => "foo/bar"

Already relative paths pass through

normalize_to_relative_path("ssr-generated")      # => "ssr-generated"
normalize_to_relative_path("./ssr-generated")    # => "./ssr-generated"

Absolute paths outside Rails.root (edge case)

normalize_to_relative_path("/other/path/bundles") # => "/other/path/bundles"

Parameters:

  • path (String, Pathname)

    The path to normalize

Returns:

  • (String, nil)

    The relative path as a string, or nil if path is nil



433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# File 'lib/react_on_rails/utils.rb', line 433

def self.normalize_to_relative_path(path)
  return nil if path.nil?

  path_str = path.to_s
  rails_root_str = Rails.root.to_s.chomp("/")

  # Treat as "inside Rails.root" only for exact match or a subdirectory
  inside_rails_root = rails_root_str.present? &&
                      (path_str == rails_root_str || path_str.start_with?("#{rails_root_str}/"))

  # If path is within Rails.root, remove that prefix
  if inside_rails_root
    # Remove Rails.root and any leading slash
    path_str.sub(%r{^#{Regexp.escape(rails_root_str)}/?}, "")
  else
    # Path is already relative or outside Rails.root
    # Warn if it's an absolute path outside Rails.root (edge case)
    if path_str.start_with?("/") && !inside_rails_root
      Rails.logger&.warn(
        "ReactOnRails: Detected absolute path outside Rails.root: '#{path_str}'. " \
        "Server bundles are typically stored within Rails.root. " \
        "Verify this is intentional."
      )
    end
    path_str
  end
end

.object_to_boolean(value) ⇒ Object



40
41
42
# File 'lib/react_on_rails/utils.rb', line 40

def self.object_to_boolean(value)
  [true, "true", "yes", 1, "1", "t"].include?(value.instance_of?(String) ? value.downcase : value)
end

.package_manager_install_exact_command(package_name, version) ⇒ String

Returns the appropriate install command for the detected package manager. Generates the correct command with exact version syntax.

Parameters:

  • package_name (String)

    The name of the package to install

  • version (String)

    The exact version to install

Returns:

  • (String)

    The command to run (e.g., “yarn add react-on-rails@16.0.0 –exact”)



367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# File 'lib/react_on_rails/utils.rb', line 367

def self.package_manager_install_exact_command(package_name, version)
  validate_package_command_inputs!(package_name, version)

  manager = detect_package_manager
  # Escape shell arguments to prevent command injection
  safe_package = Shellwords.escape("#{package_name}@#{version}")

  case manager
  when :pnpm
    "pnpm add #{safe_package} --save-exact"
  when :bun
    "bun add #{safe_package} --exact"
  when :npm
    "npm install #{safe_package} --save-exact"
  else # :yarn or unknown, default to yarn
    "yarn add #{safe_package} --exact"
  end
end

.package_manager_remove_command(package_name) ⇒ String

Returns the appropriate remove command for the detected package manager.

Parameters:

  • package_name (String)

    The name of the package to remove

Returns:

  • (String)

    The command to run (e.g., “yarn remove react-on-rails”)



390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'lib/react_on_rails/utils.rb', line 390

def self.package_manager_remove_command(package_name)
  validate_package_name!(package_name)

  manager = detect_package_manager
  # Escape shell arguments to prevent command injection
  safe_package = Shellwords.escape(package_name)

  case manager
  when :pnpm
    "pnpm remove #{safe_package}"
  when :bun
    "bun remove #{safe_package}"
  when :npm
    "npm uninstall #{safe_package}"
  else # :yarn or unknown, default to yarn
    "yarn remove #{safe_package}"
  end
end

.prepend_cd_node_modules_directory(cmd) ⇒ Object



185
186
187
# File 'lib/react_on_rails/utils.rb', line 185

def self.prepend_cd_node_modules_directory(cmd)
  "cd \"#{ReactOnRails.configuration.node_modules_location}\" && #{cmd}"
end

.prepend_to_file_if_text_not_present(file:, text_to_prepend:, regex:) ⇒ Object



275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/react_on_rails/utils.rb', line 275

def self.prepend_to_file_if_text_not_present(file:, text_to_prepend:, regex:)
  if File.exist?(file)
    file_content = File.read(file)

    return if file_content.match(regex)

    content_with_prepended_text = text_to_prepend + file_content
    File.write(file, content_with_prepended_text, mode: "w")
  else
    File.write(file, text_to_prepend, mode: "w+")
  end

  puts "Prepended\n#{text_to_prepend}to #{file}."
end

.public_bundles_full_pathObject



198
199
200
# File 'lib/react_on_rails/utils.rb', line 198

def self.public_bundles_full_path
  ReactOnRails::PackerUtils.packer_public_output_path
end

.rails_version_less_than(version) ⇒ Object



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

def self.rails_version_less_than(version)
  @rails_version_less_than ||= {}

  return @rails_version_less_than[version] if @rails_version_less_than.key?(version)

  @rails_version_less_than[version] = begin
    Gem::Version.new(Rails.version) < Gem::Version.new(version)
  end
end

.react_on_rails_pro?Boolean

Checks if the React on Rails Pro gem is installed. Note: This checks gem presence only, not license validity.

Returns:

  • (Boolean)

    true if Pro gem is available



223
224
225
226
227
# File 'lib/react_on_rails/utils.rb', line 223

def self.react_on_rails_pro?
  return @react_on_rails_pro if defined?(@react_on_rails_pro)

  @react_on_rails_pro = gem_available?("react_on_rails_pro")
end

.react_on_rails_pro_versionObject

Return an empty string if React on Rails Pro is not installed



230
231
232
233
234
235
236
237
238
# File 'lib/react_on_rails/utils.rb', line 230

def self.react_on_rails_pro_version
  return @react_on_rails_pro_version if defined?(@react_on_rails_pro_version)

  @react_on_rails_pro_version = if react_on_rails_pro?
                                  Gem.loaded_specs["react_on_rails_pro"].version.to_s
                                else
                                  ""
                                end
end

.rsc_support_enabled?Boolean

RSC support detection has been moved to React on Rails Pro See react_on_rails_pro/lib/react_on_rails_pro/utils.rb

Returns:

  • (Boolean)


242
243
244
245
246
# File 'lib/react_on_rails/utils.rb', line 242

def self.rsc_support_enabled?
  return false unless react_on_rails_pro?

  ReactOnRailsPro::Utils.rsc_support_enabled?
end

.running_on_windows?Boolean

Returns:

  • (Boolean)


160
161
162
# File 'lib/react_on_rails/utils.rb', line 160

def self.running_on_windows?
  (/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) != nil
end

.server_bundle_js_file_pathObject



153
154
155
156
157
158
# File 'lib/react_on_rails/utils.rb', line 153

def self.server_bundle_js_file_path
  return @server_bundle_path if @server_bundle_path && !Rails.env.development?

  bundle_name = ReactOnRails.configuration.server_bundle_js_file
  @server_bundle_path = bundle_js_file_path(bundle_name)
end

.server_bundle_path_is_http?Boolean

Returns:

  • (Boolean)


74
75
76
# File 'lib/react_on_rails/utils.rb', line 74

def self.server_bundle_path_is_http?
  server_bundle_js_file_path =~ %r{https?://}
end

.server_rendering_is_enabled?Boolean

Returns:

  • (Boolean)


44
45
46
# File 'lib/react_on_rails/utils.rb', line 44

def self.server_rendering_is_enabled?
  ReactOnRails.configuration.server_bundle_js_file.present?
end

.smart_trim(str, max_length = 1000) ⇒ Object



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/react_on_rails/utils.rb', line 252

def self.smart_trim(str, max_length = 1000)
  # From https://stackoverflow.com/a/831583/1009332
  str = str.to_s
  return str if full_text_errors_enabled?
  return str unless str.present? && max_length >= 1
  return str if str.length <= max_length

  return str[0, 1] + TRUNCATION_FILLER if max_length == 1

  midpoint = (str.length / 2.0).ceil
  to_remove = str.length - max_length
  lstrip = (to_remove / 2.0).ceil
  rstrip = to_remove - lstrip
  str[0..(midpoint - lstrip - 1)] + TRUNCATION_FILLER + str[(midpoint + rstrip)..]
end

.source_pathObject



189
190
191
# File 'lib/react_on_rails/utils.rb', line 189

def self.source_path
  ReactOnRails::PackerUtils.packer_source_path
end

.truthy_presence(obj) ⇒ Object



19
20
21
22
23
24
25
# File 'lib/react_on_rails/utils.rb', line 19

def self.truthy_presence(obj)
  if obj.nil? || obj == false
    nil
  else
    obj
  end
end

.using_packer_source_path_is_not_defined_and_custom_node_modules?Boolean

Returns:

  • (Boolean)


193
194
195
196
# File 'lib/react_on_rails/utils.rb', line 193

def self.using_packer_source_path_is_not_defined_and_custom_node_modules?
  !ReactOnRails::PackerUtils.packer_source_path_explicit? &&
    ReactOnRails.configuration.node_modules_location.present?
end

.wrap_message(msg, color = :red) ⇒ Object

Wraps message and makes it colored. Pass in the msg and color as a symbol.



29
30
31
32
33
34
35
36
37
38
# File 'lib/react_on_rails/utils.rb', line 29

def self.wrap_message(msg, color = :red)
  wrapper_line = ("=" * 80).to_s
  fenced_msg = <<~MSG
    #{wrapper_line}
    #{msg.strip}
    #{wrapper_line}
  MSG

  Rainbow(fenced_msg).color(color)
end